@mathscan/math-exercise-engine 1.0.8 → 1.0.10
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 +1384 -397
- package/dist/index.js +623 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -16
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +238 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -34,6 +34,10 @@ const AST_TYPES = {
|
|
|
34
34
|
TABLE_ROW: "TableRow",
|
|
35
35
|
/** Cell within a table row */
|
|
36
36
|
TABLE_CELL: "TableCell",
|
|
37
|
+
/** Number line layout block */
|
|
38
|
+
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
39
|
+
/** A position marker on the number line */
|
|
40
|
+
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
37
41
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
42
|
// Tools
|
|
39
43
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -86,6 +90,10 @@ const JSON_TYPES = {
|
|
|
86
90
|
TABLE_ROW: "table-row",
|
|
87
91
|
/** Cell within a table */
|
|
88
92
|
TABLE_CELL: "table-cell",
|
|
93
|
+
/** Number line layout block */
|
|
94
|
+
NUMBER_LINE: "number-line",
|
|
95
|
+
/** Position marker within a number line */
|
|
96
|
+
NUMBER_LINE_POSITION: "number-line-position",
|
|
89
97
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
98
|
// Widget Types
|
|
91
99
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -98,6 +106,7 @@ const JSON_TYPES = {
|
|
|
98
106
|
/** Numeric keypad widget */
|
|
99
107
|
NUMERIC_KEYPAD: "numeric-keypad"
|
|
100
108
|
};
|
|
109
|
+
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
101
110
|
const VALIDATION_MODES = {
|
|
102
111
|
/** Simple answer matching validation */
|
|
103
112
|
SIMPLE: "simple",
|
|
@@ -110,6 +119,9 @@ const INPUT_TYPES = {
|
|
|
110
119
|
/** Numeric input (digits only) */
|
|
111
120
|
NUMERIC: "numeric"
|
|
112
121
|
};
|
|
122
|
+
const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
|
|
123
|
+
const SIDE_PAD = 7;
|
|
124
|
+
const CSS_VAR_SIDE_PAD = "--nl-side-pad";
|
|
113
125
|
const Newline = createToken({
|
|
114
126
|
name: "Newline",
|
|
115
127
|
pattern: /\r?\n/,
|
|
@@ -183,6 +195,14 @@ const NUMERIC_INPUT = createToken({
|
|
|
183
195
|
name: "NUMERIC_INPUT",
|
|
184
196
|
pattern: /NUMERIC_INPUT/
|
|
185
197
|
});
|
|
198
|
+
const NUMBER_LINE = createToken({
|
|
199
|
+
name: "NUMBER_LINE",
|
|
200
|
+
pattern: /NUMBER_LINE/
|
|
201
|
+
});
|
|
202
|
+
const POSITION = createToken({
|
|
203
|
+
name: "POSITION",
|
|
204
|
+
pattern: /POSITION/
|
|
205
|
+
});
|
|
186
206
|
const TABLE = createToken({
|
|
187
207
|
name: "TABLE",
|
|
188
208
|
pattern: /TABLE/
|
|
@@ -297,6 +317,8 @@ const allTokens = [
|
|
|
297
317
|
WIDGET_ANSWER,
|
|
298
318
|
W_N_I,
|
|
299
319
|
NUMERIC_INPUT,
|
|
320
|
+
NUMBER_LINE,
|
|
321
|
+
POSITION,
|
|
300
322
|
TABLE,
|
|
301
323
|
ROW,
|
|
302
324
|
CELL,
|
|
@@ -386,14 +408,14 @@ const MathExerciseLexer = new Lexer(allTokens, {
|
|
|
386
408
|
positionTracking: "full"
|
|
387
409
|
});
|
|
388
410
|
function tokenize(input) {
|
|
389
|
-
const result = MathExerciseLexer.tokenize(input);
|
|
411
|
+
const result = MathExerciseLexer.tokenize(input.replace(/\u00A0/g, " "));
|
|
390
412
|
if (result.errors.length > 0) {
|
|
391
413
|
throw new LexerError(result.errors);
|
|
392
414
|
}
|
|
393
415
|
return result.tokens;
|
|
394
416
|
}
|
|
395
417
|
function tokenizeSafe(input) {
|
|
396
|
-
const result = MathExerciseLexer.tokenize(input);
|
|
418
|
+
const result = MathExerciseLexer.tokenize(input.replace(/\u00A0/g, " "));
|
|
397
419
|
return {
|
|
398
420
|
tokens: result.tokens,
|
|
399
421
|
errors: result.errors
|
|
@@ -421,7 +443,9 @@ class MathExerciseParser extends CstParser {
|
|
|
421
443
|
this.MANY3(() => {
|
|
422
444
|
this.SUBRULE(this.tool);
|
|
423
445
|
});
|
|
424
|
-
this.
|
|
446
|
+
this.OPTION2(() => {
|
|
447
|
+
this.SUBRULE(this.validation);
|
|
448
|
+
});
|
|
425
449
|
this.MANY4(() => {
|
|
426
450
|
this.OR2([
|
|
427
451
|
{ ALT: () => this.CONSUME2(Newline) },
|
|
@@ -451,6 +475,10 @@ class MathExerciseParser extends CstParser {
|
|
|
451
475
|
GATE: this.BACKTRACK(this.radioButtons),
|
|
452
476
|
ALT: () => this.SUBRULE(this.radioButtons)
|
|
453
477
|
},
|
|
478
|
+
{
|
|
479
|
+
GATE: this.BACKTRACK(this.numberLine),
|
|
480
|
+
ALT: () => this.SUBRULE(this.numberLine)
|
|
481
|
+
},
|
|
454
482
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
455
483
|
]);
|
|
456
484
|
});
|
|
@@ -555,6 +583,79 @@ class MathExerciseParser extends CstParser {
|
|
|
555
583
|
]);
|
|
556
584
|
});
|
|
557
585
|
});
|
|
586
|
+
this.numberLine = this.RULE("numberLine", () => {
|
|
587
|
+
this.MANY(() => {
|
|
588
|
+
this.OR2([
|
|
589
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
590
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
591
|
+
]);
|
|
592
|
+
});
|
|
593
|
+
this.CONSUME(NUMBER_LINE);
|
|
594
|
+
this.CONSUME(LBracket);
|
|
595
|
+
this.SUBRULE(this.parameterList);
|
|
596
|
+
this.CONSUME(RBracket);
|
|
597
|
+
this.MANY2(() => {
|
|
598
|
+
this.OR3([
|
|
599
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
600
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
601
|
+
]);
|
|
602
|
+
});
|
|
603
|
+
this.CONSUME(LBrace);
|
|
604
|
+
this.MANY3(() => {
|
|
605
|
+
this.OR4([
|
|
606
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
607
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
608
|
+
]);
|
|
609
|
+
});
|
|
610
|
+
this.MANY4(() => {
|
|
611
|
+
this.SUBRULE(this.position);
|
|
612
|
+
});
|
|
613
|
+
this.CONSUME(RBrace);
|
|
614
|
+
this.MANY5(() => {
|
|
615
|
+
this.OR5([
|
|
616
|
+
{ ALT: () => this.CONSUME4(Newline) },
|
|
617
|
+
{ ALT: () => this.CONSUME4(Space) }
|
|
618
|
+
]);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
this.position = this.RULE("position", () => {
|
|
622
|
+
this.MANY(() => {
|
|
623
|
+
this.OR([
|
|
624
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
625
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
626
|
+
]);
|
|
627
|
+
});
|
|
628
|
+
this.CONSUME(POSITION);
|
|
629
|
+
this.CONSUME(LBracket);
|
|
630
|
+
this.SUBRULE(this.parameterList);
|
|
631
|
+
this.CONSUME(RBracket);
|
|
632
|
+
this.MANY2(() => {
|
|
633
|
+
this.OR2([
|
|
634
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
635
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
636
|
+
]);
|
|
637
|
+
});
|
|
638
|
+
this.CONSUME(LBrace);
|
|
639
|
+
this.MANY3(() => {
|
|
640
|
+
this.OR3([
|
|
641
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
642
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
643
|
+
]);
|
|
644
|
+
});
|
|
645
|
+
this.MANY4(() => {
|
|
646
|
+
this.OR4([
|
|
647
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
648
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
649
|
+
]);
|
|
650
|
+
});
|
|
651
|
+
this.CONSUME(RBrace);
|
|
652
|
+
this.MANY5(() => {
|
|
653
|
+
this.OR5([
|
|
654
|
+
{ ALT: () => this.CONSUME5(Space) },
|
|
655
|
+
{ ALT: () => this.CONSUME5(Newline) }
|
|
656
|
+
]);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
558
659
|
this.line = this.RULE("line", () => {
|
|
559
660
|
this.MANY(() => {
|
|
560
661
|
this.CONSUME(Space);
|
|
@@ -917,6 +1018,71 @@ function isTextBlock(value) {
|
|
|
917
1018
|
function isLineBlock(value) {
|
|
918
1019
|
return LineBlockSchema.safeParse(value).success;
|
|
919
1020
|
}
|
|
1021
|
+
const NumberLinePositionSchema = z.object({
|
|
1022
|
+
type: z.literal(AST_TYPES.NUMBER_LINE_POSITION),
|
|
1023
|
+
value: z.number(),
|
|
1024
|
+
inlines: z.array(InlineNodeSchema).min(1)
|
|
1025
|
+
});
|
|
1026
|
+
function createNumberLinePosition(value, inlines) {
|
|
1027
|
+
const hasRealContent = inlines.some((n) => n.type !== AST_TYPES.SPACE);
|
|
1028
|
+
if (!hasRealContent) {
|
|
1029
|
+
throw new Error(`POSITION[value=${value}]: content cannot be empty or whitespace only`);
|
|
1030
|
+
}
|
|
1031
|
+
const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT);
|
|
1032
|
+
if (inputs.length > 1) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`POSITION[value=${value}]: only one input widget is allowed, got ${inputs.length}`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
return NumberLinePositionSchema.parse({
|
|
1038
|
+
type: AST_TYPES.NUMBER_LINE_POSITION,
|
|
1039
|
+
value,
|
|
1040
|
+
inlines
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
const NumberLineBlockSchema = z.object({
|
|
1044
|
+
type: z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
|
|
1045
|
+
min: z.number(),
|
|
1046
|
+
max: z.number(),
|
|
1047
|
+
step: z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
|
|
1048
|
+
minorTicks: z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
|
|
1049
|
+
style: z.enum(NUMBER_LINE_STYLES, {
|
|
1050
|
+
errorMap: () => ({
|
|
1051
|
+
message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
|
|
1052
|
+
})
|
|
1053
|
+
}).optional(),
|
|
1054
|
+
positions: z.array(NumberLinePositionSchema)
|
|
1055
|
+
}).refine((data) => data.min < data.max, {
|
|
1056
|
+
message: "NUMBER_LINE: min must be less than max"
|
|
1057
|
+
}).superRefine((data, ctx) => {
|
|
1058
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1059
|
+
for (const pos of data.positions) {
|
|
1060
|
+
if (pos.value < data.min || pos.value > data.max) {
|
|
1061
|
+
ctx.addIssue({
|
|
1062
|
+
code: z.ZodIssueCode.custom,
|
|
1063
|
+
message: `NUMBER_LINE: POSITION value (${pos.value}) must be within [${data.min}, ${data.max}]`
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (seen.has(pos.value)) {
|
|
1067
|
+
ctx.addIssue({
|
|
1068
|
+
code: z.ZodIssueCode.custom,
|
|
1069
|
+
message: `NUMBER_LINE: duplicate POSITION value (${pos.value})`
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
seen.add(pos.value);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
1076
|
+
return NumberLineBlockSchema.parse({
|
|
1077
|
+
type: AST_TYPES.NUMBER_LINE_BLOCK,
|
|
1078
|
+
min,
|
|
1079
|
+
max,
|
|
1080
|
+
step,
|
|
1081
|
+
minorTicks,
|
|
1082
|
+
style,
|
|
1083
|
+
positions
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
920
1086
|
const TableCellSchema = z.object({
|
|
921
1087
|
type: z.literal(AST_TYPES.TABLE_CELL),
|
|
922
1088
|
colspan: z.number().int().positive().optional(),
|
|
@@ -1031,13 +1197,13 @@ function isExpressionValidation(value) {
|
|
|
1031
1197
|
function isValidationNode(value) {
|
|
1032
1198
|
return ValidationNodeSchema.safeParse(value).success;
|
|
1033
1199
|
}
|
|
1034
|
-
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema]);
|
|
1200
|
+
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema]);
|
|
1035
1201
|
const ExerciseDocumentSchema = z.object({
|
|
1036
1202
|
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
1037
1203
|
question: TextBlockSchema.optional(),
|
|
1038
1204
|
lines: z.array(ContentBlockSchema),
|
|
1039
1205
|
tools: z.array(ToolNodeSchema).optional(),
|
|
1040
|
-
validation: ValidationNodeSchema
|
|
1206
|
+
validation: ValidationNodeSchema.optional()
|
|
1041
1207
|
});
|
|
1042
1208
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
1043
1209
|
return ExerciseDocumentSchema.parse({
|
|
@@ -1064,7 +1230,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1064
1230
|
const question = ctx.question ? this.visit(ctx.question) : void 0;
|
|
1065
1231
|
const contentBlocks = ctx.contentBlock ? ctx.contentBlock.map((cb) => this.visit(cb)) : [];
|
|
1066
1232
|
const tools = ctx.tool ? ctx.tool.map((t) => this.visit(t)) : [];
|
|
1067
|
-
const validation = this.visit(ctx.validation);
|
|
1233
|
+
const validation = ctx.validation ? this.visit(ctx.validation) : void 0;
|
|
1068
1234
|
return createExerciseDocument(question, contentBlocks, validation, tools);
|
|
1069
1235
|
}
|
|
1070
1236
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1081,6 +1247,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1081
1247
|
contentBlock(ctx) {
|
|
1082
1248
|
if (ctx.table) return this.visit(ctx.table);
|
|
1083
1249
|
if (ctx.radioButtons) return this.visit(ctx.radioButtons);
|
|
1250
|
+
if (ctx.numberLine) return this.visit(ctx.numberLine);
|
|
1084
1251
|
if (ctx.line) return this.visit(ctx.line);
|
|
1085
1252
|
throw new Error("Unknown content block");
|
|
1086
1253
|
}
|
|
@@ -1111,6 +1278,31 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1111
1278
|
return createTableCell(inlines, colspan);
|
|
1112
1279
|
}
|
|
1113
1280
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1281
|
+
// Number Line Block
|
|
1282
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1283
|
+
numberLine(ctx) {
|
|
1284
|
+
const params = this.visit(ctx.parameterList);
|
|
1285
|
+
if (params.min === void 0 || params.max === void 0) {
|
|
1286
|
+
throw new Error("NUMBER_LINE requires parameters: min and max");
|
|
1287
|
+
}
|
|
1288
|
+
const min = Number(params.min);
|
|
1289
|
+
const max = Number(params.max);
|
|
1290
|
+
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
1291
|
+
const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
|
|
1292
|
+
const style = params.style;
|
|
1293
|
+
const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
|
|
1294
|
+
return createNumberLineBlock(min, max, positions, step, minorTicks, style);
|
|
1295
|
+
}
|
|
1296
|
+
position(ctx) {
|
|
1297
|
+
const params = this.visit(ctx.parameterList);
|
|
1298
|
+
if (params.value === void 0) {
|
|
1299
|
+
throw new Error("POSITION requires parameter: value");
|
|
1300
|
+
}
|
|
1301
|
+
const value = Number(params.value);
|
|
1302
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
|
|
1303
|
+
return createNumberLinePosition(value, inlines);
|
|
1304
|
+
}
|
|
1305
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1114
1306
|
// Line Block
|
|
1115
1307
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1116
1308
|
line(ctx) {
|
|
@@ -1411,7 +1603,8 @@ const LayoutItemSchema = z.lazy(
|
|
|
1411
1603
|
LinebreakSchema,
|
|
1412
1604
|
GroupItemSchema,
|
|
1413
1605
|
OperatorItemSchema,
|
|
1414
|
-
TableItemSchema
|
|
1606
|
+
TableItemSchema,
|
|
1607
|
+
NumberLineItemSchema
|
|
1415
1608
|
])
|
|
1416
1609
|
);
|
|
1417
1610
|
const GroupItemSchema = z.object({
|
|
@@ -1436,6 +1629,22 @@ const TableItemSchema = z.object({
|
|
|
1436
1629
|
cols: z.number().int().positive(),
|
|
1437
1630
|
rows: z.array(TableRowItemSchema)
|
|
1438
1631
|
});
|
|
1632
|
+
const NumberLinePositionItemSchema = z.object({
|
|
1633
|
+
id: z.string(),
|
|
1634
|
+
type: z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1635
|
+
value: z.number(),
|
|
1636
|
+
content: z.array(z.lazy(() => LayoutItemSchema))
|
|
1637
|
+
});
|
|
1638
|
+
const NumberLineItemSchema = z.object({
|
|
1639
|
+
id: z.string(),
|
|
1640
|
+
type: z.literal(JSON_TYPES.NUMBER_LINE),
|
|
1641
|
+
min: z.number(),
|
|
1642
|
+
max: z.number(),
|
|
1643
|
+
step: z.number().positive(),
|
|
1644
|
+
minorTicks: z.number().int().nonnegative().optional(),
|
|
1645
|
+
style: z.enum(NUMBER_LINE_STYLES).optional(),
|
|
1646
|
+
positions: z.array(NumberLinePositionItemSchema)
|
|
1647
|
+
});
|
|
1439
1648
|
const StringInputJSONSchema = z.object({
|
|
1440
1649
|
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1441
1650
|
inputType: z.literal(INPUT_TYPES.STRING),
|
|
@@ -1486,7 +1695,7 @@ const ExerciseJSONSchema = z.object({
|
|
|
1486
1695
|
id: z.string().optional(),
|
|
1487
1696
|
layout: z.array(LayoutItemSchema),
|
|
1488
1697
|
widgets: z.record(WidgetJSONSchema),
|
|
1489
|
-
validation: ValidationJSONSchema,
|
|
1698
|
+
validation: ValidationJSONSchema.optional(),
|
|
1490
1699
|
meta: ExerciseMetaSchema.optional()
|
|
1491
1700
|
});
|
|
1492
1701
|
function validateExerciseJSON(data) {
|
|
@@ -1507,7 +1716,7 @@ function isQuestionText(item) {
|
|
|
1507
1716
|
function isTextItem$1(item) {
|
|
1508
1717
|
return item.type === JSON_TYPES.TEXT;
|
|
1509
1718
|
}
|
|
1510
|
-
function isWidgetRef(item) {
|
|
1719
|
+
function isWidgetRef$1(item) {
|
|
1511
1720
|
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1512
1721
|
}
|
|
1513
1722
|
function isHighlightedText(item) {
|
|
@@ -1576,6 +1785,8 @@ function toJSON(ast) {
|
|
|
1576
1785
|
for (const block of ast.lines) {
|
|
1577
1786
|
if (block.type === AST_TYPES.TABLE_BLOCK) {
|
|
1578
1787
|
layout.push(transformTable(block, idGenerator));
|
|
1788
|
+
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
1789
|
+
layout.push(transformNumberLine(block, idGenerator));
|
|
1579
1790
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
1580
1791
|
const radioBlock = block;
|
|
1581
1792
|
layout.push({
|
|
@@ -1641,13 +1852,12 @@ function toJSON(ast) {
|
|
|
1641
1852
|
}
|
|
1642
1853
|
}
|
|
1643
1854
|
}
|
|
1644
|
-
const validation = transformValidation(ast.validation, answers, widgets);
|
|
1645
1855
|
return {
|
|
1646
1856
|
version: "2.0",
|
|
1647
1857
|
// Natural DSL version
|
|
1648
1858
|
layout,
|
|
1649
1859
|
widgets,
|
|
1650
|
-
validation
|
|
1860
|
+
...ast.validation ? { validation: transformValidation(ast.validation, answers, widgets) } : {}
|
|
1651
1861
|
};
|
|
1652
1862
|
}
|
|
1653
1863
|
function collectAllInlines(blocks) {
|
|
@@ -1661,10 +1871,36 @@ function collectAllInlines(blocks) {
|
|
|
1661
1871
|
result.push(...cell.inlines);
|
|
1662
1872
|
}
|
|
1663
1873
|
}
|
|
1874
|
+
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
1875
|
+
for (const position of block.positions) {
|
|
1876
|
+
result.push(...position.inlines);
|
|
1877
|
+
}
|
|
1664
1878
|
}
|
|
1665
1879
|
}
|
|
1666
1880
|
return result;
|
|
1667
1881
|
}
|
|
1882
|
+
function transformNumberLine(numberLine, idGenerator) {
|
|
1883
|
+
const positions = numberLine.positions.map((pos) => {
|
|
1884
|
+
const content = pos.inlines.map((inline) => transformInline(inline, idGenerator));
|
|
1885
|
+
return {
|
|
1886
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1887
|
+
type: JSON_TYPES.NUMBER_LINE_POSITION,
|
|
1888
|
+
value: pos.value,
|
|
1889
|
+
content
|
|
1890
|
+
};
|
|
1891
|
+
});
|
|
1892
|
+
const item = {
|
|
1893
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
|
|
1894
|
+
type: JSON_TYPES.NUMBER_LINE,
|
|
1895
|
+
min: numberLine.min,
|
|
1896
|
+
max: numberLine.max,
|
|
1897
|
+
step: numberLine.step,
|
|
1898
|
+
positions
|
|
1899
|
+
};
|
|
1900
|
+
if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
|
|
1901
|
+
if (numberLine.style !== void 0) item.style = numberLine.style;
|
|
1902
|
+
return item;
|
|
1903
|
+
}
|
|
1668
1904
|
function transformTable(table, idGenerator) {
|
|
1669
1905
|
const rows = table.rows.map((row) => {
|
|
1670
1906
|
const cells = row.cells.map((cell) => {
|
|
@@ -2092,11 +2328,18 @@ function findValidCombination(expression, ranges) {
|
|
|
2092
2328
|
return null;
|
|
2093
2329
|
}
|
|
2094
2330
|
function validate(exercise, userInputs) {
|
|
2095
|
-
|
|
2331
|
+
if (!exercise.validation) {
|
|
2332
|
+
return {
|
|
2333
|
+
isCorrect: true,
|
|
2334
|
+
details: {}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
const validatedExercise = exercise;
|
|
2338
|
+
switch (validatedExercise.validation.mode) {
|
|
2096
2339
|
case VALIDATION_MODES.SIMPLE:
|
|
2097
|
-
return validateSimple(
|
|
2340
|
+
return validateSimple(validatedExercise, userInputs);
|
|
2098
2341
|
case VALIDATION_MODES.EXPRESSION:
|
|
2099
|
-
return validateExpression(
|
|
2342
|
+
return validateExpression(validatedExercise, userInputs);
|
|
2100
2343
|
default:
|
|
2101
2344
|
return {
|
|
2102
2345
|
isCorrect: false,
|
|
@@ -2107,6 +2350,9 @@ function validate(exercise, userInputs) {
|
|
|
2107
2350
|
}
|
|
2108
2351
|
function getMissingInputs(exercise, userInputs) {
|
|
2109
2352
|
const missing = [];
|
|
2353
|
+
if (!exercise.validation) {
|
|
2354
|
+
return missing;
|
|
2355
|
+
}
|
|
2110
2356
|
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
2111
2357
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
2112
2358
|
if (!(widgetId in userInputs) || userInputs[widgetId] === "") {
|
|
@@ -2146,6 +2392,8 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
2146
2392
|
const DragItemTypeSchema = z.enum([
|
|
2147
2393
|
"drag-option",
|
|
2148
2394
|
// Draggable answer options
|
|
2395
|
+
"placed-chip",
|
|
2396
|
+
// Chip already placed on a number-line axis (re-drag)
|
|
2149
2397
|
"ruler-handle",
|
|
2150
2398
|
// Future: ruler endpoints
|
|
2151
2399
|
"shape",
|
|
@@ -5857,7 +6105,8 @@ const ExerciseDndProvider = ({
|
|
|
5857
6105
|
}, [dropHandlers]);
|
|
5858
6106
|
const handleDragStart = (event) => {
|
|
5859
6107
|
const { active } = event;
|
|
5860
|
-
const
|
|
6108
|
+
const raw = active.data.current;
|
|
6109
|
+
const item = raw?.type === "placed-chip" ? { id: String(active.id), type: "placed-chip", data: { label: raw.label } } : raw;
|
|
5861
6110
|
setActiveItem(item);
|
|
5862
6111
|
};
|
|
5863
6112
|
const handleDragEnd = (event) => {
|
|
@@ -5908,6 +6157,10 @@ const DragOverlayContent = ({ item }) => {
|
|
|
5908
6157
|
const data = item.data;
|
|
5909
6158
|
return /* @__PURE__ */ jsx("div", { className: "drag-overlay-option", children: data.label });
|
|
5910
6159
|
}
|
|
6160
|
+
case "placed-chip": {
|
|
6161
|
+
const label = item.data?.label ?? "";
|
|
6162
|
+
return /* @__PURE__ */ jsx("div", { className: "drag-overlay-option", children: label });
|
|
6163
|
+
}
|
|
5911
6164
|
default:
|
|
5912
6165
|
return null;
|
|
5913
6166
|
}
|
|
@@ -6366,6 +6619,334 @@ const WidgetRenderer = ({
|
|
|
6366
6619
|
}
|
|
6367
6620
|
return null;
|
|
6368
6621
|
};
|
|
6622
|
+
const cleanFloat = (n) => parseFloat(n.toFixed(10));
|
|
6623
|
+
const toPercent = (value, min, max) => {
|
|
6624
|
+
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
6625
|
+
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
6626
|
+
};
|
|
6627
|
+
const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
|
|
6628
|
+
const relX = (chipCenterX - axisLeft) / axisWidth;
|
|
6629
|
+
const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
|
|
6630
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
6631
|
+
return min + clamped * (max - min);
|
|
6632
|
+
};
|
|
6633
|
+
const getMajorTicks = (min, max, step) => {
|
|
6634
|
+
if (step <= 0 || max <= min) return [min, max];
|
|
6635
|
+
const count = Math.floor((max - min) / step);
|
|
6636
|
+
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
6637
|
+
const lastTick = ticks[ticks.length - 1];
|
|
6638
|
+
if (Math.abs(lastTick - max) > 1e-9) ticks.push(max);
|
|
6639
|
+
return ticks;
|
|
6640
|
+
};
|
|
6641
|
+
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
6642
|
+
if (minorTicks <= 0) return [];
|
|
6643
|
+
const result = [];
|
|
6644
|
+
majorTicks.slice(0, -1).forEach((tick, idx) => {
|
|
6645
|
+
const next = majorTicks[idx + 1];
|
|
6646
|
+
const interval = next - tick;
|
|
6647
|
+
for (let j = 1; j <= minorTicks; j++) {
|
|
6648
|
+
result.push(cleanFloat(tick + j / (minorTicks + 1) * interval));
|
|
6649
|
+
}
|
|
6650
|
+
});
|
|
6651
|
+
return result;
|
|
6652
|
+
};
|
|
6653
|
+
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
6654
|
+
...majorTicks,
|
|
6655
|
+
...getMinorTickValues(majorTicks, minorTicks)
|
|
6656
|
+
];
|
|
6657
|
+
const snapToNearest = (raw, ticks) => {
|
|
6658
|
+
if (ticks.length === 0) return cleanFloat(raw);
|
|
6659
|
+
const snapped = ticks.reduce(
|
|
6660
|
+
(best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
|
|
6661
|
+
);
|
|
6662
|
+
return cleanFloat(snapped);
|
|
6663
|
+
};
|
|
6664
|
+
const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
|
|
6665
|
+
const getPositionWidgetId = (content) => {
|
|
6666
|
+
for (const item of content) {
|
|
6667
|
+
if (isWidgetRef(item)) return item.widgetId;
|
|
6668
|
+
}
|
|
6669
|
+
return null;
|
|
6670
|
+
};
|
|
6671
|
+
const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
|
|
6672
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
6673
|
+
id: widgetId,
|
|
6674
|
+
data: { type: "placed-chip", widgetId, label }
|
|
6675
|
+
});
|
|
6676
|
+
return /* @__PURE__ */ jsx(
|
|
6677
|
+
"div",
|
|
6678
|
+
{
|
|
6679
|
+
className: "number-line-placed-chip",
|
|
6680
|
+
style: { left: `${leftPercent}%`, opacity: isDragging ? 0.3 : 1 },
|
|
6681
|
+
children: /* @__PURE__ */ jsx(
|
|
6682
|
+
"button",
|
|
6683
|
+
{
|
|
6684
|
+
ref: setNodeRef,
|
|
6685
|
+
className: "number-line-placed-chip-btn",
|
|
6686
|
+
title: "Drag to move · Click to remove",
|
|
6687
|
+
onClick: onRemove,
|
|
6688
|
+
...listeners,
|
|
6689
|
+
...attributes,
|
|
6690
|
+
children: label
|
|
6691
|
+
}
|
|
6692
|
+
)
|
|
6693
|
+
}
|
|
6694
|
+
);
|
|
6695
|
+
};
|
|
6696
|
+
const FreeDragAxis = ({
|
|
6697
|
+
axisDropId,
|
|
6698
|
+
min,
|
|
6699
|
+
max,
|
|
6700
|
+
minorTicks,
|
|
6701
|
+
majorTicks,
|
|
6702
|
+
showArrowLeft,
|
|
6703
|
+
showArrowRight,
|
|
6704
|
+
positions,
|
|
6705
|
+
values,
|
|
6706
|
+
onChange
|
|
6707
|
+
}) => {
|
|
6708
|
+
const axisRef = useRef(null);
|
|
6709
|
+
const snapTicks = useMemo(
|
|
6710
|
+
() => getSnapTicks(majorTicks, minorTicks),
|
|
6711
|
+
[majorTicks, minorTicks]
|
|
6712
|
+
);
|
|
6713
|
+
const [dropLabels, setDropLabels] = useState(/* @__PURE__ */ new Map());
|
|
6714
|
+
const [dropSources, setDropSources] = useState(/* @__PURE__ */ new Map());
|
|
6715
|
+
useEffect(() => {
|
|
6716
|
+
setDropLabels((prev) => {
|
|
6717
|
+
let changed = false;
|
|
6718
|
+
const next = new Map(prev);
|
|
6719
|
+
for (const [wid] of next) {
|
|
6720
|
+
if (!values[wid]) {
|
|
6721
|
+
next.delete(wid);
|
|
6722
|
+
changed = true;
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
return changed ? next : prev;
|
|
6726
|
+
});
|
|
6727
|
+
setDropSources((prev) => {
|
|
6728
|
+
let changed = false;
|
|
6729
|
+
const next = new Map(prev);
|
|
6730
|
+
for (const [wid] of next) {
|
|
6731
|
+
if (!values[wid]) {
|
|
6732
|
+
next.delete(wid);
|
|
6733
|
+
changed = true;
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
return changed ? next : prev;
|
|
6737
|
+
});
|
|
6738
|
+
}, [values]);
|
|
6739
|
+
const { setNodeRef, isOver } = useDroppable({ id: axisDropId });
|
|
6740
|
+
const mergedRef = useCallback((node) => {
|
|
6741
|
+
axisRef.current = node;
|
|
6742
|
+
setNodeRef(node);
|
|
6743
|
+
}, [setNodeRef]);
|
|
6744
|
+
const snapFromEvent = useCallback((translated) => {
|
|
6745
|
+
if (!axisRef.current) return null;
|
|
6746
|
+
const axisRect = axisRef.current.getBoundingClientRect();
|
|
6747
|
+
const chipCenterX = translated.left + translated.width / 2;
|
|
6748
|
+
const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
|
|
6749
|
+
return snapToNearest(raw, snapTicks);
|
|
6750
|
+
}, [min, max, snapTicks]);
|
|
6751
|
+
useDndMonitor({
|
|
6752
|
+
onDragEnd(event) {
|
|
6753
|
+
if (!event.over || event.over.id !== axisDropId) return;
|
|
6754
|
+
if (!axisRef.current) return;
|
|
6755
|
+
const translated = event.active.rect.current.translated;
|
|
6756
|
+
if (!translated) return;
|
|
6757
|
+
const placed = snapFromEvent(translated);
|
|
6758
|
+
if (placed === null) return;
|
|
6759
|
+
const data = event.active.data.current;
|
|
6760
|
+
if (data?.type === "placed-chip") {
|
|
6761
|
+
const widgetId = data.widgetId;
|
|
6762
|
+
const label = data.label;
|
|
6763
|
+
const originalSourceId = dropSources.get(widgetId) ?? widgetId;
|
|
6764
|
+
onChange(widgetId, String(placed), true, originalSourceId);
|
|
6765
|
+
setDropLabels((prev) => new Map(prev).set(widgetId, label));
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
const draggedItem = event.active.data.current;
|
|
6769
|
+
if (draggedItem?.type !== "drag-option") return;
|
|
6770
|
+
const targetPos = positions.find((pos) => {
|
|
6771
|
+
const wid = getPositionWidgetId(pos.content);
|
|
6772
|
+
return wid && !values[wid];
|
|
6773
|
+
});
|
|
6774
|
+
if (!targetPos) return;
|
|
6775
|
+
const targetWidgetId = getPositionWidgetId(targetPos.content);
|
|
6776
|
+
if (!targetWidgetId) return;
|
|
6777
|
+
const chipLabel = draggedItem.data.value;
|
|
6778
|
+
const sourceOptId = String(event.active.id);
|
|
6779
|
+
onChange(targetWidgetId, String(placed), true, sourceOptId);
|
|
6780
|
+
setDropLabels((prev) => new Map(prev).set(targetWidgetId, chipLabel));
|
|
6781
|
+
setDropSources((prev) => new Map(prev).set(targetWidgetId, sourceOptId));
|
|
6782
|
+
}
|
|
6783
|
+
});
|
|
6784
|
+
return /* @__PURE__ */ jsxs(
|
|
6785
|
+
"div",
|
|
6786
|
+
{
|
|
6787
|
+
ref: mergedRef,
|
|
6788
|
+
className: `number-line-axis-row nl-axis-drag-mode${isOver ? " nl-axis-drag-over" : ""}`,
|
|
6789
|
+
children: [
|
|
6790
|
+
/* @__PURE__ */ jsx("div", { className: "number-line-track" }),
|
|
6791
|
+
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
|
|
6792
|
+
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
|
|
6793
|
+
getMinorTickValues(majorTicks, minorTicks).map((v) => /* @__PURE__ */ jsx(
|
|
6794
|
+
"div",
|
|
6795
|
+
{
|
|
6796
|
+
className: "number-line-minor-tick",
|
|
6797
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
6798
|
+
},
|
|
6799
|
+
`minor-${v}`
|
|
6800
|
+
)),
|
|
6801
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsx(
|
|
6802
|
+
"div",
|
|
6803
|
+
{
|
|
6804
|
+
className: "number-line-major-tick",
|
|
6805
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6806
|
+
},
|
|
6807
|
+
`major-${tick}`
|
|
6808
|
+
)),
|
|
6809
|
+
positions.map((pos) => {
|
|
6810
|
+
const wid = getPositionWidgetId(pos.content);
|
|
6811
|
+
const dropPos = wid ? parseFloat(values[wid] ?? "") : NaN;
|
|
6812
|
+
const label = wid ? dropLabels.get(wid) : void 0;
|
|
6813
|
+
if (!wid || isNaN(dropPos) || !label) return null;
|
|
6814
|
+
return /* @__PURE__ */ jsx(
|
|
6815
|
+
PlacedChip,
|
|
6816
|
+
{
|
|
6817
|
+
widgetId: wid,
|
|
6818
|
+
label,
|
|
6819
|
+
leftPercent: toPercent(dropPos, min, max),
|
|
6820
|
+
onRemove: () => {
|
|
6821
|
+
const sourceId = dropSources.get(wid);
|
|
6822
|
+
onChange(wid, "", false, sourceId);
|
|
6823
|
+
setDropLabels((prev) => {
|
|
6824
|
+
const m = new Map(prev);
|
|
6825
|
+
m.delete(wid);
|
|
6826
|
+
return m;
|
|
6827
|
+
});
|
|
6828
|
+
setDropSources((prev) => {
|
|
6829
|
+
const m = new Map(prev);
|
|
6830
|
+
m.delete(wid);
|
|
6831
|
+
return m;
|
|
6832
|
+
});
|
|
6833
|
+
}
|
|
6834
|
+
},
|
|
6835
|
+
wid
|
|
6836
|
+
);
|
|
6837
|
+
})
|
|
6838
|
+
]
|
|
6839
|
+
}
|
|
6840
|
+
);
|
|
6841
|
+
};
|
|
6842
|
+
const NumberLineRenderer = ({
|
|
6843
|
+
item,
|
|
6844
|
+
widgets,
|
|
6845
|
+
values,
|
|
6846
|
+
droppedInputs,
|
|
6847
|
+
onChange,
|
|
6848
|
+
onInputFocus
|
|
6849
|
+
}) => {
|
|
6850
|
+
const { min, max, step, minorTicks = 0, style = "arrow-right", positions } = item;
|
|
6851
|
+
const majorTicks = useMemo(() => getMajorTicks(min, max, step), [min, max, step]);
|
|
6852
|
+
const minorValues = useMemo(() => getMinorTickValues(majorTicks, minorTicks), [majorTicks, minorTicks]);
|
|
6853
|
+
const showArrowRight = style === "arrow-right" || style === "arrow-both";
|
|
6854
|
+
const showArrowLeft = style === "arrow-left" || style === "arrow-both";
|
|
6855
|
+
const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
|
|
6856
|
+
const inputTickSet = useMemo(() => {
|
|
6857
|
+
if (isDragMode) return /* @__PURE__ */ new Set();
|
|
6858
|
+
return new Set(
|
|
6859
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
|
|
6860
|
+
);
|
|
6861
|
+
}, [isDragMode, positions]);
|
|
6862
|
+
return /* @__PURE__ */ jsxs(
|
|
6863
|
+
"div",
|
|
6864
|
+
{
|
|
6865
|
+
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
|
|
6866
|
+
style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
|
|
6867
|
+
role: "img",
|
|
6868
|
+
"aria-label": `Number line from ${min} to ${max}`,
|
|
6869
|
+
children: [
|
|
6870
|
+
!isDragMode && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: positions.map((pos) => /* @__PURE__ */ jsx(
|
|
6871
|
+
"div",
|
|
6872
|
+
{
|
|
6873
|
+
className: "number-line-position-content",
|
|
6874
|
+
style: { left: `${toPercent(pos.value, min, max)}%` },
|
|
6875
|
+
children: pos.content.map((subItem) => /* @__PURE__ */ jsx(
|
|
6876
|
+
LayoutRenderer,
|
|
6877
|
+
{
|
|
6878
|
+
item: subItem,
|
|
6879
|
+
widgets,
|
|
6880
|
+
values,
|
|
6881
|
+
droppedInputs,
|
|
6882
|
+
onChange,
|
|
6883
|
+
onInputFocus
|
|
6884
|
+
},
|
|
6885
|
+
subItem.id
|
|
6886
|
+
))
|
|
6887
|
+
},
|
|
6888
|
+
pos.id
|
|
6889
|
+
)) }),
|
|
6890
|
+
isDragMode ? /* @__PURE__ */ jsx(
|
|
6891
|
+
FreeDragAxis,
|
|
6892
|
+
{
|
|
6893
|
+
axisDropId: `${item.id}--nl-axis`,
|
|
6894
|
+
min,
|
|
6895
|
+
max,
|
|
6896
|
+
minorTicks,
|
|
6897
|
+
majorTicks,
|
|
6898
|
+
showArrowLeft,
|
|
6899
|
+
showArrowRight,
|
|
6900
|
+
positions,
|
|
6901
|
+
values,
|
|
6902
|
+
onChange
|
|
6903
|
+
}
|
|
6904
|
+
) : /* @__PURE__ */ jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
|
|
6905
|
+
/* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
|
|
6906
|
+
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
6907
|
+
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
6908
|
+
minorValues.map((v) => /* @__PURE__ */ jsx(
|
|
6909
|
+
"div",
|
|
6910
|
+
{
|
|
6911
|
+
className: "number-line-minor-tick",
|
|
6912
|
+
"aria-hidden": "true",
|
|
6913
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
6914
|
+
},
|
|
6915
|
+
`minor-${v}`
|
|
6916
|
+
)),
|
|
6917
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsx(
|
|
6918
|
+
"div",
|
|
6919
|
+
{
|
|
6920
|
+
className: "number-line-major-tick",
|
|
6921
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6922
|
+
},
|
|
6923
|
+
`major-${tick}`
|
|
6924
|
+
)),
|
|
6925
|
+
positions.map((pos) => /* @__PURE__ */ jsx(
|
|
6926
|
+
"div",
|
|
6927
|
+
{
|
|
6928
|
+
className: "number-line-position-dot",
|
|
6929
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
6930
|
+
},
|
|
6931
|
+
`dot-${pos.id}`
|
|
6932
|
+
))
|
|
6933
|
+
] }),
|
|
6934
|
+
/* @__PURE__ */ jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
|
|
6935
|
+
const hasInputAtTick = inputTickSet.has(tick);
|
|
6936
|
+
return /* @__PURE__ */ jsx(
|
|
6937
|
+
"span",
|
|
6938
|
+
{
|
|
6939
|
+
className: "number-line-tick-label",
|
|
6940
|
+
style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
|
|
6941
|
+
children: tick
|
|
6942
|
+
},
|
|
6943
|
+
`lbl-${tick}`
|
|
6944
|
+
);
|
|
6945
|
+
}) })
|
|
6946
|
+
]
|
|
6947
|
+
}
|
|
6948
|
+
);
|
|
6949
|
+
};
|
|
6369
6950
|
function isArabic(text) {
|
|
6370
6951
|
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
6371
6952
|
}
|
|
@@ -6533,6 +7114,18 @@ const LayoutRenderer = ({
|
|
|
6533
7114
|
},
|
|
6534
7115
|
cell.id
|
|
6535
7116
|
)) }, row.id)) }) }) });
|
|
7117
|
+
case JSON_TYPES.NUMBER_LINE:
|
|
7118
|
+
return /* @__PURE__ */ jsx(
|
|
7119
|
+
NumberLineRenderer,
|
|
7120
|
+
{
|
|
7121
|
+
item,
|
|
7122
|
+
widgets,
|
|
7123
|
+
values,
|
|
7124
|
+
droppedInputs,
|
|
7125
|
+
onChange,
|
|
7126
|
+
onInputFocus
|
|
7127
|
+
}
|
|
7128
|
+
);
|
|
6536
7129
|
default:
|
|
6537
7130
|
return null;
|
|
6538
7131
|
}
|
|
@@ -6620,6 +7213,20 @@ const ExerciseRenderer = forwardRef(({
|
|
|
6620
7213
|
});
|
|
6621
7214
|
continue;
|
|
6622
7215
|
}
|
|
7216
|
+
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
7217
|
+
if (currentGroup.length > 0) {
|
|
7218
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
7219
|
+
const subGroups = groupLineItems(currentGroup);
|
|
7220
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
7221
|
+
currentGroup = [];
|
|
7222
|
+
}
|
|
7223
|
+
groups.push({
|
|
7224
|
+
items: [item],
|
|
7225
|
+
hasArabic: false,
|
|
7226
|
+
subGroups: []
|
|
7227
|
+
});
|
|
7228
|
+
continue;
|
|
7229
|
+
}
|
|
6623
7230
|
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
6624
7231
|
if (currentGroup.length > 0) {
|
|
6625
7232
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
@@ -6871,7 +7478,7 @@ export {
|
|
|
6871
7478
|
isToolNode,
|
|
6872
7479
|
isValidExpression,
|
|
6873
7480
|
isValidationNode,
|
|
6874
|
-
isWidgetRef,
|
|
7481
|
+
isWidgetRef$1 as isWidgetRef,
|
|
6875
7482
|
parse,
|
|
6876
7483
|
parseSafe,
|
|
6877
7484
|
renderExercise,
|