@mathscan/math-exercise-engine 1.0.11 → 1.0.13
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/index.d.ts +2343 -25
- package/dist/index.js +981 -280
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +982 -281
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +396 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ const AST_TYPES = {
|
|
|
23
23
|
BOLD_TEXT: "BoldText",
|
|
24
24
|
/** Inline input widget (string or numeric) */
|
|
25
25
|
INLINE_INPUT: "InlineInput",
|
|
26
|
+
/** Inline fraction widget (numerator/denominator with optional inputs) */
|
|
27
|
+
FRACTION: "Fraction",
|
|
26
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
29
|
// Blocks
|
|
28
30
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -40,6 +42,10 @@ const AST_TYPES = {
|
|
|
40
42
|
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
41
43
|
/** A position marker on the number line */
|
|
42
44
|
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
45
|
+
/** A dynamic subdivision counter embedded in a number line */
|
|
46
|
+
NUMBER_LINE_COUNTER: "NumberLineCounter",
|
|
47
|
+
/** A highlighted range on the number line axis */
|
|
48
|
+
NUMBER_LINE_HIGHLIGHT: "NumberLineHighlight",
|
|
43
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
50
|
// Tools
|
|
45
51
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -96,6 +102,10 @@ const JSON_TYPES = {
|
|
|
96
102
|
NUMBER_LINE: "number-line",
|
|
97
103
|
/** Position marker within a number line */
|
|
98
104
|
NUMBER_LINE_POSITION: "number-line-position",
|
|
105
|
+
/** Dynamic subdivision counter embedded in a number line */
|
|
106
|
+
NUMBER_LINE_COUNTER: "number-line-counter",
|
|
107
|
+
/** Highlighted range within a number line */
|
|
108
|
+
NUMBER_LINE_HIGHLIGHT: "number-line-highlight",
|
|
99
109
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
110
|
// Widget Types
|
|
101
111
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -106,7 +116,9 @@ const JSON_TYPES = {
|
|
|
106
116
|
/** Radio button options widget */
|
|
107
117
|
RADIO_OPTIONS: "radio-options",
|
|
108
118
|
/** Numeric keypad widget */
|
|
109
|
-
NUMERIC_KEYPAD: "numeric-keypad"
|
|
119
|
+
NUMERIC_KEYPAD: "numeric-keypad",
|
|
120
|
+
/** Inline fraction widget */
|
|
121
|
+
FRACTION: "fraction"
|
|
110
122
|
};
|
|
111
123
|
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
112
124
|
const VALIDATION_MODES = {
|
|
@@ -122,8 +134,23 @@ const INPUT_TYPES = {
|
|
|
122
134
|
NUMERIC: "numeric"
|
|
123
135
|
};
|
|
124
136
|
const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
|
|
137
|
+
const INTERNAL_PRECISION = 9;
|
|
138
|
+
const DISPLAY_PRECISION = 2;
|
|
125
139
|
const SIDE_PAD = 7;
|
|
126
140
|
const CSS_VAR_SIDE_PAD = "--nl-side-pad";
|
|
141
|
+
const NUMBER_LINE_LABELS = ["all", "extremes", "min-only", "max-only", "none"];
|
|
142
|
+
const NUMBER_LINE_HIGHLIGHT_COLORS = ["blue", "red", "green", "orange", "purple", "yellow"];
|
|
143
|
+
function getFractionKey(widgetId, part) {
|
|
144
|
+
return `${widgetId}__${part}`;
|
|
145
|
+
}
|
|
146
|
+
function isFractionKey(key2) {
|
|
147
|
+
return key2.endsWith("__num") || key2.endsWith("__den");
|
|
148
|
+
}
|
|
149
|
+
function parseFractionKey(key2) {
|
|
150
|
+
const match = key2.match(/^(.+)__(num|den)$/);
|
|
151
|
+
if (!match) return null;
|
|
152
|
+
return { widgetId: match[1], part: match[2] };
|
|
153
|
+
}
|
|
127
154
|
const Newline = chevrotain.createToken({
|
|
128
155
|
name: "Newline",
|
|
129
156
|
pattern: /\r?\n/,
|
|
@@ -217,6 +244,18 @@ const CELL = chevrotain.createToken({
|
|
|
217
244
|
name: "CELL",
|
|
218
245
|
pattern: /CELL/
|
|
219
246
|
});
|
|
247
|
+
const COUNTER = chevrotain.createToken({
|
|
248
|
+
name: "COUNTER",
|
|
249
|
+
pattern: /COUNTER/
|
|
250
|
+
});
|
|
251
|
+
const HIGHLIGHT = chevrotain.createToken({
|
|
252
|
+
name: "HIGHLIGHT",
|
|
253
|
+
pattern: /HIGHLIGHT(?!_TEXT)/
|
|
254
|
+
});
|
|
255
|
+
const FRACTION = chevrotain.createToken({
|
|
256
|
+
name: "FRACTION",
|
|
257
|
+
pattern: /FRACTION/
|
|
258
|
+
});
|
|
220
259
|
const NumberLiteral = chevrotain.createToken({
|
|
221
260
|
name: "NumberLiteral",
|
|
222
261
|
pattern: /-?\d+(\.\d+)?/
|
|
@@ -324,6 +363,9 @@ const allTokens = [
|
|
|
324
363
|
TABLE,
|
|
325
364
|
ROW,
|
|
326
365
|
CELL,
|
|
366
|
+
COUNTER,
|
|
367
|
+
HIGHLIGHT,
|
|
368
|
+
FRACTION,
|
|
327
369
|
// Literals
|
|
328
370
|
NumberLiteral,
|
|
329
371
|
StringLiteral,
|
|
@@ -431,10 +473,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
431
473
|
});
|
|
432
474
|
this.exerciseDocument = this.RULE("exerciseDocument", () => {
|
|
433
475
|
this.MANY(() => {
|
|
434
|
-
this.OR([
|
|
435
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
436
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
437
|
-
]);
|
|
476
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
438
477
|
});
|
|
439
478
|
this.OPTION(() => {
|
|
440
479
|
this.SUBRULE(this.question);
|
|
@@ -450,10 +489,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
450
489
|
this.SUBRULE(this.validation);
|
|
451
490
|
});
|
|
452
491
|
this.MANY4(() => {
|
|
453
|
-
this.OR2([
|
|
454
|
-
{ ALT: () => this.CONSUME2(Newline) },
|
|
455
|
-
{ ALT: () => this.CONSUME2(Space) }
|
|
456
|
-
]);
|
|
492
|
+
this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
|
|
457
493
|
});
|
|
458
494
|
});
|
|
459
495
|
this.question = this.RULE("question", () => {
|
|
@@ -462,10 +498,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
462
498
|
this.CONSUME(StringLiteral, { LABEL: "value" });
|
|
463
499
|
this.CONSUME(RBracket);
|
|
464
500
|
this.MANY(() => {
|
|
465
|
-
this.OR([
|
|
466
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
467
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
468
|
-
]);
|
|
501
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
469
502
|
});
|
|
470
503
|
});
|
|
471
504
|
this.contentBlock = this.RULE("contentBlock", () => {
|
|
@@ -487,77 +520,50 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
487
520
|
});
|
|
488
521
|
this.table = this.RULE("table", () => {
|
|
489
522
|
this.MANY(() => {
|
|
490
|
-
this.OR2([
|
|
491
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
492
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
493
|
-
]);
|
|
523
|
+
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
494
524
|
});
|
|
495
525
|
this.CONSUME(TABLE);
|
|
496
526
|
this.CONSUME(LBracket);
|
|
497
527
|
this.SUBRULE(this.parameterList);
|
|
498
528
|
this.CONSUME(RBracket);
|
|
499
529
|
this.MANY2(() => {
|
|
500
|
-
this.OR3([
|
|
501
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
502
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
503
|
-
]);
|
|
530
|
+
this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
504
531
|
});
|
|
505
532
|
this.CONSUME(LBrace);
|
|
506
533
|
this.MANY3(() => {
|
|
507
|
-
this.OR4([
|
|
508
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
509
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
510
|
-
]);
|
|
534
|
+
this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
511
535
|
});
|
|
512
536
|
this.AT_LEAST_ONE(() => {
|
|
513
537
|
this.SUBRULE(this.row);
|
|
514
538
|
});
|
|
515
539
|
this.CONSUME(RBrace);
|
|
516
540
|
this.MANY4(() => {
|
|
517
|
-
this.OR5([
|
|
518
|
-
{ ALT: () => this.CONSUME4(Newline) },
|
|
519
|
-
{ ALT: () => this.CONSUME4(Space) }
|
|
520
|
-
]);
|
|
541
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
521
542
|
});
|
|
522
543
|
});
|
|
523
544
|
this.row = this.RULE("row", () => {
|
|
524
545
|
this.MANY(() => {
|
|
525
|
-
this.OR([
|
|
526
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
527
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
528
|
-
]);
|
|
546
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
529
547
|
});
|
|
530
548
|
this.CONSUME(ROW);
|
|
531
549
|
this.MANY2(() => {
|
|
532
|
-
this.OR2([
|
|
533
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
534
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
535
|
-
]);
|
|
550
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
536
551
|
});
|
|
537
552
|
this.CONSUME(LBrace);
|
|
538
553
|
this.MANY3(() => {
|
|
539
|
-
this.OR3([
|
|
540
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
541
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
542
|
-
]);
|
|
554
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
543
555
|
});
|
|
544
556
|
this.AT_LEAST_ONE(() => {
|
|
545
557
|
this.SUBRULE(this.cell);
|
|
546
558
|
});
|
|
547
559
|
this.CONSUME(RBrace);
|
|
548
560
|
this.MANY4(() => {
|
|
549
|
-
this.OR4([
|
|
550
|
-
{ ALT: () => this.CONSUME4(Space) },
|
|
551
|
-
{ ALT: () => this.CONSUME4(Newline) }
|
|
552
|
-
]);
|
|
561
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
553
562
|
});
|
|
554
563
|
});
|
|
555
564
|
this.cell = this.RULE("cell", () => {
|
|
556
565
|
this.MANY(() => {
|
|
557
|
-
this.OR([
|
|
558
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
559
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
560
|
-
]);
|
|
566
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
561
567
|
});
|
|
562
568
|
this.CONSUME(CELL);
|
|
563
569
|
this.OPTION(() => {
|
|
@@ -566,10 +572,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
566
572
|
this.CONSUME(RBracket);
|
|
567
573
|
});
|
|
568
574
|
this.MANY2(() => {
|
|
569
|
-
this.OR2([
|
|
570
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
571
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
572
|
-
]);
|
|
575
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
573
576
|
});
|
|
574
577
|
this.CONSUME(LBrace);
|
|
575
578
|
this.MANY3(() => {
|
|
@@ -580,70 +583,47 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
580
583
|
});
|
|
581
584
|
this.CONSUME(RBrace);
|
|
582
585
|
this.MANY4(() => {
|
|
583
|
-
this.OR4([
|
|
584
|
-
{ ALT: () => this.CONSUME4(Space) },
|
|
585
|
-
{ ALT: () => this.CONSUME4(Newline) }
|
|
586
|
-
]);
|
|
586
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
587
587
|
});
|
|
588
588
|
});
|
|
589
589
|
this.numberLine = this.RULE("numberLine", () => {
|
|
590
590
|
this.MANY(() => {
|
|
591
|
-
this.OR2([
|
|
592
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
593
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
594
|
-
]);
|
|
591
|
+
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
595
592
|
});
|
|
596
593
|
this.CONSUME(NUMBER_LINE);
|
|
597
594
|
this.CONSUME(LBracket);
|
|
598
595
|
this.SUBRULE(this.parameterList);
|
|
599
596
|
this.CONSUME(RBracket);
|
|
600
597
|
this.MANY2(() => {
|
|
601
|
-
this.OR3([
|
|
602
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
603
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
604
|
-
]);
|
|
598
|
+
this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
605
599
|
});
|
|
606
600
|
this.CONSUME(LBrace);
|
|
607
601
|
this.MANY3(() => {
|
|
608
|
-
this.OR4([
|
|
609
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
610
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
611
|
-
]);
|
|
602
|
+
this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
612
603
|
});
|
|
613
604
|
this.MANY4(() => {
|
|
614
|
-
this.
|
|
605
|
+
this.OR6([
|
|
606
|
+
{ ALT: () => this.SUBRULE(this.position) },
|
|
607
|
+
{ ALT: () => this.SUBRULE(this.counter) },
|
|
608
|
+
{ ALT: () => this.SUBRULE(this.highlight) }
|
|
609
|
+
]);
|
|
615
610
|
});
|
|
616
611
|
this.CONSUME(RBrace);
|
|
617
612
|
this.MANY5(() => {
|
|
618
|
-
this.OR5([
|
|
619
|
-
{ ALT: () => this.CONSUME4(Newline) },
|
|
620
|
-
{ ALT: () => this.CONSUME4(Space) }
|
|
621
|
-
]);
|
|
613
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
622
614
|
});
|
|
623
615
|
});
|
|
624
616
|
this.position = this.RULE("position", () => {
|
|
625
|
-
this.MANY(() => {
|
|
626
|
-
this.OR([
|
|
627
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
628
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
629
|
-
]);
|
|
630
|
-
});
|
|
631
617
|
this.CONSUME(POSITION);
|
|
632
618
|
this.CONSUME(LBracket);
|
|
633
619
|
this.SUBRULE(this.parameterList);
|
|
634
620
|
this.CONSUME(RBracket);
|
|
635
621
|
this.MANY2(() => {
|
|
636
|
-
this.OR2([
|
|
637
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
638
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
639
|
-
]);
|
|
622
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
640
623
|
});
|
|
641
624
|
this.CONSUME(LBrace);
|
|
642
625
|
this.MANY3(() => {
|
|
643
|
-
this.OR3([
|
|
644
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
645
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
646
|
-
]);
|
|
626
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
647
627
|
});
|
|
648
628
|
this.MANY4(() => {
|
|
649
629
|
this.OR4([
|
|
@@ -653,10 +633,25 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
653
633
|
});
|
|
654
634
|
this.CONSUME(RBrace);
|
|
655
635
|
this.MANY5(() => {
|
|
656
|
-
this.OR5([
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
636
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
this.counter = this.RULE("counter", () => {
|
|
640
|
+
this.CONSUME(COUNTER);
|
|
641
|
+
this.CONSUME(LBracket);
|
|
642
|
+
this.SUBRULE(this.parameterList);
|
|
643
|
+
this.CONSUME(RBracket);
|
|
644
|
+
this.MANY2(() => {
|
|
645
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
this.highlight = this.RULE("highlight", () => {
|
|
649
|
+
this.CONSUME(HIGHLIGHT);
|
|
650
|
+
this.CONSUME(LBracket);
|
|
651
|
+
this.SUBRULE(this.parameterList);
|
|
652
|
+
this.CONSUME(RBracket);
|
|
653
|
+
this.MANY2(() => {
|
|
654
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
660
655
|
});
|
|
661
656
|
});
|
|
662
657
|
this.line = this.RULE("line", () => {
|
|
@@ -705,6 +700,8 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
705
700
|
{ ALT: () => this.SUBRULE(this.colorText) },
|
|
706
701
|
// BOLD_TEXT (for bold text)
|
|
707
702
|
{ ALT: () => this.SUBRULE(this.boldText) },
|
|
703
|
+
// FRACTION (inline fraction widget)
|
|
704
|
+
{ ALT: () => this.SUBRULE(this.fraction) },
|
|
708
705
|
// W_S_I (inline input widget)
|
|
709
706
|
{ ALT: () => this.SUBRULE(this.inlineInput) }
|
|
710
707
|
]);
|
|
@@ -730,6 +727,12 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
730
727
|
this.SUBRULE(this.parameterList);
|
|
731
728
|
this.CONSUME(RBracket);
|
|
732
729
|
});
|
|
730
|
+
this.fraction = this.RULE("fraction", () => {
|
|
731
|
+
this.CONSUME(FRACTION);
|
|
732
|
+
this.CONSUME(LBracket);
|
|
733
|
+
this.SUBRULE(this.parameterList);
|
|
734
|
+
this.CONSUME(RBracket);
|
|
735
|
+
});
|
|
733
736
|
this.inlineInput = this.RULE("inlineInput", () => {
|
|
734
737
|
this.OR([
|
|
735
738
|
{ ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
|
|
@@ -753,28 +756,19 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
753
756
|
this.SUBRULE(this.stringList);
|
|
754
757
|
this.CONSUME(RBracket);
|
|
755
758
|
this.MANY(() => {
|
|
756
|
-
this.OR([
|
|
757
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
758
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
759
|
-
]);
|
|
759
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
760
760
|
});
|
|
761
761
|
});
|
|
762
762
|
this.radioButtons = this.RULE("radioButtons", () => {
|
|
763
763
|
this.MANY(() => {
|
|
764
|
-
this.OR([
|
|
765
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
766
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
767
|
-
]);
|
|
764
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
768
765
|
});
|
|
769
766
|
this.CONSUME(RADIO_BUTTONS_OPTIONS);
|
|
770
767
|
this.CONSUME(LBracket);
|
|
771
768
|
this.SUBRULE(this.parameterList);
|
|
772
769
|
this.CONSUME(RBracket);
|
|
773
770
|
this.MANY2(() => {
|
|
774
|
-
this.OR2([
|
|
775
|
-
{ ALT: () => this.CONSUME2(Newline) },
|
|
776
|
-
{ ALT: () => this.CONSUME2(Space) }
|
|
777
|
-
]);
|
|
771
|
+
this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
|
|
778
772
|
});
|
|
779
773
|
});
|
|
780
774
|
this.numericKeypad = this.RULE("numericKeypad", () => {
|
|
@@ -785,10 +779,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
785
779
|
});
|
|
786
780
|
this.CONSUME(RBracket);
|
|
787
781
|
this.MANY(() => {
|
|
788
|
-
this.OR([
|
|
789
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
790
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
791
|
-
]);
|
|
782
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
792
783
|
});
|
|
793
784
|
});
|
|
794
785
|
this.validation = this.RULE("validation", () => {
|
|
@@ -924,13 +915,22 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
924
915
|
answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
|
|
925
916
|
})
|
|
926
917
|
]);
|
|
918
|
+
const FractionInlineSchema = zod.z.object({
|
|
919
|
+
type: zod.z.literal(AST_TYPES.FRACTION),
|
|
920
|
+
name: zod.z.string(),
|
|
921
|
+
numerator: zod.z.number().optional(),
|
|
922
|
+
denominator: zod.z.number().optional(),
|
|
923
|
+
answer: zod.z.string().optional(),
|
|
924
|
+
simplified: zod.z.boolean().default(true)
|
|
925
|
+
});
|
|
927
926
|
const InlineNodeSchema = zod.z.union([
|
|
928
927
|
FreeTextInlineSchema,
|
|
929
928
|
SpaceInlineSchema,
|
|
930
929
|
HighlightedTextInlineSchema,
|
|
931
930
|
ColorTextInlineSchema,
|
|
932
931
|
BoldTextInlineSchema,
|
|
933
|
-
InlineInputWidgetSchema
|
|
932
|
+
InlineInputWidgetSchema,
|
|
933
|
+
FractionInlineSchema
|
|
934
934
|
]);
|
|
935
935
|
function createFreeText(value) {
|
|
936
936
|
return FreeTextInlineSchema.parse({
|
|
@@ -983,6 +983,13 @@ function createNumericInput(name, digits, answer) {
|
|
|
983
983
|
answer
|
|
984
984
|
});
|
|
985
985
|
}
|
|
986
|
+
function createFraction(name, options = {}) {
|
|
987
|
+
return FractionInlineSchema.parse({
|
|
988
|
+
type: AST_TYPES.FRACTION,
|
|
989
|
+
name,
|
|
990
|
+
...options
|
|
991
|
+
});
|
|
992
|
+
}
|
|
986
993
|
function isFreeText(value) {
|
|
987
994
|
return FreeTextInlineSchema.safeParse(value).success;
|
|
988
995
|
}
|
|
@@ -992,6 +999,9 @@ function isSpace(value) {
|
|
|
992
999
|
function isInlineInput(value) {
|
|
993
1000
|
return InlineInputWidgetSchema.safeParse(value).success;
|
|
994
1001
|
}
|
|
1002
|
+
function isFraction(value) {
|
|
1003
|
+
return FractionInlineSchema.safeParse(value).success;
|
|
1004
|
+
}
|
|
995
1005
|
function isInlineNode(value) {
|
|
996
1006
|
return InlineNodeSchema.safeParse(value).success;
|
|
997
1007
|
}
|
|
@@ -1023,7 +1033,7 @@ function isLineBlock(value) {
|
|
|
1023
1033
|
}
|
|
1024
1034
|
const NumberLinePositionSchema = zod.z.object({
|
|
1025
1035
|
type: zod.z.literal(AST_TYPES.NUMBER_LINE_POSITION),
|
|
1026
|
-
value: zod.z.number(),
|
|
1036
|
+
value: zod.z.number().finite({ message: "POSITION: value must be a finite number" }),
|
|
1027
1037
|
inlines: zod.z.array(InlineNodeSchema).min(1)
|
|
1028
1038
|
});
|
|
1029
1039
|
function createNumberLinePosition(value, inlines) {
|
|
@@ -1043,10 +1053,50 @@ function createNumberLinePosition(value, inlines) {
|
|
|
1043
1053
|
inlines
|
|
1044
1054
|
});
|
|
1045
1055
|
}
|
|
1056
|
+
const NumberLineCounterSchema = zod.z.object({
|
|
1057
|
+
type: zod.z.literal(AST_TYPES.NUMBER_LINE_COUNTER),
|
|
1058
|
+
min: zod.z.number().int().min(1, { message: "COUNTER: min must be >= 1" }),
|
|
1059
|
+
max: zod.z.number().int().positive({ message: "COUNTER: max must be > 0" }),
|
|
1060
|
+
default: zod.z.number().int().nonnegative().optional(),
|
|
1061
|
+
name: zod.z.string().optional()
|
|
1062
|
+
}).refine((d) => d.min <= d.max, {
|
|
1063
|
+
message: "COUNTER: min must be <= max"
|
|
1064
|
+
});
|
|
1065
|
+
function createNumberLineCounter(max, min, defaultValue2, name) {
|
|
1066
|
+
return NumberLineCounterSchema.parse({
|
|
1067
|
+
type: AST_TYPES.NUMBER_LINE_COUNTER,
|
|
1068
|
+
min: min ?? 1,
|
|
1069
|
+
max,
|
|
1070
|
+
...defaultValue2 !== void 0 && { default: defaultValue2 },
|
|
1071
|
+
...name !== void 0 && { name }
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const NumberLineHighlightSchema = zod.z.object({
|
|
1075
|
+
type: zod.z.literal(AST_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
1076
|
+
from: zod.z.number().finite({ message: "HIGHLIGHT: from must be a finite number" }),
|
|
1077
|
+
to: zod.z.number().finite({ message: "HIGHLIGHT: to must be a finite number" }),
|
|
1078
|
+
color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS, {
|
|
1079
|
+
errorMap: () => ({
|
|
1080
|
+
message: `HIGHLIGHT: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")}`
|
|
1081
|
+
})
|
|
1082
|
+
}).optional(),
|
|
1083
|
+
label: zod.z.string().optional()
|
|
1084
|
+
}).refine((d) => d.from < d.to, {
|
|
1085
|
+
message: "HIGHLIGHT: from must be less than to"
|
|
1086
|
+
});
|
|
1087
|
+
function createNumberLineHighlight(from, to, color, label) {
|
|
1088
|
+
return NumberLineHighlightSchema.parse({
|
|
1089
|
+
type: AST_TYPES.NUMBER_LINE_HIGHLIGHT,
|
|
1090
|
+
from,
|
|
1091
|
+
to,
|
|
1092
|
+
...color !== void 0 && { color },
|
|
1093
|
+
...label !== void 0 && { label }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1046
1096
|
const NumberLineBlockSchema = zod.z.object({
|
|
1047
1097
|
type: zod.z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
|
|
1048
|
-
min: zod.z.number(),
|
|
1049
|
-
max: zod.z.number(),
|
|
1098
|
+
min: zod.z.number().finite({ message: "NUMBER_LINE: min must be a finite number" }),
|
|
1099
|
+
max: zod.z.number().finite({ message: "NUMBER_LINE: max must be a finite number" }),
|
|
1050
1100
|
step: zod.z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
|
|
1051
1101
|
minorTicks: zod.z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
|
|
1052
1102
|
style: zod.z.enum(NUMBER_LINE_STYLES, {
|
|
@@ -1054,6 +1104,13 @@ const NumberLineBlockSchema = zod.z.object({
|
|
|
1054
1104
|
message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
|
|
1055
1105
|
})
|
|
1056
1106
|
}).optional(),
|
|
1107
|
+
labelVisibility: zod.z.enum(NUMBER_LINE_LABELS, {
|
|
1108
|
+
errorMap: () => ({
|
|
1109
|
+
message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
|
|
1110
|
+
})
|
|
1111
|
+
}).optional(),
|
|
1112
|
+
counter: NumberLineCounterSchema.optional(),
|
|
1113
|
+
highlights: zod.z.array(NumberLineHighlightSchema).optional(),
|
|
1057
1114
|
positions: zod.z.array(NumberLinePositionSchema)
|
|
1058
1115
|
}).refine((data) => data.min < data.max, {
|
|
1059
1116
|
message: "NUMBER_LINE: min must be less than max"
|
|
@@ -1074,8 +1131,24 @@ const NumberLineBlockSchema = zod.z.object({
|
|
|
1074
1131
|
}
|
|
1075
1132
|
seen.add(pos.value);
|
|
1076
1133
|
}
|
|
1134
|
+
for (const h of data.highlights ?? []) {
|
|
1135
|
+
if (h.from < data.min || h.to > data.max) {
|
|
1136
|
+
ctx.addIssue({
|
|
1137
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1138
|
+
message: `NUMBER_LINE: HIGHLIGHT range [${h.from}, ${h.to}] must be within [${data.min}, ${data.max}]`
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (data.counter?.default !== void 0) {
|
|
1143
|
+
if (data.counter.default < data.counter.min || data.counter.default > data.counter.max) {
|
|
1144
|
+
ctx.addIssue({
|
|
1145
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1146
|
+
message: `NUMBER_LINE: COUNTER default (${data.counter.default}) must be within [${data.counter.min}, ${data.counter.max}]`
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1077
1150
|
});
|
|
1078
|
-
function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
1151
|
+
function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights) {
|
|
1079
1152
|
return NumberLineBlockSchema.parse({
|
|
1080
1153
|
type: AST_TYPES.NUMBER_LINE_BLOCK,
|
|
1081
1154
|
min,
|
|
@@ -1083,6 +1156,9 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
|
1083
1156
|
step,
|
|
1084
1157
|
minorTicks,
|
|
1085
1158
|
style,
|
|
1159
|
+
labelVisibility,
|
|
1160
|
+
counter,
|
|
1161
|
+
highlights,
|
|
1086
1162
|
positions
|
|
1087
1163
|
});
|
|
1088
1164
|
}
|
|
@@ -1293,8 +1369,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1293
1369
|
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
1294
1370
|
const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
|
|
1295
1371
|
const style = params.style;
|
|
1372
|
+
const labelVisibility = params.labelVisibility;
|
|
1296
1373
|
const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
|
|
1297
|
-
|
|
1374
|
+
const rawCounters = ctx.counter;
|
|
1375
|
+
if (rawCounters && rawCounters.length > 1) {
|
|
1376
|
+
throw new Error("NUMBER_LINE: only one COUNTER block is allowed");
|
|
1377
|
+
}
|
|
1378
|
+
const counter = rawCounters ? this.visit(rawCounters) : void 0;
|
|
1379
|
+
const highlights = ctx.highlight ? ctx.highlight.map((h) => this.visit(h)) : [];
|
|
1380
|
+
return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights);
|
|
1298
1381
|
}
|
|
1299
1382
|
position(ctx) {
|
|
1300
1383
|
const params = this.visit(ctx.parameterList);
|
|
@@ -1305,6 +1388,28 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1305
1388
|
const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
|
|
1306
1389
|
return createNumberLinePosition(value, inlines);
|
|
1307
1390
|
}
|
|
1391
|
+
counter(ctx) {
|
|
1392
|
+
const params = this.visit(ctx.parameterList);
|
|
1393
|
+
if (params.max === void 0) {
|
|
1394
|
+
throw new Error("COUNTER requires parameter: max");
|
|
1395
|
+
}
|
|
1396
|
+
const max = Number(params.max);
|
|
1397
|
+
const min = params.min !== void 0 ? Number(params.min) : void 0;
|
|
1398
|
+
const defaultValue2 = params.default !== void 0 ? Number(params.default) : void 0;
|
|
1399
|
+
const name = params.name;
|
|
1400
|
+
return createNumberLineCounter(max, min, defaultValue2, name);
|
|
1401
|
+
}
|
|
1402
|
+
highlight(ctx) {
|
|
1403
|
+
const params = this.visit(ctx.parameterList);
|
|
1404
|
+
if (params.from === void 0 || params.to === void 0) {
|
|
1405
|
+
throw new Error("HIGHLIGHT requires parameters: from and to");
|
|
1406
|
+
}
|
|
1407
|
+
const from = Number(params.from);
|
|
1408
|
+
const to = Number(params.to);
|
|
1409
|
+
const color = params.color;
|
|
1410
|
+
const label = params.label;
|
|
1411
|
+
return createNumberLineHighlight(from, to, color, label);
|
|
1412
|
+
}
|
|
1308
1413
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1309
1414
|
// Line Block
|
|
1310
1415
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1389,6 +1494,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1389
1494
|
if (ctx.boldText) {
|
|
1390
1495
|
return this.visit(ctx.boldText);
|
|
1391
1496
|
}
|
|
1497
|
+
if (ctx.fraction) {
|
|
1498
|
+
return this.visit(ctx.fraction);
|
|
1499
|
+
}
|
|
1392
1500
|
if (ctx.inlineInput) {
|
|
1393
1501
|
return this.visit(ctx.inlineInput);
|
|
1394
1502
|
}
|
|
@@ -1422,6 +1530,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1422
1530
|
}
|
|
1423
1531
|
return createBoldText(value, highlight);
|
|
1424
1532
|
}
|
|
1533
|
+
fraction(ctx) {
|
|
1534
|
+
const params = this.visit(ctx.parameterList);
|
|
1535
|
+
const name = params.name;
|
|
1536
|
+
const numerator = params.numerator;
|
|
1537
|
+
const denominator = params.denominator;
|
|
1538
|
+
const answer = params.answer;
|
|
1539
|
+
const simplified = params.simplified !== void 0 ? params.simplified : void 0;
|
|
1540
|
+
return createFraction(name, { numerator, denominator, answer, simplified });
|
|
1541
|
+
}
|
|
1425
1542
|
inlineInput(ctx) {
|
|
1426
1543
|
const params = this.visit(ctx.parameterList);
|
|
1427
1544
|
const name = params.name;
|
|
@@ -1635,17 +1752,36 @@ const TableItemSchema = zod.z.object({
|
|
|
1635
1752
|
const NumberLinePositionItemSchema = zod.z.object({
|
|
1636
1753
|
id: zod.z.string(),
|
|
1637
1754
|
type: zod.z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1638
|
-
value: zod.z.number(),
|
|
1639
|
-
content: zod.z.array(
|
|
1755
|
+
value: zod.z.number().finite(),
|
|
1756
|
+
content: zod.z.array(LayoutItemSchema)
|
|
1757
|
+
});
|
|
1758
|
+
const NumberLineCounterItemSchema = zod.z.object({
|
|
1759
|
+
id: zod.z.string(),
|
|
1760
|
+
type: zod.z.literal(JSON_TYPES.NUMBER_LINE_COUNTER),
|
|
1761
|
+
min: zod.z.number().int().min(1),
|
|
1762
|
+
max: zod.z.number().int().positive(),
|
|
1763
|
+
default: zod.z.number().int().nonnegative().optional(),
|
|
1764
|
+
name: zod.z.string().optional()
|
|
1765
|
+
});
|
|
1766
|
+
const NumberLineHighlightItemSchema = zod.z.object({
|
|
1767
|
+
id: zod.z.string(),
|
|
1768
|
+
type: zod.z.literal(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
1769
|
+
from: zod.z.number().finite(),
|
|
1770
|
+
to: zod.z.number().finite(),
|
|
1771
|
+
color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS).optional(),
|
|
1772
|
+
label: zod.z.string().optional()
|
|
1640
1773
|
});
|
|
1641
1774
|
const NumberLineItemSchema = zod.z.object({
|
|
1642
1775
|
id: zod.z.string(),
|
|
1643
1776
|
type: zod.z.literal(JSON_TYPES.NUMBER_LINE),
|
|
1644
|
-
min: zod.z.number(),
|
|
1645
|
-
max: zod.z.number(),
|
|
1777
|
+
min: zod.z.number().finite(),
|
|
1778
|
+
max: zod.z.number().finite(),
|
|
1646
1779
|
step: zod.z.number().positive(),
|
|
1647
1780
|
minorTicks: zod.z.number().int().nonnegative().optional(),
|
|
1648
1781
|
style: zod.z.enum(NUMBER_LINE_STYLES).optional(),
|
|
1782
|
+
labelVisibility: zod.z.enum(NUMBER_LINE_LABELS).optional(),
|
|
1783
|
+
counter: NumberLineCounterItemSchema.optional(),
|
|
1784
|
+
highlights: zod.z.array(NumberLineHighlightItemSchema).optional(),
|
|
1649
1785
|
positions: zod.z.array(NumberLinePositionItemSchema)
|
|
1650
1786
|
});
|
|
1651
1787
|
const StringInputJSONSchema = zod.z.object({
|
|
@@ -1675,11 +1811,18 @@ const NumericKeypadJSONSchema = zod.z.object({
|
|
|
1675
1811
|
includeOperators: zod.z.boolean().optional(),
|
|
1676
1812
|
includeDecimal: zod.z.boolean().optional()
|
|
1677
1813
|
});
|
|
1814
|
+
const FractionJSONSchema = zod.z.object({
|
|
1815
|
+
type: zod.z.literal(JSON_TYPES.FRACTION),
|
|
1816
|
+
numerator: zod.z.number().nullable(),
|
|
1817
|
+
denominator: zod.z.number().nullable(),
|
|
1818
|
+
simplified: zod.z.boolean()
|
|
1819
|
+
});
|
|
1678
1820
|
const WidgetJSONSchema = zod.z.union([
|
|
1679
1821
|
InlineInputJSONSchema,
|
|
1680
1822
|
DragOptionsJSONSchema,
|
|
1681
1823
|
RadioOptionsJSONSchema,
|
|
1682
|
-
NumericKeypadJSONSchema
|
|
1824
|
+
NumericKeypadJSONSchema,
|
|
1825
|
+
FractionJSONSchema
|
|
1683
1826
|
]);
|
|
1684
1827
|
const ValidationJSONSchema = zod.z.object({
|
|
1685
1828
|
mode: zod.z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
@@ -1719,7 +1862,7 @@ function isQuestionText(item) {
|
|
|
1719
1862
|
function isTextItem$1(item) {
|
|
1720
1863
|
return item.type === JSON_TYPES.TEXT;
|
|
1721
1864
|
}
|
|
1722
|
-
function isWidgetRef
|
|
1865
|
+
function isWidgetRef(item) {
|
|
1723
1866
|
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1724
1867
|
}
|
|
1725
1868
|
function isHighlightedText(item) {
|
|
@@ -1764,6 +1907,9 @@ function isRadioOptionsJSON(widget) {
|
|
|
1764
1907
|
function isNumericKeypadJSON(widget) {
|
|
1765
1908
|
return widget.type === JSON_TYPES.NUMERIC_KEYPAD;
|
|
1766
1909
|
}
|
|
1910
|
+
function isFractionJSON(widget) {
|
|
1911
|
+
return widget.type === JSON_TYPES.FRACTION;
|
|
1912
|
+
}
|
|
1767
1913
|
class LayoutIdGenerator {
|
|
1768
1914
|
constructor() {
|
|
1769
1915
|
this.counter = 0;
|
|
@@ -1828,6 +1974,17 @@ function toJSON(ast) {
|
|
|
1828
1974
|
answers[inline.name] = inline.answer;
|
|
1829
1975
|
}
|
|
1830
1976
|
}
|
|
1977
|
+
if (inline.type === AST_TYPES.FRACTION) {
|
|
1978
|
+
widgets[inline.name] = {
|
|
1979
|
+
type: JSON_TYPES.FRACTION,
|
|
1980
|
+
numerator: inline.numerator ?? null,
|
|
1981
|
+
denominator: inline.denominator ?? null,
|
|
1982
|
+
simplified: inline.simplified ?? true
|
|
1983
|
+
};
|
|
1984
|
+
if (inline.answer !== void 0) {
|
|
1985
|
+
answers[inline.name] = inline.answer;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1831
1988
|
}
|
|
1832
1989
|
for (const block of ast.lines) {
|
|
1833
1990
|
if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
@@ -1892,16 +2049,42 @@ function transformNumberLine(numberLine, idGenerator) {
|
|
|
1892
2049
|
content
|
|
1893
2050
|
};
|
|
1894
2051
|
});
|
|
2052
|
+
let counterItem;
|
|
2053
|
+
if (numberLine.counter !== void 0) {
|
|
2054
|
+
const c = numberLine.counter;
|
|
2055
|
+
counterItem = {
|
|
2056
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_COUNTER),
|
|
2057
|
+
type: JSON_TYPES.NUMBER_LINE_COUNTER,
|
|
2058
|
+
min: c.min,
|
|
2059
|
+
max: c.max,
|
|
2060
|
+
...c.default !== void 0 && { default: c.default },
|
|
2061
|
+
...c.name !== void 0 && { name: c.name }
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
let highlights;
|
|
2065
|
+
if (numberLine.highlights !== void 0 && numberLine.highlights.length > 0) {
|
|
2066
|
+
highlights = numberLine.highlights.map((h) => ({
|
|
2067
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
2068
|
+
type: JSON_TYPES.NUMBER_LINE_HIGHLIGHT,
|
|
2069
|
+
from: h.from,
|
|
2070
|
+
to: h.to,
|
|
2071
|
+
...h.color !== void 0 && { color: h.color },
|
|
2072
|
+
...h.label !== void 0 && { label: h.label }
|
|
2073
|
+
}));
|
|
2074
|
+
}
|
|
1895
2075
|
const item = {
|
|
1896
2076
|
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
|
|
1897
2077
|
type: JSON_TYPES.NUMBER_LINE,
|
|
1898
2078
|
min: numberLine.min,
|
|
1899
2079
|
max: numberLine.max,
|
|
1900
2080
|
step: numberLine.step,
|
|
2081
|
+
...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
|
|
2082
|
+
...numberLine.style !== void 0 && { style: numberLine.style },
|
|
2083
|
+
...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
|
|
2084
|
+
...counterItem !== void 0 && { counter: counterItem },
|
|
2085
|
+
...highlights !== void 0 && { highlights },
|
|
1901
2086
|
positions
|
|
1902
2087
|
};
|
|
1903
|
-
if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
|
|
1904
|
-
if (numberLine.style !== void 0) item.style = numberLine.style;
|
|
1905
2088
|
return item;
|
|
1906
2089
|
}
|
|
1907
2090
|
function transformTable(table, idGenerator) {
|
|
@@ -1992,6 +2175,12 @@ function transformInline(inline, idGenerator) {
|
|
|
1992
2175
|
type: JSON_TYPES.WIDGET_REF,
|
|
1993
2176
|
widgetId: inline.name
|
|
1994
2177
|
};
|
|
2178
|
+
case AST_TYPES.FRACTION:
|
|
2179
|
+
return {
|
|
2180
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
2181
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
2182
|
+
widgetId: inline.name
|
|
2183
|
+
};
|
|
1995
2184
|
default:
|
|
1996
2185
|
throw new Error(`Unknown inline type: ${inline.type}`);
|
|
1997
2186
|
}
|
|
@@ -2024,6 +2213,50 @@ function extractVariables$1(expression, validVariables) {
|
|
|
2024
2213
|
}
|
|
2025
2214
|
return uniqueVars;
|
|
2026
2215
|
}
|
|
2216
|
+
function normalize(value) {
|
|
2217
|
+
if (typeof value === "number") {
|
|
2218
|
+
return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
2219
|
+
}
|
|
2220
|
+
const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
2221
|
+
const asNum = Number(trimmed);
|
|
2222
|
+
if (trimmed !== "" && !isNaN(asNum)) {
|
|
2223
|
+
return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
2224
|
+
}
|
|
2225
|
+
return trimmed;
|
|
2226
|
+
}
|
|
2227
|
+
const NL_DRAG_PREFIX = "\0nl-drag\0";
|
|
2228
|
+
const isNLDragEncoded = (v) => v.startsWith(NL_DRAG_PREFIX);
|
|
2229
|
+
const encodeNLDragValue = (label, position) => `${NL_DRAG_PREFIX}${label}@${String(position)}`;
|
|
2230
|
+
function isNLDragCorrect(encoded, expectedLabel, expectedPosition) {
|
|
2231
|
+
const payload = encoded.slice(NL_DRAG_PREFIX.length);
|
|
2232
|
+
const atIdx = payload.lastIndexOf("@");
|
|
2233
|
+
const label = normalize(payload.slice(0, atIdx));
|
|
2234
|
+
const position = normalize(payload.slice(atIdx + 1));
|
|
2235
|
+
return label === normalize(expectedLabel) && position === normalize(expectedPosition);
|
|
2236
|
+
}
|
|
2237
|
+
const math$1 = mathjs.create(mathjs.all);
|
|
2238
|
+
function collectNumberLines(items) {
|
|
2239
|
+
const result = [];
|
|
2240
|
+
for (const item of items) {
|
|
2241
|
+
if (item.type === JSON_TYPES.NUMBER_LINE) result.push(item);
|
|
2242
|
+
else if (item.type === JSON_TYPES.GROUP) result.push(...collectNumberLines(item.items));
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
}
|
|
2246
|
+
function buildPositionMap(exercise) {
|
|
2247
|
+
const map = {};
|
|
2248
|
+
for (const nl of collectNumberLines(exercise.layout)) {
|
|
2249
|
+
for (const pos of nl.positions) {
|
|
2250
|
+
for (const c of pos.content) {
|
|
2251
|
+
if (isWidgetRef(c)) {
|
|
2252
|
+
map[c.widgetId] = pos.value;
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return map;
|
|
2259
|
+
}
|
|
2027
2260
|
function validateSimple(exercise, userInputs) {
|
|
2028
2261
|
const details = {};
|
|
2029
2262
|
let allCorrect = true;
|
|
@@ -2037,10 +2270,13 @@ function validateSimple(exercise, userInputs) {
|
|
|
2037
2270
|
score: 0
|
|
2038
2271
|
};
|
|
2039
2272
|
}
|
|
2273
|
+
const positionMap = buildPositionMap(exercise);
|
|
2040
2274
|
for (const [widgetId, expected] of Object.entries(answers)) {
|
|
2041
2275
|
totalCount++;
|
|
2042
2276
|
const userValue = userInputs[widgetId];
|
|
2043
|
-
const
|
|
2277
|
+
const widget = exercise.widgets[widgetId];
|
|
2278
|
+
const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
|
|
2279
|
+
const isCorrect = isFractionLenient ? areFractionsEquivalent(userValue, expected) : areValuesEqual(userValue, expected, positionMap[widgetId]);
|
|
2044
2280
|
details[widgetId] = {
|
|
2045
2281
|
widgetId,
|
|
2046
2282
|
userValue,
|
|
@@ -2068,19 +2304,23 @@ function validateSimple(exercise, userInputs) {
|
|
|
2068
2304
|
score: totalCount > 0 ? correctCount / totalCount : 0
|
|
2069
2305
|
};
|
|
2070
2306
|
}
|
|
2071
|
-
function areValuesEqual(userValue, expected) {
|
|
2072
|
-
if (userValue === void 0 || userValue === null)
|
|
2073
|
-
|
|
2307
|
+
function areValuesEqual(userValue, expected, expectedPosition) {
|
|
2308
|
+
if (userValue === void 0 || userValue === null) return false;
|
|
2309
|
+
if (typeof userValue === "string" && isNLDragEncoded(userValue)) {
|
|
2310
|
+
if (expectedPosition === void 0) return false;
|
|
2311
|
+
return isNLDragCorrect(userValue, expected, expectedPosition);
|
|
2074
2312
|
}
|
|
2075
|
-
|
|
2076
|
-
const normalizedExpected = normalize(expected);
|
|
2077
|
-
return normalizedUser === normalizedExpected;
|
|
2313
|
+
return normalize(userValue) === normalize(expected);
|
|
2078
2314
|
}
|
|
2079
|
-
function
|
|
2080
|
-
if (
|
|
2081
|
-
|
|
2315
|
+
function areFractionsEquivalent(user, expected) {
|
|
2316
|
+
if (user === void 0 || user === null) return false;
|
|
2317
|
+
try {
|
|
2318
|
+
const userFrac = math$1.fraction(String(user));
|
|
2319
|
+
const expectedFrac = math$1.fraction(String(expected));
|
|
2320
|
+
return math$1.equal(userFrac, expectedFrac);
|
|
2321
|
+
} catch {
|
|
2322
|
+
return areValuesEqual(user, expected);
|
|
2082
2323
|
}
|
|
2083
|
-
return String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
2084
2324
|
}
|
|
2085
2325
|
const math = mathjs.create(mathjs.all);
|
|
2086
2326
|
const BLOCKED_FUNCTIONS = [
|
|
@@ -2235,7 +2475,7 @@ function validateExpression(exercise, userInputs) {
|
|
|
2235
2475
|
hasInvalidInput = true;
|
|
2236
2476
|
continue;
|
|
2237
2477
|
}
|
|
2238
|
-
const numValue = typeof rawValue === "number" ? rawValue :
|
|
2478
|
+
const numValue = typeof rawValue === "number" ? rawValue : parseFractionOrNumber(String(rawValue).trim());
|
|
2239
2479
|
if (!Number.isFinite(numValue)) {
|
|
2240
2480
|
details[varName] = {
|
|
2241
2481
|
widgetId: varName,
|
|
@@ -2330,6 +2570,39 @@ function findValidCombination(expression, ranges) {
|
|
|
2330
2570
|
}
|
|
2331
2571
|
return null;
|
|
2332
2572
|
}
|
|
2573
|
+
function parseFractionOrNumber(s) {
|
|
2574
|
+
const parts = s.split("/");
|
|
2575
|
+
if (parts.length === 2) {
|
|
2576
|
+
const num = parseFloat(parts[0]);
|
|
2577
|
+
const den = parseFloat(parts[1]);
|
|
2578
|
+
if (Number.isFinite(num) && Number.isFinite(den) && den !== 0) {
|
|
2579
|
+
return num / den;
|
|
2580
|
+
}
|
|
2581
|
+
return NaN;
|
|
2582
|
+
}
|
|
2583
|
+
return parseFloat(s);
|
|
2584
|
+
}
|
|
2585
|
+
function preprocessFractionValues(exercise, userInputs) {
|
|
2586
|
+
const result = { ...userInputs };
|
|
2587
|
+
for (const [widgetId, widget] of Object.entries(exercise.widgets)) {
|
|
2588
|
+
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
2589
|
+
const numKey = getFractionKey(widgetId, "num");
|
|
2590
|
+
const denKey = getFractionKey(widgetId, "den");
|
|
2591
|
+
const numEffective = widget.numerator !== null ? String(widget.numerator) : result[numKey] !== void 0 ? String(result[numKey]) : "";
|
|
2592
|
+
const denEffective = widget.denominator !== null ? String(widget.denominator) : result[denKey] !== void 0 ? String(result[denKey]) : "";
|
|
2593
|
+
if (numEffective !== "" && denEffective !== "") {
|
|
2594
|
+
result[widgetId] = `${numEffective}/${denEffective}`;
|
|
2595
|
+
}
|
|
2596
|
+
delete result[numKey];
|
|
2597
|
+
delete result[denKey];
|
|
2598
|
+
}
|
|
2599
|
+
for (const key2 of Object.keys(result)) {
|
|
2600
|
+
if (isFractionKey(key2)) {
|
|
2601
|
+
delete result[key2];
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return result;
|
|
2605
|
+
}
|
|
2333
2606
|
function validate(exercise, userInputs) {
|
|
2334
2607
|
if (!exercise.validation) {
|
|
2335
2608
|
return {
|
|
@@ -2337,12 +2610,13 @@ function validate(exercise, userInputs) {
|
|
|
2337
2610
|
details: {}
|
|
2338
2611
|
};
|
|
2339
2612
|
}
|
|
2613
|
+
const processedInputs = preprocessFractionValues(exercise, userInputs);
|
|
2340
2614
|
const validatedExercise = exercise;
|
|
2341
2615
|
switch (validatedExercise.validation.mode) {
|
|
2342
2616
|
case VALIDATION_MODES.SIMPLE:
|
|
2343
|
-
return validateSimple(validatedExercise,
|
|
2617
|
+
return validateSimple(validatedExercise, processedInputs);
|
|
2344
2618
|
case VALIDATION_MODES.EXPRESSION:
|
|
2345
|
-
return validateExpression(validatedExercise,
|
|
2619
|
+
return validateExpression(validatedExercise, processedInputs);
|
|
2346
2620
|
default:
|
|
2347
2621
|
return {
|
|
2348
2622
|
isCorrect: false,
|
|
@@ -6412,6 +6686,7 @@ const InlineInput = ({
|
|
|
6412
6686
|
widgetId,
|
|
6413
6687
|
value,
|
|
6414
6688
|
isDropped = false,
|
|
6689
|
+
validationStatus = null,
|
|
6415
6690
|
inputType,
|
|
6416
6691
|
length,
|
|
6417
6692
|
digits,
|
|
@@ -6429,6 +6704,7 @@ const InlineInput = ({
|
|
|
6429
6704
|
}, [handleChange]);
|
|
6430
6705
|
const expectedLength = inputType === "numeric" ? digits : length;
|
|
6431
6706
|
const dynamicWidth = `${(expectedLength || 5) * 0.8 + 1}em`;
|
|
6707
|
+
const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
|
|
6432
6708
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6433
6709
|
Droppable,
|
|
6434
6710
|
{
|
|
@@ -6441,7 +6717,7 @@ const InlineInput = ({
|
|
|
6441
6717
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6442
6718
|
"button",
|
|
6443
6719
|
{
|
|
6444
|
-
className:
|
|
6720
|
+
className: `inline-input-chip${statusClass}`,
|
|
6445
6721
|
onClick: () => handleChange("", false),
|
|
6446
6722
|
title: "Click to remove",
|
|
6447
6723
|
children: value
|
|
@@ -6454,7 +6730,7 @@ const InlineInput = ({
|
|
|
6454
6730
|
{
|
|
6455
6731
|
id: widgetId + "-input",
|
|
6456
6732
|
type: "text",
|
|
6457
|
-
className:
|
|
6733
|
+
className: `inline-input${statusClass}`,
|
|
6458
6734
|
value,
|
|
6459
6735
|
onChange: (e) => handleChange(e.target.value, false),
|
|
6460
6736
|
onFocus: () => onFocus?.(widgetId),
|
|
@@ -6531,18 +6807,83 @@ const NumericKeypad = ({
|
|
|
6531
6807
|
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
6532
6808
|
] });
|
|
6533
6809
|
};
|
|
6810
|
+
const FractionInput = ({
|
|
6811
|
+
widgetId,
|
|
6812
|
+
numerator,
|
|
6813
|
+
denominator,
|
|
6814
|
+
values,
|
|
6815
|
+
droppedInputs,
|
|
6816
|
+
validationStatus = null,
|
|
6817
|
+
onChange,
|
|
6818
|
+
onFocus
|
|
6819
|
+
}) => {
|
|
6820
|
+
const numKey = getFractionKey(widgetId, "num");
|
|
6821
|
+
const denKey = getFractionKey(widgetId, "den");
|
|
6822
|
+
const numValue = values[numKey] ?? "";
|
|
6823
|
+
const denValue = values[denKey] ?? "";
|
|
6824
|
+
const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
|
|
6825
|
+
const handleDrop = React.useCallback((subKey) => (item) => {
|
|
6826
|
+
if (item.type === "drag-option") {
|
|
6827
|
+
const data = item.data;
|
|
6828
|
+
onChange(subKey, data.value, true, String(item.id));
|
|
6829
|
+
}
|
|
6830
|
+
}, [onChange]);
|
|
6831
|
+
const renderSlot = (subKey, fixedValue, currentValue, label) => {
|
|
6832
|
+
if (fixedValue !== null) {
|
|
6833
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-fixed", children: fixedValue });
|
|
6834
|
+
}
|
|
6835
|
+
const isDroppedHere = droppedInputs.has(subKey);
|
|
6836
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6837
|
+
Droppable,
|
|
6838
|
+
{
|
|
6839
|
+
id: subKey,
|
|
6840
|
+
accepts: ["drag-option"],
|
|
6841
|
+
onDrop: handleDrop(subKey),
|
|
6842
|
+
className: "fraction-slot-droppable",
|
|
6843
|
+
children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
6844
|
+
"button",
|
|
6845
|
+
{
|
|
6846
|
+
className: `inline-input-chip fraction-chip${statusClass}`,
|
|
6847
|
+
onClick: () => onChange(subKey, "", false),
|
|
6848
|
+
title: "Click to remove",
|
|
6849
|
+
children: currentValue
|
|
6850
|
+
}
|
|
6851
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
6852
|
+
"input",
|
|
6853
|
+
{
|
|
6854
|
+
id: `${subKey}-input`,
|
|
6855
|
+
type: "text",
|
|
6856
|
+
className: `inline-input fraction-input${statusClass}`,
|
|
6857
|
+
value: currentValue,
|
|
6858
|
+
onChange: (e) => onChange(subKey, e.target.value, false),
|
|
6859
|
+
onFocus: () => onFocus?.(subKey),
|
|
6860
|
+
maxLength: 4,
|
|
6861
|
+
"aria-label": label
|
|
6862
|
+
}
|
|
6863
|
+
)
|
|
6864
|
+
}
|
|
6865
|
+
);
|
|
6866
|
+
};
|
|
6867
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fraction", role: "math", "aria-label": `fraction ${widgetId}`, children: [
|
|
6868
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-numerator", children: renderSlot(numKey, numerator, numValue, "numerator") }),
|
|
6869
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-bar", "aria-hidden": "true" }),
|
|
6870
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator") })
|
|
6871
|
+
] });
|
|
6872
|
+
};
|
|
6534
6873
|
const RadioOptions = ({
|
|
6535
6874
|
name,
|
|
6536
6875
|
options,
|
|
6537
6876
|
value,
|
|
6877
|
+
validationStatus = null,
|
|
6538
6878
|
onChange
|
|
6539
6879
|
}) => {
|
|
6540
6880
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
6541
6881
|
const isSelected = value === option;
|
|
6882
|
+
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
6542
6883
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6543
6884
|
"button",
|
|
6544
6885
|
{
|
|
6545
|
-
className: `radio-option${isSelected ? " selected" : ""}`,
|
|
6886
|
+
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
6546
6887
|
onClick: () => onChange(name, option),
|
|
6547
6888
|
type: "button",
|
|
6548
6889
|
children: option
|
|
@@ -6563,11 +6904,17 @@ function isRadioOptionsWidget(widget) {
|
|
|
6563
6904
|
function isNumericKeypadWidget(widget) {
|
|
6564
6905
|
return widget.type === "numeric-keypad";
|
|
6565
6906
|
}
|
|
6907
|
+
function isFractionWidget(widget) {
|
|
6908
|
+
return widget.type === "fraction";
|
|
6909
|
+
}
|
|
6566
6910
|
const WidgetRenderer = ({
|
|
6567
6911
|
widget,
|
|
6568
6912
|
widgetId,
|
|
6569
6913
|
value,
|
|
6914
|
+
values = {},
|
|
6570
6915
|
isDropped = false,
|
|
6916
|
+
validationStatus = null,
|
|
6917
|
+
droppedInputs = /* @__PURE__ */ new Set(),
|
|
6571
6918
|
onChange,
|
|
6572
6919
|
onInputFocus,
|
|
6573
6920
|
onKeypadChange
|
|
@@ -6579,6 +6926,7 @@ const WidgetRenderer = ({
|
|
|
6579
6926
|
widgetId,
|
|
6580
6927
|
value,
|
|
6581
6928
|
isDropped,
|
|
6929
|
+
validationStatus,
|
|
6582
6930
|
inputType: widget.inputType,
|
|
6583
6931
|
length: widget.inputType === "string" ? widget.length : void 0,
|
|
6584
6932
|
digits: widget.inputType === "numeric" ? widget.digits : void 0,
|
|
@@ -6604,6 +6952,7 @@ const WidgetRenderer = ({
|
|
|
6604
6952
|
name: widgetId,
|
|
6605
6953
|
options: widget.options,
|
|
6606
6954
|
value,
|
|
6955
|
+
validationStatus,
|
|
6607
6956
|
onChange
|
|
6608
6957
|
}
|
|
6609
6958
|
);
|
|
@@ -6620,9 +6969,27 @@ const WidgetRenderer = ({
|
|
|
6620
6969
|
}
|
|
6621
6970
|
);
|
|
6622
6971
|
}
|
|
6972
|
+
if (isFractionWidget(widget)) {
|
|
6973
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6974
|
+
FractionInput,
|
|
6975
|
+
{
|
|
6976
|
+
widgetId,
|
|
6977
|
+
numerator: widget.numerator,
|
|
6978
|
+
denominator: widget.denominator,
|
|
6979
|
+
values,
|
|
6980
|
+
droppedInputs,
|
|
6981
|
+
validationStatus,
|
|
6982
|
+
onChange,
|
|
6983
|
+
onFocus: onInputFocus
|
|
6984
|
+
}
|
|
6985
|
+
);
|
|
6986
|
+
}
|
|
6623
6987
|
return null;
|
|
6624
6988
|
};
|
|
6625
|
-
const
|
|
6989
|
+
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
6990
|
+
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
6991
|
+
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
6992
|
+
const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
|
|
6626
6993
|
const toPercent = (value, min, max) => {
|
|
6627
6994
|
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
6628
6995
|
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
@@ -6638,19 +7005,24 @@ const getMajorTicks = (min, max, step) => {
|
|
|
6638
7005
|
const count = Math.floor((max - min) / step);
|
|
6639
7006
|
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
6640
7007
|
const lastTick = ticks[ticks.length - 1];
|
|
6641
|
-
if (
|
|
7008
|
+
if (!almostEqual(lastTick, max)) {
|
|
7009
|
+
ticks.push(max);
|
|
7010
|
+
} else {
|
|
7011
|
+
ticks[ticks.length - 1] = max;
|
|
7012
|
+
}
|
|
6642
7013
|
return ticks;
|
|
6643
7014
|
};
|
|
6644
7015
|
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
6645
|
-
if (minorTicks <= 0) return [];
|
|
7016
|
+
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
6646
7017
|
const result = [];
|
|
6647
|
-
|
|
6648
|
-
const
|
|
6649
|
-
const
|
|
7018
|
+
for (let i = 0; i < majorTicks.length - 1; i++) {
|
|
7019
|
+
const from = majorTicks[i];
|
|
7020
|
+
const to = majorTicks[i + 1];
|
|
7021
|
+
const interval = to - from;
|
|
6650
7022
|
for (let j = 1; j <= minorTicks; j++) {
|
|
6651
|
-
result.push(cleanFloat(
|
|
7023
|
+
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
6652
7024
|
}
|
|
6653
|
-
}
|
|
7025
|
+
}
|
|
6654
7026
|
return result;
|
|
6655
7027
|
};
|
|
6656
7028
|
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
@@ -6659,19 +7031,41 @@ const getSnapTicks = (majorTicks, minorTicks) => [
|
|
|
6659
7031
|
];
|
|
6660
7032
|
const snapToNearest = (raw, ticks) => {
|
|
6661
7033
|
if (ticks.length === 0) return cleanFloat(raw);
|
|
6662
|
-
|
|
6663
|
-
(best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
|
|
6664
|
-
);
|
|
6665
|
-
return cleanFloat(snapped);
|
|
7034
|
+
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
6666
7035
|
};
|
|
6667
|
-
const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
|
|
6668
7036
|
const getPositionWidgetId = (content) => {
|
|
6669
7037
|
for (const item of content) {
|
|
6670
7038
|
if (isWidgetRef(item)) return item.widgetId;
|
|
6671
7039
|
}
|
|
6672
7040
|
return null;
|
|
6673
7041
|
};
|
|
6674
|
-
const
|
|
7042
|
+
const toHighlightColor = (color) => `var(--nl-highlight-${color})`;
|
|
7043
|
+
const getBandGeometry = (h, min, max) => {
|
|
7044
|
+
const leftPct = toPercent(Math.min(h.from, h.to), min, max);
|
|
7045
|
+
const rightPct = toPercent(Math.max(h.from, h.to), min, max);
|
|
7046
|
+
return { leftPct, widthPct: rightPct - leftPct };
|
|
7047
|
+
};
|
|
7048
|
+
const HighlightBands = ({ highlights, min, max }) => {
|
|
7049
|
+
if (!highlights?.length) return null;
|
|
7050
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: highlights.map((h) => {
|
|
7051
|
+
const { leftPct, widthPct } = getBandGeometry(h, min, max);
|
|
7052
|
+
if (widthPct <= 0) return null;
|
|
7053
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7054
|
+
"div",
|
|
7055
|
+
{
|
|
7056
|
+
className: "number-line-highlight-band",
|
|
7057
|
+
"aria-hidden": "true",
|
|
7058
|
+
style: {
|
|
7059
|
+
left: `${leftPct}%`,
|
|
7060
|
+
width: `${widthPct}%`,
|
|
7061
|
+
...h.color && { "--nl-highlight-color": toHighlightColor(h.color) }
|
|
7062
|
+
}
|
|
7063
|
+
},
|
|
7064
|
+
h.id
|
|
7065
|
+
);
|
|
7066
|
+
}) });
|
|
7067
|
+
};
|
|
7068
|
+
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
6675
7069
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
6676
7070
|
id: widgetId,
|
|
6677
7071
|
data: { type: "placed-chip", widgetId, label }
|
|
@@ -6679,23 +7073,49 @@ const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
|
|
|
6679
7073
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6680
7074
|
"div",
|
|
6681
7075
|
{
|
|
6682
|
-
className:
|
|
6683
|
-
style: { left: `${leftPercent}
|
|
6684
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
7076
|
+
className: `number-line-placed-chip${isLanding ? " nl-chip-landing" : ""}${isDragging ? " nl-chip-dragging" : ""}`,
|
|
7077
|
+
style: { left: `${leftPercent}%` },
|
|
7078
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-placed-chip-inner", children: [
|
|
7079
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7080
|
+
"button",
|
|
7081
|
+
{
|
|
7082
|
+
ref: setNodeRef,
|
|
7083
|
+
className: "number-line-placed-chip-btn",
|
|
7084
|
+
"aria-label": `${label}, drag to move`,
|
|
7085
|
+
...listeners,
|
|
7086
|
+
...attributes,
|
|
7087
|
+
children: label
|
|
7088
|
+
}
|
|
7089
|
+
),
|
|
7090
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7091
|
+
"button",
|
|
7092
|
+
{
|
|
7093
|
+
type: "button",
|
|
7094
|
+
className: "number-line-placed-chip-remove",
|
|
7095
|
+
"aria-label": "Remove",
|
|
7096
|
+
onClick: onRemove,
|
|
7097
|
+
children: "✕"
|
|
7098
|
+
}
|
|
7099
|
+
)
|
|
7100
|
+
] })
|
|
6696
7101
|
}
|
|
6697
7102
|
);
|
|
6698
7103
|
};
|
|
7104
|
+
function findNearestFreeWidgetSlot(positions, axisValue, values, placedChips, excludeWid) {
|
|
7105
|
+
let best;
|
|
7106
|
+
let bestDist = Infinity;
|
|
7107
|
+
for (const pos of positions) {
|
|
7108
|
+
const wid = getPositionWidgetId(pos.content);
|
|
7109
|
+
if (!wid) continue;
|
|
7110
|
+
if (wid !== excludeWid && (values[wid] || placedChips.has(wid))) continue;
|
|
7111
|
+
const dist = Math.abs(pos.value - axisValue);
|
|
7112
|
+
if (dist < bestDist) {
|
|
7113
|
+
bestDist = dist;
|
|
7114
|
+
best = pos;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
return best;
|
|
7118
|
+
}
|
|
6699
7119
|
const FreeDragAxis = ({
|
|
6700
7120
|
axisDropId,
|
|
6701
7121
|
min,
|
|
@@ -6706,17 +7126,51 @@ const FreeDragAxis = ({
|
|
|
6706
7126
|
showArrowRight,
|
|
6707
7127
|
positions,
|
|
6708
7128
|
values,
|
|
6709
|
-
onChange
|
|
7129
|
+
onChange,
|
|
7130
|
+
highlights,
|
|
7131
|
+
snapKey
|
|
6710
7132
|
}) => {
|
|
6711
7133
|
const axisRef = React.useRef(null);
|
|
6712
7134
|
const snapTicks = React.useMemo(
|
|
6713
7135
|
() => getSnapTicks(majorTicks, minorTicks),
|
|
6714
7136
|
[majorTicks, minorTicks]
|
|
6715
7137
|
);
|
|
6716
|
-
const
|
|
6717
|
-
|
|
7138
|
+
const minorValues = React.useMemo(
|
|
7139
|
+
() => getMinorTickValues(majorTicks, minorTicks),
|
|
7140
|
+
[majorTicks, minorTicks]
|
|
7141
|
+
);
|
|
7142
|
+
const [placedChips, setPlacedChips] = React.useState(/* @__PURE__ */ new Map());
|
|
7143
|
+
const [hoverSnap, setHoverSnap] = React.useState(null);
|
|
7144
|
+
const [landingChips, setLandingChips] = React.useState(/* @__PURE__ */ new Set());
|
|
7145
|
+
const landingTimersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
7146
|
+
React.useEffect(() => () => landingTimersRef.current.forEach(clearTimeout), []);
|
|
7147
|
+
const onChangeRef = React.useRef(onChange);
|
|
7148
|
+
const placedChipsRef = React.useRef(placedChips);
|
|
6718
7149
|
React.useEffect(() => {
|
|
6719
|
-
|
|
7150
|
+
onChangeRef.current = onChange;
|
|
7151
|
+
}, [onChange]);
|
|
7152
|
+
React.useEffect(() => {
|
|
7153
|
+
placedChipsRef.current = placedChips;
|
|
7154
|
+
}, [placedChips]);
|
|
7155
|
+
const isFirstRender = React.useRef(true);
|
|
7156
|
+
React.useEffect(() => {
|
|
7157
|
+
if (isFirstRender.current) {
|
|
7158
|
+
isFirstRender.current = false;
|
|
7159
|
+
return;
|
|
7160
|
+
}
|
|
7161
|
+
for (const [widgetId, chip] of placedChipsRef.current) {
|
|
7162
|
+
onChangeRef.current(widgetId, "", false, chip.sourceId);
|
|
7163
|
+
}
|
|
7164
|
+
landingTimersRef.current.forEach(clearTimeout);
|
|
7165
|
+
landingTimersRef.current.clear();
|
|
7166
|
+
const empty = /* @__PURE__ */ new Map();
|
|
7167
|
+
placedChipsRef.current = empty;
|
|
7168
|
+
setPlacedChips(empty);
|
|
7169
|
+
setHoverSnap(null);
|
|
7170
|
+
setLandingChips(/* @__PURE__ */ new Set());
|
|
7171
|
+
}, [snapKey]);
|
|
7172
|
+
React.useEffect(() => {
|
|
7173
|
+
setPlacedChips((prev) => {
|
|
6720
7174
|
let changed = false;
|
|
6721
7175
|
const next = new Map(prev);
|
|
6722
7176
|
for (const [wid] of next) {
|
|
@@ -6725,21 +7179,13 @@ const FreeDragAxis = ({
|
|
|
6725
7179
|
changed = true;
|
|
6726
7180
|
}
|
|
6727
7181
|
}
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
setDropSources((prev) => {
|
|
6731
|
-
let changed = false;
|
|
6732
|
-
const next = new Map(prev);
|
|
6733
|
-
for (const [wid] of next) {
|
|
6734
|
-
if (!values[wid]) {
|
|
6735
|
-
next.delete(wid);
|
|
6736
|
-
changed = true;
|
|
6737
|
-
}
|
|
7182
|
+
if (changed) {
|
|
7183
|
+
placedChipsRef.current = next;
|
|
6738
7184
|
}
|
|
6739
7185
|
return changed ? next : prev;
|
|
6740
7186
|
});
|
|
6741
7187
|
}, [values]);
|
|
6742
|
-
const { setNodeRef
|
|
7188
|
+
const { setNodeRef } = useDroppable({ id: axisDropId });
|
|
6743
7189
|
const mergedRef = React.useCallback((node) => {
|
|
6744
7190
|
axisRef.current = node;
|
|
6745
7191
|
setNodeRef(node);
|
|
@@ -6747,91 +7193,163 @@ const FreeDragAxis = ({
|
|
|
6747
7193
|
const snapFromEvent = React.useCallback((translated) => {
|
|
6748
7194
|
if (!axisRef.current) return null;
|
|
6749
7195
|
const axisRect = axisRef.current.getBoundingClientRect();
|
|
7196
|
+
if (axisRect.width === 0) return null;
|
|
6750
7197
|
const chipCenterX = translated.left + translated.width / 2;
|
|
6751
7198
|
const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
|
|
6752
7199
|
return snapToNearest(raw, snapTicks);
|
|
6753
7200
|
}, [min, max, snapTicks]);
|
|
7201
|
+
const isTickOccupied = React.useCallback(
|
|
7202
|
+
(tick, excludeWid) => [...placedChipsRef.current.entries()].some(
|
|
7203
|
+
([wid, c]) => wid !== excludeWid && almostEqual(c.position, tick)
|
|
7204
|
+
),
|
|
7205
|
+
[]
|
|
7206
|
+
// reads placedChipsRef (a ref — stable, no dep needed)
|
|
7207
|
+
);
|
|
7208
|
+
const findNearestFreeWidgetSlotCb = React.useCallback(
|
|
7209
|
+
(axisValue, excludeWid) => findNearestFreeWidgetSlot(positions, axisValue, values, placedChipsRef.current, excludeWid),
|
|
7210
|
+
[positions, values]
|
|
7211
|
+
);
|
|
7212
|
+
const triggerLanding = React.useCallback((widgetId) => {
|
|
7213
|
+
setLandingChips((prev) => new Set(prev).add(widgetId));
|
|
7214
|
+
clearTimeout(landingTimersRef.current.get(widgetId));
|
|
7215
|
+
landingTimersRef.current.set(widgetId, setTimeout(() => {
|
|
7216
|
+
setLandingChips((prev) => {
|
|
7217
|
+
const s = new Set(prev);
|
|
7218
|
+
s.delete(widgetId);
|
|
7219
|
+
return s;
|
|
7220
|
+
});
|
|
7221
|
+
landingTimersRef.current.delete(widgetId);
|
|
7222
|
+
}, 300));
|
|
7223
|
+
}, []);
|
|
6754
7224
|
useDndMonitor({
|
|
7225
|
+
onDragMove(event) {
|
|
7226
|
+
if (!event.over || event.over.id !== axisDropId) {
|
|
7227
|
+
setHoverSnap(null);
|
|
7228
|
+
return;
|
|
7229
|
+
}
|
|
7230
|
+
const translated = event.active.rect.current.translated;
|
|
7231
|
+
if (!translated) return;
|
|
7232
|
+
setHoverSnap(snapFromEvent(translated));
|
|
7233
|
+
},
|
|
7234
|
+
onDragCancel() {
|
|
7235
|
+
setHoverSnap(null);
|
|
7236
|
+
},
|
|
6755
7237
|
onDragEnd(event) {
|
|
7238
|
+
setHoverSnap(null);
|
|
6756
7239
|
if (!event.over || event.over.id !== axisDropId) return;
|
|
6757
7240
|
if (!axisRef.current) return;
|
|
6758
7241
|
const translated = event.active.rect.current.translated;
|
|
6759
7242
|
if (!translated) return;
|
|
6760
|
-
const
|
|
6761
|
-
if (
|
|
7243
|
+
const rawPlaced = snapFromEvent(translated);
|
|
7244
|
+
if (rawPlaced === null) {
|
|
7245
|
+
if (process.env.NODE_ENV !== "production") {
|
|
7246
|
+
console.warn("[FreeDragAxis] snap calculation failed — axis may not be laid out yet");
|
|
7247
|
+
}
|
|
7248
|
+
return;
|
|
7249
|
+
}
|
|
7250
|
+
const placed = cleanFloat(rawPlaced);
|
|
6762
7251
|
const data = event.active.data.current;
|
|
6763
|
-
if (data
|
|
6764
|
-
|
|
7252
|
+
if (!data) return;
|
|
7253
|
+
if (data.type === "placed-chip") {
|
|
7254
|
+
const oldWidgetId = data.widgetId;
|
|
6765
7255
|
const label = data.label;
|
|
6766
|
-
const
|
|
6767
|
-
|
|
6768
|
-
|
|
7256
|
+
const sourceId2 = placedChipsRef.current.get(oldWidgetId)?.sourceId ?? oldWidgetId;
|
|
7257
|
+
if (isTickOccupied(placed, oldWidgetId)) return;
|
|
7258
|
+
const targetPos2 = findNearestFreeWidgetSlotCb(placed, oldWidgetId);
|
|
7259
|
+
if (!targetPos2) return;
|
|
7260
|
+
const newWidgetId = getPositionWidgetId(targetPos2.content);
|
|
7261
|
+
if (!newWidgetId) return;
|
|
7262
|
+
if (newWidgetId !== oldWidgetId) {
|
|
7263
|
+
onChangeRef.current(oldWidgetId, "", false, sourceId2);
|
|
7264
|
+
}
|
|
7265
|
+
onChangeRef.current(newWidgetId, encodeNLDragValue(label, placed), true, sourceId2);
|
|
7266
|
+
setPlacedChips((prev) => {
|
|
7267
|
+
const next = new Map(prev);
|
|
7268
|
+
if (newWidgetId !== oldWidgetId) next.delete(oldWidgetId);
|
|
7269
|
+
next.set(newWidgetId, { label, position: placed, sourceId: sourceId2 });
|
|
7270
|
+
placedChipsRef.current = next;
|
|
7271
|
+
return next;
|
|
7272
|
+
});
|
|
7273
|
+
triggerLanding(newWidgetId);
|
|
6769
7274
|
return;
|
|
6770
7275
|
}
|
|
6771
|
-
|
|
6772
|
-
if (
|
|
6773
|
-
const targetPos =
|
|
6774
|
-
const wid = getPositionWidgetId(pos.content);
|
|
6775
|
-
return wid && !values[wid];
|
|
6776
|
-
});
|
|
7276
|
+
if (data.type !== "drag-option") return;
|
|
7277
|
+
if (isTickOccupied(placed)) return;
|
|
7278
|
+
const targetPos = findNearestFreeWidgetSlotCb(placed);
|
|
6777
7279
|
if (!targetPos) return;
|
|
6778
7280
|
const targetWidgetId = getPositionWidgetId(targetPos.content);
|
|
6779
7281
|
if (!targetWidgetId) return;
|
|
6780
|
-
const chipLabel =
|
|
6781
|
-
const
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
7282
|
+
const chipLabel = data.data.value;
|
|
7283
|
+
const sourceId = String(event.active.id);
|
|
7284
|
+
onChangeRef.current(targetWidgetId, encodeNLDragValue(chipLabel, placed), true, sourceId);
|
|
7285
|
+
setPlacedChips((prev) => {
|
|
7286
|
+
const next = new Map(prev).set(targetWidgetId, { label: chipLabel, position: placed, sourceId });
|
|
7287
|
+
placedChipsRef.current = next;
|
|
7288
|
+
return next;
|
|
7289
|
+
});
|
|
7290
|
+
triggerLanding(targetWidgetId);
|
|
6785
7291
|
}
|
|
6786
7292
|
});
|
|
6787
7293
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6788
7294
|
"div",
|
|
6789
7295
|
{
|
|
6790
7296
|
ref: mergedRef,
|
|
6791
|
-
className:
|
|
7297
|
+
className: "number-line-axis-row nl-axis-drag-mode",
|
|
6792
7298
|
children: [
|
|
6793
7299
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track" }),
|
|
6794
|
-
showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
|
|
6795
|
-
showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
|
|
6796
|
-
|
|
7300
|
+
showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
7301
|
+
showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
7302
|
+
/* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
|
|
7303
|
+
minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6797
7304
|
"div",
|
|
6798
7305
|
{
|
|
6799
|
-
className:
|
|
7306
|
+
className: `number-line-minor-tick${hoverSnap === v ? " nl-tick-hover" : ""}`,
|
|
6800
7307
|
style: { left: `${toPercent(v, min, max)}%` }
|
|
6801
7308
|
},
|
|
6802
|
-
`minor-${v}`
|
|
7309
|
+
`minor-${cleanFloat(v)}`
|
|
6803
7310
|
)),
|
|
6804
7311
|
majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6805
7312
|
"div",
|
|
6806
7313
|
{
|
|
6807
|
-
className:
|
|
7314
|
+
className: `number-line-major-tick${hoverSnap === tick ? " nl-tick-hover" : ""}`,
|
|
6808
7315
|
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6809
7316
|
},
|
|
6810
|
-
`major-${tick}`
|
|
7317
|
+
`major-${cleanFloat(tick)}`
|
|
7318
|
+
)),
|
|
7319
|
+
hoverSnap !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7320
|
+
"div",
|
|
7321
|
+
{
|
|
7322
|
+
className: "nl-snap-preview",
|
|
7323
|
+
style: { left: `${toPercent(hoverSnap, min, max)}%` },
|
|
7324
|
+
"aria-hidden": "true"
|
|
7325
|
+
}
|
|
7326
|
+
),
|
|
7327
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) === null).map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
7328
|
+
"div",
|
|
7329
|
+
{
|
|
7330
|
+
className: "number-line-position-dot",
|
|
7331
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
7332
|
+
},
|
|
7333
|
+
`dot-${pos.id}`
|
|
6811
7334
|
)),
|
|
6812
7335
|
positions.map((pos) => {
|
|
6813
7336
|
const wid = getPositionWidgetId(pos.content);
|
|
6814
|
-
const
|
|
6815
|
-
|
|
6816
|
-
if (!wid || isNaN(dropPos) || !label) return null;
|
|
7337
|
+
const chip = wid ? placedChips.get(wid) : void 0;
|
|
7338
|
+
if (!wid || !chip) return null;
|
|
6817
7339
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6818
7340
|
PlacedChip,
|
|
6819
7341
|
{
|
|
6820
7342
|
widgetId: wid,
|
|
6821
|
-
label,
|
|
6822
|
-
leftPercent: toPercent(
|
|
7343
|
+
label: chip.label,
|
|
7344
|
+
leftPercent: toPercent(chip.position, min, max),
|
|
7345
|
+
isLanding: landingChips.has(wid),
|
|
6823
7346
|
onRemove: () => {
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
return
|
|
6830
|
-
});
|
|
6831
|
-
setDropSources((prev) => {
|
|
6832
|
-
const m = new Map(prev);
|
|
6833
|
-
m.delete(wid);
|
|
6834
|
-
return m;
|
|
7347
|
+
onChangeRef.current(wid, "", false, chip.sourceId);
|
|
7348
|
+
setPlacedChips((prev) => {
|
|
7349
|
+
const next = new Map(prev);
|
|
7350
|
+
next.delete(wid);
|
|
7351
|
+
placedChipsRef.current = next;
|
|
7352
|
+
return next;
|
|
6835
7353
|
});
|
|
6836
7354
|
}
|
|
6837
7355
|
},
|
|
@@ -6842,35 +7360,194 @@ const FreeDragAxis = ({
|
|
|
6842
7360
|
}
|
|
6843
7361
|
);
|
|
6844
7362
|
};
|
|
7363
|
+
const AxisTicks = ({
|
|
7364
|
+
min,
|
|
7365
|
+
max,
|
|
7366
|
+
majorTicks,
|
|
7367
|
+
minorValues,
|
|
7368
|
+
showArrowLeft,
|
|
7369
|
+
showArrowRight,
|
|
7370
|
+
highlights,
|
|
7371
|
+
positions
|
|
7372
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
|
|
7373
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
|
|
7374
|
+
showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
7375
|
+
showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
7376
|
+
/* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
|
|
7377
|
+
minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
7378
|
+
"div",
|
|
7379
|
+
{
|
|
7380
|
+
className: "number-line-minor-tick",
|
|
7381
|
+
"aria-hidden": "true",
|
|
7382
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
7383
|
+
},
|
|
7384
|
+
`minor-${v}`
|
|
7385
|
+
)),
|
|
7386
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
7387
|
+
"div",
|
|
7388
|
+
{
|
|
7389
|
+
className: "number-line-major-tick",
|
|
7390
|
+
"aria-hidden": "true",
|
|
7391
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
7392
|
+
},
|
|
7393
|
+
`major-${tick}`
|
|
7394
|
+
)),
|
|
7395
|
+
positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
7396
|
+
"div",
|
|
7397
|
+
{
|
|
7398
|
+
className: "number-line-position-dot",
|
|
7399
|
+
"aria-hidden": "true",
|
|
7400
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
7401
|
+
},
|
|
7402
|
+
`dot-${pos.id}`
|
|
7403
|
+
))
|
|
7404
|
+
] });
|
|
7405
|
+
const isLabelVisible = (tick, min, max, labelVisibility) => {
|
|
7406
|
+
switch (labelVisibility) {
|
|
7407
|
+
case "none":
|
|
7408
|
+
return false;
|
|
7409
|
+
case "min-only":
|
|
7410
|
+
return tick === roundTick(min);
|
|
7411
|
+
case "max-only":
|
|
7412
|
+
return tick === roundTick(max);
|
|
7413
|
+
case "extremes":
|
|
7414
|
+
return tick === roundTick(min) || tick === roundTick(max);
|
|
7415
|
+
default:
|
|
7416
|
+
return true;
|
|
7417
|
+
}
|
|
7418
|
+
};
|
|
7419
|
+
const AxisLabels = ({
|
|
7420
|
+
majorTicks,
|
|
7421
|
+
min,
|
|
7422
|
+
max,
|
|
7423
|
+
labelVisibility,
|
|
7424
|
+
inputTickSet
|
|
7425
|
+
}) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
|
|
7426
|
+
const visible = !inputTickSet.has(tick) && isLabelVisible(tick, min, max, labelVisibility);
|
|
7427
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7428
|
+
"span",
|
|
7429
|
+
{
|
|
7430
|
+
className: "number-line-tick-label",
|
|
7431
|
+
style: {
|
|
7432
|
+
left: `${toPercent(tick, min, max)}%`,
|
|
7433
|
+
visibility: visible ? "visible" : "hidden"
|
|
7434
|
+
},
|
|
7435
|
+
children: tick
|
|
7436
|
+
},
|
|
7437
|
+
`lbl-${tick}`
|
|
7438
|
+
);
|
|
7439
|
+
}) });
|
|
7440
|
+
const HighlightLabelsRow = ({ highlights, min, max }) => {
|
|
7441
|
+
if (!highlights?.some((h) => h.label)) return null;
|
|
7442
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-highlight-labels-row", "aria-hidden": "true", children: highlights.map((h) => {
|
|
7443
|
+
if (!h.label) return null;
|
|
7444
|
+
const centerPct = (toPercent(h.from, min, max) + toPercent(h.to, min, max)) / 2;
|
|
7445
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7446
|
+
"span",
|
|
7447
|
+
{
|
|
7448
|
+
className: "number-line-highlight-label",
|
|
7449
|
+
style: { left: `${centerPct}%` },
|
|
7450
|
+
children: h.label
|
|
7451
|
+
},
|
|
7452
|
+
`hl-lbl-${h.id}`
|
|
7453
|
+
);
|
|
7454
|
+
}) });
|
|
7455
|
+
};
|
|
7456
|
+
const CounterControls = ({
|
|
7457
|
+
counter,
|
|
7458
|
+
count,
|
|
7459
|
+
onDecrement,
|
|
7460
|
+
onIncrement
|
|
7461
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter", "aria-label": counter.name ?? "Counter", children: [
|
|
7462
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter-controls", children: [
|
|
7463
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7464
|
+
"button",
|
|
7465
|
+
{
|
|
7466
|
+
type: "button",
|
|
7467
|
+
className: "number-line-counter-btn",
|
|
7468
|
+
"aria-label": "Decrease",
|
|
7469
|
+
disabled: count <= counter.min,
|
|
7470
|
+
onClick: onDecrement,
|
|
7471
|
+
children: "−"
|
|
7472
|
+
}
|
|
7473
|
+
),
|
|
7474
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-value", "data-testid": "counter-value", children: count }),
|
|
7475
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7476
|
+
"button",
|
|
7477
|
+
{
|
|
7478
|
+
type: "button",
|
|
7479
|
+
className: "number-line-counter-btn",
|
|
7480
|
+
"aria-label": "Increase",
|
|
7481
|
+
disabled: count >= counter.max,
|
|
7482
|
+
onClick: onIncrement,
|
|
7483
|
+
children: "+"
|
|
7484
|
+
}
|
|
7485
|
+
)
|
|
7486
|
+
] }),
|
|
7487
|
+
counter.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-label", children: counter.name })
|
|
7488
|
+
] });
|
|
6845
7489
|
const NumberLineRenderer = ({
|
|
6846
7490
|
item,
|
|
6847
7491
|
widgets,
|
|
6848
7492
|
values,
|
|
6849
7493
|
droppedInputs,
|
|
7494
|
+
validationDetails,
|
|
6850
7495
|
onChange,
|
|
6851
7496
|
onInputFocus
|
|
6852
7497
|
}) => {
|
|
6853
|
-
const {
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
7498
|
+
const {
|
|
7499
|
+
min,
|
|
7500
|
+
max,
|
|
7501
|
+
step,
|
|
7502
|
+
minorTicks = 0,
|
|
7503
|
+
style: lineStyle = "arrow-right",
|
|
7504
|
+
labelVisibility,
|
|
7505
|
+
highlights,
|
|
7506
|
+
counter,
|
|
7507
|
+
positions
|
|
7508
|
+
} = item;
|
|
7509
|
+
const [counterCount, setCounterCount] = React.useState(
|
|
7510
|
+
counter !== void 0 ? counter.default ?? counter.min : 0
|
|
7511
|
+
);
|
|
7512
|
+
React.useEffect(() => {
|
|
7513
|
+
setCounterCount(counter !== void 0 ? counter.default ?? counter.min : 0);
|
|
7514
|
+
}, [counter]);
|
|
7515
|
+
const effectiveStep = React.useMemo(() => {
|
|
7516
|
+
if (counter !== void 0 && counterCount > 0) {
|
|
7517
|
+
return cleanFloat((max - min) / counterCount);
|
|
7518
|
+
}
|
|
7519
|
+
return step;
|
|
7520
|
+
}, [counter, counterCount, min, max, step]);
|
|
7521
|
+
const majorTicks = React.useMemo(
|
|
7522
|
+
() => getMajorTicks(min, max, effectiveStep).map(roundTick),
|
|
7523
|
+
[min, max, effectiveStep]
|
|
7524
|
+
);
|
|
7525
|
+
const minorValues = React.useMemo(
|
|
7526
|
+
() => getMinorTickValues(majorTicks, minorTicks),
|
|
7527
|
+
[majorTicks, minorTicks]
|
|
7528
|
+
);
|
|
7529
|
+
const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
|
|
7530
|
+
const showArrowLeft = lineStyle === "arrow-left" || lineStyle === "arrow-both";
|
|
6858
7531
|
const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
|
|
6859
7532
|
const inputTickSet = React.useMemo(() => {
|
|
6860
7533
|
if (isDragMode) return /* @__PURE__ */ new Set();
|
|
6861
7534
|
return new Set(
|
|
6862
|
-
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
|
|
7535
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => roundTick(pos.value))
|
|
6863
7536
|
);
|
|
6864
7537
|
}, [isDragMode, positions]);
|
|
7538
|
+
const visiblePositions = React.useMemo(
|
|
7539
|
+
() => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
|
|
7540
|
+
[isDragMode, positions]
|
|
7541
|
+
);
|
|
6865
7542
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6866
7543
|
"div",
|
|
6867
7544
|
{
|
|
6868
|
-
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
|
|
7545
|
+
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
|
|
6869
7546
|
style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
|
|
6870
|
-
role: "
|
|
7547
|
+
role: "group",
|
|
6871
7548
|
"aria-label": `Number line from ${min} to ${max}`,
|
|
6872
7549
|
children: [
|
|
6873
|
-
|
|
7550
|
+
visiblePositions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6874
7551
|
"div",
|
|
6875
7552
|
{
|
|
6876
7553
|
className: "number-line-position-content",
|
|
@@ -6882,6 +7559,7 @@ const NumberLineRenderer = ({
|
|
|
6882
7559
|
widgets,
|
|
6883
7560
|
values,
|
|
6884
7561
|
droppedInputs,
|
|
7562
|
+
validationDetails,
|
|
6885
7563
|
onChange,
|
|
6886
7564
|
onInputFocus
|
|
6887
7565
|
},
|
|
@@ -6902,50 +7580,43 @@ const NumberLineRenderer = ({
|
|
|
6902
7580
|
showArrowRight,
|
|
6903
7581
|
positions,
|
|
6904
7582
|
values,
|
|
6905
|
-
onChange
|
|
7583
|
+
onChange,
|
|
7584
|
+
highlights,
|
|
7585
|
+
snapKey: counterCount
|
|
6906
7586
|
}
|
|
6907
|
-
) : /* @__PURE__ */ jsxRuntime.
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
"span",
|
|
6941
|
-
{
|
|
6942
|
-
className: "number-line-tick-label",
|
|
6943
|
-
style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
|
|
6944
|
-
children: tick
|
|
6945
|
-
},
|
|
6946
|
-
`lbl-${tick}`
|
|
6947
|
-
);
|
|
6948
|
-
}) })
|
|
7587
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
7588
|
+
AxisTicks,
|
|
7589
|
+
{
|
|
7590
|
+
min,
|
|
7591
|
+
max,
|
|
7592
|
+
majorTicks,
|
|
7593
|
+
minorValues,
|
|
7594
|
+
showArrowLeft,
|
|
7595
|
+
showArrowRight,
|
|
7596
|
+
highlights,
|
|
7597
|
+
positions
|
|
7598
|
+
}
|
|
7599
|
+
),
|
|
7600
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7601
|
+
AxisLabels,
|
|
7602
|
+
{
|
|
7603
|
+
majorTicks,
|
|
7604
|
+
min,
|
|
7605
|
+
max,
|
|
7606
|
+
labelVisibility,
|
|
7607
|
+
inputTickSet
|
|
7608
|
+
}
|
|
7609
|
+
),
|
|
7610
|
+
/* @__PURE__ */ jsxRuntime.jsx(HighlightLabelsRow, { highlights, min, max }),
|
|
7611
|
+
counter !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7612
|
+
CounterControls,
|
|
7613
|
+
{
|
|
7614
|
+
counter,
|
|
7615
|
+
count: counterCount,
|
|
7616
|
+
onDecrement: () => setCounterCount((c) => Math.max(counter.min, c - 1)),
|
|
7617
|
+
onIncrement: () => setCounterCount((c) => Math.min(counter.max, c + 1))
|
|
7618
|
+
}
|
|
7619
|
+
)
|
|
6949
7620
|
]
|
|
6950
7621
|
}
|
|
6951
7622
|
);
|
|
@@ -7013,6 +7684,7 @@ const LayoutRenderer = ({
|
|
|
7013
7684
|
widgets,
|
|
7014
7685
|
values,
|
|
7015
7686
|
droppedInputs,
|
|
7687
|
+
validationDetails,
|
|
7016
7688
|
onChange,
|
|
7017
7689
|
onInputFocus
|
|
7018
7690
|
}) => {
|
|
@@ -7065,13 +7737,18 @@ const LayoutRenderer = ({
|
|
|
7065
7737
|
case JSON_TYPES.WIDGET_REF: {
|
|
7066
7738
|
const widget = widgets[item.widgetId];
|
|
7067
7739
|
if (!widget) return null;
|
|
7740
|
+
const isCorrect = validationDetails?.[item.widgetId];
|
|
7741
|
+
const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
|
|
7068
7742
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7069
7743
|
WidgetRenderer,
|
|
7070
7744
|
{
|
|
7071
7745
|
widget,
|
|
7072
7746
|
widgetId: item.widgetId,
|
|
7073
7747
|
value: values[item.widgetId] || "",
|
|
7748
|
+
values,
|
|
7074
7749
|
isDropped: droppedInputs.has(item.widgetId),
|
|
7750
|
+
validationStatus,
|
|
7751
|
+
droppedInputs,
|
|
7075
7752
|
onChange,
|
|
7076
7753
|
onInputFocus
|
|
7077
7754
|
}
|
|
@@ -7091,6 +7768,7 @@ const LayoutRenderer = ({
|
|
|
7091
7768
|
widgets,
|
|
7092
7769
|
values,
|
|
7093
7770
|
droppedInputs,
|
|
7771
|
+
validationDetails,
|
|
7094
7772
|
onChange,
|
|
7095
7773
|
onInputFocus
|
|
7096
7774
|
},
|
|
@@ -7109,6 +7787,7 @@ const LayoutRenderer = ({
|
|
|
7109
7787
|
widgets,
|
|
7110
7788
|
values,
|
|
7111
7789
|
droppedInputs,
|
|
7790
|
+
validationDetails,
|
|
7112
7791
|
onChange,
|
|
7113
7792
|
onInputFocus
|
|
7114
7793
|
},
|
|
@@ -7125,6 +7804,7 @@ const LayoutRenderer = ({
|
|
|
7125
7804
|
widgets,
|
|
7126
7805
|
values,
|
|
7127
7806
|
droppedInputs,
|
|
7807
|
+
validationDetails,
|
|
7128
7808
|
onChange,
|
|
7129
7809
|
onInputFocus
|
|
7130
7810
|
}
|
|
@@ -7152,9 +7832,15 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7152
7832
|
const [values, setValues] = React.useState({});
|
|
7153
7833
|
const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
|
|
7154
7834
|
const [focusedInputId, setFocusedInputId] = React.useState(null);
|
|
7835
|
+
const [validationDetails, setValidationDetails] = React.useState(null);
|
|
7155
7836
|
React.useImperativeHandle(ref, () => ({
|
|
7156
7837
|
submit: () => {
|
|
7157
7838
|
const result = validate(json, values);
|
|
7839
|
+
const details = {};
|
|
7840
|
+
for (const [id, v] of Object.entries(result.details)) {
|
|
7841
|
+
details[id] = v.isCorrect;
|
|
7842
|
+
}
|
|
7843
|
+
setValidationDetails(details);
|
|
7158
7844
|
onSubmit?.(values);
|
|
7159
7845
|
return result;
|
|
7160
7846
|
},
|
|
@@ -7163,6 +7849,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7163
7849
|
setValues({});
|
|
7164
7850
|
setDroppedSources(/* @__PURE__ */ new Map());
|
|
7165
7851
|
setFocusedInputId(null);
|
|
7852
|
+
setValidationDetails(null);
|
|
7166
7853
|
},
|
|
7167
7854
|
focusFirstInput: () => {
|
|
7168
7855
|
const container = document.querySelector(".math-exercise-renderer");
|
|
@@ -7173,8 +7860,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7173
7860
|
}
|
|
7174
7861
|
}), [json, values, onSubmit]);
|
|
7175
7862
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
7176
|
-
|
|
7177
|
-
setValues(newValues);
|
|
7863
|
+
setValues((prev) => ({ ...prev, [widgetId]: value }));
|
|
7178
7864
|
setDroppedSources((prev) => {
|
|
7179
7865
|
const next = new Map(prev);
|
|
7180
7866
|
if (fromDrop && value !== "" && sourceOptionId) {
|
|
@@ -7184,7 +7870,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7184
7870
|
}
|
|
7185
7871
|
return next;
|
|
7186
7872
|
});
|
|
7187
|
-
onChange?.(
|
|
7873
|
+
onChange?.({ ...values, [widgetId]: value });
|
|
7188
7874
|
};
|
|
7189
7875
|
const handleSubmit = () => {
|
|
7190
7876
|
onSubmit?.(values);
|
|
@@ -7277,6 +7963,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7277
7963
|
widgets: json.widgets,
|
|
7278
7964
|
values,
|
|
7279
7965
|
droppedInputs,
|
|
7966
|
+
validationDetails,
|
|
7280
7967
|
onChange: handleChange,
|
|
7281
7968
|
onInputFocus: setFocusedInputId
|
|
7282
7969
|
},
|
|
@@ -7291,6 +7978,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7291
7978
|
widgets: json.widgets,
|
|
7292
7979
|
values,
|
|
7293
7980
|
droppedInputs,
|
|
7981
|
+
validationDetails,
|
|
7294
7982
|
onChange: handleChange,
|
|
7295
7983
|
onInputFocus: setFocusedInputId
|
|
7296
7984
|
},
|
|
@@ -7306,6 +7994,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7306
7994
|
widgets: json.widgets,
|
|
7307
7995
|
values,
|
|
7308
7996
|
droppedInputs,
|
|
7997
|
+
validationDetails,
|
|
7309
7998
|
onChange: handleChange,
|
|
7310
7999
|
onInputFocus: setFocusedInputId
|
|
7311
8000
|
},
|
|
@@ -7394,6 +8083,8 @@ exports.ExerciseJSONSchema = ExerciseJSONSchema;
|
|
|
7394
8083
|
exports.ExerciseMetaSchema = ExerciseMetaSchema;
|
|
7395
8084
|
exports.ExerciseRenderer = ExerciseRenderer;
|
|
7396
8085
|
exports.ExpressionValidationSchema = ExpressionValidationSchema;
|
|
8086
|
+
exports.FractionInlineSchema = FractionInlineSchema;
|
|
8087
|
+
exports.FractionJSONSchema = FractionJSONSchema;
|
|
7397
8088
|
exports.FreeTextInlineSchema = FreeTextInlineSchema;
|
|
7398
8089
|
exports.GroupItemSchema = GroupItemSchema;
|
|
7399
8090
|
exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
|
|
@@ -7407,6 +8098,10 @@ exports.LayoutRenderer = LayoutRenderer;
|
|
|
7407
8098
|
exports.LexerError = LexerError;
|
|
7408
8099
|
exports.LineBlockSchema = LineBlockSchema;
|
|
7409
8100
|
exports.LinebreakSchema = LinebreakSchema;
|
|
8101
|
+
exports.NumberLineCounterItemSchema = NumberLineCounterItemSchema;
|
|
8102
|
+
exports.NumberLineHighlightItemSchema = NumberLineHighlightItemSchema;
|
|
8103
|
+
exports.NumberLineItemSchema = NumberLineItemSchema;
|
|
8104
|
+
exports.NumberLinePositionItemSchema = NumberLinePositionItemSchema;
|
|
7410
8105
|
exports.NumericKeypadJSONSchema = NumericKeypadJSONSchema;
|
|
7411
8106
|
exports.NumericKeypadToolSchema = NumericKeypadToolSchema;
|
|
7412
8107
|
exports.OperatorItemSchema = OperatorItemSchema;
|
|
@@ -7437,6 +8132,7 @@ exports.createColorText = createColorText;
|
|
|
7437
8132
|
exports.createDragOptions = createDragOptions;
|
|
7438
8133
|
exports.createExerciseDocument = createExerciseDocument;
|
|
7439
8134
|
exports.createExpressionValidation = createExpressionValidation;
|
|
8135
|
+
exports.createFraction = createFraction;
|
|
7440
8136
|
exports.createFreeText = createFreeText;
|
|
7441
8137
|
exports.createHighlightedText = createHighlightedText;
|
|
7442
8138
|
exports.createLineBlock = createLineBlock;
|
|
@@ -7449,6 +8145,7 @@ exports.createTextBlock = createTextBlock;
|
|
|
7449
8145
|
exports.evaluate = evaluate;
|
|
7450
8146
|
exports.extractExpressionVariables = extractVariables;
|
|
7451
8147
|
exports.findValidCombination = findValidCombination;
|
|
8148
|
+
exports.getFractionKey = getFractionKey;
|
|
7452
8149
|
exports.getMissingInputs = getMissingInputs;
|
|
7453
8150
|
exports.isBoldText = isBoldText;
|
|
7454
8151
|
exports.isColorText = isColorText;
|
|
@@ -7456,6 +8153,9 @@ exports.isDragOptions = isDragOptions;
|
|
|
7456
8153
|
exports.isDragOptionsJSON = isDragOptionsJSON;
|
|
7457
8154
|
exports.isExerciseDocument = isExerciseDocument;
|
|
7458
8155
|
exports.isExpressionValidation = isExpressionValidation;
|
|
8156
|
+
exports.isFraction = isFraction;
|
|
8157
|
+
exports.isFractionJSON = isFractionJSON;
|
|
8158
|
+
exports.isFractionKey = isFractionKey;
|
|
7459
8159
|
exports.isFreeText = isFreeText;
|
|
7460
8160
|
exports.isGroupItem = isGroupItem;
|
|
7461
8161
|
exports.isHighlightedText = isHighlightedText;
|
|
@@ -7480,8 +8180,9 @@ exports.isTextItem = isTextItem$1;
|
|
|
7480
8180
|
exports.isToolNode = isToolNode;
|
|
7481
8181
|
exports.isValidExpression = isValidExpression;
|
|
7482
8182
|
exports.isValidationNode = isValidationNode;
|
|
7483
|
-
exports.isWidgetRef = isWidgetRef
|
|
8183
|
+
exports.isWidgetRef = isWidgetRef;
|
|
7484
8184
|
exports.parse = parse;
|
|
8185
|
+
exports.parseFractionKey = parseFractionKey;
|
|
7485
8186
|
exports.parseSafe = parseSafe;
|
|
7486
8187
|
exports.renderExercise = renderExercise;
|
|
7487
8188
|
exports.toAST = toAST;
|