Links and transclusions in railroad diagrams

This commit is contained in:
Astrid Elocson 2015-01-06 01:39:24 +00:00
parent 71357a8e39
commit 2f42c9fb8d
11 changed files with 233 additions and 76 deletions

View File

@ -27,18 +27,13 @@ exports.init = function(parser) {
this.matchRegExp = /\[\[(.*?)(?:\|(.*?))?\]\]/mg;
};
var isLinkExternal = function(to) {
var externalRegExp = /(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|'"\\^~]+(?:\/|\b)/i;
return externalRegExp.test(to);
};
exports.parse = function() {
// Move past the match
this.parser.pos = this.matchRegExp.lastIndex;
// Process the link
var text = this.match[1],
link = this.match[2] || text;
if(isLinkExternal(link)) {
if($tw.utils.isLinkExternal(link)) {
return [{
type: "element",
tag: "a",

View File

@ -439,6 +439,12 @@ exports.escapeRegExp = function(s) {
return s.replace(/[\-\/\\\^\$\*\+\?\.\(\)\|\[\]\{\}]/g, '\\$&');
};
// Checks whether a link target is external, i.e. not a tiddler title
exports.isLinkExternal = function(to) {
var externalRegExp = /(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|'"\\^~]+(?:\/|\b)/i;
return externalRegExp.test(to);
};
exports.nextTick = function(fn) {
/*global window: false */
if(typeof process === "undefined") {

View File

@ -106,8 +106,8 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) {
this.domNodes.push(domNode);
};
LinkWidget.prototype.handleClickEvent = function (event) {
// Send the click on it's way as a navigate event
LinkWidget.prototype.handleClickEvent = function(event) {
// Send the click on its way as a navigate event
var bounds = this.domNodes[0].getBoundingClientRect();
this.dispatchEvent({
type: "tm-navigate",

View File

@ -89,13 +89,13 @@ Component.prototype.debug = function(output,indent) {
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
// Choice content is a special case: we number the branches
if(item.isChoiceBranch) {
output.push(indent);
output.push("(");
output.push(i);
output.push(")\n");
item.debug(output," " +indent);
item.debug(output," "+indent);
} else {
item.debug(output,indent);
}
@ -205,6 +205,27 @@ Repeated.prototype.toSvg = function() {
return railroad.OneOrMore(this.child.toSvg(),separatorSvg);
}
var Link = function(content,options) {
this.initialiseWithChild("Link",content);
this.options = options;
};
Link.prototype = new Component();
Link.prototype.toSvg = function() {
return railroad.Link(this.child.toSvg(),this.options);
}
var Transclusion = function(content) {
this.initialiseWithChild("Transclusion",content);
};
Transclusion.prototype = new Component();
Transclusion.prototype.toSvg = function() {
return this.child.toSvg();
}
/////////////////////////// Components with an array of children
var Root = function(content) {
@ -252,13 +273,15 @@ exports.components = {
Choice: Choice,
Comment: Comment,
Dummy: Dummy,
Link: Link,
Nonterminal: Nonterminal,
Optional: Optional,
OptionalRepeated: OptionalRepeated,
Repeated: Repeated,
Root: Root,
Sequence: Sequence,
Terminal: Terminal
Terminal: Terminal,
Transclusion: Transclusion
};
})();

View File

@ -5,5 +5,5 @@ title: $:/plugins/tiddlywiki/railroad/example-source
type: text/plain
["+"]
({digit} | "#" <'escape sequence'>)
({ [[digit|GettingStarted]] } | "#" <'escape sequence'>)
[{("@" name-char | :"--" )}]

View File

@ -8,7 +8,7 @@ title: $:/plugins/tiddlywiki/railroad/example
```
<$railroad text="""
["+"]
({digit} | "#" <'escape sequence'>)
({ [[digit|GettingStarted]] } | "#" <'escape sequence'>)
[{("@" name-char | :"--" )}]
"""/>
```

View File

@ -2,7 +2,7 @@ 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]].
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]], and has been extended to allow components of a diagram to function as links.
The content of the `<$railroad>` widget is ignored.
@ -10,8 +10,10 @@ The content of the `<$railroad>` widget is ignored.
|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:
The entire `text` can be transcluded from another tiddler:
```
<$railroad tiddler={{diagram}}>
```
```
Alternatively, the diagram syntax allows specific parts of the `text` to be transcluded from other tiddlers.

View File

@ -2,10 +2,12 @@ created: 20150103184022184
modified: 20150103184022184
title: $:/plugins/tiddlywiki/railroad/syntax
The railroad widget constructs a diagram from the components defined below.
The railroad widget uses a special ''diagram syntax'' to construct the components defined below.
`x` and `y` here stand for any component.
Names (as opposed to quoted strings) are available when a value starts with a letter and contains only letters, digits, underscores, dots and hyphens.
---
; sequence
@ -52,7 +54,6 @@ The railroad widget constructs a diagram from the components defined below.
; 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
---
@ -64,4 +65,16 @@ The railroad widget constructs a diagram from the components defined below.
; dummy
: <$railroad text=""" "-" """/>
* The absence of a component
* The absence of a component
---
; link
: <$railroad text=""" "[[" x "|" (name|string) "]]" """/>
* A link to the tiddler title or URI given by the string or name
---
; transclusion
: <$railroad text=""" "{{" (name|string) "}}" """/>
* Treats the content of another tiddler as diagram syntax and transcludes it into the current diagram

View File

@ -467,15 +467,16 @@ var temp = (function(options) {
}
/* TiddlyWiki: added linking ability */
function Link(target, item) {
if(!(this instanceof Link)) return new Link(target, item);
FakeSVG.call(this, 'a', {'xlink:href': target});
function Link(item,options) {
if(!(this instanceof Link)) return new Link(item,options);
FakeSVG.call(this,'a',options);
this.item = item;
this.width = item.width;
this.up = item.up;
this.down = item.down;
}
subclassOf(Link, FakeSVG);
Link.prototype.needsSpace = true;
Link.prototype.format = function(x, y, width) {
this.item.format(x,y,width).addTo(this);
return this;

View File

@ -21,13 +21,11 @@ x y z sequence
<"x"> nonterminal
/"blah"/ comment
- dummy
[[x|"tiddler"]] link
{{"tiddler"}} transclusion
"x" can also be written 'x' or """x"""
Future extensions:
[[x|tiddler]] link
{{tiddler}} transclusion
\*/
(function(){
@ -37,12 +35,14 @@ Future extensions:
var components = require("$:/plugins/tiddlywiki/railroad/components.js").components;
var Parser = function(source) {
var Parser = function(widget,source) {
this.widget = widget;
this.source = source;
this.tokens = this.tokenise(source);
this.tokenPos = 0;
this.advance();
this.root = new components.Root(this.parseContent());
this.content = this.parseContent();
this.root = new components.Root(this.content);
this.checkFinished();
};
@ -66,8 +66,8 @@ Parser.prototype.parseComponent = function() {
if(this.token) {
if(this.at("string")) {
component = this.parseTerminal();
} else if(this.at("identifier")) {
component = this.parseIdentifier();
} else if(this.at("name")) {
component = this.parseName();
} else {
switch(this.token.value) {
case "[":
@ -85,6 +85,12 @@ Parser.prototype.parseComponent = function() {
case "/":
component = this.parseComment();
break;
case "[[":
component = this.parseLink();
break;
case "{{":
component = this.parseTransclusion();
break;
case "<-":
component = this.parseSequence();
break;
@ -112,25 +118,21 @@ Parser.prototype.parseChoice = function() {
// 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;
// Create a component
return new components.Choice(content,colon === -1 ? 0 : colon);
};
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();
var content = this.expectString("after /");
// Consume the closing /
this.close("/");
return component;
// Create a component
return new components.Comment(content);
};
Parser.prototype.parseDummy = function() {
@ -140,27 +142,43 @@ Parser.prototype.parseDummy = function() {
return new components.Dummy();
};
Parser.prototype.parseIdentifier = function() {
Parser.prototype.parseLink = function() {
// Consume the [[
this.advance();
// Parse the content
var content = this.parseContent();
// Consume the |
this.expect("|");
// Consume the target
var target = this.expectNameOrString("as link target");
// Prepare some attributes for the SVG "a" element to carry
var options = {"data-tw-target": target};
if($tw.utils.isLinkExternal(target)) {
options["data-tw-external"] = true;
}
// Consume the closing ]]
this.close("]]");
// Create a component
return new components.Link(content,options);
};
Parser.prototype.parseName = function() {
// Create a component
var component = new components.Nonterminal(this.token.value);
// Consume the identifier
// Consume the name
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();
var content = this.expectString("after <");
// Consume the closing bracket
this.close(">");
return component;
// Create a component
return new components.Nonterminal(content);
};
Parser.prototype.parseOptional = function() {
@ -177,14 +195,13 @@ Parser.prototype.parseOptional = function() {
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;
// Create a component
return repeated ? new components.OptionalRepeated(content,separator,normal) : new components.Optional(content,normal);
};
Parser.prototype.parseRepeated = function() {
@ -197,23 +214,21 @@ Parser.prototype.parseRepeated = function() {
if(this.eat("+")) {
separator = this.parseContent();
}
// Create a component
var component = new components.Repeated(content,separator);
// Consume the closing bracket
this.close("}");
return component;
// Create a component
return new components.Repeated(content,separator);
};
Parser.prototype.parseSequence = function() {
// Consume the ~
// Consume the <-
this.advance();
// Parse the content
var content = this.parseContent();
// Create a component
var component = new components.Sequence(content);
// Consume the closing ~
// Consume the closing ->
this.close("->");
return component;
// Create a component
return new components.Sequence(content);
};
Parser.prototype.parseTerminal = function() {
@ -223,6 +238,21 @@ Parser.prototype.parseTerminal = function() {
return component;
};
Parser.prototype.parseTransclusion = function() {
// Consume the {{
this.advance();
// Consume the text reference
var textRef = this.expectNameOrString("as transclusion source");
// Consume the closing }}
this.close("}}");
// Retrieve the content of the text reference
var source = this.widget.wiki.getTextReference(textRef,"",this.widget.getVariable("currentTiddler"));
// Parse the content
var content = new Parser(this.widget,source).content;
// Create a component
return new components.Transclusion(content);
};
/////////////////////////// Token manipulation
Parser.prototype.advance = function() {
@ -244,10 +274,10 @@ Parser.prototype.eat = function(token) {
return at;
};
Parser.prototype.expectStringLiteral = function(preamble) {
if(!this.at("string")) {
throw "String expected after " + preamble;
}
Parser.prototype.tokenValue = function() {
var output = this.token.value;
this.advance();
return output;
};
Parser.prototype.close = function(token) {
@ -262,6 +292,27 @@ Parser.prototype.checkFinished = function() {
}
};
Parser.prototype.expect = function(token) {
if(!this.eat(token)) {
throw token + " expected";
}
};
Parser.prototype.expectString = function(context,token) {
if(!this.at("string")) {
token = token || "String";
throw token + " expected " + context;
}
return this.tokenValue();
};
Parser.prototype.expectNameOrString = function(context) {
if(this.at("name")) {
return this.tokenValue();
}
return this.expectString(context,"Name or string");
};
/////////////////////////// Tokenisation
Parser.prototype.tokenise = function(source) {
@ -294,12 +345,12 @@ Parser.prototype.tokenise = function(source) {
} else if(c === "-") {
// - or ->
s = source.charAt(pos+1) === ">" ? "->" : "-";
} else if("()>+|/:".indexOf(c) !== -1) {
} else if("()>+/:|".indexOf(c) !== -1) {
// Single character
s = c;
} else if(c.match(/[a-zA-Z]/)) {
// Identifier
token = this.readIdentifier(source,pos);
// Name
token = this.readName(source,pos);
} else {
throw "Syntax error at " + c;
}
@ -316,14 +367,14 @@ Parser.prototype.tokenise = function(source) {
return tokens;
};
Parser.prototype.readIdentifier = function(source,pos) {
Parser.prototype.readName = 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};
return {type: "name", value: match[1], start: pos, end: pos+match[1].length};
} else {
throw "Invalid identifier";
throw "Invalid name";
}
};

View File

@ -38,24 +38,73 @@ RailroadWidget.prototype.render = function(parent,nextSibling) {
var div = this.document.createElement("div");
try {
// Parse the source
var parser = new Parser(source);
var parser = new Parser(this,source);
// Generate content into the div
if(this.getAttribute("mode","svg") === "debug") {
var output = ["<pre>"];
parser.root.debug(output, "");
output.push("</pre>");
div.innerHTML = output.join("");
this.renderDebug(parser,div);
} else {
div.innerHTML = parser.root.toSvg();
this.renderSvg(parser,div);
}
} catch(ex) {
div.className = "tc-error";
div.textContent = ex;
}
// Insert it into the DOM
// Insert the div into the DOM
parent.insertBefore(div,nextSibling);
this.domNodes.push(div);
};
RailroadWidget.prototype.renderDebug = function(parser,div) {
var output = ["<pre>"];
parser.root.debug(output, "");
output.push("</pre>");
div.innerHTML = output.join("");
};
RailroadWidget.prototype.renderSvg = function(parser,div) {
// Generate a model of the diagram
var fakeSvg = parser.root.toSvg();
// Render the model into a tree of SVG DOM nodes
var svg = fakeSvg.toSVG();
// Fill in the remaining attributes of any link nodes
this.patchLinks(svg);
// Insert the SVG tree into the div
div.appendChild(svg);
};
RailroadWidget.prototype.patchLinks = function(node) {
var self = this;
if(node.hasChildNodes()) {
var children = node.childNodes;
for(var i=0; i<children.length; i++) {
var child = children[i];
var attributes = child.attributes;
if(attributes) {
// Find each element that has a data-tw-target attribute
var target = child.attributes["data-tw-target"];
if(target !== undefined) {
target = target.value;
if(child.attributes["data-tw-external"]) {
// External links are straightforward
child.setAttribute("target","_blank");
} else {
// Each internal link gets its own onclick handler, capturing its own copy of target
(function(myTarget) {
child.onclick = function(event) {
self.dispatchLink(myTarget,event);
return false;
}
})(target);
target = "#" + target;
}
child.setAttributeNS("http://www.w3.org/1999/xlink","href",target);
}
}
this.patchLinks(child);
}
}
};
RailroadWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.text) {
@ -65,6 +114,23 @@ RailroadWidget.prototype.refresh = function(changedTiddlers) {
return false;
};
RailroadWidget.prototype.dispatchLink = function(to,event) {
// Send the click on its way as a navigate event
var bounds = this.domNodes[0].getBoundingClientRect();
this.dispatchEvent({
type: "tm-navigate",
navigateTo: to,
navigateFromTitle: this.getVariable("storyTiddler"),
navigateFromNode: this,
navigateFromClientRect: { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
},
navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1)
});
event.preventDefault();
event.stopPropagation();
return false;
};
exports.railroad = RailroadWidget;
})();