/**
* Per-wiki config library for interfacing with MediaWiki.
*
* @module
*/
'use strict';
require('../../core-upgrade.js');
var semver = require('semver');
var baseConfig = require('../../baseconfig/enwiki.json').query;
var FromHTML = require('../FromHTML.js').FromHTML;
var JSUtils = require('../utils/jsutils.js').JSUtils;
var Promise = require('../utils/promise.js');
var Util = require('../utils/Util.js').Util;
var ParamInfoRequest = require('../mw/ApiRequest.js').ParamInfoRequest;
// Make sure our base config is never modified
JSUtils.deepFreeze(baseConfig);
/**
* Per-wiki configuration object.
*
* @class
* @param {ParsoidConfig} parsoidConfig
* @param {Object} resultConf The configuration object from a MediaWiki API request. See the #ConfigRequest class in lib/mediawiki.ApiRequest.js for information about how we get this object. If null, we use the contents of lib/mediawiki.BaseConfig.json instead.
* @param {string} prefix The interwiki prefix this config will represent. Will be used for caching elsewhere in the code.
*/
function WikiConfig(parsoidConfig, resultConf, prefix) {
var nsid, name;
var conf = this;
if (resultConf === null) {
// Use the default JSON that we've already loaded above.
resultConf = baseConfig;
}
var general = resultConf.general;
var generator = general.generator || '';
var mwApiVersion = semver.parse(generator.substr('MediaWiki '.length));
if (mwApiVersion === null) { // semver parsing failed
mwApiVersion = semver.parse('0.0.0');
}
// Cache siteinfo information that mediawiki-title requires
this.siteInfo = {
namespaces: resultConf.namespaces,
namespacealiases: resultConf.namespacealiases,
specialpagealiases: resultConf.specialpagealiases,
general: {
case: general.case,
lang: general.lang,
legaltitlechars: general.legaltitlechars,
// For the gallery extension
galleryoptions: Object.assign({
imagesPerRow: 0,
imageWidth: 120,
imageHeight: 120,
mode: "traditional",
}, general.galleryoptions),
},
};
// Warn about older versions ( 1.21 matches baseconfigs )
if (mwApiVersion.compare('1.21.0-alpha') < 0) {
// Since this WikiConfig may be being constructed in the env
// constructor, `env.log` may not be set yet. See the fixme there
// about avoiding that, so we can remove this use of console.
console.log('The MediaWiki API appears to be very old and' +
' may not support necessary features. Proceed with caution!');
}
// Was introduced in T49651 (core f9c50bd7)
if (general.legaltitlechars === undefined && mwApiVersion.compare('1.25.0') < 0) {
this.siteInfo.general.legaltitlechars = baseConfig.general.legaltitlechars;
}
// Introduced in T153341 (core 1824778e, wmf/1.29.0-wmf.15)
var languagevariants = resultConf.languagevariants;
if (languagevariants === undefined) {
// Hard-coded list of variants and fallbacks, for mediawiki
// releases before 1.29
languagevariants = require('./variants.json');
}
// Was introduced in T46449 (core 1b64ddf0)
var protocols = resultConf.protocols;
if (protocols === undefined && mwApiVersion.compare('1.21.0') < 0) {
protocols = baseConfig.protocols;
}
// Preferred words are first in MessagesXx.php as of 1.27.0-wmf.20 (T116020)
this.useOldAliasOrder = mwApiVersion.compare('1.27.0-wmf.20') < 0 &&
mwApiVersion.compare('1.27.0-alpha') !== 0;
// Set the default thumb size (core 30306c37)
var thumbConf = (mwApiVersion.compare('1.23.0') < 0) ? baseConfig : resultConf;
var thumblimits = thumbConf.general.thumblimits;
var thumbsize = thumbConf.defaultoptions.thumbsize;
this.widthOption = thumblimits[thumbsize];
// Mapping from canonical namespace name to id
// The English namespace names are built-in and will work in any language.
this.canonicalNamespaces = {
media: -2,
special: -1,
main: 0,
'': 0,
talk: 1,
user: 2,
user_talk: 3,
project: 4,
project_talk: 5,
file: 6,
image: 6,
file_talk: 7,
image_talk: 7,
mediawiki: 8,
mediawiki_talk: 9,
template: 10,
template_talk: 11,
help: 12,
help_talk: 13,
category: 14,
category_talk: 15,
};
// Seed localized namespace name to id mapping with canonicalNamespaces
this.namespaceIds = JSUtils.mapObject(this.canonicalNamespaces);
// The interwiki prefix
this.iwp = prefix || "";
var mwApiConf = parsoidConfig.mwApiMap.get(prefix);
// The URI for api.php on this wiki.
this.apiURI = mwApiConf.uri;
// Pass this along to the api request
this.strictSSL = mwApiConf.strictSSL;
// Skip any form of proxying if the wiki is non-global
this.nonGlobal = mwApiConf.nonglobal;
// A restricted wiki, where RESTBase isn't setup
this.isRestricted = mwApiConf.restricted;
// The proxy to use for this wiki.
this.apiProxy = parsoidConfig.getAPIProxy(prefix);
// Store the wiki's main page.
this.mainpage = general.mainpage;
// The *wiki* language
// This should only be used for "UI" purposes (ie, it's almost always
// wrong to use this in Parsoid); instead use `env.page.pagelanguage`
// and `env.page.pagelanguagedir` which can override this on a per-page
// basis for content.
this.lang = general.lang;
this.rtl = general.rtl !== undefined;
var names = resultConf.namespaces;
var nkeys = Object.keys(names);
// Mapping from namespace id to the canonical name. Start with defaults.
this.namespaceNames = {
'6': 'File',
'-2': 'Media',
'-1': 'Special',
'0': '',
'14': 'Category',
};
this._categoryRegexpSource = JSUtils.escapeRegExpIgnoreCase("Category");
this._specialAliases = ['Special'];
// Namespace IDs that have subpages enabled.
this.namespacesWithSubpages = {};
for (var nx = 0; nx < nkeys.length; nx++) {
nsid = nkeys[nx];
name = names[nsid];
this.namespaceNames[nsid] = name['*'];
if (nsid === '-1' && name['*'] !== 'Special') {
this._specialAliases.push(name['*']);
}
if (nsid === "14" && name['*'] !== "Category") {
this._categoryRegexpSource += "|" + JSUtils.escapeRegExpIgnoreCase(name['*']);
}
this.namespaceIds.set(Util.normalizeNamespaceName(name['*']), Number(nsid));
if (name.canonical) {
// XXX: is this b/c?
this.canonicalNamespaces[Util.normalizeNamespaceName(name.canonical)] =
Number(nsid);
} else {
this.canonicalNamespaces[Util.normalizeNamespaceName(name['*'])] =
Number(nsid);
}
if ('subpages' in names[nsid]) {
this.namespacesWithSubpages[Number(nsid)] = true;
}
}
// add in a parsoid pseudo-namespace
var aliases = resultConf.namespacealiases;
for (var ax = 0; ax < aliases.length; ax++) {
this.namespaceIds.set(Util.normalizeNamespaceName(aliases[ax]['*']), aliases[ax].id);
if (aliases[ax].id === -1) {
this._specialAliases.push(aliases[ax]['*']);
}
}
// See https://www.mediawiki.org/wiki/Manual:$wgInterwikiMagic
this.interwikimagic = general.interwikimagic !== undefined;
// Whether the Linter MediaWiki extension is installed
this.linterEnabled = general.linter !== undefined;
// Corresponds to $wgCiteResponsiveReferences
this.responsiveReferences = {
enabled: general.citeresponsivereferences !== undefined,
threshold: 10,
};
// The interwikiMap maps prefixes to the corresponding information
// gathered from the api query (prefix, language, url, local)
this.interwikiMap = new Map();
var iwMap = resultConf.interwikimap;
Object.keys(iwMap).forEach(function(key) {
var interwiki = iwMap[key];
if (!/\$1/.test(interwiki.url)) {
interwiki = Util.clone(iwMap[key]);
// Fix up broken interwiki hrefs that are missing a $1 placeholder
// Just append the placeholder at the end.
// This makes sure that the interWikiMatcher below adds one match
// group per URI, and that interwiki links work as expected.
interwiki.url += '$1';
}
conf.interwikiMap.set(interwiki.prefix, interwiki);
});
var updatePatterns = function(keys, patterns, filter) {
conf.interwikiMap.forEach(function(val, key) {
if (!filter(val)) {
return;
}
var url = val.url;
var protocolRelative = url.startsWith('//');
if (val.protorel !== undefined) {
url = url.replace(/^https?:/, '');
protocolRelative = true;
}
// full-url match pattern
keys.push(key);
patterns.push(
// Support protocol-relative URLs
(protocolRelative ? '(?:https?:)?' : '') +
// Avoid regexp escaping on placeholder
JSUtils.escapeRegExp(url.replace('$1', '%1'))
// Now convert placeholder to group match
.replace('%1', '(.*?)')
);
if (val.local !== undefined) {
// ./$interwikiPrefix:$title and
// $interwikiPrefix%3A$title shortcuts
// are recognized and the local wiki forwards
// these shortcuts to the remote wiki
keys.push(key);
patterns.push('^\\.\\/' + val.prefix + ':(.*?)');
keys.push(key);
patterns.push('^' + val.prefix + '%3A(.*?)');
}
});
};
var cachedMatcher = null;
this.interWikiMatcher = function() {
if (cachedMatcher) {
return cachedMatcher;
}
var keys = [];
var patterns = [];
// For html -> wt reverse mapping, prefer language interwiki prefixes
// over other interwiki prefixes. So, use "en" instead of "wikipedia"
// for English wikipedia interwiki links.
updatePatterns(keys, patterns, function(val) { return !!val.language; });
updatePatterns(keys, patterns, function(val) { return !val.language; });
var reString = '^(?:' + patterns.join('|') + ')$';
var regExp = new RegExp(reString, 'i');
var matchFunc = function(s) {
var matches = s.match(regExp);
if (matches) {
var groups = matches.slice(1);
for (var i = 0; i < groups.length; i++) {
if (groups[i] !== undefined) {
var key = keys[i];
// The interwiki prefix: 'en', 'de' etc
if (conf.interwikiMap.get(key).language) {
// Escape language interwikis with a colon
key = ':' + key;
}
return [ key, groups[i] ];
}
}
}
return false;
};
// cache this result so we don't have to recompute it later
cachedMatcher = { match: matchFunc };
return cachedMatcher;
};
// "mw" here refers to "magicwords", not "mediawiki"
var mws = resultConf.magicwords;
var mw;
// Function hooks on this wiki
var functionHooks = new Set(resultConf.functionhooks || []);
this.functionHooks = new Map();
// Variables on this wiki
var variables = new Set(resultConf.variables || []);
this.variables = new Map();
// FIXME: Export this from CoreParserFunctions::register, maybe?
var noHashFunctions = new Set([
'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc',
'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl',
'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', 'bidi',
'numberofpages', 'numberofusers', 'numberofactiveusers',
'numberofarticles', 'numberoffiles', 'numberofadmins',
'numberingroup', 'numberofedits', 'language',
'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath',
'pagesincategory', 'pagesize', 'protectionlevel', 'protectionexpiry',
'namespacee', 'namespacenumber', 'talkspace', 'talkspacee',
'subjectspace', 'subjectspacee', 'pagename', 'pagenamee',
'fullpagename', 'fullpagenamee', 'rootpagename', 'rootpagenamee',
'basepagename', 'basepagenamee', 'subpagename', 'subpagenamee',
'talkpagename', 'talkpagenamee', 'subjectpagename',
'subjectpagenamee', 'pageid', 'revisionid', 'revisionday',
'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear',
'revisiontimestamp', 'revisionuser', 'cascadingsources',
// Special callbacks in core
'namespace', 'int', 'displaytitle', 'pagesinnamespace',
]);
// Canonical magic word names on this wiki, indexed by aliases.
this.magicWords = {};
// Lists of aliases, indexed by canonical magic word name.
this.mwAliases = {};
// RegExps matching aliases, indexed by canonical magic word name.
this._mwRegexps = {};
// Behavior switch regexp
this.bswRegexp = null;
// List of magic word aliases with $1 in their names, indexed by canonical name.
// Ex: `img_width`, alias `$1px`; `img_page`, aliases `page=$1` and `page $1`
// Currently all such magic words are in fact image options (taking parameters), not magic words!
this._parameterizedAliases = {};
var bsws = [];
for (var j = 0; j < mws.length; j++) {
const reAliases = [];
mw = mws[j];
aliases = mw.aliases;
if (aliases.length > 0) {
this.mwAliases[mw.name] = [];
this._parameterizedAliases[mw.name] = [];
}
for (var k = 0; k < aliases.length; k++) {
var alias = aliases[k];
var match = alias.match(/^__(.*)__$/);
if (match) {
if (mw['case-sensitive'] !== '') {
bsws.push(JSUtils.escapeRegExpIgnoreCase(match[1]));
} else {
bsws.push(JSUtils.escapeRegExp(match[1]));
}
}
this.mwAliases[mw.name].push(alias);
reAliases.push(alias);
if (mw['case-sensitive'] !== '') {
alias = alias.toLowerCase();
this.mwAliases[mw.name].push(alias);
}
if (variables.has(mw.name)) {
this.variables.set(alias, mw.name);
}
// See Parser::setFunctionHook
if (functionHooks.has(mw.name)) {
var falias = alias;
if (falias.substr(-1) === ':') { falias = falias.slice(0, -1); }
if (!noHashFunctions.has(mw.name)) { falias = '#' + falias; }
this.functionHooks.set(falias, mw.name);
}
this.magicWords[alias] = mw.name;
if (alias.match(/\$1/) !== null) {
this._parameterizedAliases[mw.name].push(alias);
}
}
this._mwRegexps[mw.name] =
new RegExp('^(' +
reAliases.map(
mw['case-sensitive'] === '' ?
JSUtils.escapeRegExp :
JSUtils.escapeRegExpIgnoreCase
).join('|') +
')$');
}
this.bswRegexp = new RegExp(
'(?:' + bsws.join("|") + ')'
);
this.bswPagePropRegexp = new RegExp(
'(?:^|\\s)mw:PageProp/' + this.bswRegexp.source + '(?=$|\\s)'
);
// Special page names on this wiki, indexed by aliases.
// We need these to recognize localized ISBN magic links.
var specialPages = this.specialPages = new Map();
(resultConf.specialpagealiases || []).forEach(function(special) {
specialPages.set(
special.realname,
new RegExp(
special.aliases.map(function(a) {
// Treat space and underscore interchangeably, and use
// escapeRegExp to protect special characters in the title
return a.split(/[ _]/g).map(JSUtils.escapeRegExp).join('[ _]');
}).join('|')
)
);
});
// Special case for localized ISBN ExtResourceMatcher
var isbnRegExp = new RegExp(
'^(?:(?:[.][.]?/)*)' +
'(?:' + this._specialAliases.map(function(a) {
return a.split(/[ _]/g).map(JSUtils.escapeRegExp).join('[ _]');
}).join('|') + ')(?:%3[Aa]|:)(?:' +
(this.specialPages.get('Booksources') || /Booksources/).source +
')(?:%2[Ff]|/)(\\d+[Xx]?)$',
// Strictly speaking, only the namespace is case insensitive.
// But we're feeling generous.
'i'
);
var superMatch = WikiConfig.prototype.ExtResourceURLPatternMatcher.match;
this.ExtResourceURLPatternMatcher = {
match: function(s) {
var r = superMatch(s);
if (!r) {
var m = s.match(isbnRegExp);
if (m) {
return [ 'ISBN', m[1] ];
}
}
return r;
},
};
// Regex for stripping useless stuff out of the regex messages
var stripRegex = /^\/\^(.*)\$\//;
// Convert PCRE \x{ff} style code points used by PHP to JS \u00ff style code
// points. For background search for \u in http://www.pcre.org/pcre.txt.
function convertCodePoints(regexp) {
return regexp.replace(/\\x\{([0-9a-fA-F]+)\}/g, function(m, chars) {
// pad to four hex digits
while (chars.length < 4) {
chars = '0' + chars;
}
if (chars.length === 4) {
return '\\u' + chars;
} else {
return '\\u{' + chars + '}';
}
});
}
// Helper function for parsing the linktrail/linkprefix regexes
function buildLinkNeighbourRegex(string, isTrail) {
var regexResult = string.match(stripRegex);
if (regexResult !== null) {
regexResult = regexResult[1].replace(/\(\.\*\??\)/, '');
regexResult = convertCodePoints(regexResult);
}
if (regexResult === '()') {
// At least zh gives us a linktrail value of /^()(.*)$/sD, which
// would match anything. Refuse to use such a non-sense regexp.
return null;
}
if (isTrail) {
regexResult = '^' + regexResult;
} else {
regexResult += '$';
}
try {
return new RegExp(regexResult, 'u');
} catch (e) {
console.error(e);
return null;
}
}
// Get the general information and use it for article, upload, and image
// paths.
if (resultConf.general) {
if (general.mainpage) {
this.mainpage = general.mainpage;
}
if (general.articlepath) {
if (general.server) {
this.baseURI = general.server + general.articlepath.replace(/\$1/, '');
}
this.articlePath = general.articlepath;
}
if (general.script) {
this.script = general.script;
}
if (general.scriptpath) {
this.scriptpath = general.scriptpath;
}
if (general.server) {
this.server = general.server;
}
if (general.linktrail) {
this.linkTrailRegex = buildLinkNeighbourRegex(general.linktrail, true);
} else if (general.linktrail === '') {
this.linkTrailRegex = null;
} else {
this.linkTrailRegex = Util.linkTrailRegex;
}
if (general.linkprefixcharset) {
// New MediaWiki post https://gerrit.wikimedia.org/r/#/c/91425/
// returns just the char class, for example 'a-z'.
try {
this.linkPrefixRegex = new RegExp(
'[' + convertCodePoints(general.linkprefixcharset) + ']+$', 'u');
} catch (e) {
console.error(e);
this.linkPrefixRegex = null;
}
} else if (general.linkprefix) {
this.linkPrefixRegex = buildLinkNeighbourRegex(general.linkprefix, false);
} else {
this.linkPrefixRegex = null;
}
if (general.case === 'case-sensitive') {
this.caseSensitive = true;
} else {
this.caseSensitive = false;
}
if (general.externalimages) {
this.allowExternalImages = general.externalimages;
}
if (general.imagewhitelistenabled !== undefined) {
this.enableImageWhitelist = true;
// XXX we don't actually support the on-wiki whitelist (T53268)
// if we did, we would probably want to fetch and cache
// MediaWiki:External image whitelist
// here (rather than do so on every parse)
} else {
this.enableImageWhitelist = false;
}
}
if (!this.baseURI) {
throw new Error('No baseURI was provided by the config request we made.');
}
// Allowed protocol prefixes
this._protocols = {};
// Get information about the allowed protocols for external links.
for (var px = 0; px < protocols.length; px++) {
var proto = protocols[px];
this._protocols[proto] = true;
}
var alternation = protocols.map(JSUtils.escapeRegExp).join('|');
this._anchoredProtocolRegex = new RegExp('^(' + alternation + ')', 'i');
this._unanchoredProtocolRegex = new RegExp('(?:\\W|^)(' + alternation + ')', 'i');
// Record info about registered extensions (native and otherwise)
this.extConfig = {
tags: new Map(), // Extension tags on this wiki, indexed by their aliases.
domProcessors: [], // Array since order of running processors might matter
styles: new Set(),
contentModels: new Map(),
};
if (resultConf.extensiontags) {
var extensions = resultConf.extensiontags;
for (var ex = 0; ex < extensions.length; ex++) {
// Strip the tag wrappers because we only want the name
this.extConfig.tags.set(
extensions[ex].replace(/(^<|>$)/g, '').toLowerCase(), null);
}
}
// Register native extension handlers second to overwrite the above.
this.extConfig.contentModels.set('wikitext', {
toHTML: function(env) {
// Default: wikitext parser.
return env.pipelineFactory.parse(env.page.src);
},
fromHTML: function(env, body, useSelser) {
// Default: wikitext serializer.
return FromHTML.serializeDOM(env, body, useSelser);
},
});
mwApiConf.extensions.forEach(config => this.registerExtension(config));
// Somewhat annoyingly, although LanguageConversion is turned on by
// default for all WMF wikis (ie, $wgDisableLangConversion = false, as
// reported by `general.langconversion` in siteinfo), but the
// -{ }- syntax is only parsed when the current *page language*
// has variants. We can't use the "UI language" (in siteinfo
// `general.lang`) and "UI variants" (in `general.fallback` and
// `general.variants`), because the *page language* could be quite
// different. Use the mechanism introduced in T153341 instead.
this.variants = new Map();
this.langConverterEnabled = new Set();
Object.keys(languagevariants).forEach((code) => {
if (general.langconversion !== undefined) {
this.langConverterEnabled.add(code);
}
Object.keys(languagevariants[code]).forEach((v) => {
this.variants.set(v, {
base: code,
fallbacks: languagevariants[code][v].fallbacks,
});
});
});
// Match a wikitext line containing just whitespace, comments, and
// sol transparent links and behavior switches.
// Redirects should not contain any preceding non-whitespace chars.
this.solTransparentWikitextRegexp = JSUtils.rejoin(
"^[ \\t\\n\\r\\0\\x0b]*",
"(?:",
"(?:", this._mwRegexps.redirect.source.slice(2, -2), ")",
'[ \\t\\n\\r\\x0c]*(?::[ \\t\\n\\r\\x0c]*)?' +
'\\[\\[[^\\]]+\\]\\]',
")?",
"(?:",
"\\[\\[",
"(?:", this._categoryRegexpSource, ")",
"\\:[^\\]]*?\\]\\]",
"|",
"__", this.bswRegexp, "__",
"|",
Util.COMMENT_REGEXP,
"|",
"[ \\t\\n\\r\\0\\x0b]",
")*$"
);
// Almost the same thing (see the start/end), but don't allow whitespace.
this.solTransparentWikitextNoWsRegexp = JSUtils.rejoin(
"((?:",
"(?:", this._mwRegexps.redirect.source.slice(2, -2), ")",
'[ \\t\\n\\r\\x0c]*(?::[ \\t\\n\\r\\x0c]*)?' +
'\\[\\[[^\\]]+\\]\\]',
")?",
"(?:",
"\\[\\[",
"(?:", this._categoryRegexpSource, ")",
"\\:[^\\]]*?\\]\\]",
"|",
"__", this.bswRegexp, "__",
"|",
Util.COMMENT_REGEXP,
")*)"
);
}
WikiConfig.prototype.registerExtension = function(ExtConfig) {
var ext = new ExtConfig();
var tags = ext.config.hasOwnProperty('tags') ? ext.config.tags : [];
tags.forEach(function(tag) {
this.extConfig.tags.set(tag.name.toLowerCase(), tag);
}, this);
if (ext.config.hasOwnProperty('domProcessors')) {
this.extConfig.domProcessors.push({ extName: ext.config.name, procs: ext.config.domProcessors });
}
if (ext.config.hasOwnProperty('styles')) {
ext.config.styles.forEach(function(s) {
this.extConfig.styles.add(s);
}, this);
}
Object.keys(ext.config.contentmodels || {}).forEach(function(cm) {
// For compatibility with mediawiki core, the first
// registered extension wins.
if (this.extConfig.contentModels.has(cm)) { return; }
this.extConfig.contentModels.set(cm, ext.config.contentmodels[cm]);
}, this);
};
/**
* Get the canonical name of a magic word alias.
*
* @param {string} alias
* @return {string|null}
*/
WikiConfig.prototype.getMagicWordIdFromAlias = function(alias) {
if (this.magicWords.hasOwnProperty(alias)) {
return this.magicWords[alias];
} else {
return null;
}
};
/**
* Get canonical magicword name for the input word.
*
* @param {string} word
* @return {string}
*/
WikiConfig.prototype.magicWordCanonicalName = function(word) {
return this.getMagicWordIdFromAlias(word) || this.getMagicWordIdFromAlias(word.toLowerCase());
};
/**
* Check if a string is a recognized magic word.
*
* @param {string} word
* @return {boolean}
*/
WikiConfig.prototype.isMagicWord = function(word) {
return this.magicWordCanonicalName(word) !== null;
};
/**
* Convert the internal canonical magic word name to the wikitext alias.
*/
WikiConfig.prototype.getMagicWordWT = function(word, suggest) {
var aliases = this.mwAliases[word];
if (!Array.isArray(aliases) || aliases.length === 0) {
return suggest;
}
var ind = 0;
if (suggest) {
ind = aliases.indexOf(suggest);
}
return aliases[ind > 0 ? ind : 0];
};
/**
* Get a regexp matching a localized magic word, given its id.
*
* @param {string} id
* @return {RegExp}
*/
WikiConfig.prototype.getMagicWordMatcher = function(id) {
// if 'id' is not found, return a regexp which will never match.
return this._mwRegexps[id] || /[]/; // eslint-disable-line
};
/**
* Get a matcher function for fetching values out of "magic words"
* with `$1` in their aliases.
*
* @param {Map} optionsMap The map of options you want to check for
* (eg the map of all interpolated image options).
* @return {Function}
*/
WikiConfig.prototype.getParameterizedAliasMatcher = function(optionsMap) {
let mwlist = [];
let first = true;
let regexString = '';
optionsMap.forEach((value, option) => {
if (!first) {
regexString += '|';
}
first = false;
var aliases = this._parameterizedAliases[option];
if (aliases === undefined) { aliases = ['UNKNOWN_' + option]; }
regexString += aliases.join('|')
.replace(/((?:^|\|)(?:[^\$]|\$[^1])*)($|\|)/g, '$1$$1$2')
.replace(/\$1/g, '(.*)');
mwlist = mwlist.concat(aliases);
});
const regexp = new RegExp('^(?:' + regexString + ')$');
return (text) => {
const matches = text.match(regexp);
if (matches === null) {
return null;
}
let value;
let alias = null;
matches.shift(); // drop the matched string
for (let ix = 0; ix < matches.length && ix < mwlist.length && alias === null; ix++) {
if (matches[ix] !== undefined) {
alias = mwlist[ix];
value = matches[ix];
}
}
if (alias === null) {
return null;
}
const canonical = this.getMagicWordIdFromAlias(alias);
if (canonical !== null) {
return { k: canonical, v: value };
}
return null;
};
};
/**
* Builds a magic word string out of an alias with $1 in it and a value for the option.
*
* @param {string} alias
* @param {string} value
* @return {string}
*/
WikiConfig.prototype.replaceInterpolatedMagicWord = function(alias, value) {
return alias.replace(/\$1/, value);
};
/**
* Default ISBN/RFC/PMID resource URL patterns
* @private
*/
WikiConfig.prototype.ExtResourceURLPatterns = {
// ISBN validation is done below in serializer.
'ISBN': {
prefix: "(?:(?:[.][.]?/)*)",
re: 'Special(?:%3[Aa]|:)Book[Ss]ources(?:%2[Ff]|/)%isbn',
},
'RFC': {
prefix: '[^\/]*',
re: '//tools.ietf.org/html/rfc%s',
},
'PMID': {
prefix: '[^\/]*',
re: '//www.ncbi.nlm.nih.gov/pubmed/%s?dopt=Abstract',
},
};
var unispace = /[ \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g;
/**
* @private
*/
WikiConfig.prototype.ExtResourceSerializer = {
'ISBN': function(hrefWT, href, content) {
var normalized = Util.decodeWtEntities(content).replace(unispace, ' ')
.replace(/[\- \t]/g, '').toUpperCase();
// validate ISBN length and format, so as not to produce magic links
// which aren't actually magic
var valid = /^ISBN(97[89])?\d{9}(\d|X)$/.test(normalized);
if (hrefWT.join('') === normalized && valid) {
return content;
} else {
href = href.replace(/^\.\//, ''); // strip "./" prefix
return '[[' + href + '|' + content + ']]';
}
},
'RFC': function(hrefWT, href, content) {
var normalized = Util.decodeWtEntities(content).replace(unispace, ' ')
.replace(/[ \t]/g, ' ');
return hrefWT.join(' ') === normalized ? content : '[' + href + ' ' + content + ']';
},
'PMID': function(hrefWT, href, content) {
var normalized = Util.decodeWtEntities(content).replace(unispace, ' ')
.replace(/[ \t]/g, ' ');
return hrefWT.join(' ') === normalized ? content : '[' + href + ' ' + content + ']';
},
};
/**
* Matcher for RFC/PMID URL patterns, returning the type and number. The match
* method takes a string and returns false on no match or a tuple like this on match:
* ['RFC', '12345']
* @property {Object} ExtResourceURLPatternMatcher
* @property {Function} ExtResourceURLPatternMatcher.match
*/
WikiConfig.prototype.ExtResourceURLPatternMatcher = (function() {
var keys = Object.keys(WikiConfig.prototype.ExtResourceURLPatterns);
var patterns = [];
keys.forEach(function(key) {
var reOpts = WikiConfig.prototype.ExtResourceURLPatterns[key];
var re = JSUtils.escapeRegExp(reOpts.re).replace('%s', '(\\w+)').replace('%d', '(\\d+)').replace('%isbn', '(\\d+[Xx]?)');
patterns.push("^(?:" + (reOpts.prefix || "") + re + ")$");
});
var regExp = new RegExp(patterns.join('|'));
var match = function(s) {
var matches = s.match(regExp);
if (matches) {
var groups = matches.slice(1);
for (var i = 0; i < groups.length; i++) {
if (groups[i] !== undefined) {
return [
// The key: 'PMID', 'RFC' etc
keys[i],
groups[i],
];
}
}
}
return false;
};
return {
match: match,
};
})();
/**
* Matcher for valid protocols, must be anchored at start of string.
* @param {string} potentialLink
* @return {boolean}
*/
WikiConfig.prototype.hasValidProtocol = function(potentialLink) {
return this._anchoredProtocolRegex.test(potentialLink);
};
/**
* Matcher for valid protocols, may occur at any point within string.
* @param {string} potentialLink
* @return {boolean}
*/
WikiConfig.prototype.findValidProtocol = function(potentialLink) {
return this._unanchoredProtocolRegex.exec(potentialLink);
};
/**
* Detect which parameters are available in the MediaWiki API.
* @method
* @return {Promise}
*/
WikiConfig.prototype.detectFeatures = Promise.async(function *(env) {
var query = yield ParamInfoRequest.promise(env);
// Do we have the "videoinfo" prop? Only relevant to legacy requests.
this.useVideoInfo = !env.conf.parsoid.useBatchAPI &&
Array.isArray(query.parameters) &&
query.parameters.some(
o => (o && o.name === 'prop' && o.type.indexOf('videoinfo') > -1)
);
});
if (typeof module === 'object') {
module.exports.WikiConfig = WikiConfig;
}