mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-11-27 03:57:21 +00:00
Links and transclusions in railroad diagrams
This commit is contained in:
parent
71357a8e39
commit
2f42c9fb8d
@ -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",
|
||||
|
@ -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") {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
})();
|
@ -5,5 +5,5 @@ title: $:/plugins/tiddlywiki/railroad/example-source
|
||||
type: text/plain
|
||||
|
||||
["+"]
|
||||
({digit} | "#" <'escape sequence'>)
|
||||
({ [[digit|GettingStarted]] } | "#" <'escape sequence'>)
|
||||
[{("@" name-char | :"--" )}]
|
@ -8,7 +8,7 @@ title: $:/plugins/tiddlywiki/railroad/example
|
||||
```
|
||||
<$railroad text="""
|
||||
["+"]
|
||||
({digit} | "#" <'escape sequence'>)
|
||||
({ [[digit|GettingStarted]] } | "#" <'escape sequence'>)
|
||||
[{("@" name-char | :"--" )}]
|
||||
"""/>
|
||||
```
|
||||
|
@ -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.
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
})();
|
Loading…
Reference in New Issue
Block a user