From 88478023466f01fb39d59e3e9d1d5825db7cc114 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Sat, 16 Nov 2024 13:59:34 -0600 Subject: [PATCH] improvements to label state variables and update functions --- .../doenetml-worker/src/components/Label.js | 111 +++-- .../doenetml-worker/src/components/MMeMen.js | 16 +- .../src/test/tagSpecific/label.test.ts | 381 +++++++++++++++++- packages/doenetml-worker/src/utils/math.ts | 34 ++ 4 files changed, 501 insertions(+), 41 deletions(-) diff --git a/packages/doenetml-worker/src/components/Label.js b/packages/doenetml-worker/src/components/Label.js index 79f9276cc..50a4b3a0e 100644 --- a/packages/doenetml-worker/src/components/Label.js +++ b/packages/doenetml-worker/src/components/Label.js @@ -10,6 +10,7 @@ import { returnAnchorStateVariableDefinition, } from "../utils/graphical"; import { textFromChildren } from "../utils/text"; +import { latexToText, textToLatex } from "../utils/math"; export default class Label extends InlineComponent { constructor(args) { @@ -222,57 +223,74 @@ export default class Label extends InlineComponent { variableName: "hasLatex", }, }), - definition: function ({ dependencyValues }) { + definition: function ({ dependencyValues, componentName }) { if ( dependencyValues.inlineChildren.length === 0 && dependencyValues.valueShadow !== null ) { let value = dependencyValues.valueShadow; let text = value; + let latex = value; if (dependencyValues.hasLatex) { - text = text.replace(/\\\(/g, ""); - text = text.replace(/\\\)/g, ""); + latex = latex.replace(/\\\(/g, ""); + latex = latex.replace(/\\\)/g, ""); + + text = extractTextFromLabel(text); } - return { setValue: { text, latex: text, value } }; + + return { setValue: { text, latex, value } }; } - let textFromComponentConverter = function ( - comp, - getValue = true, - ) { + let valueFromComponentConverter = function (comp) { if (typeof comp !== "object") { return comp.toString(); } else if (comp.stateValues.hidden) { return ""; } else if ( typeof comp.stateValues.hasLatex === "boolean" && - typeof comp.stateValues.value === "string" && - typeof comp.stateValues.text === "string" + typeof comp.stateValues.value === "string" ) { // if component has a boolean hasLatex state variable - // and value and text are strings - // then use value and text directly - return getValue - ? comp.stateValues.value - : comp.stateValues.text; + // and value is string + // then use value directly + // (as it is a label or similar) + return comp.stateValues.value; } else if ( typeof comp.stateValues.renderAsMath === "boolean" && typeof comp.stateValues.latex === "string" && typeof comp.stateValues.text === "string" ) { // if have both latex and string, - // use render as math, if exists, to decide which to use + // and renderAsMath exists, then we'll use renderAsMath + // to decide which to use if (comp.stateValues.renderAsMath) { - return getValue - ? "\\(" + comp.stateValues.latex + "\\)" - : comp.stateValues.latex; + return "\\(" + comp.stateValues.latex + "\\)"; } else { return comp.stateValues.text; } } else if (typeof comp.stateValues.latex === "string") { - return getValue - ? "\\(" + comp.stateValues.latex + "\\)" - : comp.stateValues.latex; + // if no renderAsMath, then we'll use latex if it exists + return "\\(" + comp.stateValues.latex + "\\)"; + } else if (typeof comp.stateValues.text === "string") { + return comp.stateValues.text; + } + }; + + let textFromComponentConverter = function ( + comp, + preferLatex = false, + ) { + if (typeof comp !== "object") { + return comp.toString(); + } else if (comp.stateValues.hidden) { + return ""; + } else if ( + typeof comp.stateValues.text === "string" && + !preferLatex + ) { + return comp.stateValues.text; + } else if (typeof comp.stateValues.latex === "string") { + return comp.stateValues.latex; } else if (typeof comp.stateValues.text === "string") { return comp.stateValues.text; } @@ -280,14 +298,18 @@ export default class Label extends InlineComponent { let value = textFromChildren( dependencyValues.inlineChildren, - (x) => textFromComponentConverter(x, true), + valueFromComponentConverter, ); let text = textFromChildren( dependencyValues.inlineChildren, (x) => textFromComponentConverter(x, false), ); + let latex = textFromChildren( + dependencyValues.inlineChildren, + (x) => textFromComponentConverter(x, true), + ); - return { setValue: { text, latex: text, value } }; + return { setValue: { text, latex, value } }; }, inverseDefinition: function ({ desiredStateVariableValues, @@ -300,7 +322,16 @@ export default class Label extends InlineComponent { } else if ( typeof desiredStateVariableValues.text === "string" ) { - desiredValue = desiredStateVariableValues.text; + if (dependencyValues.hasLatex) { + // if hasLatex is set, then the only invertible form is where there is a single + // latex expression. + // Attempt to convert text into latex. + desiredValue = textToLatex( + desiredStateVariableValues.text, + ); + } else { + desiredValue = desiredStateVariableValues.text; + } } else if ( typeof desiredStateVariableValues.latex === "string" ) { @@ -324,7 +355,6 @@ export default class Label extends InlineComponent { }; } else if (dependencyValues.inlineChildren.length === 1) { let comp = dependencyValues.inlineChildren[0]; - let desiredValue = desiredStateVariableValues.value; if (typeof comp !== "object") { return { @@ -518,3 +548,32 @@ export default class Label extends InlineComponent { } } } + +/** + * Extract text from a label string consisting of regular text and latex snippets enclosed by `\(` and `\)`. + * + * For each latex string delimited by `\(` and `\)`, attempt to create a math expression from that latex. + */ +function extractTextFromLabel(labelValue) { + let unprocessedText = labelValue; + let text = ""; + let match = unprocessedText.match(/\\\((.*?)\\\)/); + while (match) { + let preChars = match.index; + + // add text before the latex piece found + text += unprocessedText.slice(0, preChars); + + // attempt to convert the latex piece found + text += latexToText(match[1]); + + // remove processed text and continue + unprocessedText = unprocessedText.slice(preChars + match[0].length); + match = unprocessedText.match(/\\\((.*?)\\\)/); + } + + // add any leftover text after all latex pieces + text += unprocessedText; + + return text; +} diff --git a/packages/doenetml-worker/src/components/MMeMen.js b/packages/doenetml-worker/src/components/MMeMen.js index 6e3b3a1b2..6287f8220 100644 --- a/packages/doenetml-worker/src/components/MMeMen.js +++ b/packages/doenetml-worker/src/components/MMeMen.js @@ -9,7 +9,7 @@ import { returnAnchorAttributes, returnAnchorStateVariableDefinition, } from "../utils/graphical"; -import { latexToAst, superSubscriptsToUnicode } from "../utils/math"; +import { latexToText } from "../utils/math"; import { createInputStringFromChildren } from "../utils/parseMath"; export class M extends InlineComponent { @@ -197,20 +197,8 @@ export class M extends InlineComponent { }, }), definition: function ({ dependencyValues }) { - let expression; - try { - expression = me.fromAst( - latexToAst.convert(dependencyValues.latex), - ); - } catch (e) { - // just return latex if can't parse with math-expressions - return { setValue: { text: dependencyValues.latex } }; - } - return { - setValue: { - text: superSubscriptsToUnicode(expression.toString()), - }, + setValue: { text: latexToText(dependencyValues.latex) }, }; }, }; diff --git a/packages/doenetml-worker/src/test/tagSpecific/label.test.ts b/packages/doenetml-worker/src/test/tagSpecific/label.test.ts index 932d269b8..a73017cff 100644 --- a/packages/doenetml-worker/src/test/tagSpecific/label.test.ts +++ b/packages/doenetml-worker/src/test/tagSpecific/label.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it, vi } from "vitest"; import { createTestCore, returnAllStateVariables } from "../utils/test-core"; import { cleanLatex } from "../utils/math"; -import { moveLabel, updateMathInputValue } from "../utils/actions"; +import { + moveLabel, + updateMathInputValue, + updateTextInputValue, +} from "../utils/actions"; import { test_in_graph } from "../utils/in-graph"; const Mock = vi.fn(); @@ -9,6 +13,381 @@ vi.stubGlobal("postMessage", Mock); vi.mock("hyperformula"); describe("Label tag tests", async () => { + it("label value, text, and latex", async () => { + let core = await createTestCore({ + doenetML: ` + + + + + + + + + + `, + }); + + const stateVariables = await returnAllStateVariables(core); + let l1 = "Hello"; + let l2 = "Hello"; + let l3Latex = "\\left(x^2,\\frac{y^2}{z^2}\\right)"; + let l3Value = `\\(${l3Latex}\\)`; + let l3Text = "( x², (y²)/(z²) )"; + let l4Latex = "\\left( a^{2}, \\frac{b^{2}}{c^{2}} \\right)"; + let l4Value = `\\(${l4Latex}\\)`; + let l4Text = "( a², (b²)/(c²) )"; + let l5 = "1"; + let l6Latex = "2"; + let l6Value = `\\(${l6Latex}\\)`; + let l6Text = "2"; + let l7Value = `${l2} and ${l3Value} and ${l4Value} and ${l5} and ${l6Value}`; + let l7Text = `${l2} and ${l3Text} and ${l4Text} and ${l5} and ${l6Text}`; + let l7Latex = `${l2} and ${l3Latex} and ${l4Latex} and ${l5} and ${l6Latex}`; + let l8Value = `${l1} and ${l2} and ${l3Value} and ${l4Value} and ${l5} and ${l6Value} and ${l7Value}`; + let l8Text = `${l1} and ${l2} and ${l3Text} and ${l4Text} and ${l5} and ${l6Text} and ${l7Text}`; + let l8Latex = `${l1} and ${l2} and ${l3Latex} and ${l4Latex} and ${l5} and ${l6Latex} and ${l7Latex}`; + + expect(stateVariables["/l1"].stateValues.value).eq(l1); + expect(stateVariables["/l1"].stateValues.text).eq(l1); + expect(stateVariables["/l1"].stateValues.latex).eq(l1); + expect(stateVariables["/l1"].stateValues.hasLatex).eq(false); + + expect(stateVariables["/l2"].stateValues.value).eq(l2); + expect(stateVariables["/l2"].stateValues.text).eq(l2); + expect(stateVariables["/l2"].stateValues.latex).eq(l2); + expect(stateVariables["/l2"].stateValues.hasLatex).eq(false); + + expect(stateVariables["/l3"].stateValues.value).eq(l3Value); + expect(stateVariables["/l3"].stateValues.text).eq(l3Text); + expect(stateVariables["/l3"].stateValues.latex).eq(l3Latex); + expect(stateVariables["/l3"].stateValues.hasLatex).eq(true); + + expect(stateVariables["/l4"].stateValues.value).eq(l4Value); + expect(stateVariables["/l4"].stateValues.text).eq(l4Text); + expect(stateVariables["/l4"].stateValues.latex).eq(l4Latex); + expect(stateVariables["/l4"].stateValues.hasLatex).eq(true); + + expect(stateVariables["/l5"].stateValues.value).eq(l5); + expect(stateVariables["/l5"].stateValues.text).eq(l5); + expect(stateVariables["/l5"].stateValues.latex).eq(l5); + expect(stateVariables["/l5"].stateValues.hasLatex).eq(false); + + expect(stateVariables["/l6"].stateValues.value).eq(l6Value); + expect(stateVariables["/l6"].stateValues.text).eq(l6Text); + expect(stateVariables["/l6"].stateValues.latex).eq(l6Latex); + expect(stateVariables["/l6"].stateValues.hasLatex).eq(true); + + expect(stateVariables["/l7"].stateValues.value).eq(l7Value); + expect(stateVariables["/l7"].stateValues.text).eq(l7Text); + expect(stateVariables["/l7"].stateValues.latex).eq(l7Latex); + expect(stateVariables["/l7"].stateValues.hasLatex).eq(true); + + expect(stateVariables["/l8"].stateValues.value).eq(l8Value); + expect(stateVariables["/l8"].stateValues.text).eq(l8Text); + expect(stateVariables["/l8"].stateValues.latex).eq(l8Latex); + expect(stateVariables["/l8"].stateValues.hasLatex).eq(true); + }); + + it("change text label from its value, text, or latex", async () => { + let core = await createTestCore({ + doenetML: ` + +