From 68f92d8c27ccde4e4caa0bbdc46488dcee8113e0 Mon Sep 17 00:00:00 2001 From: naskya Date: Mon, 27 Mar 2023 02:43:21 +0900 Subject: [PATCH] Multi-pass macro expansion --- packages/client/src/scripts/katex-macro.ts | 59 +++++++++++++--------- packages/client/src/scripts/preprocess.ts | 5 +- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/client/src/scripts/katex-macro.ts b/packages/client/src/scripts/katex-macro.ts index d9a5c42e9d..c1c8206226 100644 --- a/packages/client/src/scripts/katex-macro.ts +++ b/packages/client/src/scripts/katex-macro.ts @@ -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; @@ -207,11 +205,11 @@ export function expandKaTeXMacro(src: string, macrosAsJsonString: string): strin let macroBackslashPos = 0; // for macros w/o args: unused - // w/ args: the first open bracket ("(", "{", or "[") after cmd name + // w/ args: the first open bracket ("(", "{", or "[") after cmd name let macroArgBeginPos = 0; // for macros w/o args: the end of cmd name - // w/ args: the closing bracket of the last arg + // w/ args: the closing bracket of the last arg let macroArgEndPos = 0; while (checkedPos < endPos) { @@ -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) - + expandSingleKaTeXMacro(expandedArgs, macroName) - + expandKaTeXMacroImpl(macroArgEndPos + 1, endPos); + return [src.slice(beginPos, macroBackslashPos) + + expandSingleKaTeXMacro(expandedArgs, macroName) + + 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; } diff --git a/packages/client/src/scripts/preprocess.ts b/packages/client/src/scripts/preprocess.ts index d017c45bd4..311d865dee 100644 --- a/packages/client/src/scripts/preprocess.ts +++ b/packages/client/src/scripts/preprocess.ts @@ -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); } }