From 0909ca24a12611bd161983f8ea32e15caf312d52 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Wed, 30 Oct 2024 10:13:35 -0500 Subject: [PATCH] Convert list components to composites (#249) * convert mathList, numberList, textList, booleanList * correctly shadow composites that shadow prop variables * numberLists merge math lists with single child * inverse definition of text will attempt to update a composite-generated list * list props on text, textInput, math, mathInput --- package-lock.json | 14 +- package.json | 2 +- packages/doenetml-iframe/package.json | 2 +- packages/doenetml-worker/src/Core.js | 264 +- packages/doenetml-worker/src/CoreWorker.js | 1 + .../doenetml-worker/src/components/Aliases.js | 2 - .../doenetml-worker/src/components/Answer.js | 31 +- .../doenetml-worker/src/components/Award.js | 261 +- .../doenetml-worker/src/components/Boolean.js | 188 +- .../src/components/BooleanList.js | 482 +- .../doenetml-worker/src/components/Copy.js | 12 + .../src/components/Function.js | 2 +- .../doenetml-worker/src/components/Math.js | 1047 +--- .../src/components/MathInput.js | 7 + .../src/components/MathList.js | 999 +--- .../src/components/MathOperators.js | 3 +- .../doenetml-worker/src/components/Number.js | 35 +- .../src/components/NumberList.js | 923 +-- .../src/components/RegionBetweenCurves.js | 4 +- .../src/components/Substitute.js | 2 +- .../doenetml-worker/src/components/Text.js | 95 +- .../src/components/TextInput.js | 5 + .../src/components/TextList.js | 476 +- .../src/components/TupleList.js | 1 - .../doenetml-worker/src/components/When.js | 16 - .../components/abstract/MathBaseOperator.js | 284 +- .../src/test/tagSpecific/answer.test.ts | 208 +- .../src/test/tagSpecific/boolean.test.ts | 219 +- .../src/test/tagSpecific/booleanlist.test.ts | 584 ++ .../src/test/tagSpecific/callAction.test.ts | 99 +- .../src/test/tagSpecific/copy.test.ts | 2 +- .../src/test/tagSpecific/graph.test.ts | 26 + .../src/test/tagSpecific/map.test.ts | 55 + .../src/test/tagSpecific/math.test.ts | 39 + .../src/test/tagSpecific/mathinput.test.ts | 330 ++ .../src/test/tagSpecific/mathlist.test.ts | 1633 ++++++ .../test/tagSpecific/mathoperators.test.ts | 2 +- .../src/test/tagSpecific/numberlist.test.ts | 1176 ++++ .../src/test/tagSpecific/polygon.test.ts | 30 +- .../src/test/tagSpecific/polyline.test.ts | 18 +- .../selectsamplerandomnumbers.test.ts | 6 +- .../src/test/tagSpecific/sequence.test.ts | 2 - .../src/test/tagSpecific/sort.test.ts | 4 +- .../src/test/tagSpecific/text.test.ts | 83 +- .../src/test/tagSpecific/textinput.test.ts | 93 + .../src/test/tagSpecific/textlist.test.ts | 685 +++ .../src/test/tagSpecific/triggerset.test.ts | 14 +- .../doenetml-worker/src/utils/booleanLogic.js | 623 +- packages/doenetml-worker/src/utils/label.js | 10 +- .../utils/mathVectorMatrixStateVariables.ts | 739 +++ .../doenetml-worker/src/utils/parseMath.ts | 375 ++ .../doenetml-worker/src/utils/rounding.js | 63 +- packages/doenetml-worker/src/utils/text.ts | 220 +- packages/doenetml/package.json | 2 +- .../src/Viewer/renderers/mathList.jsx | 39 - .../src/Viewer/renderers/numberList.jsx | 35 - .../src/Viewer/renderers/textList.jsx | 30 - packages/standalone/package.json | 2 +- .../src/generated/doenet-relaxng-schema.json | 2226 ++----- .../src/generated/doenet-schema.json | 1626 +++--- .../cypress/e2e/tagSpecific/booleanlist.cy.js | 686 --- .../cypress/e2e/tagSpecific/mathlist.cy.js | 5197 ----------------- .../cypress/e2e/tagSpecific/numberlist.cy.js | 1669 ------ .../cypress/e2e/tagSpecific/textlist.cy.js | 404 -- packages/utils/src/math/subset-of-reals.js | 5 + 65 files changed, 10004 insertions(+), 14413 deletions(-) create mode 100644 packages/doenetml-worker/src/test/tagSpecific/booleanlist.test.ts create mode 100644 packages/doenetml-worker/src/test/tagSpecific/mathlist.test.ts create mode 100644 packages/doenetml-worker/src/test/tagSpecific/numberlist.test.ts create mode 100644 packages/doenetml-worker/src/test/tagSpecific/textlist.test.ts create mode 100644 packages/doenetml-worker/src/utils/mathVectorMatrixStateVariables.ts create mode 100644 packages/doenetml-worker/src/utils/parseMath.ts delete mode 100644 packages/doenetml/src/Viewer/renderers/mathList.jsx delete mode 100644 packages/doenetml/src/Viewer/renderers/numberList.jsx delete mode 100644 packages/doenetml/src/Viewer/renderers/textList.jsx delete mode 100644 packages/test-cypress/cypress/e2e/tagSpecific/booleanlist.cy.js delete mode 100644 packages/test-cypress/cypress/e2e/tagSpecific/mathlist.cy.js delete mode 100644 packages/test-cypress/cypress/e2e/tagSpecific/numberlist.cy.js delete mode 100644 packages/test-cypress/cypress/e2e/tagSpecific/textlist.cy.js diff --git a/package-lock.json b/package-lock.json index 10176fe6d..865628710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "./packages/*" ], "dependencies": { - "math-expressions": "^2.0.0-alpha70", + "math-expressions": "^2.0.0-alpha71", "react-router-dom": "^6.26.2" }, "devDependencies": { @@ -13748,9 +13748,9 @@ } }, "node_modules/math-expressions": { - "version": "2.0.0-alpha70", - "resolved": "https://registry.npmjs.org/math-expressions/-/math-expressions-2.0.0-alpha70.tgz", - "integrity": "sha512-hKdOeRjcO2EkTpVw5j3gt6g89QW0j9v+ke0griDaZPYN3l2HMJowk4U5dARVJoEpX4nfN9jIpSnbUA/b9UQ5QA==", + "version": "2.0.0-alpha71", + "resolved": "https://registry.npmjs.org/math-expressions/-/math-expressions-2.0.0-alpha71.tgz", + "integrity": "sha512-NMTjBbuzchbEBId1VDP0VzB3qNiqu+sJHS34MyH5ONQtJFzxqXzKH7nqsvemdsTpnlFWMjFzHEPEa524E9oo2A==", "license": "(GPL-3.0 OR Apache-2.0)", "dependencies": { "@babel/cli": "^7.25.7", @@ -22819,7 +22819,7 @@ }, "packages/doenetml": { "name": "@doenet/doenetml", - "version": "0.7.0-alpha20", + "version": "0.7.0-alpha21", "license": "AGPL-3.0-or-later", "dependencies": { "@chakra-ui/icons": "^2.0.19", @@ -22861,7 +22861,7 @@ }, "packages/doenetml-iframe": { "name": "@doenet/doenetml-iframe", - "version": "0.7.0-alpha20", + "version": "0.7.0-alpha21", "license": "AGPL-3.0-or-later", "devDependencies": {}, "peerDependencies": { @@ -22939,7 +22939,7 @@ }, "packages/standalone": { "name": "@doenet/standalone", - "version": "0.7.0-alpha20", + "version": "0.7.0-alpha21", "license": "AGPL-3.0-or-later", "devDependencies": {} }, diff --git a/package.json b/package.json index be497ce7d..30c47a8a6 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "tabWidth": 4 }, "dependencies": { - "math-expressions": "^2.0.0-alpha70", + "math-expressions": "^2.0.0-alpha71", "react-router-dom": "^6.26.2" } } diff --git a/packages/doenetml-iframe/package.json b/packages/doenetml-iframe/package.json index 789d40702..36cdd381d 100644 --- a/packages/doenetml-iframe/package.json +++ b/packages/doenetml-iframe/package.json @@ -2,7 +2,7 @@ "name": "@doenet/doenetml-iframe", "type": "module", "description": "A renderer for DoenetML contained in an iframe", - "version": "0.7.0-alpha20", + "version": "0.7.0-alpha21", "license": "AGPL-3.0-or-later", "homepage": "https://github.com/Doenet/DoenetML#readme", "private": true, diff --git a/packages/doenetml-worker/src/Core.js b/packages/doenetml-worker/src/Core.js index e452b1d11..5783e3930 100644 --- a/packages/doenetml-worker/src/Core.js +++ b/packages/doenetml-worker/src/Core.js @@ -1872,6 +1872,7 @@ export default class Core { mediatingShadowComposite.mediatesShadows.push({ shadowing: newComponent.componentName, shadowed: name, + propVariable: dep.propVariable, }); if (dep.isPrimaryShadow) { @@ -2584,7 +2585,9 @@ export default class Core { } if ( - component.shadows + component.shadows && + !component.shadows.propVariable && + !component.constructor.doNotExpandAsShadowed //&& // this.componentInfoObjects.isCompositeComponent({ // componentType: component.componentType, @@ -2855,9 +2858,9 @@ export default class Core { // mediates the shadow of compositeMediatingTheShadow let foundCircular = false; - let shadowedByShadowed = shadowedComposite.mediatesShadows?.map( - (v) => v.shadowed, - ); + let shadowedByShadowed = shadowedComposite.mediatesShadows + ?.filter((v) => v.propVariable === undefined) + .map((v) => v.shadowed); while (shadowedByShadowed?.length > 0) { if ( @@ -2892,7 +2895,9 @@ export default class Core { if (comp.mediatesShadows) { return [ ...acc, - ...comp.mediatesShadows.map((v) => v.shadowed), + ...comp.mediatesShadows + .filter((v) => v.propVariable === undefined) + .map((v) => v.shadowed), ]; } else { return acc; @@ -3201,6 +3206,7 @@ export default class Core { }); component.replacements = replacementResult.components; } catch (e) { + // throw e; component.replacements = await this.setErrorReplacements({ composite: component, message: e.message, @@ -3775,6 +3781,15 @@ export default class Core { attributeSpecification.fallBackToParentStateVariable, }; } + if ( + attributeSpecification.fallBackToSourceCompositeStateVariable + ) { + dependencies.sourceCompositeValue = { + dependencyType: "sourceCompositeStateVariable", + variableName: + attributeSpecification.fallBackToSourceCompositeStateVariable, + }; + } if (attributeSpecification.createPrimitiveOfType) { dependencies.attributePrimitive = { dependencyType: "attributePrimitive", @@ -3838,12 +3853,34 @@ export default class Core { checkForActualChange: { [varName]: true }, }; } else { - return { - useEssentialOrDefaultValue: { - [varName]: true, - }, - checkForActualChange: { [varName]: true }, - }; + // sourceCompositeValue would be undefined if fallBackToSourceCompositeStateVariable wasn't specified + // sourceCompositeValue would be null if the sourceCompositeValue state variables + // did not exist or its value was null + + let haveSourceCompositeValue = + dependencyValues.sourceCompositeValue !== + undefined && + dependencyValues.sourceCompositeValue !== null; + if ( + haveSourceCompositeValue && + !usedDefault.sourceCompositeValue && + essentialValues[varName] === undefined + ) { + return { + setValue: { + [varName]: + dependencyValues.sourceCompositeValue, + }, + checkForActualChange: { [varName]: true }, + }; + } else { + return { + useEssentialOrDefaultValue: { + [varName]: true, + }, + checkForActualChange: { [varName]: true }, + }; + } } } @@ -3905,24 +3942,49 @@ export default class Core { ], }; } else { - // no component or primitive, so value is essential and give it the desired value, but validated + let haveSourceCompositeValue = + dependencyValues.sourceCompositeValue !== + undefined && + dependencyValues.sourceCompositeValue !== null; + if ( + haveSourceCompositeValue && + !usedDefault.sourceCompositeValue && + essentialValues[varName] === undefined + ) { + // value from source composite was used, so propagate back to source composite + return { + success: true, + instructions: [ + { + setDependency: + "sourceCompositeValue", + desiredValue: + desiredStateVariableValues[ + varName + ], + }, + ], + }; + } else { + // no component or primitive, so value is essential and give it the desired value, but validated - let res = validateAttributeValue({ - value: desiredStateVariableValues[varName], - attributeSpecification, - attribute: attrName, - }); + let res = validateAttributeValue({ + value: desiredStateVariableValues[varName], + attributeSpecification, + attribute: attrName, + }); - return { - success: true, - instructions: [ - { - setEssentialValue: varName, - value: res.value, - }, - ], - sendWarnings: res.warnings, - }; + return { + success: true, + instructions: [ + { + setEssentialValue: varName, + value: res.value, + }, + ], + sendWarnings: res.warnings, + }; + } } } @@ -4118,6 +4180,7 @@ export default class Core { } let stateDef = stateVariableDefinitions[primaryStateVariableForDefinition]; + stateDef.isShadow = true; stateDef.returnDependencies = () => ({ adapterTargetVariable: { dependencyType: "stateVariable", @@ -4177,7 +4240,6 @@ export default class Core { componentClass, }) { let targetComponent = this._components[redefineDependencies.targetName]; - let core = this; if (redefineDependencies.propVariable) { // if we have an array entry state variable that hasn't been created yet @@ -4304,6 +4366,13 @@ export default class Core { attributeSpecification.fallBackToParentStateVariable, }; } + if (attributeSpecification.fallBackToSourceCompositeStateVariable) { + thisDependencies.sourceCompositeValue = { + dependencyType: "sourceCompositeStateVariable", + variableName: + attributeSpecification.fallBackToSourceCompositeStateVariable, + }; + } stateVarDef.returnDependencies = () => thisDependencies; @@ -4349,12 +4418,34 @@ export default class Core { checkForActualChange: { [varName]: true }, }; } else { - return { - useEssentialOrDefaultValue: { - [varName]: true, - }, - checkForActualChange: { [varName]: true }, - }; + // sourceCompositeValue would be undefined if fallBackToSourceCompositeStateVariable wasn't specified + // sourceCompositeValue would be null if the sourceCompositeValue state variables + // did not exist or its value was null + + let haveSourceCompositeValue = + dependencyValues.sourceCompositeValue !== + undefined && + dependencyValues.sourceCompositeValue !== null; + if ( + haveSourceCompositeValue && + !usedDefault.sourceCompositeValue && + essentialValues[varName] === undefined + ) { + return { + setValue: { + [varName]: + dependencyValues.sourceCompositeValue, + }, + checkForActualChange: { [varName]: true }, + }; + } else { + return { + useEssentialOrDefaultValue: { + [varName]: true, + }, + checkForActualChange: { [varName]: true }, + }; + } } } @@ -4418,23 +4509,48 @@ export default class Core { ], }; } else { - // no component or primitive, so value is essential and give it the desired value, but validated - let res = validateAttributeValue({ - value: desiredStateVariableValues[varName], - attributeSpecification, - attribute: attrName, - }); + let haveSourceCompositeValue = + dependencyValues.sourceCompositeValue !== + undefined && + dependencyValues.sourceCompositeValue !== null; + if ( + haveSourceCompositeValue && + !usedDefault.sourceCompositeValue && + essentialValues[varName] === undefined + ) { + // value from source composite was used, so propagate back to source composite + return { + success: true, + instructions: [ + { + setDependency: + "sourceCompositeValue", + desiredValue: + desiredStateVariableValues[ + varName + ], + }, + ], + }; + } else { + // no component or primitive, so value is essential and give it the desired value, but validated + let res = validateAttributeValue({ + value: desiredStateVariableValues[varName], + attributeSpecification, + attribute: attrName, + }); - return { - success: true, - instructions: [ - { - setEssentialValue: varName, - value: res.value, - }, - ], - sendWarnings: res.warnings, - }; + return { + success: true, + instructions: [ + { + setEssentialValue: varName, + value: res.value, + }, + ], + sendWarnings: res.warnings, + }; + } } } // attribute based on child @@ -8999,7 +9115,10 @@ export default class Core { if (parent.shadowedBy) { for (let shadowingParent of parent.shadowedBy) { - if (shadowingParent.shadows.propVariable) { + if ( + shadowingParent.shadows.propVariable || + shadowingParent.constructor.doNotExpandAsShadowed + ) { continue; } @@ -9445,7 +9564,11 @@ export default class Core { let addedComponents = {}; let parentsOfDeleted = new Set(); - if (component.shadows) { + if ( + component.shadows && + !component.shadows.propVariable && + !component.constructor.doNotExpandAsShadowed + ) { // if shadows, don't update replacements // instead, replacements will get updated when shadowed component // is updated @@ -9598,6 +9721,7 @@ export default class Core { newComponents = createResult.components; } catch (e) { + // throw e; newComponents = await this.setErrorReplacements({ composite: component, message: e.message, @@ -9876,7 +10000,10 @@ export default class Core { if (composite.shadowedBy) { for (let shadowingComposite of composite.shadowedBy) { - if (shadowingComposite.shadows.propVariable) { + if ( + shadowingComposite.shadows.propVariable || + shadowingComposite.constructor.doNotExpandAsShadowed + ) { continue; } @@ -9888,7 +10015,10 @@ export default class Core { let shadowingCompToDelete; if (compToDelete.shadowedBy) { for (let cShadow of compToDelete.shadowedBy) { - if (cShadow.shadows.propVariable) { + if ( + cShadow.shadows.propVariable || + cShadow.constructor.doNotExpandAsShadowed + ) { continue; } if ( @@ -10060,7 +10190,10 @@ export default class Core { if (component.shadowedBy) { for (let shadowingComponent of component.shadowedBy) { - if (shadowingComponent.shadows.propVariable) { + if ( + shadowingComponent.shadows.propVariable || + shadowingComponent.constructor.doNotExpandAsShadowed + ) { continue; } await this.processChildChangesAndRecurseToShadows( @@ -10106,7 +10239,10 @@ export default class Core { let newComponentsForShadows = {}; for (let shadowingComponent of componentToShadow.shadowedBy) { - if (shadowingComponent.shadows.propVariable) { + if ( + shadowingComponent.shadows.propVariable || + shadowingComponent.constructor.doNotExpandAsShadowed + ) { continue; } @@ -10320,6 +10456,7 @@ export default class Core { }); newComponents = createResult.components; } catch (e) { + // throw e; newComponents = await this.setErrorReplacements({ composite: shadowingComponent, message: e.message, @@ -10333,7 +10470,10 @@ export default class Core { if (parentToShadow) { if (parentToShadow.shadowedBy) { for (let pShadow of parentToShadow.shadowedBy) { - if (pShadow.shadows.propVariable) { + if ( + pShadow.shadows.propVariable || + pShadow.constructor.doNotExpandAsShadowed + ) { continue; } if ( @@ -10466,7 +10606,10 @@ export default class Core { if (component.shadowedBy) { for (let shadowingComponent of component.shadowedBy) { - if (shadowingComponent.shadows.propVariable) { + if ( + shadowingComponent.shadows.propVariable || + shadowingComponent.constructor.doNotExpandAsShadowed + ) { continue; } let additionalcompositesWithAdjustedReplacements = @@ -13627,7 +13770,10 @@ function calculateAllComponentsShadowing(component) { let allShadowing = []; if (component.shadowedBy) { for (let comp2 of component.shadowedBy) { - if (!comp2.shadows.propVariable) { + if ( + !comp2.shadows.propVariable & + !comp2.constructor.doNotExpandAsShadowed + ) { allShadowing.push(comp2.componentName); let additionalShadowing = calculateAllComponentsShadowing(comp2); diff --git a/packages/doenetml-worker/src/CoreWorker.js b/packages/doenetml-worker/src/CoreWorker.js index 1b02bbfc7..f63ba598f 100644 --- a/packages/doenetml-worker/src/CoreWorker.js +++ b/packages/doenetml-worker/src/CoreWorker.js @@ -183,6 +183,7 @@ async function createCore(args) { } queuedRequestActions = []; } catch (e) { + // throw e; postMessage({ messageType: "inErrorState", coreId: coreArgs.coreId, diff --git a/packages/doenetml-worker/src/components/Aliases.js b/packages/doenetml-worker/src/components/Aliases.js index 7ffdbcd28..dab51d86d 100644 --- a/packages/doenetml-worker/src/components/Aliases.js +++ b/packages/doenetml-worker/src/components/Aliases.js @@ -36,13 +36,11 @@ export class Ylabel extends Label { export class MatrixRow extends MathList { static componentType = "matrixRow"; - static rendererType = "mathList"; static excludeFromSchema = true; } export class MatrixColumn extends MathList { static componentType = "matrixColumn"; - static rendererType = "mathList"; static excludeFromSchema = true; } diff --git a/packages/doenetml-worker/src/components/Answer.js b/packages/doenetml-worker/src/components/Answer.js index fef19f330..21159329e 100644 --- a/packages/doenetml-worker/src/components/Answer.js +++ b/packages/doenetml-worker/src/components/Answer.js @@ -167,6 +167,8 @@ export default class Answer extends InlineComponent { }; attributes.type = { createPrimitiveOfType: "string", + createStateVariable: "type", + defaultValue: null, }; attributes.disableAfterCorrect = { @@ -251,8 +253,8 @@ export default class Answer extends InlineComponent { componentAttributes, componentInfoObjects, }) { - // if chidren are strings and macros - // wrap with award and type + // if children are strings and macros + // wrap with award function checkForResponseDescendant(components) { for (let component of components) { @@ -355,7 +357,7 @@ export default class Answer extends InlineComponent { } else if ( componentIsSpecifiedType(grandChild, "when") ) { - // have to test for when before boolean, sincd when is derived from boolean! + // have to test for `when` before `boolean`, since `when` is derived from `boolean`! } else if ( componentIsSpecifiedType(grandChild, "math") || componentIsSpecifiedType( @@ -596,28 +598,7 @@ export default class Answer extends InlineComponent { ...childrenToNotWrapEnd, ]; } else { - // if have one child and it has a specified componentType - // then no need to wrap with componentType - - let needToWrapWithComponentType = - childrenToWrap.length > 1 || - (componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: childrenToWrap[0].componentType, - baseComponentType: "_composite", - }) && - !childrenToWrap[0].props?.componentType); - - let awardChildren; - if (needToWrapWithComponentType) { - awardChildren = [ - { - componentType: type, - children: childrenToWrap, - }, - ]; - } else { - awardChildren = childrenToWrap; - } + let awardChildren = childrenToWrap; newChildren = [ ...childrenToNotWrapBegin, { diff --git a/packages/doenetml-worker/src/components/Award.js b/packages/doenetml-worker/src/components/Award.js index 8cf832860..19376267c 100644 --- a/packages/doenetml-worker/src/components/Award.js +++ b/packages/doenetml-worker/src/components/Award.js @@ -1,6 +1,10 @@ import BaseComponent from "./abstract/BaseComponent"; import me from "math-expressions"; -import { evaluateLogic } from "../utils/booleanLogic"; +import { + buildParsedExpression, + evaluateLogic, + returnChildrenByCodeStateVariableDefinitions, +} from "../utils/booleanLogic"; import { getNamespaceFromName } from "@doenet/utils"; export default class Award extends BaseComponent { @@ -317,40 +321,18 @@ export default class Award extends BaseComponent { componentTypes: ["when"], }, { - group: "maths", - componentTypes: ["math"], + group: "strings", + componentTypes: ["string"], }, { - group: "numbers", - componentTypes: ["number"], - }, - { - group: "texts", - componentTypes: ["text"], - }, - { - group: "booleans", - componentTypes: ["boolean"], - }, - { - group: "mathLists", - componentTypes: ["mathList"], - }, - { - group: "numberLists", - componentTypes: ["numberList"], - }, - { - group: "textLists", - componentTypes: ["textList"], - }, - { - group: "booleanLists", - componentTypes: ["booleanList"], - }, - { - group: "otherComparableTypes", - componentTypes: ["orbitalDiagram"], + group: "comparableTypes", + componentTypes: [ + "math", + "number", + "text", + "boolean", + "orbitalDiagram", + ], }, ]; } @@ -359,44 +341,84 @@ export default class Award extends BaseComponent { let stateVariableDefinitions = super.returnStateVariableDefinitions(); stateVariableDefinitions.parsedExpression = { - additionalStateVariablesDefined: ["requireInputInAnswer"], + additionalStateVariablesDefined: [ + "requireInputInAnswer", + "codePre", + ], returnDependencies: () => ({ whenChild: { dependencyType: "child", childGroups: ["whens"], }, - typeChildren: { + // call it "allChildren" so that can use the buildParsedExpression function that Boolean uses + allChildren: { dependencyType: "child", - childGroups: [ - "maths", - "numbers", - "texts", - "booleans", - "mathLists", - "numberLists", - "textLists", - "booleanLists", - "otherComparableTypes", - ], + childGroups: ["strings", "comparableTypes"], + }, + stringChildren: { + dependencyType: "child", + childGroups: ["strings"], + variableNames: ["value"], + }, + answerType: { + dependencyType: "parentStateVariable", + parentComponentType: "answer", + variableName: "type", + }, + splitSymbols: { + dependencyType: "stateVariable", + variableName: "splitSymbols", }, }), - definition: function ({ dependencyValues }) { + definition: function ({ dependencyValues, componentInfoObjects }) { let parsedExpression = null; let requireInputInAnswer = false; + let codePre = ""; if ( dependencyValues.whenChild.length == 0 && - dependencyValues.typeChildren.length > 0 + dependencyValues.allChildren.length > 0 ) { requireInputInAnswer = true; - parsedExpression = me.fromAst(["=", "comp1", "comp2"]); + let doNotSplit = + dependencyValues.splitSymbols === false || + (dependencyValues.answerType && + !["number", "math"].includes( + dependencyValues.answerType, + )); + + let { setValue } = buildParsedExpression({ + dependencyValues, + componentInfoObjects, + doNotSplit, + splitAtInitialLevel: true, + }); + + codePre = setValue.codePre; + + parsedExpression = me.fromAst([ + "=", + codePre + "Input", + setValue.parsedExpression.tree, + ]); } - return { setValue: { parsedExpression, requireInputInAnswer } }; + return { + setValue: { + parsedExpression, + requireInputInAnswer, + codePre, + }, + }; }, }; + Object.assign( + stateVariableDefinitions, + returnChildrenByCodeStateVariableDefinitions(), + ); + stateVariableDefinitions.creditAchievedIfSubmit = { public: true, shadowingInstructions: { @@ -421,63 +443,10 @@ export default class Award extends BaseComponent { childGroups: ["whens"], variableNames: ["fractionSatisfied"], }, - mathChild: { - dependencyType: "child", - childGroups: ["maths"], - variableNames: ["value", "unordered"], - }, - numberChild: { - dependencyType: "child", - childGroups: ["numbers"], - variableNames: ["value"], - }, - textChild: { - dependencyType: "child", - childGroups: ["texts"], - variableNames: ["value"], - }, - booleanChild: { - dependencyType: "child", - childGroups: ["booleans"], - variableNames: ["value"], - }, - mathListChild: { - dependencyType: "child", - childGroups: ["mathLists"], - variableNames: ["maths", "unordered"], - }, - numberListChild: { - dependencyType: "child", - childGroups: ["numberLists"], - variableNames: ["numbers", "unordered"], - }, - textListChild: { - dependencyType: "child", - childGroups: ["textLists"], - variableNames: ["texts", "unordered"], - }, - booleanListChild: { - dependencyType: "child", - childGroups: ["booleanLists"], - variableNames: ["booleans", "unordered"], - }, - otherComparableChild: { - dependencyType: "child", - childGroups: ["otherComparableTypes"], - variableNames: ["value"], - }, answerInput: { dependencyType: "parentStateVariable", variableName: "inputChildWithValues", }, - parsedExpression: { - dependencyType: "stateVariable", - variableName: "parsedExpression", - }, - matchPartial: { - dependencyType: "stateVariable", - variableName: "matchPartial", - }, symbolicEquality: { dependencyType: "stateVariable", variableName: "symbolicEquality", @@ -526,6 +495,44 @@ export default class Award extends BaseComponent { dependencyType: "stateVariable", variableName: "matchBlanks", }, + parsedExpression: { + dependencyType: "stateVariable", + variableName: "parsedExpression", + }, + allChildren: { + dependencyType: "child", + childGroups: ["strings", "comparableTypes"], + variableNames: ["value"], + variablesOptional: true, + }, + booleanChildrenByCode: { + dependencyType: "stateVariable", + variableName: "booleanChildrenByCode", + }, + textChildrenByCode: { + dependencyType: "stateVariable", + variableName: "textChildrenByCode", + }, + mathChildrenByCode: { + dependencyType: "stateVariable", + variableName: "mathChildrenByCode", + }, + numberChildrenByCode: { + dependencyType: "stateVariable", + variableName: "numberChildrenByCode", + }, + otherChildrenByCode: { + dependencyType: "stateVariable", + variableName: "otherChildrenByCode", + }, + matchPartial: { + dependencyType: "stateVariable", + variableName: "matchPartial", + }, + codePre: { + dependencyType: "stateVariable", + variableName: "codePre", + }, }), definition: function ({ dependencyValues, usedDefault }) { let fractionSatisfiedIfSubmit; @@ -803,51 +810,12 @@ export default class Award extends BaseComponent { } function evaluateLogicDirectlyFromChildren({ dependencyValues, usedDefault }) { - let dependenciesForEvaluateLogic = { - mathChildrenByCode: {}, - mathListChildrenByCode: {}, - numberChildrenByCode: {}, - numberListChildrenByCode: {}, - textChildrenByCode: {}, - textListChildrenByCode: {}, - booleanChildrenByCode: {}, - booleanListChildrenByCode: {}, - otherChildrenByCode: {}, - }; + let dependenciesForEvaluateLogic = {}; Object.assign(dependenciesForEvaluateLogic, dependencyValues); let canOverrideUnorderedCompare = usedDefault.unorderedCompare; - if (dependencyValues.textChild.length > 0) { - dependenciesForEvaluateLogic.textChildrenByCode.comp2 = - dependencyValues.textChild[0]; - } else if (dependencyValues.mathChild.length > 0) { - dependenciesForEvaluateLogic.mathChildrenByCode.comp2 = - dependencyValues.mathChild[0]; - } else if (dependencyValues.numberChild.length > 0) { - dependenciesForEvaluateLogic.numberChildrenByCode.comp2 = - dependencyValues.numberChild[0]; - } else if (dependencyValues.booleanChild.length > 0) { - dependenciesForEvaluateLogic.booleanChildrenByCode.comp2 = - dependencyValues.booleanChild[0]; - } else if (dependencyValues.textListChild.length > 0) { - dependenciesForEvaluateLogic.textListChildrenByCode.comp2 = - dependencyValues.textListChild[0]; - } else if (dependencyValues.mathListChild.length > 0) { - dependenciesForEvaluateLogic.mathListChildrenByCode.comp2 = - dependencyValues.mathListChild[0]; - } else if (dependencyValues.numberListChild.length > 0) { - dependenciesForEvaluateLogic.numberListChildrenByCode.comp2 = - dependencyValues.numberListChild[0]; - } else if (dependencyValues.booleanListChild.length > 0) { - dependenciesForEvaluateLogic.booleanListChildrenByCode.comp2 = - dependencyValues.booleanListChild[0]; - } else if (dependencyValues.otherComparableChild.length > 0) { - dependenciesForEvaluateLogic.otherChildrenByCode.comp2 = - dependencyValues.otherComparableChild[0]; - } - let answerValue = dependencyValues.answerInput.stateValues.immediateValue; if (answerValue === undefined) { answerValue = dependencyValues.answerInput.stateValues.value; @@ -857,14 +825,15 @@ function evaluateLogicDirectlyFromChildren({ dependencyValues, usedDefault }) { stateValues: { value: answerValue }, }; + let inputCode = dependencyValues.codePre + "Input"; if (dependencyValues.answerInput.componentType === "textInput") { - dependenciesForEvaluateLogic.textChildrenByCode.comp1 = + dependenciesForEvaluateLogic.textChildrenByCode[inputCode] = answerChildForLogic; } else if (dependencyValues.answerInput.componentType === "booleanInput") { - dependenciesForEvaluateLogic.booleanChildrenByCode.comp1 = + dependenciesForEvaluateLogic.booleanChildrenByCode[inputCode] = answerChildForLogic; } else { - dependenciesForEvaluateLogic.mathChildrenByCode.comp1 = + dependenciesForEvaluateLogic.mathChildrenByCode[inputCode] = answerChildForLogic; } diff --git a/packages/doenetml-worker/src/components/Boolean.js b/packages/doenetml-worker/src/components/Boolean.js index eb96f3d85..77dcc9ddf 100644 --- a/packages/doenetml-worker/src/components/Boolean.js +++ b/packages/doenetml-worker/src/components/Boolean.js @@ -1,5 +1,9 @@ import InlineComponent from "./abstract/InlineComponent"; -import { evaluateLogic, buildParsedExpression } from "../utils/booleanLogic"; +import { + evaluateLogic, + buildParsedExpression, + returnChildrenByCodeStateVariableDefinitions, +} from "../utils/booleanLogic"; export default class BooleanComponent extends InlineComponent { static componentType = "boolean"; @@ -100,13 +104,9 @@ export default class BooleanComponent extends InlineComponent { group: "comparableTypes", componentTypes: [ "math", - "mathList", "number", - "numberList", "text", - "textList", "boolean", - "booleanList", "orbitalDiagram", ], }, @@ -116,6 +116,36 @@ export default class BooleanComponent extends InlineComponent { static returnStateVariableDefinitions() { let stateVariableDefinitions = super.returnStateVariableDefinitions(); + stateVariableDefinitions.inUnorderedList = { + defaultValue: false, + returnDependencies: () => ({ + sourceCompositeUnordered: { + dependencyType: "sourceCompositeStateVariable", + variableName: "unordered", + }, + }), + definition({ dependencyValues, usedDefault }) { + if ( + dependencyValues.sourceCompositeUnordered !== null && + !usedDefault.sourceCompositeUnordered + ) { + return { + setValue: { + inUnorderedList: Boolean( + dependencyValues.sourceCompositeUnordered, + ), + }, + }; + } else { + return { + setValue: { + inUnorderedList: false, + }, + }; + } + }, + }; + stateVariableDefinitions.parsedExpression = { additionalStateVariablesDefined: ["codePre"], returnDependencies: () => ({ @@ -132,134 +162,10 @@ export default class BooleanComponent extends InlineComponent { definition: buildParsedExpression, }; - stateVariableDefinitions.mathChildrenByCode = { - additionalStateVariablesDefined: [ - "mathListChildrenByCode", - "numberChildrenByCode", - "numberListChildrenByCode", - "textChildrenByCode", - "textListChildrenByCode", - "booleanChildrenByCode", - "booleanListChildrenByCode", - "otherChildrenByCode", - ], - returnDependencies: () => ({ - allChildren: { - dependencyType: "child", - childGroups: ["strings", "comparableTypes"], - variableNames: [ - "value", - "texts", - "maths", - "numbers", - "booleans", - "fractionSatisfied", - "unordered", - ], - variablesOptional: true, - }, - codePre: { - dependencyType: "stateVariable", - variableName: "codePre", - }, - }), - definition({ dependencyValues, componentInfoObjects }) { - let mathChildrenByCode = {}; - let mathListChildrenByCode = {}; - let numberChildrenByCode = {}; - let numberListChildrenByCode = {}; - let textChildrenByCode = {}; - let textListChildrenByCode = {}; - let booleanChildrenByCode = {}; - let booleanListChildrenByCode = {}; - let otherChildrenByCode = {}; - let subnum = 0; - - let codePre = dependencyValues.codePre; - - for (let child of dependencyValues.allChildren) { - if (typeof child !== "string") { - // a math, mathList, text, textList, boolean, or booleanList - let code = codePre + subnum; - - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "math", - }) - ) { - mathChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "mathList", - }) - ) { - mathListChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "number", - }) - ) { - numberChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - numberListChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "text", - }) - ) { - textChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "textList", - }) - ) { - textListChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "boolean", - }) - ) { - booleanChildrenByCode[code] = child; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "booleanList", - }) - ) { - booleanListChildrenByCode[code] = child; - } else { - otherChildrenByCode[code] = child; - } - subnum += 1; - } - } - - return { - setValue: { - mathChildrenByCode, - mathListChildrenByCode, - numberChildrenByCode, - numberListChildrenByCode, - textChildrenByCode, - textListChildrenByCode, - booleanChildrenByCode, - booleanListChildrenByCode, - otherChildrenByCode, - }, - }; - }, - }; + Object.assign( + stateVariableDefinitions, + returnChildrenByCodeStateVariableDefinitions(), + ); stateVariableDefinitions.value = { public: true, @@ -334,34 +240,18 @@ export default class BooleanComponent extends InlineComponent { dependencyType: "stateVariable", variableName: "booleanChildrenByCode", }, - booleanListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "booleanListChildrenByCode", - }, textChildrenByCode: { dependencyType: "stateVariable", variableName: "textChildrenByCode", }, - textListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "textListChildrenByCode", - }, mathChildrenByCode: { dependencyType: "stateVariable", variableName: "mathChildrenByCode", }, - mathListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "mathListChildrenByCode", - }, numberChildrenByCode: { dependencyType: "stateVariable", variableName: "numberChildrenByCode", }, - numberListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "numberListChildrenByCode", - }, otherChildrenByCode: { dependencyType: "stateVariable", variableName: "otherChildrenByCode", diff --git a/packages/doenetml-worker/src/components/BooleanList.js b/packages/doenetml-worker/src/components/BooleanList.js index 571381dfd..e58346e4e 100644 --- a/packages/doenetml-worker/src/components/BooleanList.js +++ b/packages/doenetml-worker/src/components/BooleanList.js @@ -1,10 +1,18 @@ -import InlineComponent from "./abstract/InlineComponent"; +import CompositeComponent from "./abstract/CompositeComponent"; import { returnGroupIntoComponentTypeSeparatedBySpacesOutsideParens } from "./commonsugar/lists"; +import { + convertAttributesForComponentType, + postProcessCopy, +} from "../utils/copy"; +import { processAssignNames } from "../utils/naming"; -export default class BooleanList extends InlineComponent { +export default class BooleanList extends CompositeComponent { static componentType = "booleanList"; - static rendererType = "asList"; - static renderChildren = true; + + static stateVariableToEvaluateAfterReplacements = + "readyToExpandWhenResolved"; + + static assignNamesToReplacements = true; static includeBlankStringChildren = true; static removeBlankStringChildrenPostSugar = true; @@ -12,11 +20,14 @@ export default class BooleanList extends InlineComponent { // when another component has a attribute that is a booleanList, // use the booleans state variable to populate that attribute static stateVariableToBeShadowed = "booleans"; + static primaryStateVariableForDefinition = "booleansShadow"; // even if inside a component that turned on descendantCompositesMustHaveAReplacement // don't required composite replacements static descendantCompositesMustHaveAReplacement = false; + static doNotExpandAsShadowed = true; + static createAttributesObject() { let attributes = super.createAttributesObject(); attributes.unordered = { @@ -28,14 +39,23 @@ export default class BooleanList extends InlineComponent { attributes.maxNumber = { createComponentOfType: "number", createStateVariable: "maxNumber", - defaultValue: null, + defaultValue: Infinity, public: true, }; + + attributes.fixed = { + leaveRaw: true, + }; + + attributes.isResponse = { + leaveRaw: true, + }; + return attributes; } // Include children that can be added due to sugar - static additionalSchemaChildren = ["math", "number", "string"]; + static additionalSchemaChildren = ["string"]; static returnSugarInstructions() { let sugarInstructions = super.returnSugarInstructions(); @@ -59,22 +79,28 @@ export default class BooleanList extends InlineComponent { group: "booleans", componentTypes: ["boolean"], }, - { - group: "booleanLists", - componentTypes: ["booleanList"], - }, ]; } static returnStateVariableDefinitions() { let stateVariableDefinitions = super.returnStateVariableDefinitions(); - // set overrideChildHide so that children are hidden - // only based on whether or not the list is hidden - // so that can't have a list with partially hidden components - stateVariableDefinitions.overrideChildHide = { + stateVariableDefinitions.booleansShadow = { + defaultValue: null, + hasEssential: true, + returnDependencies: () => ({}), + definition: () => ({ + useEssentialOrDefaultValue: { + booleansShadow: true, + }, + }), + }; + + stateVariableDefinitions.asList = { returnDependencies: () => ({}), - definition: () => ({ setValue: { overrideChildHide: true } }), + definition() { + return { setValue: { asList: true } }; + }, }; stateVariableDefinitions.numComponents = { @@ -82,85 +108,59 @@ export default class BooleanList extends InlineComponent { shadowingInstructions: { createComponentOfType: "number", }, - additionalStateVariablesDefined: ["childIndexByArrayKey"], + additionalStateVariablesDefined: ["childNameByComponent"], returnDependencies() { return { maxNumber: { dependencyType: "stateVariable", variableName: "maxNumber", }, - booleanListChildren: { + booleanChildren: { dependencyType: "child", - childGroups: ["booleanLists"], - variableNames: ["numComponents"], + childGroups: ["booleans"], }, - booleanAndBooleanListChildren: { - dependencyType: "child", - childGroups: ["booleans", "booleanLists"], - skipComponentNames: true, + booleansShadow: { + dependencyType: "stateVariable", + variableName: "booleansShadow", }, }; }, - definition: function ({ dependencyValues, componentInfoObjects }) { + definition: function ({ dependencyValues }) { let numComponents = 0; - let childIndexByArrayKey = []; - - let nBooleanLists = 0; - for (let [ - childInd, - child, - ] of dependencyValues.booleanAndBooleanListChildren.entries()) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "booleanList", - }) - ) { - let booleanListChild = - dependencyValues.booleanListChildren[nBooleanLists]; - nBooleanLists++; - for ( - let i = 0; - i < booleanListChild.stateValues.numComponents; - i++ - ) { - childIndexByArrayKey[numComponents + i] = [ - childInd, - i, - ]; - } - numComponents += - booleanListChild.stateValues.numComponents; - } else { - childIndexByArrayKey[numComponents] = [childInd, 0]; - numComponents += 1; - } + let childNameByComponent = []; + + if (dependencyValues.booleanChildren.length > 0) { + childNameByComponent = dependencyValues.booleanChildren.map( + (x) => x.componentName, + ); + numComponents = dependencyValues.booleanChildren.length; + } else if (dependencyValues.booleansShadow !== null) { + numComponents = dependencyValues.booleansShadow.length; } let maxNum = dependencyValues.maxNumber; - if (maxNum !== null && numComponents > maxNum) { + if (numComponents > maxNum) { numComponents = maxNum; - childIndexByArrayKey = childIndexByArrayKey.slice( + childNameByComponent = childNameByComponent.slice( 0, maxNum, ); } return { - setValue: { numComponents, childIndexByArrayKey }, + setValue: { numComponents, childNameByComponent }, checkForActualChange: { numComponents: true }, }; }, }; stateVariableDefinitions.booleans = { - public: true, shadowingInstructions: { createComponentOfType: "boolean", }, isArray: true, entryPrefixes: ["boolean"], - stateVariablesDeterminingDependencies: ["childIndexByArrayKey"], + stateVariablesDeterminingDependencies: ["childNameByComponent"], returnArraySizeDependencies: () => ({ numComponents: { dependencyType: "stateVariable", @@ -174,28 +174,26 @@ export default class BooleanList extends InlineComponent { returnArrayDependenciesByKey({ arrayKeys, stateValues }) { let dependenciesByKey = {}; let globalDependencies = { - childIndexByArrayKey: { + childNameByComponent: { dependencyType: "stateVariable", - variableName: "childIndexByArrayKey", + variableName: "childNameByComponent", + }, + booleansShadow: { + dependencyType: "stateVariable", + variableName: "booleansShadow", }, }; for (let arrayKey of arrayKeys) { let childIndices = []; - let booleanIndex = "1"; - if (stateValues.childIndexByArrayKey[arrayKey]) { - childIndices = [ - stateValues.childIndexByArrayKey[arrayKey][0], - ]; - booleanIndex = - stateValues.childIndexByArrayKey[arrayKey][1] + 1; + if (stateValues.childNameByComponent[arrayKey]) { + childIndices = [arrayKey]; } dependenciesByKey[arrayKey] = { - booleanAndBooleanListChildren: { + booleanChildren: { dependencyType: "child", - childGroups: ["booleans", "booleanLists"], - variableNames: ["value", "boolean" + booleanIndex], - variablesOptional: true, + childGroups: ["booleans"], + variableNames: ["value"], childIndices, }, }; @@ -212,20 +210,13 @@ export default class BooleanList extends InlineComponent { for (let arrayKey of arrayKeys) { let child = - dependencyValuesByKey[arrayKey] - .booleanAndBooleanListChildren[0]; + dependencyValuesByKey[arrayKey].booleanChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - booleans[arrayKey] = child.stateValues.value; - } else { - let booleanIndex = - globalDependencyValues.childIndexByArrayKey[ - arrayKey - ][1] + 1; - booleans[arrayKey] = - child.stateValues["boolean" + booleanIndex]; - } + booleans[arrayKey] = child.stateValues.value; + } else if (globalDependencyValues.booleansShadow !== null) { + booleans[arrayKey] = + globalDependencyValues.booleansShadow[arrayKey]; } } @@ -236,7 +227,7 @@ export default class BooleanList extends InlineComponent { globalDependencyValues, dependencyValuesByKey, dependencyNamesByKey, - arraySize, + workspace, }) { let instructions = []; @@ -246,35 +237,29 @@ export default class BooleanList extends InlineComponent { } let child = - dependencyValuesByKey[arrayKey] - .booleanAndBooleanListChildren[0]; + dependencyValuesByKey[arrayKey].booleanChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .booleanAndBooleanListChildren, - desiredValue: - desiredStateVariableValues.booleans[ - arrayKey - ], - childIndex: 0, - variableIndex: 0, - }); - } else { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .booleanAndBooleanListChildren, - desiredValue: - desiredStateVariableValues.booleans[ - arrayKey - ], - childIndex: 0, - variableIndex: 1, - }); + instructions.push({ + setDependency: + dependencyNamesByKey[arrayKey].booleanChildren, + desiredValue: + desiredStateVariableValues.booleans[arrayKey], + childIndex: 0, + variableIndex: 0, + }); + } else if (globalDependencyValues.booleansShadow !== null) { + if (!workspace.desiredBooleanShadow) { + workspace.desiredBooleanShadow = [ + ...globalDependencyValues.booleansShadow, + ]; } + workspace.desiredBooleanShadow[arrayKey] = + desiredStateVariableValues.booleans[arrayKey]; + instructions.push({ + setDependency: "booleansShadow", + desiredValue: workspace.desiredBooleanShadow, + }); } } @@ -295,139 +280,170 @@ export default class BooleanList extends InlineComponent { targetVariableName: "booleans", }; - stateVariableDefinitions.componentNamesInList = { + stateVariableDefinitions.readyToExpandWhenResolved = { returnDependencies: () => ({ - booleanAndBooleanListChildren: { - dependencyType: "child", - childGroups: ["booleans", "booleanLists"], - variableNames: ["componentNamesInList"], - variablesOptional: true, - }, - maxNumber: { + childNameByComponent: { dependencyType: "stateVariable", - variableName: "maxNumber", + variableName: "childNameByComponent", }, }), - definition: function ({ dependencyValues, componentInfoObjects }) { - let componentNamesInList = []; - - for (let child of dependencyValues.booleanAndBooleanListChildren) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "booleanList", - }) - ) { - componentNamesInList.push( - ...child.stateValues.componentNamesInList, - ); - } else { - componentNamesInList.push(child.componentName); - } - } - - let maxNum = dependencyValues.maxNumber; - if (maxNum !== null && componentNamesInList.length > maxNum) { - maxNum = Math.max(0, Math.floor(maxNum)); - componentNamesInList = componentNamesInList.slice( - 0, - maxNum, - ); - } - - return { setValue: { componentNamesInList } }; + // When this state variable is marked stale + // it indicates we should update replacements. + // For this to work, must set + // stateVariableToEvaluateAfterReplacements + // to this variable so that it is marked fresh + markStale: () => ({ updateReplacements: true }), + definition: function () { + return { setValue: { readyToExpandWhenResolved: true } }; }, }; - stateVariableDefinitions.numComponentsToDisplayByChild = { - additionalStateVariablesDefined: ["numChildrenToRender"], - returnDependencies: () => ({ - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - booleanListChildren: { - dependencyType: "child", - childGroups: ["booleanLists"], - variableNames: ["numComponents"], - }, - booleanAndBooleanListChildren: { - dependencyType: "child", - childGroups: ["booleans", "booleanLists"], - skipComponentNames: true, - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "booleanList", - variableName: "numComponentsToDisplayByChild", - }, - }), - definition: function ({ - dependencyValues, + return stateVariableDefinitions; + } + + static async createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }) { + let errors = []; + let warnings = []; + + let replacements = []; + let componentsCopied = []; + + let attributesToConvert = {}; + for (let attr of ["fixed", "isResponse"]) { + if (attr in component.attributes) { + attributesToConvert[attr] = component.attributes[attr]; + } + } + + let newNamespace = component.attributes.newNamespace?.primitive; + + // allow one to override the fixed and isResponse attributes + // as well as rounding settings + // by specifying it on the sequence + let attributesFromComposite = {}; + + if (Object.keys(attributesToConvert).length > 0) { + attributesFromComposite = convertAttributesForComponentType({ + attributes: attributesToConvert, + componentType: "boolean", componentInfoObjects, - componentName, - }) { - let numComponentsToDisplay = dependencyValues.numComponents; - - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent booleanList, which could have limited - // boolean of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } + compositeCreatesNewNamespace: newNamespace, + }); + } - let numComponentsToDisplayByChild = {}; + let childNameByComponent = + await component.stateValues.childNameByComponent; - let numComponentsSoFar = 0; - let numChildrenToRender = 0; + let numComponents = await component.stateValues.numComponents; + for (let i = 0; i < numComponents; i++) { + let childName = childNameByComponent[i]; + let replacementSource = components[childName]; - let nBooleanLists = 0; - for (let child of dependencyValues.booleanAndBooleanListChildren) { - let numComponentsLeft = Math.max( - 0, - numComponentsToDisplay - numComponentsSoFar, - ); - if (numComponentsLeft > 0) { - numChildrenToRender++; - } - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "booleanList", - }) - ) { - let booleanListChild = - dependencyValues.booleanListChildren[nBooleanLists]; - nBooleanLists++; - - let numComponentsForBooleanListChild = Math.min( - numComponentsLeft, - booleanListChild.stateValues.numComponents, - ); - - numComponentsToDisplayByChild[ - booleanListChild.componentName - ] = numComponentsForBooleanListChild; - numComponentsSoFar += numComponentsForBooleanListChild; - } else { - numComponentsSoFar += 1; - } - } + if (replacementSource) { + componentsCopied.push(replacementSource.componentName); + } + replacements.push({ + componentType: "boolean", + attributes: JSON.parse(JSON.stringify(attributesFromComposite)), + downstreamDependencies: { + [component.componentName]: [ + { + dependencyType: "referenceShadow", + compositeName: component.componentName, + propVariable: `boolean${i + 1}`, + }, + ], + }, + }); + } + + workspace.uniqueIdentifiersUsed = []; + replacements = postProcessCopy({ + serializedComponents: replacements, + componentName: component.componentName, + uniqueIdentifiersUsed: workspace.uniqueIdentifiersUsed, + addShadowDependencies: true, + markAsPrimaryShadow: true, + }); - return { - setValue: { - numComponentsToDisplayByChild, - numChildrenToRender, - }, - }; - }, - markStale: () => ({ updateRenderedChildren: true }), + let processResult = processAssignNames({ + assignNames: component.doenetAttributes.assignNames, + serializedComponents: replacements, + parentName: component.componentName, + parentCreatesNewNamespace: newNamespace, + componentInfoObjects, + }); + errors.push(...processResult.errors); + warnings.push(...processResult.warnings); + + workspace.componentsCopied = componentsCopied; + + return { + replacements: processResult.serializedComponents, + errors, + warnings, }; + } - return stateVariableDefinitions; + static async calculateReplacementChanges({ + component, + components, + componentInfoObjects, + workspace, + }) { + // TODO: don't yet have a way to return errors and warnings! + let errors = []; + let warnings = []; + + let componentsToCopy = []; + + let childNameByComponent = + await component.stateValues.childNameByComponent; + + for (let childName of childNameByComponent) { + let replacementSource = components[childName]; + + if (replacementSource) { + componentsToCopy.push(replacementSource.componentName); + } + } + + if ( + componentsToCopy.length == workspace.componentsCopied.length && + workspace.componentsCopied.every( + (x, i) => x === componentsToCopy[i], + ) + ) { + return []; + } + + // for now, just recreate + let replacementResults = await this.createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }); + + let replacements = replacementResults.replacements; + errors.push(...replacementResults.errors); + warnings.push(...replacementResults.warnings); + + let replacementChanges = [ + { + changeType: "add", + changeTopLevelReplacements: true, + firstReplacementInd: 0, + numberReplacementsToReplace: component.replacements.length, + serializedReplacements: replacements, + }, + ]; + + return replacementChanges; } } diff --git a/packages/doenetml-worker/src/components/Copy.js b/packages/doenetml-worker/src/components/Copy.js index 6296f7e7a..6dec195c6 100644 --- a/packages/doenetml-worker/src/components/Copy.js +++ b/packages/doenetml-worker/src/components/Copy.js @@ -1894,6 +1894,18 @@ export default class Copy extends CompositeComponent { dontSkipAttributes: ["asList"], compositeCreatesNewNamespace: newNamespace, }); + + // Since if either displayDigits or displayDecimals is supplied in the composite, + // it should override both displayDigits and displayDecimals from the source, + // we delete the attributes from the source in this special case. + // TODO: is there a more generic way to accomplish this? + if ( + attributesFromComposite.displayDigits || + attributesFromComposite.displayDecimals + ) { + delete repl.attributes.displayDigits; + delete repl.attributes.displayDecimals; + } Object.assign(repl.attributes, attributesFromComposite); } diff --git a/packages/doenetml-worker/src/components/Function.js b/packages/doenetml-worker/src/components/Function.js index 838bb6eda..68ec75dca 100644 --- a/packages/doenetml-worker/src/components/Function.js +++ b/packages/doenetml-worker/src/components/Function.js @@ -258,7 +258,7 @@ export default class Function extends InlineComponent { }; let roundingDefinitions = returnRoundingStateVariableDefinitions({ - childsGroupIfSingleMatch: ["functions"], + childGroupsIfSingleMatch: ["functions"], }); Object.assign(stateVariableDefinitions, roundingDefinitions); diff --git a/packages/doenetml-worker/src/components/Math.js b/packages/doenetml-worker/src/components/Math.js index 47a6ae908..d4f39b8ac 100644 --- a/packages/doenetml-worker/src/components/Math.js +++ b/packages/doenetml-worker/src/components/Math.js @@ -27,6 +27,8 @@ import { unicodeToSuperSubscripts, preprocessMathInverseDefinition, } from "../utils/math"; +import { createInputStringFromChildren } from "../utils/parseMath"; +import { returnMathVectorMatrixStateVariableDefinitions } from "../utils/mathVectorMatrixStateVariables"; const vectorAndListOperators = ["list", ...vectorOperators]; @@ -113,6 +115,7 @@ export default class MathComponent extends InlineComponent { defaultValue: ["f", "g"], public: true, fallBackToParentStateVariable: "functionSymbols", + fallBackToSourceCompositeStateVariable: "functionSymbols", }; attributes.sourcesAreFunctionSymbols = { @@ -120,6 +123,7 @@ export default class MathComponent extends InlineComponent { createStateVariable: "sourcesAreFunctionSymbols", defaultValue: [], fallBackToParentStateVariable: "sourcesAreFunctionSymbols", + fallBackToSourceCompositeStateVariable: "sourcesAreFunctionSymbols", }; attributes.splitSymbols = { @@ -128,6 +132,7 @@ export default class MathComponent extends InlineComponent { defaultValue: true, public: true, fallBackToParentStateVariable: "splitSymbols", + fallBackToSourceCompositeStateVariable: "splitSymbols", }; attributes.parseScientificNotation = { @@ -198,9 +203,8 @@ export default class MathComponent extends InlineComponent { Object.assign(stateVariableDefinitions, anchorDefinition); let roundingDefinitions = returnRoundingStateVariableDefinitions({ - childsGroupIfSingleMatch: ["maths"], + childGroupsIfSingleMatch: ["maths"], childGroupsToStopSingleMatch: ["strings"], - includeListParents: true, }); Object.assign(stateVariableDefinitions, roundingDefinitions); @@ -247,10 +251,25 @@ export default class MathComponent extends InlineComponent { childGroups: ["maths"], variableNames: ["unordered"], }, + sourceCompositeUnordered: { + dependencyType: "sourceCompositeStateVariable", + variableName: "unordered", + }, }), - definition({ dependencyValues }) { + definition({ dependencyValues, usedDefault }) { if (dependencyValues.unorderedAttr === null) { - if (dependencyValues.mathChildren.length > 0) { + if ( + dependencyValues.sourceCompositeUnordered !== null && + !usedDefault.sourceCompositeUnordered + ) { + return { + setValue: { + unordered: Boolean( + dependencyValues.sourceCompositeUnordered, + ), + }, + }; + } else if (dependencyValues.mathChildren.length > 0) { let unordered = dependencyValues.mathChildren.every( (x) => x.stateValues.unordered, ); @@ -710,33 +729,12 @@ export default class MathComponent extends InlineComponent { dependencyType: "stateVariable", variableName: "expand", }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "mathList", - variableName: "numComponentsToDisplayByChild", - }, }), - definition: function ({ dependencyValues, componentName }) { + definition: function ({ dependencyValues }) { let value = dependencyValues.value; - if ( - dependencyValues.parentNComponentsToDisplayByChild?.[ - componentName - ] > 0 - ) { - // math is a list inside a mathList that is displaying only a fraction of the list - value = me.fromAst( - value.tree.slice( - 0, - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ] + 1, - ), - ); - } - // for display via latex and text, round any decimal numbers to the significant digits - // determined by displaydigits, displaydecimals, and/or displaySmallAsZero + // determined by displayDigits, displayDecimals, and/or displaySmallAsZero let rounded = roundForDisplay({ value, dependencyValues, @@ -1078,630 +1076,10 @@ export default class MathComponent extends InlineComponent { }, }; - stateVariableDefinitions.numDimensions = { - public: true, - shadowingInstructions: { - createComponentOfType: "integer", - }, - returnDependencies: () => ({ - value: { - dependencyType: "stateVariable", - variableName: "value", - }, - }), - definition({ dependencyValues }) { - let numDimensions = 1; - - let tree = dependencyValues.value.tree; - - if (Array.isArray(tree)) { - if (vectorAndListOperators.includes(tree[0])) { - numDimensions = tree.length - 1; - } else if (tree[0] === "matrix") { - let size = tree[1].slice(1); - - if (size[0] === 1) { - numDimensions = size[1]; - } else if (size[1] === 1) { - numDimensions = size[0]; - } - } else if ( - vectorOperators.includes(tree[1][0]) && - ((tree[0] === "^" && tree[2] === "T") || - tree[0] === "prime") - ) { - numDimensions = tree[1].length - 1; - } - } - - return { setValue: { numDimensions } }; - }, - }; - - stateVariableDefinitions.vector = { - public: true, - shadowingInstructions: { - createComponentOfType: "math", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), - returnWrappingComponents(prefix) { - if (prefix === "x") { - return []; - } else { - // entire array - // wrap by both and - return [ - [ - "vector", - { - componentType: "mathList", - isAttributeNamed: "xs", - }, - ], - ]; - } - }, - }, - isArray: true, - entryPrefixes: ["x"], - returnArraySizeDependencies: () => ({ - numDimensions: { - dependencyType: "stateVariable", - variableName: "numDimensions", - }, - }), - returnArraySize({ dependencyValues }) { - return [dependencyValues.numDimensions]; - }, - returnArrayDependenciesByKey() { - let globalDependencies = { - value: { - dependencyType: "stateVariable", - variableName: "value", - }, - }; - return { globalDependencies }; - }, - arrayDefinitionByKey({ globalDependencyValues, arraySize }) { - let tree = globalDependencyValues.value.tree; - - let createdVector = false; - - let vector = {}; - if (Array.isArray(tree)) { - if (vectorAndListOperators.includes(tree[0])) { - for (let ind = 0; ind < arraySize[0]; ind++) { - vector[ind] = me.fromAst(tree[ind + 1]); - } - createdVector = true; - } else if (tree[0] === "matrix") { - let size = tree[1].slice(1); - if (size[0] === 1) { - for (let ind = 0; ind < arraySize[0]; ind++) { - vector[ind] = me.fromAst(tree[2][1][ind + 1]); - } - createdVector = true; - } else if (size[1] === 1) { - for (let ind = 0; ind < arraySize[0]; ind++) { - vector[ind] = me.fromAst(tree[2][ind + 1][1]); - } - createdVector = true; - } - } else if ( - vectorOperators.includes(tree[1][0]) && - ((tree[0] === "^" && tree[2] === "T") || - tree[0] === "prime") - ) { - for (let ind = 0; ind < arraySize[0]; ind++) { - vector[ind] = me.fromAst(tree[1][ind + 1]); - } - createdVector = true; - } - } - if (!createdVector) { - vector[0] = globalDependencyValues.value; - } - - return { setValue: { vector } }; - }, - async inverseArrayDefinitionByKey({ - desiredStateVariableValues, - globalDependencyValues, - stateValues, - workspace, - arraySize, - }) { - // in case just one ind specified, merge with previous values - if (!workspace.desiredVector) { - workspace.desiredVector = []; - } - for (let ind = 0; ind < arraySize[0]; ind++) { - if (desiredStateVariableValues.vector[ind] !== undefined) { - workspace.desiredVector[ind] = - convertValueToMathExpression( - desiredStateVariableValues.vector[ind], - ); - } else if (workspace.desiredVector[ind] === undefined) { - workspace.desiredVector[ind] = ( - await stateValues.vector - )[ind]; - } - } - - let desiredValue; - let tree = globalDependencyValues.value.tree; - if (Array.isArray(tree)) { - if (vectorAndListOperators.includes(tree[0])) { - desiredValue = me.fromAst([ - tree[0], - ...workspace.desiredVector.map((x) => x.tree), - ]); - } else if (tree[0] === "matrix") { - let size = tree[1].slice(1); - if (size[0] === 1) { - let desiredMatrixVals = ["tuple"]; - for (let ind = 0; ind < arraySize[0]; ind++) { - desiredMatrixVals.push( - workspace.desiredVector[ind], - ); - } - desiredMatrixVals = ["tuple", desiredMatrixVals]; - desiredValue = me.fromAst([ - "matrix", - tree[1], - desiredMatrixVals, - ]); - } else if (size[1] === 1) { - let desiredMatrixVals = ["tuple"]; - for (let ind = 0; ind < arraySize[0]; ind++) { - desiredMatrixVals.push([ - "tuple", - workspace.desiredVector[ind], - ]); - } - desiredValue = me.fromAst([ - "matrix", - tree[1], - desiredMatrixVals, - ]); - } - } else if ( - vectorOperators.includes(tree[1][0]) && - ((tree[0] === "^" && tree[2] === "T") || - tree[0] === "prime") - ) { - desiredValue = [ - tree[0], - [ - tree[1][0], - ...workspace.desiredVector.map((x) => x.tree), - ], - ]; - if (tree[2]) { - desiredValue.push(tree[2]); - } - desiredValue = me.fromAst(desiredValue); - } - } - - if (!desiredValue) { - desiredValue = workspace.desiredVector[0]; - } - - let instructions = [ - { - setDependency: "value", - desiredValue, - }, - ]; - - return { - success: true, - instructions, - }; - }, - }; - - stateVariableDefinitions.x = { - isAlias: true, - targetVariableName: "x1", - }; - - stateVariableDefinitions.y = { - isAlias: true, - targetVariableName: "x2", - }; - - stateVariableDefinitions.z = { - isAlias: true, - targetVariableName: "x3", - }; - - stateVariableDefinitions.matrixSize = { - public: true, - shadowingInstructions: { - createComponentOfType: "numberList", - }, - returnDependencies: () => ({ - value: { - dependencyType: "stateVariable", - variableName: "value", - }, - }), - definition({ dependencyValues }) { - let matrixSize = [1, 1]; - - let tree = dependencyValues.value.tree; - - if (Array.isArray(tree)) { - if (vectorAndListOperators.includes(tree[0])) { - matrixSize = [tree.length - 1, 1]; - } else if (tree[0] === "matrix") { - matrixSize = tree[1].slice(1); - } else if ( - vectorOperators.includes(tree[1][0]) && - ((tree[0] === "^" && tree[2] === "T") || - tree[0] === "prime") - ) { - matrixSize = [1, tree[1].length - 1]; - } - } - - return { setValue: { matrixSize } }; - }, - }; - - stateVariableDefinitions.numRows = { - public: true, - shadowingInstructions: { - createComponentOfType: "integer", - }, - returnDependencies: () => ({ - matrixSize: { - dependencyType: "stateVariable", - variableName: "matrixSize", - }, - }), - definition({ dependencyValues }) { - return { - setValue: { numRows: dependencyValues.matrixSize[0] }, - }; - }, - }; - - stateVariableDefinitions.numColumns = { - public: true, - shadowingInstructions: { - createComponentOfType: "integer", - }, - returnDependencies: () => ({ - matrixSize: { - dependencyType: "stateVariable", - variableName: "matrixSize", - }, - }), - definition({ dependencyValues }) { - return { - setValue: { numColumns: dependencyValues.matrixSize[1] }, - }; - }, - }; - - stateVariableDefinitions.matrix = { - public: true, - shadowingInstructions: { - createComponentOfType: "math", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), - returnWrappingComponents(prefix) { - if (prefix === "matrixEntry") { - return []; - } else if (prefix === "row") { - return [["matrix", "matrixRow"]]; - } else if (prefix === "column") { - return [["matrix", "matrixColumn"]]; - } else { - // entire matrix - // wrap inner dimension by matrixRow and outer dimension by matrix - return [["matrixRow"], ["matrix"]]; - } - }, - }, - isArray: true, - numDimensions: 2, - entryPrefixes: ["matrixEntry", "row", "column", "rows", "columns"], - returnEntryDimensions: (prefix) => { - if (prefix === "matrixEntry") { - return 0; - } else if (prefix === "rows" || prefix === "columns") { - return 2; - } else { - return 1; - } - }, - getArrayKeysFromVarName({ - arrayEntryPrefix, - varEnding, - arraySize, - }) { - if (arrayEntryPrefix === "matrixEntry") { - // matrixEntry1_2 is the 2nd entry from the first row - let indices = varEnding - .split("_") - .map((x) => Number(x) - 1); - if ( - indices.length === 2 && - indices.every((x) => Number.isInteger(x) && x >= 0) - ) { - if (arraySize) { - if (indices.every((x, i) => x < arraySize[i])) { - return [String(indices)]; - } else { - return []; - } - } else { - // If not given the array size, - // then return the array keys assuming the array is large enough. - // Must do this as it is used to determine potential array entries. - return [String(indices)]; - } - } else { - return []; - } - } else if (arrayEntryPrefix === "row") { - // row3 is all components of the third row - - let rowInd = Number(varEnding) - 1; - if (!(Number.isInteger(rowInd) && rowInd >= 0)) { - return []; - } - - if (!arraySize) { - // If don't have array size, we just need to determine if it is a potential entry. - // Return the first entry assuming array is large enough - return [rowInd + ",0"]; - } - if (rowInd < arraySize[0]) { - // array of "rowInd,i", where i=0, ..., arraySize[1]-1 - return Array.from( - Array(arraySize[1]), - (_, i) => rowInd + "," + i, - ); - } else { - return []; - } - } else if (arrayEntryPrefix === "column") { - // column3 is all components of the third column - - let colInd = Number(varEnding) - 1; - if (!(Number.isInteger(colInd) && colInd >= 0)) { - return []; - } - - if (!arraySize) { - // If don't have array size, we just need to determine if it is a potential entry. - // Return the first entry assuming array is large enough - return ["0," + colInd]; - } - if (colInd < arraySize[1]) { - // array of "i,colInd", where i=0, ..., arraySize[1]-1 - return Array.from( - Array(arraySize[0]), - (_, i) => i + "," + colInd, - ); - } else { - return []; - } - } else if ( - arrayEntryPrefix === "rows" || - arrayEntryPrefix === "columns" - ) { - // rows or columns is the whole matrix - // (this are designed for getting rows and columns using propIndex) - // (rows and matrix are the same, but rows is added to be parallel to columns) - - if (!arraySize) { - // If don't have array size, we justr eturn the first entry - return ["0,0"]; - } - let keys = []; - for (let rowInd = 0; rowInd < arraySize[0]; rowInd++) { - keys.push( - ...Array.from( - Array(arraySize[1]), - (_, i) => rowInd + "," + i, - ), - ); - } - return keys; - } - }, - arrayVarNameFromPropIndex(propIndex, varName) { - if (varName === "matrix" || varName === "rows") { - if (propIndex.length === 1) { - return "row" + propIndex[0]; - } else { - // if propIndex has additional entries, ignore them - return `matrixEntry${propIndex[0]}_${propIndex[1]}`; - } - } - if (varName === "columns") { - if (propIndex.length === 1) { - return "column" + propIndex[0]; - } else { - // if propIndex has additional entries, ignore them - return `matrixEntry${propIndex[1]}_${propIndex[0]}`; - } - } - if (varName.slice(0, 3) === "row") { - let rowNum = Number(varName.slice(3)); - if (Number.isInteger(rowNum) && rowNum > 0) { - // if propIndex has additional entries, ignore them - return `matrixEntry${rowNum}_${propIndex[0]}`; - } - } - if (varName.slice(0, 6) === "column") { - let colNum = Number(varName.slice(6)); - if (Number.isInteger(colNum) && colNum > 0) { - // if propIndex has additional entries, ignore them - return `matrixEntry${propIndex[0]}_${colNum}`; - } - } - return null; - }, - returnArraySizeDependencies: () => ({ - matrixSize: { - dependencyType: "stateVariable", - variableName: "matrixSize", - }, - }), - returnArraySize({ dependencyValues }) { - return dependencyValues.matrixSize; - }, - returnArrayDependenciesByKey() { - let globalDependencies = { - value: { - dependencyType: "stateVariable", - variableName: "value", - }, - }; - return { globalDependencies }; - }, - arrayDefinitionByKey({ globalDependencyValues, arraySize }) { - let tree = globalDependencyValues.value.tree; - - let createdMatrix = false; - - let matrix = {}; - if (Array.isArray(tree)) { - if (vectorAndListOperators.includes(tree[0])) { - for (let ind = 0; ind < arraySize[0]; ind++) { - matrix[ind + ",0"] = me.fromAst(tree[ind + 1]); - } - createdMatrix = true; - } else if (tree[0] === "matrix") { - let matVals = tree[2]; - for (let i = 0; i < arraySize[0]; i++) { - for (let j = 0; j < arraySize[1]; j++) { - matrix[`${i},${j}`] = me.fromAst( - matVals[i + 1][j + 1], - ); - } - } - createdMatrix = true; - } else if ( - vectorOperators.includes(tree[1][0]) && - ((tree[0] === "^" && tree[2] === "T") || - tree[0] === "prime") - ) { - for (let ind = 0; ind < arraySize[1]; ind++) { - matrix["0," + ind] = me.fromAst(tree[1][ind + 1]); - } - createdMatrix = true; - } - } - if (!createdMatrix) { - matrix["0,0"] = globalDependencyValues.value; - } - - return { setValue: { matrix } }; - }, - async inverseArrayDefinitionByKey({ - desiredStateVariableValues, - globalDependencyValues, - stateValues, - workspace, - arraySize, - }) { - // in case just one ind specified, merge with previous values - if (!workspace.desiredMatrix) { - workspace.desiredMatrix = []; - } - for (let i = 0; i < arraySize[0]; i++) { - for (let j = 0; j < arraySize[1]; j++) { - let arrayKey = i + "," + j; - if ( - desiredStateVariableValues.matrix[arrayKey] !== - undefined - ) { - workspace.desiredMatrix[arrayKey] = - convertValueToMathExpression( - desiredStateVariableValues.matrix[arrayKey], - ); - } else if ( - workspace.desiredMatrix[arrayKey] === undefined - ) { - workspace.desiredMatrix[arrayKey] = ( - await stateValues.matrix - )[i][j]; - } - } - } - - let desiredValue; - let tree = globalDependencyValues.value.tree; - if (Array.isArray(tree)) { - if (vectorAndListOperators.includes(tree[0])) { - desiredValue = [tree[0]]; - for (let ind = 0; ind < arraySize[0]; ind++) { - desiredValue.push( - workspace.desiredMatrix[ind + ",0"].tree, - ); - } - } else if (tree[0] === "matrix") { - let desiredMatrixVals = ["tuple"]; - - for (let i = 0; i < arraySize[0]; i++) { - let row = ["tuple"]; - for (let j = 0; j < arraySize[1]; j++) { - row.push( - workspace.desiredMatrix[`${i},${j}`].tree, - ); - } - desiredMatrixVals.push(row); - } - desiredValue = me.fromAst([ - "matrix", - tree[1], - desiredMatrixVals, - ]); - } else if ( - vectorOperators.includes(tree[1][0]) && - ((tree[0] === "^" && tree[2] === "T") || - tree[0] === "prime") - ) { - desiredValue = [tree[0]]; - let desiredVector = [tree[1][0]]; - for (let ind = 0; ind < arraySize[1]; ind++) { - desiredVector.push( - workspace.desiredMatrix["0," + ind].tree, - ); - } - desiredValue = [tree[0], desiredVector]; - if (tree[2]) { - desiredValue.push(tree[2]); - } - desiredValue = me.fromAst(desiredValue); - } - } - - if (!desiredValue) { - desiredValue = workspace.desiredMatrix["0,0"]; - } - - let instructions = [ - { - setDependency: "value", - desiredValue, - }, - ]; - - return { - success: true, - instructions, - }; - }, - }; + Object.assign( + stateVariableDefinitions, + returnMathVectorMatrixStateVariableDefinitions(), + ); return stateVariableDefinitions; } @@ -1834,14 +1212,6 @@ function calculateExpressionWithCodes({ dependencyValues, changes }) { } } - let inputString = createInputStringFromChildren({ - stringMathChildren: dependencyValues.stringMathChildren, - codePre: dependencyValues.codePre, - format: dependencyValues.format, - }); - - let expressionWithCodes = null; - let functionSymbols = [...dependencyValues.functionSymbols]; functionSymbols.push( ...dependencyValues.mathChildrenFunctionSymbols.map( @@ -1849,340 +1219,51 @@ function calculateExpressionWithCodes({ dependencyValues, changes }) { ), ); - if (inputString === "") { - expressionWithCodes = me.fromAst("\uFF3F"); // long underscore - } else { - if (dependencyValues.format === "text") { - let fromText = getTextToMathConverter({ - functionSymbols, - splitSymbols: dependencyValues.splitSymbols, - parseScientificNotation: - dependencyValues.parseScientificNotation, - }); - try { - expressionWithCodes = fromText(inputString); - } catch (e) { - expressionWithCodes = me.fromAst("\uFF3F"); // long underscore - console.log( - "Invalid value for a math of text format: " + inputString, - ); - } - } else if (dependencyValues.format === "latex") { - let fromLatex = getLatexToMathConverter({ - functionSymbols, - splitSymbols: dependencyValues.splitSymbols, - parseScientificNotation: - dependencyValues.parseScientificNotation, - }); - try { - expressionWithCodes = fromLatex(inputString); - } catch (e) { - expressionWithCodes = me.fromAst("\uFF3F"); // long underscore - console.log( - "Invalid value for a math of latex format: " + inputString, - ); - } - } - } + let parser; - return { - setValue: { expressionWithCodes }, - setEssentialValue: { expressionWithCodes }, - }; -} - -// concatenate strings and with a numbered code for each math child -// (that will be parsed to form expression with codes) -// If compositeReplacementAsList is true, -// then add commas betweeen the components that are all from one composite, -// if that composite has asList set to true. -// Put parens around that list in some cases, as described below. -function createInputStringFromChildren({ - stringMathChildren, - codePre, - format, -}) { - let mathInd = 0; - let mathIndByChild = []; - for (let child of stringMathChildren) { - if (typeof child === "string") { - mathIndByChild.push(null); - } else { - mathIndByChild.push(mathInd); - mathInd++; - } + if (dependencyValues.format === "text") { + parser = getTextToMathConverter({ + functionSymbols, + splitSymbols: dependencyValues.splitSymbols, + parseScientificNotation: dependencyValues.parseScientificNotation, + }); + } else if (dependencyValues.format === "latex") { + parser = getLatexToMathConverter({ + functionSymbols, + splitSymbols: dependencyValues.splitSymbols, + parseScientificNotation: dependencyValues.parseScientificNotation, + }); } - let result = createInputStringFromChildrenSub({ - compositeReplacementRange: stringMathChildren.compositeReplacementRange, - stringMathChildren, - startInd: 0, - endInd: stringMathChildren.length - 1, - mathIndByChild, - format, - codePre, + let stringResults = createInputStringFromChildren({ + children: dependencyValues.stringMathChildren, + codePre: dependencyValues.codePre, + format: dependencyValues.format, + parser, }); - return result.newChildren.join(""); -} - -function createInputStringFromChildrenSub({ - compositeReplacementRange, - stringMathChildren, - startInd, - endInd, - mathIndByChild, - format, - codePre, - potentialListComponents = null, -}) { - let newChildren = []; - let newPotentialListComponents = []; - let lastChildInd = startInd - 1; - - for ( - let rangeInd = 0; - rangeInd < compositeReplacementRange.length; - rangeInd++ - ) { - let range = compositeReplacementRange[rangeInd]; - - let rangeFirstInd = range.firstInd; - let rangeLastInd = range.lastInd; - - if (rangeFirstInd > lastChildInd && rangeLastInd <= endInd) { - if (lastChildInd + 1 < rangeFirstInd) { - for (let ind = lastChildInd + 1; ind < rangeFirstInd; ind++) { - // we are not grouping these children - // but we are separately turning each one into a string - // (turning the math children into a code based on codePre and its mathInd) - newChildren.push( - baseStringFromChildren({ - stringMathChildren, - startInd: ind, - endInd: ind, - mathIndByChild, - format, - codePre, - }), - ); - } - if (potentialListComponents) { - // Since we didn't change the components, - // their status of being a potential list component is not changed - newPotentialListComponents.push( - ...potentialListComponents.slice( - lastChildInd - startInd + 1, - rangeFirstInd - startInd, - ), - ); - } - } - - // If a composite produced composites that produced children, - // then this outer composite is first in the array of replacement ranges. - // We first process the children corresponding to any of these replacement composites, - // which will concatenate the replacements of each composite into a single text, - // which may be be turned into a list according to the settings of that composite. - - // We remove the replacement range of the current composite (any all earlier ones) - let subReplacementRange = compositeReplacementRange.slice( - rangeInd + 1, - ); - - let { - newChildren: childrenInRange, - newPotentialListComponents: potentialListComponentsInRange, - } = createInputStringFromChildrenSub({ - compositeReplacementRange: subReplacementRange, - stringMathChildren, - startInd: rangeFirstInd, - endInd: rangeLastInd, - mathIndByChild, - format, - codePre, - potentialListComponents: range.potentialListComponents, - }); - - let allAreListComponents = potentialListComponentsInRange.every( - (x) => x, - ); + let inputString = stringResults.string; - if ( - range.asList && - allAreListComponents && - childrenInRange.length > 1 - ) { - // add commas between all children from a single composite - let listString = childrenInRange - .filter((v) => v.trim() !== "") - .map((v, i, a) => (i === a.length - 1 ? v : v.trimEnd())) - .join(", "); - - // The following implements the logic to determine if this comma-separated list - // should be wrapped by parens. - // Wrap with parens if the lists is surrounded by a non-delimiter on either side. - // The parens will generally turn the list into a tuple (or to arguments of a function) - // when it is parsed into a math-expression. - - let wrap = false; - - // First check if there is a non-delimiter to the left - if (rangeFirstInd > 0) { - let prevInd = rangeFirstInd - 1; - while (prevInd >= 0) { - let prevChild = stringMathChildren[prevInd]; - if (typeof prevChild === "string") { - prevChild = prevChild.trim(); - if (prevChild.length > 0) { - if ( - !["{", "[", "(", "|", ","].includes( - prevChild[prevChild.length - 1], - ) - ) { - // The string to the left did not contain one of the delimiters, - // so we must wrap the list. - wrap = true; - } - break; - } - } else { - // There is a math child to the left, - // so we must wrap the list - wrap = true; - } - } - } - - if (!wrap) { - // Since we didn't have a non-delimiter to the left, - // check if there is a non-delimiter to the right. - if (rangeLastInd < stringMathChildren.length - 1) { - let nextInd = rangeLastInd + 1; - while (nextInd <= stringMathChildren.length - 1) { - let nextChild = stringMathChildren[nextInd]; - if (typeof nextChild === "string") { - nextChild = nextChild.trim(); - if (nextChild.length > 0) { - let nextChar = nextChild[0]; - // If the format is latex, - // the delimiter could be escaped by a \ - if ( - format === "latex" && - nextChar === "\\" && - nextChild.length > 1 - ) { - nextChar = nextChild[1]; - } - if ( - !["}", "]", ")", "|", ","].includes( - nextChar, - ) - ) { - // The string to the right did not contain one of the delimiters, - // so we must wrap the list. - wrap = true; - } - break; - } - } else { - // There is a math child to the right, - // so we must wrap the list - wrap = true; - } - } - } - } - - if (wrap) { - listString = "(" + listString + ")"; - } - - newChildren.push(listString); - } else { - // We are not turning the children in a list, - // so just concatentate the strings - newChildren.push(childrenInRange.join("")); - } - - if (potentialListComponents) { - // record whether the result from the composite (a single string now) - // should be considered a list component for any outer composite - newPotentialListComponents.push(allAreListComponents); - } - lastChildInd = rangeLastInd; - } - } - - if (lastChildInd < endInd) { - for (let ind = lastChildInd + 1; ind <= endInd; ind++) { - // we are not grouping these children - // but we are separately turning each one into a string - // (turning the math children into a code based on codePre and its mathInd) - newChildren.push( - baseStringFromChildren({ - stringMathChildren, - startInd: ind, - endInd: ind, - mathIndByChild, - format, - codePre, - }), - ); - } + let expressionWithCodes = null; - if (potentialListComponents) { - // Since we didn't change the components, - // their status of being a potential list component is not changed - newPotentialListComponents.push( - ...potentialListComponents.slice( - lastChildInd - startInd + 1, - endInd - startInd + 1, - ), + if (inputString === "") { + expressionWithCodes = me.fromAst("\uFF3F"); // long underscore + } else { + try { + expressionWithCodes = parser(inputString); + } catch (e) { + expressionWithCodes = me.fromAst("\uFF3F"); // long underscore + console.log( + `Invalid value for a math of ${dependencyValues.format} format: ` + + inputString, ); } } - return { newChildren, newPotentialListComponents }; -} - -// concatenate string children and codes from math-children -// into a single string to be parsed into a math component -function baseStringFromChildren({ - stringMathChildren, - startInd, - endInd, - mathIndByChild, - format, - codePre, -}) { - let str = ""; - - for (let ind = startInd; ind <= endInd; ind++) { - let child = stringMathChildren[ind]; - if (typeof child === "string") { - str += " " + child + " "; - } else { - // a math - let code = codePre + mathIndByChild[ind]; - - let nextString; - if (format === "latex") { - // for latex, must explicitly denote that code - // is a multicharacter variable - nextString = "\\operatorname{" + code + "}"; - } else { - // for text, just make sure code is surrounded by spaces - // (the presence of numbers inside code will ensure that - // it is parsed as a multicharcter variable) - nextString = " " + code + " "; - } - - str += nextString; - } - } - - return str; + return { + setValue: { expressionWithCodes }, + setEssentialValue: { expressionWithCodes }, + }; } function calculateMathValue({ dependencyValues } = {}) { @@ -2477,7 +1558,7 @@ function checkForLinearExpression( return { foundLinear: false }; } - // if at least one componen is a linear functions, view as linear + // if at least one component is a linear functions, view as linear result.foundLinear = true; return result; } else { diff --git a/packages/doenetml-worker/src/components/MathInput.js b/packages/doenetml-worker/src/components/MathInput.js index 3144e485e..77e592ed0 100644 --- a/packages/doenetml-worker/src/components/MathInput.js +++ b/packages/doenetml-worker/src/components/MathInput.js @@ -16,6 +16,7 @@ import { roundForDisplay, stripLatex, } from "../utils/math"; +import { returnMathVectorMatrixStateVariableDefinitions } from "../utils/mathVectorMatrixStateVariables"; export default class MathInput extends Input { constructor(args) { @@ -146,6 +147,7 @@ export default class MathInput extends Input { sugarInstructions.push({ replacementFunction: returnWrapNonLabelsSugarFunction({ wrappingComponentType: "math", + wrapSingleIfNotWrappingComponentType: true, }), }); @@ -795,6 +797,11 @@ export default class MathInput extends Input { definition: () => ({ setValue: { componentType: "math" } }), }; + Object.assign( + stateVariableDefinitions, + returnMathVectorMatrixStateVariableDefinitions(), + ); + return stateVariableDefinitions; } diff --git a/packages/doenetml-worker/src/components/MathList.js b/packages/doenetml-worker/src/components/MathList.js index f81410747..65ce2e8f0 100644 --- a/packages/doenetml-worker/src/components/MathList.js +++ b/packages/doenetml-worker/src/components/MathList.js @@ -1,17 +1,21 @@ -import InlineComponent from "./abstract/InlineComponent"; +import CompositeComponent from "./abstract/CompositeComponent"; import me from "math-expressions"; import { returnGroupIntoComponentTypeSeparatedBySpacesOutsideParens } from "./commonsugar/lists"; import { convertValueToMathExpression } from "@doenet/utils"; +import { returnRoundingAttributes } from "../utils/rounding"; import { - returnRoundingAttributeComponentShadowing, - returnRoundingAttributes, - returnRoundingStateVariableDefinitions, -} from "../utils/rounding"; -import { roundForDisplay } from "../utils/math"; + convertAttributesForComponentType, + postProcessCopy, +} from "../utils/copy"; +import { processAssignNames } from "../utils/naming"; -export default class MathList extends InlineComponent { +export default class MathList extends CompositeComponent { static componentType = "mathList"; - static renderChildren = true; + + static stateVariableToEvaluateAfterReplacements = + "readyToExpandWhenResolved"; + + static assignNamesToReplacements = true; static includeBlankStringChildren = true; static removeBlankStringChildrenPostSugar = true; @@ -25,6 +29,8 @@ export default class MathList extends InlineComponent { // don't required composite replacements static descendantCompositesMustHaveAReplacement = false; + static doNotExpandAsShadowed = true; + static createAttributesObject() { let attributes = super.createAttributesObject(); @@ -37,14 +43,26 @@ export default class MathList extends InlineComponent { attributes.maxNumber = { createComponentOfType: "number", createStateVariable: "maxNumber", - defaultValue: null, + defaultValue: Infinity, public: true, }; attributes.mergeMathLists = { createComponentOfType: "boolean", }; - Object.assign(attributes, returnRoundingAttributes()); + attributes.fixed = { + leaveRaw: true, + }; + + attributes.isResponse = { + leaveRaw: true, + }; + + for (let attrName in returnRoundingAttributes()) { + attributes[attrName] = { + leaveRaw: true, + }; + } attributes.functionSymbols = { createComponentOfType: "textList", @@ -52,6 +70,7 @@ export default class MathList extends InlineComponent { defaultValue: ["f", "g"], public: true, fallBackToParentStateVariable: "functionSymbols", + fallBackToSourceCompositeStateVariable: "functionSymbols", }; attributes.sourcesAreFunctionSymbols = { @@ -59,6 +78,7 @@ export default class MathList extends InlineComponent { createStateVariable: "sourcesAreFunctionSymbols", defaultValue: [], fallBackToParentStateVariable: "sourcesAreFunctionSymbols", + fallBackToSourceCompositeStateVariable: "sourcesAreFunctionSymbols", }; attributes.splitSymbols = { @@ -67,6 +87,7 @@ export default class MathList extends InlineComponent { defaultValue: true, public: true, fallBackToParentStateVariable: "splitSymbols", + fallBackToSourceCompositeStateVariable: "splitSymbols", }; attributes.parseScientificNotation = { @@ -106,29 +127,12 @@ export default class MathList extends InlineComponent { group: "maths", componentTypes: ["math"], }, - { - group: "mathLists", - componentTypes: ["mathList"], - }, ]; } static returnStateVariableDefinitions() { let stateVariableDefinitions = super.returnStateVariableDefinitions(); - Object.assign( - stateVariableDefinitions, - returnRoundingStateVariableDefinitions(), - ); - - // set overrideChildHide so that children are hidden - // only based on whether or not the list is hidden - // so that can't have a list with partially hidden components - stateVariableDefinitions.overrideChildHide = { - returnDependencies: () => ({}), - definition: () => ({ setValue: { overrideChildHide: true } }), - }; - stateVariableDefinitions.mathsShadow = { defaultValue: null, hasEssential: true, @@ -140,6 +144,13 @@ export default class MathList extends InlineComponent { }), }; + stateVariableDefinitions.asList = { + returnDependencies: () => ({}), + definition() { + return { setValue: { asList: true } }; + }, + }; + stateVariableDefinitions.mergeMathLists = { public: true, shadowingInstructions: { @@ -151,11 +162,6 @@ export default class MathList extends InlineComponent { attributeName: "mergeMathLists", variableNames: ["value"], }, - mathListChildren: { - dependencyType: "child", - childGroups: ["mathLists"], - skipComponentNames: true, - }, mathChildren: { dependencyType: "child", childGroups: ["maths"], @@ -165,8 +171,7 @@ export default class MathList extends InlineComponent { definition({ dependencyValues }) { let mergeMathLists = dependencyValues.mergeMathListsAttr?.stateValues.value || - (dependencyValues.mathListChildren.length === 0 && - dependencyValues.mathChildren.length === 1); + dependencyValues.mathChildren.length === 1; return { setValue: { mergeMathLists } }; }, }; @@ -177,7 +182,7 @@ export default class MathList extends InlineComponent { createComponentOfType: "number", }, stateVariablesDeterminingDependencies: ["mergeMathLists"], - additionalStateVariablesDefined: ["childIndexByArrayKey"], + additionalStateVariablesDefined: ["childInfoByComponent"], returnDependencies({ stateValues }) { let dependencies = { maxNumber: { @@ -195,149 +200,91 @@ export default class MathList extends InlineComponent { }; if (stateValues.mergeMathLists) { - dependencies.mathAndMathListChildren = { + dependencies.mathChildren = { dependencyType: "child", - childGroups: ["maths", "mathLists"], - variableNames: ["value", "numComponents"], - variablesOptional: true, + childGroups: ["maths"], + variableNames: ["value"], }; } else { - dependencies.mathListChildren = { - dependencyType: "child", - childGroups: ["mathLists"], - variableNames: ["numComponents"], - }; - dependencies.mathAndMathListChildren = { + dependencies.mathChildren = { dependencyType: "child", - childGroups: ["maths", "mathLists"], - skipComponentNames: true, + childGroups: ["maths"], }; } return dependencies; }, - definition: function ({ dependencyValues, componentInfoObjects }) { + definition: function ({ dependencyValues }) { let numComponents = 0; - let childIndexByArrayKey = []; + let childInfoByComponent = []; - if (dependencyValues.mathAndMathListChildren.length > 0) { + if (dependencyValues.mathChildren.length > 0) { if (dependencyValues.mergeMathLists) { for (let [ childInd, child, - ] of dependencyValues.mathAndMathListChildren.entries()) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "mathList", - }) - ) { - for ( - let i = 0; - i < child.stateValues.numComponents; - i++ - ) { - childIndexByArrayKey[numComponents + i] = [ - childInd, - i, - ]; - } - numComponents += - child.stateValues.numComponents; - } else { - let childValue = child.stateValues.value; + ] of dependencyValues.mathChildren.entries()) { + let childValue = child.stateValues.value; - if ( - childValue && - Array.isArray(childValue.tree) && - childValue.tree[0] === "list" - ) { - let nPieces = childValue.tree.length - 1; - for (let i = 0; i < nPieces; i++) { - childIndexByArrayKey[ - i + numComponents - ] = [childInd, i, nPieces]; - } - numComponents += nPieces; - } else { - childIndexByArrayKey[numComponents] = [ - childInd, - 0, - ]; - numComponents += 1; - } - } - } - } else { - let nMathLists = 0; - for (let [ - childInd, - child, - ] of dependencyValues.mathAndMathListChildren.entries()) { if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "mathList", - }) + Array.isArray(childValue.tree) && + childValue.tree[0] === "list" ) { - let mathListChild = - dependencyValues.mathListChildren[ - nMathLists - ]; - nMathLists++; - for ( - let i = 0; - i < mathListChild.stateValues.numComponents; - i++ - ) { - childIndexByArrayKey[numComponents + i] = [ + let nComponents = childValue.tree.length - 1; + for (let i = 0; i < nComponents; i++) { + childInfoByComponent[i + numComponents] = { childInd, - i, - ]; + component: i, + nComponents, + childName: child.componentName, + }; } - numComponents += - mathListChild.stateValues.numComponents; + numComponents += nComponents; } else { - childIndexByArrayKey[numComponents] = [ + childInfoByComponent[numComponents] = { childInd, - 0, - ]; + childName: child.componentName, + }; numComponents += 1; } } + } else { + numComponents = dependencyValues.mathChildren.length; + childInfoByComponent = + dependencyValues.mathChildren.map((child, i) => ({ + childInd: i, + childName: child.componentName, + })); } } else if (dependencyValues.mathsShadow !== null) { numComponents = dependencyValues.mathsShadow.length; } let maxNum = dependencyValues.maxNumber; - if (maxNum !== null && numComponents > maxNum) { + if (numComponents > maxNum) { numComponents = maxNum; - childIndexByArrayKey = childIndexByArrayKey.slice( + childInfoByComponent = childInfoByComponent.slice( 0, maxNum, ); } return { - setValue: { numComponents, childIndexByArrayKey }, + setValue: { numComponents, childInfoByComponent }, checkForActualChange: { numComponents: true }, }; }, }; stateVariableDefinitions.maths = { - public: true, shadowingInstructions: { createComponentOfType: "math", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), }, isArray: true, entryPrefixes: ["math"], stateVariablesDeterminingDependencies: [ "mergeMathLists", - "childIndexByArrayKey", + "childInfoByComponent", ], returnArraySizeDependencies: () => ({ numComponents: { @@ -356,9 +303,9 @@ export default class MathList extends InlineComponent { dependencyType: "stateVariable", variableName: "mergeMathLists", }, - childIndexByArrayKey: { + childInfoByComponent: { dependencyType: "stateVariable", - variableName: "childIndexByArrayKey", + variableName: "childInfoByComponent", }, mathsShadow: { dependencyType: "stateVariable", @@ -368,20 +315,16 @@ export default class MathList extends InlineComponent { for (let arrayKey of arrayKeys) { let childIndices = []; - let mathIndex = "1"; - if (stateValues.childIndexByArrayKey[arrayKey]) { + if (stateValues.childInfoByComponent[arrayKey]) { childIndices = [ - stateValues.childIndexByArrayKey[arrayKey][0], + stateValues.childInfoByComponent[arrayKey].childInd, ]; - mathIndex = - stateValues.childIndexByArrayKey[arrayKey][1] + 1; } dependenciesByKey[arrayKey] = { - mathAndMathListChildren: { + mathChildren: { dependencyType: "child", - childGroups: ["maths", "mathLists"], - variableNames: ["value", "math" + mathIndex], - variablesOptional: true, + childGroups: ["maths"], + variableNames: ["value"], childIndices, }, }; @@ -396,34 +339,22 @@ export default class MathList extends InlineComponent { let maths = {}; for (let arrayKey of arrayKeys) { - let child = - dependencyValuesByKey[arrayKey] - .mathAndMathListChildren[0]; + let child = dependencyValuesByKey[arrayKey].mathChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - let childValue = child.stateValues.value; - if ( - globalDependencyValues.mergeMathLists && - Array.isArray(childValue.tree) && - childValue.tree[0] === "list" - ) { - let ind2 = - globalDependencyValues.childIndexByArrayKey[ - arrayKey - ][1]; - maths[arrayKey] = - childValue.get_component(ind2); - } else { - maths[arrayKey] = childValue; - } - } else { - let mathIndex = - globalDependencyValues.childIndexByArrayKey[ + let childValue = child.stateValues.value; + if ( + globalDependencyValues.mergeMathLists && + Array.isArray(childValue.tree) && + childValue.tree[0] === "list" + ) { + let ind2 = + globalDependencyValues.childInfoByComponent[ arrayKey - ][1] + 1; - maths[arrayKey] = - child.stateValues["math" + mathIndex]; + ].component; + maths[arrayKey] = childValue.get_component(ind2); + } else { + maths[arrayKey] = childValue; } } else if (globalDependencyValues.mathsShadow !== null) { maths[arrayKey] = @@ -444,8 +375,8 @@ export default class MathList extends InlineComponent { if (globalDependencyValues.mergeMathLists) { let instructions = []; - let childIndexByArrayKey = - await stateValues.childIndexByArrayKey; + let childInfoByComponent = + await stateValues.childInfoByComponent; let arrayKeysAddressed = []; @@ -459,16 +390,19 @@ export default class MathList extends InlineComponent { } let desiredValue; - if (childIndexByArrayKey[arrayKey][2] !== undefined) { + if ( + childInfoByComponent[arrayKey].nComponents !== + undefined + ) { // found a math that has been split due to merging // array keys that are associated with this math child let firstInd = Number(arrayKey) - - childIndexByArrayKey[arrayKey][1]; + childInfoByComponent[arrayKey].component; let lastInd = firstInd + - childIndexByArrayKey[arrayKey][2] - + childInfoByComponent[arrayKey].nComponents - 1; // in case just one ind specified, merge with previous values @@ -508,32 +442,36 @@ export default class MathList extends InlineComponent { } let child = - dependencyValuesByKey[arrayKey] - .mathAndMathListChildren[0]; + dependencyValuesByKey[arrayKey].mathChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .mathAndMathListChildren, - desiredValue, - childIndex: 0, - variableIndex: 0, - }); - } else { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .mathAndMathListChildren, - desiredValue, - childIndex: 0, - variableIndex: 1, - }); + instructions.push({ + setDependency: + dependencyNamesByKey[arrayKey].mathChildren, + desiredValue, + childIndex: 0, + variableIndex: 0, + }); + } else if ( + globalDependencyValues.mathsShadow !== null + ) { + if (!workspace.desiredMathShadow) { + workspace.desiredMathShadow = [ + ...globalDependencyValues.mathsShadow, + ]; } + workspace.desiredMathShadow[arrayKey] = + desiredValue; } } + if (workspace.desiredMathShadow) { + instructions.push({ + setDependency: "mathsShadow", + desiredValue: workspace.desiredMathShadow, + }); + } + return { success: true, instructions, @@ -547,32 +485,17 @@ export default class MathList extends InlineComponent { continue; } - let child = - dependencyValuesByKey[arrayKey] - .mathAndMathListChildren[0]; + let child = dependencyValuesByKey[arrayKey].mathChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .mathAndMathListChildren, - desiredValue: - desiredStateVariableValues.maths[arrayKey], - childIndex: 0, - variableIndex: 0, - }); - } else { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .mathAndMathListChildren, - desiredValue: - desiredStateVariableValues.maths[arrayKey], - childIndex: 0, - variableIndex: 1, - }); - } + instructions.push({ + setDependency: + dependencyNamesByKey[arrayKey].mathChildren, + desiredValue: + desiredStateVariableValues.maths[arrayKey], + childIndex: 0, + variableIndex: 0, + }); } else if (globalDependencyValues.mathsShadow !== null) { if (!workspace.desiredMathShadow) { workspace.desiredMathShadow = [ @@ -581,13 +504,16 @@ export default class MathList extends InlineComponent { } workspace.desiredMathShadow[arrayKey] = desiredStateVariableValues.maths[arrayKey]; - instructions.push({ - setDependency: "mathsShadow", - desiredValue: workspace.desiredMathShadow, - }); } } + if (workspace.desiredMathShadow) { + instructions.push({ + setDependency: "mathsShadow", + desiredValue: workspace.desiredMathShadow, + }); + } + return { success: true, instructions, @@ -595,36 +521,6 @@ export default class MathList extends InlineComponent { }, }; - stateVariableDefinitions.math = { - public: true, - shadowingInstructions: { - createComponentOfType: "math", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), - }, - returnDependencies: () => ({ - maths: { - dependencyType: "stateVariable", - variableName: "maths", - }, - }), - definition({ dependencyValues }) { - let math; - if (dependencyValues.maths.length === 0) { - math = me.fromAst("\uff3f"); - } else if (dependencyValues.maths.length === 1) { - math = dependencyValues.maths[0]; - } else { - math = me.fromAst([ - "list", - ...dependencyValues.maths.map((x) => x.tree), - ]); - } - - return { setValue: { math } }; - }, - }; - stateVariableDefinitions.numValues = { isAlias: true, targetVariableName: "numComponents", @@ -635,473 +531,186 @@ export default class MathList extends InlineComponent { targetVariableName: "maths", }; - stateVariableDefinitions.numbers = { - public: true, - shadowingInstructions: { - createComponentOfType: "number", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), - }, - isArray: true, - entryPrefixes: ["number"], - returnArraySizeDependencies: () => ({ - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - }), - returnArraySize({ dependencyValues }) { - return [dependencyValues.numComponents]; - }, - - returnArrayDependenciesByKey({ arrayKeys }) { - let dependenciesByKey = {}; - - for (let arrayKey of arrayKeys) { - dependenciesByKey[arrayKey] = { - math: { - dependencyType: "stateVariable", - variableName: `math${Number(arrayKey) + 1}`, - }, - }; - } - return { dependenciesByKey }; - }, - arrayDefinitionByKey({ dependencyValuesByKey, arrayKeys }) { - let numbers = {}; - - for (let arrayKey of arrayKeys) { - numbers[arrayKey] = - dependencyValuesByKey[ - arrayKey - ].math.evaluate_to_constant(); - } - - return { setValue: { numbers } }; - }, - async inverseArrayDefinitionByKey({ - desiredStateVariableValues, - dependencyNamesByKey, - }) { - let instructions = []; - - for (let arrayKey in desiredStateVariableValues.numbers) { - instructions.push({ - setDependency: dependencyNamesByKey[arrayKey].math, - desiredValue: me.fromAst( - desiredStateVariableValues.numbers[arrayKey], - ), - }); - } - - return { - success: true, - instructions, - }; - }, - }; - - stateVariableDefinitions.latex = { - additionalStateVariablesDefined: ["latexs"], - public: true, - shadowingInstructions: { - createComponentOfType: "latex", - }, - forRenderer: true, + stateVariableDefinitions.readyToExpandWhenResolved = { returnDependencies: () => ({ - mathAndMathListChildren: { - dependencyType: "child", - childGroups: ["maths", "mathLists"], - variableNames: ["valueForDisplay", "latex", "latexs"], - variablesOptional: true, - }, - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - mergeMathLists: { - dependencyType: "stateVariable", - variableName: "mergeMathLists", - }, - mathsShadow: { - dependencyType: "stateVariable", - variableName: "mathsShadow", - }, - displayDigits: { - dependencyType: "stateVariable", - variableName: "displayDigits", - }, - displayDecimals: { - dependencyType: "stateVariable", - variableName: "displayDecimals", - }, - displaySmallAsZero: { - dependencyType: "stateVariable", - variableName: "displaySmallAsZero", - }, - padZeros: { + childInfoByComponent: { dependencyType: "stateVariable", - variableName: "padZeros", - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "mathList", - variableName: "numComponentsToDisplayByChild", + variableName: "childInfoByComponent", }, }), - definition: function ({ dependencyValues, componentName }) { - let latexs = []; - let params = {}; - if (dependencyValues.padZeros) { - if (Number.isFinite(dependencyValues.displayDecimals)) { - params.padToDecimals = dependencyValues.displayDecimals; - } - if (dependencyValues.displayDigits >= 1) { - params.padToDigits = dependencyValues.displayDigits; - } - } - if (dependencyValues.mathAndMathListChildren.length > 0) { - for (let child of dependencyValues.mathAndMathListChildren) { - if (child.stateValues.valueForDisplay) { - let childValue = child.stateValues.valueForDisplay; - - if ( - dependencyValues.mergeMathLists && - Array.isArray(childValue.tree) && - childValue.tree[0] === "list" - ) { - for ( - let i = 0; - i < childValue.tree.length - 1; - i++ - ) { - latexs.push( - childValue - .get_component(i) - .toLatex(params), - ); - } - } else { - latexs.push(child.stateValues.latex); - } - } else { - latexs.push(...child.stateValues.latexs); - } - } - } else if (dependencyValues.mathsShadow !== null) { - latexs = dependencyValues.mathsShadow.map((x) => - roundForDisplay({ - value: x, - dependencyValues, - }).toLatex(params), - ); - } - - let numComponentsToDisplay = dependencyValues.numComponents; - - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent mathList, which could have limited - // math of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } - - latexs = latexs.slice(0, numComponentsToDisplay); - - let latex = latexs.join(", "); - - return { setValue: { latex, latexs } }; + // When this state variable is marked stale + // it indicates we should update replacements. + // For this to work, must set + // stateVariableToEvaluateAfterReplacements + // to this variable so that it is marked fresh + markStale: () => ({ updateReplacements: true }), + definition: function () { + return { setValue: { readyToExpandWhenResolved: true } }; }, }; - stateVariableDefinitions.text = { - public: true, - shadowingInstructions: { - createComponentOfType: "text", - }, - additionalStateVariablesDefined: ["texts"], - returnDependencies: () => ({ - mathAndMathListChildren: { - dependencyType: "child", - childGroups: ["maths", "mathLists"], - variableNames: ["valueForDisplay", "text", "texts"], - variablesOptional: true, - }, - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - mergeMathLists: { - dependencyType: "stateVariable", - variableName: "mergeMathLists", - }, - mathsShadow: { - dependencyType: "stateVariable", - variableName: "mathsShadow", - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "mathList", - variableName: "numComponentsToDisplayByChild", - }, - }), - definition: function ({ dependencyValues, componentName }) { - let texts = []; - - if (dependencyValues.mathAndMathListChildren.length > 0) { - for (let child of dependencyValues.mathAndMathListChildren) { - if (child.stateValues.valueForDisplay) { - let childValue = child.stateValues.valueForDisplay; + return stateVariableDefinitions; + } - if ( - dependencyValues.mergeMathLists && - Array.isArray(childValue.tree) && - childValue.tree[0] === "list" - ) { - for ( - let i = 0; - i < childValue.tree.length - 1; - i++ - ) { - texts.push( - childValue.get_component(i).toString(), - ); - } - } else { - texts.push(child.stateValues.text); - } - } else { - texts.push(...child.stateValues.texts); - } - } - } else if (dependencyValues.mathsShadow !== null) { - texts = dependencyValues.mathsShadow.map((x) => - x.toString(), + static async createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }) { + let errors = []; + let warnings = []; + + let replacements = []; + let componentsCopied = []; + + let attributesToConvert = {}; + for (let attr of [ + "fixed", + "isResponse", + ...Object.keys(returnRoundingAttributes()), + ]) { + if (attr in component.attributes) { + attributesToConvert[attr] = component.attributes[attr]; + } + } + + let newNamespace = component.attributes.newNamespace?.primitive; + + // allow one to override the fixed and isResponse attributes + // as well as rounding settings + // by specifying it on the mathList + let attributesFromComposite = {}; + + if (Object.keys(attributesToConvert).length > 0) { + attributesFromComposite = convertAttributesForComponentType({ + attributes: attributesToConvert, + componentType: "math", + componentInfoObjects, + compositeCreatesNewNamespace: newNamespace, + }); + } + + let childInfoByComponent = + await component.stateValues.childInfoByComponent; + + let numComponents = await component.stateValues.numComponents; + for (let i = 0; i < numComponents; i++) { + let childInfo = childInfoByComponent[i]; + if (childInfo) { + let replacementSource = components[childInfo.childName]; + + if (childInfo.nComponents !== undefined) { + componentsCopied.push( + replacementSource.componentName + + ":" + + childInfo.component, ); + } else { + componentsCopied.push(replacementSource.componentName); } - - let numComponentsToDisplay = dependencyValues.numComponents; - - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent mathList, which could have limited - // math of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } - - texts = texts.slice(0, numComponentsToDisplay); - - let text = texts.join(", "); - - return { setValue: { text, texts } }; - }, - }; - - stateVariableDefinitions.componentNamesInList = { - returnDependencies: () => ({ - mathAndMathListChildren: { - dependencyType: "child", - childGroups: ["maths", "mathLists"], - variableNames: ["componentNamesInList", "value"], - variablesOptional: true, - }, - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - mergeMathLists: { - dependencyType: "stateVariable", - variableName: "mergeMathLists", + } + replacements.push({ + componentType: "math", + attributes: JSON.parse(JSON.stringify(attributesFromComposite)), + downstreamDependencies: { + [component.componentName]: [ + { + dependencyType: "referenceShadow", + compositeName: component.componentName, + propVariable: `math${i + 1}`, + }, + ], }, - }), - definition: function ({ dependencyValues, componentInfoObjects }) { - let componentNamesInList = []; - let numComponentsLeft = dependencyValues.numComponents; - - for (let child of dependencyValues.mathAndMathListChildren) { - if (numComponentsLeft === 0) { - break; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "mathList", - }) - ) { - let componentNamesToAdd = - child.stateValues.componentNamesInList.slice( - 0, - numComponentsLeft, - ); - - componentNamesInList.push(...componentNamesToAdd); - numComponentsLeft -= componentNamesToAdd.length; - } else { - componentNamesInList.push(child.componentName); - - if ( - dependencyValues.mergeMathLists && - Array.isArray(child.stateValues.value.tree) && - child.stateValues.value.tree[0] === "list" - ) { - numComponentsLeft -= - child.stateValues.value.tree.length - 1; - } else { - numComponentsLeft--; - } - } - } - - return { setValue: { componentNamesInList } }; - }, - }; + }); + } + + workspace.uniqueIdentifiersUsed = []; + replacements = postProcessCopy({ + serializedComponents: replacements, + componentName: component.componentName, + uniqueIdentifiersUsed: workspace.uniqueIdentifiersUsed, + addShadowDependencies: true, + markAsPrimaryShadow: true, + }); - stateVariableDefinitions.numComponentsToDisplayByChild = { - additionalStateVariablesDefined: ["numChildrenToRender"], - returnDependencies: () => ({ - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - mathListChildren: { - dependencyType: "child", - childGroups: ["mathLists"], - variableNames: ["numComponents"], - }, - mathChildren: { - dependencyType: "child", - childGroups: ["maths"], - variableNames: ["value"], - }, - mathAndMathListChildren: { - dependencyType: "child", - childGroups: ["maths", "mathLists"], - skipComponentNames: true, - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "mathList", - variableName: "numComponentsToDisplayByChild", - }, - mergeMathLists: { - dependencyType: "stateVariable", - variableName: "mergeMathLists", - }, - }), - definition: function ({ - dependencyValues, - componentInfoObjects, - componentName, - }) { - let numComponentsToDisplay = dependencyValues.numComponents; - - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent mathList, which could have limited - // math of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } + let processResult = processAssignNames({ + assignNames: component.doenetAttributes.assignNames, + serializedComponents: replacements, + parentName: component.componentName, + parentCreatesNewNamespace: newNamespace, + componentInfoObjects, + }); + errors.push(...processResult.errors); + warnings.push(...processResult.warnings); - let numComponentsToDisplayByChild = {}; + workspace.componentsCopied = componentsCopied; - let numComponentsSoFar = 0; - let numChildrenToRender = 0; + return { + replacements: processResult.serializedComponents, + errors, + warnings, + }; + } - let nMathLists = 0; - let nMaths = 0; - for (let child of dependencyValues.mathAndMathListChildren) { - let numComponentsLeft = Math.max( - 0, - numComponentsToDisplay - numComponentsSoFar, - ); - if (numComponentsLeft > 0) { - numChildrenToRender++; - } - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "mathList", - }) - ) { - let mathListChild = - dependencyValues.mathListChildren[nMathLists]; - nMathLists++; - - let numComponentsForMathListChild = Math.min( - numComponentsLeft, - mathListChild.stateValues.numComponents, - ); - - numComponentsToDisplayByChild[ - mathListChild.componentName - ] = numComponentsForMathListChild; - numComponentsSoFar += numComponentsForMathListChild; - } else { - let mathChild = dependencyValues.mathChildren[nMaths]; - nMaths++; + static async calculateReplacementChanges({ + component, + components, + componentInfoObjects, + workspace, + }) { + // TODO: don't yet have a way to return errors and warnings! + let errors = []; + let warnings = []; + + let componentsToCopy = []; + + let childInfoByComponent = + await component.stateValues.childInfoByComponent; + + for (let childInfo of childInfoByComponent) { + let replacementSource = components[childInfo.childName]; + + if (childInfo.nComponents !== undefined) { + componentsToCopy.push( + replacementSource.componentName + ":" + childInfo.component, + ); + } else { + componentsToCopy.push(replacementSource.componentName); + } + } + + if ( + componentsToCopy.length == workspace.componentsCopied.length && + workspace.componentsCopied.every( + (x, i) => x === componentsToCopy[i], + ) + ) { + return []; + } + + // for now, just recreate + let replacementResults = await this.createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }); - if ( - dependencyValues.mergeMathLists && - Array.isArray(mathChild.stateValues.value.tree) && - mathChild.stateValues.value.tree[0] === "list" - ) { - let numComponentsInMath = - mathChild.stateValues.value.tree.length - 1; - - if (numComponentsLeft < numComponentsInMath) { - numComponentsToDisplayByChild[ - mathChild.componentName - ] = numComponentsLeft; - numComponentsSoFar += numComponentsLeft; - } else { - // if we will display the whole math list, - // don't set numComponentsToDisplayByChild for the math child - numComponentsSoFar += numComponentsInMath; - } - } else { - numComponentsSoFar += 1; - } - } - } + let replacements = replacementResults.replacements; + errors.push(...replacementResults.errors); + warnings.push(...replacementResults.warnings); - return { - setValue: { - numComponentsToDisplayByChild, - numChildrenToRender, - }, - }; + let replacementChanges = [ + { + changeType: "add", + changeTopLevelReplacements: true, + firstReplacementInd: 0, + numberReplacementsToReplace: component.replacements.length, + serializedReplacements: replacements, }, - markStale: () => ({ updateRenderedChildren: true }), - }; + ]; - return stateVariableDefinitions; + return replacementChanges; } - - static adapters = [ - { - stateVariable: "math", - stateVariablesToShadow: Object.keys( - returnRoundingStateVariableDefinitions(), - ), - }, - { - stateVariable: "numbers", - componentType: "numberList", - stateVariablesToShadow: Object.keys( - returnRoundingStateVariableDefinitions(), - ), - }, - "text", - ]; } diff --git a/packages/doenetml-worker/src/components/MathOperators.js b/packages/doenetml-worker/src/components/MathOperators.js index 0d51ee7a5..24d3623a1 100644 --- a/packages/doenetml-worker/src/components/MathOperators.js +++ b/packages/doenetml-worker/src/components/MathOperators.js @@ -286,10 +286,9 @@ export class Round extends MathBaseOperatorOneInput { // change default rounding to 14 // so that actual rounding result can be seen. - // Don't include maths for childsGroupIfSingleMatch + // Don't include maths for childGroupsIfSingleMatch // so that this overrides display rounding from children let roundingDefinitions = returnRoundingStateVariableDefinitions({ - includeListParents: true, displayDigitsDefault: 14, }); Object.assign(stateVariableDefinitions, roundingDefinitions); diff --git a/packages/doenetml-worker/src/components/Number.js b/packages/doenetml-worker/src/components/Number.js index 113b4d02a..a8bba8a1f 100644 --- a/packages/doenetml-worker/src/components/Number.js +++ b/packages/doenetml-worker/src/components/Number.js @@ -144,12 +144,41 @@ export default class NumberComponent extends InlineComponent { Object.assign(stateVariableDefinitions, anchorDefinition); let roundingDefinitions = returnRoundingStateVariableDefinitions({ - childsGroupIfSingleMatch: ["maths", "numbers"], + childGroupsIfSingleMatch: ["maths", "numbers"], childGroupsToStopSingleMatch: ["strings", "texts", "booleans"], - includeListParents: true, }); Object.assign(stateVariableDefinitions, roundingDefinitions); + stateVariableDefinitions.inUnorderedList = { + defaultValue: false, + returnDependencies: () => ({ + sourceCompositeUnordered: { + dependencyType: "sourceCompositeStateVariable", + variableName: "unordered", + }, + }), + definition({ dependencyValues, usedDefault }) { + if ( + dependencyValues.sourceCompositeUnordered !== null && + !usedDefault.sourceCompositeUnordered + ) { + return { + setValue: { + inUnorderedList: Boolean( + dependencyValues.sourceCompositeUnordered, + ), + }, + }; + } else { + return { + setValue: { + inUnorderedList: false, + }, + }; + } + }, + }; + stateVariableDefinitions.singleNumberOrStringChild = { additionalStateVariablesDefined: ["singleMathChild"], returnDependencies: () => ({ @@ -296,7 +325,7 @@ export default class NumberComponent extends InlineComponent { for (let child of dependencyValues.allChildren) { if (typeof child !== "string") { - // a math, mathList, text, textList, boolean, or booleanList + // a math, number, text, or boolean let code = codePre + subnum; if ( diff --git a/packages/doenetml-worker/src/components/NumberList.js b/packages/doenetml-worker/src/components/NumberList.js index 648831769..8dd59c3ff 100644 --- a/packages/doenetml-worker/src/components/NumberList.js +++ b/packages/doenetml-worker/src/components/NumberList.js @@ -1,16 +1,21 @@ -import { roundForDisplay } from "../utils/math"; -import { - returnRoundingAttributeComponentShadowing, - returnRoundingAttributes, - returnRoundingStateVariableDefinitions, -} from "../utils/rounding"; -import InlineComponent from "./abstract/InlineComponent"; -import { returnGroupIntoComponentTypeSeparatedBySpacesOutsideParens } from "./commonsugar/lists"; +import CompositeComponent from "./abstract/CompositeComponent"; import me from "math-expressions"; +import { returnGroupIntoComponentTypeSeparatedBySpacesOutsideParens } from "./commonsugar/lists"; +import { convertValueToMathExpression } from "@doenet/utils"; +import { returnRoundingAttributes } from "../utils/rounding"; +import { + convertAttributesForComponentType, + postProcessCopy, +} from "../utils/copy"; +import { processAssignNames } from "../utils/naming"; -export default class NumberList extends InlineComponent { +export default class NumberList extends CompositeComponent { static componentType = "numberList"; - static renderChildren = true; + + static stateVariableToEvaluateAfterReplacements = + "readyToExpandWhenResolved"; + + static assignNamesToReplacements = true; static includeBlankStringChildren = true; static removeBlankStringChildrenPostSugar = true; @@ -20,6 +25,12 @@ export default class NumberList extends InlineComponent { static stateVariableToBeShadowed = "numbers"; static primaryStateVariableForDefinition = "numbersShadow"; + // even if inside a component that turned on descendantCompositesMustHaveAReplacement + // don't required composite replacements + static descendantCompositesMustHaveAReplacement = false; + + static doNotExpandAsShadowed = true; + static createAttributesObject() { let attributes = super.createAttributesObject(); @@ -33,11 +44,23 @@ export default class NumberList extends InlineComponent { attributes.maxNumber = { createComponentOfType: "number", createStateVariable: "maxNumber", - defaultValue: null, + defaultValue: Infinity, public: true, }; - Object.assign(attributes, returnRoundingAttributes()); + attributes.fixed = { + leaveRaw: true, + }; + + attributes.isResponse = { + leaveRaw: true, + }; + + for (let attrName in returnRoundingAttributes()) { + attributes[attrName] = { + leaveRaw: true, + }; + } return attributes; } @@ -69,8 +92,8 @@ export default class NumberList extends InlineComponent { componentTypes: ["number"], }, { - group: "numberLists", - componentTypes: ["numberList"], + group: "maths", + componentTypes: ["math"], }, ]; } @@ -78,19 +101,6 @@ export default class NumberList extends InlineComponent { static returnStateVariableDefinitions() { let stateVariableDefinitions = super.returnStateVariableDefinitions(); - Object.assign( - stateVariableDefinitions, - returnRoundingStateVariableDefinitions(), - ); - - // set overrideChildHide so that children are hidden - // only based on whether or not the list is hidden - // so that can't have a list with partially hidden components - stateVariableDefinitions.overrideChildHide = { - returnDependencies: () => ({}), - definition: () => ({ setValue: { overrideChildHide: true } }), - }; - stateVariableDefinitions.numbersShadow = { defaultValue: null, hasEssential: true, @@ -102,100 +112,156 @@ export default class NumberList extends InlineComponent { }), }; - stateVariableDefinitions.numComponents = { + stateVariableDefinitions.asList = { + returnDependencies: () => ({}), + definition() { + return { setValue: { asList: true } }; + }, + }; + + stateVariableDefinitions.mergeMathLists = { public: true, shadowingInstructions: { - createComponentOfType: "number", + createComponentOfType: "boolean", }, - additionalStateVariablesDefined: ["childIndexByArrayKey"], returnDependencies: () => ({ - maxNumber: { - dependencyType: "stateVariable", - variableName: "maxNumber", + mergeMathListsAttr: { + dependencyType: "attributeComponent", + attributeName: "mergeMathLists", + variableNames: ["value"], }, - numberListChildren: { + mathChildren: { dependencyType: "child", - childGroups: ["numberLists"], - variableNames: ["numComponents"], + childGroups: ["maths"], + skipComponentNames: true, }, - numberAndNumberListChildren: { + numberChildren: { dependencyType: "child", - childGroups: ["numbers", "numberLists"], + childGroups: ["numbers"], skipComponentNames: true, }, - numbersShadow: { - dependencyType: "stateVariable", - variableName: "numbersShadow", - }, }), - definition: function ({ dependencyValues, componentInfoObjects }) { + definition({ dependencyValues }) { + let mergeMathLists = + dependencyValues.mathChildren.length === 1 && + dependencyValues.numberChildren.length === 0; + return { setValue: { mergeMathLists } }; + }, + }; + + stateVariableDefinitions.numComponents = { + public: true, + shadowingInstructions: { + createComponentOfType: "number", + }, + stateVariablesDeterminingDependencies: ["mergeMathLists"], + additionalStateVariablesDefined: ["childInfoByComponent"], + returnDependencies({ stateValues }) { + let dependencies = { + maxNumber: { + dependencyType: "stateVariable", + variableName: "maxNumber", + }, + mergeMathLists: { + dependencyType: "stateVariable", + variableName: "mergeMathLists", + }, + numbersShadow: { + dependencyType: "stateVariable", + variableName: "numbersShadow", + }, + }; + + if (stateValues.mergeMathLists) { + dependencies.numberMathChildren = { + dependencyType: "child", + childGroups: ["numbers", "maths"], + variableNames: ["value"], + }; + } else { + dependencies.numberMathChildren = { + dependencyType: "child", + childGroups: ["numbers", "maths"], + }; + } + + return dependencies; + }, + definition: function ({ dependencyValues }) { let numComponents = 0; - let childIndexByArrayKey = []; - - let nNumberLists = 0; - if (dependencyValues.numberAndNumberListChildren.length > 0) { - for (let [ - childInd, - child, - ] of dependencyValues.numberAndNumberListChildren.entries()) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - let numberListChild = - dependencyValues.numberListChildren[ - nNumberLists - ]; - nNumberLists++; - for ( - let i = 0; - i < numberListChild.stateValues.numComponents; - i++ + let childInfoByComponent = []; + + if (dependencyValues.numberMathChildren.length > 0) { + if (dependencyValues.mergeMathLists) { + for (let [ + childInd, + child, + ] of dependencyValues.numberMathChildren.entries()) { + let childValue = child.stateValues.value; + + if ( + Array.isArray(childValue.tree) && + childValue.tree[0] === "list" ) { - childIndexByArrayKey[numComponents + i] = [ + let nComponents = childValue.tree.length - 1; + for (let i = 0; i < nComponents; i++) { + childInfoByComponent[i + numComponents] = { + childInd, + component: i, + nComponents, + childName: child.componentName, + }; + } + numComponents += nComponents; + } else { + childInfoByComponent[numComponents] = { childInd, - i, - ]; + childName: child.componentName, + }; + numComponents += 1; } - numComponents += - numberListChild.stateValues.numComponents; - } else { - childIndexByArrayKey[numComponents] = [childInd, 0]; - numComponents += 1; } + } else { + numComponents = + dependencyValues.numberMathChildren.length; + childInfoByComponent = + dependencyValues.numberMathChildren.map( + (child, i) => ({ + childInd: i, + childName: child.componentName, + }), + ); } } else if (dependencyValues.numbersShadow !== null) { numComponents = dependencyValues.numbersShadow.length; } let maxNum = dependencyValues.maxNumber; - if (maxNum !== null && numComponents > maxNum) { + if (numComponents > maxNum) { numComponents = maxNum; - childIndexByArrayKey = childIndexByArrayKey.slice( + childInfoByComponent = childInfoByComponent.slice( 0, maxNum, ); } return { - setValue: { numComponents, childIndexByArrayKey }, + setValue: { numComponents, childInfoByComponent }, checkForActualChange: { numComponents: true }, }; }, }; stateVariableDefinitions.numbers = { - public: true, shadowingInstructions: { createComponentOfType: "number", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), }, isArray: true, entryPrefixes: ["number"], - stateVariablesDeterminingDependencies: ["childIndexByArrayKey"], + stateVariablesDeterminingDependencies: [ + "mergeMathLists", + "childInfoByComponent", + ], returnArraySizeDependencies: () => ({ numComponents: { dependencyType: "stateVariable", @@ -209,9 +275,13 @@ export default class NumberList extends InlineComponent { returnArrayDependenciesByKey({ arrayKeys, stateValues }) { let dependenciesByKey = {}; let globalDependencies = { - childIndexByArrayKey: { + mergeMathLists: { dependencyType: "stateVariable", - variableName: "childIndexByArrayKey", + variableName: "mergeMathLists", + }, + childInfoByComponent: { + dependencyType: "stateVariable", + variableName: "childInfoByComponent", }, numbersShadow: { dependencyType: "stateVariable", @@ -221,20 +291,16 @@ export default class NumberList extends InlineComponent { for (let arrayKey of arrayKeys) { let childIndices = []; - let numberIndex = "1"; - if (stateValues.childIndexByArrayKey[arrayKey]) { + if (stateValues.childInfoByComponent[arrayKey]) { childIndices = [ - stateValues.childIndexByArrayKey[arrayKey][0], + stateValues.childInfoByComponent[arrayKey].childInd, ]; - numberIndex = - stateValues.childIndexByArrayKey[arrayKey][1] + 1; } dependenciesByKey[arrayKey] = { - numberAndNumberListChildren: { + numberMathChildren: { dependencyType: "child", - childGroups: ["numbers", "numberLists"], - variableNames: ["value", "number" + numberIndex], - variablesOptional: true, + childGroups: ["numbers", "maths"], + variableNames: ["value"], childIndices, }, }; @@ -250,19 +316,29 @@ export default class NumberList extends InlineComponent { for (let arrayKey of arrayKeys) { let child = - dependencyValuesByKey[arrayKey] - .numberAndNumberListChildren[0]; + dependencyValuesByKey[arrayKey].numberMathChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - numbers[arrayKey] = child.stateValues.value; - } else { - let numberIndex = - globalDependencyValues.childIndexByArrayKey[ + let childValue = child.stateValues.value; + if ( + globalDependencyValues.mergeMathLists && + Array.isArray(childValue.tree) && + childValue.tree[0] === "list" + ) { + let ind2 = + globalDependencyValues.childInfoByComponent[ arrayKey - ][1] + 1; + ].component; + numbers[arrayKey] = childValue + .get_component(ind2) + .evaluate_to_constant(); + } else if ( + childValue.evaluate_to_constant !== undefined + ) { numbers[arrayKey] = - child.stateValues["number" + numberIndex]; + childValue.evaluate_to_constant(); + } else { + numbers[arrayKey] = childValue; } } else if (globalDependencyValues.numbersShadow !== null) { numbers[arrayKey] = @@ -272,13 +348,135 @@ export default class NumberList extends InlineComponent { return { setValue: { numbers } }; }, - inverseArrayDefinitionByKey({ + async inverseArrayDefinitionByKey({ desiredStateVariableValues, dependencyValuesByKey, globalDependencyValues, dependencyNamesByKey, + componentInfoObjects, + stateValues, workspace, }) { + if (globalDependencyValues.mergeMathLists) { + let instructions = []; + + let childInfoByComponent = + await stateValues.childInfoByComponent; + + let arrayKeysAddressed = []; + + for (let arrayKey in desiredStateVariableValues.numbers) { + if (!dependencyValuesByKey[arrayKey]) { + continue; + } + + if (arrayKeysAddressed.includes(arrayKey)) { + continue; + } + + let child = + dependencyValuesByKey[arrayKey] + .numberMathChildren[0]; + + let desiredValue; + if ( + childInfoByComponent[arrayKey].nComponents !== + undefined + ) { + // found a math that has been split due to merging + + // array keys that are associated with this math child + let firstInd = + Number(arrayKey) - + childInfoByComponent[arrayKey].component; + let lastInd = + firstInd + + childInfoByComponent[arrayKey].nComponents - + 1; + + // in case just one ind specified, merge with previous values + if (!workspace.desiredMaths) { + workspace.desiredMaths = []; + } + + let desiredTree = ["list"]; + + for (let i = firstInd; i <= lastInd; i++) { + if ( + desiredStateVariableValues.numbers[i] !== + undefined + ) { + workspace.desiredMaths[i] = + convertValueToMathExpression( + desiredStateVariableValues.numbers[ + i + ], + ); + } else if ( + workspace.desiredMaths[i] === undefined + ) { + workspace.desiredMaths[i] = me.fromAst( + (await stateValues.numbers)[i], + ); + } + + desiredTree.push( + workspace.desiredMaths[i].tree, + ); + arrayKeysAddressed.push(i.toString()); + } + + desiredValue = me.fromAst(desiredTree); + } else { + desiredValue = + desiredStateVariableValues.numbers[arrayKey]; + if ( + componentInfoObjects.isInheritedComponentType({ + inheritedComponentType: + child?.componentType, + baseComponentType: "math", + }) + ) { + desiredValue = + convertValueToMathExpression(desiredValue); + } + } + + if (child) { + instructions.push({ + setDependency: + dependencyNamesByKey[arrayKey] + .numberMathChildren, + desiredValue, + childIndex: 0, + variableIndex: 0, + }); + } else if ( + globalDependencyValues.numbersShadow !== null + ) { + if (!workspace.desiredNumberShadow) { + workspace.desiredNumberShadow = [ + ...globalDependencyValues.numbersShadow, + ]; + } + workspace.desiredNumberShadow[arrayKey] = + desiredValue; + } + } + + if (workspace.desiredNumberShadow) { + instructions.push({ + setDependency: "numbersShadow", + desiredValue: workspace.desiredNumberShadow, + }); + } + + return { + success: true, + instructions, + }; + } + let instructions = []; for (let arrayKey in desiredStateVariableValues.numbers) { @@ -287,35 +485,28 @@ export default class NumberList extends InlineComponent { } let child = - dependencyValuesByKey[arrayKey] - .numberAndNumberListChildren[0]; + dependencyValuesByKey[arrayKey].numberMathChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .numberAndNumberListChildren, - desiredValue: - desiredStateVariableValues.numbers[ - arrayKey - ], - childIndex: 0, - variableIndex: 0, - }); - } else { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .numberAndNumberListChildren, - desiredValue: - desiredStateVariableValues.numbers[ - arrayKey - ], - childIndex: 0, - variableIndex: 1, - }); + let desiredValue = + desiredStateVariableValues.numbers[arrayKey]; + if ( + componentInfoObjects.isInheritedComponentType({ + inheritedComponentType: child.componentType, + baseComponentType: "math", + }) + ) { + desiredValue = + convertValueToMathExpression(desiredValue); } + instructions.push({ + setDependency: + dependencyNamesByKey[arrayKey] + .numberMathChildren, + desiredValue, + childIndex: 0, + variableIndex: 0, + }); } else if (globalDependencyValues.numbersShadow !== null) { if (!workspace.desiredNumberShadow) { workspace.desiredNumberShadow = [ @@ -324,13 +515,16 @@ export default class NumberList extends InlineComponent { } workspace.desiredNumberShadow[arrayKey] = desiredStateVariableValues.numbers[arrayKey]; - instructions.push({ - setDependency: "numbersShadow", - desiredValue: workspace.desiredNumberShadow, - }); } } + if (workspace.desiredNumberShadow) { + instructions.push({ + setDependency: "numbersShadow", + desiredValue: workspace.desiredNumberShadow, + }); + } + return { success: true, instructions, @@ -348,337 +542,186 @@ export default class NumberList extends InlineComponent { targetVariableName: "numbers", }; - stateVariableDefinitions.math = { - public: true, - shadowingInstructions: { - createComponentOfType: "math", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), - }, + stateVariableDefinitions.readyToExpandWhenResolved = { returnDependencies: () => ({ - numbers: { + childInfoByComponent: { dependencyType: "stateVariable", - variableName: "numbers", + variableName: "childInfoByComponent", }, }), - definition({ dependencyValues }) { - let math; - if (dependencyValues.numbers.length === 0) { - math = me.fromAst("\uff3f"); - } else if (dependencyValues.numbers.length === 1) { - math = me.fromAst(dependencyValues.numbers[0]); - } else { - math = me.fromAst(["list", ...dependencyValues.numbers]); - } - - return { setValue: { math } }; + // When this state variable is marked stale + // it indicates we should update replacements. + // For this to work, must set + // stateVariableToEvaluateAfterReplacements + // to this variable so that it is marked fresh + markStale: () => ({ updateReplacements: true }), + definition: function () { + return { setValue: { readyToExpandWhenResolved: true } }; }, }; - stateVariableDefinitions.maths = { - public: true, - shadowingInstructions: { - createComponentOfType: "math", - addAttributeComponentsShadowingStateVariables: - returnRoundingAttributeComponentShadowing(), - }, - isArray: true, - entryPrefixes: ["math"], - returnArraySizeDependencies: () => ({ - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - }), - returnArraySize({ dependencyValues }) { - return [dependencyValues.numComponents]; - }, - - returnArrayDependenciesByKey({ arrayKeys }) { - let dependenciesByKey = {}; - - for (let arrayKey of arrayKeys) { - dependenciesByKey[arrayKey] = { - number: { - dependencyType: "stateVariable", - variableName: `number${Number(arrayKey) + 1}`, - }, - }; - } - return { dependenciesByKey }; - }, - arrayDefinitionByKey({ dependencyValuesByKey, arrayKeys }) { - let maths = {}; + return stateVariableDefinitions; + } - for (let arrayKey of arrayKeys) { - maths[arrayKey] = me.fromAst( - dependencyValuesByKey[arrayKey].number, + static async createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }) { + let errors = []; + let warnings = []; + + let replacements = []; + let componentsCopied = []; + + let attributesToConvert = {}; + for (let attr of [ + "fixed", + "isResponse", + ...Object.keys(returnRoundingAttributes()), + ]) { + if (attr in component.attributes) { + attributesToConvert[attr] = component.attributes[attr]; + } + } + + let newNamespace = component.attributes.newNamespace?.primitive; + + // allow one to override the fixed and isResponse attributes + // as well as rounding settings + // by specifying it on the sequence + let attributesFromComposite = {}; + + if (Object.keys(attributesToConvert).length > 0) { + attributesFromComposite = convertAttributesForComponentType({ + attributes: attributesToConvert, + componentType: "number", + componentInfoObjects, + compositeCreatesNewNamespace: newNamespace, + }); + } + + let childInfoByComponent = + await component.stateValues.childInfoByComponent; + + let numComponents = await component.stateValues.numComponents; + for (let i = 0; i < numComponents; i++) { + let childInfo = childInfoByComponent[i]; + if (childInfo) { + let replacementSource = components[childInfo.childName]; + + if (childInfo.nComponents !== undefined) { + componentsCopied.push( + replacementSource.componentName + + ":" + + childInfo.component, ); + } else { + componentsCopied.push(replacementSource.componentName); } - - return { setValue: { maths } }; - }, - async inverseArrayDefinitionByKey({ - desiredStateVariableValues, - dependencyNamesByKey, - }) { - let instructions = []; - - for (let arrayKey in desiredStateVariableValues.maths) { - instructions.push({ - setDependency: dependencyNamesByKey[arrayKey].number, - desiredValue: - desiredStateVariableValues.maths[ - arrayKey - ].evaluate_to_constant(), - }); - } - - return { - success: true, - instructions, - }; - }, - }; - - stateVariableDefinitions.text = { - public: true, - forRenderer: true, - shadowingInstructions: { - createComponentOfType: "text", - }, - additionalStateVariablesDefined: ["texts"], - returnDependencies: () => ({ - numberAndNumberListChildren: { - dependencyType: "child", - childGroups: ["numbers", "numberLists"], - variableNames: ["valueForDisplay", "text", "texts"], - variablesOptional: true, - }, - numbersShadow: { - dependencyType: "stateVariable", - variableName: "numbersShadow", - }, - displayDigits: { - dependencyType: "stateVariable", - variableName: "displayDigits", - }, - displayDecimals: { - dependencyType: "stateVariable", - variableName: "displayDecimals", - }, - displaySmallAsZero: { - dependencyType: "stateVariable", - variableName: "displaySmallAsZero", - }, - padZeros: { - dependencyType: "stateVariable", - variableName: "padZeros", - }, - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "numberList", - variableName: "numComponentsToDisplayByChild", + } + replacements.push({ + componentType: "number", + attributes: JSON.parse(JSON.stringify(attributesFromComposite)), + downstreamDependencies: { + [component.componentName]: [ + { + dependencyType: "referenceShadow", + compositeName: component.componentName, + propVariable: `number${i + 1}`, + }, + ], }, - }), - definition: function ({ dependencyValues, componentName }) { - let texts = []; - let params = {}; - if (dependencyValues.padZeros) { - if (Number.isFinite(dependencyValues.displayDecimals)) { - params.padToDecimals = dependencyValues.displayDecimals; - } - if (dependencyValues.displayDigits >= 1) { - params.padToDigits = dependencyValues.displayDigits; - } - } - if (dependencyValues.numberAndNumberListChildren.length > 0) { - for (let child of dependencyValues.numberAndNumberListChildren) { - if (child.stateValues.valueForDisplay !== undefined) { - texts.push(child.stateValues.text); - } else { - texts.push(...child.stateValues.texts); - } - } - } else if (dependencyValues.numbersShadow !== null) { - texts = dependencyValues.numbersShadow.map((x) => - roundForDisplay({ - value: me.fromAst(x), - dependencyValues, - }).toString(params), - ); - } - - let numComponentsToDisplay = dependencyValues.numComponents; + }); + } + + workspace.uniqueIdentifiersUsed = []; + replacements = postProcessCopy({ + serializedComponents: replacements, + componentName: component.componentName, + uniqueIdentifiersUsed: workspace.uniqueIdentifiersUsed, + addShadowDependencies: true, + markAsPrimaryShadow: true, + }); - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent numberList, which could have limited - // number of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } - texts = texts.slice(0, numComponentsToDisplay); + let processResult = processAssignNames({ + assignNames: component.doenetAttributes.assignNames, + serializedComponents: replacements, + parentName: component.componentName, + parentCreatesNewNamespace: newNamespace, + componentInfoObjects, + }); + errors.push(...processResult.errors); + warnings.push(...processResult.warnings); - let text = texts.join(", "); + workspace.componentsCopied = componentsCopied; - return { setValue: { text, texts } }; - }, + return { + replacements: processResult.serializedComponents, + errors, + warnings, }; + } - stateVariableDefinitions.componentNamesInList = { - returnDependencies: () => ({ - numberAndNumberListChildren: { - dependencyType: "child", - childGroups: ["numbers", "numberLists"], - variableNames: ["componentNamesInList"], - variablesOptional: true, - }, - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - }), - definition: function ({ dependencyValues, componentInfoObjects }) { - let componentNamesInList = []; - - for (let child of dependencyValues.numberAndNumberListChildren) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - componentNamesInList.push( - ...child.stateValues.componentNamesInList, - ); - } else { - componentNamesInList.push(child.componentName); - } - } - - componentNamesInList = componentNamesInList.slice( - 0, - dependencyValues.numComponents, - ); + static async calculateReplacementChanges({ + component, + components, + componentInfoObjects, + workspace, + }) { + // TODO: don't yet have a way to return errors and warnings! + let errors = []; + let warnings = []; - return { setValue: { componentNamesInList } }; - }, - }; + let componentsToCopy = []; - stateVariableDefinitions.numComponentsToDisplayByChild = { - additionalStateVariablesDefined: ["numChildrenToRender"], - returnDependencies: () => ({ - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - numberListChildren: { - dependencyType: "child", - childGroups: ["numberLists"], - variableNames: ["numComponents"], - }, - numberAndNumberListChildren: { - dependencyType: "child", - childGroups: ["numbers", "numberLists"], - skipComponentNames: true, - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "numberList", - variableName: "numComponentsToDisplayByChild", - }, - }), - definition: function ({ - dependencyValues, - componentInfoObjects, - componentName, - }) { - let numComponentsToDisplay = dependencyValues.numComponents; - - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent numberList, which could have limited - // number of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } + let childInfoByComponent = + await component.stateValues.childInfoByComponent; - let numComponentsToDisplayByChild = {}; + for (let childInfo of childInfoByComponent) { + let replacementSource = components[childInfo.childName]; - let numComponentsSoFar = 0; - let numChildrenToRender = 0; + if (childInfo.nComponents !== undefined) { + componentsToCopy.push( + replacementSource.componentName + ":" + childInfo.component, + ); + } else { + componentsToCopy.push(replacementSource.componentName); + } + } + + if ( + componentsToCopy.length == workspace.componentsCopied.length && + workspace.componentsCopied.every( + (x, i) => x === componentsToCopy[i], + ) + ) { + return []; + } + + // for now, just recreate + let replacementResults = await this.createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }); - let nNumberLists = 0; - for (let child of dependencyValues.numberAndNumberListChildren) { - let numComponentsLeft = Math.max( - 0, - numComponentsToDisplay - numComponentsSoFar, - ); - if (numComponentsLeft > 0) { - numChildrenToRender++; - } - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - let numberListChild = - dependencyValues.numberListChildren[nNumberLists]; - nNumberLists++; - - let numComponentsForNumberListChild = Math.min( - numComponentsLeft, - numberListChild.stateValues.numComponents, - ); - - numComponentsToDisplayByChild[ - numberListChild.componentName - ] = numComponentsForNumberListChild; - numComponentsSoFar += numComponentsForNumberListChild; - } else { - numComponentsSoFar += 1; - } - } + let replacements = replacementResults.replacements; + errors.push(...replacementResults.errors); + warnings.push(...replacementResults.warnings); - return { - setValue: { - numComponentsToDisplayByChild, - numChildrenToRender, - }, - }; + let replacementChanges = [ + { + changeType: "add", + changeTopLevelReplacements: true, + firstReplacementInd: 0, + numberReplacementsToReplace: component.replacements.length, + serializedReplacements: replacements, }, - markStale: () => ({ updateRenderedChildren: true }), - }; + ]; - return stateVariableDefinitions; + return replacementChanges; } - - static adapters = [ - { - stateVariable: "maths", - componentType: "mathList", - stateVariablesToShadow: Object.keys( - returnRoundingStateVariableDefinitions(), - ), - }, - { - stateVariable: "math", - stateVariablesToShadow: Object.keys( - returnRoundingStateVariableDefinitions(), - ), - }, - "text", - ]; } diff --git a/packages/doenetml-worker/src/components/RegionBetweenCurves.js b/packages/doenetml-worker/src/components/RegionBetweenCurves.js index 7377e89d2..e9ad0f99b 100644 --- a/packages/doenetml-worker/src/components/RegionBetweenCurves.js +++ b/packages/doenetml-worker/src/components/RegionBetweenCurves.js @@ -120,8 +120,8 @@ export default class RegionBetweenCurves extends GraphicalComponent { dependencyValues.functions[1].stateValues.numOutputs !== 1 ) { return { - setValues: { - function: [() => NaN, () => NaN], + setValue: { + functions: [() => NaN, () => NaN], haveFunctions: false, fDefinitions: [{}, {}], }, diff --git a/packages/doenetml-worker/src/components/Substitute.js b/packages/doenetml-worker/src/components/Substitute.js index 3347f6da2..1e9381aa9 100644 --- a/packages/doenetml-worker/src/components/Substitute.js +++ b/packages/doenetml-worker/src/components/Substitute.js @@ -122,7 +122,7 @@ export default class Substitute extends CompositeComponent { let stateVariableDefinitions = super.returnStateVariableDefinitions(); let roundingDefinitions = returnRoundingStateVariableDefinitions({ - childsGroupIfSingleMatch: ["anything"], + childGroupsIfSingleMatch: ["anything"], }); Object.assign(stateVariableDefinitions, roundingDefinitions); diff --git a/packages/doenetml-worker/src/components/Text.js b/packages/doenetml-worker/src/components/Text.js index 8940229a2..bc1e9908d 100644 --- a/packages/doenetml-worker/src/components/Text.js +++ b/packages/doenetml-worker/src/components/Text.js @@ -7,7 +7,10 @@ import { returnSelectedStyleStateVariableDefinition, returnTextStyleDescriptionDefinitions, } from "@doenet/utils"; -import { textFromChildren } from "../utils/text"; +import { + returnTextPieceStateVariableDefinitions, + textFromChildren, +} from "../utils/text"; import { getLatexToMathConverter, getTextToMathConverter } from "../utils/math"; import InlineComponent from "./abstract/InlineComponent"; import me from "math-expressions"; @@ -93,6 +96,36 @@ export default class Text extends InlineComponent { let anchorDefinition = returnAnchorStateVariableDefinition(); Object.assign(stateVariableDefinitions, anchorDefinition); + stateVariableDefinitions.inUnorderedList = { + defaultValue: false, + returnDependencies: () => ({ + sourceCompositeUnordered: { + dependencyType: "sourceCompositeStateVariable", + variableName: "unordered", + }, + }), + definition({ dependencyValues, usedDefault }) { + if ( + dependencyValues.sourceCompositeUnordered !== null && + !usedDefault.sourceCompositeUnordered + ) { + return { + setValue: { + inUnorderedList: Boolean( + dependencyValues.sourceCompositeUnordered, + ), + }, + }; + } else { + return { + setValue: { + inUnorderedList: false, + }, + }; + } + }, + }; + stateVariableDefinitions.value = { public: true, shadowingInstructions: { @@ -136,9 +169,48 @@ export default class Text extends InlineComponent { dependencyValues, }) { let numChildren = dependencyValues.textLikeChildren.length; + if (numChildren > 1) { + // if have multiple children, then we could still update them if + // 1. all children come from a single composite with asList set to true, and + // 2. the desired value is a comma-separated list with the number of entries + // matching the number of children. + // In that case, we will attempt to update each child to the corresponding entry + // from the desired value. + + // Check if all text children are from a composite with asList set to true + let foundAllFromListComposite = false; + for (let range of dependencyValues.textLikeChildren + .compositeReplacementRange) { + if ( + range.asList && + range.firstInd === 0 && + range.lastInd === numChildren - 1 + ) { + foundAllFromListComposite = true; + } + } + + if (foundAllFromListComposite) { + // Check if desired value is a comma-separated list with the same number of entries as children + let splitValues = desiredStateVariableValues.value + .split(",") + .map((v) => v.trim()); + + if (splitValues.length === numChildren) { + // All conditions are met, so we attempt to update the children + let instructions = splitValues.map((v, i) => ({ + setDependency: "textLikeChildren", + desiredValue: v, + childIndex: i, + variableIndex: 0, + })); + return { success: true, instructions }; + } + } return { success: false }; } + if (numChildren === 1) { return { success: true, @@ -168,24 +240,6 @@ export default class Text extends InlineComponent { }, }; - stateVariableDefinitions.numCharacters = { - public: true, - shadowingInstructions: { - createComponentOfType: "integer", - }, - returnDependencies: () => ({ - value: { - dependencyType: "stateVariable", - variableName: "value", - }, - }), - definition({ dependencyValues }) { - return { - setValue: { numCharacters: dependencyValues.value.length }, - }; - }, - }; - stateVariableDefinitions.text = { public: true, shadowingInstructions: { @@ -296,6 +350,9 @@ export default class Text extends InlineComponent { }, }; + let pieceDefs = returnTextPieceStateVariableDefinitions(); + Object.assign(stateVariableDefinitions, pieceDefs); + return stateVariableDefinitions; } diff --git a/packages/doenetml-worker/src/components/TextInput.js b/packages/doenetml-worker/src/components/TextInput.js index ed635f638..8f801059e 100644 --- a/packages/doenetml-worker/src/components/TextInput.js +++ b/packages/doenetml-worker/src/components/TextInput.js @@ -7,6 +7,7 @@ import { returnLabelStateVariableDefinitions, returnWrapNonLabelsSugarFunction, } from "../utils/label"; +import { returnTextPieceStateVariableDefinitions } from "../utils/text"; import Input from "./abstract/Input"; export default class Textinput extends Input { @@ -100,6 +101,7 @@ export default class Textinput extends Input { sugarInstructions.push({ replacementFunction: returnWrapNonLabelsSugarFunction({ wrappingComponentType: "text", + wrapSingleIfNotWrappingComponentType: true, }), }); @@ -452,6 +454,9 @@ export default class Textinput extends Input { definition: () => ({ setValue: { componentType: "text" } }), }; + let pieceDefs = returnTextPieceStateVariableDefinitions(); + Object.assign(stateVariableDefinitions, pieceDefs); + return stateVariableDefinitions; } diff --git a/packages/doenetml-worker/src/components/TextList.js b/packages/doenetml-worker/src/components/TextList.js index c0d217960..d1613d3d1 100644 --- a/packages/doenetml-worker/src/components/TextList.js +++ b/packages/doenetml-worker/src/components/TextList.js @@ -1,9 +1,18 @@ -import InlineComponent from "./abstract/InlineComponent"; +import CompositeComponent from "./abstract/CompositeComponent"; import { returnGroupIntoComponentTypeSeparatedBySpacesOutsideParens } from "./commonsugar/lists"; +import { + convertAttributesForComponentType, + postProcessCopy, +} from "../utils/copy"; +import { processAssignNames } from "../utils/naming"; -export default class TextList extends InlineComponent { +export default class TextList extends CompositeComponent { static componentType = "textList"; - static renderChildren = true; + + static stateVariableToEvaluateAfterReplacements = + "readyToExpandWhenResolved"; + + static assignNamesToReplacements = true; static includeBlankStringChildren = true; static removeBlankStringChildrenPostSugar = true; @@ -17,6 +26,8 @@ export default class TextList extends InlineComponent { // don't required composite replacements static descendantCompositesMustHaveAReplacement = false; + static doNotExpandAsShadowed = true; + static createAttributesObject() { let attributes = super.createAttributesObject(); @@ -30,10 +41,18 @@ export default class TextList extends InlineComponent { attributes.maxNumber = { createComponentOfType: "number", createStateVariable: "maxNumber", - defaultValue: null, + defaultValue: Infinity, public: true, }; + attributes.fixed = { + leaveRaw: true, + }; + + attributes.isResponse = { + leaveRaw: true, + }; + return attributes; } @@ -63,24 +82,12 @@ export default class TextList extends InlineComponent { group: "texts", componentTypes: ["text"], }, - { - group: "textLists", - componentTypes: ["textList"], - }, ]; } static returnStateVariableDefinitions() { let stateVariableDefinitions = super.returnStateVariableDefinitions(); - // set overrideChildHide so that children are hidden - // only based on whether or not the list is hidden - // so that can't have a list with partially hidden components - stateVariableDefinitions.overrideChildHide = { - returnDependencies: () => ({}), - definition: () => ({ setValue: { overrideChildHide: true } }), - }; - stateVariableDefinitions.textsShadow = { defaultValue: null, hasEssential: true, @@ -92,27 +99,28 @@ export default class TextList extends InlineComponent { }), }; + stateVariableDefinitions.asList = { + returnDependencies: () => ({}), + definition() { + return { setValue: { asList: true } }; + }, + }; + stateVariableDefinitions.numComponents = { public: true, shadowingInstructions: { createComponentOfType: "number", }, - additionalStateVariablesDefined: ["childIndexByArrayKey"], + additionalStateVariablesDefined: ["childNameByComponent"], returnDependencies() { return { maxNumber: { dependencyType: "stateVariable", variableName: "maxNumber", }, - textListChildren: { + textChildren: { dependencyType: "child", - childGroups: ["textLists"], - variableNames: ["numComponents"], - }, - textAndTextListChildren: { - dependencyType: "child", - childGroups: ["texts", "textLists"], - skipComponentNames: true, + childGroups: ["texts"], }, textsShadow: { dependencyType: "stateVariable", @@ -120,70 +128,42 @@ export default class TextList extends InlineComponent { }, }; }, - definition: function ({ dependencyValues, componentInfoObjects }) { + definition: function ({ dependencyValues }) { let numComponents = 0; - let childIndexByArrayKey = []; - - if (dependencyValues.textAndTextListChildren.length > 0) { - let nTextLists = 0; - for (let [ - childInd, - child, - ] of dependencyValues.textAndTextListChildren.entries()) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "textList", - }) - ) { - let textListChild = - dependencyValues.textListChildren[nTextLists]; - nTextLists++; - for ( - let i = 0; - i < textListChild.stateValues.numComponents; - i++ - ) { - childIndexByArrayKey[numComponents + i] = [ - childInd, - i, - ]; - } - numComponents += - textListChild.stateValues.numComponents; - } else { - childIndexByArrayKey[numComponents] = [childInd, 0]; - numComponents += 1; - } - } + let childNameByComponent = []; + + if (dependencyValues.textChildren.length > 0) { + childNameByComponent = dependencyValues.textChildren.map( + (x) => x.componentName, + ); + numComponents = dependencyValues.textChildren.length; } else if (dependencyValues.textsShadow !== null) { numComponents = dependencyValues.textsShadow.length; } let maxNum = dependencyValues.maxNumber; - if (maxNum !== null && numComponents > maxNum) { + if (numComponents > maxNum) { numComponents = maxNum; - childIndexByArrayKey = childIndexByArrayKey.slice( + childNameByComponent = childNameByComponent.slice( 0, maxNum, ); } return { - setValue: { numComponents, childIndexByArrayKey }, + setValue: { numComponents, childNameByComponent }, checkForActualChange: { numComponents: true }, }; }, }; stateVariableDefinitions.texts = { - public: true, shadowingInstructions: { createComponentOfType: "text", }, isArray: true, entryPrefixes: ["text"], - stateVariablesDeterminingDependencies: ["childIndexByArrayKey"], + stateVariablesDeterminingDependencies: ["childNameByComponent"], returnArraySizeDependencies: () => ({ numComponents: { dependencyType: "stateVariable", @@ -197,9 +177,9 @@ export default class TextList extends InlineComponent { returnArrayDependenciesByKey({ arrayKeys, stateValues }) { let dependenciesByKey = {}; let globalDependencies = { - childIndexByArrayKey: { + childNameByComponent: { dependencyType: "stateVariable", - variableName: "childIndexByArrayKey", + variableName: "childNameByComponent", }, textsShadow: { dependencyType: "stateVariable", @@ -209,20 +189,14 @@ export default class TextList extends InlineComponent { for (let arrayKey of arrayKeys) { let childIndices = []; - let textIndex = "1"; - if (stateValues.childIndexByArrayKey[arrayKey]) { - childIndices = [ - stateValues.childIndexByArrayKey[arrayKey][0], - ]; - textIndex = - stateValues.childIndexByArrayKey[arrayKey][1] + 1; + if (stateValues.childNameByComponent[arrayKey]) { + childIndices = [arrayKey]; } dependenciesByKey[arrayKey] = { - textAndTextListChildren: { + textChildren: { dependencyType: "child", - childGroups: ["texts", "textLists"], - variableNames: ["value", "text" + textIndex], - variablesOptional: true, + childGroups: ["texts"], + variableNames: ["value"], childIndices, }, }; @@ -238,21 +212,10 @@ export default class TextList extends InlineComponent { let texts = {}; for (let arrayKey of arrayKeys) { - let child = - dependencyValuesByKey[arrayKey] - .textAndTextListChildren[0]; + let child = dependencyValuesByKey[arrayKey].textChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - texts[arrayKey] = child.stateValues.value; - } else { - let textIndex = - globalDependencyValues.childIndexByArrayKey[ - arrayKey - ][1] + 1; - texts[arrayKey] = - child.stateValues["text" + textIndex]; - } + texts[arrayKey] = child.stateValues.value; } else if (globalDependencyValues.textsShadow !== null) { texts[arrayKey] = globalDependencyValues.textsShadow[arrayKey]; @@ -266,7 +229,7 @@ export default class TextList extends InlineComponent { globalDependencyValues, dependencyValuesByKey, dependencyNamesByKey, - arraySize, + workspace, }) { let instructions = []; @@ -275,32 +238,29 @@ export default class TextList extends InlineComponent { continue; } - let child = - dependencyValuesByKey[arrayKey] - .textAndTextListChildren[0]; + let child = dependencyValuesByKey[arrayKey].textChildren[0]; if (child) { - if (child.stateValues.value !== undefined) { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .textAndTextListChildren, - desiredValue: - desiredStateVariableValues.texts[arrayKey], - childIndex: 0, - variableIndex: 0, - }); - } else { - instructions.push({ - setDependency: - dependencyNamesByKey[arrayKey] - .textAndTextListChildren, - desiredValue: - desiredStateVariableValues.texts[arrayKey], - childIndex: 0, - variableIndex: 1, - }); + instructions.push({ + setDependency: + dependencyNamesByKey[arrayKey].textChildren, + desiredValue: + desiredStateVariableValues.texts[arrayKey], + childIndex: 0, + variableIndex: 0, + }); + } else if (globalDependencyValues.textsShadow !== null) { + if (!workspace.desiredTextShadow) { + workspace.desiredTextShadow = [ + ...globalDependencyValues.textsShadow, + ]; } + workspace.desiredTextShadow[arrayKey] = + desiredStateVariableValues.texts[arrayKey]; + instructions.push({ + setDependency: "textsShadow", + desiredValue: workspace.desiredTextShadow, + }); } } @@ -321,158 +281,170 @@ export default class TextList extends InlineComponent { targetVariableName: "texts", }; - stateVariableDefinitions.text = { - public: true, - shadowingInstructions: { - createComponentOfType: "text", - }, - forRenderer: true, + stateVariableDefinitions.readyToExpandWhenResolved = { returnDependencies: () => ({ - texts: { + childNameByComponent: { dependencyType: "stateVariable", - variableName: "texts", + variableName: "childNameByComponent", }, }), - definition: ({ dependencyValues }) => ({ - setValue: { text: dependencyValues.texts.join(", ") }, - }), + // When this state variable is marked stale + // it indicates we should update replacements. + // For this to work, must set + // stateVariableToEvaluateAfterReplacements + // to this variable so that it is marked fresh + markStale: () => ({ updateReplacements: true }), + definition: function () { + return { setValue: { readyToExpandWhenResolved: true } }; + }, }; - stateVariableDefinitions.componentNamesInList = { - returnDependencies: () => ({ - textAndTextListChildren: { - dependencyType: "child", - childGroups: ["texts", "textLists"], - variableNames: ["componentNamesInList"], - variablesOptional: true, - }, - maxNumber: { - dependencyType: "stateVariable", - variableName: "maxNumber", - }, - }), - definition: function ({ dependencyValues, componentInfoObjects }) { - let componentNamesInList = []; - - for (let child of dependencyValues.textAndTextListChildren) { - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "textList", - }) - ) { - componentNamesInList.push( - ...child.stateValues.componentNamesInList, - ); - } else { - componentNamesInList.push(child.componentName); - } - } + return stateVariableDefinitions; + } - let maxNum = dependencyValues.maxNumber; - if (maxNum !== null && componentNamesInList.length > maxNum) { - maxNum = Math.max(0, Math.floor(maxNum)); - componentNamesInList = componentNamesInList.slice( - 0, - maxNum, - ); - } + static async createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }) { + let errors = []; + let warnings = []; + + let replacements = []; + let componentsCopied = []; + + let attributesToConvert = {}; + for (let attr of ["fixed", "isResponse"]) { + if (attr in component.attributes) { + attributesToConvert[attr] = component.attributes[attr]; + } + } + + let newNamespace = component.attributes.newNamespace?.primitive; + + // allow one to override the fixed and isResponse attributes + // as well as rounding settings + // by specifying it on the sequence + let attributesFromComposite = {}; + + if (Object.keys(attributesToConvert).length > 0) { + attributesFromComposite = convertAttributesForComponentType({ + attributes: attributesToConvert, + componentType: "text", + componentInfoObjects, + compositeCreatesNewNamespace: newNamespace, + }); + } - return { setValue: { componentNamesInList } }; - }, - }; + let childNameByComponent = + await component.stateValues.childNameByComponent; - stateVariableDefinitions.numComponentsToDisplayByChild = { - additionalStateVariablesDefined: ["numChildrenToRender"], - returnDependencies: () => ({ - numComponents: { - dependencyType: "stateVariable", - variableName: "numComponents", - }, - textListChildren: { - dependencyType: "child", - childGroups: ["textLists"], - variableNames: ["numComponents"], - }, - textAndTextListChildren: { - dependencyType: "child", - childGroups: ["texts", "textLists"], - skipComponentNames: true, - }, - parentNComponentsToDisplayByChild: { - dependencyType: "parentStateVariable", - parentComponentType: "textList", - variableName: "numComponentsToDisplayByChild", + let numComponents = await component.stateValues.numComponents; + for (let i = 0; i < numComponents; i++) { + let childName = childNameByComponent[i]; + let replacementSource = components[childName]; + + if (replacementSource) { + componentsCopied.push(replacementSource.componentName); + } + replacements.push({ + componentType: "text", + attributes: JSON.parse(JSON.stringify(attributesFromComposite)), + downstreamDependencies: { + [component.componentName]: [ + { + dependencyType: "referenceShadow", + compositeName: component.componentName, + propVariable: `text${i + 1}`, + }, + ], }, - }), - definition: function ({ - dependencyValues, - componentInfoObjects, - componentName, - }) { - let numComponentsToDisplay = dependencyValues.numComponents; - - if ( - dependencyValues.parentNComponentsToDisplayByChild !== null - ) { - // have a parent textList, which could have limited - // text of components to display - numComponentsToDisplay = - dependencyValues.parentNComponentsToDisplayByChild[ - componentName - ]; - } + }); + } + + workspace.uniqueIdentifiersUsed = []; + replacements = postProcessCopy({ + serializedComponents: replacements, + componentName: component.componentName, + uniqueIdentifiersUsed: workspace.uniqueIdentifiersUsed, + addShadowDependencies: true, + markAsPrimaryShadow: true, + }); - let numComponentsToDisplayByChild = {}; + let processResult = processAssignNames({ + assignNames: component.doenetAttributes.assignNames, + serializedComponents: replacements, + parentName: component.componentName, + parentCreatesNewNamespace: newNamespace, + componentInfoObjects, + }); + errors.push(...processResult.errors); + warnings.push(...processResult.warnings); - let numComponentsSoFar = 0; - let numChildrenToRender = 0; + workspace.componentsCopied = componentsCopied; - let nTextLists = 0; - for (let child of dependencyValues.textAndTextListChildren) { - let numComponentsLeft = Math.max( - 0, - numComponentsToDisplay - numComponentsSoFar, - ); - if (numComponentsLeft > 0) { - numChildrenToRender++; - } - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "textList", - }) - ) { - let textListChild = - dependencyValues.textListChildren[nTextLists]; - nTextLists++; - - let numComponentsForTextListChild = Math.min( - numComponentsLeft, - textListChild.stateValues.numComponents, - ); - - numComponentsToDisplayByChild[ - textListChild.componentName - ] = numComponentsForTextListChild; - numComponentsSoFar += numComponentsForTextListChild; - } else { - numComponentsSoFar += 1; - } - } + return { + replacements: processResult.serializedComponents, + errors, + warnings, + }; + } - return { - setValue: { - numComponentsToDisplayByChild, - numChildrenToRender, - }, - }; + static async calculateReplacementChanges({ + component, + components, + componentInfoObjects, + workspace, + }) { + // TODO: don't yet have a way to return errors and warnings! + let errors = []; + let warnings = []; + + let componentsToCopy = []; + + let childNameByComponent = + await component.stateValues.childNameByComponent; + + for (let childName of childNameByComponent) { + let replacementSource = components[childName]; + + if (replacementSource) { + componentsToCopy.push(replacementSource.componentName); + } + } + + if ( + componentsToCopy.length == workspace.componentsCopied.length && + workspace.componentsCopied.every( + (x, i) => x === componentsToCopy[i], + ) + ) { + return []; + } + + // for now, just recreate + let replacementResults = await this.createSerializedReplacements({ + component, + components, + componentInfoObjects, + workspace, + }); + + let replacements = replacementResults.replacements; + errors.push(...replacementResults.errors); + warnings.push(...replacementResults.warnings); + + let replacementChanges = [ + { + changeType: "add", + changeTopLevelReplacements: true, + firstReplacementInd: 0, + numberReplacementsToReplace: component.replacements.length, + serializedReplacements: replacements, }, - markStale: () => ({ updateRenderedChildren: true }), - }; + ]; - return stateVariableDefinitions; + return replacementChanges; } - - static adapters = ["text"]; } diff --git a/packages/doenetml-worker/src/components/TupleList.js b/packages/doenetml-worker/src/components/TupleList.js index 1fd4a9114..a0fe97168 100644 --- a/packages/doenetml-worker/src/components/TupleList.js +++ b/packages/doenetml-worker/src/components/TupleList.js @@ -3,7 +3,6 @@ import { breakEmbeddedStringsIntoParensPieces } from "./commonsugar/breakstrings export default class TupleList extends MathList { static componentType = "tupleList"; - static rendererType = "mathList"; static includeBlankStringChildren = false; diff --git a/packages/doenetml-worker/src/components/When.js b/packages/doenetml-worker/src/components/When.js index 2617b4548..fbee1133d 100644 --- a/packages/doenetml-worker/src/components/When.js +++ b/packages/doenetml-worker/src/components/When.js @@ -126,34 +126,18 @@ export default class When extends BooleanComponent { dependencyType: "stateVariable", variableName: "booleanChildrenByCode", }, - booleanListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "booleanListChildrenByCode", - }, textChildrenByCode: { dependencyType: "stateVariable", variableName: "textChildrenByCode", }, - textListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "textListChildrenByCode", - }, mathChildrenByCode: { dependencyType: "stateVariable", variableName: "mathChildrenByCode", }, - mathListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "mathListChildrenByCode", - }, numberChildrenByCode: { dependencyType: "stateVariable", variableName: "numberChildrenByCode", }, - numberListChildrenByCode: { - dependencyType: "stateVariable", - variableName: "numberListChildrenByCode", - }, otherChildrenByCode: { dependencyType: "stateVariable", variableName: "otherChildrenByCode", diff --git a/packages/doenetml-worker/src/components/abstract/MathBaseOperator.js b/packages/doenetml-worker/src/components/abstract/MathBaseOperator.js index b88298846..1175d1ef4 100644 --- a/packages/doenetml-worker/src/components/abstract/MathBaseOperator.js +++ b/packages/doenetml-worker/src/components/abstract/MathBaseOperator.js @@ -79,14 +79,6 @@ export default class MathOperator extends MathComponent { group: "numbers", componentTypes: ["number"], }, - { - group: "mathLists", - componentTypes: ["mathList"], - }, - { - group: "numberLists", - componentTypes: ["numberList"], - }, ]; } @@ -94,13 +86,7 @@ export default class MathOperator extends MathComponent { let stateVariableDefinitions = super.returnStateVariableDefinitions(); let roundingDefinitions = returnRoundingStateVariableDefinitions({ - childsGroupIfSingleMatch: [ - "maths", - "numbers", - "mathLists", - "numberLists", - ], - includeListParents: true, + childGroupsIfSingleMatch: ["maths", "numbers"], }); Object.assign(stateVariableDefinitions, roundingDefinitions); @@ -120,11 +106,6 @@ export default class MathOperator extends MathComponent { variableNames: ["isNumber"], variablesOptional: true, }, - mathListChildren: { - dependencyType: "child", - childGroups: ["mathLists"], - variableNames: ["maths"], - }, shadowSource: { dependencyType: "shadowSource", variableNames: ["isNumericOperator"], @@ -136,10 +117,7 @@ export default class MathOperator extends MathComponent { isNumericOperator = true; } else if (dependencyValues.forceSymbolic) { isNumericOperator = false; - } else if ( - dependencyValues.mathChildren.length === 0 && - dependencyValues.mathListChildren.length === 0 - ) { + } else if (dependencyValues.mathChildren.length === 0) { isNumericOperator = dependencyValues.shadowSource?.stateValues .isNumericOperator; @@ -149,15 +127,9 @@ export default class MathOperator extends MathComponent { } else { // have math children and aren't forced to be numeric or symbolic // will be numeric only if have all math children are numbers - isNumericOperator = - dependencyValues.mathChildren.every( - (x) => x.stateValues.isNumber, - ) && - dependencyValues.mathListChildren.every((x) => - x.stateValues.maths.every((y) => - Number.isFinite(y.tree), - ), - ); + isNumericOperator = dependencyValues.mathChildren.every( + (x) => x.stateValues.isNumber, + ); } return { setValue: { isNumericOperator } }; @@ -198,19 +170,8 @@ export default class MathOperator extends MathComponent { returnDependencies: () => ({ mathNumberChildren: { dependencyType: "child", - childGroups: [ - "maths", - "numbers", - "mathLists", - "numberLists", - ], - variableNames: [ - "value", - "maths", - "numbers", - "canBeModified", - ], - variablesOptional: true, + childGroups: ["maths", "numbers"], + variableNames: ["value", "canBeModified"], }, isNumericOperator: { dependencyType: "stateVariable", @@ -248,32 +209,11 @@ export default class MathOperator extends MathComponent { }) ) { inputs.push(child.stateValues.value); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "math", - }) - ) { + } else { + // math let value = child.stateValues.value.evaluate_to_constant(); inputs.push(value); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - inputs.push(...child.stateValues.numbers); - } else { - // mathLIst - let values = child.stateValues.maths.map((x) => { - let value = x.evaluate_to_constant(); - if (!Number.isFinite(value)) { - value = NaN; - } - return value; - }); - inputs.push(...values); } } @@ -294,27 +234,9 @@ export default class MathOperator extends MathComponent { }) ) { inputs.push(me.fromAst(child.stateValues.value)); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "math", - }) - ) { - inputs.push(child.stateValues.value); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - inputs.push( - ...child.stateValues.numbers.map((x) => - me.fromAst(x), - ), - ); } else { - // mathList - inputs.push(...child.stateValues.maths); + // math + inputs.push(child.stateValues.value); } } @@ -353,12 +275,8 @@ export default class MathOperator extends MathComponent { child.stateValues.canBeModified, ); inputToChildIndex.push(childInd); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "math", - }) - ) { + } else { + // math let value = child.stateValues.value.evaluate_to_constant(); inputs.push(value); @@ -366,52 +284,6 @@ export default class MathOperator extends MathComponent { child.stateValues.canBeModified, ); inputToChildIndex.push(childInd); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - inputs.push(...child.stateValues.numbers); - canBeModified.push( - ...Array( - child.stateValues.numbers.length, - ).fill(child.stateValues.canBeModified), - ); - if (child.stateValues.numbers.length === 1) { - inputToChildIndex.push(childInd); - } else { - // TODO: invert entries of numberlist that isn't length 1? - inputToChildIndex.push( - ...Array( - child.stateValues.numbers.length, - ).fill(NaN), - ); - } - } else { - // mathList - let values = child.stateValues.maths.map( - (x) => { - let value = x.evaluate_to_constant(); - return value; - }, - ); - inputs.push(...values); - canBeModified.push( - ...Array( - child.stateValues.maths.length, - ).fill(child.stateValues.canBeModified), - ); - if (child.stateValues.maths.length === 1) { - inputToChildIndex.push(childInd); - } else { - // TODO: invert entries of mathlist that isn't length 1? - inputToChildIndex.push( - ...Array( - child.stateValues.maths.length, - ).fill(NaN), - ); - } } } let results = dependencyValues.inverseNumericOperator({ @@ -428,38 +300,7 @@ export default class MathOperator extends MathComponent { inputToChildIndex[results.inputNumber]; if (Number.isFinite(childIndex)) { let desiredValue = results.inputValue; - let variableIndex = 0; - - let child = - dependencyValues.mathNumberChildren[ - childIndex - ]; - if ( - componentInfoObjects.isInheritedComponentType( - { - inheritedComponentType: - child.componentType, - baseComponentType: "numberList", - }, - ) - ) { - variableIndex = 2; - // if had childIndex, must have been just one number - desiredValue = { 0: desiredValue }; - } else if ( - componentInfoObjects.isInheritedComponentType( - { - inheritedComponentType: - child.componentType, - baseComponentType: "mathList", - }, - ) - ) { - variableIndex = 1; - // if had childIndex, must have been just one math - desiredValue = { 0: desiredValue }; - } return { success: true, instructions: [ @@ -467,7 +308,7 @@ export default class MathOperator extends MathComponent { setDependency: "mathNumberChildren", desiredValue, childIndex, - variableIndex, + variableIndex: 0, }, ], }; @@ -497,59 +338,11 @@ export default class MathOperator extends MathComponent { inputs.push(me.fromAst(child.stateValues.value)); canBeModified.push(child.stateValues.canBeModified); inputToChildIndex.push(childInd); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "math", - }) - ) { + } else { + // math inputs.push(child.stateValues.value); canBeModified.push(child.stateValues.canBeModified); inputToChildIndex.push(childInd); - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - inputs.push( - ...child.stateValues.numbers.map((x) => - me.fromAst(x), - ), - ); - canBeModified.push( - ...Array(child.stateValues.numbers.length).fill( - child.stateValues.canBeModified, - ), - ); - if (child.stateValues.numbers.length === 1) { - inputToChildIndex.push(childInd); - } else { - // TODO: invert entries of numberlist that isn't length 1? - inputToChildIndex.push( - ...Array( - child.stateValues.numbers.length, - ).fill(NaN), - ); - } - } else { - // mathList - inputs.push(...child.stateValues.maths); - canBeModified.push( - ...Array(child.stateValues.maths.length).fill( - child.stateValues.canBeModified, - ), - ); - if (child.stateValues.maths.length === 1) { - inputToChildIndex.push(childInd); - } else { - // TODO: invert entries of mathlist that isn't length 1? - inputToChildIndex.push( - ...Array( - child.stateValues.maths.length, - ).fill(NaN), - ); - } } } @@ -564,30 +357,6 @@ export default class MathOperator extends MathComponent { let childIndex = inputToChildIndex[results.inputNumber]; if (Number.isFinite(childIndex)) { let desiredValue = results.inputValue; - let variableIndex = 0; - - let child = - dependencyValues.mathNumberChildren[childIndex]; - - if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "numberList", - }) - ) { - variableIndex = 2; - // if had childIndex, must have been just one number - desiredValue = { 0: desiredValue }; - } else if ( - componentInfoObjects.isInheritedComponentType({ - inheritedComponentType: child.componentType, - baseComponentType: "mathList", - }) - ) { - variableIndex = 1; - // if had childIndex, must have been just one math - desiredValue = { 0: desiredValue }; - } return { success: true, @@ -596,7 +365,7 @@ export default class MathOperator extends MathComponent { setDependency: "mathNumberChildren", desiredValue, childIndex, - variableIndex, + variableIndex: 0, }, ], }; @@ -613,7 +382,7 @@ export default class MathOperator extends MathComponent { }; // create new version on canBeModified that is true only if - // there is just one child or child list component that can be modified + // there is just one child component that can be modified // and we have a inverseMathOperator/inverseNumberOperator stateVariableDefinitions.canBeModified = { returnDependencies: () => ({ @@ -630,11 +399,6 @@ export default class MathOperator extends MathComponent { childGroups: ["maths", "numbers"], variableNames: ["canBeModified"], }, - mathNumberListChildren: { - dependencyType: "child", - childGroups: ["mathLists", "numberLists"], - variableNames: ["numComponents"], - }, isNumericOperator: { dependencyType: "stateVariable", variableName: "isNumericOperator", @@ -662,19 +426,11 @@ export default class MathOperator extends MathComponent { // TODO: if there are no children, canBeModified may be incorrectly set to true // But, we include this exception so that canBeModified is not set to false // in macros, where children aren't copied - if ( - dependencyValues.mathNumberChildren.length + - dependencyValues.mathNumberListChildren.length > - 0 - ) { + if (dependencyValues.mathNumberChildren.length > 0) { let nModifiable = dependencyValues.mathNumberChildren.filter( (x) => x.stateValues.canBeModified, - ).length + - dependencyValues.mathNumberListChildren.reduce( - (a, c) => a + c.stateValues.numComponents, - 0, - ); + ).length; if (nModifiable !== 1) { canBeModified = false; diff --git a/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts b/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts index e8f198841..a8a3cdc26 100644 --- a/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts +++ b/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts @@ -880,13 +880,15 @@ async function test_answer_multiple_inputs({ awardsUsed?: string[]; }[]; answerName?: string; - inputs: { type: "math" | "text" | "boolean"; name?: string }[]; + inputs: { type: "math" | "number" | "text" | "boolean"; name?: string }[]; }) { let fromLatexBase = getLatexToMathConverter(); let fromLatex = (x: string) => fromLatexBase(normalizeLatexString(x)); let currentResponses = inputs.map((input) => { if (input.type === "math") { return "\uff3f"; + } else if (input.type === "number") { + return NaN; } else if (input.type === "text") { return ""; } else { @@ -914,7 +916,10 @@ async function test_answer_multiple_inputs({ function transformOutputValues(values: any[]) { return values.map((val, i) => { - if (inputs[i].type === "math") { + if ( + (inputs[i].type === "math" || inputs[i].type === "number") && + val.tree !== undefined + ) { val = val.tree; } return val; @@ -925,6 +930,8 @@ async function test_answer_multiple_inputs({ return values.map((val, i) => { if (inputs[i].type === "math") { val = fromLatex(val).tree; + } else if (inputs[i].type === "number") { + val = fromLatex(val).evaluate_to_constant(); } else if (inputs[i].type === "boolean") { val = val === "true"; } @@ -946,11 +953,15 @@ async function test_answer_multiple_inputs({ inputs[i].type === "math" ? x.tree : x, ), ).eqls(submittedResponses); - expect( - transformOutputValues( - inputNames.map((name) => stateVariables[name].stateValues.value), - ), - ).eqls(currentResponses); + if (inputs.every((x) => x.type !== "number")) { + expect( + transformOutputValues( + inputNames.map( + (name) => stateVariables[name].stateValues.value, + ), + ), + ).eqls(currentResponses); + } for (let response of answers) { if (response.preAction) { @@ -977,7 +988,7 @@ async function test_answer_multiple_inputs({ // Type answers in for (let [ind, input] of inputs.entries()) { - if (input.type === "math") { + if (input.type === "math" || input.type === "number") { await updateMathInputValue({ latex: values[ind], componentName: inputNames[ind], @@ -1344,6 +1355,20 @@ describe("Answer tag tests", async () => { }); }); + it("answer sugar from one string, set to boolean 2", async () => { + const doenetML = ` + not false + `; + + await test_boolean_answer({ + doenetML, + answers: [ + { boolean: true, credit: 1 }, + { boolean: false, credit: 0 }, + ], + }); + }); + it("answer sugar from one boolean", async () => { const doenetML = ` true @@ -1745,6 +1770,35 @@ describe("Answer tag tests", async () => { }); }); + it("answer from numberList", async () => { + const doenetML = ` + + + $mi1 $mi2 = 1 2 + + + `; + + await test_answer_multiple_inputs({ + doenetML, + answers: [ + { values: ["1", "2"], credit: 1 }, + { values: ["3", "2"], credit: 0.5 }, + { values: ["3", ""], credit: 0 }, + { values: ["2", ""], credit: 0.5 }, + { values: ["", "2"], credit: 0.5 }, + { values: ["", "3"], credit: 0 }, + { values: ["", "1"], credit: 0.5 }, + { values: ["1", ""], credit: 0.5 }, + { values: ["2", "1"], credit: 0.5 }, + ], + inputs: [ + { type: "number", name: "/mi1" }, + { type: "number", name: "/mi2" }, + ], + }); + }); + it("answer award with text", async () => { const doenetML = ` hello there @@ -1843,7 +1897,7 @@ describe("Answer tag tests", async () => { }); }); - it("answer award with text, initally unresolved", async () => { + it("answer award with text, initially unresolved", async () => { const doenetML = ` $n @@ -1968,6 +2022,20 @@ describe("Answer tag tests", async () => { }); }); + it("answer set to boolean, award with sugared string", async () => { + const doenetML = ` + not false + `; + + await test_boolean_answer({ + doenetML, + answers: [ + { boolean: true, credit: 1 }, + { boolean: false, credit: 0 }, + ], + }); + }); + it("answer award with sugared boolean and string", async () => { const doenetML = ` false @@ -1983,6 +2051,56 @@ describe("Answer tag tests", async () => { }); }); + it("answer from booleanList", async () => { + const doenetML = ` + + + $bi1 $bi2= false true + + + `; + + await test_answer_multiple_inputs({ + doenetML, + answers: [ + { values: ["false", "true"], credit: 1 }, + { values: ["false", "false"], credit: 0.5 }, + { values: ["true", "false"], credit: 0 }, + { values: ["true", "true"], credit: 0.5 }, + ], + inputs: [ + { type: "boolean", name: "/bi1" }, + { type: "boolean", name: "/bi2" }, + ], + }); + }); + + it("answer with multiple booleans", async () => { + const doenetML = ` + + + false + + $bi1{isResponse} = not $b and $bi2{isResponse}=$b + + + `; + + await test_answer_multiple_inputs({ + doenetML, + answers: [ + { values: ["true", "false"], credit: 1 }, + { values: ["false", "true"], credit: 0 }, + { values: ["false", "false"], credit: 0 }, + { values: ["true", "true"], credit: 0 }, + ], + inputs: [ + { type: "boolean", name: "/bi1" }, + { type: "boolean", name: "/bi2" }, + ], + }); + }); + it("answer multiple shortcut awards", async () => { const doenetML = ` x+yx @@ -3689,6 +3807,60 @@ Enter any letter: }); }); + it("default is to split symbols, sugared answer", async () => { + const doenetML = ` +xyz + `; + await test_math_answer({ + doenetML, + answers: [ + { + latex: "xyza", + credit: 0, + }, + { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, + ], + }); + }); + + it("default is to split symbols, shortcut award, sugared math", async () => { + const doenetML = ` +xyz + `; + await test_math_answer({ + doenetML, + answers: [ + { + latex: "xyza", + credit: 0, + }, + { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, + ], + }); + }); + + it("default is to split symbols, explicit mathInput and math", async () => { + const doenetML = ` + + + xyz + + `; + await test_math_answer({ + doenetML, + answers: [ + { + latex: "xyza", + credit: 0, + }, + { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, + ], + }); + }); + it("with split symbols, specified directly on mathInput and math", async () => { const doenetML = `

split symbols:

@@ -3702,6 +3874,7 @@ Enter any letter: doenetML, answers: [ { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, { latex: "xyza", credit: 0, @@ -3712,6 +3885,7 @@ Enter any letter: }, }, { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, { latex: "xyzb", credit: 0, @@ -3723,6 +3897,7 @@ Enter any letter: overrideResponse: "xyzb", }, { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, ], }); }); @@ -3736,6 +3911,7 @@ Enter any letter: doenetML, answers: [ { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, { latex: "xyza", credit: 0, @@ -3746,6 +3922,7 @@ Enter any letter: }, }, { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, { latex: "xyzb", credit: 0, @@ -3757,6 +3934,7 @@ Enter any letter: overrideResponse: "xyzb", }, { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, ], }); }); @@ -3772,6 +3950,7 @@ Enter any letter: doenetML, answers: [ { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, { latex: "xyza", credit: 0, @@ -3782,6 +3961,7 @@ Enter any letter: }, }, { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, { latex: "xyzb", credit: 0, @@ -3793,6 +3973,7 @@ Enter any letter: overrideResponse: "xyzb", }, { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, ], }); }); @@ -3809,6 +3990,7 @@ Enter any letter: doenetML, answers: [ { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, { latex: "xyza", credit: 0, @@ -3819,6 +4001,7 @@ Enter any letter: }, }, { latex: "xyz", credit: 1 }, + { latex: "x y z", credit: 1 }, { latex: "xyzb", credit: 0, @@ -3830,6 +4013,7 @@ Enter any letter: overrideResponse: "xyzb", }, { latex: "xyz", credit: 1, overrideResponse: "xyz" }, + { latex: "x y z", credit: 0 }, ], }); }); @@ -3936,7 +4120,7 @@ Enter any letter: expect(stateVariables["/ans"].stateValues.creditAchieved).eq(1); }); - it("empty mathLists always equal", async () => { + it.skip("empty mathLists always equal", async () => { let core = await createTestCore({ doenetML: ` @@ -4858,7 +5042,7 @@ What is the derivative of x^2? }); }); - it("case-insensitive match, text", async () => { + it("case-insensitive match, math", async () => { await test_math_answer({ doenetML: `x+Y`, answers: [ @@ -4917,7 +5101,7 @@ What is the derivative of x^2? }); }); - it("case-insensitive match, math", async () => { + it("case-insensitive match, text", async () => { await test_text_answer({ doenetML: `Hello there!`, answers: [ diff --git a/packages/doenetml-worker/src/test/tagSpecific/boolean.test.ts b/packages/doenetml-worker/src/test/tagSpecific/boolean.test.ts index a13789eaf..26188e1e2 100644 --- a/packages/doenetml-worker/src/test/tagSpecific/boolean.test.ts +++ b/packages/doenetml-worker/src/test/tagSpecific/boolean.test.ts @@ -226,7 +226,7 @@ describe("Boolean tag tests", async () => { expect(stateVariables[`/b4`].stateValues.value).to.be.true; }); - it("boolean with lists", async () => { + it("boolean with lists and sequences", async () => { let core = await createTestCore({ doenetML: ` 1,2 = 1 2 @@ -239,17 +239,20 @@ describe("Boolean tag tests", async () => { 1 2 = 2 1 a b = b a true false = false true - = - = - = - = - 1 = 1 - 1 = 1 - 1 = 1 - 1 = 1 - a, b = a b - a, b = b a - a,b = a b + 1 = 1 + 1 = 1 + 1 = 1 + 1 = 1 + a, b = a b + a, b = b a + a,b = a b + 1 2 = + 1 2 = + 1, 2 = + d f = + 2x 3x = + + 1,2 = 2 1 1 2 = 2 1 @@ -270,12 +273,18 @@ describe("Boolean tag tests", async () => { = = a, b = b a + 12 = + (1, 2) = 1 2 + (1, 2) = 1 2 + 1, 2 = 1 2 + d e f = + 2x 3x = `, }); - let nTrues = 21, - nFalses = 19; + let nTrues = 22, + nFalses = 25; let stateVariables = await returnAllStateVariables(core); for (let i = 1; i <= nTrues; i++) { @@ -292,7 +301,7 @@ describe("Boolean tag tests", async () => { } }); - it("element of list, set, or string", async () => { + it("element of list, set, composite, or string", async () => { let elements = [ { element: "1", @@ -324,6 +333,12 @@ describe("Boolean tag tests", async () => { isElement: true, isElementCaseInsensitive: true, }, + { + element: "1", + set: "", + isElement: true, + isElementCaseInsensitive: true, + }, { element: "1", set: "1 2", @@ -354,6 +369,12 @@ describe("Boolean tag tests", async () => { isElement: true, isElementCaseInsensitive: true, }, + { + element: "1", + set: "", + isElement: true, + isElementCaseInsensitive: true, + }, { element: "1", set: "1 2", @@ -384,6 +405,18 @@ describe("Boolean tag tests", async () => { isElement: true, isElementCaseInsensitive: true, }, + { + element: "1", + set: "", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "", + isElement: true, + isElementCaseInsensitive: true, + }, { element: "3", set: "1 2", @@ -416,10 +449,27 @@ describe("Boolean tag tests", async () => { }, { element: "3", - set: "3", + set: "", isElement: false, isElementCaseInsensitive: false, - isInvalid: true, + }, + { + element: "3", + set: "3", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "3", + set: "3", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "3", + set: "3", + isElement: true, + isElementCaseInsensitive: true, }, { element: "2, 3", @@ -512,6 +562,12 @@ describe("Boolean tag tests", async () => { isElement: true, isElementCaseInsensitive: true, }, + { + element: "2x", + set: "", + isElement: true, + isElementCaseInsensitive: true, + }, { element: "2x", set: "x+x y/2", @@ -536,6 +592,18 @@ describe("Boolean tag tests", async () => { isElement: true, isElementCaseInsensitive: true, }, + { + element: "2x", + set: "", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+x", + isElement: true, + isElementCaseInsensitive: true, + }, { element: "2x", set: "x+X y/2", @@ -558,8 +626,7 @@ describe("Boolean tag tests", async () => { element: "2x", set: "x+X", isElement: false, - isElementCaseInsensitive: false, - isInvalid: true, + isElementCaseInsensitive: true, }, { element: "x", @@ -584,7 +651,6 @@ describe("Boolean tag tests", async () => { set: "abc", isElement: false, isElementCaseInsensitive: false, - isInvalid: true, }, { element: "b", @@ -625,8 +691,8 @@ describe("Boolean tag tests", async () => { { element: "b", set: "abc", - isElement: false, - isElementCaseInsensitive: false, + isElement: true, + isElementCaseInsensitive: true, }, { element: "b", @@ -760,20 +826,18 @@ describe("Boolean tag tests", async () => { { set1: "z", set2: "z 2x", - isSubset: false, - isSubsetCaseInsensitive: false, + isSubset: true, + isSubsetCaseInsensitive: true, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, }, { set1: "z", set2: "z 2x", - isSubset: false, - isSubsetCaseInsensitive: false, + isSubset: true, + isSubsetCaseInsensitive: true, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, }, { set1: "z", @@ -826,11 +890,10 @@ describe("Boolean tag tests", async () => { { set1: "3", set2: "[2,4)", - isSubset: false, - isSubsetCaseInsensitive: false, + isSubset: true, + isSubsetCaseInsensitive: true, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, }, { set1: "2,3", @@ -900,20 +963,98 @@ describe("Boolean tag tests", async () => { { set1: "hello", set2: "there hello bye", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "hello", + set2: "there hello bye", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "hello there", + set2: "there hello bye", isSubset: false, isSubsetCaseInsensitive: false, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, }, { - set1: "hello", + set1: "hello there", set2: "there hello bye", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "a c", + set2: "", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "a, c", + set2: "", isSubset: false, isSubsetCaseInsensitive: false, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, + }, + { + set1: "a, c", + set2: "", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "ace", + set2: "", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "ace", + set2: "", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "A c", + set2: "", + isSubset: false, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "a b", + set2: "", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "a b c", + set2: "", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: true, + isSupersetCaseInsensitive: true, }, { set1: "true true", @@ -942,20 +1083,18 @@ describe("Boolean tag tests", async () => { { set1: "true", set2: "true false", - isSubset: false, - isSubsetCaseInsensitive: false, + isSubset: true, + isSubsetCaseInsensitive: true, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, }, { set1: "true", set2: "true false", - isSubset: false, - isSubsetCaseInsensitive: false, + isSubset: true, + isSubsetCaseInsensitive: true, isSuperset: false, isSupersetCaseInsensitive: false, - isInvalid: true, }, ]; diff --git a/packages/doenetml-worker/src/test/tagSpecific/booleanlist.test.ts b/packages/doenetml-worker/src/test/tagSpecific/booleanlist.test.ts new file mode 100644 index 000000000..e50cb3852 --- /dev/null +++ b/packages/doenetml-worker/src/test/tagSpecific/booleanlist.test.ts @@ -0,0 +1,584 @@ +import { describe, expect, it, vi } from "vitest"; +import { createTestCore, returnAllStateVariables } from "../utils/test-core"; +import { + updateBooleanInputValue, + updateMathInputValue, +} from "../utils/actions"; + +const Mock = vi.fn(); +vi.stubGlobal("postMessage", Mock); + +describe("BooleanList tag tests", async () => { + async function test_booleanList({ + core, + name, + pName, + text, + booleans, + }: { + core: any; + name?: string; + pName?: string; + text?: string; + booleans?: any[]; + }) { + const stateVariables = await returnAllStateVariables(core); + + if (text !== undefined && pName !== undefined) { + expect(stateVariables[pName].stateValues.text).eq(text); + } + + if (booleans !== undefined && name !== undefined) { + expect(stateVariables[name].stateValues.booleans).eqls(booleans); + } + } + + it("booleanList from string", async () => { + let core = await createTestCore({ + doenetML: ` +

false true

+ `, + }); + + await test_booleanList({ + core, + name: "/bl1", + pName: "/p", + text: "false, true", + booleans: [false, true], + }); + }); + + it("booleanList with boolean children", async () => { + let core = await createTestCore({ + doenetML: ` +

+ false + not false +

+ +

+ falsenot false +

+ `, + }); + + let text = "false, true"; + let booleans = [false, true]; + + await test_booleanList({ + core, + name: "/bl1", + pName: "/p1", + text, + booleans, + }); + + await test_booleanList({ + core, + name: "/bl2", + pName: "/p2", + text, + booleans, + }); + }); + + it("booleanList with boolean and string children", async () => { + let core = await createTestCore({ + doenetML: ` +

+ false true + not false true not true +

+ `, + }); + + await test_booleanList({ + core, + name: "/bl1", + pName: "/p", + text: "false, true, true, true, false", + booleans: [false, true, true, true, false], + }); + }); + + async function test_nested_and_inverse(core: any) { + await test_booleanList({ + core, + name: "/bl1", + pName: "/p", + text: "true, true, false, true, false, false, true, true, false", + booleans: [ + true, + true, + false, + true, + false, + false, + true, + true, + false, + ], + }); + + await test_booleanList({ + core, + name: "/bl2", + booleans: [true, false], + }); + await test_booleanList({ + core, + name: "/bl3", + booleans: [false, false, true, true, false], + }); + await test_booleanList({ + core, + name: "/bl4", + booleans: [false, false, true], + }); + await test_booleanList({ + core, + name: "/bl5", + booleans: [false, true], + }); + await test_booleanList({ + core, + name: "/bl6", + booleans: [true, false], + }); + + // change values + + await updateBooleanInputValue({ + componentName: "/mi1", + boolean: false, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi2", + boolean: false, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi3", + boolean: true, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi4", + boolean: false, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi5", + boolean: true, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi6", + boolean: true, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi7", + boolean: false, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi8", + boolean: false, + core, + }); + await updateBooleanInputValue({ + componentName: "/mi9", + boolean: true, + core, + }); + + await test_booleanList({ + core, + name: "/bl1", + pName: "/p", + text: "false, false, true, false, true, true, false, false, true", + booleans: [ + false, + false, + true, + false, + true, + true, + false, + false, + true, + ], + }); + + await test_booleanList({ + core, + name: "/bl2", + booleans: [false, true], + }); + await test_booleanList({ + core, + name: "/bl3", + booleans: [true, true, false, false, true], + }); + await test_booleanList({ + core, + name: "/bl4", + booleans: [true, true, false], + }); + await test_booleanList({ + core, + name: "/bl5", + booleans: [true, false], + }); + await test_booleanList({ + core, + name: "/bl6", + booleans: [false, true], + }); + } + + it("booleanList with booleanList children, test inverse", async () => { + let core = await createTestCore({ + doenetML: ` +

+ true + true false + true + + + false + false true + + true false + +

+ + $bl1[1] + $bl1[2] + $bl1[3] + $bl1[4] + $bl1[5] + $bl1[6] + $bl1[7] + $bl1[8] + $bl1[9] + + `, + }); + + await test_nested_and_inverse(core); + }); + + it("booleanList with booleanList children and sugar, test inverse", async () => { + let core = await createTestCore({ + doenetML: ` +

+ true + true false + true + + + false + false true + + true false + +

+ + $bl1[1] + $bl1[2] + $bl1[3] + $bl1[4] + $bl1[5] + $bl1[6] + $bl1[7] + $bl1[8] + $bl1[9] + `, + }); + + await test_nested_and_inverse(core); + }); + + it("booleanList with maximum number", async () => { + let core = await createTestCore({ + doenetML: ` +

+ true + true false true false + false + + + false + false true + + false true true + +

+ `, + }); + + let vals6 = [false, true, true]; + let vals5 = [false, true]; + let vals4 = [false, ...vals5].slice(0, 2); + let vals3 = [...vals4, ...vals6].slice(0, 4); + let vals2 = [true, false, true, false].slice(0, 2); + let vals1 = [true, ...vals2, false, ...vals3].slice(0, 7); + + let sub_vals = [vals2, vals3, vals4, vals5, vals6]; + + await test_booleanList({ + core, + name: `/bl1`, + booleans: vals1, + pName: "/p", + text: vals1.join(", "), + }); + + for (let i = 0; i < 5; i++) { + let vals = sub_vals[i]; + await test_booleanList({ + core, + name: `/bl${i + 2}`, + booleans: vals, + }); + } + }); + + it("copy booleanList and overwrite maximum number", async () => { + let core = await createTestCore({ + doenetML: ` +

true true false false true

+

$bl1{maxNumber="3" name="bl2"}

+

$bl2{maxNumber="" name="bl3"}

+ +

true true false false true

+

$bl4{maxNumber="4" name="bl5"}

+

$bl5{maxNumber="" name="bl6"}

+ `, + }); + + let list = [true, true, false, false, true]; + + await test_booleanList({ + core, + name: "/bl1", + booleans: list, + pName: "/p1", + text: list.join(", "), + }); + await test_booleanList({ + core, + name: "/bl2", + booleans: list.slice(0, 3), + pName: "/p2", + text: list.slice(0, 3).join(", "), + }); + await test_booleanList({ + core, + name: "/bl3", + booleans: list, + pName: "/p3", + text: list.join(", "), + }); + await test_booleanList({ + core, + name: "/bl4", + booleans: list.slice(0, 3), + pName: "/p4", + text: list.slice(0, 3).join(", "), + }); + await test_booleanList({ + core, + name: "/bl5", + booleans: list.slice(0, 4), + pName: "/p5", + text: list.slice(0, 4).join(", "), + }); + await test_booleanList({ + core, + name: "/bl6", + booleans: list, + pName: "/p6", + text: list.join(", "), + }); + }); + + it("dynamic maximum number", async () => { + let core = await createTestCore({ + doenetML: ` + +

Maximum number 1:

+

Maximum number 2:

+
+

true true false true false false

+

$bl1{maxNumber="$mn2" name="bl2"}

+

$bl2{name="bl3"}

+

$bl3{name="bl4" maxNumber=""}

+
+
+ + `, + }); + + let list = [true, true, false, true, false, false]; + + async function check_items(max1, max2) { + for (let pre of ["", "/sec2"]) { + await test_booleanList({ + core, + name: `${pre}/bl1`, + booleans: list.slice(0, max1), + pName: `${pre}/p1`, + text: list.slice(0, max1).join(", "), + }); + await test_booleanList({ + core, + name: `${pre}/bl2`, + booleans: list.slice(0, max2), + pName: `${pre}/p2`, + text: list.slice(0, max2).join(", "), + }); + await test_booleanList({ + core, + name: `${pre}/bl3`, + booleans: list.slice(0, max2), + pName: `${pre}/p3`, + text: list.slice(0, max2).join(", "), + }); + await test_booleanList({ + core, + name: `${pre}/bl4`, + booleans: list, + pName: `${pre}/p4`, + text: list.join(", "), + }); + } + } + + let max1 = 2, + max2 = Infinity; + + await check_items(max1, max2); + + max1 = Infinity; + await updateMathInputValue({ latex: "", componentName: "/mn1", core }); + await check_items(max1, max2); + + max2 = 3; + await updateMathInputValue({ + latex: max2.toString(), + componentName: "/mn2", + core, + }); + await check_items(max1, max2); + + max1 = 4; + await updateMathInputValue({ + latex: max1.toString(), + componentName: "/mn1", + core, + }); + await check_items(max1, max2); + + max1 = 1; + await updateMathInputValue({ + latex: max1.toString(), + componentName: "/mn1", + core, + }); + await check_items(max1, max2); + + max2 = 10; + await updateMathInputValue({ + latex: max2.toString(), + componentName: "/mn2", + core, + }); + await check_items(max1, max2); + }); + + it("booleanList within booleanLists, ignore child hide", async () => { + let core = await createTestCore({ + doenetML: ` +

true true false

+ +

+ false + $bl1 + true + $bl1{hide="false"} +

+ +

$bl2{name="bl3" maxNumber="6"}

+ + `, + }); + + await test_booleanList({ + core, + name: "/bl2", + booleans: [false, true, true, false, true, true, true, false], + pName: "/p2", + text: "false, true, true, false, true, true, true, false", + }); + + await test_booleanList({ + core, + name: "/bl3", + booleans: [false, true, true, false, true, true], + pName: "/p3", + text: "false, true, true, false, true, true", + }); + }); + + it("booleanList does not force composite replacement, even in boolean", async () => { + let core = await createTestCore({ + doenetML: ` + + $nothing true = true + + `, + }); + + const stateVariables = await returnAllStateVariables(core); + expect(stateVariables["/b"].stateValues.value).eq(true); + }); + + it("assignNames", async () => { + let core = await createTestCore({ + doenetML: ` +

true true fall

+

$a, $b, $c

+ + `, + }); + + const stateVariables = await returnAllStateVariables(core); + + expect(stateVariables["/p1"].stateValues.text).eq("true, true, false"); + expect(stateVariables["/p2"].stateValues.text).eq("true, true, false"); + expect(stateVariables["/a"].stateValues.value).eq(true); + expect(stateVariables["/b"].stateValues.value).eq(true); + expect(stateVariables["/c"].stateValues.value).eq(false); + }); + + it("text from booleanList", async () => { + let core = await createTestCore({ + doenetML: ` + true true false + +

Text: $bl.text

+ + `, + }); + + const stateVariables = await returnAllStateVariables(core); + expect(stateVariables["/pText"].stateValues.text).eq( + "Text: true, true, false", + ); + }); +}); diff --git a/packages/doenetml-worker/src/test/tagSpecific/callAction.test.ts b/packages/doenetml-worker/src/test/tagSpecific/callAction.test.ts index 924bec46a..4d6910e7d 100644 --- a/packages/doenetml-worker/src/test/tagSpecific/callAction.test.ts +++ b/packages/doenetml-worker/src/test/tagSpecific/callAction.test.ts @@ -5,7 +5,6 @@ import { updateBooleanInputValue, updateMathInputValue, } from "../utils/actions"; -import me from "math-expressions"; const Mock = vi.fn(); vi.stubGlobal("postMessage", Mock); @@ -18,7 +17,7 @@ describe("callAction tag tests", async () => { let numbers = stateVariables["/nums"].stateValues.text .split(",") .map(Number); - expect(numbers.length).eq(5); + expect(numbers.length).eq(7); for (let [ind, num] of numbers.entries()) { expect(Number.isInteger(num)).be.true; expect(num).gte(1); @@ -42,7 +41,7 @@ describe("callAction tag tests", async () => { .split(",") .map(Number); - expect(numbers2.length).eq(5); + expect(numbers2.length).eq(7); for (let num of numbers2) { expect(Number.isInteger(num)).be.true; expect(num).gte(1); @@ -54,7 +53,7 @@ describe("callAction tag tests", async () => { it("resample random numbers", async () => { let core = await createTestCore({ doenetML: ` -

+

@@ -373,7 +372,7 @@ describe("callAction tag tests", async () => { let numbers = stateVariables["/nums"].stateValues.text .split(",") .map(Number); - expect(numbers.length).eq(5); + expect(numbers.length).eq(7); for (let num of numbers) { expect(Number.isInteger(num)).be.true; expect(num).gte(1); @@ -424,7 +423,7 @@ describe("callAction tag tests", async () => { .split(",") .map(Number); - expect(numbers2.length).eq(5); + expect(numbers2.length).eq(7); for (let num of numbers2) { expect(Number.isInteger(num)).be.true; expect(num).gte(1); @@ -436,7 +435,7 @@ describe("callAction tag tests", async () => { it("chained actions", async () => { let core = await createTestCore({ doenetML: ` -

+

@@ -461,7 +460,7 @@ describe("callAction tag tests", async () => { it("chained actions, unnecessary $", async () => { let core = await createTestCore({ doenetML: ` -

+

@@ -488,7 +487,7 @@ describe("callAction tag tests", async () => { doenetML: `