From 2f42c9fb8da677da52ffdc11fdf85c061d08e6d5 Mon Sep 17 00:00:00 2001 From: Astrid Elocson Date: Tue, 6 Jan 2015 01:39:24 +0000 Subject: [PATCH] Links and transclusions in railroad diagrams --- .../parsers/wikiparser/rules/prettylink.js | 7 +- core/modules/utils/utils.js | 6 + core/modules/widgets/link.js | 4 +- plugins/tiddlywiki/railroad/components.js | 29 +++- .../railroad/doc/example-source.tid | 2 +- plugins/tiddlywiki/railroad/doc/example.tid | 2 +- plugins/tiddlywiki/railroad/doc/readme.tid | 8 +- plugins/tiddlywiki/railroad/doc/syntax.tid | 19 ++- .../railroad/files/railroad-diagrams.js | 7 +- plugins/tiddlywiki/railroad/parser.js | 145 ++++++++++++------ plugins/tiddlywiki/railroad/wrapper.js | 80 +++++++++- 11 files changed, 233 insertions(+), 76 deletions(-) diff --git a/core/modules/parsers/wikiparser/rules/prettylink.js b/core/modules/parsers/wikiparser/rules/prettylink.js index 12683b3eb..33841f116 100644 --- a/core/modules/parsers/wikiparser/rules/prettylink.js +++ b/core/modules/parsers/wikiparser/rules/prettylink.js @@ -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", diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index b0aead3e6..4f8676c18 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -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") { diff --git a/core/modules/widgets/link.js b/core/modules/widgets/link.js index 4943a2ea9..eac53a050 100755 --- a/core/modules/widgets/link.js +++ b/core/modules/widgets/link.js @@ -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", diff --git a/plugins/tiddlywiki/railroad/components.js b/plugins/tiddlywiki/railroad/components.js index 3001f448c..959db3840 100644 --- a/plugins/tiddlywiki/railroad/components.js +++ b/plugins/tiddlywiki/railroad/components.js @@ -89,13 +89,13 @@ Component.prototype.debug = function(output,indent) { Component.prototype.debugArray = function(array,output,indent) { for(var i=0; i) +({ [[digit|GettingStarted]] } | "#" <'escape sequence'>) [{("@" name-char | :"--" )}] \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/doc/example.tid b/plugins/tiddlywiki/railroad/doc/example.tid index 6b4df3071..b64199a44 100644 --- a/plugins/tiddlywiki/railroad/doc/example.tid +++ b/plugins/tiddlywiki/railroad/doc/example.tid @@ -8,7 +8,7 @@ title: $:/plugins/tiddlywiki/railroad/example ``` <$railroad text=""" ["+"] -({digit} | "#" <'escape sequence'>) +({ [[digit|GettingStarted]] } | "#" <'escape sequence'>) [{("@" name-char | :"--" )}] """/> ``` diff --git a/plugins/tiddlywiki/railroad/doc/readme.tid b/plugins/tiddlywiki/railroad/doc/readme.tid index a7507a305..04536dae9 100644 --- a/plugins/tiddlywiki/railroad/doc/readme.tid +++ b/plugins/tiddlywiki/railroad/doc/readme.tid @@ -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}}> -``` \ No newline at end of file +``` + +Alternatively, the diagram syntax allows specific parts of the `text` to be transcluded from other tiddlers. \ No newline at end of file diff --git a/plugins/tiddlywiki/railroad/doc/syntax.tid b/plugins/tiddlywiki/railroad/doc/syntax.tid index a658c734f..d99e3bb80 100644 --- a/plugins/tiddlywiki/railroad/doc/syntax.tid +++ b/plugins/tiddlywiki/railroad/doc/syntax.tid @@ -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 \ No newline at end of file +* 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 diff --git a/plugins/tiddlywiki/railroad/files/railroad-diagrams.js b/plugins/tiddlywiki/railroad/files/railroad-diagrams.js index bccaa7747..b5bae739b 100644 --- a/plugins/tiddlywiki/railroad/files/railroad-diagrams.js +++ b/plugins/tiddlywiki/railroad/files/railroad-diagrams.js @@ -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; diff --git a/plugins/tiddlywiki/railroad/parser.js b/plugins/tiddlywiki/railroad/parser.js index 1c8116e02..1dfd50d29 100644 --- a/plugins/tiddlywiki/railroad/parser.js +++ b/plugins/tiddlywiki/railroad/parser.js @@ -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"; } }; diff --git a/plugins/tiddlywiki/railroad/wrapper.js b/plugins/tiddlywiki/railroad/wrapper.js index fec71ba76..cb1b9a0bd 100644 --- a/plugins/tiddlywiki/railroad/wrapper.js +++ b/plugins/tiddlywiki/railroad/wrapper.js @@ -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 = ["
"];
-			parser.root.debug(output, "");
-			output.push("
"); - 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 = ["
"];
+	parser.root.debug(output, "");
+	output.push("
"); + 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