Handling of YAML blocks for sequence diagrams

This commit is contained in:
Knut Sveidqvist
2025-09-18 13:37:19 +02:00
parent 574d76c674
commit d1bec402b6
5 changed files with 84 additions and 7 deletions

View File

@@ -34,12 +34,17 @@ export const detectors: Record<string, DetectorRecord> = {};
* @returns A graph definition key * @returns A graph definition key
*/ */
export const detectType = function (text: string, config?: MermaidConfig): string { export const detectType = function (text: string, config?: MermaidConfig): string {
text = text const cleanedText = text
.replace(frontMatterRegex, '') .replace(frontMatterRegex, '')
.replace(directiveRegex, '') .replace(directiveRegex, '')
.replace(anyCommentRegex, '\n'); .replace(anyCommentRegex, '\n');
for (const [key, { detector }] of Object.entries(detectors)) { for (const [key, { detector }] of Object.entries(detectors)) {
const diagram = detector(text, config); const diagram = detector(cleanedText, config);
const isSequence = /sequenceDiagram/.exec(cleanedText);
if (isSequence) {
return 'sequence';
}
if (diagram) { if (diagram) {
return key; return key;
} }

View File

@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js'; import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js'; import { registerDiagram } from './diagramAPI.js';
import { treemap } from '../diagrams/treemap/detector.js'; import { treemap } from '../diagrams/treemap/detector.js';
import { frontMatterRegex } from './regexes.js';
import '../type.d.ts'; import '../type.d.ts';
let hasLoadedDiagrams = false; let hasLoadedDiagrams = false;
@@ -68,7 +69,21 @@ export const addDiagrams = () => {
init: () => null, // no op init: () => null, // no op
}, },
(text) => { (text) => {
return text.toLowerCase().trimStart().startsWith('---'); const trimmed = text.trimStart();
if (!trimmed.startsWith('---')) {
return false;
}
// If there is a valid YAML front matter block, and the remaining text starts
// with a sequence diagram header, let the sequence diagram handle it.
const m = trimmed.match(frontMatterRegex);
if (m) {
const rest = trimmed.slice(m[0].length).trimStart();
if (/^sequencediagram\b/i.test(rest)) {
return false;
}
}
// Otherwise, treat this as an invalid diagram beginning with '---'
return true;
} }
); );

View File

@@ -1,7 +1,9 @@
lexer grammar SequenceLexer; lexer grammar SequenceLexer;
tokens { AS } tokens { AS }
@members {
private seenSD = false;
}
// Comments (skip) // Comments (skip)
HASH_COMMENT: '#' ~[\r\n]* -> skip; HASH_COMMENT: '#' ~[\r\n]* -> skip;
@@ -9,6 +11,10 @@ PERCENT_COMMENT1: '%%' ~[\r\n]* -> skip;
PERCENT_COMMENT2: ~[}] '%%' ~[\r\n]* -> skip; PERCENT_COMMENT2: ~[}] '%%' ~[\r\n]* -> skip;
// Whitespace and newline // Whitespace and newline
// YAML front matter (allowed before the diagram header)
FRONTMATTER_START: { !this.seenSD }? '---' [ \t]* ('\r'? '\n') -> pushMode(FRONTMATTER_MODE), skip;
NEWLINE: ('\r'? '\n')+; NEWLINE: ('\r'? '\n')+;
WS: [ \t]+ -> skip; WS: [ \t]+ -> skip;
@@ -19,7 +25,7 @@ PLUS: '+';
MINUS: '-'; MINUS: '-';
// Core keywords // Core keywords
SD: 'sequenceDiagram' -> pushMode(AFTER_SD); SD: 'sequenceDiagram' { this.seenSD = true; } -> pushMode(AFTER_SD);
PARTICIPANT: 'participant' -> pushMode(ID); PARTICIPANT: 'participant' -> pushMode(ID);
PARTICIPANT_ACTOR: 'actor' -> pushMode(ID); PARTICIPANT_ACTOR: 'actor' -> pushMode(ID);
CREATE: 'create'; CREATE: 'create';
@@ -105,6 +111,7 @@ mode ACC_DESCR_MODE;
ACC_DESCR_VALUE: (~[\r\n;#])* -> popMode; ACC_DESCR_VALUE: (~[\r\n;#])* -> popMode;
mode ACC_DESCR_MULTILINE_MODE; mode ACC_DESCR_MULTILINE_MODE;
ACC_DESCR_MULTILINE_END: '}' -> popMode; ACC_DESCR_MULTILINE_END: '}' -> popMode;
ACC_DESCR_MULTILINE_VALUE: (~['}'])*; ACC_DESCR_MULTILINE_VALUE: (~['}'])*;
@@ -112,6 +119,11 @@ mode CONFIG_MODE;
CONFIG_CONTENT: (~[}])+; CONFIG_CONTENT: (~[}])+;
CONFIG_END: '}' -> popMode; CONFIG_END: '}' -> popMode;
// YAML front matter mode: consume until closing '---' line, then pop
mode FRONTMATTER_MODE;
FM_END: [ \t]* '---' [ \t]* ('\r'? '\n') -> popMode, skip;
FM_LINE: (~[\r\n])* ('\r'? '\n') -> skip;
// After the diagram name keyword, consume the rest of header line then pop // After the diagram name keyword, consume the rest of header line then pop
mode AFTER_SD; mode AFTER_SD;

View File

@@ -25,7 +25,7 @@ const getEnvVar = (name: string): string | undefined => {
return undefined; return undefined;
}; };
const USE_ANTLR_PARSER = getEnvVar('USE_ANTLR_PARSER') === 'true'; const USE_ANTLR_PARSER = true; //getEnvVar('USE_ANTLR_PARSER') === 'false';
// Force logging to window for debugging // Force logging to window for debugging
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

View File

@@ -4,7 +4,7 @@ import mermaidAPI from '../../mermaidAPI.js';
import { Diagram } from '../../Diagram.js'; import { Diagram } from '../../Diagram.js';
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js'; import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
import { SequenceDB } from './sequenceDb.js'; import { SequenceDB } from './sequenceDb.js';
import { preprocessDiagram } from './preprocess.js';
beforeAll(async () => { beforeAll(async () => {
// Is required to load the sequence diagram // Is required to load the sequence diagram
await Diagram.fromText('sequenceDiagram'); await Diagram.fromText('sequenceDiagram');
@@ -1820,6 +1820,51 @@ Alice->Bob: Hello Bob, how are you?`;
expect(bounds.stopy).toBe(models.lastMessage().stopy + 10); expect(bounds.stopy).toBe(models.lastMessage().stopy + 10);
expect(msgs.every((v) => v.wrap)).toBe(true); expect(msgs.every((v) => v.wrap)).toBe(true);
}); });
it('should handle YAML front matter before sequenceDiagram', async () => {
const str = `---
title: Front matter title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
sequenceDiagram
Alice->Bob: Hello Bob`;
await mermaidAPI.parse(str);
const diagram = await Diagram.fromText(str);
await diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
const messages = diagram.db.getMessages();
expect(messages.length).toBe(1);
expect(messages[0].from).toBe('Alice');
expect(messages[0].to).toBe('Bob');
expect(messages[0].message).toBe('Hello Bob');
});
it('should handle YAML front matter before sequenceDiagram', async () => {
const str = `
sequenceDiagram
---
title: Front matter title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
Alice->Bob: Hello Bob`;
const { code, title } = preprocessDiagram(str);
await mermaidAPI.parse(str);
const diagram = await Diagram.fromText(code, { title });
await diagram.renderer.draw(code, 'tst', '1.2.3', diagram);
const messages = diagram.db.getMessages();
expect(messages.length).toBe(1);
expect(messages[0].from).toBe('Alice');
expect(messages[0].to).toBe('Bob');
expect(messages[0].message).toBe('Hello Bob');
});
it('should handle two actors and two centered shared notes', async () => { it('should handle two actors and two centered shared notes', async () => {
const str = ` const str = `
sequenceDiagram sequenceDiagram