ebnf2railroad
Advanced tools
Comparing version 1.6.0 to 1.7.0
@@ -7,2 +7,13 @@ # Changelog | ||
## [1.7.0] - 2018-11-22 | ||
### Added | ||
- Syntax diagram will wrap if sequences become very long | ||
- Split navigation bar in 3 parts. Root elements, Normal elements, | ||
Common elements | ||
- Added Marker of recursion in navigation list | ||
- Responsive design, mobile navigation, overall styling | ||
### Fixed | ||
- Small pretty print issues that caused weird line breaks | ||
## [1.6.0] - 2018-11-13 | ||
@@ -9,0 +20,0 @@ ### Added |
{ | ||
"name": "ebnf2railroad", | ||
"version": "1.6.0", | ||
"version": "1.7.0", | ||
"description": "EBNF to Railroad diagram", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -23,2 +23,3 @@ const dasherize = str => str.replace(/\s+/g, "-"); | ||
const MAX_LINE_LENGTH = 40; | ||
const LINE_MARGIN_LENGTH = 30; | ||
@@ -29,2 +30,3 @@ const defaultOptions = { | ||
maxLineLength: MAX_LINE_LENGTH, | ||
lineMargin: LINE_MARGIN_LENGTH, | ||
indent: 0 | ||
@@ -36,3 +38,5 @@ }; | ||
item.choice && | ||
item.choice.length > 2 && | ||
(item.choice.length > 2 || | ||
productionToEBNF(item, { format: false, markup: false }).length > | ||
options.maxLineLength) && | ||
item.choice.length <= 6 && | ||
@@ -44,2 +48,3 @@ options.format; | ||
...options, | ||
offsetLength: 0, | ||
multiline: true, | ||
@@ -65,2 +70,3 @@ indent: 1, | ||
multiline: true, | ||
offsetLength: 0, | ||
rowCount, | ||
@@ -72,15 +78,7 @@ padding: true, | ||
const sequence = item.sequence && options.format; | ||
if (sequence) { | ||
return { | ||
...options, | ||
lineWrap: true | ||
}; | ||
} | ||
return options; | ||
}; | ||
const calculateMaxLength = production => { | ||
const output = productionToEBNF(production, { markup: false, format: true }); | ||
const calculateMaxLength = (production, format) => { | ||
const output = productionToEBNF(production, { markup: false, format }); | ||
const multiLine = output.includes("\n"); | ||
@@ -182,23 +180,57 @@ return multiLine ? -1 : output.length; | ||
if (production.sequence) { | ||
return production.sequence | ||
.map(element => ({ | ||
output: productionToEBNF(element, { ...options, offsetLength: 0 }), | ||
length: calculateMaxLength(element) | ||
})) | ||
.map((elem, index, list) => { | ||
if (index === 0) return elem.output; | ||
const indent = options.indent + 1; | ||
const currentOffset = list | ||
.slice(0, index - 1) | ||
.reduce( | ||
(acc, elem) => (elem.length === -1 ? 0 : acc + elem.length + 3), | ||
options.offsetLength || 0 | ||
const sequenceLength = (list, offset, till = undefined) => | ||
list | ||
.slice(0, till) | ||
.reduce( | ||
(acc, elem) => (elem.length === -1 ? 0 : acc + elem.length + 3), | ||
offset | ||
); | ||
return ( | ||
production.sequence | ||
.map(element => ({ | ||
element, | ||
length: calculateMaxLength(element, options.format) | ||
})) | ||
.map(({ element }, index, list) => { | ||
if (index === 0) return productionToEBNF(element, options); | ||
const indent = options.indent + 1; | ||
const currentLength = sequenceLength( | ||
list, | ||
options.offsetLength || 0, | ||
index | ||
); | ||
const addBreak = currentOffset + indent * 2 > options.maxLineLength; | ||
if (addBreak) list[index - 1].length = -1; | ||
const nextLength = sequenceLength( | ||
list, | ||
options.offsetLength || 0, | ||
index + 1 | ||
); | ||
const totalLength = sequenceLength(list, options.offsetLength || 0); | ||
const addBreak = | ||
options.format && | ||
currentLength > options.maxLineLength && | ||
nextLength > options.maxLineLength + options.lineMargin / 2 && | ||
totalLength - currentLength > 10; | ||
if (addBreak) list[index - 1].length = -1; | ||
return ` ,${addBreak ? lineIndent(indent) : " "}${elem.output}`; | ||
}) | ||
.join(""); | ||
const offsetLength = addBreak ? 0 : currentLength; | ||
const output = productionToEBNF(element, { | ||
...options, | ||
offsetLength | ||
}); | ||
if (options.format && output.indexOf("\n") !== -1) { | ||
const lastLineLength = output.split("\n").slice(-1)[0].length; | ||
list[index].length = lastLineLength - currentLength; | ||
} | ||
return ` ,${addBreak ? lineIndent(indent) : " "}${output}`; | ||
}) | ||
.join("") | ||
// Remove potentially added whitespace paddings at the end of the line | ||
.split("\n") | ||
.map(line => line.trimEnd()) | ||
.join("\n") | ||
); | ||
} | ||
@@ -205,0 +237,0 @@ if (production.specialSequence) { |
@@ -11,4 +11,6 @@ const { | ||
Skip, | ||
Stack, | ||
Terminal | ||
} = require("railroad-diagrams"); | ||
const { optimizeProduction } = require("./structure-optimizer"); | ||
@@ -27,6 +29,59 @@ const { | ||
} = require("./references"); | ||
const { createAlphabeticalToc, createStructuralToc } = require("./toc"); | ||
const { | ||
createAlphabeticalToc, | ||
createStructuralToc, | ||
createDefinitionMetadata | ||
} = require("./toc"); | ||
const { productionToEBNF } = require("./ebnf-builder"); | ||
const dasherize = str => str.replace(/\s+/g, "-"); | ||
const EXTRA_DIAGRAM_PADDING = 1; | ||
const determineDiagramSequenceLength = production => { | ||
if (production.sequence) { | ||
return production.sequence.reduce( | ||
(total, elem) => | ||
determineDiagramSequenceLength(elem) + EXTRA_DIAGRAM_PADDING + total, | ||
0 | ||
); | ||
} | ||
if (production.nonTerminal) { | ||
return production.nonTerminal.length + EXTRA_DIAGRAM_PADDING; | ||
} | ||
if (production.terminal) { | ||
return production.terminal.length + EXTRA_DIAGRAM_PADDING; | ||
} | ||
if (production.group) { | ||
return determineDiagramSequenceLength(production.group); | ||
} | ||
if (production.choice) { | ||
return ( | ||
production.choice | ||
.map(elem => determineDiagramSequenceLength(elem)) | ||
.reduce((max, elem) => (max > elem ? max : elem), 0) + | ||
EXTRA_DIAGRAM_PADDING * 2 | ||
); | ||
} | ||
if (production.repetition) { | ||
const repetitionLength = determineDiagramSequenceLength( | ||
production.repetition | ||
); | ||
const repeaterLength = production.repeater | ||
? determineDiagramSequenceLength(production.repeater) | ||
: 0; | ||
return ( | ||
Math.max(repetitionLength, repeaterLength) + | ||
EXTRA_DIAGRAM_PADDING * 2 + | ||
(production.skippable ? EXTRA_DIAGRAM_PADDING * 2 : 0) | ||
); | ||
} | ||
if (production.optional) { | ||
return ( | ||
determineDiagramSequenceLength(production.optional) + | ||
EXTRA_DIAGRAM_PADDING * 2 | ||
); | ||
} | ||
return 0; | ||
}; | ||
const SHRINK_CHOICE = 10; | ||
@@ -70,2 +125,34 @@ | ||
if (production.sequence) { | ||
const sequenceLength = determineDiagramSequenceLength(production); | ||
if (sequenceLength > 45) { | ||
const subSequences = production.sequence | ||
.reduce( | ||
(totals, elem, index, list) => { | ||
const lastList = totals.slice(-1)[0]; | ||
lastList.push(elem); | ||
const currentLength = determineDiagramSequenceLength({ | ||
sequence: lastList | ||
}); | ||
const remainingLength = determineDiagramSequenceLength({ | ||
sequence: list.slice(index + 1) | ||
}); | ||
if ( | ||
currentLength + remainingLength > 40 && | ||
currentLength >= 25 && | ||
remainingLength > 10 | ||
) { | ||
totals.push([]); | ||
} | ||
return totals; | ||
}, | ||
[[]] | ||
) | ||
.filter(array => array.length > 0); | ||
return Stack( | ||
...subSequences.map(subSequence => | ||
Sequence(...subSequence.map(productionToDiagram)) | ||
) | ||
); | ||
} | ||
return Sequence(...production.sequence.map(productionToDiagram)); | ||
@@ -125,11 +212,23 @@ } | ||
const createTocStructure = tocData => | ||
const createTocStructure = (tocData, metadata) => | ||
tocData | ||
.map( | ||
tocNode => | ||
`<li><a href="#${dasherize( | ||
`<li${ | ||
metadata[tocNode.name].root | ||
? ' class="root-node"' | ||
: metadata[tocNode.name].common | ||
? ' class="common-node"' | ||
: "" | ||
}><a href="#${dasherize( | ||
tocNode.name.trim() | ||
)}">${tocNode.name.trim()} ${tocNode.recursive ? "↖︎" : ""}</a>${ | ||
)}">${tocNode.name.trim()}</a> | ||
${ | ||
metadata[tocNode.name].recursive | ||
? '<dfn title="recursive">♺</dfn>' | ||
: "" | ||
} | ||
${ | ||
tocNode.children | ||
? `<ul>${createTocStructure(tocNode.children)}</ul>` | ||
? `<ul>${createTocStructure(tocNode.children, metadata)}</ul>` | ||
: "" | ||
@@ -173,4 +272,13 @@ } | ||
const alphabeticalToc = createTocStructure(createAlphabeticalToc(ast)); | ||
const hierarchicalToc = createTocStructure(createStructuralToc(ast)); | ||
const structuralToc = createStructuralToc(ast); | ||
const metadata = createDefinitionMetadata(structuralToc); | ||
const alphabetical = createAlphabeticalToc(ast); | ||
const rootItems = alphabetical.filter(item => metadata[item.name].root); | ||
const commonItems = alphabetical.filter( | ||
item => !metadata[item.name].root && metadata[item.name].common | ||
); | ||
const otherItems = alphabetical.filter( | ||
item => !metadata[item.name].root && !metadata[item.name].common | ||
); | ||
const hierarchicalToc = createTocStructure(structuralToc, metadata); | ||
@@ -180,4 +288,9 @@ const htmlContent = documentContent({ | ||
contents, | ||
alphabeticalToc, | ||
hierarchicalToc | ||
singleRoot: rootItems.length === 1, | ||
toc: { | ||
hierarchical: hierarchicalToc, | ||
common: createTocStructure(commonItems, metadata), | ||
roots: createTocStructure(rootItems, metadata), | ||
other: createTocStructure(otherItems, metadata) | ||
} | ||
}); | ||
@@ -184,0 +297,0 @@ return options.full !== false |
@@ -26,127 +26,299 @@ const { Converter } = require("showdown"); | ||
const documentContent = ({ | ||
title, | ||
contents, | ||
alphabeticalToc, | ||
hierarchicalToc | ||
}) => | ||
const documentContent = ({ title, contents, toc, singleRoot }) => | ||
`<header> | ||
<h1>${title}</h1> | ||
<button type="button"></button> | ||
</header> | ||
<main> | ||
<nav> | ||
<h3>Root element${singleRoot ? "" : "s"}:</h3> | ||
<ul class="nav-alphabetical"> | ||
${toc.roots} | ||
</ul> | ||
<h3>Quick navigation:</h3> | ||
<ul class="nav-alphabetical"> | ||
${alphabeticalToc} | ||
${toc.other} | ||
</ul> | ||
<h3>Common elements:</h3> | ||
<ul class="nav-alphabetical"> | ||
${toc.common} | ||
</ul> | ||
</nav> | ||
<main> | ||
<article> | ||
${contents} | ||
</article> | ||
<nav> | ||
<h3>Language overview</h3> | ||
<ul class="nav-hierarchical"> | ||
${hierarchicalToc} | ||
</ul> | ||
</nav> | ||
</main>`; | ||
</main> | ||
<script type="text/javascript"> | ||
document.querySelector("header button").addEventListener("click", function() { | ||
document.getElementsByTagName("html")[0].classList.toggle("menu-open"); | ||
}); | ||
document.querySelector("nav").addEventListener("click", function(event) { | ||
if (event.target.tagName !== "A") return; | ||
document.getElementsByTagName("html")[0].classList.remove("menu-open"); | ||
}); | ||
</script> | ||
`; | ||
const documentStyle = () => | ||
`/* Text styling */ | ||
body { | ||
font: normal 12px Verdana, sans-serif; | ||
color: #0F0C00; | ||
background: #FFFCFC; | ||
} | ||
h1 { font-size: 2em; } | ||
h2 { font-size: 1.5em; } | ||
a, | ||
a:visited, | ||
a:active { | ||
color: #0F0C00; | ||
} | ||
a:hover { | ||
color: #000; | ||
} | ||
section h4 { | ||
margin-bottom: 0; | ||
} | ||
` | ||
html { | ||
box-sizing: border-box; | ||
} | ||
/* EBNF text representation styling */ | ||
code.ebnf { | ||
padding: 1em 1em 1em 1em; | ||
background: rgb(255, 246, 209); | ||
font-weight: bold; | ||
color: #777; | ||
*, *:before, *:after { | ||
box-sizing: inherit; | ||
} | ||
:root { | ||
--subtleText: #777; | ||
--highlightText: hotpink; | ||
--itemHeadingBackground: #eee; | ||
--diagramBackground: #f8f8f8; | ||
} | ||
html { | ||
font-family: sans-serif; | ||
} | ||
html, body { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
a { | ||
color: inherit; | ||
} | ||
a:visited { | ||
color: var(--subtleText); | ||
} | ||
a:active, a:focus, a:hover { | ||
color: var(--highlightText); | ||
} | ||
header { | ||
border-bottom: 1px solid #ccc; | ||
padding: 1rem; | ||
} | ||
header button { | ||
display: none; | ||
} | ||
main { | ||
display: flex; | ||
overflow: hidden; | ||
margin-left: 300px; | ||
} | ||
nav { | ||
position: sticky; | ||
top: 0; | ||
height: 100vh; | ||
padding: 1rem 2rem 1rem 1rem; | ||
z-index: 5; | ||
background: white; | ||
width: 300px; | ||
float: left; | ||
overflow: auto; | ||
} | ||
nav h3 { | ||
white-space: nowrap; | ||
} | ||
nav ul { | ||
list-style: none; | ||
padding: 0; | ||
} | ||
nav a { | ||
display: inline-block; | ||
color: var(--subtleText); | ||
text-decoration: none; | ||
padding: 0.33rem 0; | ||
} | ||
article { | ||
width: 100%; | ||
overflow: hidden; | ||
padding: 1rem 2rem; | ||
border-left: 1px solid #ccc; | ||
} | ||
code { | ||
width: 100%; | ||
} | ||
pre { | ||
overflow: auto; | ||
} | ||
pre > code { | ||
display: block; | ||
padding: 1em; | ||
background: var(--diagramBackground); | ||
} | ||
h4 { | ||
padding: 2rem; | ||
margin: 4rem -2rem 1rem -2rem; | ||
background: var(--itemHeadingBackground); | ||
font-size: 125%; | ||
} | ||
dfn { | ||
font-style: normal; | ||
cursor: default; | ||
} | ||
.diagram-container { | ||
background: var(--diagramBackground); | ||
margin-bottom: 0.25rem; | ||
padding: 1rem 0; | ||
display: flex; | ||
justify-content: center; | ||
overflow: auto; | ||
} | ||
/* Responsiveness */ | ||
@media (max-width: 640px) { | ||
header { | ||
padding: 0.5rem 1rem; | ||
display: flex; | ||
} | ||
code.ebnf pre { | ||
margin: 0; | ||
header h1 { | ||
margin: 0 auto 0 0; | ||
display: flex; | ||
align-items: center; | ||
} | ||
.ebnf-identifier { | ||
color: #990099; | ||
header button { | ||
display: initial; | ||
position: relative; | ||
z-index: 10; | ||
} | ||
.ebnf-terminal { | ||
color: #009900; | ||
header button::after { | ||
content: '☰'; | ||
margin-left: auto; | ||
font-size: 1.5rem; | ||
} | ||
.ebnf-non-terminal { | ||
font-weight: normal; | ||
main { | ||
display: block; | ||
position: relative; | ||
margin-left: 0; | ||
} | ||
.ebnf-comment { | ||
font-weight: normal; | ||
font-style: italic; | ||
color: #999; | ||
nav { | ||
height: auto; | ||
display: block; | ||
pointer-events: none; | ||
opacity: 0; | ||
transition: opacity 0.2s; | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
padding-top: 3rem; | ||
background: white; | ||
box-shadow: 0 0 0 1000000rem rgba(0, 0, 0, 0.35); | ||
} | ||
/* EBNF diagram representation styling */ | ||
svg.railroad-diagram path { | ||
stroke-width: 3; | ||
stroke: black; | ||
fill: rgba(0,0,0,0); | ||
.menu-open nav { | ||
pointer-events: auto; | ||
opacity: 1; | ||
} | ||
svg.railroad-diagram text { | ||
font: bold 14px monospace; | ||
text-anchor: middle; | ||
nav a { | ||
padding: 0.66rem 0; | ||
} | ||
svg.railroad-diagram text.diagram-text { | ||
font-size: 12px; | ||
article { | ||
margin-left: 0; | ||
border-left: 0; | ||
padding: 1rem; | ||
} | ||
svg.railroad-diagram text.diagram-arrow { | ||
font-size: 16px; | ||
} | ||
svg.railroad-diagram text.label { | ||
text-anchor: start; | ||
} | ||
svg.railroad-diagram text.comment { | ||
font: italic 12px monospace; | ||
} | ||
svg.railroad-diagram g.non-terminal text { | ||
/*font-style: italic;*/ | ||
} | ||
svg.railroad-diagram g.special-sequence rect { | ||
fill: #FFDB4D; | ||
} | ||
svg.railroad-diagram g.special-sequence text { | ||
font-style: italic; | ||
} | ||
svg.railroad-diagram rect { | ||
stroke-width: 3; | ||
stroke: black; | ||
} | ||
svg.railroad-diagram g.non-terminal rect { | ||
fill: hsl(120,100%,90%); | ||
} | ||
svg.railroad-diagram g.terminal rect { | ||
fill: hsl(120,100%,90%); | ||
} | ||
svg.railroad-diagram path.diagram-text { | ||
stroke-width: 3; | ||
stroke: black; | ||
fill: white; | ||
cursor: help; | ||
} | ||
svg.railroad-diagram g.diagram-text:hover path.diagram-text { | ||
fill: #eee; | ||
} | ||
} | ||
/* EBNF text representation styling */ | ||
code.ebnf { | ||
padding: 1em; | ||
background: rgb(255, 246, 209); | ||
font-weight: bold; | ||
color: #777; | ||
white-space: pre-wrap; | ||
display: inline-block; | ||
width: 100%; | ||
} | ||
.ebnf-identifier { | ||
color: #990099; | ||
} | ||
.ebnf-terminal { | ||
color: #009900; | ||
} | ||
.ebnf-non-terminal { | ||
font-weight: normal; | ||
} | ||
.ebnf-comment { | ||
font-weight: normal; | ||
font-style: italic; | ||
color: #999; | ||
} | ||
/* EBNF diagram representation styling */ | ||
svg.railroad-diagram { | ||
width: 100%; | ||
} | ||
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.diagram-text { | ||
font-size: 12px; | ||
} | ||
svg.railroad-diagram text.diagram-arrow { | ||
font-size: 16px; | ||
} | ||
svg.railroad-diagram text.label { | ||
text-anchor: start; | ||
} | ||
svg.railroad-diagram text.comment { | ||
font: italic 12px monospace; | ||
} | ||
svg.railroad-diagram g.non-terminal text { | ||
/*font-style: italic;*/ | ||
} | ||
svg.railroad-diagram g.special-sequence rect { | ||
fill: #FFDB4D; | ||
} | ||
svg.railroad-diagram g.special-sequence text { | ||
font-style: italic; | ||
} | ||
svg.railroad-diagram rect { | ||
stroke-width: 3; | ||
stroke: black; | ||
} | ||
svg.railroad-diagram g.non-terminal rect { | ||
fill: hsl(120,100%,90%); | ||
} | ||
svg.railroad-diagram g.terminal rect { | ||
fill: hsl(120,100%,90%); | ||
} | ||
svg.railroad-diagram path.diagram-text { | ||
stroke-width: 3; | ||
stroke: black; | ||
fill: white; | ||
cursor: help; | ||
} | ||
svg.railroad-diagram g.diagram-text:hover path.diagram-text { | ||
fill: #eee; | ||
} | ||
`; | ||
@@ -193,3 +365,3 @@ | ||
${diagram} </div> | ||
<code class="ebnf"><pre>${ebnf}</pre></code>${(referencedBy.length > 0 | ||
<code class="ebnf">${ebnf}</code>${(referencedBy.length > 0 | ||
? "\n " + referencesTemplate(identifier, referencedBy) | ||
@@ -196,0 +368,0 @@ : "") + |
@@ -92,5 +92,40 @@ const { | ||
const createDefinitionMetadata = (structuralToc, level = 0) => { | ||
const metadata = {}; | ||
structuralToc.forEach(item => { | ||
const data = metadata[item.name] || { counted: 0 }; | ||
if (level === 0) { | ||
data["root"] = true; | ||
} | ||
if (item.recursive) { | ||
data["recursive"] = true; | ||
} | ||
data["counted"]++; | ||
metadata[item.name] = data; | ||
if (item.children) { | ||
const childData = createDefinitionMetadata(item.children, level + 1); | ||
Object.entries(childData).forEach(([name, cData]) => { | ||
const data = metadata[name] || { counted: 0 }; | ||
metadata[name] = { | ||
...data, | ||
...cData, | ||
counted: cData.counted + data.counted | ||
}; | ||
}); | ||
} | ||
}); | ||
const values = Object.values(metadata); | ||
const total = values.reduce((acc, item) => acc + item.counted, 0); | ||
const average = total / values.length; | ||
Object.entries(metadata).forEach(([varName, value]) => { | ||
metadata[varName].common = value.counted > average; | ||
}); | ||
return metadata; | ||
}; | ||
module.exports = { | ||
createAlphabeticalToc, | ||
createDefinitionMetadata, | ||
createStructuralToc | ||
}; |
84110
2217