1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-06-25 14:53:15 +00:00

Merge pull request #1327 from aelocson/railroad-links

Links and transclusions in railroad diagrams
This commit is contained in:
Jeremy Ruston 2015-01-06 13:58:27 +00:00
commit ea07b558a3
11 changed files with 233 additions and 76 deletions

View File

@ -27,18 +27,13 @@ exports.init = function(parser) {
this.matchRegExp = /\[\[(.*?)(?:\|(.*?))?\]\]/mg; 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() { exports.parse = function() {
// Move past the match // Move past the match
this.parser.pos = this.matchRegExp.lastIndex; this.parser.pos = this.matchRegExp.lastIndex;
// Process the link // Process the link
var text = this.match[1], var text = this.match[1],
link = this.match[2] || text; link = this.match[2] || text;
if(isLinkExternal(link)) { if($tw.utils.isLinkExternal(link)) {
return [{ return [{
type: "element", type: "element",
tag: "a", tag: "a",

View File

@ -439,6 +439,12 @@ exports.escapeRegExp = function(s) {
return s.replace(/[\-\/\\\^\$\*\+\?\.\(\)\|\[\]\{\}]/g, '\\$&'); 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) { exports.nextTick = function(fn) {
/*global window: false */ /*global window: false */
if(typeof process === "undefined") { if(typeof process === "undefined") {

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ created: 20150102163222184
modified: 20150102172016663 modified: 20150102172016663
title: $:/plugins/tiddlywiki/railroad/readme 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. 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 | |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` | |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}}> <$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 modified: 20150103184022184
title: $:/plugins/tiddlywiki/railroad/syntax 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. `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 ; sequence
@ -52,7 +54,6 @@ The railroad widget constructs a diagram from the components defined below.
; nonterminal ; nonterminal
: <$railroad text=""" (name | "<" string ">") """/> : <$railroad text=""" (name | "<" string ">") """/>
* A nonterminal component, i.e. the name of another diagram * 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 ; dummy
: <$railroad text=""" "-" """/> : <$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 */ /* TiddlyWiki: added linking ability */
function Link(target, item) { function Link(item,options) {
if(!(this instanceof Link)) return new Link(target, item); if(!(this instanceof Link)) return new Link(item,options);
FakeSVG.call(this, 'a', {'xlink:href': target}); FakeSVG.call(this,'a',options);
this.item = item; this.item = item;
this.width = item.width; this.width = item.width;
this.up = item.up; this.up = item.up;
this.down = item.down; this.down = item.down;
} }
subclassOf(Link, FakeSVG); subclassOf(Link, FakeSVG);
Link.prototype.needsSpace = true;
Link.prototype.format = function(x, y, width) { Link.prototype.format = function(x, y, width) {
this.item.format(x,y,width).addTo(this); this.item.format(x,y,width).addTo(this);
return this; return this;

View File

@ -21,13 +21,11 @@ x y z sequence
<"x"> nonterminal <"x"> nonterminal
/"blah"/ comment /"blah"/ comment
- dummy - dummy
[[x|"tiddler"]] link
{{"tiddler"}} transclusion
"x" can also be written 'x' or """x""" "x" can also be written 'x' or """x"""
Future extensions:
[[x|tiddler]] link
{{tiddler}} transclusion
\*/ \*/
(function(){ (function(){
@ -37,12 +35,14 @@ Future extensions:
var components = require("$:/plugins/tiddlywiki/railroad/components.js").components; 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.source = source;
this.tokens = this.tokenise(source); this.tokens = this.tokenise(source);
this.tokenPos = 0; this.tokenPos = 0;
this.advance(); this.advance();
this.root = new components.Root(this.parseContent()); this.content = this.parseContent();
this.root = new components.Root(this.content);
this.checkFinished(); this.checkFinished();
}; };
@ -66,8 +66,8 @@ Parser.prototype.parseComponent = function() {
if(this.token) { if(this.token) {
if(this.at("string")) { if(this.at("string")) {
component = this.parseTerminal(); component = this.parseTerminal();
} else if(this.at("identifier")) { } else if(this.at("name")) {
component = this.parseIdentifier(); component = this.parseName();
} else { } else {
switch(this.token.value) { switch(this.token.value) {
case "[": case "[":
@ -85,6 +85,12 @@ Parser.prototype.parseComponent = function() {
case "/": case "/":
component = this.parseComment(); component = this.parseComment();
break; break;
case "[[":
component = this.parseLink();
break;
case "{{":
component = this.parseTransclusion();
break;
case "<-": case "<-":
component = this.parseSequence(); component = this.parseSequence();
break; break;
@ -112,25 +118,21 @@ Parser.prototype.parseChoice = function() {
// Parse the next branch // Parse the next branch
content.push(this.parseContent()); content.push(this.parseContent());
} while(this.eat("|")); } while(this.eat("|"));
// Create a component
var component = new components.Choice(content,colon === -1 ? 0 : colon);
// Consume the closing bracket // Consume the closing bracket
this.close(")"); this.close(")");
return component; // Create a component
return new components.Choice(content,colon === -1 ? 0 : colon);
}; };
Parser.prototype.parseComment = function() { Parser.prototype.parseComment = function() {
// Consume the / // Consume the /
this.advance(); this.advance();
// The comment's content should be in a string literal // The comment's content should be in a string literal
this.expectStringLiteral("/"); var content = this.expectString("after /");
// Create a component
var component = new components.Comment(this.token.value);
// Consume the string literal
this.advance();
// Consume the closing / // Consume the closing /
this.close("/"); this.close("/");
return component; // Create a component
return new components.Comment(content);
}; };
Parser.prototype.parseDummy = function() { Parser.prototype.parseDummy = function() {
@ -140,27 +142,43 @@ Parser.prototype.parseDummy = function() {
return new components.Dummy(); 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 // Create a component
var component = new components.Nonterminal(this.token.value); var component = new components.Nonterminal(this.token.value);
// Consume the identifier // Consume the name
this.advance(); this.advance();
return component; return component;
}; };
Parser.prototype.parseNonterminal = function() { Parser.prototype.parseNonterminal = function() {
// Consume the < // Consume the <
this.advance(); this.advance();
// The nonterminal's name should be in a string literal // The nonterminal's name should be in a string literal
this.expectStringLiteral("<"); var content = this.expectString("after <");
// Create a component
var component = new components.Nonterminal(this.token.value);
// Consume the string literal
this.advance();
// Consume the closing bracket // Consume the closing bracket
this.close(">"); this.close(">");
return component; // Create a component
return new components.Nonterminal(content);
}; };
Parser.prototype.parseOptional = function() { Parser.prototype.parseOptional = function() {
@ -177,14 +195,13 @@ Parser.prototype.parseOptional = function() {
if(repeated && this.eat("+")) { if(repeated && this.eat("+")) {
separator = this.parseContent(); separator = this.parseContent();
} }
// Create a component
var component = repeated ? new components.OptionalRepeated(content,separator,normal) : new components.Optional(content,normal);
// Consume the closing brackets // Consume the closing brackets
if(repeated) { if(repeated) {
this.close("}"); this.close("}");
} }
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() { Parser.prototype.parseRepeated = function() {
@ -197,23 +214,21 @@ Parser.prototype.parseRepeated = function() {
if(this.eat("+")) { if(this.eat("+")) {
separator = this.parseContent(); separator = this.parseContent();
} }
// Create a component
var component = new components.Repeated(content,separator);
// Consume the closing bracket // Consume the closing bracket
this.close("}"); this.close("}");
return component; // Create a component
return new components.Repeated(content,separator);
}; };
Parser.prototype.parseSequence = function() { Parser.prototype.parseSequence = function() {
// Consume the ~ // Consume the <-
this.advance(); this.advance();
// Parse the content // Parse the content
var content = this.parseContent(); var content = this.parseContent();
// Create a component // Consume the closing ->
var component = new components.Sequence(content);
// Consume the closing ~
this.close("->"); this.close("->");
return component; // Create a component
return new components.Sequence(content);
}; };
Parser.prototype.parseTerminal = function() { Parser.prototype.parseTerminal = function() {
@ -223,6 +238,21 @@ Parser.prototype.parseTerminal = function() {
return component; 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 /////////////////////////// Token manipulation
Parser.prototype.advance = function() { Parser.prototype.advance = function() {
@ -244,10 +274,10 @@ Parser.prototype.eat = function(token) {
return at; return at;
}; };
Parser.prototype.expectStringLiteral = function(preamble) { Parser.prototype.tokenValue = function() {
if(!this.at("string")) { var output = this.token.value;
throw "String expected after " + preamble; this.advance();
} return output;
}; };
Parser.prototype.close = function(token) { 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 /////////////////////////// Tokenisation
Parser.prototype.tokenise = function(source) { Parser.prototype.tokenise = function(source) {
@ -294,12 +345,12 @@ Parser.prototype.tokenise = function(source) {
} else if(c === "-") { } else if(c === "-") {
// - or -> // - or ->
s = source.charAt(pos+1) === ">" ? "->" : "-"; s = source.charAt(pos+1) === ">" ? "->" : "-";
} else if("()>+|/:".indexOf(c) !== -1) { } else if("()>+/:|".indexOf(c) !== -1) {
// Single character // Single character
s = c; s = c;
} else if(c.match(/[a-zA-Z]/)) { } else if(c.match(/[a-zA-Z]/)) {
// Identifier // Name
token = this.readIdentifier(source,pos); token = this.readName(source,pos);
} else { } else {
throw "Syntax error at " + c; throw "Syntax error at " + c;
} }
@ -316,14 +367,14 @@ Parser.prototype.tokenise = function(source) {
return tokens; return tokens;
}; };
Parser.prototype.readIdentifier = function(source,pos) { Parser.prototype.readName = function(source,pos) {
var re = /([a-zA-Z0-9_.-]+)/g; var re = /([a-zA-Z0-9_.-]+)/g;
re.lastIndex = pos; re.lastIndex = pos;
var match = re.exec(source); var match = re.exec(source);
if(match && match.index === pos) { 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 { } 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"); var div = this.document.createElement("div");
try { try {
// Parse the source // 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") { if(this.getAttribute("mode","svg") === "debug") {
var output = ["<pre>"]; this.renderDebug(parser,div);
parser.root.debug(output, "");
output.push("</pre>");
div.innerHTML = output.join("");
} else { } else {
div.innerHTML = parser.root.toSvg(); this.renderSvg(parser,div);
} }
} catch(ex) { } catch(ex) {
div.className = "tc-error"; div.className = "tc-error";
div.textContent = ex; div.textContent = ex;
} }
// Insert it into the DOM // Insert the div into the DOM
parent.insertBefore(div,nextSibling); parent.insertBefore(div,nextSibling);
this.domNodes.push(div); 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) { RailroadWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes(); var changedAttributes = this.computeAttributes();
if(changedAttributes.text) { if(changedAttributes.text) {
@ -65,6 +114,23 @@ RailroadWidget.prototype.refresh = function(changedTiddlers) {
return false; 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; exports.railroad = RailroadWidget;
})(); })();