diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d78d6..8a3bda4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ and this project adheres to ### Added -- Support for optimizing source diagram using `--rewrite` +- Support for generation overview diagrams on root elements, skippable with + `--no-overview-diagram` +- Support for optimizing source definition file using `--rewrite` - Skip only diagram wrapping with `--no-diagram-wrap` - Breaking of long elements over multiple lines in optional items `[]` - Plain text will now also be optimized when reasonable: Text will not be diff --git a/README.md b/README.md index a8e667d..5d7d6f8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ based on the ISO/IEC 14977 specification - Validates if document is complete and has no duplicate declarations - Shows pretty printed text syntax in the document - Pretty printing of the sourcefile +- Table of contents indicating root elements, character sets, common elements + and recursion +- Generation of large overview diagrams for root elements ## Installation @@ -41,6 +44,7 @@ Options: --lint exit with status code 2 if EBNF document has warnings --write-style rewrites the source document with styled text --no-optimizations does not try to optimize the diagrams + --no-overview-diagram skip creating overview diagrams for root elements --no-diagram-wrap does not wrap diagrams for width minimization --no-text-formatting does not format the output text version (becomes single line) --dump-ast dump EBNF file AST to target destination for further processing diff --git a/src/cli.js b/src/cli.js index 0b40152..d330368 100644 --- a/src/cli.js +++ b/src/cli.js @@ -30,6 +30,10 @@ program "--no-optimizations", "does not try to optimize the diagrams and texts" ) + .option( + "--no-overview-diagram", + "skip creating overview diagrams for root elements" + ) .option("--no-diagram-wrap", "does not wrap diagrams for width minimization") .option( "--no-text-formatting", @@ -46,6 +50,7 @@ async function run(args) { } const allowOutput = !program.quiet; const optimizeDiagrams = program.optimizations; + const overviewDiagram = program.overviewDiagram; const optimizeText = program.optimizations; const textFormatting = program.textFormatting; const diagramWrap = program.diagramWrap; @@ -112,6 +117,7 @@ async function run(args) { optimizeDiagrams, optimizeText, textFormatting, + overviewDiagram, diagramWrap }); await writeFile(targetFilename, report, "utf8"); diff --git a/src/extra-diagram-elements.js b/src/extra-diagram-elements.js index 2957b34..5f0f9fe 100644 --- a/src/extra-diagram-elements.js +++ b/src/extra-diagram-elements.js @@ -1,4 +1,10 @@ -const { FakeSVG, Path, Diagram } = require("railroad-diagrams"); +const { + FakeSVG, + Path, + Diagram, + Comment, + Terminal +} = require("railroad-diagrams"); const subclassOf = (baseClass, superClass) => { baseClass.prototype = Object.create(superClass.prototype); @@ -61,6 +67,73 @@ CommentWithLine.prototype.format = function(x, y, width) { return this; }; +function wrapString(value) { + return value instanceof FakeSVG ? value : new Terminal("" + value); +} + +const Group = function Group(item, label) { + if (!(this instanceof Group)) return new Group(item, label); + FakeSVG.call(this, "g"); + this.item = wrapString(item); + this.label = + label instanceof FakeSVG ? label : label ? new Comment(label) : undefined; + + this.width = Math.max( + this.item.width + (this.item.needsSpace ? 20 : 0), + this.label ? this.label.width : 0, + Diagram.ARC_RADIUS * 2 + ); + this.height = this.item.height; + this.boxUp = this.up = Math.max( + this.item.up + Diagram.VERTICAL_SEPARATION, + Diagram.ARC_RADIUS + ); + if (this.label) { + this.up += this.label.up + this.label.height + this.label.down; + } + this.down = Math.max( + this.item.down + Diagram.VERTICAL_SEPARATION, + Diagram.ARC_RADIUS + ); + this.needsSpace = true; + if (Diagram.DEBUG) { + this.attrs["data-updown"] = this.up + " " + this.height + " " + this.down; + this.attrs["data-type"] = "group"; + } +}; +subclassOf(Group, FakeSVG); +Group.prototype.needsSpace = true; +Group.prototype.format = function(x, y, width) { + var gaps = determineGaps(width, this.width); + new Path(x, y).h(gaps[0]).addTo(this); + new Path(x + gaps[0] + this.width, y + this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + new FakeSVG("rect", { + x, + y: y - this.boxUp, + width: this.width, + height: this.boxUp + this.height + this.down, + rx: Diagram.ARC_RADIUS, + ry: Diagram.ARC_RADIUS, + class: "group-box" + }).addTo(this); + + this.item.format(x, y, this.width).addTo(this); + if (this.label) { + this.label + .format( + x, + y - (this.boxUp + this.label.down + this.label.height), + this.label.width + ) + .addTo(this); + } + + return this; +}; + module.exports = { - CommentWithLine + CommentWithLine, + Group }; diff --git a/src/report-builder.js b/src/report-builder.js index 0c05d94..5bb378d 100644 --- a/src/report-builder.js +++ b/src/report-builder.js @@ -30,7 +30,7 @@ const { createDefinitionMetadata } = require("./toc"); const { productionToEBNF } = require("./ebnf-builder"); -const { CommentWithLine } = require("./extra-diagram-elements"); +const { CommentWithLine, Group } = require("./extra-diagram-elements"); const dasherize = str => str.replace(/\s+/g, "-"); @@ -94,6 +94,9 @@ const productionToDiagram = (production, options) => { return Terminal(production.terminal); } if (production.nonTerminal) { + if (options.renderNonTerminal) { + return options.renderNonTerminal(production.nonTerminal); + } return NonTerminal(production.nonTerminal, { href: `#${dasherize(production.nonTerminal)}` }); @@ -247,6 +250,54 @@ const createTocStructure = (tocData, metadata) => ) .join(""); +const createDiagram = (production, metadata, ast, options) => { + const renderProduction = + options.optimizeDiagrams === false + ? production + : optimizeProduction(production); + + const baseOptions = { + maxChoiceLength: options.optimizeDiagrams ? MAX_CHOICE_LENGTH : Infinity, + optimizeSequenceLength: options.diagramWrap && options.optimizeDiagrams + }; + + const expanded = []; + + const renderNonTerminal = item => { + if (options.overview) { + const expand = !expanded.includes(item) && !metadata[item].characterSet; + + if (expand) { + const nested = ast.find(node => node.identifier === item); + expanded.push(item); + return Group( + productionToDiagram(nested.definition, { + ...baseOptions, + renderNonTerminal + }), + Comment(item, { href: `#${dasherize(item)}` }) + ); + } + } + return NonTerminal(item, { + href: `#${dasherize(item)}` + }); + }; + + const diagram = productionToDiagram( + { + ...renderProduction, + complex: options.complex + }, + { + ...baseOptions, + renderNonTerminal + } + ); + + return diagram; +}; + const createDocumentation = (ast, options) => { const structuralToc = createStructuralToc(ast); const metadata = createDefinitionMetadata(structuralToc); @@ -256,28 +307,19 @@ const createDocumentation = (ast, options) => { if (production.comment) { return commentTemplate(production.comment); } + const outgoingReferences = searchReferencesFromIdentifier( production.identifier, ast ); - const renderProduction = - options.optimizeDiagrams === false - ? production - : optimizeProduction(production); - const diagram = productionToDiagram( - { - ...renderProduction, - complex: outgoingReferences.length > 0 - }, - { - maxChoiceLength: options.optimizeDiagrams - ? MAX_CHOICE_LENGTH - : Infinity, - optimizeSequenceLength: - options.diagramWrap && options.optimizeDiagrams - } - ); + const diagram = createDiagram(production, metadata, ast, { + ...options, + overview: + metadata[production.identifier].root && options.overviewDiagram, + complex: outgoingReferences.length > 0 + }); + return ebnfTemplate({ identifier: production.identifier, ebnf: productionToEBNF( diff --git a/src/report-html-template.js b/src/report-html-template.js index 4602722..ca327f7 100644 --- a/src/report-html-template.js +++ b/src/report-html-template.js @@ -403,6 +403,11 @@ svg.railroad-diagram g.special-sequence text { svg.railroad-diagram rect { stroke-width: 3; } +svg.railroad-diagram rect.group-box { + stroke: gray; + stroke-dasharray: 10 5; + fill: none; +} svg.railroad-diagram g.non-terminal rect { fill: var(--nonTerminalFill); stroke: var(--nonTerminalLines);