/**
* Some parser functions, and quite a bunch of stubs of parser functions.
*
* IMPORTANT NOTE: These parser functions are only used by the Parsoid-native
* template expansion pipeline, which is *not* the default or used in
* production. Normally we use API calls into a MediaWiki installation to
* implement parser functions and other preprocessor functionality. The only
* use of this code is currently in parserTests, but those tests should
* probably be marked as PHP-only and any mixed testing moved into separate
* tests. This means that there is not much point in spending time on
* implementing more parser functions here.
*
* There are still quite a few missing, see
* {@link http://www.mediawiki.org/wiki/Help:Magic_words} and
* {@link http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions}.
* Instantiated and called by the {@link TemplateHandler} extension.
* Any `pf_<prefix>`
* matching a lower-cased template name prefix up to the first colon will
* override that template.
* @module
*/
'use strict';
require('../../../core-upgrade.js');
var Promise = require('../../utils/promise.js');
var Sanitizer = require('./Sanitizer.js').Sanitizer;
var TokenUtils = require('../../utils/TokenUtils.js').TokenUtils;
var Util = require('../../utils/Util.js').Util;
const { KV, TagTk, EndTagTk, SelfclosingTagTk } = require('../../tokens/TokenTypes.js');
/**
* @class
* @param {MWParserEnvironment} env
*/
function ParserFunctions(env) {
this.env = env;
}
// 'cb' can only be called once after "everything" is done.
// But, we need something that can be used in async context where it is
// called repeatedly till we are done.
//
// Primarily needed in the context of async.map calls that requires a 1-shot callback.
//
// Use with caution! If the async stream that we are accumulating into the buffer
// is a firehose of tokens, the buffer will become huge.
function buildAsyncOutputBufferCB(cb) {
function AsyncOutputBufferCB(cb2) {
this.accum = [];
this.targetCB = cb2;
}
AsyncOutputBufferCB.prototype.processAsyncOutput = function(res) {
// * Ignore switch-to-async mode calls since
// we are actually collapsing async calls.
// * Accumulate async call results in an array
// till we get the signal that we are all done
// * Once we are done, pass everything to the target cb.
if (res.async !== true) {
// There are 3 kinds of callbacks:
// 1. cb({tokens: .. })
// 2. cb({}) ==> toks can be undefined
// 3. cb(foo) -- which means in some cases foo can
// be one of the two cases above, or it can also be a simple string.
//
// Version 1. is the general case.
// Versions 2. and 3. are optimized scenarios to eliminate
// additional processing of tokens.
//
// In the C++ version, this is handled more cleanly.
var toks = res.tokens;
if (!toks && res.constructor === String) {
toks = res;
}
if (toks) {
if (Array.isArray(toks)) {
for (var i = 0, l = toks.length; i < l; i++) {
this.accum.push(toks[i]);
}
// this.accum = this.accum.concat(toks);
} else {
this.accum.push(toks);
}
}
if (!res.async) {
// we are done!
this.targetCB(this.accum);
}
}
};
var r = new AsyncOutputBufferCB(cb);
return r.processAsyncOutput.bind(r);
}
// Temporary helper.
ParserFunctions.prototype._rejoinKV = function(trim, k, v) {
if (k.constructor === String && k.length > 0) {
return [k].concat(['='], v);
} else if (Array.isArray(k) && k.length > 0) {
return k.concat(['='], v);
} else {
return trim ? (v.constructor === String ? v.trim() : TokenUtils.tokenTrim(v)) : v;
}
};
// XXX: move to frame?
ParserFunctions.prototype.expandKV = function(kv, cb, defaultValue, type, trim) {
if (trim === undefined) {
trim = true;
}
if (type === undefined) {
type = 'tokens/x-mediawiki/expanded';
}
if (kv === undefined) {
cb({ tokens: [ defaultValue || '' ] });
} else if (kv.constructor === String) {
return cb({ tokens: [kv] });
} else if (kv.k.constructor === String && kv.v.constructor === String) {
if (kv.k) {
cb({ tokens: [kv.k + '=' + kv.v] });
} else {
cb({ tokens: [trim ? kv.v.trim() : kv.v] });
}
} else {
var getCB = (v) => {
cb({ tokens: this._rejoinKV(trim, kv.k, v) });
};
kv.v.get({
type: type,
cb: getCB,
asyncCB: cb,
});
}
};
ParserFunctions.prototype.pf_if = function(token, frame, cb, args) {
var target = args[0].k;
if (target.trim() !== '') {
this.expandKV(args[1], cb);
} else {
this.expandKV(args[2], cb);
}
};
ParserFunctions.prototype._switchLookupFallback = function(frame, kvs, key, dict, cb, v) {
var kv;
var l = kvs.length;
this.env.log('debug', '_switchLookupFallback', kvs.length, key, v);
var _cbTrim = function(res) {
if (res.constructor === String) {
cb({ tokens: [ res.trim() ], async: res.async });
} else if (Array.isArray(res)) {
cb({ tokens: TokenUtils.tokenTrim(res), async: res.async });
} else {
cb(res);
}
};
var _cbNoTrim = function(res) {
if (res.constructor === String) {
cb({ tokens: [ res ], async: res.async });
} else if (Array.isArray(res)) {
cb({ tokens: res, async: res.async });
} else if (res.async) {
cb(res);
} else {
this.env.log('error', 'Unprocessable res in ParserFunctions:_cbNoTrim', res);
}
};
// 'v' need not be a string in cases where it is the last fall-through case
var vStr = v ? TokenUtils.tokensToString(v) : null;
if (vStr && key === vStr.trim()) {
// This handles fall-through switch cases:
//
// {{#switch:<key>
// | c1 | c2 | c3 = <res>
// ...
// }}
//
// So if <key> matched c1, we want to return <res>.
// Hence, we are looking for the next entry with a non-empty key.
this.env.log('debug', 'switch found');
for (var j = 0; j < l; j++) {
kv = kvs[j];
// XXX: make sure the key is always one of these!
if (kv.k.length) {
kv.v.get({
type: 'tokens/x-mediawiki/expanded',
cb: _cbTrim,
asyncCB: _cbTrim,
});
return;
}
}
// No value found, return empty string? XXX: check this
cb({});
} else if (kvs.length) {
// search for value-only entry which matches
var i = 0;
if (v) {
i = 1;
}
for (; i < l; i++) {
kv = kvs[i];
if (kv.k.length || !kv.v.length) {
// skip entries with keys or empty values
continue;
} else {
if (!kv.v.get) {
this.env.log('debug', kv.v);
}
// We found a value-only entry. However, we have to verify
// if we have any fall-through cases that this matches.
//
// {{#switch:<key>
// | c1 | c2 | c3 = <res>
// ...
// }}
//
// In the switch example above, if we found 'c1', that is
// not the fallback value -- we have to check for fall-through
// cases. Hence the recursive callback to _switchLookupFallback.
//
// {{#switch:<key>
// | c1 = <..>
// | c2 = <..>
// | [[Foo]]</div>
// }}
//
// 'val' may be an array of tokens rather than a string as in the
// example above where 'val' is indeed the final return value.
// Hence 'tokens/x-mediawiki/expanded' type below.
kv.v.get({
type: 'tokens/x-mediawiki/expanded',
cb: function(k, val) {
setImmediate(
this._switchLookupFallback.bind(this, frame,
kvs.slice(k + 1), key, dict, cb, val)
);
}.bind(this, i),
asyncCB: cb,
});
return;
}
}
// value not found!
if ('#default' in dict) {
dict['#default'].get({
type: 'tokens/x-mediawiki/expanded',
cb: _cbTrim,
asyncCB: cb,
});
return;
} else if (kvs.length) {
var lastKV = kvs[kvs.length - 1];
if (lastKV && !lastKV.k.length) {
lastKV.v.get({
cb: _cbNoTrim,
asyncCB: cb,
});
return;
} else {
cb({});
}
} else {
// nothing found at all.
cb({});
}
} else if (v) {
cb({ tokens: Array.isArray(v) ? v : [v] });
} else {
// nothing found at all.
cb({});
}
};
// TODO: Implement
// http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#Grouping_results
ParserFunctions.prototype.pf_switch = function(token, frame, cb, args) {
var target = args[0].k.trim();
this.env.log('debug', 'switch enter', target, token);
// create a dict from the remaining args
args.shift();
var dict = args.dict();
if (target && dict[target] !== undefined) {
this.env.log('debug', 'switch found: ', target, dict, ' res=', dict[target]);
dict[target].get({
type: 'tokens/x-mediawiki/expanded',
cb: function(res) {
cb({ tokens: res.constructor === String ? [res.trim()] : TokenUtils.tokenTrim(res) });
},
asyncCB: cb,
});
} else {
this._switchLookupFallback(frame, args, target, dict, cb);
}
};
// #ifeq
ParserFunctions.prototype.pf_ifeq = function(token, frame, cb, args) {
if (args.length < 3) {
cb({});
} else {
var b = args[1].v;
b.get({ cb: this._ifeq_worker.bind(this, cb, args), asyncCB: cb });
}
};
ParserFunctions.prototype._ifeq_worker = function(cb, args, b) {
if (args[0].k.trim() === b.trim()) {
this.expandKV(args[2], cb);
} else {
this.expandKV(args[3], cb);
}
};
ParserFunctions.prototype.pf_expr = function(token, frame, cb, args) {
var res;
var target = args[0].k;
if (target) {
try {
// FIXME: make this safe and implement MW expressions!
var f = new Function('return (' + target + ')'); // eslint-disable-line
res = f();
} catch (e) {
cb({ tokens: [ 'class="error" in expression ' + target ] });
return;
}
} else {
res = '';
}
// Avoid crashes
if (res === undefined) {
cb({ tokens: [ 'class="error" in expression ' + target ] });
return;
}
cb({ tokens: [ res.toString() ] });
};
ParserFunctions.prototype.pf_ifexpr = function(token, frame, cb, args) {
this.env.log('debug', '#ifexp: ', args);
var res = null;
var target = args[0].k;
if (target) {
try {
// FIXME: make this safe, and fully implement MW expressions!
var f = new Function('return (' + target + ')'); // eslint-disable-line
res = f();
} catch (e) {
cb({ tokens: [ 'class="error" in expression ' + target ] });
return;
}
}
if (res) {
this.expandKV(args[1], cb);
} else {
this.expandKV(args[2], cb);
}
};
ParserFunctions.prototype.pf_iferror = function(token, frame, cb, args) {
var target = args[0].k;
if (target.indexOf('class="error"') >= 0) {
this.expandKV(args[1], cb);
} else {
this.expandKV(args[1], cb, target);
}
};
ParserFunctions.prototype.pf_lc = function(token, frame, cb, args) {
cb({ tokens: [ args[0].k.toLowerCase() ] });
};
ParserFunctions.prototype.pf_uc = function(token, frame, cb, args) {
cb({ tokens: [ args[0].k.toUpperCase() ] });
};
ParserFunctions.prototype.pf_ucfirst = function(token, frame, cb, args) {
var target = args[0].k;
if (target) {
cb({ tokens: [ target[0].toUpperCase() + target.substr(1) ] });
} else {
cb({ tokens: [] });
}
};
ParserFunctions.prototype.pf_lcfirst = function(token, frame, cb, args) {
var target = args[0].k;
if (target) {
cb({ tokens: [ target[0].toLowerCase() + target.substr(1) ] });
} else {
cb({ tokens: [] });
}
};
ParserFunctions.prototype.pf_padleft = function(token, frame, cb, params) {
var target = params[0].k;
var env = this.env;
if (!params[1]) {
return cb({ tokens: [] });
}
// expand parameters 1 and 2
params.getSlice({
type: 'text/x-mediawiki/expanded',
}, 1, 3).then(function(args) {
var n = +(args[0].v);
if (n > 0) {
var pad = '0';
if (args[1] && args[1].v !== '') {
pad = args[1].v;
}
var padLength = pad.length;
var extra = '';
while ((target.length + extra.length + padLength) < n) {
extra += pad;
}
if (target.length + extra.length < n) {
extra += pad.substr(0, n - target.length - extra.length);
}
cb({ tokens: [extra + target] });
} else {
env.log('debug', 'padleft no pad width', args);
cb({ tokens: [] });
}
});
};
ParserFunctions.prototype.pf_padright = function(token, frame, cb, params) {
var target = params[0].k;
var env = this.env;
if (!params[1]) {
return cb({});
}
// expand parameters 1 and 2
params.getSlice({
type: 'text/x-mediawiki/expanded',
}, 1, 3).then(function(args) {
var n = +(args[0].v);
if (n > 0) {
var pad = '0';
if (args[1] && args[1].v !== '') {
pad = args[1].v;
}
var padLength = pad.length;
while ((target.length + padLength) < n) {
target += pad;
}
if (target.length < n) {
target += pad.substr(0, n - target.length);
}
cb({ tokens: [target] });
} else {
env.log('debug', 'padright no pad width', args);
cb({ tokens: [] });
}
});
};
ParserFunctions.prototype.pf_tag = function(token, frame, cb, args) {
// Check http://www.mediawiki.org/wiki/Extension:TagParser for more info
// about the #tag parser function.
var target = args[0].k;
if (!target || target === '') {
cb({});
} else {
// remove tag-name
args.shift();
this.tag_worker(target, cb, args);
}
};
ParserFunctions.prototype.tag_worker = function(target, cb, kvs) {
var contentToks = [];
var tagAttribs = [];
for (var i = 0, n = kvs.length; i < n; i++) {
if (kvs[i].k === '') {
contentToks = contentToks.concat(kvs[i].v);
} else {
tagAttribs.push(kvs[i]);
}
}
var tokens = [new TagTk(target, tagAttribs)].concat(
contentToks,
[new EndTagTk(target)]
);
cb({ tokens: tokens });
};
// TODO: These are just quick wrappers for now, optimize!
[
['year', 'Y'], ['month', 'm'], ['monthname', 'F'], ['monthabbrev', 'M'],
['week', 'W'], ['day', 'j'], ['day2', 'd'], ['dow', 'w'], ['dayname', 'l'],
['time', 'H:i'], ['hour', 'H'], ['week', 'W'],
['timestamp', 'YmdHis'],
].forEach(function(a) {
var name = a[0];
var format = a[1];
ParserFunctions.prototype['pf_current' + name] =
function(token, frame, cb, args) {
cb(this._pf_time_tokens(format, [], {}));
};
ParserFunctions.prototype['pf_local' + name] =
function(token, frame, cb, args) {
cb(this._pf_timel_tokens(format, [], {}));
};
});
// XXX Actually use genitive form!
ParserFunctions.prototype.pf_currentmonthnamegen = function(token, frame, cb, args) {
cb(this._pf_time_tokens('F', [], {}));
};
ParserFunctions.prototype.pf_localmonthnamegen = function(token, frame, cb, args) {
cb(this._pf_timel_tokens('F', [], {}));
};
// A first approximation of time stuff.
// TODO: Implement time spec (+ 1 day etc), check if formats are complete etc.
// See http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
// for the full list of requirements!
//
// First (very rough) approximation below based on
// http://jacwright.com/projects/javascript/date_format/, MIT licensed.
ParserFunctions.prototype.pf_time = function(token, frame, cb, args) {
cb({ tokens: this._pf_time(args[0].k, args.slice(1)) });
};
ParserFunctions.prototype._pf_time_tokens = function(target, args) {
return { tokens: this._pf_time(target, args) };
};
ParserFunctions.prototype.pf_timel = function(token, frame, cb, args) {
cb({ tokens: this._pf_time(args[0].k, args.slice(1), 'local') });
};
ParserFunctions.prototype._pf_timel_tokens = function(target, args) {
return { tokens: this._pf_time(target, args, 'local') };
};
var ParsoidDate; // forward declaration
ParserFunctions.prototype._pf_time = function(target, args, isLocal) {
var res;
var tpl = target.trim();
var date = new ParsoidDate(this.env, isLocal);
try {
res = [ date.format(tpl) ];
} catch (e2) {
this.env.log("error", "#time " + e2);
res = [ date.toString() ];
}
return res;
};
// Simulates PHP's date function
// NOTE that Javascript doesn't have a proper user-specified-timezone API.
// PHP format specifiers which return the name of the timezone (for example,
// 'e' and 'T') can't be implemented in JavaScript w/o the use of an external
// timezone database, like for instance https://github.com/mde/timezone-js
// CURRENTLY NO SUPPORT FOR NON-GREGORIAN CALENDARS
ParsoidDate = function(env, isLocal, forcetime) {
var date = new Date();
var offset = date.getTimezoneOffset();
// XXX: parse forcetime and change date
// when testing, look aside to other date?
if (typeof (env.conf.wiki.fakeTimestamp) === 'number') {
// php time stamps are in seconds; js timestamps are in milliseconds
date.setTime(env.conf.wiki.fakeTimestamp * 1000);
}
if (typeof (env.conf.wiki.timezoneOffset) === 'number') {
// this is the wiki's $wgLocaltimezone (if set)
offset = env.conf.wiki.timezoneOffset;
}
if (!isLocal) {
offset = 0; // UTC
}
this._date = date;
// _localdate is a date object which is, in UTC, the desired local time.
// for example, if _date is 'Tue, 02 Apr 2013 21:30:44 GMT-0400 (EDT)'
// then _localdate is 'Tue, 02 Apr 2013 21:30:44 GMT'
offset *= 60 * 1000; /* convert from minutes to milliseconds */
this._localdate = new Date(date.getTime() - offset);
};
ParsoidDate.prototype.format = function(format) {
var returnStr = '';
var replace = ParsoidDate.replaceChars;
for (var i = 0; i < format.length; i++) {
var curChar = format.charAt(i);
if (i - 1 >= 0 && format.charAt(i - 1) === "\\") {
returnStr += curChar;
} else if (replace[curChar]) {
returnStr += replace[curChar].call(this);
} else if (curChar !== "\\") {
returnStr += curChar;
}
}
return returnStr;
};
ParsoidDate.prototype.toString = function() {
return this.format('D, d M Y H:i:s O');
};
ParsoidDate.prototype.getTimezoneOffset = function() {
return (this._date.getTime() - this._localdate.getTime()) / (60 * 1000);
};
var getJan1 = function(d) {
d = new Date(d.getTime());
d.setUTCMonth(0);
d.setUTCDate(1);
d.setUTCHours(0);
d.setUTCMinutes(0);
d.setUTCSeconds(0);
d.setUTCMilliseconds(0);
return d;
};
ParsoidDate.prototype.getWeek = function() {
var start = getJan1(this._localdate);
return Math.ceil((((this._localdate.valueOf() - start.valueOf()) / 86400000) + start.getUTCDay() + 1) / 7);
};
ParsoidDate.prototype.getWeekYear = function() { // ISO-8601 week year
var d = new Date(this._localdate);
d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7) + 3);
return d.getUTCFullYear();
};
ParsoidDate.prototype.getDayOfYear = function() {
var start = getJan1(this._localdate);
return Math.ceil((this._localdate.valueOf() - start.valueOf()) / 86400000);
};
// proxy certain methods of _date into ParsoidDate.
[
'getUTCHours', 'getUTCMinutes', 'getUTCSeconds',
'getTime', 'valueOf',
].forEach(function(f) {
ParsoidDate.prototype[f] = function() {
var d = this._date;
return d[f].apply(d, arguments);
};
});
// local dates use UTC methods, but on _localdate
[
'getHours', 'getMinutes', 'getSeconds', 'getMilliseconds',
'getDate', 'getDay', 'getMonth', 'getFullYear',
].forEach(function(f) {
var ff = f.replace('get', 'getUTC');
ParsoidDate.prototype[f] = function() {
var d = this._localdate;
return d[ff].apply(d, arguments);
};
});
// XXX: support localization
ParsoidDate.replaceChars = {
shortMonths: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
'Sep', 'Oct', 'Nov', 'Dec',
],
longMonths: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
],
shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
longDays: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday',
],
// Day
d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
D: function() { return ParsoidDate.replaceChars.shortDays[this.getDay()]; },
j: function() { return this.getDate(); },
l: function() { return ParsoidDate.replaceChars.longDays[this.getDay()]; },
N: function() { return this.getDay() + 1; },
S: function() {
return (this.getDate() % 10 === 1 &&
this.getDate() !== 11 ? 'st' : (this.getDate() % 10 === 2 &&
this.getDate() !== 12 ? 'nd' : (this.getDate() % 10 === 3 &&
this.getDate() !== 13 ? 'rd' : 'th')));
},
w: function() { return this.getDay(); },
z: function() { return this.getDayOfYear(); },
// Week
W: function() { return this.getWeek(); },
// Month
F: function() { return ParsoidDate.replaceChars.longMonths[this.getMonth()]; },
m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
M: function() { return ParsoidDate.replaceChars.shortMonths[this.getMonth()]; },
n: function() { return this.getMonth() + 1; },
t: function() {
return new Date(this.getFullYear(), this.getMonth() + 1, 0).getDate();
},
// Year
L: function() {
var year = this.getFullYear();
return (year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0));
},
o: function() { return this.getWeekYear(); },
Y: function() { return this.getFullYear(); },
y: function() { return ('' + this.getFullYear()).substr(2); },
// Time
a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
B: function() {
return Math.floor((((this.getUTCHours() + 1) % 24) +
this.getUTCMinutes() / 60 +
this.getUTCSeconds() / 3600) * 1000 / 24);
},
g: function() { return this.getHours() % 12 || 12; },
G: function() { return this.getHours(); },
h: function() {
return ((this.getHours() % 12 || 12) < 10 ? '0' : '') +
(this.getHours() % 12 || 12);
},
H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
u: function() {
var m = this.getMilliseconds();
return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m;
},
// Timezone
e: function() { return "Not Yet Supported"; },
I: function() { return "Not Yet Supported"; },
O: function() {
return (-this.getTimezoneOffset() < 0 ? '-' : '+') +
(Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') +
(Math.abs(this.getTimezoneOffset() / 60)) + '00';
},
P: function() {
return (-this.getTimezoneOffset() < 0 ? '-' : '+') +
(Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') +
(Math.abs(this.getTimezoneOffset() / 60)) + ':00';
},
T: function() { return "Not Yet Supported"; },
Z: function() { return -this.getTimezoneOffset() * 60; },
// Full Date/Time
c: function() { return this.format("Y-m-d\\TH:i:sP"); },
r: function() { return this.toString(); },
U: function() { return this.getTime() / 1000; },
};
ParserFunctions.prototype.pf_localurl = function(token, frame, cb, args) {
var target = args[0].k;
var env = this.env;
args = args.slice(1);
Promise.all(args.map(item => new Promise((resolve, reject) => {
// FIXME: we are swallowing all errors
var resCB = buildAsyncOutputBufferCB(resolve);
this.expandKV(item, resCB, '', 'text/x-mediawiki/expanded', false);
}))).then(function(expandedArgs) {
cb({
tokens: [
env.conf.wiki.script + '?title=' +
env.normalizedTitleKey(target) + '&' +
expandedArgs.join('&'),
],
});
}).done();
};
/* Stub section: Pick any of these and actually implement them! */
// The page name and similar information should be carried around in
// this.env
ParserFunctions.prototype.pf_formatnum = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [ target ] });
};
ParserFunctions.prototype.pf_currentpage = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [ target ] });
};
ParserFunctions.prototype.pf_pagenamee = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [ target.split(':', 2)[1] || '' ] });
};
ParserFunctions.prototype.pf_fullpagename = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [target || this.env.page.name || '' ] });
};
ParserFunctions.prototype.pf_fullpagenamee = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [ target || this.env.page.name || '' ] });
};
ParserFunctions.prototype.pf_pagelanguage = function(token, frame, cb, args) {
// The language (code) of the current page.
cb({ tokens: [ this.env.page.pagelanguage || 'en' ] });
};
ParserFunctions.prototype.pf_dirmark =
ParserFunctions.prototype.pf_directionmark = function(token, frame, cb, args) {
// The directionality of the current page.
var dir = this.env.page.pagelanguagedir ||
(this.env.conf.wiki.rtl ? "rtl" : "ltr");
var mark = (dir === 'rtl') ? '‏' : '‎';
// See Parser.php::getVariableValue()
cb({ tokens: [ Util.decodeWtEntities(mark) ] });
};
// This should be doable with the information in the envirionment
// (this.env) already.
ParserFunctions.prototype.pf_fullurl = function(token, frame, cb, args) {
var target = (args[0].k || this.env.page.name).replace(' ','_');
var wikiConf = this.env.conf.wiki;
var url;
if (args[1]) {
url = wikiConf.server + wikiConf.script + '?title=' + encodeURIComponent(target) + '&' + args[1].k + '=' + args[1].v;
} else {
url = wikiConf.baseURI + target.replace(' ','_').split('/').map(encodeURIComponent).join('/');
}
cb({ tokens: [ url ] });
};
ParserFunctions.prototype.pf_urlencode = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [encodeURIComponent(target.trim())] });
};
// The following items all depends on information from the Wiki, so are hard
// to implement independently. Some might require using action=parse in the
// API to get the value. See
// http://www.mediawiki.org/wiki/Parsoid#Token_stream_transforms,
// http://etherpad.wikimedia.org/ParserNotesExtensions and
// http://www.mediawiki.org/wiki/Wikitext_parser/Environment.
// There might be better solutions for some of these.
ParserFunctions.prototype.pf_ifexist = function(token, frame, cb, args) {
this.expandKV(args[1], cb);
};
ParserFunctions.prototype.pf_pagesize = function(token, frame, cb, args) {
cb({ tokens: [ '100' ] });
};
ParserFunctions.prototype.pf_sitename = function(token, frame, cb, args) {
cb({ tokens: [ "MediaWiki" ] });
};
ParserFunctions.prototype.pf_anchorencode = function(token, frame, cb, args) {
var target = args[0].k;
// Parser::guessSectionNameFromWikiText, which invokes
// Sanitizer::normalizeSectionNameWhitespace and
// Sanitizer::escapeIdForLink, then calls
// Sanitizer::safeEncodeAttribute on the result. See: T179544
target = target.replace(/[ _]+/g, ' ').trim();
target = Sanitizer.decodeCharReferences(target);
target = Sanitizer.escapeIdForLink(target);
var tokens = [];
var charEntity = (c) => {
var enc = Util.entityEncodeAll(c);
tokens.push(
new TagTk(
'span',
[new KV('typeof', 'mw:Entity')],
{ src: enc, srcContent: c }
),
c,
new EndTagTk('span', [], {})
);
};
target.split(/([\{\}\[\]|]|''|ISBN|RFC|PMID|__)/g).forEach((s,i) => {
if ((i % 2) === 0) {
tokens.push(s);
} else if (s === "''") {
charEntity(s[0]); charEntity(s[1]);
} else {
charEntity(s[0]); tokens.push(s.slice(1));
}
});
cb({ tokens: tokens });
};
ParserFunctions.prototype.pf_protectionlevel = function(token, frame, cb, args) {
cb({ tokens: [''] });
};
ParserFunctions.prototype.pf_ns = function(token, frame, cb, args) {
var nsid;
var target = args[0].k;
var env = this.env;
var normalizedTarget = target.toLowerCase().replace(' ', '_');
if (env.conf.wiki.namespaceIds.has(normalizedTarget)) {
nsid = env.conf.wiki.namespaceIds.get(normalizedTarget);
} else if (env.conf.wiki.canonicalNamespaces[normalizedTarget]) {
nsid = env.conf.wiki.canonicalNamespaces[normalizedTarget];
}
if (nsid !== undefined && env.conf.wiki.namespaceNames[nsid]) {
target = env.conf.wiki.namespaceNames[nsid];
}
cb({ tokens: [target] });
};
ParserFunctions.prototype.pf_subjectspace = function(token, frame, cb, args) {
cb({ tokens: ['Main'] });
};
ParserFunctions.prototype.pf_talkspace = function(token, frame, cb, args) {
cb({ tokens: ['Talk'] });
};
ParserFunctions.prototype.pf_numberofarticles = function(token, frame, cb, args) {
cb({ tokens: ["1"] });
};
ParserFunctions.prototype.pf_language = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [target] });
};
ParserFunctions.prototype.pf_contentlang =
ParserFunctions.prototype.pf_contentlanguage = function(token, frame, cb, args) {
// Despite the name, this returns the wiki's default interface language
// ($wgLanguageCode), *not* the language of the current page content.
cb({ tokens: [ this.env.conf.wiki.lang || 'en' ] });
};
ParserFunctions.prototype.pf_numberoffiles = function(token, frame, cb, args) {
cb({ tokens: ['2'] });
};
ParserFunctions.prototype.pf_namespace = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [target.split(':').pop() || 'Main'] });
};
ParserFunctions.prototype.pf_namespacee = function(token, frame, cb, args) {
var target = args[0].k;
cb({ tokens: [target.split(':').pop() || 'Main'] });
};
ParserFunctions.prototype.pf_namespacenumber = function(token, frame, cb, args) {
var target = args[0].k.split(':').pop();
cb({ tokens: [String(this.env.conf.wiki.namespaceIds.get(target))] });
};
ParserFunctions.prototype.pf_pagename = function(token, frame, cb, args) {
cb({ tokens: [this.env.page.name || ''] });
};
ParserFunctions.prototype.pf_pagenamebase = function(token, frame, cb, args) {
cb({ tokens: [this.env.page.name || ''] });
};
ParserFunctions.prototype.pf_scriptpath = function(token, frame, cb, args) {
cb({ tokens: [this.env.conf.wiki.scriptpath] });
};
ParserFunctions.prototype.pf_server = function(token, frame, cb, args) {
var dataAttribs = Util.clone(token.dataAttribs);
cb({
tokens: [
new TagTk('a', [
new KV('rel', 'nofollow'),
new KV('class', 'external free'),
new KV('href', this.env.conf.wiki.server),
new KV('typeof', 'mw:ExtLink/URL'),
], dataAttribs),
this.env.conf.wiki.server,
new EndTagTk('a'),
],
});
};
ParserFunctions.prototype.pf_servername = function(token, frame, cb, args) {
cb({ tokens: [this.env.conf.wiki.server.replace(/^https?:\/\//, '')] });
};
ParserFunctions.prototype.pf_talkpagename = function(token, frame, cb, args) {
cb({ tokens: [this.env.page.name.replace(/^[^:]:/, 'Talk:') || ''] });
};
ParserFunctions.prototype.pf_defaultsort = function(token, frame, cb, args) {
var key = args[0].k;
cb({
tokens: [
new SelfclosingTagTk('meta', [
new KV('property', 'mw:PageProp/categorydefaultsort'),
new KV('content', key.trim()),
]),
],
});
};
ParserFunctions.prototype.pf_displaytitle = function(token, frame, cb, args) {
var key = args[0].k;
cb({
tokens: [
new SelfclosingTagTk('meta', [
new KV('property', 'mw:PageProp/displaytitle'),
new KV('content', key.trim()),
]),
],
});
};
// TODO: #titleparts, SUBJECTPAGENAME, BASEPAGENAME. SUBPAGENAME, DEFAULTSORT
if (typeof module === "object") {
module.exports.ParserFunctions = ParserFunctions;
}