mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-24 00:50:28 +00:00
Introduce railroad plugin
This commit is contained in:
parent
42efd4116d
commit
4f3cb8b9ae
264
plugins/tiddlywiki/railroad/components.js
Normal file
264
plugins/tiddlywiki/railroad/components.js
Normal 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
|
||||
};
|
||||
|
||||
})();
|
9
plugins/tiddlywiki/railroad/doc/example-source.tid
Normal file
9
plugins/tiddlywiki/railroad/doc/example-source.tid
Normal file
@ -0,0 +1,9 @@
|
||||
created: 20150103184022184
|
||||
modified: 20150103185522184
|
||||
tags:
|
||||
title: $:/plugins/tiddlywiki/railroad/example-source
|
||||
type: text/plain
|
||||
|
||||
["+"]
|
||||
({digit} | "#" <'escape sequence'>)
|
||||
[{("@" name-char | :"--" )}]
|
16
plugins/tiddlywiki/railroad/doc/example.tid
Normal file
16
plugins/tiddlywiki/railroad/doc/example.tid
Normal 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}}/>
|
17
plugins/tiddlywiki/railroad/doc/readme.tid
Normal file
17
plugins/tiddlywiki/railroad/doc/readme.tid
Normal 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}}>
|
||||
```
|
5
plugins/tiddlywiki/railroad/doc/syntax-string.tid
Normal file
5
plugins/tiddlywiki/railroad/doc/syntax-string.tid
Normal file
@ -0,0 +1,5 @@
|
||||
created: 20150103184022184
|
||||
modified: 20150103184022184
|
||||
title: $:/plugins/tiddlywiki/railroad/syntax-string
|
||||
|
||||
('"' text '"' | "'" text "'" | '"""' text '"""')
|
67
plugins/tiddlywiki/railroad/doc/syntax.tid
Normal file
67
plugins/tiddlywiki/railroad/doc/syntax.tid
Normal 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
|
23
plugins/tiddlywiki/railroad/files/railroad-diagrams.css
Normal file
23
plugins/tiddlywiki/railroad/files/railroad-diagrams.css
Normal 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%);
|
||||
}
|
508
plugins/tiddlywiki/railroad/files/railroad-diagrams.js
Normal file
508
plugins/tiddlywiki/railroad/files/railroad-diagrams.js
Normal 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, '&').replace(/"/g, '"') + '"';
|
||||
}
|
||||
str += '>';
|
||||
if(group) str += "\n";
|
||||
if(typeof this.children == 'string') {
|
||||
str += this.children.replace(/&/g, '&').replace(/</g, '<');
|
||||
} 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 */
|
21
plugins/tiddlywiki/railroad/files/tiddlywiki.files
Normal file
21
plugins/tiddlywiki/railroad/files/tiddlywiki.files
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
334
plugins/tiddlywiki/railroad/parser.js
Normal file
334
plugins/tiddlywiki/railroad/parser.js
Normal 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;
|
||||
|
||||
})();
|
7
plugins/tiddlywiki/railroad/plugin.info
Normal file
7
plugins/tiddlywiki/railroad/plugin.info
Normal 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"
|
||||
}
|
70
plugins/tiddlywiki/railroad/wrapper.js
Normal file
70
plugins/tiddlywiki/railroad/wrapper.js
Normal 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;
|
||||
|
||||
})();
|
Loading…
Reference in New Issue
Block a user