@mathscan/math-exercise-engine 1.0.1 → 1.0.2

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/README.md CHANGED
@@ -15,26 +15,27 @@ A TypeScript DSL parser for mathematical exercises. Parse exercise definitions i
15
15
  ## 📦 Installation
16
16
 
17
17
  ```bash
18
- npm install math-exercise-engine
18
+ npm install @mathscan/math-exercise-engine
19
19
  # or
20
- pnpm add math-exercise-engine
20
+ pnpm add @mathscan/math-exercise-engine
21
21
  # or
22
- yarn add math-exercise-engine
22
+ yarn add @mathscan/math-exercise-engine
23
23
  ```
24
24
 
25
25
  ## 🚀 Quick Start
26
26
 
27
27
  ```typescript
28
- import { compile, validate, ExerciseRenderer } from 'math-exercise-engine';
28
+ import { compile, validate, ExerciseRenderer } from '@mathscan/math-exercise-engine';
29
29
 
30
30
  // Compile a DSL exercise to JSON
31
31
  const json = compile(`
32
- COMPARISON[350158 ? 2334443, answer="<"]
32
+ QUESTION_TEXT["What is 25 + 5?"]
33
+ 25 + NUMERIC_INPUT[name="x", digits=2, answer="5"] = 30
33
34
  WIDGET_ANSWER[type=simple]
34
35
  `);
35
36
 
36
37
  // Render in React
37
- import 'math-exercise-engine/style.css'; // Opt-in: import minimal default styles
38
+ import '@mathscan/math-exercise-engine/style.css'; // Opt-in: import minimal default styles
38
39
 
39
40
  // Option 1: Use built-in submit button
40
41
  <ExerciseRenderer
@@ -88,7 +89,7 @@ const exerciseRef = useRef<ExerciseRendererRef>(null);
88
89
  When using `ref` for custom button styling, you get full programmatic control:
89
90
 
90
91
  ```typescript
91
- import type { ExerciseRendererRef } from 'math-exercise-engine';
92
+ import type { ExerciseRendererRef } from '@mathscan/math-exercise-engine';
92
93
 
93
94
  const exerciseRef = useRef<ExerciseRendererRef>(null);
94
95
 
@@ -108,21 +109,37 @@ exerciseRef.current?.focusFirstInput(); // Focus first input field
108
109
 
109
110
  ## 📝 DSL Syntax
110
111
 
111
- ### Numeric Input Widget
112
+ ### Input Widgets
113
+
114
+ The DSL supports both legacy short names and modern readable aliases:
115
+
116
+ #### Numeric Input Widget
112
117
 
113
118
  ```
114
119
  W_N_I[name=a, digits=2]
120
+ // or use the readable alias
121
+ NUMERIC_INPUT[name=a, digits=2]
115
122
  ```
116
123
 
117
124
  Creates an input field for numbers with up to 2 digits.
118
125
 
119
- ### Comparison Widget
126
+ #### String Input Widget
127
+
128
+ ```
129
+ W_S_I[name=x, length=5]
130
+ // or use the readable alias
131
+ STRING_INPUT[name=x, length=5]
132
+ ```
133
+
134
+ Creates an input field for text with up to 5 characters.
135
+
136
+ ### Text Highlighter
120
137
 
121
138
  ```
122
- COMPARISON[350158 ? 2334443, answer="<"]
139
+ TEXT_HIGHLIGHTER[value="123456", start=2, end=3]
123
140
  ```
124
141
 
125
- Student must choose `<`, `>`, or `=` to compare two numbers.
142
+ Displays text with specific characters highlighted (useful for highlighting digits in numbers).
126
143
 
127
144
  ### Validators
128
145
 
@@ -140,25 +157,27 @@ WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
140
157
 
141
158
  ## 📚 Examples
142
159
 
143
- ### Example 1: Simple Comparison
160
+ ### Example 1: Math Expression (using readable aliases)
144
161
 
145
162
  ```
146
- COMPARISON[350158 ? 2334443, answer="<"]
147
- WIDGET_ANSWER[type=simple]
163
+ (NUMERIC_INPUT[name=a, digits=2] × NUMERIC_INPUT[name=b, digits=2]) + (NUMERIC_INPUT[name=c, digits=2] × NUMERIC_INPUT[name=d, digits=2]) = 1200
164
+ WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
148
165
  ```
149
166
 
150
- ### Example 2: Math Expression
167
+ ### Example 2: Simple Addition
151
168
 
152
169
  ```
153
- (W_N_I[name=a, digits=2] × W_N_I[name=b, digits=2]) + (W_N_I[name=c, digits=2] × W_N_I[name=d, digits=2]) = 1200
154
- WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
170
+ NUMERIC_INPUT[name=x, digits=2] + NUMERIC_INPUT[name=y, digits=2] = 100
171
+ WIDGET_ANSWER[type=expression, expr="x + y == 100"]
155
172
  ```
156
173
 
157
- ### Example 3: Simple Addition
174
+ ### Example 3: Text Highlighter
158
175
 
159
176
  ```
160
- W_N_I[name=x, digits=2] + W_N_I[name=y, digits=2] = 100
161
- WIDGET_ANSWER[type=expression, expr="x + y == 100"]
177
+ QUESTION_TEXT["What digit is highlighted?"]
178
+ The number TEXT_HIGHLIGHTER[value="123456", start=2, end=3] has a highlighted digit
179
+ STRING_INPUT[name=answer, length=1, answer="3"]
180
+ WIDGET_ANSWER[type=simple]
162
181
  ```
163
182
 
164
183
  ## 🔧 API Reference
@@ -166,7 +185,7 @@ WIDGET_ANSWER[type=expression, expr="x + y == 100"]
166
185
  ### Parsing
167
186
 
168
187
  ```typescript
169
- import { compile, tokenize, parse, toAST, toJSON } from 'math-exercise-engine';
188
+ import { compile, tokenize, parse, toAST, toJSON } from '@mathscan/math-exercise-engine';
170
189
 
171
190
  // Full pipeline (recommended)
172
191
  const json = compile(dslText);
@@ -184,7 +203,7 @@ const { ast, json } = compileWithAST(dslText);
184
203
  ### Validation
185
204
 
186
205
  ```typescript
187
- import { validate, validateSimple, validateExpression } from 'math-exercise-engine';
206
+ import { validate, validateSimple, validateExpression } from '@mathscan/math-exercise-engine';
188
207
 
189
208
  // Auto-detect validation type
190
209
  const result = validate(exerciseJSON, userInputs);
@@ -203,7 +222,7 @@ import type {
203
222
  ValidationResult,
204
223
  WidgetJSON,
205
224
  ExerciseRendererRef
206
- } from 'math-exercise-engine';
225
+ } from '@mathscan/math-exercise-engine';
207
226
 
208
227
  // ExerciseRendererRef interface:
209
228
  interface ExerciseRendererRef {
@@ -222,47 +241,43 @@ All layout items include unique `id` fields for React/JSX rendering:
222
241
  {
223
242
  "version": "2.0",
224
243
  "layout": [
225
- { "id": "text_1", "type": "text", "value": "350158" },
226
- { "id": "widget-ref_2", "type": "widget-ref", "widgetId": "comparison_1" },
227
- { "id": "text_3", "type": "text", "value": "2334443" }
244
+ { "id": "text_1", "type": "text", "value": "25" },
245
+ { "id": "operator_2", "type": "operator", "value": "+" },
246
+ { "id": "widget-ref_3", "type": "widget-ref", "widgetId": "x" },
247
+ { "id": "operator_4", "type": "operator", "value": "=" },
248
+ { "id": "text_5", "type": "text", "value": "30" }
228
249
  ],
229
250
  "widgets": {
230
- "comparison_1": {
231
- "type": "comparison",
232
- "id": "comparison_1",
251
+ "x": {
252
+ "type": "numeric-input",
253
+ "id": "x",
233
254
  "config": {
234
- "left": 350158,
235
- "right": 2334443
255
+ "name": "x",
256
+ "digits": 2
236
257
  }
237
258
  }
238
259
  },
239
260
  "validation": {
240
261
  "mode": "simple",
241
262
  "answers": {
242
- "comparison_1": "<"
263
+ "x": "5"
243
264
  }
244
265
  }
245
266
  }
246
267
  ```
247
268
 
248
- > 💡 **New:** Layout items now include unique IDs perfect for React keys. [Learn more](docs/LAYOUT_IDS.md)
249
-
250
269
  ## ✅ Testing
251
270
 
252
271
  ```bash
253
- pnpm test
272
+ pnpm test:run
254
273
  ```
255
274
 
256
- All 83 tests passing ✅
257
-
258
275
  ## 📄 License
259
276
 
260
- [Your License]
277
+ mathscan
261
278
 
262
279
  ## 👥 Authors
263
280
 
264
- [Your Name]
281
+ mathscan
265
282
 
266
283
  ---
267
-
268
- **Status**: Production ready. All tests passing.
package/dist/index.d.ts CHANGED
@@ -137,11 +137,6 @@ export declare function createExpressionValidation(expression: string): Expressi
137
137
  */
138
138
  export declare function createFreeText(value: string): FreeTextInline;
139
139
 
140
- /**
141
- * @deprecated Use createStringInput or createNumericInput instead
142
- */
143
- export declare function createInlineInput(name: string, inputType: "string" | "numeric", lengthOrDigits: number, answer?: string | number): InlineInputWidget;
144
-
145
140
  /**
146
141
  * Create a line block
147
142
  */
@@ -1025,13 +1020,6 @@ export declare function isNumberText(item: LayoutItem): item is Extract<LayoutIt
1025
1020
  type: 'number-text';
1026
1021
  }>;
1027
1022
 
1028
- /**
1029
- * @deprecated Use isInlineInputJSON instead
1030
- */
1031
- export declare function isNumericInputJSON(widget: WidgetJSON): widget is Extract<WidgetJSON, {
1032
- type: 'inline-input';
1033
- }>;
1034
-
1035
1023
  export declare function isNumericKeypad(value: unknown): value is NumericKeypadTool;
1036
1024
 
1037
1025
  /**
@@ -1080,13 +1068,6 @@ export declare function isValidationNode(value: unknown): value is ValidationNod
1080
1068
  */
1081
1069
  export declare function isValidExpression(expression: string): boolean;
1082
1070
 
1083
- /**
1084
- * @deprecated Use isWidgetRef instead
1085
- */
1086
- export declare function isWidgetPlaceholder(item: LayoutItem): item is Extract<LayoutItem, {
1087
- type: 'widget-ref';
1088
- }>;
1089
-
1090
1071
  /**
1091
1072
  * Type guard for WidgetRef
1092
1073
  */
@@ -1975,9 +1956,7 @@ export declare const WidgetJSONSchema: z.ZodUnion<[z.ZodDiscriminatedUnion<"inpu
1975
1956
  includeDecimal: boolean;
1976
1957
  }>]>;
1977
1958
 
1978
- declare type WidgetRef = z.infer<typeof WidgetRefSchema>;
1979
- export { WidgetRef as WidgetPlaceholder }
1980
- export { WidgetRef }
1959
+ export declare type WidgetRef = z.infer<typeof WidgetRefSchema>;
1981
1960
 
1982
1961
  export declare const WidgetRefSchema: z.ZodObject<{
1983
1962
  id: z.ZodString;
package/dist/index.js CHANGED
@@ -31,14 +31,18 @@ const QUESTION_TEXT = chevrotain.createToken({
31
31
  name: "QUESTION_TEXT",
32
32
  pattern: /QUESTION_TEXT/
33
33
  });
34
- const NUMBER_TEXT = chevrotain.createToken({
35
- name: "NUMBER_TEXT",
36
- pattern: /NUMBER_TEXT/
34
+ const TEXT_HIGHLIGHTER = chevrotain.createToken({
35
+ name: "TEXT_HIGHLIGHTER",
36
+ pattern: /TEXT_HIGHLIGHTER/
37
37
  });
38
38
  const W_S_I = chevrotain.createToken({
39
39
  name: "W_S_I",
40
40
  pattern: /W_S_I/
41
41
  });
42
+ const STRING_INPUT = chevrotain.createToken({
43
+ name: "STRING_INPUT",
44
+ pattern: /STRING_INPUT/
45
+ });
42
46
  const DRAGABLE_BUTTONS_OPTIONS = chevrotain.createToken({
43
47
  name: "DRAGABLE_BUTTONS_OPTIONS",
44
48
  pattern: /DRAGABLE_BUTTONS_OPTIONS/
@@ -55,6 +59,10 @@ const W_N_I = chevrotain.createToken({
55
59
  name: "W_N_I",
56
60
  pattern: /W_N_I/
57
61
  });
62
+ const NUMERIC_INPUT = chevrotain.createToken({
63
+ name: "NUMERIC_INPUT",
64
+ pattern: /NUMERIC_INPUT/
65
+ });
58
66
  const NumberLiteral = chevrotain.createToken({
59
67
  name: "NumberLiteral",
60
68
  pattern: /-?\d+(\.\d+)?/
@@ -137,13 +145,14 @@ const allTokens = [
137
145
  Comment,
138
146
  // Component keywords (before Identifier!)
139
147
  QUESTION_TEXT,
140
- NUMBER_TEXT,
148
+ TEXT_HIGHLIGHTER,
141
149
  W_S_I,
150
+ STRING_INPUT,
142
151
  DRAGABLE_BUTTONS_OPTIONS,
143
152
  NUMERIC_KEYPAD,
144
153
  WIDGET_ANSWER,
145
- // Legacy keywords
146
154
  W_N_I,
155
+ NUMERIC_INPUT,
147
156
  // Literals
148
157
  NumberLiteral,
149
158
  StringLiteral,
@@ -321,14 +330,14 @@ class MathExerciseParser extends chevrotain.CstParser {
321
330
  // Parentheses (for grouping)
322
331
  { ALT: () => this.CONSUME(LParen) },
323
332
  { ALT: () => this.CONSUME(RParen) },
324
- // NUMBER_TEXT (for highlighting)
325
- { ALT: () => this.SUBRULE(this.numberText) },
333
+ // TEXT_HIGHLIGHTER (for highlighting)
334
+ { ALT: () => this.SUBRULE(this.textHighlighter) },
326
335
  // W_S_I (inline input widget)
327
336
  { ALT: () => this.SUBRULE(this.inlineInput) }
328
337
  ]);
329
338
  });
330
- this.numberText = this.RULE("numberText", () => {
331
- this.CONSUME(NUMBER_TEXT);
339
+ this.textHighlighter = this.RULE("textHighlighter", () => {
340
+ this.CONSUME(TEXT_HIGHLIGHTER);
332
341
  this.CONSUME(LBracket);
333
342
  this.SUBRULE(this.parameterList);
334
343
  this.CONSUME(RBracket);
@@ -336,7 +345,9 @@ class MathExerciseParser extends chevrotain.CstParser {
336
345
  this.inlineInput = this.RULE("inlineInput", () => {
337
346
  this.OR([
338
347
  { ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
339
- { ALT: () => this.CONSUME(W_N_I, { LABEL: "numericInput" }) }
348
+ { ALT: () => this.CONSUME(STRING_INPUT, { LABEL: "stringInput" }) },
349
+ { ALT: () => this.CONSUME(W_N_I, { LABEL: "numericInput" }) },
350
+ { ALT: () => this.CONSUME(NUMERIC_INPUT, { LABEL: "numericInput" }) }
340
351
  ]);
341
352
  this.CONSUME(LBracket);
342
353
  this.SUBRULE(this.parameterList);
@@ -529,13 +540,6 @@ function createNumericInput(name, digits, answer) {
529
540
  answer
530
541
  });
531
542
  }
532
- function createInlineInput(name, inputType, lengthOrDigits, answer) {
533
- if (inputType === "string") {
534
- return createStringInput(name, lengthOrDigits, answer);
535
- } else {
536
- return createNumericInput(name, lengthOrDigits, answer);
537
- }
538
- }
539
543
  function isFreeText(value) {
540
544
  return FreeTextInlineSchema.safeParse(value).success;
541
545
  }
@@ -754,18 +758,21 @@ class NaturalDSLVisitor extends BaseCstVisitor {
754
758
  const token = ctx.RParen[0];
755
759
  return createFreeText(token.image);
756
760
  }
757
- if (ctx.numberText) {
758
- return this.visit(ctx.numberText);
761
+ if (ctx.textHighlighter) {
762
+ return this.visit(ctx.textHighlighter);
759
763
  }
760
764
  if (ctx.inlineInput) {
761
765
  return this.visit(ctx.inlineInput);
762
766
  }
763
767
  throw new Error("Unknown inline element");
764
768
  }
765
- numberText(ctx) {
769
+ textHighlighter(ctx) {
766
770
  const params = this.visit(ctx.parameterList);
767
771
  const value = params.value;
768
- const highlight = params.highlight;
772
+ let highlight = void 0;
773
+ if (params.start !== void 0 && params.end !== void 0) {
774
+ highlight = [params.start, params.end];
775
+ }
769
776
  return createNumberText(value, highlight);
770
777
  }
771
778
  inlineInput(ctx) {
@@ -1024,12 +1031,6 @@ function isDragOptionsJSON(widget) {
1024
1031
  function isNumericKeypadJSON(widget) {
1025
1032
  return widget.type === "numeric-keypad";
1026
1033
  }
1027
- function isWidgetPlaceholder(item) {
1028
- return item.type === "widget-ref";
1029
- }
1030
- function isNumericInputJSON(widget) {
1031
- return widget.type === "inline-input";
1032
- }
1033
1034
  class LayoutIdGenerator {
1034
1035
  constructor() {
1035
1036
  this.counter = 0;
@@ -5745,7 +5746,7 @@ const LayoutRenderer = ({
5745
5746
  const [start, end] = item.highlight;
5746
5747
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "number-text", children: [
5747
5748
  item.value.slice(0, start),
5748
- /* @__PURE__ */ jsxRuntime.jsx("mark", { className: "highlight", children: item.value.slice(start, end) }),
5749
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "highlight", children: item.value.slice(start, end) }),
5749
5750
  item.value.slice(end)
5750
5751
  ] });
5751
5752
  }
@@ -6025,7 +6026,6 @@ exports.createDragOptions = createDragOptions;
6025
6026
  exports.createExerciseDocument = createExerciseDocument;
6026
6027
  exports.createExpressionValidation = createExpressionValidation;
6027
6028
  exports.createFreeText = createFreeText;
6028
- exports.createInlineInput = createInlineInput;
6029
6029
  exports.createLineBlock = createLineBlock;
6030
6030
  exports.createNumberText = createNumberText;
6031
6031
  exports.createNumericInput = createNumericInput;
@@ -6050,7 +6050,6 @@ exports.isInlineNode = isInlineNode;
6050
6050
  exports.isLineBlock = isLineBlock;
6051
6051
  exports.isLinebreak = isLinebreak;
6052
6052
  exports.isNumberText = isNumberText;
6053
- exports.isNumericInputJSON = isNumericInputJSON;
6054
6053
  exports.isNumericKeypad = isNumericKeypad;
6055
6054
  exports.isNumericKeypadJSON = isNumericKeypadJSON;
6056
6055
  exports.isOperatorItem = isOperatorItem;
@@ -6062,7 +6061,6 @@ exports.isTextItem = isTextItem;
6062
6061
  exports.isToolNode = isToolNode;
6063
6062
  exports.isValidExpression = isValidExpression;
6064
6063
  exports.isValidationNode = isValidationNode;
6065
- exports.isWidgetPlaceholder = isWidgetPlaceholder;
6066
6064
  exports.isWidgetRef = isWidgetRef;
6067
6065
  exports.parse = parse;
6068
6066
  exports.parseSafe = parseSafe;