diff --git a/plugins/tiddlywiki/railroad/components.js b/plugins/tiddlywiki/railroad/components.js new file mode 100644 index 000000000..3001f448c --- /dev/null +++ b/plugins/tiddlywiki/railroad/components.js @@ -0,0 +1,264 @@ +/*\ +title: $:/plugins/tiddlywiki/railroad/components.js +type: application/javascript +module-type: library + +Components of a railroad diagram. + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var railroad = require("$:/plugins/tiddlywiki/railroad/railroad-diagrams.js"); + +/////////////////////////// Base component + +var Component = function() { + this.type = "Component"; +}; + +// Set up a leaf component +Component.prototype.initialiseLeaf = function(type,text) { + this.type = type; + this.text = text; +}; + +// Set up a component with a single child +Component.prototype.initialiseWithChild = function(type,content) { + this.type = type; + this.child = toSingleChild(content); +}; + +// Set up a component with an array of children +Component.prototype.initialiseWithChildren = function(type,content) { + this.type = type; + // Force the content to be an array + this.children = $tw.utils.isArray(content) ? content : [content]; +} + +// Return an array of the SVG strings of an array of children +Component.prototype.getSvgOfChildren = function() { + return this.children.map(function(child) { + return child.toSvg(); + }); +} + +Component.prototype.toSvg = function() { + return ""; +} + +Component.prototype.debug = function(output,indent) { + output.push(indent); + output.push(this.type); + // Add the text of a leaf component + if(this.text && this.text !== "") { + output.push(": "); + output.push(this.text); + } + // Flag the normal route + if(this.normal !== undefined) { + if(this.normal === true) { + output.push(" (normal)"); + } else if(this.normal !== false) { + output.push(" (normal: "); + output.push(this.normal); + output.push(")"); + } + } + output.push("\n"); + var contentIndent = indent + " "; + // Add the one child + if(this.child) { + this.child.debug(output,contentIndent); + } + // Add the array of children + if(this.children) { + this.debugArray(this.children,output,contentIndent); + } + // Add the separator if there is one + if(this.separator) { + output.push(indent); + output.push("(separator)\n"); + this.separator.debug(output,contentIndent); + } +}; + +Component.prototype.debugArray = function(array,output,indent) { + for(var i=0; i) +[{("@" name-char | :"--" )}] \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/doc/example.tid b/plugins/tiddlywiki/railroad/doc/example.tid new file mode 100644 index 000000000..6b4df3071 --- /dev/null +++ b/plugins/tiddlywiki/railroad/doc/example.tid @@ -0,0 +1,16 @@ +created: 20150102165032410 +modified: 20150102172010663 +tags: +title: $:/plugins/tiddlywiki/railroad/example + +<$railroad text={{$:/plugins/tiddlywiki/railroad/example-source}}/> + +``` +<$railroad text=""" +["+"] +({digit} | "#" <'escape sequence'>) +[{("@" name-char | :"--" )}] +"""/> +``` + +<$railroad mode="debug" text={{$:/plugins/tiddlywiki/railroad/example-source}}/> \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/doc/readme.tid b/plugins/tiddlywiki/railroad/doc/readme.tid new file mode 100644 index 000000000..a7507a305 --- /dev/null +++ b/plugins/tiddlywiki/railroad/doc/readme.tid @@ -0,0 +1,17 @@ +created: 20150102163222184 +modified: 20150102172016663 +title: $:/plugins/tiddlywiki/railroad/readme + +This plugin provides a `<$railroad>` widget for generating railroad syntax diagrams as SVG images. It is based on [[a library by Tab Atkins|https://github.com/tabatkins/railroad-diagrams]]. + +The content of the `<$railroad>` widget is ignored. + +|!Attribute |!Description | +|text |Text in a special syntax that defines the diagram's layout | +|mode |If set to `debug`, the diagram will display its internal tree structure. The default mode is `svg` | + +The `text` can be transcluded from another tiddler: + +``` +<$railroad tiddler={{diagram}}> +``` \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/doc/syntax-string.tid b/plugins/tiddlywiki/railroad/doc/syntax-string.tid new file mode 100644 index 000000000..aa5007149 --- /dev/null +++ b/plugins/tiddlywiki/railroad/doc/syntax-string.tid @@ -0,0 +1,5 @@ +created: 20150103184022184 +modified: 20150103184022184 +title: $:/plugins/tiddlywiki/railroad/syntax-string + +('"' text '"' | "'" text "'" | '"""' text '"""') \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/doc/syntax.tid b/plugins/tiddlywiki/railroad/doc/syntax.tid new file mode 100644 index 000000000..a658c734f --- /dev/null +++ b/plugins/tiddlywiki/railroad/doc/syntax.tid @@ -0,0 +1,67 @@ +created: 20150103184022184 +modified: 20150103184022184 +title: $:/plugins/tiddlywiki/railroad/syntax + +The railroad widget constructs a diagram from the components defined below. + +`x` and `y` here stand for any component. + +--- + +; sequence +: <$railroad text=""" ["<-"] {x} ["->"] """/> +* A sequence of components +* The `<-` and `->` delimiters allow you to force a single component to be treated as a sequence. This is occasionally useful for spacing a diagram out + +--- + +; optional +: <$railroad text=""" "[" [":"] x "]" """/> +* A component that can be omitted +* The colon makes `x` appear straight ahead + +--- + +; repeated +: <$railroad text=""" "{" x [:"+" y] "}" """/> +* A list of one or more `x` +* The `+` suffix adds `y` as a separator between each `x` and the next + +--- + +; optional repeated +: <$railroad text=""" "[{" [":"] x [:"+" y] "}]" """/> +* An optional list of `x`, i.e. a list of zero or more `x` + +--- + +; choice +: <$railroad text=""" "(" {[:":"] x +"|"} ")" """/> +* A set of alternatives +* The colon indicates which branch appears straight ahead. By default, it's the first branch + +--- + +; string / terminal +: <$railroad text={{$:/plugins/tiddlywiki/railroad/syntax-string}}/> +* A literal or terminal component +* This follows the normal ~TiddlyWiki rules for quoted strings + +--- + +; nonterminal +: <$railroad text=""" (name | "<" string ">") """/> +* A nonterminal component, i.e. the name of another diagram +* The simple `name` option is available when the text starts with a letter and contains only letters, digits, underscores, dots and hyphens + +--- + +; comment +: <$railroad text=""" "/" string "/" """/> +* A comment + +--- + +; dummy +: <$railroad text=""" "-" """/> +* The absence of a component \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/files/railroad-diagrams.css b/plugins/tiddlywiki/railroad/files/railroad-diagrams.css new file mode 100644 index 000000000..4f9ba13b3 --- /dev/null +++ b/plugins/tiddlywiki/railroad/files/railroad-diagrams.css @@ -0,0 +1,23 @@ +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%); +} \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/files/railroad-diagrams.js b/plugins/tiddlywiki/railroad/files/railroad-diagrams.js new file mode 100644 index 000000000..bccaa7747 --- /dev/null +++ b/plugins/tiddlywiki/railroad/files/railroad-diagrams.js @@ -0,0 +1,508 @@ +/* TiddlyWiki: modifications to the original library are commented like this */ + +/* +Railroad Diagrams +by Tab Atkins Jr. (and others) +http://xanthir.com +http://twitter.com/tabatkins +http://github.com/tabatkins/railroad-diagrams + +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. +*/ + +/* +This file uses a module pattern to avoid leaking names into the global scope. +The only accidental leakage is the name "temp". +The exported names can be found at the bottom of this file; +simply change the names in the array of strings to change what they are called in your application. + +As well, several configuration constants are passed into the module function at the bottom of this file. +At runtime, these constants can be found on the Diagram class. +*/ + +var temp = (function(options) { + function subclassOf(baseClass, superClass) { + baseClass.prototype = Object.create(superClass.prototype); + baseClass.prototype.$super = superClass.prototype; + } + + function unnull(/* children */) { + return [].slice.call(arguments).reduce(function(sofar, x) { return sofar !== undefined ? sofar : x; }); + } + + function determineGaps(outer, inner) { + var diff = outer - inner; + switch(Diagram.INTERNAL_ALIGNMENT) { + case 'left': return [0, diff]; break; + case 'right': return [diff, 0]; break; + case 'center': + default: return [diff/2, diff/2]; break; + } + } + + function wrapString(value) { + return ((typeof value) == 'string') ? new Terminal(value) : value; + } + + + 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) { + el.setAttribute(attr, attrs[attr]); + } + el.textContent = text; + return el; + } + + function FakeSVG(tagName, attrs, text){ + if(!(this instanceof FakeSVG)) return new FakeSVG(tagName, attrs, text); + if(text) this.children = text; + else this.children = []; + this.tagName = tagName; + this.attrs = unnull(attrs, {}); + return this; + }; + FakeSVG.prototype.format = function(x, y, width) { + // Virtual + }; + FakeSVG.prototype.addTo = function(parent) { + if(parent instanceof FakeSVG) { + parent.children.push(this); + return this; + } else { + var svg = this.toSVG(); + parent.appendChild(svg); + return svg; + } + }; + FakeSVG.prototype.toSVG = function() { + 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; + }; + FakeSVG.prototype.toString = function() { + 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 += this.children.replace(/&/g, '&').replace(/\n'; + return str; + } + + function Path(x,y) { + if(!(this instanceof Path)) return new Path(x,y); + FakeSVG.call(this, 'path'); + this.attrs.d = "M"+x+' '+y; + } + subclassOf(Path, FakeSVG); + Path.prototype.m = function(x,y) { + this.attrs.d += 'm'+x+' '+y; + return this; + } + Path.prototype.h = function(val) { + this.attrs.d += 'h'+val; + return this; + } + Path.prototype.right = Path.prototype.h; + Path.prototype.left = function(val) { return this.h(-val); } + Path.prototype.v = function(val) { + this.attrs.d += 'v'+val; + return this; + } + Path.prototype.down = Path.prototype.v; + Path.prototype.up = function(val) { return this.v(-val); } + Path.prototype.arc = function(sweep){ + var x = Diagram.ARC_RADIUS; + var y = Diagram.ARC_RADIUS; + if(sweep[0] == 'e' || sweep[1] == 'w') { + x *= -1; + } + if(sweep[0] == 's' || sweep[1] == 'n') { + y *= -1; + } + if(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 'wn') { + var cw = 1; + } else { + var cw = 0; + } + this.attrs.d += "a"+Diagram.ARC_RADIUS+" "+Diagram.ARC_RADIUS+" 0 0 "+cw+' '+x+' '+y; + return this; + } + Path.prototype.format = function() { + // 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; + } + + function Diagram(items) { + if(!(this instanceof Diagram)) return new Diagram([].slice.call(arguments)); + FakeSVG.call(this, 'svg', {class: Diagram.DIAGRAM_CLASS}); + this.items = items.map(wrapString); + this.items.unshift(new Start); + this.items.push(new End); + this.width = this.items.reduce(function(sofar, el) { return sofar + el.width + (el.needsSpace?20:0)}, 0)+1; + this.up = Math.max.apply(null, this.items.map(function (x) { return x.up; })); + this.down = Math.max.apply(null, this.items.map(function (x) { return x.down; })); + this.formatted = false; + } + subclassOf(Diagram, FakeSVG); + for(var option in options) { + Diagram[option] = options[option]; + } + Diagram.prototype.format = function(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 = FakeSVG('g', Diagram.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) { + Path(x,y).h(10).addTo(g); + x += 10; + } + item.format(x, y, item.width).addTo(g); + x += item.width; + if(item.needsSpace) { + Path(x,y).h(10).addTo(g); + x += 10; + } + } + this.attrs.width = this.width + paddingl + paddingr; + this.attrs.height = this.up + this.down + paddingt + paddingb; + this.attrs.viewBox = "0 0 " + this.attrs.width + " " + this.attrs.height; + g.addTo(this); + this.formatted = true; + return this; + } + Diagram.prototype.addTo = function(parent) { + var scriptTag = document.getElementsByTagName('script'); + scriptTag = scriptTag[scriptTag.length - 1]; + var parentTag = scriptTag.parentNode; + parent = parent || parentTag; + return this.$super.addTo.call(this, parent); + } + Diagram.prototype.toSVG = function() { + if (!this.formatted) { + this.format(); + } + return this.$super.toSVG.call(this); + } + Diagram.prototype.toString = function() { + if (!this.formatted) { + this.format(); + } + return this.$super.toString.call(this); + } + + function Sequence(items) { + if(!(this instanceof Sequence)) return new Sequence([].slice.call(arguments)); + FakeSVG.call(this, 'g'); + this.items = items.map(wrapString); + this.width = this.items.reduce(function(sofar, el) { return sofar + el.width + (el.needsSpace?20:0)}, 0); + this.up = this.items.reduce(function(sofar,el) { return Math.max(sofar, el.up)}, 0); + this.down = this.items.reduce(function(sofar,el) { return Math.max(sofar, el.down)}, 0); + } + subclassOf(Sequence, FakeSVG); + Sequence.prototype.format = function(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + Path(x,y).h(gaps[0]).addTo(this); + Path(x+gaps[0]+this.width,y).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) { + Path(x,y).h(10).addTo(this); + x += 10; + } + item.format(x, y, item.width).addTo(this); + x += item.width; + if(item.needsSpace) { + Path(x,y).h(10).addTo(this); + x += 10; + } + } + return this; + } + + function Choice(normal, items) { + if(!(this instanceof Choice)) return new Choice(normal, [].slice.call(arguments,1)); + FakeSVG.call(this, 'g'); + 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; + } + this.items = items.map(wrapString); + this.width = this.items.reduce(function(sofar, el){return Math.max(sofar, el.width)},0) + Diagram.ARC_RADIUS*4; + this.up = this.down = 0; + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if(i < normal) { this.up += Math.max(Diagram.ARC_RADIUS,item.up + item.down + Diagram.VERTICAL_SEPARATION); } + if(i == normal) { this.up += Math.max(Diagram.ARC_RADIUS, item.up); this.down += Math.max(Diagram.ARC_RADIUS, item.down); } + if(i > normal) { this.down += Math.max(Diagram.ARC_RADIUS,Diagram.VERTICAL_SEPARATION + item.up + item.down); } + } + } + subclassOf(Choice, FakeSVG); + Choice.prototype.format = function(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + Path(x,y).h(gaps[0]).addTo(this); + Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + var last = this.items.length -1; + var innerWidth = this.width - Diagram.ARC_RADIUS*4; + + // Do the elements that curve above + for(var i = this.normal - 1; i >= 0; i--) { + var item = this.items[i]; + if( i == this.normal - 1 ) { + var distanceFromY = Math.max(Diagram.ARC_RADIUS*2, this.items[i+1].up + Diagram.VERTICAL_SEPARATION + item.down); + } + Path(x,y).arc('se').up(distanceFromY - Diagram.ARC_RADIUS*2).arc('wn').addTo(this); + item.format(x+Diagram.ARC_RADIUS*2,y - distanceFromY,innerWidth).addTo(this); + Path(x+Diagram.ARC_RADIUS*2+innerWidth, y-distanceFromY).arc('ne').down(distanceFromY - Diagram.ARC_RADIUS*2).arc('ws').addTo(this); + distanceFromY += Math.max(Diagram.ARC_RADIUS, item.up + Diagram.VERTICAL_SEPARATION + (i == 0 ? 0 : this.items[i-1].down)); + } + + // Do the straight-line path. + Path(x,y).right(Diagram.ARC_RADIUS*2).addTo(this); + this.items[this.normal].format(x+Diagram.ARC_RADIUS*2, y, innerWidth).addTo(this); + Path(x+Diagram.ARC_RADIUS*2+innerWidth, y).right(Diagram.ARC_RADIUS*2).addTo(this); + + // Do the elements that curve below + for(var i = this.normal+1; i <= last; i++) { + var item = this.items[i]; + if( i == this.normal + 1 ) { + var distanceFromY = Math.max(Diagram.ARC_RADIUS*2, this.items[i-1].down + Diagram.VERTICAL_SEPARATION + item.up); + } + Path(x,y).arc('ne').down(distanceFromY - Diagram.ARC_RADIUS*2).arc('ws').addTo(this); + item.format(x+Diagram.ARC_RADIUS*2, y+distanceFromY, innerWidth).addTo(this); + Path(x+Diagram.ARC_RADIUS*2+innerWidth, y+distanceFromY).arc('se').up(distanceFromY - Diagram.ARC_RADIUS*2).arc('wn').addTo(this); + distanceFromY += Math.max(Diagram.ARC_RADIUS, item.down + Diagram.VERTICAL_SEPARATION + (i == last ? 0 : this.items[i+1].up)); + } + + return this; + } + + function Optional(item, skip) { + if( skip === undefined ) + return Choice(1, Skip(), item); + else if ( skip === "skip" ) + return Choice(0, Skip(), item); + else + throw "Unknown value for Optional()'s 'skip' argument."; + } + + function OneOrMore(item, rep) { + if(!(this instanceof OneOrMore)) return new OneOrMore(item, rep); + FakeSVG.call(this, 'g'); + rep = rep || (new Skip); + this.item = wrapString(item); + this.rep = wrapString(rep); + this.width = Math.max(this.item.width, this.rep.width) + Diagram.ARC_RADIUS*2; + this.up = this.item.up; + this.down = Math.max(Diagram.ARC_RADIUS*2, this.item.down + Diagram.VERTICAL_SEPARATION + this.rep.up + this.rep.down); + } + subclassOf(OneOrMore, FakeSVG); + OneOrMore.prototype.needsSpace = true; + OneOrMore.prototype.format = function(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + Path(x,y).h(gaps[0]).addTo(this); + Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + // Draw item + Path(x,y).right(Diagram.ARC_RADIUS).addTo(this); + this.item.format(x+Diagram.ARC_RADIUS,y,this.width-Diagram.ARC_RADIUS*2).addTo(this); + Path(x+this.width-Diagram.ARC_RADIUS,y).right(Diagram.ARC_RADIUS).addTo(this); + + // Draw repeat arc + var distanceFromY = Math.max(Diagram.ARC_RADIUS*2, this.item.down+Diagram.VERTICAL_SEPARATION+this.rep.up); + Path(x+Diagram.ARC_RADIUS,y).arc('nw').down(distanceFromY-Diagram.ARC_RADIUS*2).arc('ws').addTo(this); + this.rep.format(x+Diagram.ARC_RADIUS, y+distanceFromY, this.width - Diagram.ARC_RADIUS*2).addTo(this); + Path(x+this.width-Diagram.ARC_RADIUS, y+distanceFromY).arc('se').up(distanceFromY-Diagram.ARC_RADIUS*2).arc('en').addTo(this); + + return this; + } + + function ZeroOrMore(item, rep, skip) { + return Optional(OneOrMore(item, rep), skip); + } + + function Start() { + if(!(this instanceof Start)) return new Start(); + FakeSVG.call(this, 'path'); + this.width = 20; + this.up = 10; + this.down = 10; + } + subclassOf(Start, FakeSVG); + Start.prototype.format = function(x,y) { + this.attrs.d = 'M '+x+' '+(y-10)+' v 20 m 10 -20 v 20 m -10 -10 h 20.5'; + return this; + } + + function End() { + if(!(this instanceof End)) return new End(); + FakeSVG.call(this, 'path'); + this.width = 20; + this.up = 10; + this.down = 10; + } + subclassOf(End, FakeSVG); + End.prototype.format = function(x,y) { + this.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 -20 v 20'; + return this; + } + + function Terminal(text) { + if(!(this instanceof Terminal)) return new Terminal(text); + FakeSVG.call(this, 'g'); + this.text = text; + this.width = text.length * 8 + 20; /* Assume that each char is .5em, and that the em is 16px */ + this.up = 11; + this.down = 11; + } + subclassOf(Terminal, FakeSVG); + Terminal.prototype.needsSpace = true; + Terminal.prototype.format = function(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + Path(x,y).h(gaps[0]).addTo(this); + Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down, rx:10, ry:10}).addTo(this); + FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text).addTo(this); + return this; + } + + function NonTerminal(text) { + if(!(this instanceof NonTerminal)) return new NonTerminal(text); + FakeSVG.call(this, 'g'); + this.text = text; + this.width = text.length * 8 + 20; + this.up = 11; + this.down = 11; + } + subclassOf(NonTerminal, FakeSVG); + NonTerminal.prototype.needsSpace = true; + NonTerminal.prototype.format = function(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + Path(x,y).h(gaps[0]).addTo(this); + Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down}).addTo(this); + FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text).addTo(this); + return this; + } + + function Comment(text) { + if(!(this instanceof Comment)) return new Comment(text); + FakeSVG.call(this, 'g'); + this.text = text; + this.width = text.length * 7 + 10; + this.up = 11; + this.down = 11; + } + subclassOf(Comment, FakeSVG); + Comment.prototype.needsSpace = true; + Comment.prototype.format = function(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + Path(x,y).h(gaps[0]).addTo(this); + Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + FakeSVG('text', {x:x+this.width/2, y:y+5, class:'comment'}, this.text).addTo(this); + return this; + } + + function Skip() { + if(!(this instanceof Skip)) return new Skip(); + FakeSVG.call(this, 'g'); + this.width = 0; + this.up = 0; + this.down = 0; + } + subclassOf(Skip, FakeSVG); + Skip.prototype.format = function(x, y, width) { + Path(x,y).right(width).addTo(this); + return this; + } + +/* TiddlyWiki: added linking ability */ + function Link(target, item) { + if(!(this instanceof Link)) return new Link(target, item); + FakeSVG.call(this, 'a', {'xlink:href': target}); + this.item = item; + this.width = item.width; + this.up = item.up; + this.down = item.down; + } + subclassOf(Link, FakeSVG); + Link.prototype.format = function(x, y, width) { + this.item.format(x,y,width).addTo(this); + return this; + } + +/* TiddlyWiki: this block replaces the export mechanism in the original library */ + if (exports) { + exports.Diagram = Diagram; + exports.Sequence = Sequence; + exports.Choice = Choice; + exports.Optional = Optional; + exports.OneOrMore = OneOrMore; + exports.ZeroOrMore = ZeroOrMore; + exports.Terminal = Terminal; + exports.NonTerminal = NonTerminal; + exports.Comment = Comment; + exports.Skip = Skip; + exports.Link = Link; + }; +})( + { + VERTICAL_SEPARATION: 8, + ARC_RADIUS: 10, + DIAGRAM_CLASS: 'railroad-diagram', + STROKE_ODD_PIXEL_LENGTH: true, + INTERNAL_ALIGNMENT: 'center', + } +); + +/* TiddlyWiki: removed assignments to properties of the window object */ diff --git a/plugins/tiddlywiki/railroad/files/tiddlywiki.files b/plugins/tiddlywiki/railroad/files/tiddlywiki.files new file mode 100644 index 000000000..f479f394d --- /dev/null +++ b/plugins/tiddlywiki/railroad/files/tiddlywiki.files @@ -0,0 +1,21 @@ +{ + "tiddlers": [ + { + "file": "railroad-diagrams.css", + "fields": { + "type": "text/css", + "title": "$:/plugins/tiddlywiki/railroad/railroad-diagrams.css", + "tags": "$:/tags/Stylesheet" + } + },{ + "file": "railroad-diagrams.js", + "fields": { + "type": "application/javascript", + "title": "$:/plugins/tiddlywiki/railroad/railroad-diagrams.js", + "module-type": "library" + }, + "prefix": "(function(document) {\n", + "suffix": "\n})($tw.node ? $tw.fakeDocument : window.document)\n" + } + ] +} diff --git a/plugins/tiddlywiki/railroad/parser.js b/plugins/tiddlywiki/railroad/parser.js new file mode 100644 index 000000000..1c8116e02 --- /dev/null +++ b/plugins/tiddlywiki/railroad/parser.js @@ -0,0 +1,334 @@ +/*\ +title: $:/plugins/tiddlywiki/railroad/parser.js +type: application/javascript +module-type: library + +Parser for the source of a railroad diagram. + +[:x] optional, normally included +[x] optional, normally omitted +{x} one or more +{x +","} one or more, comma-separated +[{:x}] zero or more, normally included +[{:x +","}] zero or more, comma-separated, normally included +[{x}] zero or more, normally omitted +[{x +","}] zero or more, comma-separated, normally omitted +x y z sequence +<-x y z-> explicit sequence +(x|y|z) alternatives +(x|:y|z) alternatives, normally y +"x" terminal +<"x"> nonterminal +/"blah"/ comment +- dummy + +"x" can also be written 'x' or """x""" + +Future extensions: +[[x|tiddler]] link +{{tiddler}} transclusion + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var components = require("$:/plugins/tiddlywiki/railroad/components.js").components; + +var Parser = function(source) { + this.source = source; + this.tokens = this.tokenise(source); + this.tokenPos = 0; + this.advance(); + this.root = new components.Root(this.parseContent()); + this.checkFinished(); +}; + +/////////////////////////// Parser dispatch + +Parser.prototype.parseContent = function() { + var content = []; + // Parse zero or more components + while(true) { + var component = this.parseComponent(); + if(!component) { + break; + } + content.push(component); + } + return content; +}; + +Parser.prototype.parseComponent = function() { + var component = null; + if(this.token) { + if(this.at("string")) { + component = this.parseTerminal(); + } else if(this.at("identifier")) { + component = this.parseIdentifier(); + } else { + switch(this.token.value) { + case "[": + component = this.parseOptional(); + break; + case "{": + component = this.parseRepeated(); + break; + case "<": + component = this.parseNonterminal(); + break; + case "(": + component = this.parseChoice(); + break; + case "/": + component = this.parseComment(); + break; + case "<-": + component = this.parseSequence(); + break; + case "-": + component = this.parseDummy(); + break; + } + } + } + return component; +}; + +/////////////////////////// Specific components + +Parser.prototype.parseChoice = function() { + // Consume the ( + this.advance(); + var content = [], + colon = -1; + do { + // Allow at most one branch to be prefixed with a colon + if(colon === -1 && this.eat(":")) { + colon = content.length; + } + // Parse the next branch + content.push(this.parseContent()); + } while(this.eat("|")); + // Create a component + var component = new components.Choice(content,colon === -1 ? 0 : colon); + // Consume the closing bracket + this.close(")"); + return component; +}; + +Parser.prototype.parseComment = function() { + // Consume the / + this.advance(); + // The comment's content should be in a string literal + this.expectStringLiteral("/"); + // Create a component + var component = new components.Comment(this.token.value); + // Consume the string literal + this.advance(); + // Consume the closing / + this.close("/"); + return component; +}; + +Parser.prototype.parseDummy = function() { + // Consume the - + this.advance(); + // Create a component + return new components.Dummy(); +}; + +Parser.prototype.parseIdentifier = function() { + // Create a component + var component = new components.Nonterminal(this.token.value); + // Consume the identifier + this.advance(); + return component; +}; + + +Parser.prototype.parseNonterminal = function() { + // Consume the < + this.advance(); + // The nonterminal's name should be in a string literal + this.expectStringLiteral("<"); + // Create a component + var component = new components.Nonterminal(this.token.value); + // Consume the string literal + this.advance(); + // Consume the closing bracket + this.close(">"); + return component; +}; + +Parser.prototype.parseOptional = function() { + // Consume the [ + this.advance(); + // Consume the { if there is one + var repeated = this.eat("{"); + // Note whether omission is the normal route + var normal = this.eat(":"); + // Parse the content + var content = this.parseContent(), + separator = null; + // Parse the separator if there is one + if(repeated && this.eat("+")) { + separator = this.parseContent(); + } + // Create a component + var component = repeated ? new components.OptionalRepeated(content,separator,normal) : new components.Optional(content,normal); + // Consume the closing brackets + if(repeated) { + this.close("}"); + } + this.close("]"); + return component; +}; + +Parser.prototype.parseRepeated = function() { + // Consume the { + this.advance(); + // Parse the content + var content = this.parseContent(), + separator = null; + // Parse the separator if there is one + if(this.eat("+")) { + separator = this.parseContent(); + } + // Create a component + var component = new components.Repeated(content,separator); + // Consume the closing bracket + this.close("}"); + return component; +}; + +Parser.prototype.parseSequence = function() { + // Consume the ~ + this.advance(); + // Parse the content + var content = this.parseContent(); + // Create a component + var component = new components.Sequence(content); + // Consume the closing ~ + this.close("->"); + return component; +}; + +Parser.prototype.parseTerminal = function() { + var component = new components.Terminal(this.token.value); + // Consume the string literal + this.advance(); + return component; +}; + +/////////////////////////// Token manipulation + +Parser.prototype.advance = function() { + if(this.tokenPos >= this.tokens.length) { + this.token = null; + } + this.token = this.tokens[this.tokenPos++]; +}; + +Parser.prototype.at = function(token) { + return this.token && (this.token.type === token || this.token.type === "token" && this.token.value === token); +}; + +Parser.prototype.eat = function(token) { + var at = this.at(token); + if(at) { + this.advance(); + } + return at; +}; + +Parser.prototype.expectStringLiteral = function(preamble) { + if(!this.at("string")) { + throw "String expected after " + preamble; + } +}; + +Parser.prototype.close = function(token) { + if(!this.eat(token)) { + throw "Closing " + token + " expected"; + } +}; + +Parser.prototype.checkFinished = function() { + if(this.token) { + throw "Syntax error at " + this.token.value; + } +}; + +/////////////////////////// Tokenisation + +Parser.prototype.tokenise = function(source) { + var tokens = [], + pos = 0, + c, s, token; + while(pos < source.length) { + // Initialise this iteration + s = token = null; + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Avoid falling off the end of the string + if (pos >= source.length) { + break; + } + // Examine the next character + c = source.charAt(pos); + if("\"'".indexOf(c) !== -1) { + // String literal + token = $tw.utils.parseStringLiteral(source,pos); + if(!token) { + throw "Unterminated string literal"; + } + } else if("[]{}".indexOf(c) !== -1) { + // Single or double character + s = source.charAt(pos+1) === c ? c + c : c; + } else if(c === "<") { + // < or <- + s = source.charAt(pos+1) === "-" ? "<-" : "<"; + } else if(c === "-") { + // - or -> + s = source.charAt(pos+1) === ">" ? "->" : "-"; + } else if("()>+|/:".indexOf(c) !== -1) { + // Single character + s = c; + } else if(c.match(/[a-zA-Z]/)) { + // Identifier + token = this.readIdentifier(source,pos); + } else { + throw "Syntax error at " + c; + } + // Add our findings to the return array + if(token) { + tokens.push(token); + } else { + token = $tw.utils.parseTokenString(source,pos,s); + tokens.push(token); + } + // Prepare for the next character + pos = token.end; + } + return tokens; +}; + +Parser.prototype.readIdentifier = function(source,pos) { + var re = /([a-zA-Z0-9_.-]+)/g; + re.lastIndex = pos; + var match = re.exec(source); + if(match && match.index === pos) { + return {type: "identifier", value: match[1], start: pos, end: pos + match[1].length}; + } else { + throw "Invalid identifier"; + } +}; + +/////////////////////////// Exports + +exports.parser = Parser; + +})(); \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/plugin.info b/plugins/tiddlywiki/railroad/plugin.info new file mode 100644 index 000000000..6ed870514 --- /dev/null +++ b/plugins/tiddlywiki/railroad/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/plugins/tiddlywiki/railroad", + "description": "Plugin for generating SVG railroad diagrams", + "author": "Astrid Elocson", + "plugin-type": "plugin", + "list": "readme syntax example" +} diff --git a/plugins/tiddlywiki/railroad/wrapper.js b/plugins/tiddlywiki/railroad/wrapper.js new file mode 100644 index 000000000..fec71ba76 --- /dev/null +++ b/plugins/tiddlywiki/railroad/wrapper.js @@ -0,0 +1,70 @@ +/*\ +title: $:/plugins/tiddlywiki/railroad/wrapper.js +type: application/javascript +module-type: widget + +Wrapper for `railroad-diagrams.js` that provides a `<$railroad>` widget. + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var Parser = require("$:/plugins/tiddlywiki/railroad/parser.js").parser, + Widget = require("$:/core/modules/widgets/widget.js").widget; + +var RailroadWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +RailroadWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +RailroadWidget.prototype.render = function(parent,nextSibling) { + // Housekeeping + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + // Get the source text + var source = this.getAttribute("text",this.parseTreeNode.text || ""); + // Create a div to contain the SVG or error message + var div = this.document.createElement("div"); + try { + // Parse the source + var parser = new Parser(source); + if(this.getAttribute("mode","svg") === "debug") { + var output = ["
"];
+			parser.root.debug(output, "");
+			output.push("
"); + div.innerHTML = output.join(""); + } else { + div.innerHTML = parser.root.toSvg(); + } + } catch(ex) { + div.className = "tc-error"; + div.textContent = ex; + } + // Insert it into the DOM + parent.insertBefore(div,nextSibling); + this.domNodes.push(div); +}; + +RailroadWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes.text) { + this.refreshSelf(); + return true; + } + return false; +}; + +exports.railroad = RailroadWidget; + +})(); \ No newline at end of file