1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-12-29 11:30:28 +00:00

Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bram Chen 2015-01-05 08:44:19 +08:00
commit d19e2fa217
20 changed files with 1432 additions and 69 deletions

View File

@ -3,7 +3,7 @@ title: $:/core/ui/SideBarLists
<div class="tc-sidebar-lists">
<div class="tc-search">
<$edit-text tiddler="$:/temp/search" type="search" tag="input"/>
<$edit-text tiddler="$:/temp/search" type="search" tag="input" focus="true"/>
<$reveal state="$:/temp/search" type="nomatch" text="">
<$button tooltip={{$:/language/Buttons/AdvancedSearch/Hint}} aria-label={{$:/language/Buttons/AdvancedSearch/Caption}} class="tc-btn-invisible">
<$action-setfield $tiddler="$:/temp/advancedsearch" text={{$:/temp/search}}/>

View File

@ -6,6 +6,6 @@ type: text/vnd.tiddlywiki
The TiddlyWiki community holds regular Google Hangouts, usually every Tuesday from 4pm to 6pm (UK time). They are announced in the [[TiddlyWiki Google group|https://groups.google.com/d/forum/tiddlywiki]] and on the [[TiddlyWiki Twitter account|https://twitter.com/TiddlyWiki]].
Past Hangouts are archived in this YouTube playlist:
Past Hangouts are archived in this ~YouTube playlist:
<iframe width="560" height="315" src="http://www.youtube.com/embed/videoseries?list=PLVT_2PPd-1p34gGCQ5qpwC8QdykxVAI3u" frameborder="0" allowfullscreen></iframe>

View File

@ -7,7 +7,7 @@ type: text/vnd.tiddlywiki
Filters are used in TiddlyWiki to choose tiddlers by specifying simple match criteria. They are used by widgets like the ListWidget and the CountWidget to perform operations on multiple tiddlers at once.
* [[Introduction to Filters]] is a step-by-step introduction to how filters are used
* [[Filter Formal Grammar]] is a technical explanation of the filter syntax.
* [[Filter Syntax]] explains the exact rules for writing filters
The most common filter operators are:

View File

@ -0,0 +1,80 @@
created: 20140210141217955
modified: 20150102174633890
tags: Filters
title: Filter Syntax
type: text/vnd.tiddlywiki
[[Filter expressions|Filters]] follow a grammar that is presented here, using [[RailroadDiagrams]], for those who find formal syntax descriptions helpful. However, you can write your own filter expressions without needing to understand this tiddler.
---
;filter
: <$railroad text="""
{ [:whitespace] ("+" | :- | "-") run }
"""/>
A sequence of runs is evaluated from left to right, as follows:
|!Sequence |!Interpretation |
|`run1 run2` |union of the sets, i.e. the tiddlers in //either// run1 //or// run2 |
|`run1 -run2` |difference of the sets, i.e. run1 but excluding any tiddlers in run2 |
|`run1 +run2` |run2 takes run1 as its input |
The first run takes `[all[tiddlers]]` as its input, i.e. the set of all non-missing tiddlers.
---
;run
: <$railroad text="""
( "[" {step} "]"
|
'"' [{/'any character except "'/}] '"'
|
"'" [{/"any character except '"/}] '"'
|
[{/"any character except [ ] or whitespace"/}]
)
"""/>
* The last three options are short for `[title[text]]`
* A run evaluates each of its steps and returns the intersection of the results
---
;step
: <$railroad text="""
[:"!"] operator parameter
"""/>
* A step returns a set of tiddlers, in the form of a TitleList
---
;operator
: <$railroad text="""
( keyword [:":" fieldname] | fieldname )
"""/>
* Keywords are reserved names (<code>[[is|FilterOperator: is]]</code>, <code>[[has|FilterOperator: has]]</code>, <code>[[tag|FilterOperator: tag]]</code>, etc) of specific filtering functions
* A fieldname on its own implies the keyword `field`
* An entirely omitted operator defaults to `title`
---
;parameter
: <$railroad text="""
( "[" [{/"any character except ]"/}] "]"
|
"{" [{/"any character except }"/}] "}"
|
"<" [{/"any character except >"/}] ">"
)
"""/>
* `[`...`]` encloses a literal parameter
* `{`...`}` encloses a TextReference parameter
* `<`...`>` encloses a [[variable|Variables]] parameter
---
;whitespace
: <$railroad text="""
{( "space" | "tab" | "linefeed" | "return" | "vertical tab" | "formfeed" )}
"""/>
* A match for the JavaScript regular expression `\s+`

View File

@ -1,61 +0,0 @@
created: 20140210141217955
modified: 20140912145655663
tags: Filters
title: Filter Formal Grammar
type: text/vnd.tiddlywiki
[[Filter expressions|Filters]] follow a grammar that is presented here for those who find formal syntax descriptions helpful. However, you can write your own filter expressions without needing to understand this tiddler.
* [//x//] denotes an optional //x//
* (//x//)... denotes 1 or more instances of //x//
* Literal characters are `monospaced`
* Top-level bullets indicate alternative possibilities
* Second-level bullets are comments and clarifications
;filter
* ( [//whitespace//] [`+`|`-`] //run// )...
;run
* `[` (//operation//)... `]`
* `"` //text// `"`
** The text can contain anything but `"`
* `'` //text// `'`
** The text can contain anything but `'`
* //text//
** The text can contain anything but whitespace and `[` and `]`
** These last three alternatives are short for `[title[text]]`
;operation
* [`!`] //operator// //operand//
;operator
* [//keyword//] [`:` //fieldname//]
** Keywords (`is`, `has`, `tag`, etc) are reserved names that identify filter functions
** A fieldname on its own implies the keyword `field`
** An entirely omitted operator defaults to `title`
;operand
* `[` //text// `]`
** literal -- the text can contain anything but `]`
* `{` //text// `}`
** text reference -- the text can contain anything but `}`
* `<` //text// `>`
** variable -- the text can contain anything but `>`
;whitespace
* One or more spaces, tabs or linefeeds, i.e. a match for the JavaScript regular expression `\s+`
!Evaluation
Each operation returns a set of tiddlers, in the form of a TitleList.
A run evaluates each of the operations it contains, and returns the intersection of the resulting sets.
A sequence of runs is evaluated from left to right, as follows:
|!Sequence |!Interpretation |
|run1 run2 |union of the sets, i.e. the tiddlers in //either// run1 //or// run2 |
|run1 -run2 |difference of the sets, i.e. run1 but excluding any tiddlers in run2 |
|run1 +run2 |run2 takes run1 as its input |
The first run of a sequence takes `[all[tiddlers]]` as its input, i.e. the set of all non-missing tiddlers.

View File

@ -22,6 +22,7 @@ The content of the `<$edit-text>` widget is ignored.
|class |A CSS class to be assigned to the generated HTML editing element |
|placeholder |Placeholder text to be displayed when the edit field is empty |
|focusPopup |Title of a state tiddler for a popup that is displayed when the editing element has focus |
|focus |Set to "true" to automatically focus the editor after creation |
|tag |Overrides the generated HTML editing element tag. Use `textarea` for a multi-line editor |
|type |Overrides the generated HTML editing element `type` attribute |
|size |The size of the input field (in characters) |

View File

@ -4,7 +4,8 @@
"tiddlywiki/googleanalytics",
"tiddlywiki/nodewebkitsaver",
"tiddlywiki/github-fork-ribbon",
"tiddlywiki/browser-sniff"
"tiddlywiki/browser-sniff",
"tiddlywiki/railroad"
],
"themes": [
"tiddlywiki/vanilla",

View File

@ -92,16 +92,17 @@ Given a tiddler title and an array of existing filenames, generate a new legal f
*/
FileSystemAdaptor.prototype.generateTiddlerFilename = function(title,extension,existingFilenames) {
// First remove any of the characters that are illegal in Windows filenames
var baseFilename = title.replace(/<|>|\:|\"|\/|\\|\||\?|\*|\^|\s/g,"_");
var baseFilename = transliterate(title.replace(/<|>|\:|\"|\/|\\|\||\?|\*|\^|\s/g,"_"));
// Truncate the filename if it is too long
if(baseFilename.length > 200) {
baseFilename = baseFilename.substr(0,200);
}
// Start with the base filename plus the extension
var filename = transliterate(baseFilename) + extension,
var filename = baseFilename + extension,
count = 1;
// Add a discriminator if we're clashing with an existing filename
while(existingFilenames.indexOf(filename) !== -1) {
// Add a discriminator if we're clashing with an existing filename while
// handling case-insensitive filesystems (NTFS, FAT/FAT32, etc.)
while(existingFilenames.some(function(value) {return value.toLocaleLowerCase() === filename.toLocaleLowerCase();})) {
filename = baseFilename + " " + (count++) + extension;
}
return filename;

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