UNPKG

langium-railroad

Version:

Use Langium as source for railroad syntax diagrams

161 lines 5.68 kB
/****************************************************************************** * Copyright 2023 TypeFox GmbH * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ import { GrammarAST, GrammarUtils } from 'langium'; import { expandToStringLF, expandToStringLFWithNL } from 'langium/generate'; import { default as rr } from 'railroad-diagrams'; export const defaultCss = ` svg.railroad-diagram { background-color: hsl(30,20%,95%); } svg.railroad-diagram path { stroke-width: 3; stroke: black; fill: rgba(0,0,0,0); } svg.railroad-diagram text { font: bold 14px monospace; text-anchor: middle; } svg.railroad-diagram text.label { text-anchor: start; } svg.railroad-diagram text.comment { font: italic 12px monospace; } svg.railroad-diagram rect { stroke-width: 3; stroke: black; fill: hsl(120,100%,90%); } `.trim(); function styling(options) { return expandToStringLF ` <style> ${defaultCss} </style> ${(options === null || options === void 0 ? void 0 : options.css) ? expandToStringLF ` <style> ${options.css.trim()} </style> ` : ''} `; } /** * Creates a whole HTML file that contains all railroad diagrams. * * @param grammar * @param options * Use to add additional styling to all diagrams and additional behavior for the created HTML. * * @returns A complete HTML document containing all diagrams. */ export function createGrammarDiagramHtml(rules, options) { return expandToStringLFWithNL ` <!DOCTYPE HTML> <html> <head> ${styling(options)}${(options === null || options === void 0 ? void 0 : options.javascript) ? ` <script> ${options.javascript} </script>` : ''} </head> <body> ${createGrammarDiagram(rules)} </body> </html> `; } /** * Creates a standalone SVG diagram for each non-terminal of grammar. * * @param grammar * @param options * Use to add additional styling to all diagrams. * * @returns diagrams * For the rule named 'NonTerminal', diagrams.get('NonTerminal') has its SVG content. */ export function createGrammarDiagramSvg(rules, options) { const diagrams = new Map(); const style = styling(options); for (const nonTerminal of rules) { const ruleDiagram = new rr.Diagram(toRailroad(nonTerminal.definition)); ruleDiagram.attrs.xmlns = 'http://www.w3.org/2000/svg'; ruleDiagram.children = ruleDiagram.children.concat(style); diagrams.set(nonTerminal.name, ruleDiagram.toString()); } return diagrams; } export function createGrammarDiagram(rules) { const text = []; for (const nonTerminal of rules) { text.push('<h2 class="non-terminal-name">', nonTerminal.name, '</h2>', '\n', createRuleDiagram(nonTerminal)); } return text.join(''); } function createRuleDiagram(rule) { const diagram = new rr.Diagram(toRailroad(rule.definition)); return diagram.toString(); } function toRailroad(element) { if (GrammarAST.isAssignment(element)) { return wrapCardinality(element.cardinality, toRailroad(element.terminal)); } else if (GrammarAST.isAlternatives(element)) { return wrapCardinality(element.cardinality, new rr.Choice(0, element.elements.flatMap(e => toRailroad(e)))); } else if (GrammarAST.isUnorderedGroup(element)) { const choice = new rr.Choice(0, element.elements.flatMap(e => toRailroad(e))); const repetition = new rr.ZeroOrMore(choice); return wrapCardinality(element.cardinality, repetition); } else if (GrammarAST.isGroup(element)) { return wrapCardinality(element.cardinality, new rr.Sequence(element.elements.flatMap(e => toRailroad(e)))); } else if (GrammarAST.isKeyword(element)) { return wrapCardinality(element.cardinality, new rr.Terminal(element.value)); } else if (GrammarAST.isRuleCall(element)) { return wrapCardinality(element.cardinality, new rr.NonTerminal(element.rule.$refText)); } else if (GrammarAST.isCrossReference(element)) { if (GrammarAST.isKeyword(element.terminal)) { return wrapCardinality(element.cardinality, new rr.Terminal(element.terminal.value)); } else if (GrammarAST.isRuleCall(element.terminal)) { return wrapCardinality(element.cardinality, new rr.NonTerminal(element.terminal.rule.$refText)); } else { const nameAssignment = element.type.ref && GrammarUtils.findNameAssignment(element.type.ref); if (nameAssignment) { return wrapCardinality(element.cardinality, toRailroad(nameAssignment)); } else { return wrapCardinality(element.cardinality, new rr.NonTerminal('UNKNOWN')); } } } else { return []; } } function wrapCardinality(cardinality, items) { items = Array.isArray(items) ? items : [items]; if (cardinality) { if (cardinality === '*') { return [new rr.ZeroOrMore(new rr.Sequence(items))]; } else if (cardinality === '+') { return [new rr.OneOrMore(new rr.Sequence(items))]; } else if (cardinality === '?') { return [new rr.Optional(new rr.Sequence(items))]; } } return items; } //# sourceMappingURL=grammar-railroad.js.map