// @ts-check
module.exports = processOnSave;
/** Transformation categories. */
var transformCats = /** @type const */ ({
NONE: undefined,
INVISIBLE: "osynliga tecken",
SPACE: "mellanslag",
LINE: "radbrytningar",
LANG_CODE: "språkkoder",
TPL_NAME: "mallnamn",
WARN: "spåra varning",
});
var importantTransformCats = [
transformCats.WARN,
transformCats.LANG_CODE,
transformCats.TPL_NAME,
];
/** @returns {Operation} */
function getOperations() {
return [
// 💾 Lägg till varningar i {{tidy}} (steg 1: ta bort)
trackWarnings("remove"),
// 💾 Inga hårda mellanslag eller tabbtecken
replace("INVISIBLE", //g, " "),
// 💾 Max 1 mellanslag i följd
replace("SPACE", / {2,}/g, " "),
// 💾 Trimma bort mellanslag på rader
replace("SPACE", /^ | $/gm, ""),
// 💾 Max 1 tom rad
replace("LINE", /\n{3,}/g, "\n\n"),
// 💾 Ingen tom rad i början på en sida
replace("LINE", /^\n/, ""),
opacify("html comment"), // <!-- --> => OPAQUE
opacify("nowiki"), // <nowiki>...</nowiki> => OPAQUE
// 💾 Trimma bort mellanslag i mallnamn
replace("SPACE", /{{ ?(+?) ?(}}|\|)/g, "{{$1$2"),
// 💾 Korrigera mallnamn
{
cat: "TPL_NAME",
op: "template name",
process: correctTemplateNames,
},
// 💾 Mellanslag före genus- och numerusmall m.fl.
replace(
"SPACE",
/(,;)}])({{(?:|mf|(?:s|pl)|pf|impf|impfpf|bf|okomp|oböjl|oräkn|peri)(?=\||}}))/g,
"$1 $2"
),
// 💾 Inget mellanslag i början på rader i listor
replace("SPACE", /^(+) /gm, "$1"),
opacify("link"), // ] => OPAQUE
opacify("tag"), // <div style="...">...</div> => OPAQUE
opacify("template param"), // {{mall|a=b}} => {{mall|OPAQUE}} (recursively)
opacify("table"), // {| ... |} => OPAQUE
// 💾 Trimma bort mellanslag i rubriknamn
replace("SPACE", /^(==+) ?(+?) ?(==+)$/gm, "$1$2$3"),
replaceAll("LINE", [
// 💾 Tom rad ovanför rubrik
{ search: /(\n)(==++==+)$/gm, replace: "$1\n$2" },
// 💾 Ingen tom rad under rubrik
{ search: /^(==++==+\n)\n/gm, replace: "$1" },
]),
// ⚠️ Varna om felaktiga rubriker
{ cat: "NONE", op: "check headings", process: checkHeadings },
// 💾 Tom rad mellan översättningsavsnitt
replace(
"LINE",
/^({{(ö-botten|ö-se\|+)}})\n(?={{(ö-topp|ö-topp-även|ö-topp-granska|ö-se))/gm,
"$1\n\n"
),
// 💾 Ingen tom rad ovanför listrader
replace("LINE", /\n(\n)/g, "$1"),
// 💾 Mellanslag på fetstilsrad
replace(
"SPACE",
/^('''+)( ?)(.+?)( ?)('''+)(?)(.*)$/gm,
replaceSpaceOnHeadwordLine
),
// 💾 Mellanslag efter {{tagg}}
replace("SPACE", /^(#{{tagg\|+}})()/gm, "$1 $2"),
opacify("inner template"), // {{mall|a {{mall}} b}} => {{mall|a OPAQUE b}}
// 💾 Trimma bort mellanslag i mallparametrar
{
cat: "SPACE",
op: "template param space",
process: removeTemplateParamSpace,
},
// 💾 Fixa språkkod
{ op: "lang code", cat: "LANG_CODE", process: processLangCodes },
{ op: "trans lang code", cat: "LANG_CODE", process: processTransLangCodes },
// ⚠️ Varna om upprepad parameter
{ cat: "NONE", op: "check template param", process: checkTemplateParam },
// 💾 Lägg till varningar i {{tidy}} (steg 2: lägg till)
trackWarnings("add"),
];
}
var maxDepth = 10;
/**
* @typedef {import("./tidy-data.js").Data} Data
* @typedef {import("./tidy-data.js").LangNameLc} LangNameLc
* @typedef {import("./tidy-data.js").LangNameUcfirst} LangNameUcfirst
* @typedef {import("./tidy-data.js").Template} Template
*
* @typedef Operation
* @property {OperationName} op Operation name
* @property {string} Description
* @property {TransformationCategory} cat Transformation category
* @property {boolean} Whether to process this operation, even when there are errors.
* @property {(context: Context) => void} process
*
* @typedef {(
* | "replace"
* | "opacify"
* | "lang code"
* | "trans lang code"
* | "check headings"
* | "check template param"
* | "template name"
* | "template param space"
* | "track warnings"
* )} OperationName
*
* @typedef Context
* @property {Context} rootContext
* @property {Context} allContexts
* @property {string} _wikitext Private
* @property {string} wikitext
* @property {string} replacement The replacement string (`\0${number}\0`). Empty string if not a replacement.
* @property {OpacifyNames}
* @property {string}
* @property {boolean} aborted
* @property {string} transformCats Transformation categories, to be displayed in a notification.
* @property {Set<string>} _transformCats Transformation categories, to be displayed in a notification.
* @property {number} revCount Revision index. Changes when *any* of the contexts changes.
* @property {Set<string>} warnings
* @property {(...args: InterpolateArgs) => void} warn
* @property {(this: Context, ...args: InterpolateArgs) => void} error
* @property {Set<OperationName>} warnOps Operations that have caused warnings.
* @property {(str: string) => string} unopaque
* @property {(creator: OpacifyNames, wikitext: string, creatorTemplate?: string) => Context} createChild
* @property {(operations: Operation) => Context} process
* @property {(replacement: string) => Context} getByReplacement
* @property {number} processCounter To prevent infinite loops due to coding bugs
* @property {Data} data
* @property {(langName: string) => { langName: string; replacement: string } | undefined} getLangReplacement
*
* @typedef {keyof typeof transformCats} TransformationCategory
*/
/** @type {Operation | undefined} */
var operationsCache;
/**
* @param {string} wikitext
* @param {Data} data
*/
function processOnSave(wikitext, data) {
if (!operationsCache) {
operationsCache = getOperations();
}
return processInternal(createContext(wikitext, data), operationsCache);
}
/**
* @param {string} wikitext
* @param {Data} data
* @returns {Context}
*/
function createContext(wikitext, data) {
var regex = /\0(+)\0/g;
/** @type {Context} */
var context = {
_wikitext: wikitext,
get wikitext() {
return this._wikitext;
},
set wikitext(value) {
if (this._wikitext !== value) {
this._wikitext = value;
this.rootContext.revCount++;
}
},
replacement: "",
data: data,
rootContext: /** @type {*} */ (undefined),
allContexts: ,
createChild: function (creator, wikitext, creatorTemplate) {
/** @type {Context} */
var child = Object.create(this);
child._wikitext = wikitext;
child.replacement = "\0" + this.allContexts.length + "\0";
child.creator = creator;
child.creatorTemplate = creatorTemplate;
this.allContexts.push(child);
return child;
},
processCounter: 0,
process: function (operations) {
if (this.rootContext.processCounter > 1000) {
throw new Error("Infinite process loop?");
}
this.rootContext.processCounter++;
return processInternal(this, operations);
},
aborted: false,
warnings: new Set(),
_transformCats: new Set(),
get transformCats() {
var t = this._transformCats;
// Find only the important transformation categories, if any.
var important = importantTransformCats.filter(function (x) {
return t.has(x);
});
if (important.length) {
return important;
}
// If there are no important categories, return them all.
return Array.from(this._transformCats);
},
revCount: 0,
warn: function () {
var text = interpolate(/** @type {*} */ (arguments));
text = this.unopaque(text);
this.warnings.add(text);
},
error: function () {
arguments += " Processningen avbröts.";
this.warn.apply(this, /** @type {*} */ (arguments));
this.aborted = true;
},
warnOps: new Set(),
unopaque: function (str) {
var opaque = this.allContexts;
/**
* @param {string} _match
* @param {string} index
*/
function replacer(_match, index) {
return opaque.wikitext;
}
for (var i = 0; reTest(regex, str); i++) {
if (i > maxDepth) {
throw new Error(" Max depth exceeded");
}
str = str.replace(regex, replacer);
}
return str;
},
getByReplacement: function (replacement) {
regex.lastIndex = 0;
var match = regex.exec(replacement);
return match && match.length === replacement.length
? this.allContexts]
: undefined;
},
getLangReplacement: function (name) {
name = name.toLowerCase();
var repl = this.data.langReplacements.get(
/** @type {LangNameLc} */ (name)
);
if (repl) {
return { langName: name, replacement: repl };
}
},
};
context.rootContext = context;
context.allContexts.push(context);
return context;
}
/**
* @param {Context} context
* @param {Operation} operations
* @returns {Context}
*/
function processInternal(context, operations) {
operations.forEach(function (op) {
if (!context.aborted || op.alwaysProcess) {
var revCountBefore = context.revCount;
var warnCountBefore = context.warnings.size;
op.process(context);
if (context.revCount !== revCountBefore && op.cat !== "NONE") {
context._transformCats.add(transformCats);
}
if (context.warnings.size !== warnCountBefore) {
context.warnOps.add(op.op);
}
}
});
return context;
}
/**
* @typedef {]} InterpolateArgs
*
* @param {InterpolateArgs} args
*/
function interpolate(args) {
var i = 0;
return args.length === 1
? args
: args.replace(/%s/g, function () {
i++;
return args;
});
}
/**
* @param {string} str
* @param {RegExp} regex
* @param {(match: RegExpExecArray) => void} fn
*/
function regexForEach(str, regex, fn) {
regex.lastIndex = 0;
for (;;) {
var match = regex.exec(str);
if (!match) break;
fn(match);
}
}
/**
* @typedef {"html comment" | "nowiki" | "tag" | "template param" | "inner template" | "link" | "table"} TopLevelOpacifyNames
* @typedef {TopLevelOpacifyNames | "lang codes"} OpacifyNames
*/
/** @type {Record<TopLevelOpacifyNames, (context: Context) => void>} */
var opacifySetup = {
"html comment": processOpacifySimple.bind(
null,
"html comment",
/<!--*?-->/g,
/<!--|-->/g
),
// Ignored: <noinclude>, <includeonly>, <syntaxhighlight>
nowiki: processOpacifySimple.bind(
null,
"nowiki",
/<(nowiki|pre|ref|math|gallery|hiero)(?:\s*)?>*?<\/\1>|<(nowiki|ref)(?:\s*)?\/>/g,
/<\/?(nowiki|pre|ref|math|gallery|hiero)(?:\s*)?>/g
),
// Ignored: Self-closing <references>, <br>, <hr>
tag: processOpacifyTag.bind(
null,
/<\/?(div|span|sup|sub|big|small|code|u|s|b|p)(?:\s*)?>/gi
),
table: processOpacifySimple.bind(
null,
"table",
/^:*{\|*?^\|}$/gm,
/^{\||^\|}$/gm
),
// Match internal links.
link: processOpacifyLink.bind(
null,
// In the link target, don't support {}|<>.
/\{}|<>]+)(\|+?)?\]\]/g
),
// Match all templates and parser functions.
"template param": processOpacifyTemplateParams.bind(
null,
// Assume that templates don't have ':' in their name, so that we can match
// parser functions.
/{{+?|}}/g
),
// Match all templates and parser functions.
"inner template": processOpacifyInnerTemplate.bind(null, /{{+}}/g),
};
/**
* @param {TopLevelOpacifyNames} name
* @returns {Operation}
*/
function opacify(name) {
return {
cat: "NONE",
op: "opacify",
desc: name,
process: opacifySetup,
};
}
/**
* @param {TransformationCategory} cat
* @param {RegExp} search
* @param {Replacement} replace
* @returns {Operation}
*/
function replace(cat, search, replace) {
return replaceAll(cat, );
}
/**
* @typedef {string | ((...args: string) => string)} Replacement
*
* @param {TransformationCategory} cat
* @param {{search: RegExp, replace: Replacement}} replacements
* @returns {Operation}
*/
function replaceAll(cat, replacements) {
return {
cat: cat,
op: "replace",
desc: replacements
.map(function (x) {
return x.search + " => " + x.replace;
})
.join("; "),
process: function (context) {
replacements.forEach(function (x) {
context.wikitext = context.wikitext.replace(
x.search,
/** @type {string} */ (x.replace)
);
});
},
};
}
/**
* @param {OpacifyNames} name
* @param {RegExp} regex Regex for replacing.
* @param {RegExp} regexCheck Regex checking for any stray starts or ends.
* @param {Context} context
*/
function processOpacifySimple(name, regex, regexCheck, context) {
context.wikitext = context.wikitext.replace(regex, function (match) {
return context.createChild(name, match).replacement;
});
// Get first stray.
var stray = (context.wikitext.match(regexCheck) || );
if (stray) {
// Missing match.
context.error("Start/avslut för %s hittades inte.", stray);
}
}
/**
* @param {RegExp} regex
* @param {Context} context
*/
function processOpacifyLink(regex, context) {
// In the link text, don't support link or template syntax.
var reBadText = /\\]|{{|}}/;
var msg =
"Sidor innehållande ], {{mallar}} eller i vissa fall <taggar> som placerats innanför ] kan inte tolkas korrekt.";
context.wikitext = context.wikitext.replace(
regex,
/**
* @param {string} match
* @param {string} target
* @param {string} text
*/
function (match, target, text) {
if (reTest(reBadText, text)) {
context.error(msg);
return match;
}
if (match.includes("\n")) {
if (target.includes("\n")) {
context.error("Radbrytningar är inte tillåtna i länkar.");
return match;
}
// The newline is in the text, which *is* allowed. However, let's
// normalize it to just a space.
match = match.replace(/\n/g, " ");
}
return context.createChild("link", match).replacement;
}
);
var stray = context.wikitext.match(/\\]/g);
if (!context.aborted && stray) {
var balance = stray.reduce(function (sum, x) {
return sum + (x === "[[" ? 1 : -1);
}, 0);
if (balance === 0) {
// We've found an equal number of ']'. Assume that this means
// that the page is using syntax that we don't support, such as links in
// images or templates in images.
context.error(msg);
} else {
// Missing match.
context.error(
"%s hittades inte.",
balance < 0 ? "Start för ]]" : "Avslut för [["
);
}
}
}
/**
* @param {RegExp} regex
* @param {Context} context
*/
function processOpacifyTag(regex, context) {
regex.lastIndex = 0;
/**
* A stack of starts, i.e. `<div style="...">`.
*/
var stack = ;
var parts = "";
var lastIndex = 0;
for (;;) {
var match = regex.exec(context.wikitext);
if (!match) break;
var isStart = !match.startsWith("</");
if (isStart) {
stack.push(match);
continue;
}
var matchingStart = stack.pop();
if (!matchingStart) {
// Unexpected end tag.
context.error('Oväntad "%s" (ingen matchande start).', match);
stack.length = 0;
break;
}
// Tag names must match.
if (match !== matchingStart) {
// Tag doesn't match.
context.error(
"%s har en %s som inte matchar.",
matchingStart,
match
);
stack.length = 0;
break;
}
// Only opacify the outermost tag.
if (stack.length === 0) {
parts +=
context.wikitext.substring(lastIndex, matchingStart.index) +
context.createChild(
"tag",
context.wikitext.substring(
matchingStart.index,
match.index + match.length
)
).replacement;
lastIndex = match.index + match.length;
}
}
if (stack.length !== 0) {
// Missing end tag.
context.error(
"Avslut för %s hittades inte.",
stack
.map(function (x) {
return x;
})
.join(", ")
);
}
parts += context.wikitext.substring(lastIndex);
context.wikitext = parts;
}
/**
* @param {RegExp} regex
* @param {Context} context
*/
function processOpacifyTemplateParams(regex, context) {
// It is important to clone the regex, so as to not get stuck in an infinite loop.
regex = new RegExp(regex);
/**
* A stack of starts, i.e. `{{template|`.
*/
var stack = ;
var parts = "";
var lastIndex = 0;
for (;;) {
var match = regex.exec(context.wikitext);
if (!match) break;
var isStart = match.startsWith("{{");
if (isStart) {
stack.push(match);
continue;
}
var matchingStart = stack.pop();
if (!matchingStart) {
// Unexpected template end.
context.error('Oväntad "}}" (ingen matchande start).');
break;
}
// Handle the outermost template by reprocessing inner templates.
if (stack.length === 0) {
var paramsStartIndex = matchingStart.index + matchingStart.length;
// Nothing to do, if there are no params.
if (paramsStartIndex === match.index) {
continue;
}
parts +=
context.wikitext.substring(
lastIndex,
matchingStart.index + matchingStart.length
) +
context
.createChild(
"template param",
context.wikitext.substring(paramsStartIndex, match.index),
matchingStart.slice(2, -1)
)
.process().replacement +
"}}";
lastIndex = match.index + match.length;
}
}
if (stack.length !== 0) {
// Unexpected template end.
context.error('Avslut för "%s" hittades inte.', stack.trim());
}
parts += context.wikitext.substring(lastIndex);
context.wikitext = parts;
}
/**
* Requires {@link processOpacifyTemplateParams} to run before this function.
* @param {RegExp} regex Regex for replacing.
* @param {Context} context
*/
function processOpacifyInnerTemplate(regex, context) {
context.allContexts.forEach(function (innerContext) {
// Only process inner templates, inside template params.
if (innerContext.creator !== "template param") {
return;
}
innerContext.wikitext = innerContext.wikitext.replace(
regex,
function (match) {
return innerContext.createChild("inner template", match).replacement;
}
);
if (innerContext.wikitext.includes("{{")) {
// Missing template end.
context.error('Avslut för "{{" hittades inte.');
} else if (innerContext.wikitext.includes("}}")) {
// Unexpected template end.
context.error('Oväntad "}}" (ingen matchande start).');
}
});
}
/**
* @param {"remove" | "add"} type
* @returns {Operation}
*/
function trackWarnings(type) {
if (type === "remove") {
return replace("NONE", /{{tidy}}/g, "");
}
return {
cat: "WARN",
op: "track warnings",
process: function (context) {
if (context.warnings.size) {
context.wikitext =
context.wikitext.replace(/\n+$/, "") + "\n\n{{tidy}}\n";
}
},
alwaysProcess: true,
};
}
/**
* @param {string} match The whole line.
* @param {string} apos1 Apostrophies before the word.
* @param {string} sp1 Space before the word.
* @param {string} word The word.
* @param {string} sp2 Space after the word.
* @param {string} apos2 Apostrophies after the word.
* @param {string} sep
* Separator between the bolded word and the rest of the line.
*
* It's a space (or missing) in the normal case, but can also be one of
* a few punctuation marks (`,`, `!`, `?`) or anything opaque.
*
* @param {string} rest The rest of the line.
*/
function replaceSpaceOnHeadwordLine(
match,
apos1,
sp1,
word,
sp2,
apos2,
sep,
rest
) {
// If the word itself contains syntax for italic or bold, something is
// wrong. Just return the original string without making any changes.
if (word.includes("''")) {
return match;
}
/** Whether the word starts with an apostrophy. */
var aStart = word === "'";
/** Whether the word end with an apostrophy. */
var aEnd = word === "'";
// 3: ''' is bold.
// 5: ''''' is bold+italic.
// 4: '''' is bold + apos as part of word (start or end).
// 6: '''''' is bold+italic + apos as part of word (start or end).
return (
// Apostrophies before the word.
(apos1 === "'''" || apos1 === "'''''"
? // apos1 doesn't contain a part of the word. Add a space if the
// word starts with an apostrophy.
apos1 + (aStart ? " " : "")
: // The final ' of apos1 is part of the word. Split apos1 by
// adding a space.
apos1.slice(1) + " '" + sp1) +
//
// The word itself.
word +
//
// Apostrophies after the word.
(apos2 === "'''" || apos2 === "'''''"
? // apos2 doesn't contain a part of the word. Add a space if the
// word ends with an apostrophy.
(aEnd ? " " : "") + apos2
: // The first ' of apos2 is part of the word. Split apos2 by
// adding a space.
sp2 + "' " + apos2.slice(1)) +
//
// The rest of the headword line. Add a space if necessary.
(rest ? (sep || " ") + rest : sep)
);
}
/** @type {string | undefined} */
var lcLangNamesCache;
/**
* Process lang codes in the translation section, {{ö}} and {{ö+}}.
* @param {Context} context
*/
function processTransLangCodes(context) {
var langCodesByName = context.data.langCodesByLcName;
if (!lcLangNamesCache) {
lcLangNamesCache = Array.from(langCodesByName.keys());
}
var langNames = lcLangNamesCache;
/** Language context. */
var langCtx = {
/** @type {LangNameLc | undefined} */
_name: undefined,
/** @type {LangNameLc | undefined} */
_subName: undefined,
/** @type {string | undefined} */
_code: undefined,
reset: function () {
this._name = this._subName = this._code = undefined;
},
/** @param {string} x */
set name(x) {
this._subName = this._code = undefined;
this._name = /** @type {LangNameLc} */ (x);
},
get hasName() {
return !!this._name;
},
/** @param {string} x */
set subName(x) {
this._code = undefined;
this._subName = /** @type {LangNameLc} */ (x);
},
/** @param {string} origCode */
_getCode: function (origCode) {
var name = this._name;
var subName = this._subName;
if (!name) {
// `name` is guaranteed to exist - the case when `name` is missing is
// handled elsewhere.
throw new Error("name invariant");
}
var mainCode = langCodesByName.get(name);
var subCode = subName && langCodesByName.get(subName);
// If there's a valid code to use, use that.
var mostSpecific = subCode || mainCode;
if (mostSpecific) {
return mostSpecific;
}
// Suppress any warnings by setting the param value to an empty string.
var suppressedWarning = origCode === "";
var suppress =
"För att undertrycka varningen, lämna språkkodsparametern tom.";
if (subName) {
if (!suppressedWarning) {
context.warn(
'Kombinationen av språknamnen "%s" och "%s" i översättningsavsnitt är inte tillåten eftersom båda språknamnen saknar en motsvarande språkkod. %s',
name,
subName,
suppress
);
}
} else {
// No `subName`.
// If there's a suggested replacement, use that.
var replacement = context.getLangReplacement(name);
if (replacement) {
context.warn(
'Ogiltigt språknamn "%s" i översättningsavsnitt. Använd %s.',
replacement.langName,
replacement.replacement
);
} else if (!suppressedWarning) {
// Suggest fuzzy match if it's good enough.
var bestMatch = levenshteinBest(langNames, name);
// Allowed fuzziness: 0.6.
if (bestMatch.distance < 0.6) {
context.warn(
'Ogiltigt språknamn "%s" i översättningsavsnitt: Menade du "%s"? %s',
bestMatch.search,
bestMatch.match,
suppress
);
} else {
context.warn(
'Ogiltigt språknamn "%s" i översättningsavsnitt. %s',
/** @type {string} */ (name || subName),
suppress
);
}
}
}
return origCode || "";
},
/**
* @this {typeof langCtx} For whatever reason, this must be given to make TypeScript happy.
* @param {string} origCode
*/
getCode: function (origCode) {
if (!this._code) {
this._code = this._getCode(origCode);
}
return this._code;
},
};
context.wikitext = context.wikitext.replace(
/((?:^|\n)====Översättningar====\n)(+?)(\n==|$)/g,
/**
* @param {string} match
* @param {string} curHeading
* @param {string} section
* @param {string} nextHeading
*/
function (match, curHeading, section, nextHeading) {
langCtx.reset();
var didChange = false;
var lines = section.split("\n");
for (var i = 0; i < lines.length; i++) {
var line = lines;
var didLineChange = false;
// Parse translation line.
var parts = /^(+)(+):(.*)/.exec(line);
if (!parts) {
// If the line starts with a bullet or contains {{ö}}, parsing should
// have succeeded.
if (/^|{{ö\+?/.test(line)) {
context.warn(
'Rad i översättningsavsnittet ska börja med "*språknamn:". Hittade "%s".',
line
);
}
langCtx.reset();
continue;
}
var bullet = parts;
var lang = parts;
var rest = parts;
if (rest.includes("*")) {
context.warn(
'Rad i översättningsavsnittet har "*" i mitten, saknas en radbrytning? Hittade "%s".',
line
);
langCtx.reset();
continue;
}
// Normalize lines beginning with `:`, `:*`, and `::`.
if (bullet.includes(":")) {
bullet = bullet.length === 1 ? "*" : "**";
didChange = didLineChange = true;
}
if (bullet === "*") {
langCtx.name = lang;
} else {
// bullet === "**"
if (!langCtx.hasName) {
context.warn(
'Rad i översättningsavsnittet ska börja med "*språknamn:". Hittade "%s".',
line
);
continue;
}
langCtx.subName = lang;
}
regexForEach(rest, /{{ö\+?(?:\|(*))?}}/g, function (match) {
var paramsContext = context.getByReplacement(match);
var params = parseParams(paramsContext);
var p1 = params.find(function (x) {
return x.name === "1";
});
var minParams = 2;
var maxParams = 4;
var didParamsChange = handleFirstParamLangCode(
context,
match,
params,
minParams,
maxParams,
langCtx.getCode(p1 ? p1.val : "")
);
if (didParamsChange) {
var paramsStr = params
.map(function (p) {
return p.full;
})
.join("|");
// If we are able to change the params, the `paramsContext`
// necessarily already exists. Update within that context.
paramsContext.wikitext = paramsStr;
}
});
if (didLineChange) {
lines = bullet + lang + ":" + rest;
}
}
if (didChange) {
return curHeading + lines.join("\n") + nextHeading;
}
return match;
}
);
}
/**
* @param {Context} context
* @param {string} Only set when called recursively.
* - `undefined`: no valid language heading found yet
* - _other_: a valid lang code
* @param {number}
*/
function processLangCodes(context, curLangCode, depthIn) {
var depth = (depthIn || 0) + 1;
if (depth > maxDepth) {
context.error("För många nivåer av mallar i mallar.");
return;
}
var langCodesByName = context.data.langCodesByUcfirstName;
var langCodeTemplates = context.data.langCodeTemplates;
// Process also inner templates when we know the lang code.
if (curLangCode) {
var replacements = context.wikitext.match(/\0\d+\0/g) || ;
replacements.forEach(function (replacement) {
var innerContext = context.getByReplacement(replacement);
if (innerContext.creator === "inner template") {
processLangCodes(innerContext, curLangCode, depth);
}
});
}
context.wikitext =
// Pseudocode: / ==$1== | {{$2|$3}} /
context.wikitext.replace(
/^==(+)==$|{{(+)\|?(*)}}/gm,
/**
* @param {string} match
* @param {string} heading
* @param {string} template
* @param {string} paramsOpaque
*/
function (match, heading, template, paramsOpaque) {
if (heading) {
curLangCode = langCodesByName.get(
/** @type {LangNameUcfirst} */ (heading)
);
return match;
}
// TODO Exception: {{uttal|en}} under ==Tvärspråkligt==
// https://sv.wiktionary.orghttps://sv.wiktionary.org/w/index.php?title=echo&oldid=3718468
if (template === "uttal" && curLangCode === "--") {
return match;
}
var paramsContext = context.getByReplacement(paramsOpaque);
// Process lang codes inside the opaque value.
if (paramsContext) {
processLangCodes(paramsContext, curLangCode, depth);
} else {
// If we thought that we had an opaque value, but didn't, abort
// processing. E.g., maybe we thought that we got
// {{template|OPAQUE}}, but got {{template|something}}.
if (paramsOpaque) {
return match;
}
}
var paramInfo = langCodeTemplates.get(
/** @type {Template} */ (template)
);
if (!paramInfo) {
// The template doesn't use lang code.
return match;
}
// We have a template that requires a lang code, but there was no
// previous heading. Warn about this.
if (curLangCode === undefined) {
// Only warn if there haven't been previous warnings about headings.
if (!context.warnOps.has("check headings")) {
// Missing lang heading.
context.warn("Språkrubrik saknas.");
}
return match;
}
var params = parseParams(paramsContext);
var didChange = false;
if (paramInfo === "språk") {
// The {{tagg}} and {{homofoner}} templates.
var param = params.find(function (p) {
return p.name === "språk";
});
var shouldUseParam =
curLangCode !== "sv" &&
params.some(function (p) {
return p.name === "1" || p.name === "kat";
});
if (!param && shouldUseParam) {
params.push({
implicit: false,
name: "",
val: "",
full: "språk=" + curLangCode,
});
didChange = true;
} else if (param && !shouldUseParam) {
params = params.filter(function (p) {
return p.name !== "språk";
});
didChange = true;
} else if (param && param.val !== curLangCode) {
param.full = "språk=" + curLangCode;
didChange = true;
}
} else {
var minParams = paramInfo;
var maxParams = paramInfo;
didChange = handleFirstParamLangCode(
context,
match,
params,
minParams,
maxParams,
curLangCode
);
}
if (didChange) {
var paramsStr = params
.map(function (p) {
return p.full;
})
.join("|");
// If the params are opaque, update within that context, otherwise
// create a new context for the params.
if (paramsContext) {
paramsContext.wikitext = paramsStr;
return match;
} else {
paramsContext = context.createChild("lang codes", paramsStr);
return "{{" + template + "|" + paramsContext.replacement + "}}";
}
} else {
return match;
}
}
);
}
/**
* @typedef {{
* implicit: boolean;
* name: string;
* val: string;
* full: string;
* }} ParsedParam
*
* @param {Context | undefined} paramsContext
* @returns {ParsedParam}
*/
function parseParams(paramsContext) {
// Split by `|`. Inner templates and links should already be opaque, so
// all instances of `|` should be param separators.
var implicitParamNo = 1;
return !paramsContext
?
: paramsContext.wikitext.split("|").map(function (param) {
var eq = param.indexOf("=");
if (eq === -1) {
return {
implicit: true,
name: "" + implicitParamNo++,
val: param,
full: param,
};
}
return {
implicit: false,
name: param.substring(0, eq).trim(),
val: param.substring(eq + 1).trim(),
full: param,
};
});
}
/**
* @param {Context} context
* @param {string} fullTpl Full matched template (including opaque parts).
* @param {ParsedParam} params These params will be mutated if needed.
* @param {number} minParams
* @param {number} maxParams
* @param {string} langCode Expected lang code.
* @returns {boolean} Whether any changes were made.
*/
function handleFirstParamLangCode(
context,
fullTpl,
params,
minParams,
maxParams,
langCode
) {
var p1 = params.find(function (p) {
return p.name === "1";
});
var numFound = params.reduce(function (max, p) {
return +p.name ? Math.max(max, +p.name) : max;
}, 0);
var firstExplicitNumeric = params.find(function (p) {
return !p.implicit && !Number.isNaN(+p.name);
});
if (firstExplicitNumeric) {
// Missing or bad lang code.
context.warn(
"Mall med språkkod får inte ha explicit numerisk parameter %s: %s",
firstExplicitNumeric.name + "=",
fullTpl
);
} else {
if (minParams <= numFound && numFound <= maxParams) {
// Expected number of params.
if (!p1) {
// `p1` is guaranteed to exist.
throw new Error("p1 invariant");
}
if (p1.val !== langCode) {
if (
// Can't add another param, so just fix it.
numFound === maxParams ||
// The first param is empty, so just fix it.
p1.val === ""
) {
p1.full = langCode;
return true;
} else {
// Either the first param is missing or it is incorrect. Impossible to know which.
context.warn(
'Mall saknar språkkod "%s": %s',
/** @type {string} */ (langCode),
fullTpl
);
}
}
} else if (
// Missing just one param...
minParams === numFound + 1 &&
// ...and the first param isn't the lang code
(!p1 || p1.val !== langCode)
) {
// Missing the lang param (presumably).
params.unshift({
implicit: true,
name: "",
val: "",
full: langCode,
});
return true;
} else {
// Too few or too many params.
context.warn(
"Mall har för %s parametrar: %s",
numFound < minParams ? "få" : "många",
fullTpl
);
}
}
// No changes made.
return false;
}
/**
* @typedef {2 | 3 | 4} HeadingLevel
*
* @typedef {{
* ok: Record<HeadingLevel, string>;
* exceptions: Record<HeadingLevel, string>;
* eq: Record<HeadingLevel, string>;
* fuzziness: Record<HeadingLevel, number>;
* }} AllHeadings
* @type {AllHeadings | undefined}
*/
var allHeadings;
/** @param {Context} context */
function checkHeadings(context) {
var regex = /^(=+)(+?)(=+)$/gm;
if (!allHeadings) {
allHeadings = {
// Valid headings.
ok: {
2: /** @type {string} */ ().concat(
Array.from(context.data.langCodesByUcfirstName.keys()),
context.data.headings.h2
),
3: Array.from(context.data.h3TemplatesByName.keys()),
4: context.data.headings.h4,
},
// Grandfathered exceptions by heading level.
exceptions: {
2: context.data.headings.h2Exceptions,
3: context.data.headings.h3Exceptions,
4: context.data.headings.h4Exceptions,
},
// Equal signs.
eq: { 2: "==", 3: "===", 4: "====" },
// Sensitivity when fuzzy matching: 0=exact match, 1=match anything.
fuzziness: {
2: 0.6,
3: 0.3,
4: 0.6,
},
};
}
var ok = allHeadings.ok;
var exceptions = allHeadings.exceptions;
var eq = allHeadings.eq;
var fuzziness = allHeadings.fuzziness;
for (;;) {
var match = regex.exec(context.wikitext);
if (!match) break;
var heading = match;
var level = match === match ? match.length : 0;
var name = match;
if (level === 2 || level === 3 || level === 4) {
// Valid heading or grandfathered exception.
if (ok.includes(name) || exceptions.includes(name)) {
continue;
}
}
if (level === 2) {
var replacement = context.getLangReplacement(name);
if (replacement) {
// Invalid heading.
context.warn(
'Ogiltigt språk "%s". Använd %s.',
replacement.langName,
replacement.replacement
);
continue;
}
}
var didYouMean = undefined;
// Find the correct heading level, if any.
var correctLevel = ok.includes(name)
? 2
: ok.includes(name)
? 3
: ok.includes(name)
? 4
: 0;
if (correctLevel) {
// Suggest correct level.
didYouMean = eq + name + eq;
} else if (level === 2 || level === 3 || level === 4) {
// Suggest fuzzy match of the same heading level, if it's good enough.
var bestMatch = levenshteinBest(ok, name);
if (bestMatch.distance < fuzziness) {
didYouMean = eq + bestMatch.match + eq;
}
}
// Invalid heading.
if (didYouMean) {
context.warn('Ogiltig rubrik "%s". Menade du "%s"?', heading, didYouMean);
} else {
context.warn('Ogiltig rubrik "%s".', heading);
}
}
}
/** @param {Context} context */
function correctTemplateNames(context) {
var replacements = {
c: "u",
pl: "p",
radera: "raderas",
delete: "raderas",
Delete: "raderas",
verifiera: "verifieras",
};
context.wikitext = context.wikitext.replace(
/{{(c|pl|radera|elete|verifiera)}}/g,
function (_match, template) {
return "{{" + replacements + "}}";
}
);
}
/** @param {Context} context */
function removeTemplateParamSpace(context) {
context.allContexts.forEach(function (ctx) {
if (ctx.creator === "template param") {
ctx.wikitext = ctx.wikitext
.replace(/() $/, "$1")
// Remove the spaces in:
// - Initial ` param = `
// - Subsequent ` | param = `
.replace(/(?:^| ?()) ?(?:(+?) ?(=) ?)?/g, "$1$2$3");
}
});
}
/** @param {Context} context */
function checkTemplateParam(context) {
context.allContexts.forEach(function (ctx) {
if (ctx.creator === "template param") {
var implicitParamNo = 1;
/** @type {string | undefined} */
var duplicate;
/** @type {string} */
var params = ;
ctx.wikitext.split("|").some(function (x) {
var eq = x.indexOf("=");
var name = eq === -1 ? "" + implicitParamNo++ : x.slice(0, eq);
if (params.includes(name)) {
duplicate = name;
return true;
}
params.push(name);
});
if (duplicate) {
context.warn(
"Ogiltig mallsyntax - upprepad parameter %s=: {{%s|%s}}",
duplicate,
ctx.creatorTemplate || "",
ctx.wikitext
);
}
}
});
}
/**
* @param {RegExp} re
* @param {string} str
*/
function reTest(re, str) {
re.lastIndex = 0;
return re.test(str);
}
/**
* Finds the best match in the array, using the Levenshtein algorithm. The distance returned is a value normalized by dividing by the length of the searched string.
* @param {string} arr
* @param {string} search
*/
function levenshteinBest(arr, search) {
var best = arr.reduce(
function (best, x) {
var dist = levenshtein(search, x);
return dist < best.dist ? { match: x, dist: dist } : best;
},
{ dist: Infinity, match: "" }
);
return {
distance: best.dist / search.length,
match: best.match,
search: search,
};
}
/**
* Calculate the Levenshtein distance between two strings.
* Refactored from https://stackoverflow.com/a/18514751.
* CC BY-SA 3.0 by Marco de Wit
* @param {string} s1
* @param {string} s2
*/
function levenshtein(s1, s2) {
if (s1 === s2) {
return 0;
} else {
var s1_len = s1.length,
s2_len = s2.length;
if (s1_len && s2_len) {
var i1 = 0,
i2 = 0,
a,
b,
c,
c2,
row = ;
while (i1 < s1_len) row = ++i1;
while (i2 < s2_len) {
c2 = s2.charCodeAt(i2);
a = i2;
++i2;
b = i2;
for (i1 = 0; i1 < s1_len; ++i1) {
c = a + (s1.charCodeAt(i1) === c2 ? 0 : 1);
a = row;
b = b < a ? (b < c ? b + 1 : c) : a < c ? a + 1 : c;
row = b;
}
}
return /** @type {number} */ (b);
} else {
return s1_len + s2_len;
}
}
}