mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-01-26 17:06:51 +00:00
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:
parent
916ca8eecf
commit
d338a54370
57
core/modules/parsers/wikiparser/rules/block/heading.js
Normal file
57
core/modules/parsers/wikiparser/rules/block/heading.js
Normal 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;
|
||||
|
||||
})();
|
141
core/modules/parsers/wikiparser/rules/block/list.js
Normal file
141
core/modules/parsers/wikiparser/rules/block/list.js
Normal 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;
|
||||
|
||||
})();
|
98
core/modules/parsers/wikiparser/rules/pragma/macrodef.js
Normal file
98
core/modules/parsers/wikiparser/rules/pragma/macrodef.js
Normal 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;
|
||||
|
||||
})();
|
52
core/modules/parsers/wikiparser/rules/run/entity.js
Normal file
52
core/modules/parsers/wikiparser/rules/run/entity.js
Normal 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: ©
|
||||
}}}
|
||||
|
||||
\*/
|
||||
(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;
|
||||
|
||||
})();
|
113
core/modules/parsers/wikiparser/rules/run/html.js
Normal file
113
core/modules/parsers/wikiparser/rules/run/html.js
Normal 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;
|
||||
|
||||
})();
|
71
core/modules/parsers/wikiparser/rules/run/macrocall.js
Normal file
71
core/modules/parsers/wikiparser/rules/run/macrocall.js
Normal 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;
|
||||
|
||||
})();
|
296
core/modules/parsers/wikiparser/wikiparser.js
Normal file
296
core/modules/parsers/wikiparser/wikiparser.js
Normal 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;
|
||||
|
||||
})();
|
||||
|
151
core/modules/rendertree/renderers/element.js
Normal file
151
core/modules/rendertree/renderers/element.js
Normal 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
|
||||
|
||||
})();
|
35
core/modules/rendertree/renderers/entity.js
Normal file
35
core/modules/rendertree/renderers/entity.js
Normal 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
|
||||
|
||||
})();
|
101
core/modules/rendertree/renderers/macrocall.js
Normal file
101
core/modules/rendertree/renderers/macrocall.js
Normal 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
|
||||
|
||||
})();
|
37
core/modules/rendertree/renderers/raw.js
Normal file
37
core/modules/rendertree/renderers/raw.js
Normal 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
|
||||
|
||||
})();
|
35
core/modules/rendertree/renderers/text.js
Normal file
35
core/modules/rendertree/renderers/text.js
Normal 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
|
||||
|
||||
})();
|
151
core/modules/rendertree/renderers/widget.js
Normal file
151
core/modules/rendertree/renderers/widget.js
Normal 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
|
||||
|
||||
})();
|
91
core/modules/rendertree/wikirendertree.js
Normal file
91
core/modules/rendertree/wikirendertree.js
Normal 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;
|
||||
|
||||
})();
|
34
core/modules/testnewwikiparser.js
Normal file
34
core/modules/testnewwikiparser.js
Normal 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;
|
||||
|
||||
})();
|
145
core/modules/widgets/button.js
Normal file
145
core/modules/widgets/button.js
Normal 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;
|
||||
|
||||
})();
|
147
core/modules/widgets/link.js
Normal file
147
core/modules/widgets/link.js
Normal 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;
|
||||
|
||||
})();
|
295
core/modules/widgets/list/list.js
Normal file
295
core/modules/widgets/list/list.js
Normal 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;
|
||||
|
||||
})();
|
241
core/modules/widgets/navigator.js
Normal file
241
core/modules/widgets/navigator.js
Normal 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;
|
||||
|
||||
})();
|
150
core/modules/widgets/transclude.js
Normal file
150
core/modules/widgets/transclude.js
Normal 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;
|
||||
|
||||
})();
|
141
core/modules/widgets/view/view.js
Normal file
141
core/modules/widgets/view/view.js
Normal 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;
|
||||
|
||||
})();
|
36
core/modules/widgets/view/viewers/date.js
Normal file
36
core/modules/widgets/view/viewers/date.js
Normal 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;
|
||||
|
||||
})();
|
44
core/modules/widgets/view/viewers/link.js
Normal file
44
core/modules/widgets/view/viewers/link.js
Normal 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;
|
||||
|
||||
})();
|
41
core/modules/widgets/view/viewers/wikified.js
Normal file
41
core/modules/widgets/view/viewers/wikified.js
Normal 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;
|
||||
|
||||
})();
|
@ -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";
|
||||
|
32
core/modules/wikivocabulary.js
Normal file
32
core/modules/wikivocabulary.js
Normal 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;
|
||||
|
||||
})();
|
||||
|
33
core/templates/NewPageTemplate.tid
Normal file
33
core/templates/NewPageTemplate.tid
Normal 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>
|
16
core/templates/NewViewTemplate.tid
Normal file
16
core/templates/NewViewTemplate.tid
Normal 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>
|
Loading…
Reference in New Issue
Block a user