UNPKG

mermaid

Version:

Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.

886 lines (868 loc) 31.1 kB
import { selectSvgElement } from "./chunk-CLGD4ZFX.mjs"; import { clear, configureSvgSize, getConfig, getConfig2, getThemeVariables, sanitizeText } from "./chunk-DU6HZSFF.mjs"; import { log } from "./chunk-X3CZISLH.mjs"; import { __name } from "./chunk-Y2CYZVJY.mjs"; // src/diagrams/railroad/railroadDb.ts var diagramTitle = ""; var accTitle = ""; var accDescription = ""; var rules = []; var ruleMap = /* @__PURE__ */ new Map(); var sanitizeText2 = /* @__PURE__ */ __name((text) => { return sanitizeText(text, getConfig2()); }, "sanitizeText"); var sanitizeAstNode = /* @__PURE__ */ __name((node) => { switch (node.type) { case "terminal": return { ...node, value: sanitizeText2(node.value) }; case "nonterminal": return { ...node, name: sanitizeText2(node.name) }; case "sequence": return { ...node, elements: node.elements.map(sanitizeAstNode) }; case "choice": return { ...node, alternatives: node.alternatives.map(sanitizeAstNode) }; case "optional": return { ...node, element: sanitizeAstNode(node.element) }; case "repetition": return { ...node, element: sanitizeAstNode(node.element), separator: node.separator ? sanitizeAstNode(node.separator) : void 0 }; case "special": return { ...node, text: sanitizeText2(node.text) }; } }, "sanitizeAstNode"); var clear2 = /* @__PURE__ */ __name(() => { diagramTitle = ""; accTitle = ""; accDescription = ""; rules.length = 0; ruleMap.clear(); clear(); log.debug("[Railroad] Database cleared"); }, "clear"); var setTitle = /* @__PURE__ */ __name((text) => { diagramTitle = sanitizeText2(text); log.debug("[Railroad] Title set:", text); }, "setTitle"); var getTitle = /* @__PURE__ */ __name(() => { return diagramTitle; }, "getTitle"); var addRule = /* @__PURE__ */ __name((rule) => { const sanitizedRule = { ...rule, name: sanitizeText2(rule.name), definition: sanitizeAstNode(rule.definition), comment: rule.comment ? sanitizeText2(rule.comment) : void 0 }; log.debug("[Railroad] Adding rule:", sanitizedRule.name); if (ruleMap.has(sanitizedRule.name)) { log.warn(`[Railroad] Rule '${sanitizedRule.name}' is already defined. Overwriting.`); } rules.push(sanitizedRule); ruleMap.set(sanitizedRule.name, sanitizedRule); }, "addRule"); var getRules = /* @__PURE__ */ __name(() => { return rules; }, "getRules"); var getRule = /* @__PURE__ */ __name((name) => { return ruleMap.get(name); }, "getRule"); var setAccTitle = /* @__PURE__ */ __name((text) => { accTitle = sanitizeText2(text).replace(/^\s+/g, ""); log.debug("[Railroad] Accessibility title set:", text); }, "setAccTitle"); var getAccTitle = /* @__PURE__ */ __name(() => { return accTitle; }, "getAccTitle"); var setAccDescription = /* @__PURE__ */ __name((text) => { accDescription = sanitizeText2(text).replace(/\n\s+/g, "\n"); log.debug("[Railroad] Accessibility description set:", text); }, "setAccDescription"); var getAccDescription = /* @__PURE__ */ __name(() => { return accDescription; }, "getAccDescription"); var setDiagramTitle = setTitle; var getDiagramTitle = getTitle; var db = { clear: clear2, setTitle, getTitle, addRule, getRules, getRule, setAccTitle, getAccTitle, setAccDescription, getAccDescription, setDiagramTitle, getDiagramTitle }; // src/diagrams/railroad/railroadTypes.ts var DEFAULT_RAILROAD_CONFIG = { // Layout compactMode: false, // Spacing padding: 10, verticalSeparation: 8, horizontalSeparation: 10, arcRadius: 10, // Typography fontSize: 14, fontFamily: "monospace", // Colors - Terminal terminalFill: "#FFFFC0", terminalStroke: "#000000", terminalTextColor: "#000000", // Colors - Non-terminal nonTerminalFill: "#FFFFFF", nonTerminalStroke: "#000000", nonTerminalTextColor: "#000000", // Colors - Lines lineColor: "#000000", strokeWidth: 2, // Colors - Markers markerFill: "#000000", // Colors - Comment commentFill: "#E8E8E8", commentStroke: "#888888", commentTextColor: "#666666", // Colors - Special specialFill: "#F0E0FF", specialStroke: "#8800CC", // Other ruleNameColor: "#000066", showMarkers: true, markerRadius: 5 }; // src/diagrams/railroad/styles.ts var COLOR_VALUE_PATTERN = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$|^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([\d\s%+,./-]+\)$|^[a-z]+$/i; var FONT_FAMILY_PATTERN = /^[\w "',.-]+$/; var RAILROAD_STYLE_OPTION_KEYS = /* @__PURE__ */ new Set([ "compactMode", "padding", "verticalSeparation", "horizontalSeparation", "arcRadius", "fontSize", "fontFamily", "terminalFill", "terminalStroke", "terminalTextColor", "nonTerminalFill", "nonTerminalStroke", "nonTerminalTextColor", "lineColor", "strokeWidth", "markerFill", "commentFill", "commentStroke", "commentTextColor", "specialFill", "specialStroke", "ruleNameColor", "showMarkers", "markerRadius" ]); var isRailroadStyleOptions = /* @__PURE__ */ __name((options) => { if (!options) { return false; } return Object.keys(options).every( (key) => key === "railroad" || RAILROAD_STYLE_OPTION_KEYS.has(key) ); }, "isRailroadStyleOptions"); var extractRailroadOverrides = /* @__PURE__ */ __name((options) => { if (!options) { return {}; } if ("railroad" in options && options.railroad) { return options.railroad; } return isRailroadStyleOptions(options) ? options : {}; }, "extractRailroadOverrides"); var extractThemeOverrides = /* @__PURE__ */ __name((options) => { if (!options || isRailroadStyleOptions(options)) { return {}; } const { railroad: _railroad, svgId: _svgId, theme: _theme, look: _look, ...themeOverrides } = options; return themeOverrides; }, "extractThemeOverrides"); var sanitizeColorValue = /* @__PURE__ */ __name((value, fallback) => { if (typeof value !== "string") { return fallback; } const normalized = value.trim(); return COLOR_VALUE_PATTERN.test(normalized) ? normalized : fallback; }, "sanitizeColorValue"); var sanitizeFontFamilyValue = /* @__PURE__ */ __name((value, fallback) => { if (typeof value !== "string") { return fallback; } const normalized = value.trim(); return FONT_FAMILY_PATTERN.test(normalized) ? normalized : fallback; }, "sanitizeFontFamilyValue"); var sanitizeNumberValue = /* @__PURE__ */ __name((value, fallback) => { const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseFloat(value) : Number.NaN; return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; }, "sanitizeNumberValue"); var parseThemeFontSize = /* @__PURE__ */ __name((value) => { const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseFloat(value) : Number.NaN; return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0; }, "parseThemeFontSize"); var buildThemeDefaults = /* @__PURE__ */ __name((themeVariables) => { const fontFamily = sanitizeFontFamilyValue( themeVariables.fontFamily, DEFAULT_RAILROAD_CONFIG.fontFamily ); const fontSize = parseThemeFontSize(themeVariables.fontSize) ?? DEFAULT_RAILROAD_CONFIG.fontSize; return { ...DEFAULT_RAILROAD_CONFIG, fontFamily, fontSize, terminalFill: sanitizeColorValue( themeVariables.secondBkg ?? themeVariables.secondaryColor, DEFAULT_RAILROAD_CONFIG.terminalFill ), terminalStroke: sanitizeColorValue( themeVariables.secondaryBorderColor ?? themeVariables.lineColor, DEFAULT_RAILROAD_CONFIG.terminalStroke ), terminalTextColor: sanitizeColorValue( themeVariables.secondaryTextColor ?? themeVariables.textColor, DEFAULT_RAILROAD_CONFIG.terminalTextColor ), nonTerminalFill: sanitizeColorValue( themeVariables.mainBkg ?? themeVariables.background, DEFAULT_RAILROAD_CONFIG.nonTerminalFill ), nonTerminalStroke: sanitizeColorValue( themeVariables.primaryBorderColor ?? themeVariables.lineColor, DEFAULT_RAILROAD_CONFIG.nonTerminalStroke ), nonTerminalTextColor: sanitizeColorValue( themeVariables.primaryTextColor ?? themeVariables.textColor, DEFAULT_RAILROAD_CONFIG.nonTerminalTextColor ), lineColor: sanitizeColorValue(themeVariables.lineColor, DEFAULT_RAILROAD_CONFIG.lineColor), markerFill: sanitizeColorValue(themeVariables.lineColor, DEFAULT_RAILROAD_CONFIG.markerFill), commentFill: sanitizeColorValue( themeVariables.labelBackground ?? themeVariables.tertiaryColor, DEFAULT_RAILROAD_CONFIG.commentFill ), commentStroke: sanitizeColorValue( themeVariables.tertiaryBorderColor ?? themeVariables.lineColor, DEFAULT_RAILROAD_CONFIG.commentStroke ), commentTextColor: sanitizeColorValue( themeVariables.tertiaryTextColor ?? themeVariables.textColor, DEFAULT_RAILROAD_CONFIG.commentTextColor ), specialFill: sanitizeColorValue( themeVariables.tertiaryColor ?? themeVariables.secondaryColor, DEFAULT_RAILROAD_CONFIG.specialFill ), specialStroke: sanitizeColorValue( themeVariables.tertiaryBorderColor ?? themeVariables.secondaryBorderColor, DEFAULT_RAILROAD_CONFIG.specialStroke ), ruleNameColor: sanitizeColorValue( themeVariables.titleColor ?? themeVariables.textColor, DEFAULT_RAILROAD_CONFIG.ruleNameColor ) }; }, "buildThemeDefaults"); var buildRailroadStyleOptions = /* @__PURE__ */ __name((options) => { const currentConfig = getConfig(); const themeVariables = { ...getThemeVariables(), ...currentConfig.themeVariables ?? {}, ...extractThemeOverrides(options) }; const themeDefaults = buildThemeDefaults(themeVariables); const railroadOptions = { ...currentConfig.railroad ?? {}, ...extractRailroadOverrides(options) }; return { compactMode: railroadOptions.compactMode ?? themeDefaults.compactMode, padding: sanitizeNumberValue(railroadOptions.padding, themeDefaults.padding), verticalSeparation: sanitizeNumberValue( railroadOptions.verticalSeparation, themeDefaults.verticalSeparation ), horizontalSeparation: sanitizeNumberValue( railroadOptions.horizontalSeparation, themeDefaults.horizontalSeparation ), arcRadius: sanitizeNumberValue(railroadOptions.arcRadius, themeDefaults.arcRadius), fontSize: sanitizeNumberValue(railroadOptions.fontSize, themeDefaults.fontSize), fontFamily: sanitizeFontFamilyValue(railroadOptions.fontFamily, themeDefaults.fontFamily), terminalFill: sanitizeColorValue(railroadOptions.terminalFill, themeDefaults.terminalFill), terminalStroke: sanitizeColorValue( railroadOptions.terminalStroke, themeDefaults.terminalStroke ), terminalTextColor: sanitizeColorValue( railroadOptions.terminalTextColor, themeDefaults.terminalTextColor ), nonTerminalFill: sanitizeColorValue( railroadOptions.nonTerminalFill, themeDefaults.nonTerminalFill ), nonTerminalStroke: sanitizeColorValue( railroadOptions.nonTerminalStroke, themeDefaults.nonTerminalStroke ), nonTerminalTextColor: sanitizeColorValue( railroadOptions.nonTerminalTextColor, themeDefaults.nonTerminalTextColor ), lineColor: sanitizeColorValue(railroadOptions.lineColor, themeDefaults.lineColor), strokeWidth: sanitizeNumberValue(railroadOptions.strokeWidth, themeDefaults.strokeWidth), markerFill: sanitizeColorValue(railroadOptions.markerFill, themeDefaults.markerFill), commentFill: sanitizeColorValue(railroadOptions.commentFill, themeDefaults.commentFill), commentStroke: sanitizeColorValue(railroadOptions.commentStroke, themeDefaults.commentStroke), commentTextColor: sanitizeColorValue( railroadOptions.commentTextColor, themeDefaults.commentTextColor ), specialFill: sanitizeColorValue(railroadOptions.specialFill, themeDefaults.specialFill), specialStroke: sanitizeColorValue(railroadOptions.specialStroke, themeDefaults.specialStroke), ruleNameColor: sanitizeColorValue(railroadOptions.ruleNameColor, themeDefaults.ruleNameColor), showMarkers: railroadOptions.showMarkers ?? themeDefaults.showMarkers, markerRadius: sanitizeNumberValue(railroadOptions.markerRadius, themeDefaults.markerRadius) }; }, "buildRailroadStyleOptions"); var getStyles = /* @__PURE__ */ __name((options) => { const { fontFamily, fontSize, terminalFill, terminalStroke, terminalTextColor, nonTerminalFill, nonTerminalStroke, nonTerminalTextColor, lineColor, strokeWidth, markerFill, commentFill, commentStroke, commentTextColor, specialFill, specialStroke, ruleNameColor } = buildRailroadStyleOptions(options); return ` .railroad-diagram { font-family: ${fontFamily}; font-size: ${fontSize}px; } .railroad-terminal rect { fill: ${terminalFill}; stroke: ${terminalStroke}; stroke-width: ${strokeWidth}px; } .railroad-terminal text { fill: ${terminalTextColor}; font-family: ${fontFamily}; font-size: ${fontSize}px; text-anchor: middle; dominant-baseline: middle; } .railroad-nonterminal rect { fill: ${nonTerminalFill}; stroke: ${nonTerminalStroke}; stroke-width: ${strokeWidth}px; } .railroad-nonterminal text { fill: ${nonTerminalTextColor}; font-family: ${fontFamily}; font-size: ${fontSize}px; text-anchor: middle; dominant-baseline: middle; } .railroad-line { stroke: ${lineColor}; stroke-width: ${strokeWidth}px; fill: none; } .railroad-start circle, .railroad-end circle { fill: ${markerFill}; } .railroad-comment ellipse { fill: ${commentFill}; stroke: ${commentStroke}; stroke-width: ${strokeWidth}px; } .railroad-comment text { fill: ${commentTextColor}; font-style: italic; font-family: ${fontFamily}; font-size: ${fontSize}px; text-anchor: middle; dominant-baseline: middle; } .railroad-special rect { fill: ${specialFill}; stroke: ${specialStroke}; stroke-width: ${strokeWidth}px; stroke-dasharray: 5,3; } .railroad-special text { fill: ${nonTerminalTextColor}; font-family: ${fontFamily}; font-size: ${fontSize}px; text-anchor: middle; dominant-baseline: middle; } .railroad-rule-name { font-weight: bold; fill: ${ruleNameColor}; font-family: ${fontFamily}; font-size: ${fontSize}px; } .railroad-group { /* Grouping container, no specific styles */ } `; }, "getStyles"); // src/diagrams/railroad/railroadRenderer.ts var PathBuilder = class { constructor() { this.d = ""; } static { __name(this, "PathBuilder"); } moveTo(x, y) { this.d += `M ${x} ${y} `; return this; } lineTo(x, y) { this.d += `L ${x} ${y} `; return this; } horizontalTo(x) { this.d += `H ${x} `; return this; } verticalTo(y) { this.d += `V ${y} `; return this; } arcTo(rx, ry, rotation, largeArc, sweep, x, y) { this.d += `A ${rx} ${ry} ${rotation} ${largeArc ? 1 : 0} ${sweep ? 1 : 0} ${x} ${y} `; return this; } build() { return this.d.trim(); } }; var RailroadRenderer = class { constructor(svg, config = buildRailroadStyleOptions()) { this.textCache = /* @__PURE__ */ new Map(); this.svg = svg; this.config = config; } static { __name(this, "RailroadRenderer"); } /** * Measure text dimensions */ measureText(text) { if (this.textCache.has(text)) { return this.textCache.get(text); } const tempText = this.svg.append("text").attr("font-family", this.config.fontFamily).attr("font-size", this.config.fontSize).text(text); const bbox = tempText.node().getBBox(); const dimensions = { width: bbox.width, height: bbox.height }; tempText.remove(); this.textCache.set(text, dimensions); return dimensions; } /** * Render terminal symbol (rounded rectangle) */ renderTerminal(parent, value) { const textDim = this.measureText(value); const width = textDim.width + this.config.padding * 2; const height = textDim.height + this.config.padding * 2; const group = parent.append("g").attr("class", "railroad-terminal"); group.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("rx", 10).attr("ry", 10); group.append("text").attr("x", width / 2).attr("y", height / 2).text(value); return { element: group.node(), dimensions: { width, height, up: height / 2, down: height / 2 } }; } /** * Render non-terminal symbol (rectangle) */ renderNonTerminal(parent, name) { const textDim = this.measureText(name); const width = textDim.width + this.config.padding * 2; const height = textDim.height + this.config.padding * 2; const group = parent.append("g").attr("class", "railroad-nonterminal"); group.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height); group.append("text").attr("x", width / 2).attr("y", height / 2).text(name); return { element: group.node(), dimensions: { width, height, up: height / 2, down: height / 2 } }; } /** * Render sequence (horizontal concatenation) */ renderSequence(parent, elements) { const rendered = elements.map((e) => this.renderExpression(parent, e)); let totalWidth = 0; let maxUp = 0; let maxDown = 0; for (const r of rendered) { totalWidth += r.dimensions.width; maxUp = Math.max(maxUp, r.dimensions.up); maxDown = Math.max(maxDown, r.dimensions.down); } totalWidth += (rendered.length - 1) * this.config.horizontalSeparation; const group = parent.append("g").attr("class", "railroad-sequence"); let x = 0; for (let i = 0; i < rendered.length; i++) { const r = rendered[i]; const y = maxUp - r.dimensions.up; const elem = group.node().appendChild(r.element); elem.setAttribute("transform", `translate(${x}, ${y})`); if (i < rendered.length - 1) { const lineX1 = x + r.dimensions.width; const lineX2 = lineX1 + this.config.horizontalSeparation; const lineY = maxUp; group.append("path").attr("class", "railroad-line").attr("d", new PathBuilder().moveTo(lineX1, lineY).lineTo(lineX2, lineY).build()); } x += r.dimensions.width + this.config.horizontalSeparation; } return { element: group.node(), dimensions: { width: totalWidth, height: maxUp + maxDown, up: maxUp, down: maxDown } }; } /** * Render choice (vertical alternatives) */ renderChoice(parent, alternatives) { const rendered = alternatives.map((a) => this.renderExpression(parent, a)); let maxWidth = 0; let totalHeight = 0; for (const r of rendered) { maxWidth = Math.max(maxWidth, r.dimensions.width); totalHeight += r.dimensions.height; } totalHeight += (rendered.length - 1) * this.config.verticalSeparation; const arcRadius = this.config.arcRadius; const arcWidth = arcRadius * 4; const totalWidth = maxWidth + arcWidth; const group = parent.append("g").attr("class", "railroad-choice"); let y = 0; const centerY = totalHeight / 2; for (const r of rendered) { const elemY = y; const elemCenterY = elemY + r.dimensions.up; const elemX = arcRadius * 2 + (maxWidth - r.dimensions.width) / 2; const elem = group.node().appendChild(r.element); elem.setAttribute("transform", `translate(${elemX}, ${elemY})`); const leftPath = new PathBuilder(); const isBelowCenter = elemCenterY > centerY; if (elemCenterY === centerY) { leftPath.moveTo(0, centerY).lineTo(elemX, elemCenterY); } else { leftPath.moveTo(0, centerY).arcTo( arcRadius, arcRadius, 0, false, isBelowCenter, arcRadius, centerY + (isBelowCenter ? arcRadius : -arcRadius) ).lineTo(arcRadius, elemCenterY - (isBelowCenter ? arcRadius : -arcRadius)).arcTo(arcRadius, arcRadius, 0, false, !isBelowCenter, arcRadius * 2, elemCenterY).lineTo(elemX, elemCenterY); } group.append("path").attr("class", "railroad-line").attr("d", leftPath.build()); const rightPath = new PathBuilder(); const rightStart = elemX + r.dimensions.width; const rightLaneX = totalWidth - arcRadius * 2; if (elemCenterY === centerY) { rightPath.moveTo(rightStart, elemCenterY).lineTo(totalWidth, centerY); } else { rightPath.moveTo(rightStart, elemCenterY).lineTo(rightLaneX, elemCenterY).arcTo( arcRadius, arcRadius, 0, false, !isBelowCenter, totalWidth - arcRadius, elemCenterY + (isBelowCenter ? -arcRadius : arcRadius) ).lineTo(totalWidth - arcRadius, centerY + (isBelowCenter ? arcRadius : -arcRadius)).arcTo(arcRadius, arcRadius, 0, false, isBelowCenter, totalWidth, centerY); } group.append("path").attr("class", "railroad-line").attr("d", rightPath.build()); y += r.dimensions.height + this.config.verticalSeparation; } return { element: group.node(), dimensions: { width: totalWidth, height: totalHeight, up: centerY, down: totalHeight - centerY } }; } /** * Render optional (bypass path above) */ renderOptional(parent, element) { const inner = this.renderExpression(parent, element); const arcRadius = this.config.arcRadius; const arcHeight = arcRadius * 2; const totalWidth = inner.dimensions.width + arcRadius * 4; const totalHeight = inner.dimensions.height + arcHeight; const group = parent.append("g").attr("class", "railroad-optional"); const elemX = arcRadius * 2; const elemY = arcHeight; const elem = group.node().appendChild(inner.element); elem.setAttribute("transform", `translate(${elemX}, ${elemY})`); const centerY = elemY + inner.dimensions.up; const lowerPath = new PathBuilder().moveTo(0, centerY).lineTo(arcRadius * 2, centerY); group.append("path").attr("class", "railroad-line").attr("d", lowerPath.build()); const lowerPath2 = new PathBuilder().moveTo(elemX + inner.dimensions.width, centerY).lineTo(totalWidth, centerY); group.append("path").attr("class", "railroad-line").attr("d", lowerPath2.build()); const bypassPath = new PathBuilder().moveTo(0, centerY).arcTo(arcRadius, arcRadius, 0, false, false, arcRadius, centerY - arcRadius).lineTo(arcRadius, arcRadius).arcTo(arcRadius, arcRadius, 0, false, true, arcRadius * 2, 0).lineTo(totalWidth - arcRadius * 2, 0).arcTo(arcRadius, arcRadius, 0, false, true, totalWidth - arcRadius, arcRadius).lineTo(totalWidth - arcRadius, centerY - arcRadius).arcTo(arcRadius, arcRadius, 0, false, false, totalWidth, centerY); group.append("path").attr("class", "railroad-line").attr("d", bypassPath.build()); return { element: group.node(), dimensions: { width: totalWidth, height: totalHeight, up: centerY, down: totalHeight - centerY } }; } /** * Render repetition (loop back path) */ renderRepetition(parent, element, min) { const inner = this.renderExpression(parent, element); const arcRadius = this.config.arcRadius; const arcHeight = arcRadius * 2; const totalWidth = inner.dimensions.width + arcRadius * 4; const hasBypass = min === 0; const totalHeight = inner.dimensions.height + arcHeight + (hasBypass ? arcHeight : 0); const group = parent.append("g").attr("class", "railroad-repetition"); const elemX = arcRadius * 2; const elemY = hasBypass ? arcHeight : 0; const elem = group.node().appendChild(inner.element); elem.setAttribute("transform", `translate(${elemX}, ${elemY})`); const centerY = elemY + inner.dimensions.up; group.append("path").attr("class", "railroad-line").attr( "d", new PathBuilder().moveTo(0, centerY).lineTo(arcRadius * 2, centerY).build() ); group.append("path").attr("class", "railroad-line").attr( "d", new PathBuilder().moveTo(elemX + inner.dimensions.width, centerY).lineTo(totalWidth, centerY).build() ); const loopY = elemY + inner.dimensions.height + arcRadius; const loopPath = new PathBuilder().moveTo(elemX + inner.dimensions.width, centerY).arcTo( arcRadius, arcRadius, 0, false, true, elemX + inner.dimensions.width + arcRadius, centerY + arcRadius ).lineTo(elemX + inner.dimensions.width + arcRadius, loopY).arcTo( arcRadius, arcRadius, 0, false, true, elemX + inner.dimensions.width, loopY + arcRadius ).lineTo(arcRadius * 2, loopY + arcRadius).arcTo(arcRadius, arcRadius, 0, false, true, arcRadius, loopY).lineTo(arcRadius, centerY + arcRadius).arcTo(arcRadius, arcRadius, 0, false, true, arcRadius * 2, centerY); group.append("path").attr("class", "railroad-line").attr("d", loopPath.build()); if (hasBypass) { const bypassPath = new PathBuilder().moveTo(0, centerY).arcTo(arcRadius, arcRadius, 0, false, false, arcRadius, centerY - arcRadius).lineTo(arcRadius, arcRadius).arcTo(arcRadius, arcRadius, 0, false, true, arcRadius * 2, 0).lineTo(totalWidth - arcRadius * 2, 0).arcTo(arcRadius, arcRadius, 0, false, true, totalWidth - arcRadius, arcRadius).lineTo(totalWidth - arcRadius, centerY - arcRadius).arcTo(arcRadius, arcRadius, 0, false, false, totalWidth, centerY); group.append("path").attr("class", "railroad-line").attr("d", bypassPath.build()); } return { element: group.node(), dimensions: { width: totalWidth, height: totalHeight, up: centerY, down: totalHeight - centerY } }; } /** * Render special sequence */ renderSpecial(parent, text) { const textDim = this.measureText("? " + text + " ?"); const width = textDim.width + this.config.padding * 2; const height = textDim.height + this.config.padding * 2; const group = parent.append("g").attr("class", "railroad-special"); group.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height); group.append("text").attr("x", width / 2).attr("y", height / 2).text("? " + text + " ?"); return { element: group.node(), dimensions: { width, height, up: height / 2, down: height / 2 } }; } /** * Render an expression (recursive) */ renderExpression(parent, node) { switch (node.type) { case "terminal": return this.renderTerminal(parent, node.value); case "nonterminal": return this.renderNonTerminal(parent, node.name); case "sequence": return this.renderSequence(parent, node.elements); case "choice": return this.renderChoice(parent, node.alternatives); case "optional": return this.renderOptional(parent, node.element); case "repetition": return this.renderRepetition(parent, node.element, node.min); case "special": return this.renderSpecial(parent, node.text); default: throw new Error(`Unknown node type: ${node.type}`); } } /** * Render a single rule */ renderRule(rule, y) { const group = this.svg.append("g").attr("class", "railroad-rule").attr("transform", `translate(0, ${y})`); const ruleName = rule.name + " ="; const nameWidth = this.measureText(ruleName).width + 20; const definitionX = nameWidth + 20; const defGroup = group.append("g"); const result = this.renderExpression(defGroup, rule.definition); const baselineY = Math.max(20, result.dimensions.up); const definitionY = baselineY - result.dimensions.up; defGroup.attr("transform", `translate(${definitionX}, ${definitionY})`); const nameGroup = group.append("g").attr("class", "railroad-rule-name-group"); nameGroup.append("text").attr("class", "railroad-rule-name").attr("x", 0).attr("y", baselineY).text(ruleName); const startMarker = group.append("g").attr("class", "railroad-start"); startMarker.append("circle").attr("cx", nameWidth).attr("cy", baselineY).attr("r", this.config.markerRadius); const endMarker = group.append("g").attr("class", "railroad-end"); endMarker.append("circle").attr("cx", definitionX + result.dimensions.width + 10).attr("cy", baselineY).attr("r", this.config.markerRadius); group.append("path").attr("class", "railroad-line").attr( "d", new PathBuilder().moveTo(nameWidth + this.config.markerRadius, baselineY).lineTo(definitionX, baselineY).build() ); group.append("path").attr("class", "railroad-line").attr( "d", new PathBuilder().moveTo(definitionX + result.dimensions.width, baselineY).lineTo(definitionX + result.dimensions.width + 10 - this.config.markerRadius, baselineY).build() ); return { height: Math.max(40, definitionY + result.dimensions.height + this.config.padding * 2), width: definitionX + result.dimensions.width + 10 + this.config.markerRadius }; } /** * Render all rules */ renderDiagram(rules2) { let y = this.config.padding; let maxWidth = 0; for (const rule of rules2) { const result = this.renderRule(rule, y); y += result.height + this.config.verticalSeparation; maxWidth = Math.max(maxWidth, result.width); } return { width: maxWidth + this.config.padding * 2, height: y + this.config.padding }; } }; var configureRailroadSvgSize = /* @__PURE__ */ __name((svg, dimensions, useMaxWidth) => { configureSvgSize(svg, dimensions.height, dimensions.width, useMaxWidth); svg.attr("viewBox", `0 0 ${dimensions.width} ${dimensions.height}`); }, "configureRailroadSvgSize"); var draw = /* @__PURE__ */ __name((text, id, _version) => { log.debug("[Railroad] Rendering diagram\n" + text); try { const svg = selectSvgElement(id); svg.attr("class", "railroad-diagram"); const railroadConfig = getConfig().railroad; const useMaxWidth = railroadConfig?.useMaxWidth ?? true; const rules2 = db.getRules(); log.debug(`[Railroad] Rendering ${rules2.length} rules`); if (rules2.length === 0) { log.warn("[Railroad] No rules to render"); configureRailroadSvgSize(svg, { height: 100, width: 200 }, useMaxWidth); return; } const renderer2 = new RailroadRenderer(svg, buildRailroadStyleOptions()); const dimensions = renderer2.renderDiagram(rules2); configureRailroadSvgSize(svg, dimensions, useMaxWidth); log.debug("[Railroad] Render complete"); } catch (error) { log.error("[Railroad] Render error:", error); throw error; } }, "draw"); var renderer = { draw }; export { db, getStyles, renderer };