mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 01:16:30 +00:00
500 lines
12 KiB
JavaScript
500 lines
12 KiB
JavaScript
var MWParserEnvironment = function(opts) {
|
|
var options = {
|
|
tagHooks: {},
|
|
parserFunctions: {},
|
|
pageCache: {}, // @fixme use something with managed space
|
|
domCache: {}
|
|
};
|
|
$.extend(options, opts);
|
|
this.debug = false;
|
|
this.tagHooks = options.tagHooks;
|
|
this.parserFunctions = options.parserFunctions;
|
|
this.pageCache = options.pageCache;
|
|
this.domCache = options.domCache;
|
|
};
|
|
|
|
$.extend(MWParserEnvironment.prototype, {
|
|
// Does this need separate UI/content inputs?
|
|
formatNum: function( num ) {
|
|
return num + '';
|
|
},
|
|
|
|
getVariable: function( varname, options ) {
|
|
//
|
|
},
|
|
|
|
/**
|
|
* @return MWParserFunction
|
|
*/
|
|
getParserFunction: function( name ) {
|
|
if (name in this.parserFunctions) {
|
|
return new this.parserFunctions[name]( this );
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @return MWParserTagHook
|
|
*/
|
|
getTagHook: function( name ) {
|
|
if (name in this.tagHooks) {
|
|
return new this.tagHooks[name](this);
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @fixme do this for real eh
|
|
*/
|
|
resolveTitle: function( name, namespace ) {
|
|
// hack!
|
|
if (name.indexOf(':') == -1 && typeof namespace ) {
|
|
// hack hack hack
|
|
name = namespace + ':' + name;
|
|
}
|
|
return name;
|
|
},
|
|
|
|
/**
|
|
* Async.
|
|
*
|
|
* @todo make some optimizations for fetching multiple at once
|
|
*
|
|
* @param string name
|
|
* @param function(text, err) callback
|
|
*/
|
|
fetchTemplate: function( title, callback ) {
|
|
this.fetchTemplateAndTitle( title, function( text, title, err ) {
|
|
callback(title, err);
|
|
});
|
|
},
|
|
|
|
fetchTemplateAndTitle: function( title, callback ) {
|
|
// @fixme normalize name?
|
|
if (title in this.pageCache) {
|
|
// @fixme should this be forced to run on next event?
|
|
callback( this.pageCache[title], title );
|
|
} else {
|
|
// whee fun hack!
|
|
console.log(title);
|
|
console.log(this.pageCache);
|
|
$.ajax({
|
|
url: wgScriptPath + '/api' + wgScriptExtension,
|
|
data: {
|
|
format: 'json',
|
|
action: 'query',
|
|
prop: 'revisions',
|
|
rvprop: 'content',
|
|
titles: name
|
|
},
|
|
success: function(data, xhr) {
|
|
var src = null, title = null;
|
|
$.each(data.query.pages, function(i, page) {
|
|
if (page.revisions && page.revisions.length) {
|
|
src = page.revisions[0]['*'];
|
|
title = page.title;
|
|
}
|
|
});
|
|
if (typeof src !== 'string') {
|
|
callback(null, null, 'Page not found');
|
|
} else {
|
|
callback(src, title);
|
|
}
|
|
},
|
|
error: function(msg) {
|
|
callback(null, null, 'Page/template fetch failure');
|
|
},
|
|
dataType: 'json',
|
|
cache: false // @fixme caching, versions etc?
|
|
}, 'json');
|
|
}
|
|
},
|
|
|
|
getTemplateDom: function( title, callback ) {
|
|
var self = this;
|
|
if (title in this.domCache) {
|
|
callback(this.domCache[title], null);
|
|
return;
|
|
}
|
|
this.fetchTemplateAndTitle( title, function( text, title, err ) {
|
|
if (err) {
|
|
callback(null, err);
|
|
return;
|
|
}
|
|
self.pageCache[title] = text;
|
|
self.parser.parseToTree( text, function( templateTree, err ) {
|
|
this.domCache[title] = templateTree;
|
|
callback(templateTree, err);
|
|
});
|
|
});
|
|
},
|
|
|
|
braceSubstitution: function( templateNode, frame, callback ) {
|
|
// stuff in Parser.braceSubstitution
|
|
// expand/flatten the 'title' piece (to get the template reference)
|
|
var self = this;
|
|
frame.flatten(self.resolveTitle(templateNode.name, 'Template'), function(templateName, err) {
|
|
if (err) {
|
|
callback(null, err);
|
|
return;
|
|
}
|
|
var out = {
|
|
type: 'placeholder',
|
|
orig: templateNode,
|
|
contents: []
|
|
};
|
|
|
|
// check for 'subst:'
|
|
// check for variable magic names
|
|
// check for msg, msgnw, raw magics
|
|
// check for parser functions
|
|
|
|
// resolve template name
|
|
// load template w/ canonical name
|
|
// load template w/ variant names
|
|
// recursion depth check
|
|
// fetch from DB or interwiki
|
|
// infinte loop check
|
|
self.getTemplateDom(templateName, function(dom, err) {
|
|
// Expand in-place!
|
|
var templateFrame = frame.newChild(templateNode.params || {});
|
|
templateFrame.expand(dom, 0, function(expandedTemplateNode) {
|
|
out.contents = expandedTemplateNode.contents;
|
|
callback(out);
|
|
return; // done
|
|
});
|
|
return; // wait for async
|
|
});
|
|
});
|
|
},
|
|
|
|
argSubstitution: function( argNode, frame, callback ) {
|
|
frame.flatten(argNode.name, function(argName, err) {
|
|
if (err) {
|
|
callback(null, err);
|
|
return;
|
|
}
|
|
|
|
var arg = frame.getArgument(argName);
|
|
console.log(argName, arg, frame);
|
|
if (arg === false && 'params' in argNode && argNode.params.length) {
|
|
// No match in frame, use the supplied default
|
|
arg = argNode.params[0].val;
|
|
}
|
|
var out = {
|
|
type: 'placeholder',
|
|
orig: argNode,
|
|
contents: [arg]
|
|
};
|
|
callback(out);
|
|
});
|
|
}
|
|
|
|
|
|
});
|
|
|
|
function PPFrame(env) {
|
|
this.env = env;
|
|
this.loopCheckHash = [];
|
|
this.depth = 0;
|
|
}
|
|
|
|
// Flag constants
|
|
$.extend(PPFrame, {
|
|
NO_ARGS: 1,
|
|
NO_TEMPLATES: 2,
|
|
STRIP_COMMENTS: 4,
|
|
NO_IGNORE: 8,
|
|
RECOVER_COMMENTS: 16
|
|
});
|
|
PPFrame.RECOVER_ORIG = PPFrame.NO_ARGS
|
|
| PPFrame.NO_TEMPLATES
|
|
| PPFrame.STRIP_COMMENTS
|
|
| PPFrame.NO_IGNORE
|
|
| PPFrame.RECOVER_COMMENTS;
|
|
|
|
$.extend(PPFrame.prototype, {
|
|
newChild: function(args, title) {
|
|
//
|
|
var child = new PPFrame(this.env);
|
|
child.args = args || {};
|
|
child.title = title;
|
|
return child;
|
|
},
|
|
|
|
/**
|
|
* Using simple recursion for now -- PHP version is a little fancier.
|
|
*
|
|
* The iterator loop is set off in a closure so we can continue it after
|
|
* waiting for an asynchronous template fetch.
|
|
*
|
|
* Note that this is inefficient, as we have to wait for the entire round
|
|
* trip before continuing -- in browser-based work this may be particularly
|
|
* slow. This can be mitigated by prefetching templates based on previous
|
|
* knowledge or an initial tree-walk.
|
|
*
|
|
* @param {object} tree
|
|
* @param {number} flags
|
|
* @param {function(tree, error)} callback
|
|
*/
|
|
expand: function(root, flags, callback) {
|
|
/**
|
|
* Clone a node, but give the clone an empty contents
|
|
*/
|
|
var cloneNode = function(node) {
|
|
var out = $.extend({}, node);
|
|
out.contents = [];
|
|
return out;
|
|
}
|
|
|
|
// stub node to write into
|
|
var rootOut = cloneNode(root);
|
|
|
|
var self = this,
|
|
env = self.env,
|
|
expansionDepth = 0,
|
|
outStack = [{contents: []}, cloneNode(root)],
|
|
iteratorStack = [false, root],
|
|
indexStack = [0, 0],
|
|
contextNode = false,
|
|
newIterator = false,
|
|
continuing = false,
|
|
iters = 0,
|
|
maxIters = 10; // for debugging
|
|
|
|
if (env.debug) {
|
|
var $chunk = $('<div style="border: solid 1px blue">').append('<hr>');
|
|
$chunk.append('<h3>Original</h3>');
|
|
$chunk.nodeTree(root);
|
|
$chunk.appendTo('body');
|
|
var debug = function(label, node) {
|
|
$('<h3></h3>').text(label).appendTo($chunk);
|
|
if (typeof node == "string" || typeof node == "number") {
|
|
$('<p>').text(node).appendTo($chunk);
|
|
} else if (node) {
|
|
$chunk.nodeTree(node);
|
|
}
|
|
};
|
|
} else {
|
|
var debug = function() {};
|
|
}
|
|
var iteration = function() {
|
|
// This while loop is a tail call recursion optimization simulator :)
|
|
while (iteratorStack.length > 1) {
|
|
var level = outStack.length - 1,
|
|
iteratorNode = iteratorStack[level],
|
|
out = outStack[level],
|
|
index = indexStack[level]; // ????
|
|
|
|
if (env.debug) {
|
|
$chunk.append('<hr>');
|
|
iters++;
|
|
var $h = $('<h3>').text('iter ' + iters).attr('id', 'iter' + iters);
|
|
if (iters > 1) {
|
|
$h.append(' ');
|
|
$('<a>').attr('href', '#iter' + (iters - 1)).text('prev').appendTo($h);
|
|
}
|
|
$h.append(' ');
|
|
$('<a>').attr('href', '#iter' + (iters + 1)).text('next').appendTo($h);
|
|
$chunk.append($h);
|
|
|
|
if (iters > maxIters) {
|
|
debug('aborted');
|
|
return;
|
|
}
|
|
$chunk.append('<h3>level ' + level + '</h3>');
|
|
}
|
|
debug('index', index);
|
|
if (continuing) {
|
|
// If we're re-entering from an asynchronous data fetch,
|
|
// skip over this part, we've done it before.
|
|
continuing = false;
|
|
} else {
|
|
newIterator = false;
|
|
if (index >= iteratorNode.contents.length) {
|
|
// All done with this iterator.
|
|
iteratorStack[level] = false;
|
|
contextNode = false;
|
|
} else {
|
|
// Increment for the next round...
|
|
contextNode = iteratorNode.contents[index];
|
|
indexStack[level]++;
|
|
index++;
|
|
}
|
|
debug('contextNode', contextNode);
|
|
debug('indexStack (next)', indexStack);
|
|
debug('outStack', outStack);
|
|
|
|
if (contextNode === false) {
|
|
// nothing to do
|
|
} else if (typeof contextNode === 'string') {
|
|
out.contents.push(contextNode);
|
|
} else if (contextNode.type === 'template') {
|
|
// Double-brace expansion
|
|
continuing = true;
|
|
self.env.braceSubstitution(contextNode, self, function(replacementNode, err) {
|
|
//out.contents.push(replacementNode);
|
|
newIterator = replacementNode;
|
|
// ... and continue on the next node!
|
|
iteration();
|
|
});
|
|
return; // pause for async work...
|
|
} else if (contextNode.type == 'tplarg') {
|
|
// Triple-brace expansion
|
|
continuing = true;
|
|
self.env.argSubstitution(contextNode, self, function(replacementNode, err) {
|
|
//out.contents.push(replacementNode);
|
|
newIterator = replacementNode;
|
|
// ... and continue on the next node!
|
|
iteration();
|
|
});
|
|
return; // pause for async work...
|
|
} else {
|
|
if ('contents' in contextNode && contextNode.contents.length) {
|
|
// Generic recursive expansion
|
|
newIterator = contextNode;
|
|
debug('diving into child');
|
|
} else {
|
|
// No children; push as-is.
|
|
out.contents.push(contextNode);
|
|
debug('no children');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newIterator !== false) {
|
|
outStack.push(cloneNode(newIterator));
|
|
iteratorStack.push(newIterator);
|
|
indexStack.push(0);
|
|
debug('iterator stack push!');
|
|
debug('outStack', outStack);
|
|
debug('iteratorStack', iteratorStack);
|
|
debug('indexStack', indexStack);
|
|
} else if ( iteratorStack[level] === false) {
|
|
// Return accumulated value to parent
|
|
// With tail recursion
|
|
debug('returning output up the stack');
|
|
while (iteratorStack[level] === false && level > 0) {
|
|
outStack[level - 1].contents.push(out);
|
|
outStack.pop();
|
|
iteratorStack.pop();
|
|
indexStack.pop();
|
|
level--;
|
|
}
|
|
}
|
|
debug('end of iteration');
|
|
|
|
// hack!
|
|
if (iteratorStack.length > 1) {
|
|
// Run us after running the event loop
|
|
setTimeout(iteration, 0);
|
|
return;
|
|
}
|
|
}
|
|
// We've reached the end of the loop!
|
|
--expansionDepth;
|
|
var finalOut = outStack.pop().contents[0];
|
|
debug('done', finalOut);
|
|
callback(finalOut, null);
|
|
};
|
|
iteration();
|
|
},
|
|
|
|
flatten: function(root, callback) {
|
|
new MWTreeSerializer(this).treeToSource(root, callback);
|
|
},
|
|
|
|
implodeWithFlags: function(sep, flags) {
|
|
|
|
},
|
|
|
|
implode: function(sep) {
|
|
|
|
},
|
|
|
|
virtualImport: function(sep) {
|
|
|
|
},
|
|
|
|
virtualBracketedImplode: function(start, sep, end /*, ... */ ) {
|
|
|
|
},
|
|
|
|
isEmpty: function() {
|
|
|
|
},
|
|
|
|
getArguments: function() {
|
|
|
|
},
|
|
|
|
getNumberedArguments: function() {
|
|
|
|
},
|
|
|
|
getNamedArguments: function() {
|
|
|
|
},
|
|
|
|
getArgument: function( name ) {
|
|
if (name in this.args) {
|
|
return this.args[name];
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
loopCheck: function(title) {
|
|
},
|
|
|
|
isTemplate: function() {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* @parm MWParserEnvironment env
|
|
* @constructor
|
|
*/
|
|
MWParserTagHook = function( env ) {
|
|
if (!env) {
|
|
throw new Error( 'Tag hook requires a parser environment.' );
|
|
}
|
|
this.env = env;
|
|
};
|
|
|
|
/**
|
|
* @param string text (or a parse tree?)
|
|
* @param object params map of named parameters (strings or parse frames?)
|
|
* @return either a string or a parse frame -- finalize this?
|
|
*/
|
|
MWParserTagHook.execute = function( text, params ) {
|
|
return '';
|
|
};
|
|
|
|
|
|
MWParserFunction = function( env) {
|
|
if (!env) {
|
|
throw new Error( 'Parser function requires a parser environment.');
|
|
}
|
|
this.env = env;
|
|
};
|
|
|
|
/**
|
|
* @param string text (or a parse tree?)
|
|
* @param object params map of named parameters (strings or parse frames?)
|
|
* @return either a string or a parse frame -- finalize this?
|
|
*/
|
|
MWParserFunction.execute = function( text, params ) {
|
|
return '';
|
|
};
|
|
|
|
if (typeof module == "object") {
|
|
module.exports.MWParserEnvironment = MWParserEnvironment;
|
|
}
|