From 7a4bdef5dc1c8dd0a5887380a90f0acb81194370 Mon Sep 17 00:00:00 2001
From: ys_teng <58208381+YeeShin504@users.noreply.github.com>
Date: Sat, 21 Mar 2026 20:05:55 +0800
Subject: [PATCH 1/6] Fix annotation position and word wrap
---
src/layout.ts | 19 ++++++++-----------
src/renderer.ts | 45 ++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 48 insertions(+), 16 deletions(-)
diff --git a/src/layout.ts b/src/layout.ts
index 5a1fc55..dc2d989 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -51,25 +51,22 @@ export function resolveLayout(entities: Entities): Diagram {
// 1. check if need extra spacing for annotations
let timeTickMargin = settings.showTimeTicks ? 60 : 0;
- let width = settings.participantSpacingX * (participants.length - 1)
- + 2 * settings.paddingX + timeTickMargin;
+ let annMargin = numAnnotations > 0 ? settings.annotationSpacingX : 0;
+
+ let timeAxisX = settings.paddingX + timeTickMargin;
+ let startX = timeAxisX + (settings.showTimeTicks ? 20 : 0) + annMargin;
- if (numAnnotations > 0)
- width += 2 * settings.annotationSpacingX;
+ let width = startX + settings.participantSpacingX * (participants.length - 1) + annMargin + settings.paddingX;
// 2. figure out top spacing for participant label and padding
- let height = settings.paddingX + settings.participantLabelHeight + settings.messageSpacingY; // last one is a hack
+ let height = settings.paddingY + settings.participantLabelHeight + settings.messageSpacingY; // last one is a hack
// 3. resolve participant x position
const participantsX = new Map(); // map alias -> x coord
- let timeAxisX = settings.paddingX + (numAnnotations > 0 ? settings.annotationSpacingX : 0) + (timeTickMargin > 0 ? 20 : 0);
{
- let x = timeAxisX + timeTickMargin;
- if (timeTickMargin === 0) {
- x = settings.paddingX + (numAnnotations > 0 ? settings.annotationSpacingX : 0);
- }
+ let x = startX;
for (const participant of participants) {
participantsX.set(participant.alias, x);
x += settings.participantSpacingX;
@@ -120,7 +117,7 @@ export function resolveLayout(entities: Entities): Diagram {
draws.push({
type: action.type,
- x: participantsX.get(action.participant) - ((action.side == "left" ? 1 : -1) * (settings.annotationSpacingX / 2)),
+ x: participantsX.get(action.participant) - ((action.side == "left" ? 1 : -1) * settings.labelOffset),
y: height + y * settings.messageSpacingY,
align: action.side == "left" ? "right" : "left", // if on left, use right align
text: action.text,
diff --git a/src/renderer.ts b/src/renderer.ts
index 53aae31..8f509a0 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -57,20 +57,55 @@ function drawParticipant(settings: Settings, participant: ParticipantPos): strin
`;
}
+function wrapText(text: string, maxChars: number): string[] {
+ const words = text.split(" ");
+ const lines: string[] = [];
+ let currentLine = "";
+
+ for (let i = 0; i < words.length; i++) {
+ const word = words[i];
+ if (currentLine.length === 0) {
+ currentLine = word;
+ } else if (currentLine.length + 1 + word.length <= maxChars) {
+ currentLine += " " + word;
+ } else {
+ lines.push(currentLine);
+ currentLine = word;
+ }
+ }
+ if (currentLine) lines.push(currentLine);
+
+ return lines;
+}
+
function drawAnnotation(settings: Settings, annotation: AnnotationPos): string {
const { x, y, align, text } = annotation;
- // TODO: handle align and wrapping
+
+ const charWidth = settings.messageFontSize * 0.6;
+ const marginOffset = settings.labelOffset;
+ const rectWidth = settings.annotationSpacingX - marginOffset;
+ const maxChars = Math.max(1, Math.floor(rectWidth / charWidth));
+
+ const lines = wrapText(text, maxChars);
+ const lineHeight = settings.messageFontSize * 1.2;
+ const startY = y;
+
+ const anchor = align === "left" ? "start" : align === "right" ? "end" : "middle";
+
+ const tspans = lines.map((line, i) =>
+ `${line}`
+ ).join("\n");
+
return `
- ${text}
+ ${tspans}
`
}
From 24a723fdb797ca292c8c387c4c388fe2bd2988ae Mon Sep 17 00:00:00 2001
From: ys_teng <58208381+YeeShin504@users.noreply.github.com>
Date: Sat, 21 Mar 2026 20:06:29 +0800
Subject: [PATCH 2/6] Update README on stable vs dev editor
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b5215fd..b258db0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ Markup language for rendering network protocol diagrams.
## Interactive Demo
-Try the live editor [here](https://alieron.github.io/protocol-ml/)
+Try the [stable live editor](https://yeeshin504.github.io/protocol-ml/) or the [development live editor](https://yeeshin504.github.io/protocol-ml/dev/).
### Development
From 2e2301da6d9fa07effdd717892973f8de30cab00 Mon Sep 17 00:00:00 2001
From: ys_teng <58208381+YeeShin504@users.noreply.github.com>
Date: Sat, 21 Mar 2026 21:20:43 +0800
Subject: [PATCH 3/6] Remove case sensitivity when parsing settings
---
docs/editor.js | 4 +-
src/layout.ts | 29 +++++-----
src/parser.ts | 144 +++++++++++++++++++++++++++++++++++++-----------
src/renderer.ts | 40 +++++++-------
4 files changed, 151 insertions(+), 66 deletions(-)
diff --git a/docs/editor.js b/docs/editor.js
index 68e6fe7..0a1cac2 100644
--- a/docs/editor.js
+++ b/docs/editor.js
@@ -6,8 +6,8 @@ const copyCodeBtn = document.getElementById('copyCode');
const copyPNGBtn = document.getElementById('copyPNG');
const copySVGBtn = document.getElementById('copySVG');
-const INITIAL_CODE = `def messageSpacing 20px
-def participantSpacing 160px
+const INITIAL_CODE = `def timeTickInterval 40px
+def participantSpacing 240px
def showTimeTicks true
def showGrid true
diff --git a/src/layout.ts b/src/layout.ts
index dc2d989..5d5aea9 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -51,16 +51,16 @@ export function resolveLayout(entities: Entities): Diagram {
// 1. check if need extra spacing for annotations
let timeTickMargin = settings.showTimeTicks ? 60 : 0;
- let annMargin = numAnnotations > 0 ? settings.annotationSpacingX : 0;
+ let annMargin = numAnnotations > 0 ? settings.annotationWidth : 0;
let timeAxisX = settings.paddingX + timeTickMargin;
let startX = timeAxisX + (settings.showTimeTicks ? 20 : 0) + annMargin;
- let width = startX + settings.participantSpacingX * (participants.length - 1) + annMargin + settings.paddingX;
+ let width = startX + settings.participantSpacing * (participants.length - 1) + annMargin + settings.paddingX;
// 2. figure out top spacing for participant label and padding
- let height = settings.paddingY + settings.participantLabelHeight + settings.messageSpacingY; // last one is a hack
+ let height = settings.paddingY + settings.participantLabelHeight + settings.timeTickInterval; // last one is a hack
// 3. resolve participant x position
@@ -69,7 +69,7 @@ export function resolveLayout(entities: Entities): Diagram {
let x = startX;
for (const participant of participants) {
participantsX.set(participant.alias, x);
- x += settings.participantSpacingX;
+ x += settings.participantSpacing;
}
}
@@ -98,14 +98,16 @@ export function resolveLayout(entities: Entities): Diagram {
draws.push({
type: action.type,
x1: participantsX.get(action.from),
- y1: height + startY * settings.messageSpacingY,
+ y1: height + startY * settings.timeTickInterval,
x2: participantsX.get(action.to),
- y2: height + endY * settings.messageSpacingY,
+ y2: height + endY * settings.timeTickInterval,
arrowType: action.arrowType,
label: action.label,
});
- counter++;
+ const thicknessRatio = action.arrowType === "thick" ? settings.thickArrowThickness / settings.timeTickInterval : 0;
+ counterMax = Math.max(counterMax, startY + thicknessRatio, endY + thicknessRatio);
+ counter = endY + thicknessRatio;
break;
case "annotation":
@@ -118,26 +120,27 @@ export function resolveLayout(entities: Entities): Diagram {
draws.push({
type: action.type,
x: participantsX.get(action.participant) - ((action.side == "left" ? 1 : -1) * settings.labelOffset),
- y: height + y * settings.messageSpacingY,
+ y: height + y * settings.timeTickInterval,
align: action.side == "left" ? "right" : "left", // if on left, use right align
text: action.text,
});
+ counterMax = Math.max(counterMax, y);
break;
// default:
// // should not reach here
// console.error(`[protocol-ml] Errror: invalid action type ${action.type}`);
}
- counterMax = Math.max(counterMax, counter);
}
// 5. resolve height of participant lifetimes
// draws.reverse(); // TBC: might look better if one rendered above
- const oldHeight = height - settings.messageSpacingY; // another hack
- height += (counterMax + 1) * settings.messageSpacingY; // add some extra length to the lifetime
+ const oldHeight = height - settings.timeTickInterval; // another hack
+ const totalTicks = Math.ceil(counterMax);
+ height += (totalTicks + 1) * settings.timeTickInterval; // add some extra length to the lifetime
const lifelines: ParticipantPos[] = participants.map(p => {
return {
@@ -150,10 +153,10 @@ export function resolveLayout(entities: Entities): Diagram {
});
if (settings.showTimeTicks || settings.showGrid) {
- for (let i = 0; i <= counterMax; i++) {
+ for (let i = 0; i <= totalTicks; i++) {
draws.push({
type: "tick",
- y: oldHeight + settings.messageSpacingY + i * settings.messageSpacingY,
+ y: oldHeight + settings.timeTickInterval + i * settings.timeTickInterval,
label: `${i}`,
});
}
diff --git a/src/parser.ts b/src/parser.ts
index 7ea02f0..ade2d8a 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -35,35 +35,98 @@ export interface Entities {
numAnnotations: number;
}
-const DEFAULT_SETTINGS = {
- arrowSize: 10,
- crossSize: 12,
- thickArrowSize: 30,
+export interface Settings {
+ arrowHeadSize: number;
+ dropCrossSize: number;
+ thickArrowThickness: number;
+ labelOffset: number;
+ corruptStartRatio: number;
+ dropStartRatio: number;
+
+ squiggleSize: number;
+ squiggleCount: number;
+
+ timeTickInterval: number;
+ participantSpacing: number;
+ participantLabelHeight: number;
+ annotationWidth: number;
+
+ participantFontSize: number;
+ messageFontSize: number;
+
+ paddingX: number;
+ paddingY: number;
+
+ showGrid: boolean;
+ showTimeTicks: boolean;
+ timeUnit: string;
+}
+
+const DEFAULT_SETTINGS: Settings = {
+ arrowHeadSize: 10,
+ dropCrossSize: 12,
+ thickArrowThickness: 40,
labelOffset: 10,
- corruptStart: 0.85,
- dropStart: 0.85,
+ corruptStartRatio: 0.85,
+ dropStartRatio: 0.85,
squiggleSize: 20,
squiggleCount: 2,
- messageSpacingY: 40,
- participantSpacingX: 240,
+ timeTickInterval: 40,
+ participantSpacing: 240,
participantLabelHeight: 30,
- annotationSpacingX: 80,
+ annotationWidth: 80,
participantFontSize: 20,
messageFontSize: 15,
paddingX: 30,
paddingY: 20,
-
+
showGrid: false,
showTimeTicks: false,
+ timeUnit: "",
};
-export type Settings = {
- [key: string]: number | boolean | undefined;
-} & typeof DEFAULT_SETTINGS;
+function parseString(val: string): string {
+ const v = val.trim();
+ // Strip quotes if present
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
+ return v.slice(1, -1);
+ }
+ return v;
+}
+
+function parseNumber(val: string): number | null {
+ const v = parseString(val);
+ if (v === "") return null;
+
+ // Percentage: "85%" -> 0.85
+ if (v.endsWith("%")) {
+ const numPart = v.slice(0, -1);
+ const n = Number(numPart);
+ if (numPart !== "" && !isNaN(n)) return n / 100;
+ }
+
+ // Pixels: "20px" -> 20
+ if (v.endsWith("px")) {
+ const numPart = v.slice(0, -2);
+ const n = Number(numPart);
+ if (numPart !== "" && !isNaN(n)) return n;
+ }
+
+ // Strict number coercion
+ const n = Number(v);
+ return isNaN(n) ? null : n;
+}
+
+function parseBoolean(val: string): boolean | null {
+ const v = parseString(val).toLowerCase();
+ if (v === "true") return true;
+ if (v === "false") return false;
+ return null;
+}
export function parse(src: string): Entities {
const settings: Settings = { ...DEFAULT_SETTINGS };
@@ -81,26 +144,45 @@ export function parse(src: string): Entities {
if (!line || line.startsWith("//")) continue;
// overwrite default settings
- if (line.startsWith("def")) {
-
- const [, name, value] = line.split(/\s+/);
-
- if (/^true$/i.test(value)) {
- settings[name] = true;
- } else if (/^false$/i.test(value)) {
- settings[name] = false;
- } else {
- settings[name] = parseFloat(value);
+ if (line.startsWith("def ")) {
+ // Use regex to capture the full value which might contain spaces and quotes
+ const match = line.match(/^def\s+([\w.]+)\s+(.+)$/);
+ if (!match) continue;
+
+ const name = match[1];
+ const rawValue = match[2];
+
+ const normalizedName = name.toLowerCase();
+ // find exact match (case-insensitive)
+ const targetKey = Object.keys(DEFAULT_SETTINGS).find(k => k.toLowerCase() === normalizedName) as keyof Settings | undefined;
+
+ if (targetKey) {
+ const defaultValue = DEFAULT_SETTINGS[targetKey];
+
+ // Use specialized parsers based on the target type
+ if (typeof defaultValue === "number") {
+ const val = parseNumber(rawValue);
+ if (val !== null) (settings as any)[targetKey] = val;
+ } else if (typeof defaultValue === "boolean") {
+ const val = parseBoolean(rawValue);
+ if (val !== null) (settings as any)[targetKey] = val;
+ } else if (typeof defaultValue === "string") {
+ (settings as any)[targetKey] = parseString(rawValue);
+ }
}
continue;
}
// participants
- // /^participant\s+((?:["'][^"'@]+["'])|[^\s"'@]+)\s+([^\s"'@]+)/
- if (line.startsWith("participant")) {
+ // Example: participant Client c1
+ // Example: participant "Web Server" ws
+ if (line.startsWith("participant ")) {
+ const match = line.match(/^participant\s+(?:(?:"([^"]+)")|(\S+))\s+(\w+)/);
+ if (!match) continue;
- const [, name, alias] = line.split(/\s+/);
+ const name = match[1] || match[2];
+ const alias = match[3];
participants.push({
name,
@@ -113,7 +195,7 @@ export function parse(src: string): Entities {
// annotations
const annMatch = line.match(
- /^(\w+)(?:\s*@([\d.]+))?\s*([<>])\s*"(.+)"/
+ /^(\w+)(?:\s*@([\d.%px]+))?\s*([<>])\s*"(.+)"/
);
if (annMatch) {
@@ -122,7 +204,7 @@ export function parse(src: string): Entities {
actions.push({
type: "annotation",
participant: annMatch[1],
- height: annMatch[2] ? parseFloat(annMatch[2]) : undefined,
+ height: annMatch[2] ? (parseNumber(annMatch[2]) ?? undefined) : undefined,
side: annMatch[3] === "<" ? "left" : "right",
text: annMatch[4]
});
@@ -132,7 +214,7 @@ export function parse(src: string): Entities {
// arrows
const arrowMatch = line.match(
- /^(\w+)(?:\s*@([\d.]+))?\s*(->|=>|~>|-x)\s*(\w+)(?:\s*@([\d.]+))?(?:\s*:\s*"(.+)")?/
+ /^(\w+)(?:\s*@([\d.%px]+))?\s*(->|=>|~>|-x)\s*(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/
);
if (arrowMatch) {
@@ -147,10 +229,10 @@ export function parse(src: string): Entities {
actions.push({
type: "arrow",
from: arrowMatch[1],
- start: arrowMatch[2] ? parseFloat(arrowMatch[2]) : undefined,
+ start: arrowMatch[2] ? (parseNumber(arrowMatch[2]) ?? undefined) : undefined,
arrowType: typeMap[arrowMatch[3]],
to: arrowMatch[4],
- end: arrowMatch[5] ? parseFloat(arrowMatch[5]) : undefined,
+ end: arrowMatch[5] ? (parseNumber(arrowMatch[5]) ?? undefined) : undefined,
label: arrowMatch[6]
});
diff --git a/src/renderer.ts b/src/renderer.ts
index 8f509a0..dcfe139 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -18,7 +18,7 @@ function drawTimeAxis(settings: Settings, axis: TimeAxisPos, ticks: TickPos[]):
fill="white"
font-size="${settings.participantFontSize}"
>
- Time
+ Time ${settings.timeUnit}
`;
@@ -83,7 +83,7 @@ function drawAnnotation(settings: Settings, annotation: AnnotationPos): string {
const charWidth = settings.messageFontSize * 0.6;
const marginOffset = settings.labelOffset;
- const rectWidth = settings.annotationSpacingX - marginOffset;
+ const rectWidth = settings.annotationWidth - marginOffset;
const maxChars = Math.max(1, Math.floor(rectWidth / charWidth));
const lines = wrapText(text, maxChars);
@@ -129,14 +129,14 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string {
let arrowsvg = '';
// arrow head
- const baseX = x2 - ux * settings.arrowSize;
- const baseY = y2 - uy * settings.arrowSize;
+ const baseX = x2 - ux * settings.arrowHeadSize;
+ const baseY = y2 - uy * settings.arrowHeadSize;
- const leftX = baseX + px * settings.arrowSize * 0.5;
- const leftY = baseY + py * settings.arrowSize * 0.5;
+ const leftX = baseX + px * settings.arrowHeadSize * 0.5;
+ const leftY = baseY + py * settings.arrowHeadSize * 0.5;
- const rightX = baseX - px * settings.arrowSize * 0.5;
- const rightY = baseY - py * settings.arrowSize * 0.5;
+ const rightX = baseX - px * settings.arrowHeadSize * 0.5;
+ const rightY = baseY - py * settings.arrowHeadSize * 0.5;
switch (arrowType) {
case "normal":
@@ -150,23 +150,23 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string {
case "dropped":
// cross
- const dropX = x1 + ux * len * settings.dropStart;
- const dropY = y1 + uy * len * settings.dropStart;
+ const dropX = x1 + ux * len * settings.dropStartRatio;
+ const dropY = y1 + uy * len * settings.dropStartRatio;
arrowsvg = `
-
-
+
+
`;
break;
case "corrupt":
// draw the squiggly
- const waveX = x1 + ux * len * settings.corruptStart;
- const waveY = y1 + uy * len * settings.corruptStart;
+ const waveX = x1 + ux * len * settings.corruptStartRatio;
+ const waveY = y1 + uy * len * settings.corruptStartRatio;
- const waveLen = (len * (1 - settings.corruptStart)) - 2 * settings.arrowSize;
+ const waveLen = (len * (1 - settings.corruptStartRatio)) - 2 * settings.arrowHeadSize;
const waveInterval = waveLen / (4 * settings.squiggleCount);
@@ -207,13 +207,13 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string {
case "thick":
arrowsvg = `
-
+
-
-
-
+
+
+
`;
break;
@@ -240,7 +240,7 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string {
const lx = mx - px * offset;
const ly = (arrowType === "thick")
- ? my + (direction * py * settings.thickArrowSize / 2)
+ ? my + (direction * py * settings.thickArrowThickness / 2)
: my - py * offset;
From 3436ec2206af59cdae7f4e38cd1c62ae7638e889 Mon Sep 17 00:00:00 2001
From: ys_teng <58208381+YeeShin504@users.noreply.github.com>
Date: Sat, 21 Mar 2026 22:44:38 +0800
Subject: [PATCH 4/6] Add syntax reference to live editor
---
docs/editor.css | 181 +++++++++++++++++++++-
docs/editor.js | 201 +++++++++++++++++++++++++
docs/example.svg | 382 ++++++++++++++++++++++++++++++++++++++++-------
docs/index.html | 33 +++-
src/renderer.ts | 2 +-
5 files changed, 731 insertions(+), 68 deletions(-)
diff --git a/docs/editor.css b/docs/editor.css
index 0f01077..de360bb 100644
--- a/docs/editor.css
+++ b/docs/editor.css
@@ -18,6 +18,79 @@ header {
border-bottom: 1px solid #3e3e42;
}
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.resizer {
+ width: 6px;
+ background: #1e1e1e;
+ cursor: col-resize;
+ position: relative;
+ z-index: 100;
+ border-left: 1px solid #3e3e42;
+ border-right: 1px solid #3e3e42;
+ transition: background 0.2s;
+}
+
+.resizer:hover, .resizer.dragging {
+ background: #0e639c;
+}
+
+.toggle-arrow {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: #2d2d2d;
+ border: 1px solid #3e3e42;
+ color: #858585;
+ width: 20px;
+ height: 40px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 10px;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+ transition: all 0.2s;
+}
+
+.toggle-arrow:hover {
+ background: #3e3e42;
+ color: #ffffff;
+ border-color: #0e639c;
+}
+
+.panel {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+#panel-reference {
+ flex: 0 0 280px;
+ background: #252526;
+ transition: flex-basis 0.3s ease-in-out;
+}
+
+#panel-reference.collapsed {
+ flex: 0 0 0px !important;
+ overflow: hidden;
+}
+
+#panel-editor {
+ flex: 1 1 0;
+}
+
+#panel-preview {
+ flex: 1 1 0;
+}
+
h1 {
font-size: 1.5rem;
font-weight: 600;
@@ -35,11 +108,101 @@ main {
height: calc(100vh - 80px);
}
-.panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- border-right: 1px solid #3e3e42;
+.reference-content {
+ background: #252526;
+ padding: 1.25rem;
+ overflow-y: auto;
+}
+
+.search-container {
+ padding: 0.75rem 1.25rem;
+ background: #252526;
+ border-bottom: 1px solid #3e3e42;
+}
+
+#refSearch {
+ width: 100%;
+ background: #3c3c3c;
+ border: 1px solid #3e3e42;
+ color: #cccccc;
+ padding: 0.4rem 0.75rem;
+ border-radius: 3px;
+ font-size: 0.85rem;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+#refSearch:focus {
+ border-color: #0e639c;
+}
+
+.loading {
+ color: #858585;
+ font-size: 0.875rem;
+ font-style: italic;
+}
+
+.ref-group h4 {
+ color: #569cd6;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin: 1.5rem 0 0.75rem 0;
+}
+
+.ref-group:first-child h4 {
+ margin-top: 0;
+}
+
+.ref-item {
+ background: #2d2d2d;
+ border: 1px solid #3e3e42;
+ border-radius: 6px;
+ padding: 0.75rem;
+ margin-bottom: 0.75rem;
+ transition: border-color 0.2s;
+}
+
+.ref-item:hover {
+ border-color: #454545;
+}
+
+.ref-title {
+ font-size: 0.85rem;
+ color: #cccccc;
+ font-weight: 500;
+ margin-bottom: 0.4rem;
+}
+
+.ref-syntax {
+ display: block;
+ font-family: inherit;
+ font-size: 0.75rem;
+ background: #1e1e1e;
+ padding: 0.4rem 0.6rem;
+ color: #ce9178;
+ border-radius: 4px;
+ margin-bottom: 0.6rem;
+ border: 1px solid #3e3e42;
+}
+
+.try-btn-small {
+ background: transparent;
+ border: 1px solid #3e3e42;
+ color: #858585;
+ padding: 0.25rem 0.6rem;
+ border-radius: 3px;
+ font-size: 0.7rem;
+ cursor: pointer;
+ width: 100%;
+ transition: all 0.2s;
+}
+
+.try-btn-small:hover {
+ background: #3e3e42;
+ color: #ffffff;
+ border-color: #0e639c;
}
.panel:last-child {
@@ -108,12 +271,16 @@ main {
}
.protocol-ml-wrapper {
- display: inline-block;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
svg {
max-width: 100%;
- height: auto;
+ max-height: 100%;
}
@media (max-width: 768px) {
diff --git a/docs/editor.js b/docs/editor.js
index 0a1cac2..60ec6a5 100644
--- a/docs/editor.js
+++ b/docs/editor.js
@@ -6,6 +6,203 @@ const copyCodeBtn = document.getElementById('copyCode');
const copyPNGBtn = document.getElementById('copyPNG');
const copySVGBtn = document.getElementById('copySVG');
+// Panel & Resizer elements
+const panelRef = document.getElementById('panel-reference');
+const panelEditor = document.getElementById('panel-editor');
+const panelPreview = document.getElementById('panel-preview');
+const resizerRef = document.getElementById('resizer-ref');
+const resizerPreview = document.getElementById('resizer-preview');
+const toggleRefBtn = document.getElementById('toggleReference');
+
+// Resizing logic
+function initResizer(resizer, leftPanel, rightPanel, isFirst) {
+ let startX, startWidthLeft, startWidthRight;
+
+ resizer.addEventListener('mousedown', (e) => {
+ if (e.target.id === 'toggleReference') return;
+
+ startX = e.clientX;
+ startWidthLeft = leftPanel.offsetWidth;
+ startWidthRight = rightPanel.offsetWidth;
+
+ // Disable transitions during resizing to prevent lag
+ leftPanel.style.transition = 'none';
+
+ const onMouseMove = (e) => {
+ const deltaX = e.clientX - startX;
+ if (isFirst) {
+ const newWidth = Math.max(0, startWidthLeft + deltaX);
+ leftPanel.style.flex = `0 0 ${newWidth}px`;
+ if (editor) editor.refresh();
+ } else {
+ const newWidth = Math.max(100, startWidthLeft + deltaX);
+ leftPanel.style.flex = `0 0 ${newWidth}px`;
+ if (editor) editor.refresh();
+ }
+ };
+
+ const onMouseUp = () => {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ resizer.classList.remove('dragging');
+ document.body.style.cursor = 'default';
+
+ // Re-enable transition after resizing
+ leftPanel.style.transition = '';
+
+ if (editor) editor.refresh();
+ };
+
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ resizer.classList.add('dragging');
+ document.body.style.cursor = 'col-resize';
+ });
+}
+
+// Toggle Reference logic
+toggleRefBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ panelRef.classList.toggle('collapsed');
+ const isCollapsed = panelRef.classList.contains('collapsed');
+ toggleRefBtn.textContent = isCollapsed ? '▶' : '◀';
+});
+
+const REFERENCE_DATA = [
+ {
+ group: "Participants",
+ items: [
+ { title: "Define Participant", syntax: 'participant Name alias', example: 'participant Client c\nparticipant Server s' },
+ { title: "Participant Spacing", syntax: 'def participantSpacing 240px', example: 'def participantSpacing 300px' },
+ { title: "Label Height", syntax: 'def participantLabelHeight 30px', example: 'def participantLabelHeight 50px' },
+ { title: "Font Size", syntax: 'def participantFontSize 20px', example: 'def participantFontSize 24px' }
+ ]
+ },
+ {
+ group: "Message Arrows",
+ items: [
+ { title: "Normal Arrow", syntax: 'a -> b : "label"', example: 'a -> b : "Request"' },
+ { title: "Thick Arrow", syntax: 'a => b : "label"', example: 'a => b : "Big Data"' },
+ { title: "Corrupt Arrow", syntax: 'a ~> b : "label"', example: 'a ~> b : "Fragmented"' },
+ { title: "Dropped Message", syntax: 'a -x b : "label"', example: 'a -x b : "Timeout"' },
+ { title: "Time Offsets", syntax: 'a @1.5 -> b @1', example: 'a @1.5 -> b @1 : "Relative Time"' }
+ ]
+ },
+ {
+ group: "Arrow Styles",
+ items: [
+ { title: "Arrow Head Size", syntax: 'def arrowHeadSize 10px', example: 'def arrowHeadSize 15px' },
+ { title: "Thick Thickness", syntax: 'def thickArrowThickness 40px', example: 'def thickArrowThickness 20px' },
+ { title: "Label Offset", syntax: 'def labelOffset 10px', example: 'def labelOffset 20px' },
+ { title: "Message Font Size", syntax: 'def messageFontSize 15px', example: 'def messageFontSize 18px' }
+ ]
+ },
+ {
+ group: "Corrupt & Dropped",
+ items: [
+ { title: "Corrupt Start %", syntax: 'def corruptStartRatio 85%', example: 'def corruptStartRatio 50%' },
+ { title: "Drop Start %", syntax: 'def dropStartRatio 85%', example: 'def dropStartRatio 50%' },
+ { title: "Drop Cross Size", syntax: 'def dropCrossSize 12px', example: 'def dropCrossSize 20px' },
+ { title: "Corrupt Squiggle Size", syntax: 'def squiggleSize 20px', example: 'def squiggleSize 10px' },
+ { title: "Corrupt Squiggle Count", syntax: 'def squiggleCount 2', example: 'def squiggleCount 5' }
+ ]
+ },
+ {
+ group: "Annotations",
+ items: [
+ { title: "Left Note", syntax: 'a < "text"', example: 'a < "Local check"' },
+ { title: "Right Note", syntax: 'a > "text"', example: 'a > "Processing"' },
+ { title: "Annotation Width", syntax: 'def annotationWidth 80px', example: 'def annotationWidth 120px' }
+ ]
+ },
+ {
+ group: "Grid & Time",
+ items: [
+ { title: "Show Grid", syntax: 'def showGrid true', example: 'def showGrid true' },
+ { title: "Show Time Ticks", syntax: 'def showTimeTicks true', example: 'def showTimeTicks true' },
+ { title: "Time Interval", syntax: 'def timeTickInterval 40px', example: 'def timeTickInterval 60px' },
+ { title: "Time Unit", syntax: 'def timeUnit /ms', example: 'def timeUnit /ms' }
+ ]
+ },
+ {
+ group: "Layout",
+ items: [
+ { title: "Horizontal Padding", syntax: 'def paddingX 30px', example: 'def paddingX 50px' },
+ { title: "Vertical Padding", syntax: 'def paddingY 20px', example: 'def paddingY 50px' }
+ ]
+ }
+];
+
+function renderReference(filter = '') {
+ const container = document.getElementById('reference-list');
+ if (!container) return;
+
+ container.innerHTML = ''; // Clear loading message
+ const query = filter.toLowerCase().trim();
+
+ REFERENCE_DATA.forEach(group => {
+ const filteredItems = group.items.filter(item =>
+ item.title.toLowerCase().includes(query) ||
+ item.syntax.toLowerCase().includes(query) ||
+ group.group.toLowerCase().includes(query)
+ );
+
+ if (filteredItems.length === 0) return;
+
+ const groupEl = document.createElement('div');
+ groupEl.className = 'ref-group';
+ groupEl.innerHTML = `
${group.group}
`;
+
+ const itemsContainer = document.createElement('div');
+ itemsContainer.className = 'group-items';
+
+ filteredItems.forEach(item => {
+ const itemEl = document.createElement('div');
+ itemEl.className = 'ref-item';
+
+ const titleEl = document.createElement('div');
+ titleEl.className = 'ref-title';
+ titleEl.textContent = item.title;
+
+ const codeEl = document.createElement('code');
+ codeEl.className = 'ref-syntax';
+ // Use the editor engine to highlight this static snippet!
+ CodeMirror.runMode(item.syntax, 'protocol-ml', codeEl);
+
+ const btn = document.createElement('button');
+ btn.className = 'try-btn-small';
+ btn.textContent = 'Try it';
+ btn.addEventListener('click', () => {
+ const currentValue = editor.getValue();
+ const newValue = currentValue ? (currentValue + '\n\n' + item.example) : item.example;
+ editor.setValue(newValue);
+ editor.setCursor(editor.lineCount(), 0);
+ editor.focus();
+ });
+
+ itemEl.appendChild(titleEl);
+ itemEl.appendChild(codeEl);
+ itemEl.appendChild(btn);
+ itemsContainer.appendChild(itemEl);
+ });
+
+ groupEl.appendChild(itemsContainer);
+ container.appendChild(groupEl);
+ });
+
+ if (container.innerHTML === '' && query !== '') {
+ container.innerHTML = 'No results found for "' + filter + '"
';
+ }
+}
+
+const refSearch = document.getElementById('refSearch');
+if (refSearch) {
+ refSearch.addEventListener('input', (e) => {
+ renderReference(e.target.value);
+ });
+}
+
+
const INITIAL_CODE = `def timeTickInterval 40px
def participantSpacing 240px
def showTimeTicks true
@@ -144,3 +341,7 @@ copySVGBtn.addEventListener('click', async () => {
// Initial render
render();
+renderReference();
+
+initResizer(resizerRef, panelRef, panelEditor, true);
+initResizer(resizerPreview, panelEditor, panelPreview, false);
diff --git a/docs/example.svg b/docs/example.svg
index 9f1e630..df0da80 100644
--- a/docs/example.svg
+++ b/docs/example.svg
@@ -1,105 +1,377 @@
-
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
index 77ff5b3..bf11c63 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -11,12 +11,32 @@
-
-