Multi-pass macro expansion

This commit is contained in:
naskya 2023-03-27 02:43:21 +09:00
parent fba94d0ad5
commit 68f92d8c27
No known key found for this signature in database
GPG Key ID: 164DFF24E2D40139
2 changed files with 40 additions and 24 deletions

View File

@ -125,9 +125,10 @@ export function parseKaTeXMacros(src: string): string {
return JSON.stringify(result);
}
export function expandKaTeXMacro(src: string, macrosAsJsonString: string): string {
const macros = JSON.parse(macrosAsJsonString);
// returns [expanded text, whether something is expanded, how many times we can expand more]
// the boolean value is used for multi-pass expansions (macros can expand to other macros)
function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro }, maxNumberOfExpansions: number)
: [string, boolean, number] {
const bracketKinds = 3;
const openBracketId: { [bracket: string]: number } = {"(": 0, "{": 1, "[": 2};
const closeBracketId: { [bracket: string]: number } = {")": 0, "}": 1, "]": 2};
@ -186,20 +187,17 @@ export function expandKaTeXMacro(src: string, macrosAsJsonString: string): strin
return result;
}
let numberOfExpansions = 0;
const maxNumberOfExpansions = 200; // to prevent infinite expansion loop
// only expand src.slice(beginPos, endPos)
function expandKaTeXMacroImpl(beginPos: number, endPos: number): string {
function expandKaTeXMacroImpl(beginPos: number, endPos: number): [string, boolean] {
if (endPos <= beginPos)
return "";
return ["", false];
const raw: string = src.slice(beginPos, endPos);
const fallback: string = raw; // returned for invalid inputs
const fallback: string = raw; // returned for invalid inputs or too many expansions
if (maxNumberOfExpansions <= numberOfExpansions)
return fallback;
++numberOfExpansions;
if (maxNumberOfExpansions <= 0)
return [fallback, false];
--maxNumberOfExpansions;
// search for a custom macro
let checkedPos = beginPos - 1;
@ -219,11 +217,14 @@ export function expandKaTeXMacro(src: string, macrosAsJsonString: string): strin
// there is no macro to expand
if (checkedPos === -1)
return raw;
return [raw, false];
// is it a custom macro?
let nonAlphaPos = src.slice(checkedPos + 1).search(/[^A-Za-z]/) + checkedPos + 1;
if (nonAlphaPos === checkedPos)
nonAlphaPos = endPos;
let macroNameCandidate = src.slice(checkedPos + 1, nonAlphaPos);
if (macros.hasOwnProperty(macroNameCandidate)) {
// this is a custom macro without args
@ -241,7 +242,7 @@ export function expandKaTeXMacro(src: string, macrosAsJsonString: string): strin
}
if (nextOpenBracketPos === endPos)
return fallback; // there is no open bracket
return [fallback, false]; // there is no open bracket
macroNameCandidate += src[nextOpenBracketPos];
@ -263,18 +264,30 @@ export function expandKaTeXMacro(src: string, macrosAsJsonString: string): strin
// find the first open bracket after what we've searched
const nextOpenBracketPos = src.indexOf(openBracket, macroArgEndPos);
if (nextOpenBracketPos === -1)
return fallback; // not enough arguments are provided
return [fallback, false]; // not enough arguments are provided
if (!bracketMapping[nextOpenBracketPos])
return fallback; // found open bracket doesn't correspond to any close bracket
return [fallback, false]; // found open bracket doesn't correspond to any close bracket
macroArgEndPos = bracketMapping[nextOpenBracketPos];
expandedArgs[i] = expandKaTeXMacroImpl(nextOpenBracketPos + 1, macroArgEndPos);
expandedArgs[i] = expandKaTeXMacroImpl(nextOpenBracketPos + 1, macroArgEndPos)[0];
}
return src.slice(beginPos, macroBackslashPos)
return [src.slice(beginPos, macroBackslashPos)
+ expandSingleKaTeXMacro(expandedArgs, macroName)
+ expandKaTeXMacroImpl(macroArgEndPos + 1, endPos);
+ expandKaTeXMacroImpl(macroArgEndPos + 1, endPos)[0], true];
}
return expandKaTeXMacroImpl(0, src.length);
const [expandedText, expandedFlag]: [string, boolean] = expandKaTeXMacroImpl(0, src.length);
return [expandedText, expandedFlag, maxNumberOfExpansions];
}
export function expandKaTeXMacro(src: string, macrosAsJSONString: string, maxNumberOfExpansions: number): string {
const macros = JSON.parse(macrosAsJSONString);
let expandMore = true;
while (expandMore && (0 < maxNumberOfExpansions))
[src, expandMore, maxNumberOfExpansions] = expandKaTeXMacroOnce(src, macros, maxNumberOfExpansions);
return src;
}

View File

@ -5,11 +5,14 @@ import { expandKaTeXMacro } from "@/scripts/katex-macro";
export function preprocess(text: string): string {
if (defaultStore.state.enableCustomKaTeXMacro) {
const parsedKaTeXMacro = localStorage.getItem("customKaTeXMacroParsed") ?? "{}";
const maxNumberOfExpansions = 200; // to prevent infinite expansion loops
let nodes = mfm.parse(text);
for (let node of nodes) {
if (node["type"] === "mathInline" || node["type"] === "mathBlock") {
node["props"]["formula"] = expandKaTeXMacro(node["props"]["formula"], parsedKaTeXMacro);
node["props"]["formula"]
= expandKaTeXMacro(node["props"]["formula"], parsedKaTeXMacro, maxNumberOfExpansions);
}
}