ebnf2railroad
Version:
EBNF to Railroad diagram
505 lines (451 loc) • 10.4 kB
JavaScript
const { Converter } = require("showdown");
const { dedent } = require("./dedent");
const converter = new Converter({
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
tables: true,
});
const documentFrame = ({ head, body, title }) =>
`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="generator" content="ebnf2railroad" />
<title>${title}</title>
${head}
</head>
<body>
${body}
</body>
</html>
`;
const currentDate = () => {
const date = new Date();
const pad = (number) => (number < 10 ? "0" + number : number);
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(
date.getUTCDate()
)}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
};
const navigation = ({ title, content }) =>
content.length === 0
? ""
: `
<h3>${title}:</h3>
<ul class="nav-alphabetical">
${content}
</ul>
`;
const documentContent = ({ title, contents, toc, singleRoot }) =>
`
<script type="text/javascript">
const htmlTag = document.getElementsByTagName("html")[0];
const options = (document.location.search || "")
.slice(1)
.split("&")
.map(kv => kv.split("="))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
if (options["theme"]) {
htmlTag.classList.add("theme-" + options["theme"]);
}
</script>
<header>
<h1>${title}</h1>
<button type="button"></button>
</header>
<nav>
${navigation({
title: singleRoot ? "Root element" : "Root elements",
content: toc.roots,
})}
${navigation({
title: "Quick navigation",
content: toc.other,
})}
${navigation({
title: "Common elements",
content: toc.common,
})}
${navigation({
title: "Character sets",
content: toc.characterSets,
})}
</nav>
<main>
<article>
${contents}
</article>
<footer>
<p>Date: ${currentDate()} - <a href="?theme=dark" data-theme="dark">Dark</a> - <a href="?theme=light" data-theme="light">Light</a></p>
</footer>
</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;
htmlTag.classList.remove("menu-open");
});
document.querySelectorAll("footer a").forEach(element =>
element.addEventListener("click", function(event) {
event.preventDefault();
const theme = event.target.getAttribute("data-theme");
const remove = Array.from(htmlTag.classList).filter(className => className.startsWith("theme-"));
remove.forEach(name => htmlTag.classList.remove(name));
htmlTag.classList.add("theme-" + theme);
})
);
</script>
`;
const documentStyle = () =>
`
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
:root {
--subtleText: #777;
--highlightText: hotpink;
--itemHeadingBackground: #eee;
--background: white;
--borderColor: #ccc;
--textColor: #111;
--diagramBackground: #f8f8f8;
--diagramLines: black;
--diagramText: black;
--terminalLines: black;
--terminalFill: #feffdf;
--nonTerminalLines: black;
--nonTerminalFill: #feffdf;
--specialSequenceLines: black;
--specialSequenceFill: #ffe79a;
--ebnfCodeBackground: #e8e8e8;
--ebnfIdentifier: #ef5a5a;
--ebnfTerminal: #ffa952;
--ebnfBaseColor: #777;
}
.theme-dark {
--subtleText: #777;
--highlightText: hotpink;
--itemHeadingBackground: #444;
--background: #333;
--borderColor: lightblue;
--textColor: #ddd;
--diagramBackground: #222;
--diagramLines: #3e432e;
--diagramText: #a7d129;
--terminalLines: #a7d129;
--terminalFill: #3e432e;
--nonTerminalLines: #a7d129;
--nonTerminalFill: #3e432e;
--specialSequenceLines: #a7d129;
--specialSequenceFill: #444;
--ebnfCodeBackground: #3e432e;
--ebnfIdentifier: lightblue;
--ebnfTerminal: #a7d129;
--ebnfBaseColor: #ddd;
}
html {
font-family: sans-serif;
}
html, body {
margin: 0;
padding: 0;
background: var(--background);
color: var(--textColor);
}
a {
color: inherit;
}
a:visited {
color: var(--subtleText);
}
a:active, a:focus, a:hover {
color: var(--highlightText);
}
header {
border-bottom: 1px solid var(--borderColor);
padding: 1rem;
}
header button {
display: none;
}
main {
overflow: hidden;
margin-left: 300px;
}
nav {
position: sticky;
top: 0;
max-height: 100vh;
padding: 1rem 2rem 1rem 1rem;
z-index: 5;
background: var(--background);
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 var(--borderColor);
}
article + footer {
padding: 1rem 2rem;
border-left: 1px solid var(--borderColor);
background: var(--itemHeadingBackground);
}
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%;
}
blockquote {
margin-left: 0;
margin-top: calc(1em - 1px);
margin-bottom: calc(1em - 1px);
padding: 1px 0 1px 1rem;
border-left: 1rem solid var(--ebnfCodeBackground);
}
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: 800px) {
body {
overflow-x: hidden;
}
header {
padding: 0.5rem 1rem;
display: flex;
}
header h1 {
margin: 0 auto 0 0;
display: flex;
align-items: center;
}
header button {
display: initial;
position: relative;
z-index: 10;
}
header button::after {
content: '☰';
margin-left: auto;
font-size: 1.5rem;
}
main {
display: block;
position: relative;
margin-left: 0;
}
nav {
height: auto;
display: block;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
position: absolute;
top: 0;
right: 0;
transform: translateX(300px);
padding-top: 3rem;
background: var(--background);
box-shadow: 0 0 0 1000000rem rgba(0, 0, 0, 0.35);
}
.menu-open nav {
pointer-events: auto;
opacity: 1;
transform: translateX(0px);
}
nav a {
padding: 0.66rem 0;
}
article {
margin-left: 0;
border-left: 0;
padding: 1rem;
}
article + footer {
padding: 1rem;
border-left: 0;
}
}
/* EBNF text representation styling */
code.ebnf {
padding: 1em;
background: var(--ebnfCodeBackground);
font-weight: bold;
color: var(--ebnfBaseColor);
white-space: pre-wrap;
display: inline-block;
width: 100%;
}
.ebnf-identifier {
color: var(--ebnfIdentifier);
}
.ebnf-terminal {
color: var(--ebnfTerminal);
}
.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: var(--diagramLines);
fill: rgba(0,0,0,0);
}
svg.railroad-diagram text {
font: bold 14px monospace;
text-anchor: middle;
fill: var(--diagramText);
}
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: var(--specialSequenceFill);
stroke: var(--specialSequenceLines);
}
svg.railroad-diagram g.special-sequence text {
font-style: italic;
}
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);
}
svg.railroad-diagram g.terminal rect {
fill: var(--terminalFill);
stroke: var(--terminalLines);
}
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;
}
`;
const dasherize = (str) => str.replace(/\s+/g, "-");
const referencesTemplate = (identifier, references) =>
`<p>Items referencing <strong>${identifier}</strong>:<p>
<ul>
${references
.map(
(reference) =>
`<li><a href="#${dasherize(
reference.trim()
)}">${reference.trim()}</a></li>`
)
.join("")}
</ul>`;
const referencesToTemplate = (identifier, references) =>
`<p><strong>${identifier}</strong> is referencing:<p>
<ul>
${references
.map(
(reference) =>
`<li><a href="#${dasherize(
reference.trim()
)}">${reference.trim()}</a></li>`
)
.join("")}
</ul>`;
const ebnfTemplate = ({
identifier,
ebnf,
diagram,
referencedBy,
referencesTo,
}) =>
`<section>
<h4 id="${dasherize(identifier)}">${identifier}</h4>
<div class="diagram-container">${diagram}</div>
<code class="ebnf">${ebnf}</code>${
(referencedBy.length > 0
? "\n " + referencesTemplate(identifier, referencedBy)
: "") +
(referencesTo.length > 0
? "\n " + referencesToTemplate(identifier, referencesTo)
: "")
}
</section>
`;
const commentTemplate = (comment) => converter.makeHtml(dedent(comment));
module.exports = {
documentContent,
documentFrame,
documentStyle,
ebnfTemplate,
commentTemplate,
};