From d33279291050db6018971e4555f768fe45395074 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Tue, 19 Nov 2024 17:26:48 -0800 Subject: [PATCH 1/2] [Locked Labels + Aria] Create math only parser to help parse TeX how we want --- .changeset/mighty-cobras-kick.md | 5 + packages/perseus/src/index.ts | 1 + .../widgets/interactive-graphs/utils.test.ts | 137 +++++++++++++++++- .../src/widgets/interactive-graphs/utils.ts | 34 ++++- 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 .changeset/mighty-cobras-kick.md diff --git a/.changeset/mighty-cobras-kick.md b/.changeset/mighty-cobras-kick.md new file mode 100644 index 0000000000..a8ce7b1de7 --- /dev/null +++ b/.changeset/mighty-cobras-kick.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +[Locked Labels + Aria] Create math only parser to help parse TeX how we want diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index deb9fac110..fcb40a1451 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -105,6 +105,7 @@ export { containerSizeClass, getInteractiveBoxFromSizeClass, } from "./util/sizing-utils"; +export {mathOnlyParser} from "./widgets/interactive-graphs/utils"; export { getAnswersFromWidgets, injectWidgets, diff --git a/packages/perseus/src/widgets/interactive-graphs/utils.test.ts b/packages/perseus/src/widgets/interactive-graphs/utils.test.ts index 2c380df7fb..6a4850338d 100644 --- a/packages/perseus/src/widgets/interactive-graphs/utils.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/utils.test.ts @@ -1,4 +1,9 @@ -import {normalizePoints, normalizeCoords, replaceOutsideTeX} from "./utils"; +import { + normalizePoints, + normalizeCoords, + replaceOutsideTeX, + mathOnlyParser, +} from "./utils"; import type {Coord} from "../../interactive2/types"; import type {GraphRange} from "../../perseus-types"; @@ -148,3 +153,133 @@ describe("replaceOutsideTeX", () => { expect(convertedString).toEqual("\\text{\\\\}"); }); }); + +describe("mathOnlyParser", () => { + test("empty string", () => { + const nodes = mathOnlyParser(""); + + expect(nodes).toEqual([]); + }); + + test("text-only string", () => { + const nodes = mathOnlyParser("abc"); + + expect(nodes).toEqual([{content: "abc", type: "text"}]); + }); + + test("math", () => { + const nodes = mathOnlyParser("$x^2$"); + + expect(nodes).toEqual([{content: "x^2", type: "math"}]); + }); + + test("math at the start", () => { + const nodes = mathOnlyParser("$x^2$ yippee"); + + expect(nodes).toEqual([ + {content: "x^2", type: "math"}, + {content: " yippee", type: "text"}, + ]); + }); + + test("math at the end", () => { + const nodes = mathOnlyParser("yippee $x^2$"); + + expect(nodes).toEqual([ + {content: "yippee ", type: "text"}, + {content: "x^2", type: "math"}, + ]); + }); + + test("math contained within text", () => { + const nodes = mathOnlyParser("The equation is $x^2$ yippee"); + + expect(nodes).toEqual([ + {content: "The equation is ", type: "text"}, + {content: "x^2", type: "math"}, + {content: " yippee", type: "text"}, + ]); + }); + + test("multiple math blocks", () => { + const nodes = mathOnlyParser("$x^2$ and $y^2$"); + + expect(nodes).toEqual([ + {content: "x^2", type: "math"}, + {content: " and ", type: "text"}, + {content: "y^2", type: "math"}, + ]); + }); + + test.each` + character + ${">"} + ${"> "} + ${" "} + ${"["} + ${"]"} + ${"("} + ${")"} + ${"^"} + ${"*"} + ${"/"} + `("nonspecial special character as text: '$character'", ({character}) => { + const nodes = mathOnlyParser(character); + + expect(nodes).toEqual([{content: character, type: "text"}]); + }); + + test.each` + character + ${"\\"} + ${"\\\\"} + ${"{"} + ${"}"} + ${"$"} + ${"\\$"} + `("actually special character: '$character'", ({character}) => { + const nodes = mathOnlyParser(character); + + expect(nodes).toEqual([{content: character, type: "specialCharacter"}]); + }); + + test("special character in text", () => { + const nodes = mathOnlyParser("a\\$b"); + + expect(nodes).toEqual([ + {content: "a", type: "text"}, + {content: "\\$", type: "specialCharacter"}, + {content: "b", type: "text"}, + ]); + }); + + test("special character in math", () => { + const nodes = mathOnlyParser("$\\$$"); + + expect(nodes).toEqual([{content: "\\$", type: "math"}]); + }); + + test("mix of special characters", () => { + const nodes = mathOnlyParser("\\$\\\\\\$$"); + + expect(nodes).toEqual([ + {content: "\\$", type: "specialCharacter"}, + {content: "\\\\", type: "specialCharacter"}, + {content: "\\$", type: "specialCharacter"}, + {content: "$", type: "specialCharacter"}, + ]); + }); + + test("mix all types", () => { + const nodes = mathOnlyParser("Hello \\$ \\\\ world $\\frac{1}{2}$"); + + expect(nodes).toEqual([ + {content: "Hello ", type: "text"}, + {content: "\\$", type: "specialCharacter"}, + {content: " ", type: "text"}, + {content: "\\\\", type: "specialCharacter"}, + {content: " world ", type: "text"}, + {content: "\\frac{1}{2}", type: "math"}, + ]); + }); +}); diff --git a/packages/perseus/src/widgets/interactive-graphs/utils.ts b/packages/perseus/src/widgets/interactive-graphs/utils.ts index bc25cc0f60..c39ce0fd5e 100644 --- a/packages/perseus/src/widgets/interactive-graphs/utils.ts +++ b/packages/perseus/src/widgets/interactive-graphs/utils.ts @@ -1,4 +1,5 @@ -import * as SimpleMarkdown from "@khanacademy/pure-markdown"; +import {parse, pureMarkdownRules} from "@khanacademy/pure-markdown"; +import SimpleMarkdown from "@khanacademy/simple-markdown"; import {clampToBox, inset, MIN, size} from "./math"; @@ -81,7 +82,7 @@ export function isUnlimitedGraphState( export function replaceOutsideTeX(mathString: string) { // All the information we need is in the first section, // whether it's typed as "blockmath" or "paragraph" - const firstSection = SimpleMarkdown.parse(mathString)[0]; + const firstSection = parse(mathString)[0]; // If it's blockMath, the outer level has the full math content. if (firstSection.type === "blockMath") { @@ -141,3 +142,32 @@ function escapeSpecialChars(str) { // Escape $, \, {, and } characters return str.replace(/([$\\{}])/g, "\\$1"); } + +/** + * Parse a string of text and math into a list of objects with type and content + * + * Example: "Pi is about $\frac{22}{7}$" ==> + * [ + * {type: "text", content: "Pi is about "}, + * {type: "math", content: "\\frac{22}{7}"}, + * ] + */ +export const mathOnlyParser = SimpleMarkdown.parserFor( + { + math: { + ...pureMarkdownRules.math, + order: 0, + }, + text: { + order: 1, + match: SimpleMarkdown.anyScopeRegex(/^([^$\\{}]+)/), + parse: (capture) => ({content: capture[0]}), + }, + specialCharacter: { + order: 2, + match: SimpleMarkdown.anyScopeRegex(/^(\\[\S\s]|\$|\\$|{|})/), + parse: (capture) => ({content: capture[0]}), + }, + }, + {inline: true}, +); From 860d91bd489229e80c4c5cca0ae5db53e8a5c4cb Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Wed, 20 Nov 2024 10:31:09 -0800 Subject: [PATCH 2/2] more test cases --- .../widgets/interactive-graphs/utils.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/perseus/src/widgets/interactive-graphs/utils.test.ts b/packages/perseus/src/widgets/interactive-graphs/utils.test.ts index 6a4850338d..cf529ef9ff 100644 --- a/packages/perseus/src/widgets/interactive-graphs/utils.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/utils.test.ts @@ -201,6 +201,31 @@ describe("mathOnlyParser", () => { ]); }); + test("math text without math markers", () => { + const nodes = mathOnlyParser("yippee x^2"); + + expect(nodes).toEqual([{content: "yippee x^2", type: "text"}]); + }); + + test("lone unescaped dollar sign middle", () => { + const nodes = mathOnlyParser("yippee $x^2"); + + expect(nodes).toEqual([ + {content: "yippee ", type: "text"}, + {content: "$", type: "specialCharacter"}, + {content: "x^2", type: "text"}, + ]); + }); + + test("lone unescaped dollar sign end", () => { + const nodes = mathOnlyParser("yippee x^2$"); + + expect(nodes).toEqual([ + {content: "yippee x^2", type: "text"}, + {content: "$", type: "specialCharacter"}, + ]); + }); + test("multiple math blocks", () => { const nodes = mathOnlyParser("$x^2$ and $y^2$"); @@ -211,6 +236,23 @@ describe("mathOnlyParser", () => { ]); }); + test("TeX syntax without dollars", () => { + const nodes = mathOnlyParser("\\frac{1}{2}"); + + // This looks odd, but this is expected based on our logic + // since this is within a text block, not a math block + expect(nodes).toEqual([ + {content: "\\f", type: "specialCharacter"}, + {content: "rac", type: "text"}, + {content: "{", type: "specialCharacter"}, + {content: "1", type: "text"}, + {content: "}", type: "specialCharacter"}, + {content: "{", type: "specialCharacter"}, + {content: "2", type: "text"}, + {content: "}", type: "specialCharacter"}, + ]); + }); + test.each` character ${">"}