From c26acf5f8caef66045847120e1c3427a3dd30216 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 7 Oct 2024 21:57:29 -0700 Subject: [PATCH] Bring in existing railroad support JS --- assets/js/railroad.js | 1461 ++--------------------------------------- 1 file changed, 52 insertions(+), 1409 deletions(-) diff --git a/assets/js/railroad.js b/assets/js/railroad.js index ab8f126..9612b60 100644 --- a/assets/js/railroad.js +++ b/assets/js/railroad.js @@ -1,1420 +1,63 @@ -"use strict"; -/* -Railroad Diagrams -by Tab Atkins Jr. (and others) -http://xanthir.com -http://twitter.com/tabatkins -http://github.com/tabatkins/railroad-diagrams +import rr from "./lib/railroad.js" -This document and all associated files in the github project are licensed under CC0: http://creativecommons.org/publicdomain/zero/1.0/ -This means you can reuse, remix, or otherwise appropriate this project for your own use WITHOUT RESTRICTION. -(The actual legal meaning can be found at the above link.) -Don't ask me for permission to use any part of this project, JUST USE IT. -I would appreciate attribution, but that is not required by the license. -*/ +class RailroadDiagramManager { + constructor() { + this.figures = new Map(); + this.isCurrentlyNarrow = undefined; -// Export function versions of all the constructors. -// Each class will add itself to this object. -const funcs = {}; -export default funcs; + this.diagramBreakpoint = window.matchMedia("(max-width: 450px)"); + this.diagramBreakpoint.addEventListener("change", () => { + this.updateVisiblity(); + }); + } -export const Options = { - DEBUG: false, // if true, writes some debug information into attributes - VS: 11, // minimum vertical separation between things. For a 3px stroke, must be at least 4 - AR: 11, // radius of arcs - DIAGRAM_CLASS: 'railroad-diagram', // class to put on the root - STROKE_ODD_PIXEL_LENGTH: false, // is the stroke width an odd (1px, 3px, etc) pixel length? - INTERNAL_ALIGNMENT: 'center', // how to align items when they have extra space. left/right/center - CHAR_WIDTH: 11, // width of each monospace character. play until you find the right value for your font - COMMENT_CHAR_WIDTH: 7, // comments are in smaller text by default -}; + add(svg) { + const parent = svg.parentElement; + if (!this.figures.has(parent)) { + this.figures.set(parent, []); + } -export const defaultCSS = ` - svg { - background-color: hsl(30,20%,95%); - } - path { - stroke-width: 3; - stroke: black; - fill: rgba(0,0,0,0); - } - text { - font: bold 14px monospace; - text-anchor: middle; - white-space: pre; - } - text.diagram-text { - font-size: 12px; - } - text.diagram-arrow { - font-size: 16px; - } - text.label { - text-anchor: start; - } - text.comment { - font: italic 12px monospace; - } - g.non-terminal text { - /*font-style: italic;*/ - } - rect { - stroke-width: 3; - stroke: black; - fill: hsl(120,100%,90%); - } - rect.group-box { - stroke: gray; - stroke-dasharray: 10 5; - fill: none; - } - path.diagram-text { - stroke-width: 3; - stroke: black; - fill: white; - cursor: help; - } - g.diagram-text:hover path.diagram-text { - fill: #eee; - }`; + this.figures.get(parent).push(svg); + parent.removeChild(svg); + } -export class FakeSVG { - constructor(tagName, attrs, text) { - if(text) this.children = text; - else this.children = []; - this.tagName = tagName; - this.attrs = unnull(attrs, {}); - } - format(x, y, width) { - // Virtual - } - addTo(parent) { - if(parent instanceof FakeSVG) { - parent.children.push(this); - return this; - } else { - var svg = this.toSVG(); - parent.appendChild(svg); - return svg; - } - } - toSVG() { - var el = SVG(this.tagName, this.attrs); - if(typeof this.children == 'string') { - el.textContent = this.children; - } else { - this.children.forEach(function(e) { - el.appendChild(e.toSVG()); - }); - } - return el; - } - toString() { - var str = '<' + this.tagName; - var group = this.tagName == "g" || this.tagName == "svg"; - for(var attr in this.attrs) { - str += ' ' + attr + '="' + (this.attrs[attr]+'').replace(/&/g, '&').replace(/"/g, '"') + '"'; - } - str += '>'; - if(group) str += "\n"; - if(typeof this.children == 'string') { - str += escapeString(this.children); - } else { - this.children.forEach(function(e) { - str += e; - }); - } - str += '\n'; - return str; - } - walk(cb) { - cb(this); - } + updateVisiblity() { + const isNarrow = this.diagramBreakpoint.matches; + + if (isNarrow === this.isCurrentlyNarrow) { + return; + } + + this.isCurrentlyNarrow = isNarrow; + + for (let [figure, svgs] of this.figures.entries()) { + for (let svg of svgs) { + const svgHasNarrowClass = svg.classList.contains("narrow"); + if (isNarrow && svgHasNarrowClass) + figure.appendChild(svg); + else if (!isNarrow && !svgHasNarrowClass) + figure.appendChild(svg); + else if (svg.parentElement === figure) + figure.removeChild(svg); + } + } + } } +let railroadDiagramManager = new RailroadDiagramManager(); -export class Path extends FakeSVG { - constructor(x,y) { - super('path'); - this.attrs.d = "M"+x+' '+y; - } - m(x,y) { - this.attrs.d += 'm'+x+' '+y; - return this; - } - h(val) { - this.attrs.d += 'h'+val; - return this; - } - right(val) { return this.h(Math.max(0, val)); } - left(val) { return this.h(-Math.max(0, val)); } - v(val) { - this.attrs.d += 'v'+val; - return this; - } - down(val) { return this.v(Math.max(0, val)); } - up(val) { return this.v(-Math.max(0, val)); } - arc(sweep){ - // 1/4 of a circle - var x = Options.AR; - var y = Options.AR; - if(sweep[0] == 'e' || sweep[1] == 'w') { - x *= -1; - } - if(sweep[0] == 's' || sweep[1] == 'n') { - y *= -1; - } - var cw; - if(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 'wn') { - cw = 1; - } else { - cw = 0; - } - this.attrs.d += "a"+Options.AR+" "+Options.AR+" 0 0 "+cw+' '+x+' '+y; - return this; - } - arc_8(start, dir) { - // 1/8 of a circle - const arc = Options.AR; - const s2 = 1/Math.sqrt(2) * arc; - const s2inv = (arc - s2); - let path = "a " + arc + " " + arc + " 0 0 " + (dir=='cw' ? "1" : "0") + " "; - const sd = start+dir; - const offset = - sd == 'ncw' ? [s2, s2inv] : - sd == 'necw' ? [s2inv, s2] : - sd == 'ecw' ? [-s2inv, s2] : - sd == 'secw' ? [-s2, s2inv] : - sd == 'scw' ? [-s2, -s2inv] : - sd == 'swcw' ? [-s2inv, -s2] : - sd == 'wcw' ? [s2inv, -s2] : - sd == 'nwcw' ? [s2, -s2inv] : - sd == 'nccw' ? [-s2, s2inv] : - sd == 'nwccw' ? [-s2inv, s2] : - sd == 'wccw' ? [s2inv, s2] : - sd == 'swccw' ? [s2, s2inv] : - sd == 'sccw' ? [s2, -s2inv] : - sd == 'seccw' ? [s2inv, -s2] : - sd == 'eccw' ? [-s2inv, -s2] : - sd == 'neccw' ? [-s2, -s2inv] : null - ; - path += offset.join(" "); - this.attrs.d += path; - return this; - } - l(x, y) { - this.attrs.d += 'l'+x+' '+y; - return this; - } - format() { - // All paths in this library start/end horizontally. - // The extra .5 ensures a minor overlap, so there's no seams in bad rasterizers. - this.attrs.d += 'h.5'; - return this; - } +export function railroadDiagram(builder, elementID, isNarrow) { + const diagram = builder(rr); + const svg = diagram.addTo(document.getElementById(elementID)); + + if (isNarrow) { + svg.classList.add("narrow"); + } + + railroadDiagramManager.add(svg); } - -export class DiagramMultiContainer extends FakeSVG { - constructor(tagName, items, attrs, text) { - super(tagName, attrs, text); - this.items = items.map(wrapString); - } - walk(cb) { - cb(this); - this.items.forEach(x=>x.walk(cb)); - } -} - - -export class Diagram extends DiagramMultiContainer { - constructor(...items) { - super('svg', items, {class: Options.DIAGRAM_CLASS}); - if(!(this.items[0] instanceof Start)) { - this.items.unshift(new Start()); - } - if(!(this.items[this.items.length-1] instanceof End)) { - this.items.push(new End()); - } - this.up = this.down = this.height = this.width = 0; - for(const item of this.items) { - this.width += item.width + (item.needsSpace?20:0); - this.up = Math.max(this.up, item.up - this.height); - this.height += item.height; - this.down = Math.max(this.down - item.height, item.down); - } - this.formatted = false; - } - format(paddingt, paddingr, paddingb, paddingl) { - paddingt = unnull(paddingt, 20); - paddingr = unnull(paddingr, paddingt, 20); - paddingb = unnull(paddingb, paddingt, 20); - paddingl = unnull(paddingl, paddingr, 20); - var x = paddingl; - var y = paddingt; - y += this.up; - var g = new FakeSVG('g', Options.STROKE_ODD_PIXEL_LENGTH ? {transform:'translate(.5 .5)'} : {}); - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - if(item.needsSpace) { - new Path(x,y).h(10).addTo(g); - x += 10; - } - item.format(x, y, item.width).addTo(g); - x += item.width; - y += item.height; - if(item.needsSpace) { - new Path(x,y).h(10).addTo(g); - x += 10; - } - } - this.attrs.width = this.width + paddingl + paddingr; - this.attrs.height = this.up + this.height + this.down + paddingt + paddingb; - this.attrs.viewBox = "0 0 " + this.attrs.width + " " + this.attrs.height; - g.addTo(this); - this.formatted = true; - return this; - } - addTo(parent) { - if(!parent) { - var scriptTag = document.getElementsByTagName('script'); - scriptTag = scriptTag[scriptTag.length - 1]; - parent = scriptTag.parentNode; - } - return super.addTo.call(this, parent); - } - toSVG() { - if(!this.formatted) { - this.format(); - } - return super.toSVG.call(this); - } - toString() { - if(!this.formatted) { - this.format(); - } - return super.toString.call(this); - } - toStandalone(style) { - if(!this.formatted) { - this.format(); - } - const s = new FakeSVG('style', {}, style || defaultCSS); - this.children.push(s); - this.attrs.xmlns = "http://www.w3.org/2000/svg"; - this.attrs['xmlns:xlink'] = "http://www.w3.org/1999/xlink"; - const result = super.toString.call(this); - this.children.pop(); - delete this.attrs.xmlns; - return result; - } -} -funcs.Diagram = (...args)=>new Diagram(...args); - - -export class ComplexDiagram extends FakeSVG { - constructor(...items) { - var diagram = new Diagram(...items); - diagram.items[0] = new Start({type:"complex"}); - diagram.items[diagram.items.length-1] = new End({type:"complex"}); - return diagram; - } -} -funcs.ComplexDiagram = (...args)=>new ComplexDiagram(...args); - - -export class Sequence extends DiagramMultiContainer { - constructor(...items) { - super('g', items); - var numberOfItems = this.items.length; - this.needsSpace = true; - this.up = this.down = this.height = this.width = 0; - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - this.width += item.width + (item.needsSpace?20:0); - this.up = Math.max(this.up, item.up - this.height); - this.height += item.height; - this.down = Math.max(this.down - item.height, item.down); - } - if(this.items[0].needsSpace) this.width -= 10; - if(this.items[this.items.length-1].needsSpace) this.width -= 10; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "sequence"; - } - } - format(x,y,width) { - // Hook up the two sides if this is narrower than its stated 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]; - - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - if(item.needsSpace && i > 0) { - new Path(x,y).h(10).addTo(this); - x += 10; - } - item.format(x, y, item.width).addTo(this); - x += item.width; - y += item.height; - if(item.needsSpace && i < this.items.length-1) { - new Path(x,y).h(10).addTo(this); - x += 10; - } - } - return this; - } -} -funcs.Sequence = (...args)=>new Sequence(...args); - - -export class Stack extends DiagramMultiContainer { - constructor(...items) { - super('g', items); - if( items.length === 0 ) { - throw new RangeError("Stack() must have at least one child."); - } - this.width = Math.max.apply(null, this.items.map(function(e) { return e.width + (e.needsSpace?20:0); })); - //if(this.items[0].needsSpace) this.width -= 10; - //if(this.items[this.items.length-1].needsSpace) this.width -= 10; - if(this.items.length > 1){ - this.width += Options.AR*2; - } - this.needsSpace = true; - this.up = this.items[0].up; - this.down = this.items[this.items.length-1].down; - - this.height = 0; - var last = this.items.length - 1; - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - this.height += item.height; - if(i > 0) { - this.height += Math.max(Options.AR*2, item.up + Options.VS); - } - if(i < last) { - this.height += Math.max(Options.AR*2, item.down + Options.VS); - } - } - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "stack"; - } - } - format(x,y,width) { - var gaps = determineGaps(width, this.width); - new Path(x,y).h(gaps[0]).addTo(this); - x += gaps[0]; - var xInitial = x; - if(this.items.length > 1) { - new Path(x, y).h(Options.AR).addTo(this); - x += Options.AR; - } - - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - var innerWidth = this.width - (this.items.length>1 ? Options.AR*2 : 0); - item.format(x, y, innerWidth).addTo(this); - x += innerWidth; - y += item.height; - - if(i !== this.items.length-1) { - new Path(x, y) - .arc('ne').down(Math.max(0, item.down + Options.VS - Options.AR*2)) - .arc('es').left(innerWidth) - .arc('nw').down(Math.max(0, this.items[i+1].up + Options.VS - Options.AR*2)) - .arc('ws').addTo(this); - y += Math.max(item.down + Options.VS, Options.AR*2) + Math.max(this.items[i+1].up + Options.VS, Options.AR*2); - //y += Math.max(Options.AR*4, item.down + Options.VS*2 + this.items[i+1].up) - x = xInitial+Options.AR; - } - - } - - if(this.items.length > 1) { - new Path(x,y).h(Options.AR).addTo(this); - x += Options.AR; - } - new Path(x,y).h(gaps[1]).addTo(this); - - return this; - } -} -funcs.Stack = (...args)=>new Stack(...args); - - -export class OptionalSequence extends DiagramMultiContainer { - constructor(...items) { - super('g', items); - if( items.length === 0 ) { - throw new RangeError("OptionalSequence() must have at least one child."); - } - if( items.length === 1 ) { - return new Sequence(items); - } - var arc = Options.AR; - this.needsSpace = false; - this.width = 0; - this.up = 0; - this.height = sum(this.items, function(x){return x.height}); - this.down = this.items[0].down; - var heightSoFar = 0; - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - this.up = Math.max(this.up, Math.max(arc*2, item.up + Options.VS) - heightSoFar); - heightSoFar += item.height; - if(i > 0) { - this.down = Math.max(this.height + this.down, heightSoFar + Math.max(arc*2, item.down + Options.VS)) - this.height; - } - var itemWidth = (item.needsSpace?10:0) + item.width; - if(i === 0) { - this.width += arc + Math.max(itemWidth, arc); - } else { - this.width += arc*2 + Math.max(itemWidth, arc) + arc; - } - } - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "optseq"; - } - } - format(x, y, width) { - var arc = Options.AR; - var gaps = determineGaps(width, this.width); - new Path(x, y).right(gaps[0]).addTo(this); - new Path(x + gaps[0] + this.width, y + this.height).right(gaps[1]).addTo(this); - x += gaps[0]; - var upperLineY = y - this.up; - var last = this.items.length - 1; - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - var itemSpace = (item.needsSpace?10:0); - var itemWidth = item.width + itemSpace; - if(i === 0) { - // Upper skip - new Path(x,y) - .arc('se') - .up(y - upperLineY - arc*2) - .arc('wn') - .right(itemWidth - arc) - .arc('ne') - .down(y + item.height - upperLineY - arc*2) - .arc('ws') - .addTo(this); - // Straight line - new Path(x, y) - .right(itemSpace + arc) - .addTo(this); - item.format(x + itemSpace + arc, y, item.width).addTo(this); - x += itemWidth + arc; - y += item.height; - // x ends on the far side of the first element, - // where the next element's skip needs to begin - } else if(i < last) { - // Upper skip - new Path(x, upperLineY) - .right(arc*2 + Math.max(itemWidth, arc) + arc) - .arc('ne') - .down(y - upperLineY + item.height - arc*2) - .arc('ws') - .addTo(this); - // Straight line - new Path(x,y) - .right(arc*2) - .addTo(this); - item.format(x + arc*2, y, item.width).addTo(this); - new Path(x + item.width + arc*2, y + item.height) - .right(itemSpace + arc) - .addTo(this); - // Lower skip - new Path(x,y) - .arc('ne') - .down(item.height + Math.max(item.down + Options.VS, arc*2) - arc*2) - .arc('ws') - .right(itemWidth - arc) - .arc('se') - .up(item.down + Options.VS - arc*2) - .arc('wn') - .addTo(this); - x += arc*2 + Math.max(itemWidth, arc) + arc; - y += item.height; - } else { - // Straight line - new Path(x, y) - .right(arc*2) - .addTo(this); - item.format(x + arc*2, y, item.width).addTo(this); - new Path(x + arc*2 + item.width, y + item.height) - .right(itemSpace + arc) - .addTo(this); - // Lower skip - new Path(x,y) - .arc('ne') - .down(item.height + Math.max(item.down + Options.VS, arc*2) - arc*2) - .arc('ws') - .right(itemWidth - arc) - .arc('se') - .up(item.down + Options.VS - arc*2) - .arc('wn') - .addTo(this); - } - } - return this; - } -} -funcs.OptionalSequence = (...args)=>new OptionalSequence(...args); - - -export class AlternatingSequence extends DiagramMultiContainer { - constructor(...items) { - super('g', items); - if( items.length === 1 ) { - return new Sequence(items); - } - if( items.length !== 2 ) { - throw new RangeError("AlternatingSequence() must have one or two children."); - } - this.needsSpace = false; - - const arc = Options.AR; - const vert = Options.VS; - const max = Math.max; - const first = this.items[0]; - const second = this.items[1]; - - const arcX = 1 / Math.sqrt(2) * arc * 2; - const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2; - const crossY = Math.max(arc, Options.VS); - const crossX = (crossY - arcY) + arcX; - - const firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + first.down); - this.up = firstOut + first.height + first.up; - - const secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + second.up); - this.down = secondIn + second.height + second.down; - - this.height = 0; - - const firstWidth = 2*(first.needsSpace?10:0) + first.width; - const secondWidth = 2*(second.needsSpace?10:0) + second.width; - this.width = 2*arc + max(firstWidth, crossX, secondWidth) + 2*arc; - - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "altseq"; - } - } - format(x, y, width) { - const arc = Options.AR; - const gaps = determineGaps(width, this.width); - new Path(x,y).right(gaps[0]).addTo(this); - x += gaps[0]; - new Path(x+this.width, y).right(gaps[1]).addTo(this); - // bounding box - //new Path(x+gaps[0], y).up(this.up).right(this.width).down(this.up+this.down).left(this.width).up(this.down).addTo(this); - const first = this.items[0]; - const second = this.items[1]; - - // top - const firstIn = this.up - first.up; - const firstOut = this.up - first.up - first.height; - new Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(this); - first.format(x + 2*arc, y - firstIn, this.width - 4*arc).addTo(this); - new Path(x + this.width - 2*arc, y - firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(this); - - // bottom - const secondIn = this.down - second.down - second.height; - const secondOut = this.down - second.down; - new Path(x,y).arc('ne').down(secondIn - 2*arc).arc('ws').addTo(this); - second.format(x + 2*arc, y + secondIn, this.width - 4*arc).addTo(this); - new Path(x + this.width - 2*arc, y + secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(this); - - // crossover - const arcX = 1 / Math.sqrt(2) * arc * 2; - const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2; - const crossY = Math.max(arc, Options.VS); - const crossX = (crossY - arcY) + arcX; - const crossBar = (this.width - 4*arc - crossX)/2; - new Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar) - .arc_8('n', 'cw').l(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw') - .right(crossBar).arc('ne').addTo(this); - new Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar) - .arc_8('s', 'ccw').l(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw') - .right(crossBar).arc('se').addTo(this); - - return this; - } -} -funcs.AlternatingSequence = (...args)=>new AlternatingSequence(...args); - - -export class Choice extends DiagramMultiContainer { - constructor(normal, ...items) { - super('g', items); - if( typeof normal !== "number" || normal !== Math.floor(normal) ) { - throw new TypeError("The first argument of Choice() must be an integer."); - } else if(normal < 0 || normal >= items.length) { - throw new RangeError("The first argument of Choice() must be an index for one of the items."); - } else { - this.normal = normal; - } - var first = 0; - var last = items.length - 1; - this.width = Math.max.apply(null, this.items.map(function(el){return el.width})) + Options.AR*4; - this.height = this.items[normal].height; - this.up = this.items[first].up; - var arcs; - for(var i = first; i < normal; i++) { - if(i == normal-1) arcs = Options.AR*2; - else arcs = Options.AR; - this.up += Math.max(arcs, this.items[i].height + this.items[i].down + Options.VS + this.items[i+1].up); - } - this.down = this.items[last].down; - for(i = normal+1; i <= last; i++) { - if(i == normal+1) arcs = Options.AR*2; - else arcs = Options.AR; - this.down += Math.max(arcs, this.items[i-1].height + this.items[i-1].down + Options.VS + this.items[i].up); - } - this.down -= this.items[normal].height; // already counted in Choice.height - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "choice"; - } - } - format(x,y,width) { - // Hook up the two sides if this is narrower than its stated 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]; - - var last = this.items.length -1; - var innerWidth = this.width - Options.AR*4; - - // Do the elements that curve above - var distanceFromY; - for(var i = this.normal - 1; i >= 0; i--) { - let item = this.items[i]; - if( i == this.normal - 1 ) { - distanceFromY = Math.max(Options.AR*2, this.items[this.normal].up + Options.VS + item.down + item.height); - } - new Path(x,y) - .arc('se') - .up(distanceFromY - Options.AR*2) - .arc('wn').addTo(this); - item.format(x+Options.AR*2,y - distanceFromY,innerWidth).addTo(this); - new Path(x+Options.AR*2+innerWidth, y-distanceFromY+item.height) - .arc('ne') - .down(distanceFromY - item.height + this.height - Options.AR*2) - .arc('ws').addTo(this); - distanceFromY += Math.max(Options.AR, item.up + Options.VS + (i === 0 ? 0 : this.items[i-1].down+this.items[i-1].height)); - } - - // Do the straight-line path. - new Path(x,y).right(Options.AR*2).addTo(this); - this.items[this.normal].format(x+Options.AR*2, y, innerWidth).addTo(this); - new Path(x+Options.AR*2+innerWidth, y+this.height).right(Options.AR*2).addTo(this); - - // Do the elements that curve below - for(i = this.normal+1; i <= last; i++) { - let item = this.items[i]; - if( i == this.normal + 1 ) { - distanceFromY = Math.max(Options.AR*2, this.height + this.items[this.normal].down + Options.VS + item.up); - } - new Path(x,y) - .arc('ne') - .down(distanceFromY - Options.AR*2) - .arc('ws').addTo(this); - item.format(x+Options.AR*2, y+distanceFromY, innerWidth).addTo(this); - new Path(x+Options.AR*2+innerWidth, y+distanceFromY+item.height) - .arc('se') - .up(distanceFromY - Options.AR*2 + item.height - this.height) - .arc('wn').addTo(this); - distanceFromY += Math.max(Options.AR, item.height + item.down + Options.VS + (i == last ? 0 : this.items[i+1].up)); - } - - return this; - } -} -funcs.Choice = (...args)=>new Choice(...args); - - -export class HorizontalChoice extends DiagramMultiContainer { - constructor(...items) { - super('g', items); - if( items.length === 0 ) { - throw new RangeError("HorizontalChoice() must have at least one child."); - } - if( items.length === 1) { - return new Sequence(items); - } - const allButLast = this.items.slice(0, -1); - const middles = this.items.slice(1, -1); - const first = this.items[0]; - const last = this.items[this.items.length - 1]; - this.needsSpace = false; - - this.width = Options.AR; // starting track - this.width += Options.AR*2 * (this.items.length-1); // inbetween tracks - this.width += sum(this.items, x=>x.width + (x.needsSpace?20:0)); // items - this.width += (last.height > 0 ? Options.AR : 0); // needs space to curve up - this.width += Options.AR; //ending track - - // Always exits at entrance height - this.height = 0; - - // All but the last have a track running above them - this._upperTrack = Math.max( - Options.AR*2, - Options.VS, - max(allButLast, x=>x.up) + Options.VS - ); - this.up = Math.max(this._upperTrack, last.up); - - // All but the first have a track running below them - // Last either straight-lines or curves up, so has different calculation - this._lowerTrack = Math.max( - Options.VS, - max(middles, x=>x.height+Math.max(x.down+Options.VS, Options.AR*2)), - last.height + last.down + Options.VS - ); - if(first.height < this._lowerTrack) { - // Make sure there's at least 2*AR room between first exit and lower track - this._lowerTrack = Math.max(this._lowerTrack, first.height + Options.AR*2); - } - this.down = Math.max(this._lowerTrack, first.height + first.down); - - - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "horizontalchoice"; - } - } - format(x,y,width) { - // Hook up the two sides if this is narrower than its stated 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]; - - const first = this.items[0]; - const last = this.items[this.items.length-1]; - const allButFirst = this.items.slice(1); - const allButLast = this.items.slice(0, -1); - - // upper track - var upperSpan = (sum(allButLast, x=>x.width+(x.needsSpace?20:0)) - + (this.items.length - 2) * Options.AR*2 - - Options.AR - ); - new Path(x,y) - .arc('se') - .v(-(this._upperTrack - Options.AR*2)) - .arc('wn') - .h(upperSpan) - .addTo(this); - - // lower track - var lowerSpan = (sum(allButFirst, x=>x.width+(x.needsSpace?20:0)) - + (this.items.length - 2) * Options.AR*2 - + (last.height > 0 ? Options.AR : 0) - - Options.AR - ); - var lowerStart = x + Options.AR + first.width+(first.needsSpace?20:0) + Options.AR*2; - new Path(lowerStart, y+this._lowerTrack) - .h(lowerSpan) - .arc('se') - .v(-(this._lowerTrack - Options.AR*2)) - .arc('wn') - .addTo(this); - - // Items - for(const [i, item] of enumerate(this.items)) { - // input track - if(i === 0) { - new Path(x,y) - .h(Options.AR) - .addTo(this); - x += Options.AR; - } else { - new Path(x, y - this._upperTrack) - .arc('ne') - .v(this._upperTrack - Options.AR*2) - .arc('ws') - .addTo(this); - x += Options.AR*2; - } - - // item - var itemWidth = item.width + (item.needsSpace?20:0); - item.format(x, y, itemWidth).addTo(this); - x += itemWidth; - - // output track - if(i === this.items.length-1) { - if(item.height === 0) { - new Path(x,y) - .h(Options.AR) - .addTo(this); - } else { - new Path(x,y+item.height) - .arc('se') - .addTo(this); - } - } else if(i === 0 && item.height > this._lowerTrack) { - // Needs to arc up to meet the lower track, not down. - if(item.height - this._lowerTrack >= Options.AR*2) { - new Path(x, y+item.height) - .arc('se') - .v(this._lowerTrack - item.height + Options.AR*2) - .arc('wn') - .addTo(this); - } else { - // Not enough space to fit two arcs - // so just bail and draw a straight line for now. - new Path(x, y+item.height) - .l(Options.AR*2, this._lowerTrack - item.height) - .addTo(this); - } - } else { - new Path(x, y+item.height) - .arc('ne') - .v(this._lowerTrack - item.height - Options.AR*2) - .arc('ws') - .addTo(this); - } - } - return this; - } -} -funcs.HorizontalChoice = (...args)=>new HorizontalChoice(...args); - - -export class MultipleChoice extends DiagramMultiContainer { - constructor(normal, type, ...items) { - super('g', items); - if( typeof normal !== "number" || normal !== Math.floor(normal) ) { - throw new TypeError("The first argument of MultipleChoice() must be an integer."); - } else if(normal < 0 || normal >= items.length) { - throw new RangeError("The first argument of MultipleChoice() must be an index for one of the items."); - } else { - this.normal = normal; - } - if( type != "any" && type != "all" ) { - throw new SyntaxError("The second argument of MultipleChoice must be 'any' or 'all'."); - } else { - this.type = type; - } - this.needsSpace = true; - this.innerWidth = max(this.items, function(x){return x.width}); - this.width = 30 + Options.AR + this.innerWidth + Options.AR + 20; - this.up = this.items[0].up; - this.down = this.items[this.items.length-1].down; - this.height = this.items[normal].height; - for(var i = 0; i < this.items.length; i++) { - let item = this.items[i]; - let minimum; - if(i == normal - 1 || i == normal + 1) minimum = 10 + Options.AR; - else minimum = Options.AR; - if(i < normal) { - this.up += Math.max(minimum, item.height + item.down + Options.VS + this.items[i+1].up); - } else if(i > normal) { - this.down += Math.max(minimum, item.up + Options.VS + this.items[i-1].down + this.items[i-1].height); - } - } - this.down -= this.items[normal].height; // already counted in this.height - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "multiplechoice"; - } - } - format(x, y, width) { - var gaps = determineGaps(width, this.width); - new Path(x, y).right(gaps[0]).addTo(this); - new Path(x + gaps[0] + this.width, y + this.height).right(gaps[1]).addTo(this); - x += gaps[0]; - - var normal = this.items[this.normal]; - - // Do the elements that curve above - var distanceFromY; - for(var i = this.normal - 1; i >= 0; i--) { - var item = this.items[i]; - if( i == this.normal - 1 ) { - distanceFromY = Math.max(10 + Options.AR, normal.up + Options.VS + item.down + item.height); - } - new Path(x + 30,y) - .up(distanceFromY - Options.AR) - .arc('wn').addTo(this); - item.format(x + 30 + Options.AR, y - distanceFromY, this.innerWidth).addTo(this); - new Path(x + 30 + Options.AR + this.innerWidth, y - distanceFromY + item.height) - .arc('ne') - .down(distanceFromY - item.height + this.height - Options.AR - 10) - .addTo(this); - if(i !== 0) { - distanceFromY += Math.max(Options.AR, item.up + Options.VS + this.items[i-1].down + this.items[i-1].height); - } - } - - new Path(x + 30, y).right(Options.AR).addTo(this); - normal.format(x + 30 + Options.AR, y, this.innerWidth).addTo(this); - new Path(x + 30 + Options.AR + this.innerWidth, y + this.height).right(Options.AR).addTo(this); - - for(i = this.normal+1; i < this.items.length; i++) { - let item = this.items[i]; - if(i == this.normal + 1) { - distanceFromY = Math.max(10+Options.AR, normal.height + normal.down + Options.VS + item.up); - } - new Path(x + 30, y) - .down(distanceFromY - Options.AR) - .arc('ws') - .addTo(this); - item.format(x + 30 + Options.AR, y + distanceFromY, this.innerWidth).addTo(this); - new Path(x + 30 + Options.AR + this.innerWidth, y + distanceFromY + item.height) - .arc('se') - .up(distanceFromY - Options.AR + item.height - normal.height) - .addTo(this); - if(i != this.items.length - 1) { - distanceFromY += Math.max(Options.AR, item.height + item.down + Options.VS + this.items[i+1].up); - } - } - var text = new FakeSVG('g', {"class": "diagram-text"}).addTo(this); - new FakeSVG('title', {}, (this.type=="any"?"take one or more branches, once each, in any order":"take all branches, once each, in any order")).addTo(text); - new FakeSVG('path', { - "d": "M "+(x+30)+" "+(y-10)+" h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z", - "class": "diagram-text" - }).addTo(text); - new FakeSVG('text', { - "x": x + 15, - "y": y + 4, - "class": "diagram-text" - }, (this.type=="any"?"1+":"all")).addTo(text); - new FakeSVG('path', { - "d": "M "+(x+this.width-20)+" "+(y-10)+" h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z", - "class": "diagram-text" - }).addTo(text); - new FakeSVG('path', { - "d": "M "+(x+this.width-13)+" "+(y-2)+" a 4 4 0 1 0 6 -1 m 2.75 -1 h -4 v 4 m 0 -3 h 2", - "style": "stroke-width: 1.75" - }).addTo(text); - return this; - } -} -funcs.MultipleChoice = (...args)=>new MultipleChoice(...args); - - -export class Optional extends FakeSVG { - constructor(item, skip) { - if( skip === undefined ) - return new Choice(1, new Skip(), item); - else if ( skip === "skip" ) - return new Choice(0, new Skip(), item); - else - throw "Unknown value for Optional()'s 'skip' argument."; - } -} -funcs.Optional = (...args)=>new Optional(...args); - - -export class OneOrMore extends FakeSVG { - constructor(item, rep) { - super('g'); - rep = rep || (new Skip()); - this.item = wrapString(item); - this.rep = wrapString(rep); - this.width = Math.max(this.item.width, this.rep.width) + Options.AR*2; - this.height = this.item.height; - this.up = this.item.up; - this.down = Math.max(Options.AR*2, this.item.down + Options.VS + this.rep.up + this.rep.height + this.rep.down); - this.needsSpace = true; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "oneormore"; - } - } - format(x,y,width) { - // Hook up the two sides if this is narrower than its stated 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]; - - // Draw item - new Path(x,y).right(Options.AR).addTo(this); - this.item.format(x+Options.AR,y,this.width-Options.AR*2).addTo(this); - new Path(x+this.width-Options.AR,y+this.height).right(Options.AR).addTo(this); - - // Draw repeat arc - var distanceFromY = Math.max(Options.AR*2, this.item.height+this.item.down+Options.VS+this.rep.up); - new Path(x+Options.AR,y).arc('nw').down(distanceFromY-Options.AR*2).arc('ws').addTo(this); - this.rep.format(x+Options.AR, y+distanceFromY, this.width - Options.AR*2).addTo(this); - new Path(x+this.width-Options.AR, y+distanceFromY+this.rep.height).arc('se').up(distanceFromY-Options.AR*2+this.rep.height-this.item.height).arc('en').addTo(this); - - return this; - } - walk(cb) { - cb(this); - this.item.walk(cb); - this.rep.walk(cb); - } -} -funcs.OneOrMore = (...args)=>new OneOrMore(...args); - - -export class ZeroOrMore extends FakeSVG { - constructor(item, rep, skip) { - return new Optional(new OneOrMore(item, rep), skip); - } -} -funcs.ZeroOrMore = (...args)=>new ZeroOrMore(...args); - - -export class Group extends FakeSVG { - constructor(item, label) { - super('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, - Options.AR*2); - this.height = this.item.height; - this.boxUp = this.up = Math.max(this.item.up + Options.VS, Options.AR); - if(this.label) { - this.up += this.label.up + this.label.height + this.label.down; - } - this.down = Math.max(this.item.down + Options.VS, Options.AR); - this.needsSpace = true; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "group"; - } - } - format(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: Options.AR, - ry: Options.AR, - '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; - } - walk(cb) { - cb(this); - this.item.walk(cb); - this.label.walk(cb); - } -} -funcs.Group = (...args)=>new Group(...args); - - -export class Start extends FakeSVG { - constructor({type="simple", label}={}) { - super('g'); - this.width = 20; - this.height = 0; - this.up = 10; - this.down = 10; - this.type = type; - if(label) { - this.label = ""+label; - this.width = Math.max(20, this.label.length * Options.CHAR_WIDTH + 10); - } - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "start"; - } - } - format(x,y) { - let path = new Path(x, y-10); - if (this.type === "complex") { - path.down(20) - .m(0, -10) - .right(this.width) - .addTo(this); - } else { - path.down(20) - .m(10, -20) - .down(20) - .m(-10, -10) - .right(this.width) - .addTo(this); - } - if(this.label) { - new FakeSVG('text', {x:x, y:y-15, style:"text-anchor:start"}, this.label).addTo(this); - } - return this; - } -} -funcs.Start = (...args)=>new Start(...args); - - -export class End extends FakeSVG { - constructor({type="simple"}={}) { - super('path'); - this.width = 20; - this.height = 0; - this.up = 10; - this.down = 10; - this.type = type; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "end"; - } - } - format(x,y) { - if (this.type === "complex") { - this.attrs.d = 'M '+x+' '+y+' h 20 m 0 -10 v 20'; - } else { - this.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 -20 v 20'; - } - return this; - } -} -funcs.End = (...args)=>new End(...args); - - -export class Terminal extends FakeSVG { - constructor(text, {href, title, cls}={}) { - super('g', {'class': ['terminal', cls].join(" ")}); - this.text = ""+text; - this.href = href; - this.title = title; - this.cls = cls; - this.width = this.text.length * Options.CHAR_WIDTH + 20; /* Assume that each char is .5em, and that the em is 16px */ - this.height = 0; - this.up = 11; - this.down = 11; - this.needsSpace = true; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "terminal"; - } - } - format(x, y, width) { - // Hook up the two sides if this is narrower than its stated width. - var gaps = determineGaps(width, this.width); - new Path(x,y).h(gaps[0]).addTo(this); - new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); - x += gaps[0]; - - new FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down, rx:10, ry:10}).addTo(this); - var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text); - if(this.href) - new FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this); - else - text.addTo(this); - if(this.title) - new FakeSVG('title', {}, [this.title]).addTo(this); - return this; - } -} -funcs.Terminal = (...args)=>new Terminal(...args); - - -export class NonTerminal extends FakeSVG { - constructor(text, {href, title, cls=""}={}) { - super('g', {'class': ['non-terminal', cls].join(" ")}); - this.text = ""+text; - this.href = href; - this.title = title; - this.cls = cls; - this.width = this.text.length * Options.CHAR_WIDTH + 20; - this.height = 0; - this.up = 11; - this.down = 11; - this.needsSpace = true; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "nonterminal"; - } - } - format(x, y, width) { - // Hook up the two sides if this is narrower than its stated width. - var gaps = determineGaps(width, this.width); - new Path(x,y).h(gaps[0]).addTo(this); - new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); - x += gaps[0]; - - new FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down}).addTo(this); - var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text); - if(this.href) - new FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this); - else - text.addTo(this); - if(this.title) - new FakeSVG('title', {}, [this.title]).addTo(this); - return this; - } -} -funcs.NonTerminal = (...args)=>new NonTerminal(...args); - - -export class Comment extends FakeSVG { - constructor(text, {href, title, cls=""}={}) { - super('g', {'class': ['comment', cls].join(" ")}); - this.text = ""+text; - this.href = href; - this.title = title; - this.cls = cls; - this.width = this.text.length * Options.COMMENT_CHAR_WIDTH + 10; - this.height = 0; - this.up = 8; - this.down = 8; - this.needsSpace = true; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "comment"; - } - } - format(x, y, width) { - // Hook up the two sides if this is narrower than its stated 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]; - - var text = new FakeSVG('text', {x:x+this.width/2, y:y+5, class:'comment'}, this.text); - if(this.href) - new FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this); - else - text.addTo(this); - if(this.title) - new FakeSVG('title', {}, this.title).addTo(this); - return this; - } -} -funcs.Comment = (...args)=>new Comment(...args); - - -export class Skip extends FakeSVG { - constructor() { - super('g'); - this.width = 0; - this.height = 0; - this.up = 0; - this.down = 0; - this.needsSpace = false; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "skip"; - } - } - format(x, y, width) { - new Path(x,y).right(width).addTo(this); - return this; - } -} -funcs.Skip = (...args)=>new Skip(...args); - - -export class Block extends FakeSVG { - constructor({width=50, up=15, height=25, down=15, needsSpace=true}={}) { - super('g'); - this.width = width; - this.height = height; - this.up = up; - this.down = down; - this.needsSpace = true; - if(Options.DEBUG) { - this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; - this.attrs['data-type'] = "block"; - } - } - format(x, y, width) { - // Hook up the two sides if this is narrower than its stated width. - var gaps = determineGaps(width, this.width); - new Path(x,y).h(gaps[0]).addTo(this); - new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); - x += gaps[0]; - - new FakeSVG('rect', {x:x, y:y-this.up, width:this.width, height:this.up+this.height+this.down}).addTo(this); - return this; - } -} -funcs.Block = (...args)=>new Block(...args); - - -function unnull(...args) { - // Return the first value that isn't undefined. - // More correct than `v1 || v2 || v3` because falsey values will be returned. - return args.reduce(function(sofar, x) { return sofar !== undefined ? sofar : x; }); -} - -function determineGaps(outer, inner) { - var diff = outer - inner; - switch(Options.INTERNAL_ALIGNMENT) { - case 'left': return [0, diff]; - case 'right': return [diff, 0]; - default: return [diff/2, diff/2]; - } -} - -function wrapString(value) { - return value instanceof FakeSVG ? value : new Terminal(""+value); -} - -function sum(iter, func) { - if(!func) func = function(x) { return x; }; - return iter.map(func).reduce(function(a,b){return a+b}, 0); -} - -function max(iter, func) { - if(!func) func = function(x) { return x; }; - return Math.max.apply(null, iter.map(func)); -} - -function SVG(name, attrs, text) { - attrs = attrs || {}; - text = text || ''; - var el = document.createElementNS("http://www.w3.org/2000/svg",name); - for(var attr in attrs) { - if(attr === 'xlink:href') - el.setAttributeNS("http://www.w3.org/1999/xlink", 'href', attrs[attr]); - else - el.setAttribute(attr, attrs[attr]); - } - el.textContent = text; - return el; -} - -function escapeString(string) { - // Escape markdown and HTML special characters - return string.replace(/[*_\`\[\]<&]/g, function(charString) { - return '&#' + charString.charCodeAt(0) + ';'; - }); -} - -function* enumerate(iter) { - var count = 0; - for(const x of iter) { - yield [count, x]; - count++; - } -} +window.addEventListener("DOMContentLoaded", () => { + railroadDiagramManager.updateVisiblity(); +});