1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-12-24 00:50:28 +00:00

Introduce railroad plugin

This commit is contained in:
Astrid Elocson 2015-01-03 20:02:27 +00:00
parent 42efd4116d
commit 4f3cb8b9ae
12 changed files with 1341 additions and 0 deletions

View File

@ -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<array.length; i++) {
var item = array[i];
// Choice content is a special case: an array of arrays
if(item.isChoiceBranch) {
output.push(indent);
output.push("(");
output.push(i);
output.push(")\n");
item.debug(output," " +indent);
} else {
item.debug(output,indent);
}
}
}
var toSingleChild = function(content) {
if($tw.utils.isArray(content)) {
// Reduce an array of one child to just the child
if(content.length === 1) {
return content[0];
} else {
// Never allow an empty sequence
if(content.length === 0) {
content.push(new Dummy());
}
// Wrap multiple children into a single sequence component
return new Sequence(content);
}
} else {
// Already single
return content;
}
}
/////////////////////////// Leaf components
var Comment = function(text) {
this.initialiseLeaf("Comment",text);
};
Comment.prototype = new Component();
Comment.prototype.toSvg = function() {
return railroad.Comment(this.text);
}
var Dummy = function() {
this.initialiseLeaf("Dummy");
};
Dummy.prototype = new Component();
Dummy.prototype.toSvg = function() {
return railroad.Skip();
}
var Nonterminal = function(text) {
this.initialiseLeaf("Nonterminal",text);
};
Nonterminal.prototype = new Component();
Nonterminal.prototype.toSvg = function() {
return railroad.NonTerminal(this.text);
}
var Terminal = function(text) {
this.initialiseLeaf("Terminal",text);
};
Terminal.prototype = new Component();
Terminal.prototype.toSvg = function() {
return railroad.Terminal(this.text);
}
/////////////////////////// Components with one child
var Optional = function(content,normal) {
this.initialiseWithChild("Optional",content);
this.normal = normal;
};
Optional.prototype = new Component();
Optional.prototype.toSvg = function() {
// Call Optional(component,"skip")
return railroad.Optional(this.child.toSvg(), this.normal ? undefined : "skip");
}
var OptionalRepeated = function(content,separator,normal) {
this.initialiseWithChild("OptionalRepeated",content);
this.separator = toSingleChild(separator);
this.normal = normal;
};
OptionalRepeated.prototype = new Component();
OptionalRepeated.prototype.toSvg = function() {
// Call ZeroOrMore(component,separator,"skip")
var separatorSvg = this.separator ? this.separator.toSvg() : null;
var skip = this.normal ? undefined : "skip";
return railroad.ZeroOrMore(this.child.toSvg(),separatorSvg,skip);
}
var Repeated = function(content,separator) {
this.initialiseWithChild("Repeated",content);
this.separator = toSingleChild(separator);
};
Repeated.prototype = new Component();
Repeated.prototype.toSvg = function() {
// Call OneOrMore(component,separator)
var separatorSvg = this.separator ? this.separator.toSvg() : null;
return railroad.OneOrMore(this.child.toSvg(),separatorSvg);
}
/////////////////////////// Components with an array of children
var Root = function(content) {
this.initialiseWithChildren("Root",content);
};
Root.prototype = new Component();
Root.prototype.toSvg = function() {
// Call Diagram(component1,component2,...)
return railroad.Diagram.apply(null, this.getSvgOfChildren());
}
var Sequence = function(content) {
this.initialiseWithChildren("Sequence",content);
};
Sequence.prototype = new Component();
Sequence.prototype.toSvg = function() {
// Call Sequence(component1,component2,...)
return railroad.Sequence.apply(null, this.getSvgOfChildren());
}
var Choice = function(content,normal) {
this.initialiseWithChildren("Choice",content.map(toSingleChild));
for(var i=0; i<this.children.length; i++) {
this.children[i].isChoiceBranch = true;
}
this.normal = normal;
};
Choice.prototype = new Component();
Choice.prototype.toSvg = function() {
// Call Choice(normal,component1,component2,...)
var args = this.getSvgOfChildren();
args.unshift(this.normal);
return railroad.Choice.apply(null, args);
}
/////////////////////////// Exports
exports.components = {
Choice: Choice,
Comment: Comment,
Dummy: Dummy,
Nonterminal: Nonterminal,
Optional: Optional,
OptionalRepeated: OptionalRepeated,
Repeated: Repeated,
Root: Root,
Sequence: Sequence,
Terminal: Terminal
};
})();

View File

@ -0,0 +1,9 @@
created: 20150103184022184
modified: 20150103185522184
tags:
title: $:/plugins/tiddlywiki/railroad/example-source
type: text/plain
["+"]
({digit} | "#" <'escape sequence'>)
[{("@" name-char | :"--" )}]

View File

@ -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}}/>

View File

@ -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}}>
```

View File

@ -0,0 +1,5 @@
created: 20150103184022184
modified: 20150103184022184
title: $:/plugins/tiddlywiki/railroad/syntax-string
('"' text '"' | "'" text "'" | '"""' text '"""')

View File

@ -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

View File

@ -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%);
}

View File

@ -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, '&amp;').replace(/"/g, '&quot;') + '"';
}
str += '>';
if(group) str += "\n";
if(typeof this.children == 'string') {
str += this.children.replace(/&/g, '&amp;').replace(/</g, '&lt;');
} else {
this.children.forEach(function(e) {
str += e;
});
}
str += '</' + this.tagName + '>\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 */

View File

@ -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"
}
]
}

View File

@ -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;
})();

View File

@ -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"
}

View File

@ -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 = ["<pre>"];
parser.root.debug(output, "");
output.push("</pre>");
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;
})();