Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Locked Labels + Aria] Create math only parser to help parse TeX how we want #1890

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mighty-cobras-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

[Locked Labels + Aria] Create math only parser to help parse TeX how we want
1 change: 1 addition & 0 deletions packages/perseus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export {
containerSizeClass,
getInteractiveBoxFromSizeClass,
} from "./util/sizing-utils";
export {mathOnlyParser} from "./widgets/interactive-graphs/utils";
export {
getAnswersFromWidgets,
injectWidgets,
Expand Down
137 changes: 136 additions & 1 deletion packages/perseus/src/widgets/interactive-graphs/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -148,3 +153,133 @@ describe("replaceOutsideTeX", () => {
expect(convertedString).toEqual("\\text{\\\\}");
});
});

describe("mathOnlyParser", () => {
test("empty string", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any edge cases we should consider adding?
A few that I'm wondering about are yippee $x^2, yippee x^2$ and yippee x^2.
I know you cover ^ and $ in the table tests, but I'm curious about capturing them in the context of a string with other text

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"},
]);
});
});
34 changes: 32 additions & 2 deletions packages/perseus/src/widgets/interactive-graphs/utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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];

Copy link
Contributor Author

@nishasy nishasy Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not making any changes here, just updating the import. This whole function will be updated in a different PR.

// If it's blockMath, the outer level has the full math content.
if (firstSection.type === "blockMath") {
Expand Down Expand Up @@ -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},
);
Loading