@qti-components/base 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/interaction.js +1 -1
- package/dist/abstract/interaction.js.map +1 -1
- package/dist/abstract/qti-expression.d.ts +26 -1
- package/dist/abstract/qti-expression.js +219 -99
- package/dist/abstract/qti-expression.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/custom-operators.d.ts +31 -0
- package/dist/lib/custom-operators.js +63 -0
- package/dist/lib/custom-operators.js.map +1 -0
- package/dist/lib/qti-rule-base.d.ts +25 -0
- package/dist/lib/qti-rule-base.js +31 -0
- package/dist/lib/qti-rule-base.js.map +1 -1
- package/dist/lib/variables.d.ts +17 -1
- package/dist/lib/variables.js.map +1 -1
- package/dist/utils/scoring-helper.js +37 -4
- package/dist/utils/scoring-helper.js.map +1 -1
- package/dist/utils/utils.d.ts +10 -0
- package/dist/utils/utils.js +11 -11
- package/dist/utils/utils.js.map +1 -1
- package/package.json +3 -3
|
@@ -84,7 +84,7 @@ export class Interaction extends LitElement {
|
|
|
84
84
|
if (config.disableAfterIfMaxChoicesReached !== undefined) {
|
|
85
85
|
if (!this.#didLogDisableAfterIfMaxChoicesReachedDeprecation) {
|
|
86
86
|
this.#didLogDisableAfterIfMaxChoicesReachedDeprecation = true;
|
|
87
|
-
console.
|
|
87
|
+
console.warn('[QTI Config] `disableAfterIfMaxChoicesReached` is deprecated. Use `disableAfterMaxReached` instead.');
|
|
88
88
|
}
|
|
89
89
|
return config.disableAfterIfMaxChoicesReached;
|
|
90
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/abstract/interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { consume, provide } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport { configContext } from '../context/config.context';\nimport { interactionContext } from '../context/interaction.context';\nimport { itemContext } from '../context/item.context';\n\nimport type { PropertyValues } from 'lit';\nimport type { InteractionContext } from '../context/interaction.context';\nimport type { ConfigContext, ValidationDisplayMode } from '../context/config.context';\nimport type { ItemContext } from '../context/types/item.types';\nimport type { ValidatableInteraction } from '../lib/interaction.interface';\nimport type { ResponseVariable } from '../lib/variables';\n\n/**\n * Shared QTI interaction base. Formative correction behavior is supplied by the\n * `@qti-components/corrections` package.\n */\nexport abstract class Interaction extends LitElement implements ValidatableInteraction {\n @consume({ context: itemContext, subscribe: true })\n private _context: ItemContext;\n\n /**\n * Delivery configuration, from the nearest provider — `qti-test` or `qti-item`.\n *\n * **Public so it can be assigned directly, which is the development-time route.** An interaction\n * has to work standalone, and in a story or a spec there is usually no provider above it — so\n * setting the field is all that is needed:\n *\n * orderInteraction.configContext = { allowReorder: false, validationDisplayMode: 'none' };\n *\n * With no provider, nothing ever overwrites it. That is the whole contract, and its limit: where a\n * provider DOES exist it re-emits on its next update and wins, because `@consume` writes this same\n * field. There is no merging — set the value on the provider in that case.\n *\n * In a lit-html template, bind it as a property rather than assigning after the fact:\n *\n * html`<qti-order-interaction .configContext=${{ allowReorder: false }}>…`\n *\n * The binding is committed on the detached template clone, before the fragment is inserted, so it\n * is in place by `connectedCallback` — which matters for anything that reads config on connect.\n *\n * It was `protected`, which fooled nobody: every caller reached it anyway through an\n * `as any` / `as { configContext?: … }` cast. Public says what was already true and removes the\n * casts, rather than pretending at an encapsulation the type system was the only thing enforcing.\n */\n @consume({ context: configContext, subscribe: true })\n public configContext: ConfigContext;\n\n #didLogDisableAfterIfMaxChoicesReachedDeprecation = false;\n\n /*\n * Published to the choice elements inside this interaction — their role, and how to tell\n * whether they are draggable. Provided here on the base rather than in a mixin so that\n * ChoicesMixin and the drag-drop mixins can each contribute without two providers of the same\n * context competing on one element.\n *\n * Reassign, never mutate: an in-place change does not notify consumers.\n */\n @provide({ context: interactionContext })\n protected _interactionContext: Readonly<InteractionContext> = { choiceRole: null, draggablesSelector: null };\n\n static formAssociated = true;\n protected _internals: ElementInternals;\n\n get internals(): ElementInternals {\n return this._internals;\n }\n\n @property({ type: String, attribute: 'response-identifier' }) responseIdentifier: string;\n\n @property({ reflect: true, type: Boolean }) disabled = false;\n\n @property({ reflect: true, type: Boolean }) readonly = false;\n\n @property({ type: String }) name;\n\n /** Extension point used by wrappers that should not register as candidate inputs. */\n protected get registersWithItem(): boolean {\n return true;\n }\n\n get isInline(): boolean {\n return false;\n }\n\n get responseVariable(): ResponseVariable | undefined {\n if (!this._context?.variables) {\n return undefined;\n }\n const responseVariables = this._context.variables.filter(v => v.type === 'response') as ResponseVariable[];\n const responseIdentifier = this.getAttribute('response-identifier');\n return responseVariables.find(v => v.identifier === responseIdentifier);\n }\n\n abstract validate(): boolean;\n\n get value(): string | null {\n return JSON.stringify(this.response);\n }\n\n set value(val: string | null) {\n this.response = val ? JSON.parse(val) : null;\n }\n\n abstract get response(): string | string[] | null;\n abstract set response(val: string | string[] | null);\n\n public reportValidity(): boolean {\n const isValid = this._internals.validity.valid;\n const mode = this.validationDisplayMode;\n\n if (mode === 'native' || mode === 'both') {\n this._internals.reportValidity();\n }\n\n if (mode === 'inline' || mode === 'both') {\n this.updateInlineValidationMessage();\n } else {\n this.clearInlineValidationMessage();\n }\n\n if (mode === 'none') {\n this.clearInlineValidationMessage();\n }\n\n return isValid;\n }\n\n protected get validationDisplayMode(): ValidationDisplayMode {\n return this.configContext?.validationDisplayMode ?? 'inline';\n }\n\n protected resolveAllowReorder(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? true;\n const config = this.configContext;\n if (!config) {\n return defaultWhenUnset;\n }\n if (config.allowReorder !== undefined) {\n return config.allowReorder;\n }\n return defaultWhenUnset;\n }\n\n protected resolveDisableAfterMaxReached(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? false;\n const config = this.configContext;\n\n if (!config) {\n return defaultWhenUnset;\n }\n\n if (config.disableAfterMaxReached !== undefined) {\n return config.disableAfterMaxReached;\n }\n\n if (config.disableAfterIfMaxChoicesReached !== undefined) {\n if (!this.#didLogDisableAfterIfMaxChoicesReachedDeprecation) {\n this.#didLogDisableAfterIfMaxChoicesReachedDeprecation = true;\n console.log(\n '[QTI Config] `disableAfterIfMaxChoicesReached` is deprecated. Use `disableAfterMaxReached` instead.'\n );\n }\n return config.disableAfterIfMaxChoicesReached;\n }\n\n return defaultWhenUnset;\n }\n\n protected setInteractionValidity(\n isValid: boolean,\n validityMessage = '',\n anchor?: HTMLElement | null,\n options?: { suppressInline?: boolean }\n ): void {\n const validityAnchor = anchor ?? this;\n this._internals.setValidity(isValid ? {} : { customError: true }, validityMessage, validityAnchor);\n\n if (\n options?.suppressInline !== true &&\n (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both')\n ) {\n this.updateInlineValidationMessage();\n }\n }\n\n protected updateInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n if (!this._internals.validity.valid) {\n validationMessageElement.textContent = this._internals.validationMessage;\n validationMessageElement.style.setProperty('display', 'block', 'important');\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected clearInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected getValidationMessageElement(): HTMLElement | null {\n return this.shadowRoot?.querySelector('#validation-message') as HTMLElement | null;\n }\n\n public reset(): void {\n this.response = null;\n }\n\n public formResetCallback(): void {\n this.reset();\n }\n\n public override connectedCallback(): void {\n super.connectedCallback();\n\n if (!this.registersWithItem) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent('qti-register-interaction', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n interactionElement: this,\n responseIdentifier: this.responseIdentifier\n }\n })\n );\n }\n\n public saveResponse(value: string | string[], state?: string | null): void {\n this.dispatchEvent(\n new CustomEvent('qti-interaction-response', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n responseIdentifier: this.responseIdentifier,\n response: Array.isArray(value) ? [...value] : value,\n ...(state !== undefined ? { state } : {})\n }\n })\n );\n }\n\n constructor() {\n super();\n this._internals = this.attachInternals();\n }\n\n /**\n * Optional lifecycle bridge for interaction subclasses that do not need changed-property data.\n *\n * The parameter is `PropertyValues`, matching LitElement, and NOT `PropertyValues<this>`.\n * The polymorphic `this` narrowed the parameter per subclass, which made every interaction\n * class structurally incompatible with `LitElement` — so `Constructor<LitElement>`, the\n * standard constraint for a Lit mixin, rejected all of them and consumers could not wrap an\n * interaction in a mixin without casting. `this` buys nothing in a parameter position and\n * costs contravariant compatibility with the base class.\n *\n * Still optional, so subclasses that ignore the argument can override with `firstUpdated()`.\n */\n protected override firstUpdated(_changedProperties?: PropertyValues): void {}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/abstract/interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { consume, provide } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport { configContext } from '../context/config.context';\nimport { interactionContext } from '../context/interaction.context';\nimport { itemContext } from '../context/item.context';\n\nimport type { PropertyValues } from 'lit';\nimport type { InteractionContext } from '../context/interaction.context';\nimport type { ConfigContext, ValidationDisplayMode } from '../context/config.context';\nimport type { ItemContext } from '../context/types/item.types';\nimport type { ValidatableInteraction } from '../lib/interaction.interface';\nimport type { ResponseVariable } from '../lib/variables';\n\n/**\n * Shared QTI interaction base. Formative correction behavior is supplied by the\n * `@qti-components/corrections` package.\n */\nexport abstract class Interaction extends LitElement implements ValidatableInteraction {\n @consume({ context: itemContext, subscribe: true })\n private _context: ItemContext;\n\n /**\n * Delivery configuration, from the nearest provider — `qti-test` or `qti-item`.\n *\n * **Public so it can be assigned directly, which is the development-time route.** An interaction\n * has to work standalone, and in a story or a spec there is usually no provider above it — so\n * setting the field is all that is needed:\n *\n * orderInteraction.configContext = { allowReorder: false, validationDisplayMode: 'none' };\n *\n * With no provider, nothing ever overwrites it. That is the whole contract, and its limit: where a\n * provider DOES exist it re-emits on its next update and wins, because `@consume` writes this same\n * field. There is no merging — set the value on the provider in that case.\n *\n * In a lit-html template, bind it as a property rather than assigning after the fact:\n *\n * html`<qti-order-interaction .configContext=${{ allowReorder: false }}>…`\n *\n * The binding is committed on the detached template clone, before the fragment is inserted, so it\n * is in place by `connectedCallback` — which matters for anything that reads config on connect.\n *\n * It was `protected`, which fooled nobody: every caller reached it anyway through an\n * `as any` / `as { configContext?: … }` cast. Public says what was already true and removes the\n * casts, rather than pretending at an encapsulation the type system was the only thing enforcing.\n */\n @consume({ context: configContext, subscribe: true })\n public configContext: ConfigContext;\n\n #didLogDisableAfterIfMaxChoicesReachedDeprecation = false;\n\n /*\n * Published to the choice elements inside this interaction — their role, and how to tell\n * whether they are draggable. Provided here on the base rather than in a mixin so that\n * ChoicesMixin and the drag-drop mixins can each contribute without two providers of the same\n * context competing on one element.\n *\n * Reassign, never mutate: an in-place change does not notify consumers.\n */\n @provide({ context: interactionContext })\n protected _interactionContext: Readonly<InteractionContext> = { choiceRole: null, draggablesSelector: null };\n\n static formAssociated = true;\n protected _internals: ElementInternals;\n\n get internals(): ElementInternals {\n return this._internals;\n }\n\n @property({ type: String, attribute: 'response-identifier' }) responseIdentifier: string;\n\n @property({ reflect: true, type: Boolean }) disabled = false;\n\n @property({ reflect: true, type: Boolean }) readonly = false;\n\n @property({ type: String }) name;\n\n /** Extension point used by wrappers that should not register as candidate inputs. */\n protected get registersWithItem(): boolean {\n return true;\n }\n\n get isInline(): boolean {\n return false;\n }\n\n get responseVariable(): ResponseVariable | undefined {\n if (!this._context?.variables) {\n return undefined;\n }\n const responseVariables = this._context.variables.filter(v => v.type === 'response') as ResponseVariable[];\n const responseIdentifier = this.getAttribute('response-identifier');\n return responseVariables.find(v => v.identifier === responseIdentifier);\n }\n\n abstract validate(): boolean;\n\n get value(): string | null {\n return JSON.stringify(this.response);\n }\n\n set value(val: string | null) {\n this.response = val ? JSON.parse(val) : null;\n }\n\n abstract get response(): string | string[] | null;\n abstract set response(val: string | string[] | null);\n\n public reportValidity(): boolean {\n const isValid = this._internals.validity.valid;\n const mode = this.validationDisplayMode;\n\n if (mode === 'native' || mode === 'both') {\n this._internals.reportValidity();\n }\n\n if (mode === 'inline' || mode === 'both') {\n this.updateInlineValidationMessage();\n } else {\n this.clearInlineValidationMessage();\n }\n\n if (mode === 'none') {\n this.clearInlineValidationMessage();\n }\n\n return isValid;\n }\n\n protected get validationDisplayMode(): ValidationDisplayMode {\n return this.configContext?.validationDisplayMode ?? 'inline';\n }\n\n protected resolveAllowReorder(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? true;\n const config = this.configContext;\n if (!config) {\n return defaultWhenUnset;\n }\n if (config.allowReorder !== undefined) {\n return config.allowReorder;\n }\n return defaultWhenUnset;\n }\n\n protected resolveDisableAfterMaxReached(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? false;\n const config = this.configContext;\n\n if (!config) {\n return defaultWhenUnset;\n }\n\n if (config.disableAfterMaxReached !== undefined) {\n return config.disableAfterMaxReached;\n }\n\n if (config.disableAfterIfMaxChoicesReached !== undefined) {\n if (!this.#didLogDisableAfterIfMaxChoicesReachedDeprecation) {\n this.#didLogDisableAfterIfMaxChoicesReachedDeprecation = true;\n console.warn(\n '[QTI Config] `disableAfterIfMaxChoicesReached` is deprecated. Use `disableAfterMaxReached` instead.'\n );\n }\n return config.disableAfterIfMaxChoicesReached;\n }\n\n return defaultWhenUnset;\n }\n\n protected setInteractionValidity(\n isValid: boolean,\n validityMessage = '',\n anchor?: HTMLElement | null,\n options?: { suppressInline?: boolean }\n ): void {\n const validityAnchor = anchor ?? this;\n this._internals.setValidity(isValid ? {} : { customError: true }, validityMessage, validityAnchor);\n\n if (\n options?.suppressInline !== true &&\n (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both')\n ) {\n this.updateInlineValidationMessage();\n }\n }\n\n protected updateInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n if (!this._internals.validity.valid) {\n validationMessageElement.textContent = this._internals.validationMessage;\n validationMessageElement.style.setProperty('display', 'block', 'important');\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected clearInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected getValidationMessageElement(): HTMLElement | null {\n return this.shadowRoot?.querySelector('#validation-message') as HTMLElement | null;\n }\n\n public reset(): void {\n this.response = null;\n }\n\n public formResetCallback(): void {\n this.reset();\n }\n\n public override connectedCallback(): void {\n super.connectedCallback();\n\n if (!this.registersWithItem) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent('qti-register-interaction', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n interactionElement: this,\n responseIdentifier: this.responseIdentifier\n }\n })\n );\n }\n\n public saveResponse(value: string | string[], state?: string | null): void {\n this.dispatchEvent(\n new CustomEvent('qti-interaction-response', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n responseIdentifier: this.responseIdentifier,\n response: Array.isArray(value) ? [...value] : value,\n ...(state !== undefined ? { state } : {})\n }\n })\n );\n }\n\n constructor() {\n super();\n this._internals = this.attachInternals();\n }\n\n /**\n * Optional lifecycle bridge for interaction subclasses that do not need changed-property data.\n *\n * The parameter is `PropertyValues`, matching LitElement, and NOT `PropertyValues<this>`.\n * The polymorphic `this` narrowed the parameter per subclass, which made every interaction\n * class structurally incompatible with `LitElement` — so `Constructor<LitElement>`, the\n * standard constraint for a Lit mixin, rejected all of them and consumers could not wrap an\n * interaction in a mixin without casting. `this` buys nothing in a parameter position and\n * costs contravariant compatibility with the base class.\n *\n * Still optional, so subclasses that ignore the argument can override with `firstUpdated()`.\n */\n protected override firstUpdated(_changedProperties?: PropertyValues): void {}\n}\n"]}
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import { LitElement } from 'lit';
|
|
2
2
|
import type { ItemContext } from '../context/types/item.types';
|
|
3
3
|
import type { QtiContext, QtiContextType } from '../context/qti.context';
|
|
4
|
+
import type { BaseType } from '../lib/expression-result';
|
|
4
5
|
import type { ResponseVariable, VariableDeclaration } from '../lib/variables';
|
|
5
6
|
import type { TestContext } from '../context/test.context';
|
|
7
|
+
type ExpressionVariable = ResponseVariable | VariableDeclaration<QtiContextType>;
|
|
6
8
|
export interface QtiExpressionBase<T> {
|
|
7
9
|
calculate(): Readonly<T>;
|
|
8
10
|
}
|
|
9
11
|
export declare abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {
|
|
12
|
+
#private;
|
|
10
13
|
protected result: any;
|
|
14
|
+
/**
|
|
15
|
+
* What the last `calculate()` produced, or undefined if it has not run.
|
|
16
|
+
*
|
|
17
|
+
* This is the supported way to inspect a scoring run: process the responses,
|
|
18
|
+
* then walk the rule tree reading this off each expression. A tool that wants
|
|
19
|
+
* to visualise how a score came about gets the whole tree annotated with its
|
|
20
|
+
* values, without the components having to re-render to show them.
|
|
21
|
+
*/
|
|
22
|
+
get lastResult(): Readonly<T> | undefined;
|
|
23
|
+
get lastVariables(): readonly ExpressionVariable[];
|
|
11
24
|
static styles: import("lit").CSSResult;
|
|
12
25
|
render(): import("lit").TemplateResult<1>;
|
|
26
|
+
protected shouldUpdate(): boolean;
|
|
13
27
|
calculate(): Readonly<T>;
|
|
14
28
|
protected getResult(): Readonly<T>;
|
|
29
|
+
/**
|
|
30
|
+
* The base type this operator always produces, when the QTI vocabulary fixes
|
|
31
|
+
* one regardless of its operands — `qti-divide` is a float even dividing two
|
|
32
|
+
* integers exactly, and `qti-round` is an integer whatever it rounds.
|
|
33
|
+
*
|
|
34
|
+
* Left undefined by operators whose result type follows their operands (a
|
|
35
|
+
* `qti-sum` of integers is an integer) and by those that return no number at
|
|
36
|
+
* all; the type is then read off the value.
|
|
37
|
+
*/
|
|
38
|
+
get resultBaseType(): BaseType | undefined;
|
|
15
39
|
protected context?: ItemContext;
|
|
16
40
|
protected qtiContext?: QtiContext;
|
|
17
41
|
protected _testContext?: TestContext;
|
|
@@ -24,5 +48,6 @@ export declare abstract class QtiExpression<T> extends LitElement implements Qti
|
|
|
24
48
|
* throwing, so callers degrade gracefully.
|
|
25
49
|
*/
|
|
26
50
|
protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null;
|
|
27
|
-
getVariables: () =>
|
|
51
|
+
getVariables: () => ExpressionVariable[];
|
|
28
52
|
}
|
|
53
|
+
export {};
|
|
@@ -6,33 +6,105 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { consume } from '@lit/context';
|
|
8
8
|
import { css, html, LitElement } from 'lit';
|
|
9
|
-
import { state } from 'lit/decorators.js';
|
|
10
9
|
import { itemContext } from '../context/item.context';
|
|
11
10
|
import { qtiContext } from '../context/qti.context';
|
|
12
11
|
import { testContext } from '../context/test.context';
|
|
12
|
+
/**
|
|
13
|
+
* Wrap whatever an expression handed back in a variable, reading the base type
|
|
14
|
+
* off the value itself.
|
|
15
|
+
*
|
|
16
|
+
* An operator that knows its own result type says so through `resultBaseType`
|
|
17
|
+
* and that wins, because the QTI vocabulary fixes it regardless of the operands
|
|
18
|
+
* — `qti-divide` is a float even when it divides two integers exactly. Only
|
|
19
|
+
* when nothing is declared, as for a custom operator returning whatever the
|
|
20
|
+
* host computed, is the type read off the value.
|
|
21
|
+
*
|
|
22
|
+
* Everything used to be labelled `integer`, and since `compareSingleValues`
|
|
23
|
+
* parses an integer with `parseInt`, that silently truncated:
|
|
24
|
+
* `equal(divide(5, 2), 2.4)` compared 2 against 2 and answered true.
|
|
25
|
+
*/
|
|
26
|
+
const variableFromValue = (value, declared) => {
|
|
27
|
+
/*
|
|
28
|
+
* A NULL result is still a value: the operand exists and is NULL. Returning
|
|
29
|
+
* nothing would drop it from the list, which both hides the NULL from
|
|
30
|
+
* `qti-is-null` and shifts every operand after it — a binary operator with a
|
|
31
|
+
* NULL first argument would silently read its second as its first. The base
|
|
32
|
+
* type is moot here, since every operator tests the value before it.
|
|
33
|
+
*/
|
|
34
|
+
if (value === null || value === undefined) {
|
|
35
|
+
return {
|
|
36
|
+
identifier: '',
|
|
37
|
+
baseType: 'string',
|
|
38
|
+
value: null,
|
|
39
|
+
cardinality: 'single',
|
|
40
|
+
type: 'response'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return {
|
|
45
|
+
identifier: '',
|
|
46
|
+
baseType: declared ?? inferBaseType(value[0]),
|
|
47
|
+
value: value.map(entry => String(entry)),
|
|
48
|
+
cardinality: 'multiple',
|
|
49
|
+
type: 'response'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
identifier: '',
|
|
54
|
+
baseType: declared ?? inferBaseType(value),
|
|
55
|
+
value: String(value),
|
|
56
|
+
cardinality: 'single',
|
|
57
|
+
type: 'response'
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** A JS value's QTI base type. A string stays text; it is not re-parsed. */
|
|
61
|
+
const inferBaseType = (value) => {
|
|
62
|
+
if (typeof value === 'number')
|
|
63
|
+
return Number.isInteger(value) ? 'integer' : 'float';
|
|
64
|
+
if (typeof value === 'boolean')
|
|
65
|
+
return 'boolean';
|
|
66
|
+
return 'string';
|
|
67
|
+
};
|
|
68
|
+
const inferContainerBaseType = (expression, value) => {
|
|
69
|
+
if (!Array.isArray(value) || value.length > 0) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return expression
|
|
73
|
+
.lastVariables
|
|
74
|
+
.find(variable => variable?.baseType && variable.baseType !== 'record' && variable.cardinality !== 'record')
|
|
75
|
+
?.baseType;
|
|
76
|
+
};
|
|
13
77
|
export class QtiExpression extends LitElement {
|
|
14
78
|
constructor() {
|
|
15
79
|
super(...arguments);
|
|
16
|
-
this
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
80
|
+
this.#lastVariables = [];
|
|
81
|
+
this.getVariables = () => {
|
|
82
|
+
// FIXME: if this itself is multiple, this will never enter the qti-multiple switch
|
|
83
|
+
// See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml
|
|
84
|
+
const variables = Array.from(this.children)
|
|
85
|
+
.map((e) => {
|
|
86
|
+
switch (e.tagName.toLowerCase()) {
|
|
87
|
+
case 'qti-base-value': {
|
|
88
|
+
return {
|
|
89
|
+
baseType: e.getAttribute('base-type'),
|
|
90
|
+
value: e.textContent.trim(),
|
|
91
|
+
cardinality: 'single'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case 'qti-variable': {
|
|
95
|
+
const identifier = e.getAttribute('identifier') || '';
|
|
96
|
+
// Special handling for QTI_CONTEXT
|
|
97
|
+
if (identifier === 'QTI_CONTEXT') {
|
|
98
|
+
if (!this.qtiContext?.QTI_CONTEXT) {
|
|
99
|
+
console.warn('QTI_CONTEXT not available');
|
|
100
|
+
return {
|
|
101
|
+
identifier: 'QTI_CONTEXT',
|
|
102
|
+
baseType: 'record',
|
|
103
|
+
value: this.qtiContext.QTI_CONTEXT,
|
|
104
|
+
cardinality: 'record',
|
|
105
|
+
type: 'context'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
36
108
|
return {
|
|
37
109
|
identifier: 'QTI_CONTEXT',
|
|
38
110
|
baseType: 'record',
|
|
@@ -41,82 +113,102 @@ export class QtiExpression extends LitElement {
|
|
|
41
113
|
type: 'context'
|
|
42
114
|
};
|
|
43
115
|
}
|
|
44
|
-
return
|
|
45
|
-
identifier: 'QTI_CONTEXT',
|
|
46
|
-
baseType: 'record',
|
|
47
|
-
value: this.qtiContext.QTI_CONTEXT,
|
|
48
|
-
cardinality: 'record',
|
|
49
|
-
type: 'context'
|
|
50
|
-
};
|
|
116
|
+
return this.resolveVariable(identifier);
|
|
51
117
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
118
|
+
case 'qti-multiple': {
|
|
119
|
+
const multiple = e;
|
|
120
|
+
const values = multiple.getResult();
|
|
121
|
+
if (values?.length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
identifier: '',
|
|
124
|
+
baseType: values[0].baseType,
|
|
125
|
+
value: values.map(v => v.value),
|
|
126
|
+
cardinality: 'multiple',
|
|
127
|
+
type: 'response'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
66
131
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
case 'qti-ordered': {
|
|
133
|
+
const multiple = e;
|
|
134
|
+
const values = multiple.getResult();
|
|
135
|
+
if (values?.length > 0) {
|
|
136
|
+
return {
|
|
137
|
+
identifier: '',
|
|
138
|
+
baseType: values[0].baseType,
|
|
139
|
+
value: values.map(v => v.value),
|
|
140
|
+
cardinality: 'ordered',
|
|
141
|
+
type: 'response'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
80
145
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
146
|
+
case 'qti-repeat': {
|
|
147
|
+
const repeat = e;
|
|
148
|
+
const values = repeat.getResult();
|
|
149
|
+
if (values?.length > 0) {
|
|
150
|
+
return values;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
88
153
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
case 'qti-custom-operator': {
|
|
155
|
+
// Not a QtiExpression — it has `calculate` but no `getResult`, so
|
|
156
|
+
// the default branch below throws on it and the surrounding
|
|
157
|
+
// expression is handed nothing.
|
|
158
|
+
return variableFromValue(e.calculate?.());
|
|
159
|
+
}
|
|
160
|
+
case 'qti-correct': {
|
|
161
|
+
const identifier = e.getAttribute('identifier') || '';
|
|
162
|
+
const responseVariable = this.context?.variables?.find(v => v.identifier === identifier);
|
|
163
|
+
// A `qti-correct` naming a response that is not declared — a typo,
|
|
164
|
+
// or an expression evaluated before the declaration registered —
|
|
165
|
+
// used to dereference the null this deliberately produced and take
|
|
166
|
+
// down the whole processing run. It is an unresolved variable, so
|
|
167
|
+
// it is NULL.
|
|
168
|
+
if (!responseVariable) {
|
|
169
|
+
console.warn(`qti-correct: no response declaration for "${identifier}"`);
|
|
170
|
+
return variableFromValue(null);
|
|
171
|
+
}
|
|
105
172
|
return {
|
|
106
|
-
baseType:
|
|
107
|
-
value:
|
|
108
|
-
cardinality:
|
|
173
|
+
baseType: responseVariable.baseType,
|
|
174
|
+
value: responseVariable.correctResponse,
|
|
175
|
+
cardinality: responseVariable.cardinality
|
|
109
176
|
};
|
|
110
177
|
}
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
default: {
|
|
179
|
+
// Every operator without a case of its own lands here.
|
|
180
|
+
try {
|
|
181
|
+
const expression = e;
|
|
182
|
+
const result = expression.getResult();
|
|
183
|
+
return variableFromValue(result, expression.resultBaseType ?? inferContainerBaseType(expression, result));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn(`getVariables: could not read a value from <${e.tagName.toLowerCase()}>`, error);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
113
189
|
}
|
|
114
|
-
return null;
|
|
115
190
|
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
191
|
+
})
|
|
192
|
+
.flatMap(v => (Array.isArray(v) ? v : [v]))
|
|
193
|
+
.filter((v) => v !== null);
|
|
194
|
+
this.#lastVariables = variables;
|
|
195
|
+
return variables;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
#lastVariables;
|
|
199
|
+
/**
|
|
200
|
+
* What the last `calculate()` produced, or undefined if it has not run.
|
|
201
|
+
*
|
|
202
|
+
* This is the supported way to inspect a scoring run: process the responses,
|
|
203
|
+
* then walk the rule tree reading this off each expression. A tool that wants
|
|
204
|
+
* to visualise how a score came about gets the whole tree annotated with its
|
|
205
|
+
* values, without the components having to re-render to show them.
|
|
206
|
+
*/
|
|
207
|
+
get lastResult() {
|
|
208
|
+
return this.result;
|
|
209
|
+
}
|
|
210
|
+
get lastVariables() {
|
|
211
|
+
return this.#lastVariables;
|
|
120
212
|
}
|
|
121
213
|
/*
|
|
122
214
|
* Written here rather than imported from a .css file, and that is a packaging constraint rather
|
|
@@ -145,9 +237,31 @@ export class QtiExpression extends LitElement {
|
|
|
145
237
|
display: none;
|
|
146
238
|
}
|
|
147
239
|
`; }
|
|
240
|
+
/*
|
|
241
|
+
* Just the slot. This used to render `JSON.stringify(this.result, null, 2)`
|
|
242
|
+
* into a <pre> beside it — a development aid that nobody could see, because
|
|
243
|
+
* expressions live inside `qti-response-processing`, which is `display: none`.
|
|
244
|
+
* It cost a full serialisation of the result on every render.
|
|
245
|
+
*/
|
|
148
246
|
render() {
|
|
149
|
-
return html `<
|
|
150
|
-
|
|
247
|
+
return html `<slot></slot>`;
|
|
248
|
+
}
|
|
249
|
+
/*
|
|
250
|
+
* Render once, then never again: the template above is constant, so every
|
|
251
|
+
* later update is pure overhead.
|
|
252
|
+
*
|
|
253
|
+
* It is not the `@state()` decorators that drive those updates — `@consume`
|
|
254
|
+
* subscribes through `ContextConsumer`, which calls `host.requestUpdate()`
|
|
255
|
+
* itself on every context change, "in case this value is used in a template".
|
|
256
|
+
* Here it is not. An item with 30 response-processing rules holds ~180
|
|
257
|
+
* expression elements and the test context changes on every keystroke, so
|
|
258
|
+
* that was ~180 renders per keystroke producing identical output.
|
|
259
|
+
*
|
|
260
|
+
* The subscriptions stay, because `calculate()` reads the current values when
|
|
261
|
+
* a rule invokes it. Only the rendering stops.
|
|
262
|
+
*/
|
|
263
|
+
shouldUpdate() {
|
|
264
|
+
return !this.hasUpdated;
|
|
151
265
|
}
|
|
152
266
|
calculate() {
|
|
153
267
|
this.result = this.getResult();
|
|
@@ -156,6 +270,18 @@ export class QtiExpression extends LitElement {
|
|
|
156
270
|
getResult() {
|
|
157
271
|
throw new Error('Not implemented');
|
|
158
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* The base type this operator always produces, when the QTI vocabulary fixes
|
|
275
|
+
* one regardless of its operands — `qti-divide` is a float even dividing two
|
|
276
|
+
* integers exactly, and `qti-round` is an integer whatever it rounds.
|
|
277
|
+
*
|
|
278
|
+
* Left undefined by operators whose result type follows their operands (a
|
|
279
|
+
* `qti-sum` of integers is an integer) and by those that return no number at
|
|
280
|
+
* all; the type is then read off the value.
|
|
281
|
+
*/
|
|
282
|
+
get resultBaseType() {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
159
285
|
/**
|
|
160
286
|
* Resolve a declared variable from the closest available scope: item-level
|
|
161
287
|
* first, then the test-level outcome variables. So an expression used in an
|
|
@@ -171,18 +297,12 @@ export class QtiExpression extends LitElement {
|
|
|
171
297
|
}
|
|
172
298
|
}
|
|
173
299
|
__decorate([
|
|
174
|
-
|
|
175
|
-
], QtiExpression.prototype, "result", void 0);
|
|
176
|
-
__decorate([
|
|
177
|
-
consume({ context: itemContext, subscribe: true }),
|
|
178
|
-
state()
|
|
300
|
+
consume({ context: itemContext, subscribe: true })
|
|
179
301
|
], QtiExpression.prototype, "context", void 0);
|
|
180
302
|
__decorate([
|
|
181
|
-
consume({ context: qtiContext, subscribe: true })
|
|
182
|
-
state()
|
|
303
|
+
consume({ context: qtiContext, subscribe: true })
|
|
183
304
|
], QtiExpression.prototype, "qtiContext", void 0);
|
|
184
305
|
__decorate([
|
|
185
|
-
consume({ context: testContext, subscribe: true })
|
|
186
|
-
state()
|
|
306
|
+
consume({ context: testContext, subscribe: true })
|
|
187
307
|
], QtiExpression.prototype, "_testContext", void 0);
|
|
188
308
|
//# sourceMappingURL=qti-expression.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qti-expression.js","sourceRoot":"","sources":["../../src/abstract/qti-expression.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AActD,MAAM,OAAgB,aAAiB,SAAQ,UAAU;IAAzD;;QA0EE,iBAAY,GAAG,GAA+D,EAAE;QAC9E,mFAAmF;QACnF,iJAAiJ;QAEjJ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;aACtB,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE;YAClB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;YACvD,QAAQ,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;gBAChC,KAAK,gBAAgB,CAAC,CAAC,CAAC;oBACtB,OAAO;wBACL,QAAQ,EAAE,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC;wBACrC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE;wBAC3B,WAAW,EAAE,QAAQ;qBACF,CAAC;gBACxB,CAAC;gBACD,KAAK,cAAc,CAAC,CAAC,CAAC;oBACpB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;oBAEtD,mCAAmC;oBACnC,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;wBACjC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC;4BAClC,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;4BAC1C,OAAO;gCACL,UAAU,EAAE,aAAa;gCACzB,QAAQ,EAAE,QAAQ;gCAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;gCAClC,WAAW,EAAE,QAAQ;gCACrB,IAAI,EAAE,SAAS;6BACuB,CAAC;wBAC3C,CAAC;wBACD,OAAO;4BACL,UAAU,EAAE,aAAa;4BACzB,QAAQ,EAAE,QAAQ;4BAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;4BAClC,WAAW,EAAE,QAAQ;4BACrB,IAAI,EAAE,SAAS;yBACuB,CAAC;oBAC3C,CAAC;oBAED,OAAO,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;gBAC1C,CAAC;gBACD,KAAK,cAAc,CAAC,CAAC,CAAC;oBACpB,MAAM,QAAQ,GAAG,CAAsC,CAAC;oBAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;oBACpC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;oBAChC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvB,OAAO;4BACL,UAAU,EAAE,EAAE;4BACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;4BAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;4BAC/B,WAAW,EAAE,UAAU;4BACvB,IAAI,EAAE,UAAU;yBACG,CAAC;oBACxB,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,KAAK,aAAa,CAAC,CAAC,CAAC;oBACnB,MAAM,QAAQ,GAAG,CAAsC,CAAC;oBACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;oBACpC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvB,OAAO;4BACL,UAAU,EAAE,EAAE;4BACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;4BAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;4BAC/B,WAAW,EAAE,SAAS;4BACtB,IAAI,EAAE,UAAU;yBACG,CAAC;oBACxB,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,KAAK,YAAY,CAAC,CAAC,CAAC;oBAClB,MAAM,MAAM,GAAG,CAAsC,CAAC;oBACtD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;oBAClC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvB,OAAO,MAAM,CAAC;oBAChB,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,KAAK,aAAa,CAAC,CAAC,CAAC;oBACnB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;oBACtD,MAAM,gBAAgB,GACpB,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,IAAI,IAAI,CAAC;oBAC1E,OAAO;wBACL,QAAQ,EAAE,gBAAgB,CAAC,QAAQ;wBACnC,KAAK,EAAE,gBAAgB,CAAC,eAAe;wBACvC,WAAW,EAAE,gBAAgB,CAAC,WAAW;qBACtB,CAAC;gBACxB,CAAC;gBACD,OAAO,CAAC,CAAC,CAAC;oBACR,qCAAqC;oBACrC,IAAI,CAAC;wBACH,MAAM,UAAU,GAAG,CAA0B,CAAC;wBAC9C,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;wBACrC,OAAO;4BACL,QAAQ,EAAE,SAAS;4BACnB,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,IAAI;4BAChC,WAAW,EAAE,QAAQ;yBACF,CAAC;oBACxB,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,OAAO,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;oBACzC,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;YACH,CAAC;QACH,CAAC,CAAC;aACD,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aAC1C,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAC/B,CAAC;IAlLC;;;;;;;;;;;;;;;;;;;;;OAqBG;aACa,WAAM,GAAG,GAAG,CAAA;;;;GAI3B,AAJqB,CAIpB;IAEO,MAAM;QACb,OAAO,IAAI,CAAA,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;oBACvC,CAAC;IACnB,CAAC;IAEM,SAAS;QACd,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAES,SAAS;QACjB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;IAcD;;;;;;;OAOG;IACO,eAAe,CAAC,UAAkB;QAC1C,OAAO,CACL,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;YAC9D,IAAI,CAAC,YAAY,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;YAC/E,IAAI,CACL,CAAC;IACJ,CAAC;;AAtES;IADT,KAAK,EAAE;6CACc;AA8CZ;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAClD,KAAK,EAAE;8CACwB;AAItB;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACjD,KAAK,EAAE;iDAC0B;AAIxB;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAClD,KAAK,EAAE;mDAC6B","sourcesContent":["import { consume } from '@lit/context';\nimport { css, html, LitElement } from 'lit';\nimport { state } from 'lit/decorators.js';\n\nimport { itemContext } from '../context/item.context';\nimport { qtiContext } from '../context/qti.context';\nimport { testContext } from '../context/test.context';\n\nimport type { ItemContext } from '../context/types/item.types';\nimport type { QtiContext, QtiContextType } from '../context/qti.context';\nimport type { ResponseVariable, VariableDeclaration } from '../lib/variables';\nimport type { TestContext } from '../context/test.context';\n\nexport interface QtiExpressionBase<T> {\n // get assessmentItem(): QtiAssessmentItem;\n\n // getVariables(): VariableDeclaration<number | string | (number | string)[] | null>[];\n calculate(): Readonly<T>;\n}\n\nexport abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {\n @state()\n protected result: any;\n\n /*\n * Written here rather than imported from a .css file, and that is a packaging constraint rather\n * than a style preference.\n *\n * The rule used to live in qti-expression.css, imported with Vite's inline-stylesheet query so\n * DevTools could show source-file provenance. That query is a VITE specifier, and tsc copies it\n * verbatim into dist — so the published package shipped an import only a Vite-based consumer\n * could resolve. Every other consumer broke on it: plain Node, tsx, any non-Vite bundler. It was\n * the single such specifier reachable from this package, and it took down QTI-Editor's schema\n * check, which loads the components under tsx to build the real ProseMirror schema\n * (ERR_UNKNOWN_FILE_EXTENSION on a file that has nothing to do with schemas).\n *\n * A `build:assets` step used to copy the .css into dist beside it. That went too — it existed\n * only to give the unresolvable import something to point at.\n *\n * Two lines of CSS are not worth a dist that only one bundler can consume. If this ever grows\n * into a real stylesheet, give it a `.styles.ts` exporting a `css` template — the house pattern\n * everywhere else in this repo — rather than reintroducing a bundler-specific import.\n *\n * The element renders a debug <pre> of its computed result plus a <slot>; hiding the slot is all\n * the styling it has ever had.\n */\n static override styles = css`\n slot {\n display: none;\n }\n `;\n\n override render() {\n return html`<pre>${JSON.stringify(this.result, null, 2)}</pre>\n <slot></slot>`;\n }\n\n public calculate(): Readonly<T> {\n this.result = this.getResult();\n return this.result;\n }\n\n protected getResult(): Readonly<T> {\n throw new Error('Not implemented');\n }\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n protected context?: ItemContext;\n\n @consume({ context: qtiContext, subscribe: true })\n @state()\n protected qtiContext?: QtiContext;\n\n @consume({ context: testContext, subscribe: true })\n @state()\n protected _testContext?: TestContext;\n\n /**\n * Resolve a declared variable from the closest available scope: item-level\n * first, then the test-level outcome variables. So an expression used in an\n * assessmentTest's outcome processing can reference a test-level outcome\n * (e.g. a total set earlier in the same run) — which the QTI variable\n * expression is specified to do. Returns null when unresolved rather than\n * throwing, so callers degrade gracefully.\n */\n protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null {\n return (\n this.context?.variables.find(v => v.identifier === identifier) ??\n this._testContext?.testOutcomeVariables?.find(v => v.identifier === identifier) ??\n null\n );\n }\n\n getVariables = (): (ResponseVariable | VariableDeclaration<QtiContextType>)[] =>\n // FIXME: if this itself is multiple, this will never enter the qti-multiple switch\n // See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml\n\n Array.from(this.children)\n .map((e: Element) => {\n console.debug('getVariables', e.tagName.toLowerCase());\n switch (e.tagName.toLowerCase()) {\n case 'qti-base-value': {\n return {\n baseType: e.getAttribute('base-type'),\n value: e.textContent.trim(),\n cardinality: 'single'\n } as ResponseVariable;\n }\n case 'qti-variable': {\n const identifier = e.getAttribute('identifier') || '';\n\n // Special handling for QTI_CONTEXT\n if (identifier === 'QTI_CONTEXT') {\n if (!this.qtiContext?.QTI_CONTEXT) {\n console.warn('QTI_CONTEXT not available');\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n\n return this.resolveVariable(identifier);\n }\n case 'qti-multiple': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n\n const values = multiple.getResult();\n console.debug('values', values);\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-ordered': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'ordered',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-repeat': {\n const repeat = e as QtiExpression<ResponseVariable[]>;\n const values = repeat.getResult();\n if (values?.length > 0) {\n return values;\n }\n return null;\n }\n case 'qti-correct': {\n const identifier = e.getAttribute('identifier') || '';\n const responseVariable: ResponseVariable =\n this.context?.variables?.find(v => v.identifier === identifier) || null;\n return {\n baseType: responseVariable.baseType,\n value: responseVariable.correctResponse,\n cardinality: responseVariable.cardinality\n } as ResponseVariable;\n }\n default: {\n // added for use of qti-equal-rounded\n try {\n const expression = e as QtiExpression<number>;\n const value = expression.getResult();\n return {\n baseType: 'integer',\n value: value?.toString() || null,\n cardinality: 'single'\n } as ResponseVariable;\n } catch (error) {\n console.warn('default not sufficient');\n }\n return null;\n }\n }\n })\n .flatMap(v => (Array.isArray(v) ? v : [v]))\n .filter(v => v !== null);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"qti-expression.js","sourceRoot":"","sources":["../../src/abstract/qti-expression.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAkBtD;;;;;;;;;;;;;GAaG;AACH,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAE,QAAmB,EAAoB,EAAE;IAClF;;;;;;OAMG;IACH,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO;YACL,UAAU,EAAE,EAAE;YACd,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,IAAI;YACX,WAAW,EAAE,QAAQ;YACrB,IAAI,EAAE,UAAU;SACG,CAAC;IACxB,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,UAAU,EAAE,EAAE;YACd,QAAQ,EAAE,QAAQ,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxC,WAAW,EAAE,UAAU;YACvB,IAAI,EAAE,UAAU;SACG,CAAC;IACxB,CAAC;IAED,OAAO;QACL,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,QAAQ,IAAI,aAAa,CAAC,KAAK,CAAC;QAC1C,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;QACpB,WAAW,EAAE,QAAQ;QACrB,IAAI,EAAE,UAAU;KACG,CAAC;AACxB,CAAC,CAAC;AAEF,4EAA4E;AAC5E,MAAM,aAAa,GAAG,CAAC,KAAc,EAAY,EAAE;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IACpF,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACjD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,sBAAsB,GAAG,CAAC,UAAkC,EAAE,KAAc,EAAwB,EAAE;IAC1G,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,UAAU;SACd,aAAa;SACb,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,CAAC;QAC5G,EAAE,QAAgC,CAAC;AACvC,CAAC,CAAC;AAEF,MAAM,OAAgB,aAAiB,SAAQ,UAAU;IAAzD;;QAOE,mBAAc,GAAyB,EAAE,CAAC;QAoI1C,iBAAY,GAAG,GAAyB,EAAE;YACxC,mFAAmF;YACnF,iJAAiJ;YAEjJ,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;iBACxC,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE;gBAClB,QAAQ,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;oBAChC,KAAK,gBAAgB,CAAC,CAAC,CAAC;wBACtB,OAAO;4BACL,QAAQ,EAAE,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC;4BACrC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE;4BAC3B,WAAW,EAAE,QAAQ;yBACF,CAAC;oBACxB,CAAC;oBACD,KAAK,cAAc,CAAC,CAAC,CAAC;wBACpB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;wBAEtD,mCAAmC;wBACnC,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;4BACjC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC;gCAClC,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;gCAC1C,OAAO;oCACL,UAAU,EAAE,aAAa;oCACzB,QAAQ,EAAE,QAAQ;oCAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;oCAClC,WAAW,EAAE,QAAQ;oCACrB,IAAI,EAAE,SAAS;iCACuB,CAAC;4BAC3C,CAAC;4BACD,OAAO;gCACL,UAAU,EAAE,aAAa;gCACzB,QAAQ,EAAE,QAAQ;gCAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;gCAClC,WAAW,EAAE,QAAQ;gCACrB,IAAI,EAAE,SAAS;6BACuB,CAAC;wBAC3C,CAAC;wBAED,OAAO,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;oBAC1C,CAAC;oBACD,KAAK,cAAc,CAAC,CAAC,CAAC;wBACpB,MAAM,QAAQ,GAAG,CAAsC,CAAC;wBAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;wBACpC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;4BACvB,OAAO;gCACL,UAAU,EAAE,EAAE;gCACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;gCAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;gCAC/B,WAAW,EAAE,UAAU;gCACvB,IAAI,EAAE,UAAU;6BACG,CAAC;wBACxB,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;oBACD,KAAK,aAAa,CAAC,CAAC,CAAC;wBACnB,MAAM,QAAQ,GAAG,CAAsC,CAAC;wBACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;wBACpC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;4BACvB,OAAO;gCACL,UAAU,EAAE,EAAE;gCACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;gCAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;gCAC/B,WAAW,EAAE,SAAS;gCACtB,IAAI,EAAE,UAAU;6BACG,CAAC;wBACxB,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;oBACD,KAAK,YAAY,CAAC,CAAC,CAAC;wBAClB,MAAM,MAAM,GAAG,CAAsC,CAAC;wBACtD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;wBAClC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;4BACvB,OAAO,MAAM,CAAC;wBAChB,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;oBACD,KAAK,qBAAqB,CAAC,CAAC,CAAC;wBAC3B,kEAAkE;wBAClE,4DAA4D;wBAC5D,gCAAgC;wBAChC,OAAO,iBAAiB,CAAE,CAA0B,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;oBACtE,CAAC;oBACD,KAAK,aAAa,CAAC,CAAC,CAAC;wBACnB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;wBACtD,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAE1E,CAAC;wBAEd,mEAAmE;wBACnE,iEAAiE;wBACjE,mEAAmE;wBACnE,kEAAkE;wBAClE,cAAc;wBACd,IAAI,CAAC,gBAAgB,EAAE,CAAC;4BACtB,OAAO,CAAC,IAAI,CAAC,6CAA6C,UAAU,GAAG,CAAC,CAAC;4BACzE,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;wBACjC,CAAC;wBAED,OAAO;4BACL,QAAQ,EAAE,gBAAgB,CAAC,QAAQ;4BACnC,KAAK,EAAE,gBAAgB,CAAC,eAAe;4BACvC,WAAW,EAAE,gBAAgB,CAAC,WAAW;yBACtB,CAAC;oBACxB,CAAC;oBACD,OAAO,CAAC,CAAC,CAAC;wBACR,uDAAuD;wBACvD,IAAI,CAAC;4BACH,MAAM,UAAU,GAAG,CAA2B,CAAC;4BAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;4BACtC,OAAO,iBAAiB,CAAC,MAAM,EAAE,UAAU,CAAC,cAAc,IAAI,sBAAsB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;wBAC5G,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BACf,OAAO,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;wBAChG,CAAC;wBACD,OAAO,IAAI,CAAC;oBACd,CAAC;gBACH,CAAC;YACH,CAAC,CAAC;iBACD,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;iBAC1C,MAAM,CAAC,CAAC,CAAC,EAA2B,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YAEtD,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;YAChC,OAAO,SAAS,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC;IAhQC,cAAc,CAA4B;IAE1C;;;;;;;OAOG;IACH,IAAW,UAAU;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,IAAW,aAAa;QACtB,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;aACa,WAAM,GAAG,GAAG,CAAA;;;;GAI3B,AAJqB,CAIpB;IAEF;;;;;OAKG;IACM,MAAM;QACb,OAAO,IAAI,CAAA,eAAe,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;;;;OAaG;IACgB,YAAY;QAC7B,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;IAC1B,CAAC;IAEM,SAAS;QACd,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAES,SAAS;QACjB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;IAED;;;;;;;;OAQG;IACH,IAAW,cAAc;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAsBD;;;;;;;OAOG;IACO,eAAe,CAAC,UAAkB;QAC1C,OAAO,CACL,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;YAC9D,IAAI,CAAC,YAAY,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;YAC/E,IAAI,CACL,CAAC;IACJ,CAAC;;AAtBS;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;8CACnB;AAGtB;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;iDAChB;AAGxB;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;mDACd","sourcesContent":["import { consume } from '@lit/context';\nimport { css, html, LitElement } from 'lit';\n\nimport { itemContext } from '../context/item.context';\nimport { qtiContext } from '../context/qti.context';\nimport { testContext } from '../context/test.context';\n\nimport type { ItemContext } from '../context/types/item.types';\nimport type { QtiContext, QtiContextType } from '../context/qti.context';\nimport type { Calculate } from '../lib/expression-result';\nimport type { BaseType } from '../lib/expression-result';\nimport type { ResponseVariable, VariableDeclaration } from '../lib/variables';\nimport type { TestContext } from '../context/test.context';\n\ntype ExpressionVariable = ResponseVariable | VariableDeclaration<QtiContextType>;\n\nexport interface QtiExpressionBase<T> {\n // get assessmentItem(): QtiAssessmentItem;\n\n // getVariables(): VariableDeclaration<number | string | (number | string)[] | null>[];\n calculate(): Readonly<T>;\n}\n\n/**\n * Wrap whatever an expression handed back in a variable, reading the base type\n * off the value itself.\n *\n * An operator that knows its own result type says so through `resultBaseType`\n * and that wins, because the QTI vocabulary fixes it regardless of the operands\n * — `qti-divide` is a float even when it divides two integers exactly. Only\n * when nothing is declared, as for a custom operator returning whatever the\n * host computed, is the type read off the value.\n *\n * Everything used to be labelled `integer`, and since `compareSingleValues`\n * parses an integer with `parseInt`, that silently truncated:\n * `equal(divide(5, 2), 2.4)` compared 2 against 2 and answered true.\n */\nconst variableFromValue = (value: unknown, declared?: BaseType): ResponseVariable => {\n /*\n * A NULL result is still a value: the operand exists and is NULL. Returning\n * nothing would drop it from the list, which both hides the NULL from\n * `qti-is-null` and shifts every operand after it — a binary operator with a\n * NULL first argument would silently read its second as its first. The base\n * type is moot here, since every operator tests the value before it.\n */\n if (value === null || value === undefined) {\n return {\n identifier: '',\n baseType: 'string',\n value: null,\n cardinality: 'single',\n type: 'response'\n } as ResponseVariable;\n }\n\n if (Array.isArray(value)) {\n return {\n identifier: '',\n baseType: declared ?? inferBaseType(value[0]),\n value: value.map(entry => String(entry)),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n\n return {\n identifier: '',\n baseType: declared ?? inferBaseType(value),\n value: String(value),\n cardinality: 'single',\n type: 'response'\n } as ResponseVariable;\n};\n\n/** A JS value's QTI base type. A string stays text; it is not re-parsed. */\nconst inferBaseType = (value: unknown): BaseType => {\n if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'float';\n if (typeof value === 'boolean') return 'boolean';\n return 'string';\n};\n\nconst inferContainerBaseType = (expression: QtiExpression<unknown>, value: unknown): BaseType | undefined => {\n if (!Array.isArray(value) || value.length > 0) {\n return undefined;\n }\n\n return expression\n .lastVariables\n .find(variable => variable?.baseType && variable.baseType !== 'record' && variable.cardinality !== 'record')\n ?.baseType as BaseType | undefined;\n};\n\nexport abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {\n /*\n * Not `@state()`. Nothing renders it any more (see `render` below), so making\n * it reactive only scheduled an update per `calculate()` — and response\n * processing calls that on every expression of every rule it walks.\n */\n protected result: any;\n #lastVariables: ExpressionVariable[] = [];\n\n /**\n * What the last `calculate()` produced, or undefined if it has not run.\n *\n * This is the supported way to inspect a scoring run: process the responses,\n * then walk the rule tree reading this off each expression. A tool that wants\n * to visualise how a score came about gets the whole tree annotated with its\n * values, without the components having to re-render to show them.\n */\n public get lastResult(): Readonly<T> | undefined {\n return this.result;\n }\n\n public get lastVariables(): readonly ExpressionVariable[] {\n return this.#lastVariables;\n }\n\n /*\n * Written here rather than imported from a .css file, and that is a packaging constraint rather\n * than a style preference.\n *\n * The rule used to live in qti-expression.css, imported with Vite's inline-stylesheet query so\n * DevTools could show source-file provenance. That query is a VITE specifier, and tsc copies it\n * verbatim into dist — so the published package shipped an import only a Vite-based consumer\n * could resolve. Every other consumer broke on it: plain Node, tsx, any non-Vite bundler. It was\n * the single such specifier reachable from this package, and it took down QTI-Editor's schema\n * check, which loads the components under tsx to build the real ProseMirror schema\n * (ERR_UNKNOWN_FILE_EXTENSION on a file that has nothing to do with schemas).\n *\n * A `build:assets` step used to copy the .css into dist beside it. That went too — it existed\n * only to give the unresolvable import something to point at.\n *\n * Two lines of CSS are not worth a dist that only one bundler can consume. If this ever grows\n * into a real stylesheet, give it a `.styles.ts` exporting a `css` template — the house pattern\n * everywhere else in this repo — rather than reintroducing a bundler-specific import.\n *\n * The element renders a debug <pre> of its computed result plus a <slot>; hiding the slot is all\n * the styling it has ever had.\n */\n static override styles = css`\n slot {\n display: none;\n }\n `;\n\n /*\n * Just the slot. This used to render `JSON.stringify(this.result, null, 2)`\n * into a <pre> beside it — a development aid that nobody could see, because\n * expressions live inside `qti-response-processing`, which is `display: none`.\n * It cost a full serialisation of the result on every render.\n */\n override render() {\n return html`<slot></slot>`;\n }\n\n /*\n * Render once, then never again: the template above is constant, so every\n * later update is pure overhead.\n *\n * It is not the `@state()` decorators that drive those updates — `@consume`\n * subscribes through `ContextConsumer`, which calls `host.requestUpdate()`\n * itself on every context change, \"in case this value is used in a template\".\n * Here it is not. An item with 30 response-processing rules holds ~180\n * expression elements and the test context changes on every keystroke, so\n * that was ~180 renders per keystroke producing identical output.\n *\n * The subscriptions stay, because `calculate()` reads the current values when\n * a rule invokes it. Only the rendering stops.\n */\n protected override shouldUpdate(): boolean {\n return !this.hasUpdated;\n }\n\n public calculate(): Readonly<T> {\n this.result = this.getResult();\n return this.result;\n }\n\n protected getResult(): Readonly<T> {\n throw new Error('Not implemented');\n }\n\n /**\n * The base type this operator always produces, when the QTI vocabulary fixes\n * one regardless of its operands — `qti-divide` is a float even dividing two\n * integers exactly, and `qti-round` is an integer whatever it rounds.\n *\n * Left undefined by operators whose result type follows their operands (a\n * `qti-sum` of integers is an integer) and by those that return no number at\n * all; the type is then read off the value.\n */\n public get resultBaseType(): BaseType | undefined {\n return undefined;\n }\n\n /*\n * Subscribed but deliberately NOT `@state()`.\n *\n * An expression is pull-based: nothing reads these until a rule calls\n * `calculate()`, which reads whatever the consumer has by then. Marking them\n * reactive made every context change schedule a render of every expression\n * element in the document — an item with 30 response-processing rules holds\n * ~180 of them, and the test context changes on every keystroke. The\n * subscription still keeps the properties current; it just no longer asks\n * for a render nothing depends on.\n */\n @consume({ context: itemContext, subscribe: true })\n protected context?: ItemContext;\n\n @consume({ context: qtiContext, subscribe: true })\n protected qtiContext?: QtiContext;\n\n @consume({ context: testContext, subscribe: true })\n protected _testContext?: TestContext;\n\n /**\n * Resolve a declared variable from the closest available scope: item-level\n * first, then the test-level outcome variables. So an expression used in an\n * assessmentTest's outcome processing can reference a test-level outcome\n * (e.g. a total set earlier in the same run) — which the QTI variable\n * expression is specified to do. Returns null when unresolved rather than\n * throwing, so callers degrade gracefully.\n */\n protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null {\n return (\n this.context?.variables.find(v => v.identifier === identifier) ??\n this._testContext?.testOutcomeVariables?.find(v => v.identifier === identifier) ??\n null\n );\n }\n\n getVariables = (): ExpressionVariable[] => {\n // FIXME: if this itself is multiple, this will never enter the qti-multiple switch\n // See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml\n\n const variables = Array.from(this.children)\n .map((e: Element) => {\n switch (e.tagName.toLowerCase()) {\n case 'qti-base-value': {\n return {\n baseType: e.getAttribute('base-type'),\n value: e.textContent.trim(),\n cardinality: 'single'\n } as ResponseVariable;\n }\n case 'qti-variable': {\n const identifier = e.getAttribute('identifier') || '';\n\n // Special handling for QTI_CONTEXT\n if (identifier === 'QTI_CONTEXT') {\n if (!this.qtiContext?.QTI_CONTEXT) {\n console.warn('QTI_CONTEXT not available');\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n\n return this.resolveVariable(identifier);\n }\n case 'qti-multiple': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-ordered': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'ordered',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-repeat': {\n const repeat = e as QtiExpression<ResponseVariable[]>;\n const values = repeat.getResult();\n if (values?.length > 0) {\n return values;\n }\n return null;\n }\n case 'qti-custom-operator': {\n // Not a QtiExpression — it has `calculate` but no `getResult`, so\n // the default branch below throws on it and the surrounding\n // expression is handed nothing.\n return variableFromValue((e as unknown as Calculate).calculate?.());\n }\n case 'qti-correct': {\n const identifier = e.getAttribute('identifier') || '';\n const responseVariable = this.context?.variables?.find(v => v.identifier === identifier) as\n | ResponseVariable\n | undefined;\n\n // A `qti-correct` naming a response that is not declared — a typo,\n // or an expression evaluated before the declaration registered —\n // used to dereference the null this deliberately produced and take\n // down the whole processing run. It is an unresolved variable, so\n // it is NULL.\n if (!responseVariable) {\n console.warn(`qti-correct: no response declaration for \"${identifier}\"`);\n return variableFromValue(null);\n }\n\n return {\n baseType: responseVariable.baseType,\n value: responseVariable.correctResponse,\n cardinality: responseVariable.cardinality\n } as ResponseVariable;\n }\n default: {\n // Every operator without a case of its own lands here.\n try {\n const expression = e as QtiExpression<unknown>;\n const result = expression.getResult();\n return variableFromValue(result, expression.resultBaseType ?? inferContainerBaseType(expression, result));\n } catch (error) {\n console.warn(`getVariables: could not read a value from <${e.tagName.toLowerCase()}>`, error);\n }\n return null;\n }\n }\n })\n .flatMap(v => (Array.isArray(v) ? v : [v]))\n .filter((v): v is ExpressionVariable => v !== null);\n\n this.#lastVariables = variables;\n return variables;\n };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from './lib/expression-result';
|
|
|
23
23
|
export * from './lib/interaction.interface';
|
|
24
24
|
export * from './lib/variables';
|
|
25
25
|
export * from './lib/qti-rule-base';
|
|
26
|
+
export * from './lib/custom-operators';
|
|
26
27
|
export * from './utils/custom-state-set';
|
|
27
28
|
export * from './utils/scoring-helper';
|
|
28
29
|
export * from './utils/utils';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export * from './lib/expression-result';
|
|
|
23
23
|
export * from './lib/interaction.interface';
|
|
24
24
|
export * from './lib/variables';
|
|
25
25
|
export * from './lib/qti-rule-base';
|
|
26
|
+
export * from './lib/custom-operators';
|
|
26
27
|
export * from './utils/custom-state-set';
|
|
27
28
|
export * from './utils/scoring-helper';
|
|
28
29
|
export * from './utils/utils';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/qti-rule-base';\nexport * from './lib/custom-operators';\n\nexport * from './utils/custom-state-set';\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry behind `<qti-custom-operator definition="…">`.
|
|
3
|
+
*
|
|
4
|
+
* A custom operator is by definition outside the QTI vocabulary, so which ones
|
|
5
|
+
* exist is a property of the delivery engine rather than of an item. An item
|
|
6
|
+
* names one in its `definition` attribute and the engine supplies it — here,
|
|
7
|
+
* through this registry.
|
|
8
|
+
*
|
|
9
|
+
* This is the second of two mechanisms on `qti-custom-operator`. The other runs
|
|
10
|
+
* inline JavaScript out of a CDATA section and is selected with
|
|
11
|
+
* `class="js.org"`; it stays as it is. `definition` is what items authored
|
|
12
|
+
* against the Citolab scoring engine use, and the two do not interact.
|
|
13
|
+
*/
|
|
14
|
+
export type CustomOperatorValue = string | string[] | number | boolean | null;
|
|
15
|
+
/**
|
|
16
|
+
* Handed the calculated values of the element's child expressions, in document
|
|
17
|
+
* order, and returns the value of the expression.
|
|
18
|
+
*/
|
|
19
|
+
export type CustomOperator = (values: CustomOperatorValue[]) => CustomOperatorValue;
|
|
20
|
+
/**
|
|
21
|
+
* Make an operator available to every `qti-custom-operator` that names it.
|
|
22
|
+
*
|
|
23
|
+
* The key is the `definition` attribute in full, prefix included. Registering a
|
|
24
|
+
* key a built-in already uses overrides that built-in, which is how a delivery
|
|
25
|
+
* engine replaces one it disagrees with.
|
|
26
|
+
*/
|
|
27
|
+
export declare const registerCustomOperator: (definition: string, operator: CustomOperator) => void;
|
|
28
|
+
/** Undo a {@link registerCustomOperator}, restoring any built-in of that name. */
|
|
29
|
+
export declare const unregisterCustomOperator: (definition: string) => void;
|
|
30
|
+
/** The operator for a `definition`, or null when nothing provides it. */
|
|
31
|
+
export declare const getCustomOperator: (definition: string) => CustomOperator | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry behind `<qti-custom-operator definition="…">`.
|
|
3
|
+
*
|
|
4
|
+
* A custom operator is by definition outside the QTI vocabulary, so which ones
|
|
5
|
+
* exist is a property of the delivery engine rather than of an item. An item
|
|
6
|
+
* names one in its `definition` attribute and the engine supplies it — here,
|
|
7
|
+
* through this registry.
|
|
8
|
+
*
|
|
9
|
+
* This is the second of two mechanisms on `qti-custom-operator`. The other runs
|
|
10
|
+
* inline JavaScript out of a CDATA section and is selected with
|
|
11
|
+
* `class="js.org"`; it stays as it is. `definition` is what items authored
|
|
12
|
+
* against the Citolab scoring engine use, and the two do not interact.
|
|
13
|
+
*/
|
|
14
|
+
/** The value an operator most often works on: the first child, as text. */
|
|
15
|
+
const firstString = (values) => {
|
|
16
|
+
const value = values[0];
|
|
17
|
+
if (value === null || value === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return Array.isArray(value) ? (value[0] ?? null) : String(value);
|
|
20
|
+
};
|
|
21
|
+
/** A text operator that leaves a NULL or absent first value alone. */
|
|
22
|
+
const textOperator = (transform) => values => {
|
|
23
|
+
const value = firstString(values);
|
|
24
|
+
return value === null ? null : transform(value);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The three operators the scoring engine ships, under each of the prefixes it
|
|
28
|
+
* registers them with. The prefix is part of the key because `definition` is
|
|
29
|
+
* matched whole, exactly as the item writes it.
|
|
30
|
+
*/
|
|
31
|
+
const BUILT_IN_PREFIXES = ['depcp', 'questify', 'qade'];
|
|
32
|
+
const BUILT_IN_OPERATORS = {
|
|
33
|
+
Trim: textOperator(value => value.trim()),
|
|
34
|
+
// Decompose, then drop the combining marks that decomposition split off, so
|
|
35
|
+
// "café" compares as "cafe".
|
|
36
|
+
ToAscii: textOperator(value => value.normalize('NFD').replace(/[̀-ͯ]/g, '')),
|
|
37
|
+
// A decimal comma written where the response processing expects a dot. Only
|
|
38
|
+
// the first comma, which is the decimal separator in the locales that use it.
|
|
39
|
+
ParseCommaDecimal: textOperator(value => value.replace(',', '.'))
|
|
40
|
+
};
|
|
41
|
+
const builtIns = new Map(BUILT_IN_PREFIXES.flatMap(prefix => Object.entries(BUILT_IN_OPERATORS).map(([name, operator]) => [
|
|
42
|
+
`${prefix}:${name}`,
|
|
43
|
+
operator
|
|
44
|
+
])));
|
|
45
|
+
/** Registered by the host application; consulted before the built-ins. */
|
|
46
|
+
const registered = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Make an operator available to every `qti-custom-operator` that names it.
|
|
49
|
+
*
|
|
50
|
+
* The key is the `definition` attribute in full, prefix included. Registering a
|
|
51
|
+
* key a built-in already uses overrides that built-in, which is how a delivery
|
|
52
|
+
* engine replaces one it disagrees with.
|
|
53
|
+
*/
|
|
54
|
+
export const registerCustomOperator = (definition, operator) => {
|
|
55
|
+
registered.set(definition, operator);
|
|
56
|
+
};
|
|
57
|
+
/** Undo a {@link registerCustomOperator}, restoring any built-in of that name. */
|
|
58
|
+
export const unregisterCustomOperator = (definition) => {
|
|
59
|
+
registered.delete(definition);
|
|
60
|
+
};
|
|
61
|
+
/** The operator for a `definition`, or null when nothing provides it. */
|
|
62
|
+
export const getCustomOperator = (definition) => registered.get(definition) ?? builtIns.get(definition) ?? null;
|
|
63
|
+
//# sourceMappingURL=custom-operators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-operators.js","sourceRoot":"","sources":["../../src/lib/custom-operators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,2EAA2E;AAC3E,MAAM,WAAW,GAAG,CAAC,MAA6B,EAAiB,EAAE;IACnE,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC,CAAC;AAEF,sEAAsE;AACtE,MAAM,YAAY,GAChB,CAAC,SAAoC,EAAkB,EAAE,CACzD,MAAM,CAAC,EAAE;IACP,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC,CAAC;AAEJ;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAU,CAAC;AAEjE,MAAM,kBAAkB,GAAmC;IACzD,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACzC,4EAA4E;IAC5E,6BAA6B;IAC7B,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5E,4EAA4E;IAC5E,8EAA8E;IAC9E,iBAAiB,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,iBAAiB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CACjC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAA4B,EAAE,CAAC;IACrF,GAAG,MAAM,IAAI,IAAI,EAAE;IACnB,QAAQ;CACT,CAAC,CACH,CACF,CAAC;AAEF,0EAA0E;AAC1E,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,UAAkB,EAAE,QAAwB,EAAQ,EAAE;IAC3F,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AACvC,CAAC,CAAC;AAEF,kFAAkF;AAClF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,UAAkB,EAAQ,EAAE;IACnE,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AAChC,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,UAAkB,EAAyB,EAAE,CAC7E,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC","sourcesContent":["/**\n * The registry behind `<qti-custom-operator definition=\"…\">`.\n *\n * A custom operator is by definition outside the QTI vocabulary, so which ones\n * exist is a property of the delivery engine rather than of an item. An item\n * names one in its `definition` attribute and the engine supplies it — here,\n * through this registry.\n *\n * This is the second of two mechanisms on `qti-custom-operator`. The other runs\n * inline JavaScript out of a CDATA section and is selected with\n * `class=\"js.org\"`; it stays as it is. `definition` is what items authored\n * against the Citolab scoring engine use, and the two do not interact.\n */\n\nexport type CustomOperatorValue = string | string[] | number | boolean | null;\n\n/**\n * Handed the calculated values of the element's child expressions, in document\n * order, and returns the value of the expression.\n */\nexport type CustomOperator = (values: CustomOperatorValue[]) => CustomOperatorValue;\n\n/** The value an operator most often works on: the first child, as text. */\nconst firstString = (values: CustomOperatorValue[]): string | null => {\n const value = values[0];\n if (value === null || value === undefined) return null;\n return Array.isArray(value) ? (value[0] ?? null) : String(value);\n};\n\n/** A text operator that leaves a NULL or absent first value alone. */\nconst textOperator =\n (transform: (value: string) => string): CustomOperator =>\n values => {\n const value = firstString(values);\n return value === null ? null : transform(value);\n };\n\n/**\n * The three operators the scoring engine ships, under each of the prefixes it\n * registers them with. The prefix is part of the key because `definition` is\n * matched whole, exactly as the item writes it.\n */\nconst BUILT_IN_PREFIXES = ['depcp', 'questify', 'qade'] as const;\n\nconst BUILT_IN_OPERATORS: Record<string, CustomOperator> = {\n Trim: textOperator(value => value.trim()),\n // Decompose, then drop the combining marks that decomposition split off, so\n // \"café\" compares as \"cafe\".\n ToAscii: textOperator(value => value.normalize('NFD').replace(/[̀-ͯ]/g, '')),\n // A decimal comma written where the response processing expects a dot. Only\n // the first comma, which is the decimal separator in the locales that use it.\n ParseCommaDecimal: textOperator(value => value.replace(',', '.'))\n};\n\nconst builtIns = new Map<string, CustomOperator>(\n BUILT_IN_PREFIXES.flatMap(prefix =>\n Object.entries(BUILT_IN_OPERATORS).map(([name, operator]): [string, CustomOperator] => [\n `${prefix}:${name}`,\n operator\n ])\n )\n);\n\n/** Registered by the host application; consulted before the built-ins. */\nconst registered = new Map<string, CustomOperator>();\n\n/**\n * Make an operator available to every `qti-custom-operator` that names it.\n *\n * The key is the `definition` attribute in full, prefix included. Registering a\n * key a built-in already uses overrides that built-in, which is how a delivery\n * engine replaces one it disagrees with.\n */\nexport const registerCustomOperator = (definition: string, operator: CustomOperator): void => {\n registered.set(definition, operator);\n};\n\n/** Undo a {@link registerCustomOperator}, restoring any built-in of that name. */\nexport const unregisterCustomOperator = (definition: string): void => {\n registered.delete(definition);\n};\n\n/** The operator for a `definition`, or null when nothing provides it. */\nexport const getCustomOperator = (definition: string): CustomOperator | null =>\n registered.get(definition) ?? builtIns.get(definition) ?? null;\n"]}
|
|
@@ -11,3 +11,28 @@ export interface QtiRuleBase {
|
|
|
11
11
|
export declare abstract class QtiRuleElement extends LitElement implements QtiRuleBase {
|
|
12
12
|
abstract process(): any;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Thrown by `qti-exit-response` to end the response processing of an attempt.
|
|
16
|
+
*
|
|
17
|
+
* An exit can sit at any depth — inside a `qti-response-condition` branch, or
|
|
18
|
+
* inside a `qti-response-processing-fragment` — and has to stop every rule
|
|
19
|
+
* after it, not just its own siblings. Unwinding the stack is what reaches
|
|
20
|
+
* every one of those loops without each of them having to report upwards.
|
|
21
|
+
*
|
|
22
|
+
* `qti-response-processing` catches it at the top of the run. The outcomes set
|
|
23
|
+
* before the exit are kept, because each `qti-set-outcome-value` has already
|
|
24
|
+
* dispatched by the time this is thrown.
|
|
25
|
+
*/
|
|
26
|
+
export declare class QtiExitResponseSignal extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown by `qti-exit-test` to end the outcome processing of a test.
|
|
31
|
+
*
|
|
32
|
+
* The test-level twin of {@link QtiExitResponseSignal}, and unwinds the stack
|
|
33
|
+
* for the same reason: an exit inside a `qti-outcome-condition` branch has to
|
|
34
|
+
* stop the rules after the condition, not just its own siblings.
|
|
35
|
+
*/
|
|
36
|
+
export declare class QtiExitTestSignal extends Error {
|
|
37
|
+
constructor();
|
|
38
|
+
}
|
|
@@ -4,4 +4,35 @@ import { LitElement } from 'lit';
|
|
|
4
4
|
*/
|
|
5
5
|
export class QtiRuleElement extends LitElement {
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Thrown by `qti-exit-response` to end the response processing of an attempt.
|
|
9
|
+
*
|
|
10
|
+
* An exit can sit at any depth — inside a `qti-response-condition` branch, or
|
|
11
|
+
* inside a `qti-response-processing-fragment` — and has to stop every rule
|
|
12
|
+
* after it, not just its own siblings. Unwinding the stack is what reaches
|
|
13
|
+
* every one of those loops without each of them having to report upwards.
|
|
14
|
+
*
|
|
15
|
+
* `qti-response-processing` catches it at the top of the run. The outcomes set
|
|
16
|
+
* before the exit are kept, because each `qti-set-outcome-value` has already
|
|
17
|
+
* dispatched by the time this is thrown.
|
|
18
|
+
*/
|
|
19
|
+
export class QtiExitResponseSignal extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super('qti-exit-response');
|
|
22
|
+
this.name = 'QtiExitResponseSignal';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by `qti-exit-test` to end the outcome processing of a test.
|
|
27
|
+
*
|
|
28
|
+
* The test-level twin of {@link QtiExitResponseSignal}, and unwinds the stack
|
|
29
|
+
* for the same reason: an exit inside a `qti-outcome-condition` branch has to
|
|
30
|
+
* stop the rules after the condition, not just its own siblings.
|
|
31
|
+
*/
|
|
32
|
+
export class QtiExitTestSignal extends Error {
|
|
33
|
+
constructor() {
|
|
34
|
+
super('qti-exit-test');
|
|
35
|
+
this.name = 'QtiExitTestSignal';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
7
38
|
//# sourceMappingURL=qti-rule-base.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qti-rule-base.js","sourceRoot":"","sources":["../../src/lib/qti-rule-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AASjC;;GAEG;AACH,MAAM,OAAgB,cAAe,SAAQ,UAAU;CAEtD","sourcesContent":["import { LitElement } from 'lit';\n\n/**\n * Interface for any object that can process QTI rules\n */\nexport interface QtiRuleBase {\n process(): any;\n}\n\n/**\n * Abstract base class for QTI rule DOM elements\n */\nexport abstract class QtiRuleElement extends LitElement implements QtiRuleBase {\n abstract process(): any;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"qti-rule-base.js","sourceRoot":"","sources":["../../src/lib/qti-rule-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AASjC;;GAEG;AACH,MAAM,OAAgB,cAAe,SAAQ,UAAU;CAEtD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C;QACE,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF","sourcesContent":["import { LitElement } from 'lit';\n\n/**\n * Interface for any object that can process QTI rules\n */\nexport interface QtiRuleBase {\n process(): any;\n}\n\n/**\n * Abstract base class for QTI rule DOM elements\n */\nexport abstract class QtiRuleElement extends LitElement implements QtiRuleBase {\n abstract process(): any;\n}\n\n/**\n * Thrown by `qti-exit-response` to end the response processing of an attempt.\n *\n * An exit can sit at any depth — inside a `qti-response-condition` branch, or\n * inside a `qti-response-processing-fragment` — and has to stop every rule\n * after it, not just its own siblings. Unwinding the stack is what reaches\n * every one of those loops without each of them having to report upwards.\n *\n * `qti-response-processing` catches it at the top of the run. The outcomes set\n * before the exit are kept, because each `qti-set-outcome-value` has already\n * dispatched by the time this is thrown.\n */\nexport class QtiExitResponseSignal extends Error {\n constructor() {\n super('qti-exit-response');\n this.name = 'QtiExitResponseSignal';\n }\n}\n\n/**\n * Thrown by `qti-exit-test` to end the outcome processing of a test.\n *\n * The test-level twin of {@link QtiExitResponseSignal}, and unwinds the stack\n * for the same reason: an exit inside a `qti-outcome-condition` branch has to\n * stop the rules after the condition, not just its own siblings.\n */\nexport class QtiExitTestSignal extends Error {\n constructor() {\n super('qti-exit-test');\n this.name = 'QtiExitTestSignal';\n }\n}\n"]}
|
package/dist/lib/variables.d.ts
CHANGED
|
@@ -33,8 +33,24 @@ export interface VariableDeclaration<T> extends VariableValue<T> {
|
|
|
33
33
|
cardinality?: Cardinality;
|
|
34
34
|
baseType?: BaseType;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* One `qti-interpolation-table-entry`. `sourceValue` is the *lower bound* of a
|
|
38
|
+
* range rather than a value to match exactly, so the entries have to keep their
|
|
39
|
+
* document order: the first whose bound the source value clears wins.
|
|
40
|
+
*/
|
|
41
|
+
export interface InterpolationTableEntry {
|
|
42
|
+
sourceValue: number;
|
|
43
|
+
targetValue: number;
|
|
44
|
+
/** Whether `sourceValue` itself is in range. Defaults to true, per the spec. */
|
|
45
|
+
includeBoundary: boolean;
|
|
46
|
+
}
|
|
36
47
|
export interface OutcomeVariable extends VariableDeclaration<string | string[] | null> {
|
|
37
|
-
interpolationTable?:
|
|
48
|
+
interpolationTable?: InterpolationTableEntry[];
|
|
49
|
+
/**
|
|
50
|
+
* A qti-match-table's entries. Its targets are not necessarily numeric — a
|
|
51
|
+
* match table may map onto identifiers or strings — so they stay as written.
|
|
52
|
+
*/
|
|
53
|
+
matchTable?: Map<number, string>;
|
|
38
54
|
externalScored?: 'human' | 'externalMachine' | null;
|
|
39
55
|
}
|
|
40
56
|
export interface ResponseVariable extends VariableDeclaration<string | string[] | null> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"variables.js","sourceRoot":"","sources":["../../src/lib/variables.ts"],"names":[],"mappings":"","sourcesContent":["import type { BaseType, Cardinality } from './expression-result';\n\nexport type QtiAreaMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n areaMapEntries: QtiAreaMapEntry[];\n};\n\nexport type QtiAreaMapEntry = {\n shape: areaShape;\n coords: string;\n mappedValue: number;\n defaultValue: number;\n};\n\nexport type QtiMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n mapEntries: QtiMapEntry[];\n};\n\nexport type QtiMapEntry = { mapKey: string; mappedValue: number; caseSensitive: boolean };\n\nexport type areaShape = 'default' | 'circle' | 'rect' | 'ellipse' | 'poly';\nexport interface VariableValue<T> {\n identifier: string;\n value: Readonly<T>;\n defaultValue?: T | null;\n type: 'outcome' | 'response' | 'context' | 'template';\n}\n\nexport interface VariableDeclaration<T> extends VariableValue<T> {\n cardinality?: Cardinality;\n baseType?: BaseType;\n}\n\nexport interface OutcomeVariable extends VariableDeclaration<string | string[] | null> {\n // specific to outcome variables\n interpolationTable?: Map<number,
|
|
1
|
+
{"version":3,"file":"variables.js","sourceRoot":"","sources":["../../src/lib/variables.ts"],"names":[],"mappings":"","sourcesContent":["import type { BaseType, Cardinality } from './expression-result';\n\nexport type QtiAreaMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n areaMapEntries: QtiAreaMapEntry[];\n};\n\nexport type QtiAreaMapEntry = {\n shape: areaShape;\n coords: string;\n mappedValue: number;\n defaultValue: number;\n};\n\nexport type QtiMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n mapEntries: QtiMapEntry[];\n};\n\nexport type QtiMapEntry = { mapKey: string; mappedValue: number; caseSensitive: boolean };\n\nexport type areaShape = 'default' | 'circle' | 'rect' | 'ellipse' | 'poly';\nexport interface VariableValue<T> {\n identifier: string;\n value: Readonly<T>;\n defaultValue?: T | null;\n type: 'outcome' | 'response' | 'context' | 'template';\n}\n\nexport interface VariableDeclaration<T> extends VariableValue<T> {\n cardinality?: Cardinality;\n baseType?: BaseType;\n}\n\n/**\n * One `qti-interpolation-table-entry`. `sourceValue` is the *lower bound* of a\n * range rather than a value to match exactly, so the entries have to keep their\n * document order: the first whose bound the source value clears wins.\n */\nexport interface InterpolationTableEntry {\n sourceValue: number;\n targetValue: number;\n /** Whether `sourceValue` itself is in range. Defaults to true, per the spec. */\n includeBoundary: boolean;\n}\n\nexport interface OutcomeVariable extends VariableDeclaration<string | string[] | null> {\n // specific to outcome variables\n interpolationTable?: InterpolationTableEntry[];\n /**\n * A qti-match-table's entries. Its targets are not necessarily numeric — a\n * match table may map onto identifiers or strings — so they stay as written.\n */\n matchTable?: Map<number, string>;\n externalScored?: 'human' | 'externalMachine' | null;\n}\n\nexport interface ResponseVariable extends VariableDeclaration<string | string[] | null> {\n // specific to response variables\n candidateResponse?: string | string[] | null;\n mapping?: QtiMapping;\n areaMapping?: QtiAreaMapping; // Optional property for area mappings\n correctResponse?: string | string[] | null;\n}\n\nexport interface TemplateVariable extends VariableDeclaration<string | string[] | number | boolean | null> {\n type: 'template';\n mathVariable?: boolean;\n paramVariable?: boolean;\n}\n"]}
|
|
@@ -17,8 +17,15 @@ export class ScoringHelper {
|
|
|
17
17
|
const [shape, ...coords] = areaKey.split(',');
|
|
18
18
|
const coordinates = coords.map(Number);
|
|
19
19
|
switch (shape.toLowerCase()) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// "The default shape refers to the entire area of the associated image" (QTI 3.0 §8.29), so
|
|
21
|
+
// every point is inside it and the coords are not read — §7.9 says none should be given, and
|
|
22
|
+
// the XSD still requires the attribute, so whatever is there is filler.
|
|
23
|
+
//
|
|
24
|
+
// This used to share the `circle` branch below, which demands exactly three coords and so
|
|
25
|
+
// rejected any real default entry as an "Invalid circle definition" (Citolab/qti-components#83).
|
|
26
|
+
case 'default':
|
|
27
|
+
return true;
|
|
28
|
+
case 'circle': {
|
|
22
29
|
const [cx, cy, radius] = coordinates;
|
|
23
30
|
if (coordinates.length !== 3) {
|
|
24
31
|
console.warn(`Invalid circle definition: ${areaKey}`);
|
|
@@ -88,6 +95,23 @@ export class ScoringHelper {
|
|
|
88
95
|
const int1 = parseInt(value1, 10);
|
|
89
96
|
const int2 = parseInt(value2, 10);
|
|
90
97
|
if (!isNaN(int1) && !isNaN(int2)) {
|
|
98
|
+
/*
|
|
99
|
+
* `parseInt` truncates, so on its own it reads "2" and "2.5" as the
|
|
100
|
+
* same number. That matters because the base type is taken from one
|
|
101
|
+
* operand: an expression whose result happens to be whole is an
|
|
102
|
+
* integer, and it was then compared against a fractional value with
|
|
103
|
+
* the fraction thrown away — `equal(divide(4, 2), 2.5)` was true.
|
|
104
|
+
*
|
|
105
|
+
* When either side has a fractional part the two are, by definition,
|
|
106
|
+
* not the same number. Comparing them in full says so, while the
|
|
107
|
+
* integer path keeps `parseInt`'s tolerance of a value like "12 euro"
|
|
108
|
+
* for everything that really is whole.
|
|
109
|
+
*/
|
|
110
|
+
const float1 = parseFloat(value1);
|
|
111
|
+
const float2 = parseFloat(value2);
|
|
112
|
+
if (!Number.isInteger(float1) || !Number.isInteger(float2)) {
|
|
113
|
+
return float1 === float2;
|
|
114
|
+
}
|
|
91
115
|
return int1 === int2;
|
|
92
116
|
}
|
|
93
117
|
else {
|
|
@@ -108,8 +132,17 @@ export class ScoringHelper {
|
|
|
108
132
|
}
|
|
109
133
|
case 'pair':
|
|
110
134
|
case 'directedPair': {
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
/*
|
|
136
|
+
* Only a `pair` is unordered. A `directedPair` is directed — "A B" and
|
|
137
|
+
* "B A" are different values — so it must be compared as written.
|
|
138
|
+
*
|
|
139
|
+
* Both operands used to be sorted the moment they were split, which
|
|
140
|
+
* made the `baseType === 'pair'` sort below it dead code and every
|
|
141
|
+
* directedPair comparison order-insensitive: a candidate who matched
|
|
142
|
+
* the right two identifiers the wrong way round scored as correct.
|
|
143
|
+
*/
|
|
144
|
+
const pair1 = value1.split(' ');
|
|
145
|
+
const pair2 = value2.split(' ');
|
|
113
146
|
if (pair1.length === 2 && pair2.length === 2) {
|
|
114
147
|
if (baseType === 'pair') {
|
|
115
148
|
pair1.sort();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoring-helper.js","sourceRoot":"","sources":["../../src/utils/scoring-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BaseType } from '../lib/expression-result';\n\nexport class ScoringHelper {\n /**\n * Checks if a given point is within a specified area.\n * @param point The point to test, represented as a string \"x y\" (e.g., \"102 113\").\n * @param areaKey The area definition, including shape and coordinates (e.g., \"circle,102,113,16\").\n * @param baseType The base type of the response, must be \"point\" for this method to proceed.\n * @returns True if the point is within the area; false otherwise.\n */\n public static isPointInArea(point: string, areaKey: string, baseType: string): boolean {\n if (baseType !== 'point') {\n console.warn(`Base type ${baseType} is not supported for point area mapping.`);\n return false;\n }\n\n // Parse the point as x and y coordinates\n const [px, py] = point.split(' ').map(Number);\n\n // Parse the area definition\n const [shape, ...coords] = areaKey.split(',');\n const coordinates = coords.map(Number);\n\n switch (shape.toLowerCase()) {\n case 'circle':\n case 'default': {\n const [cx, cy, radius] = coordinates;\n if (coordinates.length !== 3) {\n console.warn(`Invalid circle definition: ${areaKey}`);\n return false;\n }\n const distance = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);\n return distance <= radius;\n }\n\n case 'rect': {\n const [x1, y1, x2, y2] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid rectangle definition: ${areaKey}`);\n return false;\n }\n return px >= x1 && px <= x2 && py >= y1 && py <= y2;\n }\n\n case 'ellipse': {\n const [cx, cy, rx, ry] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid ellipse definition: ${areaKey}`);\n return false;\n }\n // Ellipse equation: ((px - cx)² / rx²) + ((py - cy)² / ry²) <= 1\n const normalizedX = (px - cx) ** 2 / rx ** 2;\n const normalizedY = (py - cy) ** 2 / ry ** 2;\n return normalizedX + normalizedY <= 1;\n }\n\n case 'poly': {\n if (coordinates.length < 6 || coordinates.length % 2 !== 0) {\n console.warn(`Invalid polygon definition: ${areaKey}`);\n return false;\n }\n const vertices = [];\n for (let i = 0; i < coordinates.length; i += 2) {\n vertices.push({ x: coordinates[i], y: coordinates[i + 1] });\n }\n return this.isPointInPolygon({ x: px, y: py }, vertices);\n }\n\n default:\n console.warn(`Unsupported shape type: ${shape}`);\n return false;\n }\n }\n\n /**\n * Checks if a point is inside a polygon using the ray-casting algorithm.\n * @param point The point to test.\n * @param vertices The vertices of the polygon in order (array of {x, y} objects).\n * @returns True if the point is inside the polygon; false otherwise.\n */\n static isPointInPolygon(point: { x: number; y: number }, vertices: { x: number; y: number }[]): boolean {\n let inside = false;\n for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {\n const xi = vertices[i].x,\n yi = vertices[i].y;\n const xj = vertices[j].x,\n yj = vertices[j].y;\n\n const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;\n\n if (intersect) inside = !inside;\n }\n return inside;\n }\n\n public static compareSingleValues(value1: Readonly<string>, value2: Readonly<string>, baseType: BaseType): boolean {\n switch (baseType) {\n case 'identifier':\n case 'string':\n return value1 === value2;\n case 'integer': {\n const int1 = parseInt(value1, 10);\n const int2 = parseInt(value2, 10);\n if (!isNaN(int1) && !isNaN(int2)) {\n return int1 === int2;\n } else {\n console.error(`Cannot convert ${value1} and/or ${value2} to int.`);\n }\n break;\n }\n case 'float': {\n const float1 = parseFloat(value1);\n const float2 = parseFloat(value2);\n if (!isNaN(float1) && !isNaN(float2)) {\n return float1 === float2;\n } else {\n console.error(`couldn't convert ${value1} and/or ${value2} to float.`);\n }\n break;\n }\n case 'pair':\n case 'directedPair': {\n const pair1 = value1.split(' ').sort();\n const pair2 = value2.split(' ').sort();\n if (pair1.length === 2 && pair2.length === 2) {\n if (baseType === 'pair') {\n pair1.sort();\n pair2.sort();\n }\n return pair1.join(' ') === pair2.join(' ');\n } else {\n console.error(`compared two pair but one of the values does not have 2 values: 1: ${value1} 2: ${value2}`);\n }\n break;\n }\n }\n\n return false;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scoring-helper.js","sourceRoot":"","sources":["../../src/utils/scoring-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BaseType } from '../lib/expression-result';\n\nexport class ScoringHelper {\n /**\n * Checks if a given point is within a specified area.\n * @param point The point to test, represented as a string \"x y\" (e.g., \"102 113\").\n * @param areaKey The area definition, including shape and coordinates (e.g., \"circle,102,113,16\").\n * @param baseType The base type of the response, must be \"point\" for this method to proceed.\n * @returns True if the point is within the area; false otherwise.\n */\n public static isPointInArea(point: string, areaKey: string, baseType: string): boolean {\n if (baseType !== 'point') {\n console.warn(`Base type ${baseType} is not supported for point area mapping.`);\n return false;\n }\n\n // Parse the point as x and y coordinates\n const [px, py] = point.split(' ').map(Number);\n\n // Parse the area definition\n const [shape, ...coords] = areaKey.split(',');\n const coordinates = coords.map(Number);\n\n switch (shape.toLowerCase()) {\n // \"The default shape refers to the entire area of the associated image\" (QTI 3.0 §8.29), so\n // every point is inside it and the coords are not read — §7.9 says none should be given, and\n // the XSD still requires the attribute, so whatever is there is filler.\n //\n // This used to share the `circle` branch below, which demands exactly three coords and so\n // rejected any real default entry as an \"Invalid circle definition\" (Citolab/qti-components#83).\n case 'default':\n return true;\n\n case 'circle': {\n const [cx, cy, radius] = coordinates;\n if (coordinates.length !== 3) {\n console.warn(`Invalid circle definition: ${areaKey}`);\n return false;\n }\n const distance = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);\n return distance <= radius;\n }\n\n case 'rect': {\n const [x1, y1, x2, y2] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid rectangle definition: ${areaKey}`);\n return false;\n }\n return px >= x1 && px <= x2 && py >= y1 && py <= y2;\n }\n\n case 'ellipse': {\n const [cx, cy, rx, ry] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid ellipse definition: ${areaKey}`);\n return false;\n }\n // Ellipse equation: ((px - cx)² / rx²) + ((py - cy)² / ry²) <= 1\n const normalizedX = (px - cx) ** 2 / rx ** 2;\n const normalizedY = (py - cy) ** 2 / ry ** 2;\n return normalizedX + normalizedY <= 1;\n }\n\n case 'poly': {\n if (coordinates.length < 6 || coordinates.length % 2 !== 0) {\n console.warn(`Invalid polygon definition: ${areaKey}`);\n return false;\n }\n const vertices = [];\n for (let i = 0; i < coordinates.length; i += 2) {\n vertices.push({ x: coordinates[i], y: coordinates[i + 1] });\n }\n return this.isPointInPolygon({ x: px, y: py }, vertices);\n }\n\n default:\n console.warn(`Unsupported shape type: ${shape}`);\n return false;\n }\n }\n\n /**\n * Checks if a point is inside a polygon using the ray-casting algorithm.\n * @param point The point to test.\n * @param vertices The vertices of the polygon in order (array of {x, y} objects).\n * @returns True if the point is inside the polygon; false otherwise.\n */\n static isPointInPolygon(point: { x: number; y: number }, vertices: { x: number; y: number }[]): boolean {\n let inside = false;\n for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {\n const xi = vertices[i].x,\n yi = vertices[i].y;\n const xj = vertices[j].x,\n yj = vertices[j].y;\n\n const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;\n\n if (intersect) inside = !inside;\n }\n return inside;\n }\n\n public static compareSingleValues(value1: Readonly<string>, value2: Readonly<string>, baseType: BaseType): boolean {\n switch (baseType) {\n case 'identifier':\n case 'string':\n return value1 === value2;\n case 'integer': {\n const int1 = parseInt(value1, 10);\n const int2 = parseInt(value2, 10);\n if (!isNaN(int1) && !isNaN(int2)) {\n /*\n * `parseInt` truncates, so on its own it reads \"2\" and \"2.5\" as the\n * same number. That matters because the base type is taken from one\n * operand: an expression whose result happens to be whole is an\n * integer, and it was then compared against a fractional value with\n * the fraction thrown away — `equal(divide(4, 2), 2.5)` was true.\n *\n * When either side has a fractional part the two are, by definition,\n * not the same number. Comparing them in full says so, while the\n * integer path keeps `parseInt`'s tolerance of a value like \"12 euro\"\n * for everything that really is whole.\n */\n const float1 = parseFloat(value1);\n const float2 = parseFloat(value2);\n if (!Number.isInteger(float1) || !Number.isInteger(float2)) {\n return float1 === float2;\n }\n return int1 === int2;\n } else {\n console.error(`Cannot convert ${value1} and/or ${value2} to int.`);\n }\n break;\n }\n case 'float': {\n const float1 = parseFloat(value1);\n const float2 = parseFloat(value2);\n if (!isNaN(float1) && !isNaN(float2)) {\n return float1 === float2;\n } else {\n console.error(`couldn't convert ${value1} and/or ${value2} to float.`);\n }\n break;\n }\n case 'pair':\n case 'directedPair': {\n /*\n * Only a `pair` is unordered. A `directedPair` is directed — \"A B\" and\n * \"B A\" are different values — so it must be compared as written.\n *\n * Both operands used to be sorted the moment they were split, which\n * made the `baseType === 'pair'` sort below it dead code and every\n * directedPair comparison order-insensitive: a candidate who matched\n * the right two identifiers the wrong way round scored as correct.\n */\n const pair1 = value1.split(' ');\n const pair2 = value2.split(' ');\n if (pair1.length === 2 && pair2.length === 2) {\n if (baseType === 'pair') {\n pair1.sort();\n pair2.sort();\n }\n return pair1.join(' ') === pair2.join(' ');\n } else {\n console.error(`compared two pair but one of the values does not have 2 values: 1: ${value1} 2: ${value2}`);\n }\n break;\n }\n }\n\n return false;\n }\n}\n"]}
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export declare const decimalSeparator: () => string;
|
|
2
|
+
/**
|
|
3
|
+
* Renders a number the way QTI stores one: a plain decimal string with `.` as the separator and no
|
|
4
|
+
* grouping, whatever the browser's locale is. Strings are already in that form and pass through.
|
|
5
|
+
*
|
|
6
|
+
* `Number.prototype.toString()` is exactly that format. The locale-aware versions this used to call
|
|
7
|
+
* are not: under a comma-decimal locale (nl, de, fr, ...) it ran `.replace('.', '')` over a string
|
|
8
|
+
* that always uses `.`, so a score of 0.5 was stored as "05" and 1.5 as "15" — an item's SCORE came
|
|
9
|
+
* out ten times too big for every value with a fraction. Under a dot-decimal locale
|
|
10
|
+
* `toLocaleString()` grouped thousands instead, turning 1234.5 into "1,234.5".
|
|
11
|
+
*/
|
|
2
12
|
export declare const convertNumberToUniversalFormat: (number: number | string) => string;
|
|
3
13
|
export declare function IsNullOrUndefined(value: unknown): boolean;
|
|
4
14
|
export declare function removeDoubleSlashes(str: string): string;
|
package/dist/utils/utils.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export const decimalSeparator = () => {
|
|
2
2
|
return new Intl.NumberFormat().format(0.1).replace(/\d/g, '');
|
|
3
3
|
};
|
|
4
|
+
/**
|
|
5
|
+
* Renders a number the way QTI stores one: a plain decimal string with `.` as the separator and no
|
|
6
|
+
* grouping, whatever the browser's locale is. Strings are already in that form and pass through.
|
|
7
|
+
*
|
|
8
|
+
* `Number.prototype.toString()` is exactly that format. The locale-aware versions this used to call
|
|
9
|
+
* are not: under a comma-decimal locale (nl, de, fr, ...) it ran `.replace('.', '')` over a string
|
|
10
|
+
* that always uses `.`, so a score of 0.5 was stored as "05" and 1.5 as "15" — an item's SCORE came
|
|
11
|
+
* out ten times too big for every value with a fraction. Under a dot-decimal locale
|
|
12
|
+
* `toLocaleString()` grouped thousands instead, turning 1234.5 into "1,234.5".
|
|
13
|
+
*/
|
|
4
14
|
export const convertNumberToUniversalFormat = (number) => {
|
|
5
|
-
|
|
6
|
-
if (typeof number === 'string') {
|
|
7
|
-
return number;
|
|
8
|
-
}
|
|
9
|
-
const dSep = decimalSeparator();
|
|
10
|
-
if (dSep === '.') {
|
|
11
|
-
return number.toLocaleString();
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
return number.toString().replace('.', '').replace(dSep, '.');
|
|
15
|
-
}
|
|
15
|
+
return typeof number === 'string' ? number : number.toString();
|
|
16
16
|
};
|
|
17
17
|
export function IsNullOrUndefined(value) {
|
|
18
18
|
return value === null || value === undefined;
|
package/dist/utils/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,EAAE;IACnC,OAAO,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,MAAuB,EAAE,EAAE;IACxE,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,EAAE;IACnC,OAAO,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,MAAuB,EAAE,EAAE;IACxE,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;AACjE,CAAC,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,oBAAoB,GAAG,GAAG;SAC7B,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC;SAC7B,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC;SAC5B,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClC,OAAO,oBAAoB,CAAC;AAC9B,CAAC","sourcesContent":["export const decimalSeparator = () => {\n return new Intl.NumberFormat().format(0.1).replace(/\\d/g, '');\n};\n\n/**\n * Renders a number the way QTI stores one: a plain decimal string with `.` as the separator and no\n * grouping, whatever the browser's locale is. Strings are already in that form and pass through.\n *\n * `Number.prototype.toString()` is exactly that format. The locale-aware versions this used to call\n * are not: under a comma-decimal locale (nl, de, fr, ...) it ran `.replace('.', '')` over a string\n * that always uses `.`, so a score of 0.5 was stored as \"05\" and 1.5 as \"15\" — an item's SCORE came\n * out ten times too big for every value with a fraction. Under a dot-decimal locale\n * `toLocaleString()` grouped thousands instead, turning 1234.5 into \"1,234.5\".\n */\nexport const convertNumberToUniversalFormat = (number: number | string) => {\n return typeof number === 'string' ? number : number.toString();\n};\n\nexport function IsNullOrUndefined(value: unknown) {\n return value === null || value === undefined;\n}\n\nexport function removeDoubleSlashes(str: string) {\n const singleForwardSlashes = str\n .replace(/([^:]\\/)\\/+/g, '$1')\n .replace(/\\/\\//g, '/')\n .replace('http:/', 'http://')\n .replace('https:/', 'https://');\n return singleForwardSlashes;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qti-components/base",
|
|
3
3
|
"description": "QTI shared utilities",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"author": "",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@qti-components/utilities": "^1.3.3"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@lit/context": "^1.1.6",
|
|
11
|
-
"lit": "^3.3.
|
|
11
|
+
"lit": "^3.3.1"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"qti",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"attw": "attw --profile esm-only --pack",
|
|
53
|
-
"build": "tsc --paths null",
|
|
53
|
+
"build": "pnpm run clean && tsc --paths null",
|
|
54
54
|
"clean": "rm -rf dist",
|
|
55
55
|
"yalc": "yalc publish",
|
|
56
56
|
"yalc:push": "yalc push"
|