Introduce refactored wiki parser and renderer

This is a half-way through a big refactoring of the parsing and
rendering infrastructure. The main change is to separate the parse and
render trees, which makes the code a lot cleaner. The new parser isn't
yet functional enough to replace the existing parser so for the moment
you have to manually invoke it with `$tw.testNewParser()` in your
browser console. I really ought to use branches for this kind of
thing...
This commit is contained in:
Jeremy Ruston 2012-12-13 21:34:31 +00:00
parent 916ca8eecf
commit d338a54370
28 changed files with 2837 additions and 33 deletions

View File

@ -0,0 +1,57 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/block/heading.js
type: application/javascript
module-type: wikiblockrule
Wiki text block rule for headings
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var HeadingRule = function(parser,startPos) {
// Save state
this.parser = parser;
// Regexp to match
this.reMatch = /(!{1,6})/mg;
// Get the first match
this.matchIndex = startPos-1;
this.findNextMatch(startPos);
};
HeadingRule.prototype.findNextMatch = function(startPos) {
if(this.matchIndex !== undefined && startPos > this.matchIndex) {
this.reMatch.lastIndex = startPos;
this.match = this.reMatch.exec(this.parser.source);
this.matchIndex = this.match ? this.match.index : undefined;
}
return this.matchIndex;
};
/*
Parse the most recent match
*/
HeadingRule.prototype.parse = function() {
// Get all the details of the match
var headingLevel = this.match[1].length;
// Move past the !s
this.parser.pos = this.reMatch.lastIndex;
// Parse the heading
var classedRun = this.parser.parseClassedRun(/(\r?\n)/mg);
// Return the heading
return [{
type: "element",
tag: "h" + this.match[1].length,
attributes: {
"class": {type: "string", value: classedRun["class"]}
},
children: classedRun.tree
}];
};
exports.HeadingRule = HeadingRule;
})();

View File

@ -0,0 +1,141 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/block/list.js
type: application/javascript
module-type: wikiblockrule
Wiki text block rule for lists. For example:
{{{
* This is an unordered list
* It has two items
# This is a numbered list
## With a subitem
# And a third item
; This is a term that is being defined
: This is the definition of that term
}}}
Note that lists can be nested arbitrarily:
{{{
#** One
#* Two
#** Three
#**** Four
#**# Five
#**## Six
## Seven
### Eight
## Nine
}}}
A CSS class can be applied to a list item as follows:
{{{
* List item one
*.active List item two has the class `active`
* List item three
}}}
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var ListRule = function(parser,startPos) {
// Save state
this.parser = parser;
// Regexp to match
this.reMatch = /([\\*#;:]+)/mg;
// Get the first match
this.matchIndex = startPos-1;
this.findNextMatch(startPos);
};
ListRule.prototype.findNextMatch = function(startPos) {
if(this.matchIndex !== undefined && startPos > this.matchIndex) {
this.reMatch.lastIndex = startPos;
this.match = this.reMatch.exec(this.parser.source);
this.matchIndex = this.match ? this.match.index : undefined;
}
return this.matchIndex;
};
var listTypes = {
"*": {listTag: "ul", itemTag: "li"},
"#": {listTag: "ol", itemTag: "li"},
";": {listTag: "dl", itemTag: "dt"},
":": {listTag: "dl", itemTag: "dd"}
};
/*
Parse the most recent match
*/
ListRule.prototype.parse = function() {
// Array of parse tree nodes for the previous row of the list
var listStack = [];
// Cycle through the items in the list
while(true) {
// Match the list marker
var reMatch = /(^[\*#;:]+)/mg;
reMatch.lastIndex = this.parser.pos;
var match = reMatch.exec(this.parser.source);
if(!match || match.index !== this.parser.pos) {
break;
}
// Check whether the list type of the top level matches
var listInfo = listTypes[match[0].charAt(0)];
if(listStack.length > 0 && listStack[0].tag !== listInfo.listTag) {
break;
}
// Move past the list marker
this.parser.pos = match.index + match[0].length;
// Walk through the list markers for the current row
for(var t=0; t<match[0].length; t++) {
listInfo = listTypes[match[0].charAt(t)];
// Remove any stacked up element if we can't re-use it because the list type doesn't match
if(listStack.length > t && listStack[t].tag !== listInfo.listTag) {
listStack.splice(t,listStack.length - t);
}
// Construct the list element or reuse the previous one at this level
if(listStack.length <= t) {
var listElement = {type: "element", tag: listInfo.listTag, children: [
{type: "element", tag: listInfo.itemTag, children: []}
]};
// Link this list element into the last child item of the parent list item
if(t) {
var prevListItem = listStack[t-1].children[listStack[t-1].children.length-1];
prevListItem.children.push(listElement);
}
// Save this element in the stack
listStack[t] = listElement;
} else if(t === (match[0].length - 1)) {
listStack[t].children.push({type: "element", tag: listInfo.itemTag, children: []});
}
}
if(listStack.length > match[0].length) {
listStack.splice(match[0].length,listStack.length - match[0].length);
}
// Process the body of the list item into the last list item
var lastListChildren = listStack[listStack.length-1].children,
lastListItem = lastListChildren[lastListChildren.length-1],
classedRun = this.parser.parseClassedRun(/(\r?\n)/mg);
lastListItem.children.push.apply(lastListItem.children,classedRun.tree);
if(classedRun["class"]) {
lastListItem.attributes = lastListItem.attributes || {};
lastListItem.attributes["class"] = {type: "string", value: classedRun["class"]};
}
// Consume any whitespace following the list item
this.parser.skipWhitespace();
};
// Return the root element of the list
return [listStack[0]];
};
exports.ListRule = ListRule;
})();

View File

@ -0,0 +1,98 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/pragma/macrodef.js
type: application/javascript
module-type: wikipragmarule
Wiki pragma rule for macro definitions
{{{
/define name(param:defaultvalue,param2:defaultvalue)
definition text, including $param$ markers
/end
}}}
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Instantiate parse rule
*/
var MacroDefRule = function(parser,startPos) {
// Save state
this.parser = parser;
// Regexp to match
this.reMatch = /^\\define\s*([^(\s]+)\(\s*([^)]*)\)(\r?\n)?/mg;
// Get the first match
this.matchIndex = startPos-1;
this.findNextMatch(startPos);
};
MacroDefRule.prototype.findNextMatch = function(startPos) {
if(this.matchIndex !== undefined && startPos > this.matchIndex) {
this.reMatch.lastIndex = startPos;
this.match = this.reMatch.exec(this.parser.source);
this.matchIndex = this.match ? this.match.index : undefined;
}
return this.matchIndex;
};
/*
Parse the most recent match
*/
MacroDefRule.prototype.parse = function() {
// Move past the macro name and parameters
this.parser.pos = this.reMatch.lastIndex;
// Parse the parameters
var paramString = this.match[2],
params = [];
if(paramString !== "") {
var reParam = /\s*([A-Za-z0-9\-_]+)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|([^"'\s]+)))?/mg,
paramMatch = reParam.exec(paramString);
while(paramMatch) {
// Save the parameter details
var paramInfo = {name: paramMatch[1]},
defaultValue = paramMatch[2] || paramMatch[3] || paramMatch[4] || paramMatch[5];
if(defaultValue) {
paramInfo["default"] = defaultValue;
}
params.push(paramInfo);
// Look for the next parameter
paramMatch = reParam.exec(paramString);
}
}
// Is this a multiline definition?
var reEnd;
if(this.match[3]) {
// If so, the end of the body is marked with \end
reEnd = /(\r?\n\\end\r?\n)/mg;
} else {
// Otherwise, the end of the definition is marked by the end of the line
reEnd = /(\r?\n)/mg;
}
// Find the end of the definition
reEnd.lastIndex = this.parser.pos;
var text,
endMatch = reEnd.exec(this.parser.source);
if(endMatch) {
text = this.parser.source.substring(this.parser.pos,endMatch.index).trim();
this.parser.pos = endMatch.index + endMatch[0].length;
} else {
// We didn't find the end of the definition, so we'll make it blank
text = "";
}
// Save the macro definition
this.parser.macroDefinitions[this.match[1]] = {
type: "textmacro",
name: this.match[1],
params: params,
text: text
};
};
exports.MacroDefRule = MacroDefRule;
})();

View File

@ -0,0 +1,52 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/run/entity.js
type: application/javascript
module-type: wikirunrule
Wiki text run rule for HTML entities. For example:
{{{
This is a copyright symbol: &copy;
}}}
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var EntityRule = function(parser,startPos) {
// Save state
this.parser = parser;
// Regexp to match
this.reMatch = /(&#?[a-zA-Z0-9]{2,8};)/mg;
// Get the first match
this.matchIndex = startPos-1;
this.findNextMatch(startPos);
};
EntityRule.prototype.findNextMatch = function(startPos) {
if(this.matchIndex !== undefined && startPos > this.matchIndex) {
this.reMatch.lastIndex = startPos;
this.match = this.reMatch.exec(this.parser.source);
this.matchIndex = this.match ? this.match.index : undefined;
}
return this.matchIndex;
};
/*
Parse the most recent match
*/
EntityRule.prototype.parse = function() {
// Get all the details of the match
var entityString = this.match[1];
// Move past the macro call
this.parser.pos = this.reMatch.lastIndex;
// Return the entity
return [{type: "entity", entity: this.match[0]}];
};
exports.EntityRule = EntityRule;
})();

View File

@ -0,0 +1,113 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/run/html.js
type: application/javascript
module-type: wikirunrule
Wiki rule for HTML elements and widgets. For example:
{{{
<aside>
This is an HTML5 aside element
</aside>
<_slider target="MyTiddler">
This is a widget invocation
</_slider>
}}}
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var voidElements = "area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(",");
var HtmlRule = function(parser,startPos) {
// Save state
this.parser = parser;
// Regexp to match
this.reMatch = /<(_)?([A-Za-z]+)(\s*[^>]*?)(\/)?>/mg;
// Get the first match
this.matchIndex = startPos-1;
this.findNextMatch(startPos);
};
HtmlRule.prototype.findNextMatch = function(startPos) {
if(this.matchIndex !== undefined && startPos > this.matchIndex) {
this.reMatch.lastIndex = startPos;
this.match = this.reMatch.exec(this.parser.source);
this.matchIndex = this.match ? this.match.index : undefined;
}
return this.matchIndex;
};
/*
Parse the most recent match
*/
HtmlRule.prototype.parse = function() {
// Get all the details of the match in case this parser is called recursively
var isWidget = !!this.match[1],
tagName = this.match[2],
attributeString = this.match[3],
isSelfClosing = !!this.match[4];
// Move past the tag name and parameters
this.parser.pos = this.reMatch.lastIndex;
var reLineBreak = /(\r?\n)/mg,
reAttr = /\s*([A-Za-z\-_]+)(?:\s*=\s*(?:("[^"]*")|('[^']*')|(\{\{[^\}]*\}\})|([^"'\s]+)))?/mg,
isBlock;
// Process the attributes
var attrMatch = reAttr.exec(attributeString),
attributes = {};
while(attrMatch) {
var name = attrMatch[1],
value;
if(attrMatch[2]) { // Double quoted
value = {type: "string", value: attrMatch[2].substring(1,attrMatch[2].length-1)};
} else if(attrMatch[3]) { // Single quoted
value = {type: "string", value: attrMatch[3].substring(1,attrMatch[3].length-1)};
} else if(attrMatch[4]) { // Double curly brace quoted
value = {type: "indirect", textReference: attrMatch[4].substr(2,attrMatch[4].length-4)};
} else if(attrMatch[5]) { // Unquoted
value = {type: "string", value: attrMatch[5]};
} else { // Valueless
value = {type: "string", value: "true"}; // TODO: We should have a way of indicating we want an attribute without a value
}
attributes[name] = value;
attrMatch = reAttr.exec(attributeString);
}
// Check for a line break immediate after the opening tag
reLineBreak.lastIndex = this.parser.pos;
var lineBreakMatch = reLineBreak.exec(this.parser.source);
if(lineBreakMatch && lineBreakMatch.index === this.parser.pos) {
this.parser.pos = lineBreakMatch.index + lineBreakMatch[0].length;
isBlock = true;
} else {
isBlock = false;
}
if(!isSelfClosing && (isWidget || voidElements.indexOf(tagName) === -1)) {
var reEndString = "(</" + (isWidget ? "_" : "") + tagName + ">)",
reEnd = new RegExp(reEndString,"mg"),
content;
if(isBlock) {
content = this.parser.parseBlocks(reEndString);
} else {
content = this.parser.parseRun(reEnd);
}
reEnd.lastIndex = this.parser.pos;
var endMatch = reEnd.exec(this.parser.source);
if(endMatch && endMatch.index === this.parser.pos) {
this.parser.pos = endMatch.index + endMatch[0].length;
}
} else {
content = [];
}
var element = {type: isWidget ? "widget" : "element", tag: tagName, isBlock: isBlock, attributes: attributes, children: content};
return [element];
};
exports.HtmlRule = HtmlRule;
})();

View File

@ -0,0 +1,71 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/run/macrocall.js
type: application/javascript
module-type: wikirunrule
Wiki rule for macro calls
{{{
<<name value value2>>
}}}
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var MacroCallRule = function(parser,startPos) {
// Save state
this.parser = parser;
// Regexp to match
this.reMatch = /<<([^\s>]+)\s*([\s\S]*?)>>/mg;
// Get the first match
this.matchIndex = startPos-1;
this.findNextMatch(startPos);
};
MacroCallRule.prototype.findNextMatch = function(startPos) {
if(this.matchIndex !== undefined && startPos > this.matchIndex) {
this.reMatch.lastIndex = startPos;
this.match = this.reMatch.exec(this.parser.source);
this.matchIndex = this.match ? this.match.index : undefined;
}
return this.matchIndex;
};
/*
Parse the most recent match
*/
MacroCallRule.prototype.parse = function() {
// Get all the details of the match
var macroName = this.match[1],
paramString = this.match[2];
// Move past the macro call
this.parser.pos = this.reMatch.lastIndex;
var params = [],
reParam = /\s*(?:([A-Za-z0-9\-_]+)\s*:)?(?:\s*(?:"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|([^"'\s]+)))/mg,
paramMatch = reParam.exec(paramString);
while(paramMatch) {
// Process this parameter
var paramInfo = {
value: paramMatch[2] || paramMatch[3] || paramMatch[4] || paramMatch[5]
};
if(paramMatch[1]) {
paramInfo.name = paramMatch[1];
}
params.push(paramInfo);
// Find the next match
paramMatch = reParam.exec(paramString);
}
return [{
type: "macrocall",
name: macroName,
params: params
}];
};
exports.MacroCallRule = MacroCallRule;
})();

View File

@ -0,0 +1,296 @@
/*\
title: $:/core/modules/parsers/wikiparser/wikiparser.js
type: application/javascript
module-type: global
The wiki text parser processes blocks of source text into a parse tree.
The parse tree is made up of nested arrays of these JavaScript objects:
{type: "element", tag: <string>, attributes: {}, children: []} - an HTML element
{type: "text", text: <string>} - a text node
{type: "entity", value: <string>} - an entity
{type: "raw", html: <string>} - raw HTML
Attributes are stored as hashmaps of the following objects:
{type: "string", value: <string>} - literal string
{type: "array", value: <string array>} - array of strings
{type: "styles", value: <object>} - hashmap of style strings
{type: "indirect", textReference: <textReference>} - indirect through a text reference
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var WikiParser = function(vocabulary,type,text,options) {
this.wiki = options.wiki;
this.vocabulary = vocabulary;
// Save the parse text
this.type = type || "text/vnd.tiddlywiki";
this.source = text || "";
this.sourceLength = this.source.length;
// Set current parse position
this.pos = 0;
// Initialise the things that pragma rules can change
this.macroDefinitions = {}; // Hash map of macro definitions
// Instantiate the pragma parse rules
this.pragmaRules = this.instantiateRules(this.vocabulary.pragmaRuleClasses,0);
// Parse any pragmas
this.parsePragmas();
// Instantiate the parser block and run rules
this.blockRules = this.instantiateRules(this.vocabulary.blockRuleClasses,this.pos);
this.runRules = this.instantiateRules(this.vocabulary.runRuleClasses,this.pos);
// Parse the text into runs or blocks
if(this.type === "text/vnd.tiddlywiki-run") {
this.tree = this.parseRun();
} else {
this.tree = this.parseBlocks();
}
};
/*
Instantiate an array of parse rules
*/
WikiParser.prototype.instantiateRules = function(classes,startPos) {
var rules = [],
self = this;
$tw.utils.each(classes,function(RuleClass) {
// Instantiate the rule
var rule = new RuleClass(self,startPos);
// Only save the rule if there is at least one match
if(rule.matchIndex !== undefined) {
rules.push(rule);
}
});
return rules;
};
/*
Skip any whitespace at the current position. Options are:
treatNewlinesAsNonWhitespace: true if newlines are NOT to be treated as whitespace
*/
WikiParser.prototype.skipWhitespace = function(options) {
options = options || {};
var whitespaceRegExp = options.treatNewlinesAsNonWhitespace ? /([^\S\n]+)/mg : /(\s+)/mg;
whitespaceRegExp.lastIndex = this.pos;
var whitespaceMatch = whitespaceRegExp.exec(this.source);
if(whitespaceMatch && whitespaceMatch.index === this.pos) {
this.pos = whitespaceRegExp.lastIndex;
}
};
/*
Get the next match out of an array of parse rule instances
*/
WikiParser.prototype.findNextMatch = function(rules,startPos) {
var nextMatch = undefined,
nextMatchPos = this.sourceLength;
for(var t=0; t<rules.length; t++) {
var matchPos = rules[t].findNextMatch(startPos);
if(matchPos !== undefined && matchPos <= nextMatchPos) {
nextMatch = rules[t];
nextMatchPos = matchPos;
}
}
return nextMatch;
};
/*
Parse any pragmas at the beginning of a block of parse text
*/
WikiParser.prototype.parsePragmas = function() {
while(true) {
// Skip whitespace
this.skipWhitespace();
// Check for the end of the text
if(this.pos >= this.sourceLength) {
return;
}
// Check if we've arrived at a pragma rule match
var nextMatch = this.findNextMatch(this.pragmaRules,this.pos);
// If not, just exit
if(!nextMatch || nextMatch.matchIndex !== this.pos) {
return;
}
// Process the pragma rule
nextMatch.parse();
}
};
/*
Parse a block from the current position
terminatorRegExpString: optional regular expression string that identifies the end of plain paragraphs. Must not include capturing parenthesis
*/
WikiParser.prototype.parseBlock = function(terminatorRegExpString) {
var terminatorRegExp = terminatorRegExpString ? new RegExp("(" + terminatorRegExpString + "|\\r?\\n\\r?\\n)","mg") : /(\r?\n\r?\n)/mg;
this.skipWhitespace();
if(this.pos >= this.sourceLength) {
return [];
}
// Look for a block rule that applies at the current position
var nextMatch = this.findNextMatch(this.blockRules,this.pos);
if(nextMatch && nextMatch.matchIndex === this.pos) {
return nextMatch.parse();
}
// Treat it as a paragraph if we didn't find a block rule
return [{type: "element", tag: "p", children: this.parseRun(terminatorRegExp)}];
};
/*
Parse a series of blocks of text until a terminating regexp is encountered or the end of the text
terminatorRegExpString: terminating regular expression
*/
WikiParser.prototype.parseBlocks = function(terminatorRegExpString) {
if(terminatorRegExpString) {
return this.parseBlocksTerminated(terminatorRegExpString);
} else {
return this.parseBlocksUnterminated();
}
};
/*
Parse a block from the current position to the end of the text
*/
WikiParser.prototype.parseBlocksUnterminated = function() {
var tree = [];
while(this.pos < this.sourceLength) {
tree.push.apply(tree,this.parseBlock());
}
return tree;
};
/*
Parse blocks of text until a terminating regexp is encountered
*/
WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) {
var terminatorRegExp = new RegExp("(" + terminatorRegExpString + ")","mg"),
tree = [];
// Skip any whitespace
this.skipWhitespace();
// Check if we've got the end marker
terminatorRegExp.lastIndex = this.pos;
var match = terminatorRegExp.exec(this.source);
// Parse the text into blocks
while(this.pos < this.sourceLength && !(match && match.index === this.pos)) {
var blocks = this.parseBlock(terminatorRegExpString);
tree.push.apply(tree,blocks);
// Skip any whitespace
this.skipWhitespace();
// Check if we've got the end marker
terminatorRegExp.lastIndex = this.pos;
match = terminatorRegExp.exec(this.source);
}
if(match && match.index === this.pos) {
this.pos = match.index + match[0].length;
}
return tree;
};
/*
Parse a run of text at the current position
terminatorRegExp: a regexp at which to stop the run
*/
WikiParser.prototype.parseRun = function(terminatorRegExp) {
if(terminatorRegExp) {
return this.parseRunTerminated(terminatorRegExp);
} else {
return this.parseRunUnterminated();
}
};
WikiParser.prototype.parseRunUnterminated = function() {
var tree = [];
// Find the next occurrence of a runrule
var nextMatch = this.findNextMatch(this.runRules,this.pos);
// Loop around the matches until we've reached the end of the text
while(this.pos < this.sourceLength && nextMatch) {
// Process the text preceding the run rule
if(nextMatch.matchIndex > this.pos) {
tree.push({type: "text", text: this.source.substring(this.pos,nextMatch.matchIndex)});
this.pos = nextMatch.matchIndex;
}
// Process the run rule
tree.push.apply(tree,nextMatch.parse());
// Look for the next run rule
nextMatch = this.findNextMatch(this.runRules,this.pos);
}
// Process the remaining text
if(this.pos < this.sourceLength) {
tree.push({type: "text", text: this.source.substr(this.pos)});
}
this.pos = this.sourceLength;
return tree;
};
WikiParser.prototype.parseRunTerminated = function(terminatorRegExp) {
var tree = [];
// Find the next occurrence of the terminator
terminatorRegExp.lastIndex = this.pos;
var terminatorMatch = terminatorRegExp.exec(this.source);
// Find the next occurrence of a runrule
var runRuleMatch = this.findNextMatch(this.runRules,this.pos);
// Loop around until we've reached the end of the text
while(this.pos < this.sourceLength && (terminatorMatch || runRuleMatch)) {
// Return if we've found the terminator, and it precedes any run rule match
if(terminatorMatch) {
if(!runRuleMatch || runRuleMatch.matchIndex >= terminatorMatch.index) {
if(terminatorMatch.index > this.pos) {
tree.push({type: "text", text: this.source.substring(this.pos,terminatorMatch.index)});
}
this.pos = terminatorMatch.index;
return tree;
}
}
// Process any run rule, along with the text preceding it
if(runRuleMatch) {
// Preceding text
if(runRuleMatch.matchIndex > this.pos) {
tree.push({type: "text", text: this.source.substring(this.pos,runRuleMatch.matchIndex)});
this.pos = runRuleMatch.matchIndex;
}
// Process the run rule
tree.push.apply(tree,runRuleMatch.parse());
// Look for the next run rule
runRuleMatch = this.findNextMatch(this.runRules,this.pos);
// Look for the next terminator match
terminatorRegExp.lastIndex = this.pos;
terminatorMatch = terminatorRegExp.exec(this.source);
}
}
// Process the remaining text
if(this.pos < this.sourceLength) {
tree.push({type: "text", text: this.source.substr(this.pos)});
}
this.pos = this.sourceLength;
return tree;
};
/*
Parse a run of text preceded by zero or more class specifiers `.classname`
*/
WikiParser.prototype.parseClassedRun = function(terminatorRegExp) {
var classRegExp = /\.([^\s\.]+)/mg,
classNames = [];
classRegExp.lastIndex = this.pos;
var match = classRegExp.exec(this.source);
while(match && match.index === this.pos) {
this.pos = match.index + match[0].length;
classNames.push(match[1]);
var match = classRegExp.exec(this.source);
}
this.skipWhitespace({treatNewlinesAsNonWhitespace: true});
var tree = this.parseRun(terminatorRegExp);
return {
"class": classNames.join(" "),
tree: tree
};
};
exports.WikiParser = WikiParser;
})();

View File

@ -0,0 +1,151 @@
/*\
title: $:/core/modules/rendertree/renderers/element.js
type: application/javascript
module-type: wikirenderer
Element renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Element renderer
*/
var ElementRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
// Compute our dependencies
this.dependencies = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
var tr = $tw.utils.parseTextReference(attribute.textReference);
if(tr.title) {
self.dependencies[tr.title] = true;
} else {
self.dependencies[renderContext.tiddlerTitle] = true;
}
}
});
// Compute our attributes
this.computeAttributes();
// Create the renderers for the child nodes
this.children = this.renderTree.createRenderers(this.renderContext,this.parseTreeNode.children);
};
ElementRenderer.prototype.computeAttributes = function() {
this.attributes = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
self.attributes[name] = self.renderTree.wiki.getTextReference(attribute.textReference,self.renderContext.tiddlerTitle);
} else { // String attribute
self.attributes[name] = attribute.value;
}
});
};
ElementRenderer.prototype.render = function(type) {
var isHtml = type === "text/html",
output = [],attr,a,v;
if(isHtml) {
output.push("<",this.parseTreeNode.tag);
if(this.attributes) {
attr = [];
for(a in this.attributes) {
attr.push(a);
}
attr.sort();
for(a=0; a<attr.length; a++) {
v = this.attributes[attr[a]];
if(v !== undefined) {
if($tw.utils.isArray(v)) {
v = v.join(" ");
} else if(typeof v === "object") {
var s = [];
for(var p in v) {
s.push(p + ":" + v[p] + ";");
}
v = s.join("");
}
output.push(" ",attr[a],"='",$tw.utils.htmlEncode(v),"'");
}
}
}
if(!this.children || this.children.length === 0) {
output.push("/");
}
output.push(">");
}
if(this.children && this.children.length > 0) {
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
if(isHtml) {
output.push("</",this.parseTreeNode.tag,">");
}
}
return output.join("");
};
ElementRenderer.prototype.renderInDom = function() {
// Create the element
this.domNode = document.createElement(this.parseTreeNode.tag);
// Assign the attributes
this.assignAttributes();
// Render any child nodes
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.domNode.appendChild(node.renderInDom());
}
});
// Assign any specified event handlers
$tw.utils.addEventListeners(this.domNode,this.parseTreeNode.events);
// Return the dom node
return this.domNode;
};
ElementRenderer.prototype.assignAttributes = function() {
var self = this;
$tw.utils.each(this.attributes,function(v,a) {
if(v !== undefined) {
if($tw.utils.isArray(v)) { // Ahem, could there be arrays other than className?
self.domNode.className = v.join(" ");
} else if (typeof v === "object") { // ...or objects other than style?
for(var p in v) {
self.domNode.style[$tw.utils.unHyphenateCss(p)] = v[p];
}
} else {
self.domNode.setAttribute(a,v);
}
}
});
};
ElementRenderer.prototype.refreshInDom = function(changes) {
// Check if any of our dependencies have changed
if($tw.utils.checkDependencies(this.dependencies,changes)) {
// Update our attributes
this.computeAttributes();
this.assignAttributes();
}
// Refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changes);
}
});
};
exports.element = ElementRenderer
})();

View File

@ -0,0 +1,35 @@
/*\
title: $:/core/modules/rendertree/renderers/entity.js
type: application/javascript
module-type: wikirenderer
Entity renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Entity renderer
*/
var EntityRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
};
EntityRenderer.prototype.render = function(type) {
return type === "text/html" ? this.parseTreeNode.entity : $tw.utils.entityDecode(this.parseTreeNode.entity);
};
EntityRenderer.prototype.renderInDom = function() {
return document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity));
};
exports.entity = EntityRenderer
})();

View File

@ -0,0 +1,101 @@
/*\
title: $:/core/modules/rendertree/renderers/macrocall.js
type: application/javascript
module-type: wikirenderer
Macro call renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Macro call renderer
*/
var MacroCallRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
// Find the macro definition
var macro,childTree;
if($tw.utils.hop(this.renderTree.parser.macroDefinitions,this.parseTreeNode.name)) {
macro = this.renderTree.parser.macroDefinitions[this.parseTreeNode.name];
}
// Insert an error message if we couldn't find the macro
if(!macro) {
childTree = [{type: "text", text: "<<Undefined macro: " + this.parseTreeNode.name + ">>"}];
} else {
// Substitute the macro parameters
var text = this.substituteParameters(macro.text,this.parseTreeNode,macro);
// Parse the text
childTree = this.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",text).tree;
}
// Create the renderers for the child nodes
this.children = this.renderTree.createRenderers(this.renderContext,childTree);
};
/*
Expand the parameters in a block of text
*/
MacroCallRenderer.prototype.substituteParameters = function(text,macroCallParseTreeNode,macroDefinition) {
var nextAnonParameter = 0; // Next candidate anonymous parameter in macro call
// Step through each of the parameters in the macro definition
for(var p=0; p<macroDefinition.params.length; p++) {
// Check if we've got a macro call parameter with the same name
var paramInfo = macroDefinition.params[p],
paramValue;
for(var m=0; m<macroCallParseTreeNode.params.length; m++) {
if(macroCallParseTreeNode.params[m].name === paramInfo.name) {
paramValue = macroCallParseTreeNode.params[m].value;
}
}
// If not, use the next available anonymous macro call parameter
if(!paramValue && macroCallParseTreeNode.params.length > 0) {
while(macroCallParseTreeNode.params[nextAnonParameter].name && nextAnonParameter < macroCallParseTreeNode.params.length-1) {
nextAnonParameter++;
}
if(!macroCallParseTreeNode.params[nextAnonParameter].name) {
paramValue = macroCallParseTreeNode.params[nextAnonParameter].value;
nextAnonParameter++;
}
}
// If we've still not got a value, use the default, if any
paramValue = paramValue || paramInfo["default"] || "";
// Replace any instances of this parameter
text = text.replace(new RegExp("\\$" + $tw.utils.escapeRegExp(paramInfo.name) + "\\$","mg"),paramValue);
}
return text;
};
MacroCallRenderer.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
MacroCallRenderer.prototype.renderInDom = function() {
// Create the element
this.domNode = document.createElement("macrocall");
this.domNode.setAttribute("data-macro-name",this.parseTreeNode.name);
// Render any child nodes
var self = this;
$tw.utils.each(this.children,function(node,index) {
if(node.renderInDom) {
self.domNode.appendChild(node.renderInDom());
}
});
// Return the dom node
return this.domNode;
};
exports.macrocall = MacroCallRenderer
})();

View File

@ -0,0 +1,37 @@
/*\
title: $:/core/modules/rendertree/renderers/raw.js
type: application/javascript
module-type: wikirenderer
Raw HTML renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Raw HTML renderer
*/
var RawRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
};
RawRenderer.prototype.render = function(type) {
return this.parseTreeNode.html;
};
RawRenderer.prototype.renderInDom = function() {
var domNode = document.createElement("div");
domNode.innerHTML = this.parseTreeNode.html;
return domNode;
};
exports.raw = RawRenderer
})();

View File

@ -0,0 +1,35 @@
/*\
title: $:/core/modules/rendertree/renderers/text.js
type: application/javascript
module-type: wikirenderer
Text renderer
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Text renderer
*/
var TextRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
};
TextRenderer.prototype.render = function(type) {
return type === "text/html" ? $tw.utils.htmlEncode(this.parseTreeNode.text) : this.parseTreeNode.text;
};
TextRenderer.prototype.renderInDom = function() {
return document.createTextNode(this.parseTreeNode.text);
};
exports.text = TextRenderer
})();

View File

@ -0,0 +1,151 @@
/*\
title: $:/core/modules/rendertree/renderers/widget.js
type: application/javascript
module-type: wikirenderer
Widget renderer.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Widget renderer
*/
var WidgetRenderer = function(renderTree,renderContext,parseTreeNode) {
// Store state information
this.renderTree = renderTree;
this.renderContext = renderContext;
this.parseTreeNode = parseTreeNode;
// Compute the default dependencies
this.dependencies = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
var tr = self.renderTree.wiki.parseTextReference(attribute.textReference);
if(tr.title) {
self.dependencies[tr.title] = true;
} else {
self.dependencies[renderContext.tiddlerTitle] = true;
}
}
});
// Compute our attributes
this.attributes = {};
this.computeAttributes();
// Create the widget object
var WidgetClass = this.renderTree.parser.vocabulary.widgetClasses[this.parseTreeNode.tag];
if(WidgetClass) {
this.widget = new WidgetClass(this);
} else {
// Error if we couldn't find the widget
this.children = this.renderTree.createRenderers(this.renderContext,[
{type: "text", text: "Unknown widget type '" + this.parseTreeNode.tag + "'"}
]);
}
};
WidgetRenderer.prototype.computeAttributes = function() {
var changedAttributes = {};
var self = this;
$tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) {
if(attribute.type === "indirect") {
var value = self.renderTree.wiki.getTextReference(attribute.textReference,self.renderContext.tiddlerTitle);
if(self.attributes[name] !== value) {
self.attributes[name] = value;
changedAttributes[name] = true;
}
} else { // String attribute
if(self.attributes[name] !== attribute.value) {
self.attributes[name] = attribute.value;
changedAttributes[name] = true;
}
}
});
return changedAttributes;
};
WidgetRenderer.prototype.hasAttribute = function(name) {
return $tw.utils.hop(this.attributes,name);
};
WidgetRenderer.prototype.getAttribute = function(name,defaultValue) {
if($tw.utils.hop(this.attributes,name)) {
return this.attributes[name];
} else {
return defaultValue;
}
};
WidgetRenderer.prototype.render = function(type) {
// Render the widget if we've got one
if(this.widget && this.widget.render) {
return this.widget.render(type);
}
};
WidgetRenderer.prototype.renderInDom = function() {
// Create the wrapper element
this.domNode = document.createElement("widget");
this.domNode.setAttribute("data-widget-type",this.parseTreeNode.tag);
this.domNode.setAttribute("data-widget-attr",JSON.stringify(this.attributes));
// Render the widget if we've got one
if(this.widget && this.widget.renderInDom) {
this.widget.renderInDom(this.domNode);
}
// Return the dom node
return this.domNode;
};
WidgetRenderer.prototype.refreshInDom = function(changedTiddlers) {
// Refresh if the widget cleared the depencies hashmap to indicate that it should always be refreshed, or if any of our dependencies have changed
if(!this.dependencies || $tw.utils.checkDependencies(this.dependencies,changedTiddlers)) {
// Update our attributes
var changedAttributes = this.computeAttributes();
// Refresh the widget
if(this.widget && this.widget.refreshInDom) {
this.widget.refreshInDom(changedAttributes,changedTiddlers);
return;
}
}
// If the widget itself didn't need refreshing, just refresh any child nodes
var self = this;
$tw.utils.each(this.children,function(node,index) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
};
WidgetRenderer.prototype.getContextTiddlerTitle = function() {
return this.renderContext ? this.renderContext.tiddlerTitle : undefined;
};
/*
Check for render context recursion by returning true if the members of a proposed new render context are already present in the render context chain
*/
WidgetRenderer.prototype.checkContextRecursion = function(newRenderContext) {
var context = this.renderContext;
while(context) {
var match = true;
for(var member in newRenderContext) {
if($tw.utils.hop(newRenderContext,member)) {
if(newRenderContext[member] && newRenderContext[member] !== context[member]) {
match = false;
}
}
}
if(match) {
return true;
}
context = context.parentContext;
}
return false;
};
exports.widget = WidgetRenderer
})();

View File

@ -0,0 +1,91 @@
/*\
title: $:/core/modules/rendertree/wikirendertree.js
type: application/javascript
module-type: global
Wiki text render tree
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Create a render tree object for a parse tree
*/
var WikiRenderTree = function(parser,options) {
this.parser = parser;
this.wiki = options.wiki;
};
/*
Generate the full render tree for this parse tree
renderContext: see below
An renderContext consists of these fields:
tiddlerTitle: title of the tiddler providing the context
parentContext: reference back to previous context in the stack
*/
WikiRenderTree.prototype.execute = function(renderContext) {
this.rendererTree = this.createRenderers(renderContext,this.parser.tree);
};
/*
Create an array of renderers for an array of parse tree nodes
*/
WikiRenderTree.prototype.createRenderers = function(renderContext,parseTreeNodes) {
var rendererNodes = [];
for(var t=0; t<parseTreeNodes.length; t++) {
rendererNodes.push(this.createRenderer(renderContext,parseTreeNodes[t]));
}
return rendererNodes;
};
/*
Create a renderer node for a parse tree node
*/
WikiRenderTree.prototype.createRenderer = function(renderContext,parseTreeNode) {
var RenderNodeClass = this.parser.vocabulary.rendererClasses[parseTreeNode.type];
return new RenderNodeClass(this,renderContext,parseTreeNode);
};
/*
Render as a string
*/
WikiRenderTree.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.rendererTree,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
/*
Render to the DOM
*/
WikiRenderTree.prototype.renderInDom = function(container) {
this.container = container;
$tw.utils.each(this.rendererTree,function(node) {
if(node.renderInDom) {
container.appendChild(node.renderInDom());
}
});
};
/*
Update the DOM rendering in the light of a set of changes
*/
WikiRenderTree.prototype.refreshInDom = function(changes) {
$tw.utils.each(this.rendererTree,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changes);
}
});
};
exports.WikiRenderTree = WikiRenderTree;
})();

View File

@ -0,0 +1,34 @@
/*\
title: $:/core/modules/testnewwikiparser.js
type: application/javascript
module-type: global
Test the new parser
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var testNewParser = function() {
$tw.wiki.new_initParsers();
var templateTitle = "$:/templates/NewPageTemplate";
var parser = $tw.wiki.new_parseTiddler(templateTitle);
console.log("parsetree after execution",parser);
var renderTree = new $tw.WikiRenderTree(parser,{wiki: $tw.wiki});
renderTree.execute({tiddlerTitle: templateTitle});
console.log("html rendering:",renderTree.render("text/html"));
console.log("renderTree after execution",renderTree);
var container = document.createElement("div");
document.body.insertBefore(container,document.body.firstChild);
renderTree.renderInDom(container);
$tw.wiki.addEventListener("",function(changes) {
renderTree.refreshInDom(changes);
});
};
exports.testNewParser = testNewParser;
})();

View File

@ -0,0 +1,145 @@
/*\
title: $:/core/modules/widget/button.js
type: application/javascript
module-type: widget
Implements the button widget.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var ButtonWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
ButtonWidget.prototype.generateChildNodes = function() {
// Get the parameters from the attributes
this.message = this.renderer.getAttribute("message");
this.param = this.renderer.getAttribute("param");
this.set = this.renderer.getAttribute("set");
this.setTo = this.renderer.getAttribute("setTo");
this.popup = this.renderer.getAttribute("popup");
this.hover = this.renderer.getAttribute("hover");
this.qualifyTiddlerTitles = this.renderer.getAttribute("qualifyTiddlerTitles");
this["class"] = this.renderer.getAttribute("class");
// Compose the button
var classes = ["tw-tiddlybutton"];
if(this.classes) {
$tw.utils.pushTop(classes,this.classes);
}
if(this["class"]) {
$tw.utils.pushTop(classes,this["class"]);
}
var events = [{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"}];
if(this.hover === "yes") {
events.push({name: "mouseover", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
events.push({name: "mouseout", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
}
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[{
type: "element",
tag: "button",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: this.renderer.parseTreeNode.children,
events: events
}]);
};
ButtonWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
ButtonWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
ButtonWidget.prototype.dispatchMessage = function(event) {
$tw.utils.dispatchCustomEvent(event.target,this.message,{
param: this.param,
tiddlerTitle: this.renderer.getContextTiddlerTitle()
});
};
ButtonWidget.prototype.triggerPopup = function(event) {
$tw.popup.triggerPopup({
textRef: this.popup,
domNode: this.renderer.domNode,
qualifyTiddlerTitles: this.qualifyTiddlerTitles,
renderContext: this.renderer.renderContext,
wiki: this.renderer.renderTree.wiki
});
};
ButtonWidget.prototype.setTiddler = function() {
var tiddler = this.renderer.renderTree.wiki.getTiddler(this.set);
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.set, text: this.setTo}));
};
ButtonWidget.prototype.handleClickEvent = function(event) {
if(this.message) {
this.dispatchMessage(event);
}
if(this.popup) {
this.triggerPopup(event);
}
if(this.set && this.setTo) {
this.setTiddler();
}
event.preventDefault();
return false;
};
ButtonWidget.prototype.handleMouseOverOrOutEvent = function(event) {
if(this.popup) {
this.triggerPopup(event);
}
event.preventDefault();
return false;
};
ButtonWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.qualifyTiddlerTitles || changedAttributes["class"] || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.button = ButtonWidget;
})();

View File

@ -0,0 +1,147 @@
/*\
title: $:/core/modules/widget/link.js
type: application/javascript
module-type: widget
Implements the link widget.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var isLinkExternal = function(to) {
var externalRegExp = /(?:file|http|https|mailto|ftp|irc|news|data):[^\s'"]+(?:\/|\b)/i;
return externalRegExp.test(to);
};
var LinkWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
LinkWidget.prototype.generateChildNodes = function() {
// Get the parameters from the attributes
this.to = this.renderer.getAttribute("to");
this.hover = this.renderer.getAttribute("hover");
this.qualifyHoverTitle = this.renderer.getAttribute("qualifyHoverTitle");
// Determine the default link characteristics
this.isExternal = isLinkExternal(this.to);
if(!this.isExternal) {
this.isMissing = !this.renderer.renderTree.wiki.tiddlerExists(this.to);
}
// Compose the link
var classes = ["tw-tiddlylink"]
if(this.isExternal) {
$tw.utils.pushTop(classes,"tw-tiddlylink-external");
} else {
$tw.utils.pushTop(classes,"tw-tiddlylink-internal");
if(this.isMissing) {
$tw.utils.pushTop(classes,"tw-tiddlylink-missing");
} else {
$tw.utils.pushTop(classes,"tw-tiddlylink-resolves");
}
}
if(this.classes) {
$tw.utils.pushTop(classes,this.classes);
}
var events = [{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"}];
if(this.hover) {
events.push({name: "mouseover", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
events.push({name: "mouseout", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
}
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[{
type: "element",
tag: "a",
attributes: {
href: {type: "string", value: this.isExternal ? this.to : encodeURIComponent(this.to)},
"class": {type: "string", value: classes.join(" ")}
},
children: this.renderer.parseTreeNode.children,
events: events
}]);
};
LinkWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
LinkWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
LinkWidget.prototype.handleClickEvent = function(event) {
if(isLinkExternal(this.to)) {
event.target.setAttribute("target","_blank");
return true;
} else {
$tw.utils.dispatchCustomEvent(event.target,"tw-navigate",{
navigateTo: this.to,
navigateFromNode: this,
navigateFromClientRect: this.children[0].domNode.getBoundingClientRect()
});
event.preventDefault();
return false;
}
};
LinkWidget.prototype.handleMouseOverOrOutEvent = function(event) {
if(this.hover) {
$tw.popup.triggerPopup({
textRef: this.hover,
domNode: this.children[0].domNode,
qualifyTiddlerTitles: this.qualifyHoverTitle,
contextTiddlerTitle: this.renderer.getContextTiddlerTitle(),
wiki: this.renderer.renderTree.wiki
});
}
event.preventDefault();
return false;
};
LinkWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Set the class for missing tiddlers
if(this.targetTitle) {
$tw.utils.toggleClass(this.children[0].domNode,"tw-tiddler-missing",!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle));
}
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.to || changedAttributes.hover || (this.to && changedTiddlers[this.to]) || (this.hover && changedTiddlers[this.hover])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.link = LinkWidget;
})();

View File

@ -0,0 +1,295 @@
/*\
title: $:/core/modules/widgets/list/list.js
type: application/javascript
module-type: widget
The list widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
These types are shorthands for particular filters
*/
var typeMappings = {
all: "[!is[shadow]sort[title]]",
recent: "[!is[shadow]sort[modified]]",
missing: "[is[missing]sort[title]]",
orphans: "[is[orphan]sort[title]]",
shadowed: "[is[shadow]sort[title]]"
};
var ListWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
ListWidget.prototype.generateChildNodes = function() {
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get our attributes
this.itemClass = this.renderer.getAttribute("itemClass");
this.template = this.renderer.getAttribute("template");
this.editTemplate = this.renderer.getAttribute("editTemplate");
this.emptyMessage = this.renderer.getAttribute("emptyMessage");
// Get the list of tiddlers object
this.getTiddlerList();
// Create the list
var listMembers = [];
if(this.list.length === 0) {
// Check for an empty list
listMembers = [this.getEmptyMessage()];
} else {
// Create the list
for(var t=0; t<this.list.length; t++) {
listMembers.push(this.createListElement(this.list[t]));
}
}
// Create the list frame element
var classes = ["tw-list-frame"];
if(this.classes) {
$tw.utils.pushTop(classes,this.classes);
}
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[{
type: "element",
tag: "div",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: listMembers
}]);
};
ListWidget.prototype.getTiddlerList = function() {
var filter;
if(this.renderer.hasAttribute("type")) {
filter = typeMappings[this.renderer.getAttribute("type")];
} else if(this.renderer.hasAttribute("filter")) {
filter = this.renderer.getAttribute("filter");
}
if(!filter) {
filter = "[!is[shadow]]";
}
this.list = this.renderer.renderTree.wiki.filterTiddlers(filter,this.renderer.getContextTiddlerTitle());
};
/*
Create and execute the nodes representing the empty message
*/
ListWidget.prototype.getEmptyMessage = function() {
return {
type: "element",
tag: "span",
children: this.renderer.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",this.emptyMessage).tree
};
};
/*
Create a list element representing a given tiddler
*/
ListWidget.prototype.createListElement = function(title) {
// Define an event handler that adds navigation information to the event
var handleEvent = function(event) {
event.navigateFromTitle = title;
return true;
},
classes = ["tw-list-element"];
// Add any specified classes
if(this.itemClass) {
$tw.utils.pushTop(classes,this.itemClass);
}
// Return the list element
return {
type: "element",
tag: "div",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: [this.createListElementMacro(title)],
events: [
{name: "tw-navigate", handlerFunction: handleEvent},
{name: "tw-EditTiddler", handlerFunction: handleEvent},
{name: "tw-SaveTiddler", handlerFunction: handleEvent},
{name: "tw-CloseTiddler", handlerFunction: handleEvent},
{name: "tw-NewTiddler", handlerFunction: handleEvent}
]
};
};
/*
Create the tiddler macro needed to represent a given tiddler
*/
ListWidget.prototype.createListElementMacro = function(title) {
// Check if the tiddler is a draft
var tiddler = this.renderer.renderTree.wiki.getTiddler(title),
isDraft = tiddler ? tiddler.hasField("draft.of") : false;
// Figure out the template to use
var template = this.template,
templateTree = undefined;
if(isDraft && this.editTemplate) {
template = this.editTemplate;
}
// Check for not having a template
if(!template) {
if(this.renderer.parseTreeNode.children && this.renderer.parseTreeNode.children.length > 0) {
// Use our content as the template
templateTree = this.renderer.parseTreeNode.children;
} else {
// Use default content
templateTree = [{
type: "widget",
tag: "view",
attributes: {
field: {type: "string", value: "title"},
format: {type: "string", value: "link"}
}
}];
}
}
// Create the tiddler macro
return {
type: "widget",
tag: "transclude",
attributes: {
target: {type: "string", value: title},
template: {type: "string", value: template}
},
children: templateTree
};
};
/*
Remove a list element from the list, along with the attendant DOM nodes
*/
ListWidget.prototype.removeListElement = function(index) {
// Get the list element
var listElement = this.children[0].children[index];
// Remove the DOM node
listElement.domNode.parentNode.removeChild(listElement.domNode);
// Then delete the actual renderer node
this.children[0].children.splice(index,1);
};
/*
Return the index of the list element that corresponds to a particular title
startIndex: index to start search (use zero to search from the top)
title: tiddler title to seach for
*/
ListWidget.prototype.findListElementByTitle = function(startIndex,title) {
while(startIndex < this.children[0].children.length) {
if(this.children[0].children[startIndex].children[0].attributes.target === title) {
return startIndex;
}
startIndex++;
}
return undefined;
};
ListWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
ListWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
ListWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Reexecute the widget if any of our attributes have changed
if(changedAttributes.itemClass || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.type || changedAttributes.filter || changedAttributes.template) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// Handle any changes to the list, and refresh any nodes we're reusing
this.handleListChanges(changedTiddlers);
}
};
ListWidget.prototype.handleListChanges = function(changedTiddlers) {
var t,
prevListLength = this.list.length,
frame = this.children[0];
// Get the list of tiddlers, having saved the previous length
this.getTiddlerList();
// Check if the list is empty
if(this.list.length === 0) {
// Check if it was empty before
if(prevListLength === 0) {
// If so, just refresh the empty message
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
return;
} else {
// If the list wasn't empty before, empty it
for(t=prevListLength-1; t>=0; t--) {
this.removeListElement(t);
}
// Insert the empty message
frame.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[this.getEmptyMessage()]);
$tw.utils.each(frame.children,function(node) {
if(node.renderInDom) {
frame.domNode.appendChild(node.renderInDom());
}
});
return;
}
} else {
// If it is not empty now, but was empty previously, then remove the empty message
if(prevListLength === 0) {
this.removeListElement(0);
}
}
// Step through the list and adjust our child list elements appropriately
for(t=0; t<this.list.length; t++) {
// Check to see if the list element is already there
var index = this.findListElementByTitle(t,this.list[t]);
if(index === undefined) {
// The list element isn't there, so we need to insert it
frame.children.splice(t,0,this.renderer.renderTree.createRenderer(this.renderer.renderContext,this.createListElement(this.list[t])));
frame.domNode.insertBefore(frame.children[t].renderInDom(),frame.domNode.childNodes[t]);
} else {
// Delete any list elements preceding the one we want
for(var n=index-1; n>=t; n--) {
this.removeListElement(n);
}
// Refresh the node we're reusing
frame.children[t].refreshInDom(changedTiddlers);
}
}
// Remove any left over elements
for(t=frame.children.length-1; t>=this.list.length; t--) {
this.removeListElement(t);
}
};
exports.list = ListWidget;
})();

View File

@ -0,0 +1,241 @@
/*\
title: $:/core/modules/widget/navigator.js
type: application/javascript
module-type: widget
Implements the navigator widget.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var NavigatorWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
NavigatorWidget.prototype.generateChildNodes = function() {
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get our parameters
this.storyTitle = this.renderer.getAttribute("story");
this.historyTitle = this.renderer.getAttribute("history");
// Render our children
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,this.renderer.parseTreeNode.children);
};
NavigatorWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
NavigatorWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
// Attach our event handlers
$tw.utils.addEventListeners(this.renderer.domNode,[
{name: "tw-navigate", handlerObject: this, handlerMethod: "handleNavigateEvent"},
{name: "tw-EditTiddler", handlerObject: this, handlerMethod: "handleEditTiddlerEvent"},
{name: "tw-SaveTiddler", handlerObject: this, handlerMethod: "handleSaveTiddlerEvent"},
{name: "tw-close", handlerObject: this, handlerMethod: "handleCloseTiddlerEvent"},
{name: "tw-NewTiddler", handlerObject: this, handlerMethod: "handleNewTiddlerEvent"}
]);
};
NavigatorWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
};
NavigatorWidget.prototype.getStoryList = function() {
var text = this.renderer.renderTree.wiki.getTextReference(this.storyTitle,"");
if(text && text.length > 0) {
this.storyList = text.split("\n");
} else {
this.storyList = [];
}
};
NavigatorWidget.prototype.saveStoryList = function() {
var storyTiddler = this.renderer.renderTree.wiki.getTiddler(this.storyTitle);
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler({
title: this.storyTitle
},storyTiddler,{text: this.storyList.join("\n")}));
};
NavigatorWidget.prototype.findTitleInStory = function(title,defaultIndex) {
for(var t=0; t<this.storyList.length; t++) {
if(this.storyList[t] === title) {
return t;
}
}
return defaultIndex;
};
// Navigate to a specified tiddler
NavigatorWidget.prototype.handleNavigateEvent = function(event) {
if(this.storyTitle) {
// Update the story tiddler if specified
this.getStoryList();
// See if the tiddler is already there
var slot = this.findTitleInStory(event.navigateTo,-1);
// If not we need to add it
if(slot === -1) {
// First we try to find the position of the story element we navigated from
slot = this.findTitleInStory(event.navigateFromTitle,-1) + 1;
// Add the tiddler
this.storyList.splice(slot,0,event.navigateTo);
// Save the story
this.saveStoryList();
}
}
// Add a new record to the top of the history stack
if(this.historyTitle) {
var historyList = this.renderer.renderTree.wiki.getTiddlerData(this.historyTitle,[]);
historyList.push({title: event.navigateTo, fromPageRect: event.navigateFromClientRect});
this.renderer.renderTree.wiki.setTiddlerData(this.historyTitle,historyList);
}
event.stopPropagation();
return false;
};
// Close a specified tiddler
NavigatorWidget.prototype.handleCloseTiddlerEvent = function(event) {
this.getStoryList();
// Look for tiddlers with this title to close
var slot = this.findTitleInStory(event.tiddlerTitle,-1);
if(slot !== -1) {
this.storyList.splice(slot,1);
this.saveStoryList();
}
event.stopPropagation();
return false;
};
// Place a tiddler in edit mode
NavigatorWidget.prototype.handleEditTiddlerEvent = function(event) {
this.getStoryList();
// Replace the specified tiddler with a draft in edit mode
for(var t=0; t<this.storyList.length; t++) {
if(this.storyList[t] === event.tiddlerTitle) {
// Compute the title for the draft
var draftTitle = "Draft " + (new Date()) + " of " + event.tiddlerTitle;
this.storyList[t] = draftTitle;
// Get the current value of the tiddler we're editing
var tiddler = this.renderer.renderTree.wiki.getTiddler(event.tiddlerTitle);
// Save the initial value of the draft tiddler
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler(
{
text: "Type the text for the tiddler '" + event.tiddlerTitle + "'"
},
tiddler,
{
title: draftTitle,
"draft.title": event.tiddlerTitle,
"draft.of": event.tiddlerTitle
}));
}
}
this.saveStoryList();
event.stopPropagation();
return false;
};
// Take a tiddler out of edit mode, saving the changes
NavigatorWidget.prototype.handleSaveTiddlerEvent = function(event) {
this.getStoryList();
var storyTiddlerModified = false;
for(var t=0; t<this.storyList.length; t++) {
if(this.storyList[t] === event.tiddlerTitle) {
var tiddler = this.renderer.renderTree.wiki.getTiddler(event.tiddlerTitle);
if(tiddler && $tw.utils.hop(tiddler.fields,"draft.title")) {
// Save the draft tiddler as the real tiddler
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler(tiddler,{
title: tiddler.fields["draft.title"],
modified: new Date(),
"draft.title": undefined,
"draft.of": undefined
}));
// Remove the draft tiddler
this.renderer.renderTree.wiki.deleteTiddler(event.tiddlerTitle);
// Remove the original tiddler if we're renaming it
if(tiddler.fields["draft.of"] !== tiddler.fields["draft.title"]) {
this.renderer.renderTree.wiki.deleteTiddler(tiddler.fields["draft.of"]);
}
// Make the story record point to the newly saved tiddler
this.storyList[t] = tiddler.fields["draft.title"];
// Check if we're modifying the story tiddler itself
if(tiddler.fields["draft.title"] === this.storyTitle) {
storyTiddlerModified = true;
}
}
}
}
if(!storyTiddlerModified) {
this.saveStoryList();
}
event.stopPropagation();
return false;
};
// Create a new draft tiddler
NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
// Get the story details
this.getStoryList();
// Create the new tiddler
var title;
for(var t=0; true; t++) {
title = "New Tiddler" + (t ? " " + t : "");
if(!this.renderer.renderTree.wiki.tiddlerExists(title)) {
break;
}
}
var tiddler = new $tw.Tiddler({
title: title,
text: "Newly created tiddler"
});
this.renderer.renderTree.wiki.addTiddler(tiddler);
// Create the draft tiddler
var draftTitle = "New Tiddler at " + (new Date()),
draftTiddler = new $tw.Tiddler({
text: "Type the text for the new tiddler",
title: draftTitle,
"draft.title": title,
"draft.of": title
});
this.renderer.renderTree.wiki.addTiddler(draftTiddler);
// Update the story to insert the new draft at the top
var slot = this.findTitleInStory(event.navigateFromTitle,-1) + 1;
this.storyList.splice(slot,0,draftTitle);
// Save the updated story
this.saveStoryList();
// Add a new record to the top of the history stack
var history = this.renderer.renderTree.wiki.getTiddlerData(this.historyTitle,[]);
history.push({title: draftTitle});
this.renderer.renderTree.wiki.setTiddlerData(this.historyTitle,historyList);
event.stopPropagation();
return false;
};
exports.navigator = NavigatorWidget;
})();

View File

@ -0,0 +1,150 @@
/*\
title: $:/core/modules/widgets/transclude.js
type: application/javascript
module-type: widget
The transclude widget includes another tiddler into the tiddler being rendered.
Attributes:
target: the title of the tiddler to transclude
template: the title of the tiddler to use as a template for the transcluded tiddler
The simplest case is to just supply a target tiddler:
{{{
<_transclude target="Foo"/>
}}}
This will render the tiddler Foo within the current tiddler. If the tiddler Foo includes
the view widget (or other widget that reference the fields of the current tiddler), then the
fields of the tiddler Foo will be accessed.
If you want to transclude the tiddler as a template, so that the fields referenced by the view
widget are those of the tiddler doing the transcluding, then you can instead specify the tiddler
as a template:
{{{
<_transclude template="Foo"/>
}}}
The effect is the same as the previous example: the text of the tiddler Foo is rendered. The
difference is that the view widget will access the fields of the tiddler doing the transcluding.
The `target` and `template` attributes may be combined:
{{{
<_transclude template="Bar" target="Foo"/>
}}}
Here, the text of the tiddler `Bar` will be transcluded, with the widgets within it accessing the fields
of the tiddler `Foo`.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var TranscludeWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
TranscludeWidget.prototype.generateChildNodes = function() {
var tr, templateParseTree, templateTiddler;
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get the render target details
this.targetTitle = this.renderer.getAttribute("target",this.renderer.getContextTiddlerTitle());
// Get the render tree for the template
this.templateTitle = undefined;
if(this.renderer.parseTreeNode.children && this.renderer.parseTreeNode.children.length > 0) {
// Use the child nodes as the template if we've got them
templateParseTree = this.renderer.parseTreeNode.children;
} else {
this.templateTitle = this.renderer.getAttribute("template",this.targetTitle);
// Check for recursion
if(this.renderer.checkContextRecursion({
tiddlerTitle: this.targetTitle,
templateTitle: this.templateTitle
})) {
templateParseTree = [{type: "text", text: "Tiddler recursion error in transclude widget"}];
} else {
var parser = this.renderer.renderTree.wiki.new_parseTiddler(this.templateTitle);
templateParseTree = parser ? parser.tree : [];
}
}
// Set up the attributes for the wrapper element
var classes = [];
if(!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle)) {
$tw.utils.pushTop(classes,"tw-tiddler-missing");
}
// Create the renderers for the wrapper and the children
var newRenderContext = {
tiddlerTitle: this.targetTitle,
templateTitle: this.templateTitle,
parentContext: this.renderer.renderContext
};
this.children = this.renderer.renderTree.createRenderers(newRenderContext,[{
type: "element",
tag: this.renderer.parseTreeNode.isBlock ? "div" : "span",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: templateParseTree
}]);
};
TranscludeWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
TranscludeWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
TranscludeWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Set the class for missing tiddlers
if(this.targetTitle) {
$tw.utils.toggleClass(this.children[0].domNode,"tw-tiddler-missing",!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle));
}
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.target || changedAttributes.template || (this.targetTitle && changedTiddlers[this.targetTitle]) || (this.templateTitle && changedTiddlers[this.templateTitle])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.transclude = TranscludeWidget;
})();

View File

@ -0,0 +1,141 @@
/*\
title: $:/core/modules/widgets/view.js
type: application/javascript
module-type: widget
The view widget displays a tiddler field.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Define the "text" viewer here so that it is always available
*/
var TextViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
TextViewer.prototype.render = function() {
// Get the value as a string
if(this.field !== "text" && this.tiddler) {
this.value = this.tiddler.getFieldString(this.field);
}
var value = "";
if(this.value !== undefined && this.value !== null) {
value = this.value;
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,[{
type: "text",
text: value
}]);
};
// We'll cache the available field viewers here
var fieldViewers = undefined;
var ViewWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Initialise the field viewers if they've not been done already
if(!fieldViewers) {
fieldViewers = {text: TextViewer}; // Start with the built-in text viewer
$tw.modules.applyMethods("newfieldviewer",fieldViewers);
}
// Generate child nodes
this.generateChildNodes();
};
ViewWidget.prototype.generateChildNodes = function() {
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get parameters from our attributes
this.tiddlerTitle = this.renderer.getAttribute("tiddler",this.renderer.getContextTiddlerTitle());
this.fieldName = this.renderer.getAttribute("field","text");
this.format = this.renderer.getAttribute("format","text");
// Get the value to display
var tiddler = this.renderer.renderTree.wiki.getTiddler(this.tiddlerTitle),
value;
if(tiddler) {
if(this.fieldName === "text") {
// Calling getTiddlerText() triggers lazy loading of skinny tiddlers
value = this.renderer.renderTree.wiki.getTiddlerText(this.tiddlerTitle);
} else {
value = tiddler.fields[this.fieldName];
}
} else { // Use a special value if the tiddler is missing
switch(this.fieldName) {
case "title":
value = this.tiddlerTitle;
break;
case "modified":
case "created":
value = new Date();
break;
default:
value = "";
break;
}
}
// Choose the viewer to use
var Viewer = fieldViewers.text;
if($tw.utils.hop(fieldViewers,this.format)) {
Viewer = fieldViewers[this.format];
}
this.viewer = new Viewer(this,tiddler,this.fieldName,value);
// Ask the viewer to create the children
this.children = this.viewer.render();
};
ViewWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
ViewWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
ViewWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.format || (this.tiddlerTitle && changedTiddlers[this.tiddlerTitle])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.view = ViewWidget;
})();

View File

@ -0,0 +1,36 @@
/*\
title: $:/core/modules/widgets/view/viewers/date.js
type: application/javascript
module-type: newfieldviewer
A viewer for viewing tiddler fields as a date
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var DateViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
DateViewer.prototype.render = function() {
var template = this.viewWidget.renderer.getAttribute("template","DD MMM YYYY"),
value = "";
if(this.value !== undefined) {
value = $tw.utils.formatDateString(this.value,template);
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,[{
type: "text",
text: value
}]);
};
exports.date = DateViewer;
})();

View File

@ -0,0 +1,44 @@
/*\
title: $:/core/modules/widgets/view/viewers/link.js
type: application/javascript
module-type: newfieldviewer
A viewer for viewing tiddler fields as a link
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var LinkViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
LinkViewer.prototype.render = function() {
var parseTree = [];
if(this.value === undefined) {
parseTree.push({type: "text", text: ""});
} else {
parseTree.push({
type: "widget",
tag: "link",
attributes: {
to: {type: "string", value: this.value}
},
children: [{
type: "text",
text: this.value
}]
})
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,parseTree);
};
exports.link = LinkViewer;
})();

View File

@ -0,0 +1,41 @@
/*\
title: $:/core/modules/widgets/view/viewers/wikified.js
type: application/javascript
module-type: newfieldviewer
A viewer for viewing tiddler fields as wikified text
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var WikifiedViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
WikifiedViewer.prototype.render = function() {
var parseTree;
// If we're viewing the text field of a tiddler then we'll transclude it
if(this.tiddler && this.field === "text") {
parseTree = [{
type: "widget",
tag: "transclude",
attributes: {
target: {type: "string", value: this.tiddler.fields.title}
}
}];
} else {
parseTree = this.viewWidget.renderer.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",this.value).tree;
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,parseTree);
};
exports.wikified = WikifiedViewer;
})();

View File

@ -32,7 +32,7 @@ Get the value of a text reference. Text references can have any of these forms:
##<fieldname> - specifies a field of the current tiddlers
*/
exports.getTextReference = function(textRef,defaultText,currTiddlerTitle) {
var tr = this.parseTextReference(textRef),
var tr = $tw.utils.parseTextReference(textRef),
title = tr.title || currTiddlerTitle,
field = tr.field || "text",
tiddler = this.getTiddler(title);
@ -44,7 +44,7 @@ exports.getTextReference = function(textRef,defaultText,currTiddlerTitle) {
};
exports.setTextReference = function(textRef,value,currTiddlerTitle) {
var tr = this.parseTextReference(textRef),
var tr = $tw.utils.parseTextReference(textRef),
title,tiddler,fields;
// Check if it is a reference to a tiddler
if(tr.title && !tr.field) {
@ -63,7 +63,7 @@ exports.setTextReference = function(textRef,value,currTiddlerTitle) {
};
exports.deleteTextReference = function(textRef,currTiddlerTitle) {
var tr = this.parseTextReference(textRef),
var tr = $tw.utils.parseTextReference(textRef),
title,tiddler,fields;
// Check if it is a reference to a tiddler
if(tr.title && !tr.field) {
@ -80,33 +80,6 @@ exports.deleteTextReference = function(textRef,currTiddlerTitle) {
}
};
/*
Parse a text reference into its constituent parts
*/
exports.parseTextReference = function(textRef,currTiddlerTitle) {
// Look for a metadata field separator
var pos = textRef.indexOf("!!");
if(pos !== -1) {
if(pos === 0) {
// Just a field
return {
field: textRef.substring(2)
};
} else {
// Field and title
return {
title: textRef.substring(0,pos),
field: textRef.substring(pos + 2)
};
}
} else {
// Otherwise, we've just got a title
return {
title: textRef
};
}
};
exports.addEventListener = function(filter,listener) {
this.eventListeners = this.eventListeners || [];
this.eventListeners.push({
@ -184,9 +157,8 @@ exports.addTiddler = function(tiddler) {
if(!(tiddler instanceof $tw.Tiddler)) {
tiddler = new $tw.Tiddler(tiddler);
}
// Get the title, and the current tiddler with that title
var title = tiddler.fields.title,
prevTiddler = this.tiddlers[title];
// Get the title
var title = tiddler.fields.title;
// Save the tiddler
this.tiddlers[title] = tiddler;
this.clearCache(title);
@ -384,6 +356,54 @@ exports.clearCache = function(title) {
}
};
exports.new_initParsers = function() {
// Create a default vocabulary
this.vocabulary = new $tw.WikiVocabulary({wiki: this});
};
/*
Parse a block of text of a specified MIME type
*/
exports.new_parseText = function(type,text) {
return this.vocabulary.parseText(type,text);
};
/*
Parse a tiddler according to its MIME type
*/
exports.new_parseTiddler = function(title,options) {
var tiddler = this.getTiddler(title),
self = this;
return tiddler ? this.getCacheForTiddler(title,"newParseTree",function() {
return self.new_parseText(tiddler.fields.type,tiddler.fields.text);
}) : null;
};
/*
Parse text in a specified format and render it into another format
outputType: content type for the output
textType: content type of the input text
text: input text
*/
exports.new_renderText = function(outputType,textType,text) {
var parser = this.new_parseText(textType,text),
renderTree = new $tw.WikiRenderTree(parser,{wiki: this});
renderTree.execute();
return renderTree.render(outputType);
};
/*
Parse text from a tiddler and render it into another format
outputType: content type for the output
title: title of the tiddler to be rendered
*/
exports.new_renderTiddler = function(outputType,title) {
var parser = this.new_parseTiddler(title),
renderTree = new $tw.WikiRenderTree(parser,{wiki: this});
renderTree.execute();
return renderTree.render(outputType);
};
exports.initParsers = function(moduleType) {
// Install the parser modules
moduleType = moduleType || "parser";

View File

@ -0,0 +1,32 @@
/*\
title: $:/core/modules/parsers/wikiparser/wikivocabulary.js
type: application/javascript
module-type: global
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var WikiVocabulary = function(options) {
this.wiki = options.wiki;
// Hashmaps of the various parse rule classes
this.pragmaRuleClasses = $tw.modules.applyMethods("wikipragmarule");
this.blockRuleClasses = $tw.modules.applyMethods("wikiblockrule");
this.runRuleClasses = $tw.modules.applyMethods("wikirunrule");
// Hashmap of the various renderer classes
this.rendererClasses = $tw.modules.applyMethods("wikirenderer");
// Hashmap of the available widgets
this.widgetClasses = $tw.modules.applyMethods("widget");
};
WikiVocabulary.prototype.parseText = function(type,text) {
return new $tw.WikiParser(this,type,text,{wiki: this.wiki});
};
exports.WikiVocabulary = WikiVocabulary;
})();

View File

@ -0,0 +1,33 @@
title: $:/templates/NewPageTemplate
\define coolmacro(p:ridiculously) This is my $p$ cool macro!
\define me(one two)
some<br>thing
\end
\define another(first:default second third:default3) that is
* This
*.disabled Is a
* List!!
<_navigator story="$:/StoryList" history="$:/HistoryList">
<_link to="JeremyRuston" hover="HelloThere">
Go to it!
</_link>
! Heading1
!!.myclass Heading2
!!! Heading3
!!!! Heading4
<div class="container">
<div class="row">
<div class="span10">
<_list filter="[list[$:/StoryList]]" history="$:/HistoryList" template="$:/templates/NewViewTemplate" editTemplate="$:/templates/EditTemplate" listview=classic itemClass="tw-tiddler-frame"/>
</div>
</div>
</div>
</_navigator>

View File

@ -0,0 +1,16 @@
title: $:/templates/NewViewTemplate
modifier: JeremyRuston
<span class="title">
<_view field="title"/>
<_button popup="HelloThere">close</_button>
</span>
<div class:"small">
<_view field="modifier" format="link"/>
<_view field="modified" format="date"/>
</div>
<div class="body">
<_view field="text" format="wikified"/>
</div>