mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-23 18:26:39 +02:00
WIP 3
This commit is contained in:
@@ -120,11 +120,17 @@ InvTrapEnd { invTrapEnd }
|
|||||||
"<--" $[-]* $[-xo>] | // < + 2+ dashes + ending
|
"<--" $[-]* $[-xo>] | // < + 2+ dashes + ending
|
||||||
"--" $[-]* $[-xo>] | // 2+ dashes + ending (includes --> and ---)
|
"--" $[-]* $[-xo>] | // 2+ dashes + ending (includes --> and ---)
|
||||||
|
|
||||||
// Edge text start patterns - for patterns like A<-- text -->B
|
// Edge text start patterns - for patterns like A<-- text -->B and A x== text ==x B
|
||||||
// These need to be separate from complete arrows to handle edge text properly
|
// These need to be separate from complete arrows to handle edge text properly
|
||||||
"<--" | // Left-pointing edge text start (matches START_LINK)
|
"<--" | // Left-pointing edge text start (matches START_LINK)
|
||||||
"<==" | // Left-pointing thick edge text start
|
"<==" | // Left-pointing thick edge text start
|
||||||
"<-." | // Left-pointing dotted edge text start (matches START_DOTTED_LINK)
|
"<-." | // Left-pointing dotted edge text start (matches START_DOTTED_LINK)
|
||||||
|
"x--" | // Cross head open normal start (A x-- text --x B)
|
||||||
|
"o--" | // Circle head open normal start (A o-- text --o B)
|
||||||
|
"x==" | // Cross head open thick start (A x== text ==x B)
|
||||||
|
"o==" | // Circle head open thick start (A o== text ==o B)
|
||||||
|
"x-." | // Cross head open dotted start (A x-. text .-x B)
|
||||||
|
"o-." | // Circle head open dotted start (A o-. text .-o B)
|
||||||
|
|
||||||
// Thick arrows - JISON: [xo<]?\=\=+[=xo>]
|
// Thick arrows - JISON: [xo<]?\=\=+[=xo>]
|
||||||
// Optional left head + 2+ equals + right ending
|
// Optional left head + 2+ equals + right ending
|
||||||
|
@@ -100,6 +100,7 @@ class LezerFlowParser {
|
|||||||
edgeType: string;
|
edgeType: string;
|
||||||
edgeStroke: string;
|
edgeStroke: string;
|
||||||
} | null = null; // Track last edge for retroactive target chaining
|
} | null = null; // Track last edge for retroactive target chaining
|
||||||
|
private originalSource = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.yy = undefined;
|
this.yy = undefined;
|
||||||
@@ -122,6 +123,9 @@ class LezerFlowParser {
|
|||||||
|
|
||||||
log.debug('UIO Parsing flowchart with Lezer:', newSrc);
|
log.debug('UIO Parsing flowchart with Lezer:', newSrc);
|
||||||
|
|
||||||
|
// Keep a copy of the original source for substring extraction
|
||||||
|
this.originalSource = newSrc;
|
||||||
|
|
||||||
// Parse with Lezer
|
// Parse with Lezer
|
||||||
const tree = lezerParser.parse(newSrc);
|
const tree = lezerParser.parse(newSrc);
|
||||||
|
|
||||||
@@ -169,6 +173,15 @@ class LezerFlowParser {
|
|||||||
const processedTokens: { type: string; value: string; from: number; to: number }[] = [];
|
const processedTokens: { type: string; value: string; from: number; to: number }[] = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
|
// Helper: detect head-open tokens like x--, o--, x==, o==, x-., o-.
|
||||||
|
const isHeadOpenToken = (val: string) =>
|
||||||
|
val === 'x--' ||
|
||||||
|
val === 'o--' ||
|
||||||
|
val === 'x==' ||
|
||||||
|
val === 'o==' ||
|
||||||
|
val === 'x-.' ||
|
||||||
|
val === 'o-.';
|
||||||
|
|
||||||
while (i < tokens.length) {
|
while (i < tokens.length) {
|
||||||
const token = tokens[i];
|
const token = tokens[i];
|
||||||
|
|
||||||
@@ -179,6 +192,31 @@ class LezerFlowParser {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert NODE_STRING head-open tokens (x--, o--, x==, o==, x-., o-.) into LINK when used as arrow openers
|
||||||
|
if (token.type === 'NODE_STRING' && isHeadOpenToken(token.value)) {
|
||||||
|
// Require a plausible source node immediately before in the processed stream
|
||||||
|
const prev = processedTokens[processedTokens.length - 1];
|
||||||
|
// Look ahead for a closing LINK that ends with matching head (x/o)
|
||||||
|
const head = token.value[0]; // 'x' or 'o'
|
||||||
|
let hasClosingTail = false;
|
||||||
|
for (let j = i + 1; j < Math.min(tokens.length, i + 6); j++) {
|
||||||
|
const t = tokens[j];
|
||||||
|
if (t.type === 'LINK' && (t.value.endsWith(head) || t.value.endsWith('>'))) {
|
||||||
|
hasClosingTail = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prev && (prev.type === 'Identifier' || prev.type === 'NODE_STRING') && hasClosingTail) {
|
||||||
|
const converted = { ...token, type: 'LINK' };
|
||||||
|
console.log(
|
||||||
|
`UIO DEBUG: Converted head-open token ${token.value} to LINK for double-ended arrow`
|
||||||
|
);
|
||||||
|
processedTokens.push(converted);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try to detect fragmented edge patterns
|
// Try to detect fragmented edge patterns
|
||||||
const mergedPattern = this.tryMergeFragmentedEdgePattern(tokens, i);
|
const mergedPattern = this.tryMergeFragmentedEdgePattern(tokens, i);
|
||||||
if (mergedPattern) {
|
if (mergedPattern) {
|
||||||
@@ -213,25 +251,8 @@ class LezerFlowParser {
|
|||||||
// 2. A-- text including URL space and send -->B
|
// 2. A-- text including URL space and send -->B
|
||||||
// 3. A---|text|B (pipe-delimited)
|
// 3. A---|text|B (pipe-delimited)
|
||||||
|
|
||||||
// Check for simple edge pattern first (A---B, A--xB, etc.)
|
// Defer simple one-token edge merging until after checking for pipe or text-between-arrows
|
||||||
// But only if it's not part of a pipe-delimited pattern
|
// This ensures patterns like A--text ... -->B are handled as text, not as A -- text.
|
||||||
if (
|
|
||||||
this.isSimpleEdgePattern(tokens[startIndex]) &&
|
|
||||||
!this.isPartOfPipeDelimitedPattern(tokens, startIndex)
|
|
||||||
) {
|
|
||||||
const patternTokens = [tokens[startIndex]];
|
|
||||||
console.log(
|
|
||||||
`UIO DEBUG: Analyzing simple edge pattern: ${patternTokens.map((t) => t.value).join(' ')}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const merged = this.detectAndMergeEdgePattern(patternTokens, tokens, startIndex);
|
|
||||||
if (merged) {
|
|
||||||
return {
|
|
||||||
mergedTokens: merged,
|
|
||||||
nextIndex: startIndex + 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for pipe-delimited pattern (A---|text|B)
|
// Check for pipe-delimited pattern (A---|text|B)
|
||||||
if (this.isPipeDelimitedEdgePattern(tokens, startIndex)) {
|
if (this.isPipeDelimitedEdgePattern(tokens, startIndex)) {
|
||||||
@@ -282,6 +303,20 @@ class LezerFlowParser {
|
|||||||
return null; // Not a complex edge pattern
|
return null; // Not a complex edge pattern
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling: if this looks like A--text ... -->B or A-- text ... -->B,
|
||||||
|
// fall back to Pattern1/Pattern2 detection so we retain the text.
|
||||||
|
// This helps edge text without pipes.
|
||||||
|
{
|
||||||
|
const slice = tokens.slice(startIndex, endIndex);
|
||||||
|
const merged = this.detectAndMergeEdgePattern(slice, tokens, startIndex);
|
||||||
|
if (merged) {
|
||||||
|
return {
|
||||||
|
mergedTokens: merged,
|
||||||
|
nextIndex: endIndex,
|
||||||
|
} as any; // Will be handled by caller above
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the tokens that form this edge pattern
|
// Extract the tokens that form this edge pattern
|
||||||
const patternTokens = tokens.slice(startIndex, endIndex);
|
const patternTokens = tokens.slice(startIndex, endIndex);
|
||||||
console.log(
|
console.log(
|
||||||
@@ -1038,18 +1073,34 @@ class LezerFlowParser {
|
|||||||
case 'RectStart':
|
case 'RectStart':
|
||||||
case 'TrapStart':
|
case 'TrapStart':
|
||||||
case 'InvTrapStart':
|
case 'InvTrapStart':
|
||||||
case 'TagEnd': // Odd shape start ('>text]')
|
case 'TagEnd': // Odd shape start ('>text]') or split-arrow head ('>')
|
||||||
// Handle orphaned shape tokens (shape tokens without preceding node ID)
|
// Priority 1: If we have a pending shaped target from an embedded arrow, consume as shaped node now
|
||||||
// Check if we have a pending shaped target ID from an embedded arrow edge
|
|
||||||
if (this.pendingShapedTargetId) {
|
if (this.pendingShapedTargetId) {
|
||||||
console.log(
|
console.log(
|
||||||
`UIO DEBUG: Applying shape to pending target node: ${this.pendingShapedTargetId}`
|
`UIO DEBUG: Applying shape to pending target node: ${this.pendingShapedTargetId}`
|
||||||
);
|
);
|
||||||
i = this.parseShapedNodeForTarget(tokens, i, this.pendingShapedTargetId);
|
i = this.parseShapedNodeForTarget(tokens, i, this.pendingShapedTargetId);
|
||||||
this.pendingShapedTargetId = null; // Clear the pending target
|
this.pendingShapedTargetId = null; // Clear the pending target
|
||||||
} else {
|
break;
|
||||||
i = this.parseStatement(tokens, i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priority 2: Orphaned shape token for the last referenced node (e.g., A-->B>text])
|
||||||
|
if (this.isShapeStart(token) && this.lastReferencedNodeId) {
|
||||||
|
console.log(
|
||||||
|
`UIO DEBUG: Detected orphaned shape token '${token.type}:${token.value}' for lastReferencedNodeId=${this.lastReferencedNodeId}`
|
||||||
|
);
|
||||||
|
i = this.parseOrphanedShapeStatement(tokens, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Continuation edge head (e.g., A-->B-->C)
|
||||||
|
if (token.type === 'TagEnd' && token.value === '>' && this.lastTargetNodes.length > 0) {
|
||||||
|
i = this.parseContinuationEdgeStatement(tokens, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Delegate to parseStatement
|
||||||
|
i = this.parseStatement(tokens, i);
|
||||||
break;
|
break;
|
||||||
case 'CLICK':
|
case 'CLICK':
|
||||||
i = this.parseClickStatement(tokens, i);
|
i = this.parseClickStatement(tokens, i);
|
||||||
@@ -1185,6 +1236,19 @@ class LezerFlowParser {
|
|||||||
lookahead.map((t) => `${t.type}:${t.value}`)
|
lookahead.map((t) => `${t.type}:${t.value}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Accessibility statements: accTitle / accDescr
|
||||||
|
if (
|
||||||
|
lookahead.length >= 1 &&
|
||||||
|
lookahead[0].type === 'NODE_STRING' &&
|
||||||
|
(lookahead[0].value === 'accTitle' || lookahead[0].value === 'accDescr')
|
||||||
|
) {
|
||||||
|
if (lookahead[0].value === 'accTitle') {
|
||||||
|
return this.parseAccTitleStatement(tokens, i);
|
||||||
|
} else {
|
||||||
|
return this.parseAccDescrStatement(tokens, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a direction statement (direction BT)
|
// Check if this is a direction statement (direction BT)
|
||||||
if (
|
if (
|
||||||
lookahead.length >= 2 &&
|
lookahead.length >= 2 &&
|
||||||
@@ -1281,7 +1345,7 @@ class LezerFlowParser {
|
|||||||
// Check if this is an edge (A --> B pattern or A(text) --> B pattern)
|
// Check if this is an edge (A --> B pattern or A(text) --> B pattern)
|
||||||
// Check for orphaned shape tokens (shape tokens without preceding node ID) FIRST
|
// Check for orphaned shape tokens (shape tokens without preceding node ID) FIRST
|
||||||
// This happens when an edge creates a target node but leaves the shape tokens for later processing
|
// This happens when an edge creates a target node but leaves the shape tokens for later processing
|
||||||
if (lookahead.length >= 3 && this.isShapeStart(lookahead[0].type)) {
|
if (lookahead.length >= 3 && this.isShapeStart(lookahead[0])) {
|
||||||
console.log(`UIO DEBUG: Taking orphaned shape statement path (shape without node ID)`);
|
console.log(`UIO DEBUG: Taking orphaned shape statement path (shape without node ID)`);
|
||||||
return this.parseOrphanedShapeStatement(tokens, i);
|
return this.parseOrphanedShapeStatement(tokens, i);
|
||||||
}
|
}
|
||||||
@@ -1633,11 +1697,14 @@ class LezerFlowParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a token type represents a shape start delimiter
|
* Check if a token represents a shape start delimiter
|
||||||
* @param tokenType - The token type to check
|
* Accepts either a token object or a token type string for backward compatibility
|
||||||
* @returns True if it's a shape start delimiter
|
|
||||||
*/
|
*/
|
||||||
private isShapeStart(tokenType: string): boolean {
|
private isShapeStart(tokenOrType: { type: string; value: string } | string): boolean {
|
||||||
|
const type = typeof tokenOrType === 'string' ? tokenOrType : tokenOrType.type;
|
||||||
|
const val = typeof tokenOrType === 'string' ? '' : tokenOrType.value;
|
||||||
|
|
||||||
|
// Base shape starts by token type
|
||||||
const shapeStarts = [
|
const shapeStarts = [
|
||||||
'SquareStart', // [
|
'SquareStart', // [
|
||||||
'ParenStart', // (
|
'ParenStart', // (
|
||||||
@@ -1650,7 +1717,18 @@ class LezerFlowParser {
|
|||||||
'InvTrapStart', // [\
|
'InvTrapStart', // [\
|
||||||
'TagEnd', // > (for odd shapes)
|
'TagEnd', // > (for odd shapes)
|
||||||
];
|
];
|
||||||
return shapeStarts.includes(tokenType);
|
|
||||||
|
if (shapeStarts.includes(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some punctuation comes through as generic '⚠' tokens in the lexer
|
||||||
|
// Treat '⚠' with value '>' as an odd-shape start
|
||||||
|
if (type === '⚠' && val === '>') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1775,21 +1853,103 @@ class LezerFlowParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track string parsing state inside shape text
|
||||||
|
let inString = false;
|
||||||
|
let stringQuote: '"' | "'" | null = null;
|
||||||
|
let seenStr = false; // saw a single quoted string token as entire text
|
||||||
|
const sawEllipseCloseHyphen = false; // for ellipse (-text-)
|
||||||
|
|
||||||
// Collect all tokens until we find any valid shape end delimiter
|
// Collect all tokens until we find any valid shape end delimiter
|
||||||
while (i < tokens.length && !possibleEndTokens.includes(tokens[i].type)) {
|
while (i < tokens.length && !possibleEndTokens.includes(tokens[i].type)) {
|
||||||
|
const tk = tokens[i];
|
||||||
|
|
||||||
|
// If we get a complete quoted string token (STR), allow it only if it's the only content
|
||||||
|
if (tk.type === 'STR') {
|
||||||
|
if (shapeText.trim().length > 0 || seenStr) {
|
||||||
|
throw new Error("got 'STR'");
|
||||||
|
}
|
||||||
|
shapeText += tk.value; // keep quotes; processNodeText will strip and classify
|
||||||
|
seenStr = true;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// For ellipse shapes, stop when we encounter the closing hyphen
|
// For ellipse shapes, stop when we encounter the closing hyphen
|
||||||
if (actualShapeType === 'EllipseStart' && tokens[i].type === 'Hyphen') {
|
if (actualShapeType === 'EllipseStart' && tk.type === 'Hyphen') {
|
||||||
break; // This is the closing hyphen, don't include it in the text
|
break; // This is the closing hyphen, don't include it in the text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a full STR was consumed as the only text, parentheses should trigger SQE (legacy)
|
||||||
|
if (
|
||||||
|
seenStr &&
|
||||||
|
(tk.type === 'ParenStart' || tk.type === 'ParenEnd' || tk.value === '(' || tk.value === ')')
|
||||||
|
) {
|
||||||
|
throw new Error("Expecting 'SQE'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote handling - mirror legacy JISON error behavior
|
||||||
|
const isQuoteToken =
|
||||||
|
tk.type === 'STR' ||
|
||||||
|
tk.type === 'SQS' ||
|
||||||
|
tk.type === 'SQE' ||
|
||||||
|
tk.type === 'DQS' ||
|
||||||
|
tk.type === 'DQE' ||
|
||||||
|
(tk.type === '⚠' && (tk.value === '"' || tk.value === "'"));
|
||||||
|
|
||||||
|
if (isQuoteToken) {
|
||||||
|
const quoteChar: '"' | "'" = tk.value === "'" ? "'" : '"';
|
||||||
|
|
||||||
|
if (!inString) {
|
||||||
|
// If there is already plain text before a quote, error: mixing text and string
|
||||||
|
if (shapeText.trim().length > 0) {
|
||||||
|
throw new Error("got 'STR'");
|
||||||
|
}
|
||||||
|
// Enter string mode; do not include quote char itself in text
|
||||||
|
inString = true;
|
||||||
|
stringQuote = quoteChar;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Already inside a string
|
||||||
|
if (stringQuote === quoteChar) {
|
||||||
|
// Closing the string
|
||||||
|
inString = false;
|
||||||
|
stringQuote = null;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Nested/mismatched quote inside string
|
||||||
|
throw new Error("Expecting 'SQE'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If inside a string, any parentheses should trigger the SQE error (unterminated string expected)
|
||||||
|
if (
|
||||||
|
inString &&
|
||||||
|
(tk.type === 'ParenStart' || tk.type === 'ParenEnd' || tk.value === '(' || tk.value === ')')
|
||||||
|
) {
|
||||||
|
throw new Error("Expecting 'SQE'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// In square/rect shapes, parentheses are not allowed within text (legacy behavior)
|
||||||
|
if ((actualShapeType === 'SquareStart' || actualShapeType === 'RectStart') && !inString) {
|
||||||
|
if (tk.type === 'ParenStart' || tk.value === '(') {
|
||||||
|
throw new Error("got 'PS'");
|
||||||
|
}
|
||||||
|
if (tk.type === 'ParenEnd' || tk.value === ')') {
|
||||||
|
throw new Error("got 'PE'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note: We don't stop for statement keywords when inside shape delimiters
|
// Note: We don't stop for statement keywords when inside shape delimiters
|
||||||
// Keywords like 'linkStyle', 'classDef', etc. should be treated as regular text
|
// Keywords like 'linkStyle', 'classDef', etc. should be treated as regular text
|
||||||
// when they appear inside shapes like [linkStyle] or (classDef)
|
// when they appear inside shapes like [linkStyle] or (classDef)
|
||||||
|
|
||||||
// Check for HTML tag pattern: < + tag_name + >
|
// Check for HTML tag pattern: < + tag_name + >
|
||||||
if (
|
if (
|
||||||
tokens[i].type === '⚠' &&
|
tk.type === '⚠' &&
|
||||||
tokens[i].value === '<' &&
|
tk.value === '<' &&
|
||||||
i + 2 < tokens.length &&
|
i + 2 < tokens.length &&
|
||||||
!possibleEndTokens.includes(tokens[i + 1].type)
|
!possibleEndTokens.includes(tokens[i + 1].type)
|
||||||
) {
|
) {
|
||||||
@@ -1803,7 +1963,7 @@ class LezerFlowParser {
|
|||||||
// Preserve original spacing before HTML tag
|
// Preserve original spacing before HTML tag
|
||||||
if (shapeText && i > startIndex + 1) {
|
if (shapeText && i > startIndex + 1) {
|
||||||
const prevToken = tokens[i - 1];
|
const prevToken = tokens[i - 1];
|
||||||
const currentToken = tokens[i];
|
const currentToken = tk;
|
||||||
const gap = currentToken.from - prevToken.to;
|
const gap = currentToken.from - prevToken.to;
|
||||||
|
|
||||||
if (gap > 0) {
|
if (gap > 0) {
|
||||||
@@ -1826,13 +1986,13 @@ class LezerFlowParser {
|
|||||||
// Preserve original spacing by checking token position gaps
|
// Preserve original spacing by checking token position gaps
|
||||||
if (shapeText && i > startIndex + 1) {
|
if (shapeText && i > startIndex + 1) {
|
||||||
const prevToken = tokens[i - 1];
|
const prevToken = tokens[i - 1];
|
||||||
const currentToken = tokens[i];
|
const currentToken = tk;
|
||||||
const gap = currentToken.from - prevToken.to;
|
const gap = currentToken.from - prevToken.to;
|
||||||
|
|
||||||
if (gap > 0) {
|
if (gap > 0) {
|
||||||
// Preserve original spacing (gap represents number of spaces)
|
// Preserve original spacing (gap represents number of spaces)
|
||||||
shapeText += ' '.repeat(gap);
|
shapeText += ' '.repeat(gap);
|
||||||
} else if (this.shouldAddSpaceBetweenTokens(shapeText, tokens[i].value, tokens[i].type)) {
|
} else if (this.shouldAddSpaceBetweenTokens(shapeText, tk.value, tk.type)) {
|
||||||
// Fall back to smart spacing if no gap
|
// Fall back to smart spacing if no gap
|
||||||
shapeText += ' ';
|
shapeText += ' ';
|
||||||
}
|
}
|
||||||
@@ -1840,16 +2000,16 @@ class LezerFlowParser {
|
|||||||
|
|
||||||
// Special handling for ellipse shapes: if this is the last token and it ends with '-',
|
// Special handling for ellipse shapes: if this is the last token and it ends with '-',
|
||||||
// strip the trailing hyphen as it's part of the shape syntax (-text-)
|
// strip the trailing hyphen as it's part of the shape syntax (-text-)
|
||||||
let tokenValue = tokens[i].value;
|
let tokenValue = tk.value;
|
||||||
if (
|
if (
|
||||||
actualShapeType === 'EllipseStart' &&
|
actualShapeType === 'EllipseStart' &&
|
||||||
tokens[i].type === 'NODE_STRING' &&
|
tk.type === 'NODE_STRING' &&
|
||||||
tokenValue.endsWith('-') &&
|
tokenValue.endsWith('-') &&
|
||||||
(i + 1 >= tokens.length || possibleEndTokens.includes(tokens[i + 1].type))
|
(i + 1 >= tokens.length || possibleEndTokens.includes(tokens[i + 1].type))
|
||||||
) {
|
) {
|
||||||
tokenValue = tokenValue.slice(0, -1); // Remove trailing hyphen
|
tokenValue = tokenValue.slice(0, -1); // Remove trailing hyphen
|
||||||
console.log(
|
console.log(
|
||||||
`UIO DEBUG: Stripped trailing hyphen from ellipse text: "${tokens[i].value}" -> "${tokenValue}"`
|
`UIO DEBUG: Stripped trailing hyphen from ellipse text: "${tk.value}" -> "${tokenValue}"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1857,6 +2017,11 @@ class LezerFlowParser {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are still in a string when the shape ends or input ends, error
|
||||||
|
if (inString) {
|
||||||
|
throw new Error("Expecting 'SQE'");
|
||||||
|
}
|
||||||
|
|
||||||
// Special handling for ellipse end: need to skip the final hyphen
|
// Special handling for ellipse end: need to skip the final hyphen
|
||||||
if (
|
if (
|
||||||
actualShapeType === 'EllipseStart' && // Skip the final hyphen before the closing parenthesis
|
actualShapeType === 'EllipseStart' && // Skip the final hyphen before the closing parenthesis
|
||||||
@@ -1866,14 +2031,16 @@ class LezerFlowParser {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture the actual end token for shape mapping
|
// If we ran out of tokens before encountering the shape end, throw to avoid hanging
|
||||||
let actualEndToken = '';
|
if (i >= tokens.length) {
|
||||||
if (i < tokens.length) {
|
throw new Error('Unexpected end of input');
|
||||||
actualEndToken = tokens[i].type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture the actual end token for shape mapping
|
||||||
|
const actualEndToken = tokens[i].type;
|
||||||
|
|
||||||
// Skip the shape end delimiter
|
// Skip the shape end delimiter
|
||||||
if (i < tokens.length && tokens[i].type === shapeEndType) {
|
if (tokens[i].type === shapeEndType) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4023,6 +4190,8 @@ class LezerFlowParser {
|
|||||||
/^<=+$/, // <==, <===, etc.
|
/^<=+$/, // <==, <===, etc.
|
||||||
/^[ox]-+$/, // o--, x--, etc.
|
/^[ox]-+$/, // o--, x--, etc.
|
||||||
/^-+[ox]$/, // --o, --x, etc.
|
/^-+[ox]$/, // --o, --x, etc.
|
||||||
|
/^[ox]=+$/, // o==, x==, etc. (thick open with head)
|
||||||
|
/^=+[ox]$/, // ==o, ==x, etc. (thick close with head)
|
||||||
/^<-\.$/, // <-.
|
/^<-\.$/, // <-.
|
||||||
/^\.->$/, // .->
|
/^\.->$/, // .->
|
||||||
/^=+$/, // open thick continuation (==, ===)
|
/^=+$/, // open thick continuation (==, ===)
|
||||||
@@ -4913,6 +5082,188 @@ class LezerFlowParser {
|
|||||||
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse accTitle: single-line accessibility title
|
||||||
|
*/
|
||||||
|
private parseAccTitleStatement(
|
||||||
|
tokens: { type: string; value: string; from: number; to: number }[],
|
||||||
|
startIndex: number
|
||||||
|
): number {
|
||||||
|
let i = startIndex;
|
||||||
|
// Consume 'accTitle'
|
||||||
|
i++;
|
||||||
|
// Optional ':' which may come as a generic token (⚠) with value ':'
|
||||||
|
if (i < tokens.length && tokens[i].value.trim() === ':') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect text until semicolon or statement boundary/newline gap
|
||||||
|
let title = '';
|
||||||
|
while (i < tokens.length) {
|
||||||
|
const t = tokens[i];
|
||||||
|
if (t.type === 'SEMI') {
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Stop on obvious statement starters/structural tokens
|
||||||
|
if (
|
||||||
|
['GRAPH', 'SUBGRAPH', 'STYLE', 'CLASSDEF', 'CLASS', 'LINKSTYLE', 'CLICK'].includes(
|
||||||
|
t.type
|
||||||
|
) ||
|
||||||
|
t.type === 'AMP' ||
|
||||||
|
t.type === 'LINK' ||
|
||||||
|
t.type === 'Arrow'
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Stop if large gap (newline) and we already collected some text
|
||||||
|
if (title.length > 0 && i > startIndex + 1) {
|
||||||
|
const prev = tokens[i - 1];
|
||||||
|
const gap = t.from - prev.to;
|
||||||
|
if (gap > 5) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append with spacing rules
|
||||||
|
if (title.length === 0) {
|
||||||
|
title = t.value;
|
||||||
|
} else {
|
||||||
|
if (this.shouldAddSpaceBetweenTokens(title, t.value, t.type)) {
|
||||||
|
title += ' ' + t.value;
|
||||||
|
} else {
|
||||||
|
title += t.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
title = title.trim();
|
||||||
|
if (this.yy && typeof (this.yy as any).setAccTitle === 'function') {
|
||||||
|
(this.yy as any).setAccTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse accDescr: single-line or block form with braces
|
||||||
|
*/
|
||||||
|
private parseAccDescrStatement(
|
||||||
|
tokens: { type: string; value: string; from: number; to: number }[],
|
||||||
|
startIndex: number
|
||||||
|
): number {
|
||||||
|
let i = startIndex;
|
||||||
|
// Consume 'accDescr'
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// Optional ':' which may come as a generic token (⚠) with value ':'
|
||||||
|
if (i < tokens.length && tokens[i].value.trim() === ':') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block form if next token is DiamondStart ("{")
|
||||||
|
if (i < tokens.length && tokens[i].type === 'DiamondStart') {
|
||||||
|
const blockStart = tokens[i]; // '{'
|
||||||
|
i++;
|
||||||
|
// Find matching DiamondEnd ("}")
|
||||||
|
let j = i;
|
||||||
|
let blockEndIndex = -1;
|
||||||
|
while (j < tokens.length) {
|
||||||
|
if (tokens[j].type === 'DiamondEnd') {
|
||||||
|
blockEndIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (blockEndIndex === -1) {
|
||||||
|
// No closing brace; fall back to single-line accumulation
|
||||||
|
return this.parseAccDescrSingleLine(tokens, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract substring from original source preserving newlines, trim indentation and empty lines
|
||||||
|
const startPos = blockStart.to; // position right after '{'
|
||||||
|
const endPos = tokens[blockEndIndex].from; // position right before '}'
|
||||||
|
let raw = '';
|
||||||
|
try {
|
||||||
|
raw = this.originalSource.slice(startPos, endPos);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to token concat if something goes wrong
|
||||||
|
return this.parseAccDescrSingleLine(tokens, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = raw
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((ln) => ln.trim())
|
||||||
|
.filter((ln) => ln.length > 0);
|
||||||
|
const descr = lines.join('\n');
|
||||||
|
|
||||||
|
if (this.yy && typeof (this.yy as any).setAccDescription === 'function') {
|
||||||
|
(this.yy as any).setAccDescription(descr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move index past the closing brace
|
||||||
|
return blockEndIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, treat as single-line form
|
||||||
|
return this.parseAccDescrSingleLine(tokens, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAccDescrSingleLine(
|
||||||
|
tokens: { type: string; value: string; from: number; to: number }[],
|
||||||
|
startIndex: number
|
||||||
|
): number {
|
||||||
|
let i = startIndex;
|
||||||
|
let descr = '';
|
||||||
|
|
||||||
|
while (i < tokens.length) {
|
||||||
|
const t = tokens[i];
|
||||||
|
if (t.type === 'SEMI') {
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Stop at obvious statement boundaries
|
||||||
|
if (
|
||||||
|
['GRAPH', 'SUBGRAPH', 'STYLE', 'CLASSDEF', 'CLASS', 'LINKSTYLE', 'CLICK'].includes(
|
||||||
|
t.type
|
||||||
|
) ||
|
||||||
|
t.type === 'AMP' ||
|
||||||
|
t.type === 'LINK' ||
|
||||||
|
t.type === 'Arrow'
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if large gap (newline) and we already collected some text
|
||||||
|
if (descr.length > 0) {
|
||||||
|
const prev = tokens[i - 1];
|
||||||
|
const gap = t.from - prev.to;
|
||||||
|
if (gap > 5) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descr.length === 0) {
|
||||||
|
descr = t.value;
|
||||||
|
} else {
|
||||||
|
if (this.shouldAddSpaceBetweenTokens(descr, t.value, t.type)) {
|
||||||
|
descr += ' ' + t.value;
|
||||||
|
} else {
|
||||||
|
descr += t.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
descr = descr.trim();
|
||||||
|
if (this.yy && typeof (this.yy as any).setAccDescription === 'function') {
|
||||||
|
(this.yy as any).setAccDescription(descr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create parser instance
|
// Create parser instance
|
||||||
|
Reference in New Issue
Block a user