diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 6b263a4f..7a41557a 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -2,9 +2,9 @@ name: Node.js CI on: push: - branches: ["main"] + branches: ["main", "pretext"] pull_request: - branches: ["main"] + branches: ["main", "pretext"] jobs: build: diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/expand-user-defined-macros.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/expand-user-defined-macros.ts new file mode 100644 index 00000000..2c241d01 --- /dev/null +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/expand-user-defined-macros.ts @@ -0,0 +1,49 @@ +import * as Ast from "@unified-latex/unified-latex-types"; +import { + expandMacrosExcludingDefinitions, + listNewcommands, +} from "@unified-latex/unified-latex-util-macros"; +import { attachMacroArgs } from "@unified-latex/unified-latex-util-arguments"; +import { anyMacro } from "@unified-latex/unified-latex-util-match"; +import { EXIT, visit } from "@unified-latex/unified-latex-util-visit"; + +type NewCommandSpec = ReturnType[number]; + +/** + * Expands user-defined macros + */ +export function expandUserDefinedMacros(ast: Ast.Ast): void { + const newcommands = listNewcommands(ast); + + // get a set of all macros to be expanded + const macrosToExpand = new Set(newcommands.map((command) => command.name)); + + const macroInfo = Object.fromEntries( + newcommands.map((m) => [m.name, { signature: m.signature }]) + ); + + // recursively expand at most 100 times + for (let i = 0; i < 100; i++) { + // check if any macros still need expanding + if (!needToExpand(ast, macrosToExpand)) { + break; + } + + // attach the arguments to each macro before processing it + attachMacroArgs(ast, macroInfo); + expandMacrosExcludingDefinitions(ast, newcommands); + } +} + +function needToExpand(ast: Ast.Ast, macros: Set): boolean { + let needExpand = false; + + visit(ast, (node) => { + if (anyMacro(node) && macros.has(node.content)) { + needExpand = true; + EXIT; + } + }); + + return needExpand; +} diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts new file mode 100644 index 00000000..38651036 --- /dev/null +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts @@ -0,0 +1,27 @@ +import * as Ast from "@unified-latex/unified-latex-types"; +import { anyMacro, match } from "@unified-latex/unified-latex-util-match"; +import { visit } from "@unified-latex/unified-latex-util-visit"; +import { KATEX_SUPPORT } from "./katex-subs"; + +/** + * Return a list of macros used in ast that are unsupported by KaTeX + */ +export function reportMacrosUnsupportedByKatex(ast: Ast.Ast): string[] { + const unsupported: string[] = []; + + // match a macro supported by Katex + const isSupported = match.createMacroMatcher(KATEX_SUPPORT.macros); + + // visit all nodes + visit(ast, (node, info) => { + // macro in math mode + if (anyMacro(node) && info.context.hasMathModeAncestor) { + // check if not supported by katex + if (!isSupported(node)) { + unsupported.push((node as Ast.Macro).content); + } + } + }); + + return unsupported; +} diff --git a/packages/unified-latex-to-pretext/tests/expand-user-defined-macros.test.ts b/packages/unified-latex-to-pretext/tests/expand-user-defined-macros.test.ts new file mode 100644 index 00000000..6a31b1b7 --- /dev/null +++ b/packages/unified-latex-to-pretext/tests/expand-user-defined-macros.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "vitest"; +import util from "util"; +import { getParser } from "@unified-latex/unified-latex-util-parse"; +import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; +import { expandUserDefinedMacros } from "@unified-latex/unified-latex-to-pretext/libs/pre-conversion-subs/expand-user-defined-macros"; + +// Make console.log pretty-print by default +const origLog = console.log; +console.log = (...args) => { + origLog(...args.map((x) => util.inspect(x, false, 10, true))); +}; + +describe("unified-latex-to-pretext:expand-user-deifned-macros", () => { + let value: string; + + it("can expand newcommand", () => { + value = String.raw`\newcommand{\foo}{\bar} \foo`; + + const parser = getParser(); + const ast = parser.parse(value); + + expandUserDefinedMacros(ast); + + expect(printRaw(ast)).toEqual(String.raw`\newcommand{\foo}{\bar} \bar`); + }); + + it("can expand renewcommand", () => { + value = String.raw`\renewcommand{\O}{\mathcal{O}} \O`; + + const parser = getParser(); + const ast = parser.parse(value); + + expandUserDefinedMacros(ast); + + expect(printRaw(ast)).toEqual( + String.raw`\renewcommand{\O}{\mathcal{O}} \mathcal{O}` + ); + }); + + it("can recursively expand multiple user-defined commands", () => { + value = + String.raw`\newcommand{\join}{\vee}` + + String.raw`\join` + + String.raw`\renewcommand{\vee}{\foo}` + + String.raw`\vee` + + String.raw`\renewcommand{\foo}{\bar}` + + String.raw`\foo`; + + const parser = getParser(); + const ast = parser.parse(value); + + expandUserDefinedMacros(ast); + + expect(printRaw(ast)).toEqual( + String.raw`\newcommand{\join}{\vee}` + + String.raw`\bar` + + String.raw`\renewcommand{\vee}{\foo}` + + String.raw`\bar` + + String.raw`\renewcommand{\foo}{\bar}` + + String.raw`\bar` + ); + }); + + it("can expand providecommand", () => { + value = String.raw`\providecommand{\bar}{\b} \bar`; + + const parser = getParser(); + const ast = parser.parse(value); + + expandUserDefinedMacros(ast); + + expect(printRaw(ast)).toEqual(String.raw`\providecommand{\bar}{\b} \b`); + }); +}); diff --git a/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts b/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts new file mode 100644 index 00000000..be40a3d8 --- /dev/null +++ b/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import util from "util"; +import { getParser } from "@unified-latex/unified-latex-util-parse"; +import { reportMacrosUnsupportedByKatex } from "@unified-latex/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex"; + +// Make console.log pretty-print by default +const origLog = console.log; +console.log = (...args) => { + origLog(...args.map((x) => util.inspect(x, false, 10, true))); +}; + +describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { + let value: string; + + it("can report unsupported macros in inline mathmode", () => { + value = String.raw`$\mathbb{R} \fakemacro{X}$`; + + const parser = getParser(); + const ast = parser.parse(value); + + expect(reportMacrosUnsupportedByKatex(ast)).toEqual(["fakemacro"]); + }); + + it("can report no unsupported macros in mathmode", () => { + value = String.raw`$\mathbb{R} \frac{1}{2} \cup$`; + + const parser = getParser(); + const ast = parser.parse(value); + + expect(reportMacrosUnsupportedByKatex(ast)).toEqual([]); + }); + + it("doesn't report unsupported macros outside of math mode", () => { + value = String.raw`\fakemacro`; + + const parser = getParser(); + const ast = parser.parse(value); + + expect(reportMacrosUnsupportedByKatex(ast)).toEqual([]); + }); + + it("reports unsupported macros in text mode with a math anscestor", () => { + value = String.raw`$\frac{1}{\text{ hi \unsupported}}$`; + + const parser = getParser(); + const ast = parser.parse(value); + + expect(reportMacrosUnsupportedByKatex(ast)).toEqual(["unsupported"]); + }); + + it("can report unsupported macros in display mathmode", () => { + value = String.raw`\[ \frac{a}{b} \fake \text{bar \baz}\] \bang`; + + const parser = getParser(); + const ast = parser.parse(value); + + expect(reportMacrosUnsupportedByKatex(ast)).toEqual(["fake", "baz"]); + }); + + it("can report unsupported macros in equation environment", () => { + value = String.raw`\unsupported \begin{equation} \mathbb{N} \unsupported \text{\baz}\end{equation}`; + + const parser = getParser(); + const ast = parser.parse(value); + + expect(reportMacrosUnsupportedByKatex(ast)).toEqual([ + "unsupported", + "baz", + ]); + }); +});