@knitting-tools/core 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2572 @@
1
+ // src/constants/units.ts
2
+ var INCH_TO_CM = 2.54;
3
+ var CM_TO_INCH = 1 / INCH_TO_CM;
4
+ var MM_TO_CM = 0.1;
5
+ var CM_TO_MM = 10;
6
+
7
+ // src/gauge/gauge.ts
8
+ var unitToCm = (value, unit) => {
9
+ if (unit === "metric") return value / 10;
10
+ else return value / 4 / INCH_TO_CM;
11
+ };
12
+ function stitchesPerCm(gauge) {
13
+ if (gauge.unit === "metric") {
14
+ return unitToCm(gauge.stitchesPer10cm, "metric");
15
+ }
16
+ return unitToCm(gauge.stitchesPer4in, "imperial");
17
+ }
18
+ function rowsPerCm(gauge) {
19
+ if (gauge.unit === "metric") {
20
+ return unitToCm(gauge.rowsPer10cm, "metric");
21
+ }
22
+ return unitToCm(gauge.rowsPer4in, "imperial");
23
+ }
24
+
25
+ // src/gauge/conversion.ts
26
+ function cmToStitches(cm, gauge) {
27
+ return Math.round(stitchesPerCm(gauge) * cm);
28
+ }
29
+ function cmToRows(cm, gauge) {
30
+ return Math.round(rowsPerCm(gauge) * cm);
31
+ }
32
+
33
+ // src/measurement/measurement.ts
34
+ function convertLength(value, from, to) {
35
+ if (from === to) {
36
+ return value;
37
+ }
38
+ if (from === "cm" && to === "in") {
39
+ return value * CM_TO_INCH;
40
+ }
41
+ if (from === "in" && to === "cm") {
42
+ return value * INCH_TO_CM;
43
+ }
44
+ throw new Error(`Unsupported conversion from ${from} \u2192 ${to}`);
45
+ }
46
+ function normaliseMeasurement(value, unit) {
47
+ return convertLength(value, unit, "cm");
48
+ }
49
+ function roundStitches(stitches) {
50
+ if (!Number.isFinite(stitches)) {
51
+ throw new Error("Stitch count must be a finite number.");
52
+ }
53
+ return Math.round(stitches);
54
+ }
55
+
56
+ // src/pattern/row.ts
57
+ var InstructionBuffer = class {
58
+ items = [];
59
+ append(instruction, count = instruction.count) {
60
+ this.items[this.items.length] = cloneInstruction(instruction, count);
61
+ }
62
+ toArray() {
63
+ return this.items;
64
+ }
65
+ };
66
+ var SHAPING_CONSUME = {
67
+ decrease: 2,
68
+ increase: 1
69
+ };
70
+ var SHAPING_PRODUCE = {
71
+ decrease: 1,
72
+ increase: 2
73
+ };
74
+ function isRecord(value) {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
76
+ }
77
+ function isRepeatBlock(element) {
78
+ return element.type === "repeat";
79
+ }
80
+ function isShapingElement(element) {
81
+ return element.type === "shaping";
82
+ }
83
+ function isEdgeShapingElement(element) {
84
+ return element.type === "shaping";
85
+ }
86
+ function isStitchInstruction(element) {
87
+ return !isRepeatBlock(element) && !isShapingElement(element);
88
+ }
89
+ function countEdgeSourceStitches(stitches) {
90
+ return (stitches == null ? void 0 : stitches.reduce(
91
+ (sum, stitch) => sum + (isEdgeShapingElement(stitch) ? stitch.count * SHAPING_CONSUME[stitch.shapingType] : stitch.count),
92
+ 0
93
+ )) ?? 0;
94
+ }
95
+ function countSequenceStitches(elements) {
96
+ let total = 0;
97
+ for (const element of elements) {
98
+ if (isStitchInstruction(element)) {
99
+ total += element.count;
100
+ continue;
101
+ }
102
+ if (isShapingElement(element)) {
103
+ total += element.count * SHAPING_CONSUME[element.shapingType];
104
+ continue;
105
+ }
106
+ if (element.times !== void 0) {
107
+ const inner = countSequenceStitches(element.sequence);
108
+ if (inner === null) {
109
+ return null;
110
+ }
111
+ total += element.times * inner;
112
+ continue;
113
+ }
114
+ return null;
115
+ }
116
+ return total;
117
+ }
118
+ function cloneVariantParameters(variantParameters) {
119
+ return variantParameters ? { ...variantParameters } : void 0;
120
+ }
121
+ function cloneInstruction(instruction, count = instruction.count) {
122
+ return {
123
+ count,
124
+ type: instruction.type,
125
+ variant: instruction.variant,
126
+ variantParameters: cloneVariantParameters(instruction.variantParameters)
127
+ };
128
+ }
129
+ function inferDecreaseVariant(element) {
130
+ const isPurl = element.stitch === "purl";
131
+ switch (element.direction) {
132
+ case "left":
133
+ return isPurl ? "ssp" : "ssk";
134
+ case "right":
135
+ return isPurl ? "p2tog" : "k2tog";
136
+ default:
137
+ return isPurl ? "p2tog" : void 0;
138
+ }
139
+ }
140
+ function inferIncreaseVariant(element) {
141
+ switch (element.direction) {
142
+ case "left":
143
+ return "inv-l";
144
+ case "right":
145
+ return "inv-r";
146
+ default:
147
+ return void 0;
148
+ }
149
+ }
150
+ function inferShapingVariant(element) {
151
+ if (element.shapingType === "decrease") {
152
+ return inferDecreaseVariant(element);
153
+ }
154
+ return inferIncreaseVariant(element);
155
+ }
156
+ function createInstructionFromShaping(element, operationCount) {
157
+ const inferredVariant = element.variant ?? inferShapingVariant(element);
158
+ return {
159
+ count: operationCount * SHAPING_PRODUCE[element.shapingType],
160
+ type: element.shapingType,
161
+ variant: inferredVariant,
162
+ variantParameters: cloneVariantParameters(element.variantParameters)
163
+ };
164
+ }
165
+ function areVariantParametersEqual(left, right) {
166
+ if (left === right) {
167
+ return true;
168
+ }
169
+ if (left === void 0 || right === void 0) {
170
+ return false;
171
+ }
172
+ const leftKeys = Object.keys(left);
173
+ const rightKeys = Object.keys(right);
174
+ if (leftKeys.length !== rightKeys.length) {
175
+ return false;
176
+ }
177
+ for (const key of leftKeys) {
178
+ if (!(key in right) || left[key] !== right[key]) {
179
+ return false;
180
+ }
181
+ }
182
+ return true;
183
+ }
184
+ function canMergeInstructions(left, right) {
185
+ return left.type === right.type && left.variant === right.variant && areVariantParametersEqual(left.variantParameters, right.variantParameters);
186
+ }
187
+ function assertPositiveInteger(value, label) {
188
+ if (!Number.isInteger(value) || value <= 0) {
189
+ throw new Error(`${label} must be a positive integer. Received ${String(value)}.`);
190
+ }
191
+ }
192
+ function assertNonNegativeInteger(value, label) {
193
+ if (!Number.isInteger(value) || value < 0) {
194
+ throw new Error(`${label} must be a non-negative integer. Received ${String(value)}.`);
195
+ }
196
+ }
197
+ function validateVariantParameters(variantParameters, path) {
198
+ if (variantParameters === void 0) {
199
+ return;
200
+ }
201
+ if (!isRecord(variantParameters)) {
202
+ throw new Error(`${path} must be a plain object when provided.`);
203
+ }
204
+ for (const [key, value] of Object.entries(variantParameters)) {
205
+ const valueType = typeof value;
206
+ if (valueType !== "string" && valueType !== "boolean" && (valueType !== "number" || !Number.isFinite(value))) {
207
+ throw new Error(
208
+ `${path}.${key} must be a string, finite number, or boolean. Received ${String(value)}.`
209
+ );
210
+ }
211
+ }
212
+ }
213
+ function validateStitchInstruction(instruction, path) {
214
+ if (!isRecord(instruction)) {
215
+ throw new Error(`${path} must be an object.`);
216
+ }
217
+ assertPositiveInteger(instruction.count, `${path}.count`);
218
+ validateVariantParameters(instruction.variantParameters, `${path}.variantParameters`);
219
+ }
220
+ function validateShapingElement(element, path) {
221
+ if (!isRecord(element)) {
222
+ throw new Error(`${path} must be an object.`);
223
+ }
224
+ if (element.shapingType !== "decrease" && element.shapingType !== "increase") {
225
+ throw new Error(`${path}.shapingType must be 'decrease' or 'increase'.`);
226
+ }
227
+ if (element.direction !== void 0 && element.direction !== "left" && element.direction !== "right" && element.direction !== "centre") {
228
+ throw new Error(`${path}.direction must be 'left', 'right', or 'centre' when provided.`);
229
+ }
230
+ if (element.stitch !== void 0 && element.stitch !== "knit" && element.stitch !== "purl") {
231
+ throw new Error(`${path}.stitch must be 'knit' or 'purl' when provided.`);
232
+ }
233
+ assertPositiveInteger(element.count, `${path}.count`);
234
+ validateVariantParameters(element.variantParameters, `${path}.variantParameters`);
235
+ }
236
+ function assertRepeatPositiveInteger(value, label) {
237
+ if (typeof value !== "number") {
238
+ throw new TypeError(`${label} must be a positive integer.`);
239
+ }
240
+ assertPositiveInteger(value, label);
241
+ return value;
242
+ }
243
+ function assertOptionalRepeatPositiveInteger(value, label) {
244
+ if (value === void 0) {
245
+ return void 0;
246
+ }
247
+ return assertRepeatPositiveInteger(value, label);
248
+ }
249
+ function getRepeatMode(repeat, path) {
250
+ const repeatRecord = repeat;
251
+ const hasTimes = repeatRecord.times !== void 0;
252
+ const hasToEnd = repeatRecord.toEnd !== void 0;
253
+ const hasTargetStitches = repeatRecord.targetStitches !== void 0;
254
+ const hasRemainingStitches = repeatRecord.remainingStitches !== void 0;
255
+ const modes = [hasTimes, hasToEnd, hasTargetStitches, hasRemainingStitches].filter(
256
+ Boolean
257
+ ).length;
258
+ if (modes !== 1) {
259
+ throw new Error(
260
+ `${path} must define exactly one termination mode: times, toEnd, targetStitches, or remainingStitches.`
261
+ );
262
+ }
263
+ const assertMaxRepeats = () => {
264
+ assertOptionalRepeatPositiveInteger(repeatRecord.maxRepeats, `${path}.maxRepeats`);
265
+ };
266
+ if (hasTimes) {
267
+ assertRepeatPositiveInteger(repeatRecord.times, `${path}.times`);
268
+ if (repeatRecord.maxRepeats !== void 0) {
269
+ throw new Error(`${path}.maxRepeats cannot be combined with fixed times.`);
270
+ }
271
+ return "times";
272
+ }
273
+ if (hasToEnd) {
274
+ if (repeatRecord.toEnd !== true) {
275
+ throw new Error(`${path}.toEnd must be true when provided.`);
276
+ }
277
+ if (repeatRecord.allowPartial !== void 0 && typeof repeatRecord.allowPartial !== "boolean") {
278
+ throw new Error(`${path}.allowPartial must be a boolean when provided.`);
279
+ }
280
+ assertMaxRepeats();
281
+ return "to-end";
282
+ }
283
+ if (hasRemainingStitches) {
284
+ assertRepeatPositiveInteger(repeatRecord.remainingStitches, `${path}.remainingStitches`);
285
+ assertMaxRepeats();
286
+ return "until-remaining";
287
+ }
288
+ assertRepeatPositiveInteger(repeatRecord.targetStitches, `${path}.targetStitches`);
289
+ assertMaxRepeats();
290
+ return "until-stitches";
291
+ }
292
+ function validateRowElements(elements, path, isNested = false) {
293
+ if (!Array.isArray(elements)) {
294
+ throw new TypeError(`${path} must be an array.`);
295
+ }
296
+ for (let index = 0; index < elements.length; index += 1) {
297
+ const element = elements[index];
298
+ const elementPath = `${path}[${index}]`;
299
+ if (isStitchInstruction(element)) {
300
+ validateStitchInstruction(element, elementPath);
301
+ continue;
302
+ }
303
+ if (isShapingElement(element)) {
304
+ validateShapingElement(element, elementPath);
305
+ continue;
306
+ }
307
+ if (!isRecord(element)) {
308
+ throw new Error(`${elementPath} must be an object.`);
309
+ }
310
+ if (!Array.isArray(element.sequence) || element.sequence.length === 0) {
311
+ throw new Error(`${elementPath}.sequence must be a non-empty array.`);
312
+ }
313
+ getRepeatMode(element, elementPath);
314
+ if (isNested && "remainingStitches" in element && element.remainingStitches !== void 0) {
315
+ throw new Error(
316
+ `${elementPath}.remainingStitches can only be used in top-level repeats, not nested repeats.`
317
+ );
318
+ }
319
+ validateRowElements(element.sequence, `${elementPath}.sequence`, true);
320
+ }
321
+ }
322
+ function validateEdgeElements(stitches, path) {
323
+ if (stitches === void 0) {
324
+ return;
325
+ }
326
+ if (!Array.isArray(stitches)) {
327
+ throw new TypeError(`${path} must be an array when provided.`);
328
+ }
329
+ for (let index = 0; index < stitches.length; index += 1) {
330
+ const edge = stitches[index];
331
+ const edgePath = `${path}[${index}]`;
332
+ if (isEdgeShapingElement(edge)) {
333
+ validateShapingElement(edge, edgePath);
334
+ continue;
335
+ }
336
+ validateStitchInstruction(edge, edgePath);
337
+ }
338
+ }
339
+ function validateRowInput(row, totalStitches) {
340
+ if (!isRecord(row)) {
341
+ throw new Error("row must be an object.");
342
+ }
343
+ assertNonNegativeInteger(totalStitches, "totalStitches");
344
+ validateEdgeElements(row.edgeStart, "row.edgeStart");
345
+ validateEdgeElements(row.edgeEnd, "row.edgeEnd");
346
+ validateRowElements(row.elements, "row.elements");
347
+ }
348
+ function createFrame(elements, path, repeat, applyRemainingConstraint = false) {
349
+ return {
350
+ applyRemainingConstraint,
351
+ elements,
352
+ index: 0,
353
+ iterationsCompleted: 0,
354
+ path,
355
+ producedInIteration: 0,
356
+ producedStitches: 0,
357
+ repeat
358
+ };
359
+ }
360
+ function getRemainingCapacity(frame, available, used) {
361
+ if (!frame.repeat || !frame.applyRemainingConstraint || frame.repeat.remainingStitches === void 0) {
362
+ return null;
363
+ }
364
+ return available - frame.repeat.remainingStitches - used;
365
+ }
366
+ function getFrameRemaining(frame, bodyStitchesUsed, available) {
367
+ if (!frame.repeat) {
368
+ return null;
369
+ }
370
+ if (frame.repeat.targetStitches !== void 0) {
371
+ return frame.repeat.targetStitches - (frame.producedStitches + frame.producedInIteration);
372
+ }
373
+ return getRemainingCapacity(frame, available, bodyStitchesUsed);
374
+ }
375
+ function shouldStopFrameEarly(frame, bodyStitchesUsed, available) {
376
+ if (!frame.repeat) {
377
+ return false;
378
+ }
379
+ if (bodyStitchesUsed >= available) {
380
+ return true;
381
+ }
382
+ const remaining = getFrameRemaining(frame, bodyStitchesUsed, available);
383
+ return remaining !== null && remaining <= 0;
384
+ }
385
+ function shouldContinueRepeat(frame, bodyStitchesUsed, available) {
386
+ const repeat = frame.repeat;
387
+ if (!repeat || bodyStitchesUsed >= available) {
388
+ return false;
389
+ }
390
+ if (repeat.times !== void 0) {
391
+ return frame.iterationsCompleted < repeat.times;
392
+ }
393
+ if (repeat.maxRepeats !== void 0 && frame.iterationsCompleted >= repeat.maxRepeats) {
394
+ return false;
395
+ }
396
+ const remainingCapacity = getRemainingCapacity(frame, available, bodyStitchesUsed);
397
+ if (remainingCapacity !== null) {
398
+ return remainingCapacity > 0;
399
+ }
400
+ if (repeat.targetStitches !== void 0) {
401
+ return frame.producedStitches < repeat.targetStitches;
402
+ }
403
+ return true;
404
+ }
405
+ var MAX_EXPANSION_STEPS = 1e5;
406
+ function increaseProducedInIteration(frames, amount) {
407
+ for (const frame of frames) {
408
+ if (frame.repeat) {
409
+ frame.producedInIteration += amount;
410
+ }
411
+ }
412
+ }
413
+ function fillInstructionInBody(state, instruction) {
414
+ const remaining = state.available - state.bodyStitchesUsed;
415
+ if (remaining <= 0) {
416
+ return;
417
+ }
418
+ let stitchesToAdd = Math.min(instruction.count, remaining);
419
+ for (const frame of state.frames) {
420
+ const frameRemaining = getFrameRemaining(frame, state.bodyStitchesUsed, state.available);
421
+ if (frameRemaining !== null) {
422
+ stitchesToAdd = Math.min(stitchesToAdd, frameRemaining);
423
+ }
424
+ }
425
+ if (stitchesToAdd <= 0) {
426
+ return;
427
+ }
428
+ state.result.append(instruction, stitchesToAdd);
429
+ state.bodyStitchesUsed += stitchesToAdd;
430
+ increaseProducedInIteration(state.frames, stitchesToAdd);
431
+ }
432
+ function fillShapingInBody(state, element) {
433
+ const remaining = state.available - state.bodyStitchesUsed;
434
+ if (remaining <= 0) {
435
+ return;
436
+ }
437
+ let operationsToAdd = Math.min(
438
+ element.count,
439
+ Math.floor(remaining / SHAPING_CONSUME[element.shapingType])
440
+ );
441
+ for (const frame of state.frames) {
442
+ const frameRemaining = getFrameRemaining(frame, state.bodyStitchesUsed, state.available);
443
+ if (frameRemaining !== null) {
444
+ operationsToAdd = Math.min(
445
+ operationsToAdd,
446
+ Math.floor(frameRemaining / SHAPING_CONSUME[element.shapingType])
447
+ );
448
+ }
449
+ }
450
+ if (operationsToAdd <= 0) {
451
+ return;
452
+ }
453
+ state.result.append(createInstructionFromShaping(element, operationsToAdd));
454
+ const consumed = operationsToAdd * SHAPING_CONSUME[element.shapingType];
455
+ state.bodyStitchesUsed += consumed;
456
+ increaseProducedInIteration(state.frames, consumed);
457
+ }
458
+ function shouldPopForCycleConstraint(frame, state) {
459
+ const repeat = frame.repeat;
460
+ if (!repeat || frame.index !== 0 || frame.producedInIteration !== 0) {
461
+ return false;
462
+ }
463
+ const toEndRequiresCycles = repeat.toEnd === true && !repeat.allowPartial;
464
+ const remainingConstraintActive = frame.applyRemainingConstraint;
465
+ if (!toEndRequiresCycles && !remainingConstraintActive) {
466
+ return false;
467
+ }
468
+ const seqSize = countSequenceStitches(repeat.sequence);
469
+ if (seqSize === null) {
470
+ throw new Error(
471
+ `${frame.path} must have a fixed stitch count (no nested to-end/target repeats) when used with 'to last' or strict to-end.`
472
+ );
473
+ }
474
+ const reserved = remainingConstraintActive ? repeat.remainingStitches ?? 0 : 0;
475
+ return state.available - state.bodyStitchesUsed - reserved < seqSize;
476
+ }
477
+ function handleCompletedFrame(frame, state) {
478
+ if (frame.index < frame.elements.length) {
479
+ return false;
480
+ }
481
+ if (!frame.repeat) {
482
+ state.frames.pop();
483
+ return true;
484
+ }
485
+ if (frame.producedInIteration === 0) {
486
+ if (frame.iterationsCompleted === 0) {
487
+ throw new Error(`${frame.path} must consume at least one stitch per iteration.`);
488
+ }
489
+ state.frames.pop();
490
+ return true;
491
+ }
492
+ frame.producedStitches += frame.producedInIteration;
493
+ frame.producedInIteration = 0;
494
+ frame.iterationsCompleted += 1;
495
+ if (shouldContinueRepeat(frame, state.bodyStitchesUsed, state.available)) {
496
+ frame.index = 0;
497
+ return true;
498
+ }
499
+ state.frames.pop();
500
+ return true;
501
+ }
502
+ function pushChildRepeatFrame(state, element, elementPath) {
503
+ const hasRemainingConstraintAncestor = state.frames.some(
504
+ (candidate) => candidate.applyRemainingConstraint === true
505
+ );
506
+ const applyRemainingConstraint = element.remainingStitches !== void 0 && !hasRemainingConstraintAncestor;
507
+ state.frames.push(
508
+ createFrame(element.sequence, `${elementPath}.sequence`, element, applyRemainingConstraint)
509
+ );
510
+ }
511
+ function shouldAdvanceFrame(frame, state) {
512
+ const shouldPop = shouldStopFrameEarly(frame, state.bodyStitchesUsed, state.available) || shouldPopForCycleConstraint(frame, state);
513
+ if (shouldPop) {
514
+ state.frames.pop();
515
+ return false;
516
+ }
517
+ return !handleCompletedFrame(frame, state);
518
+ }
519
+ function runBodyExpansion(state) {
520
+ let expansionSteps = 0;
521
+ while (state.frames.length > 0 && state.bodyStitchesUsed < state.available) {
522
+ expansionSteps += 1;
523
+ if (expansionSteps > MAX_EXPANSION_STEPS) {
524
+ throw new Error(
525
+ `Row expansion exceeded ${MAX_EXPANSION_STEPS} steps. Possible infinite loop in repeat structure.`
526
+ );
527
+ }
528
+ const frame = state.frames.at(-1);
529
+ if (!frame) {
530
+ break;
531
+ }
532
+ if (!shouldAdvanceFrame(frame, state)) {
533
+ continue;
534
+ }
535
+ const element = frame.elements[frame.index];
536
+ const elementPath = `${frame.path}[${frame.index}]`;
537
+ frame.index += 1;
538
+ if (isStitchInstruction(element)) {
539
+ fillInstructionInBody(state, element);
540
+ continue;
541
+ }
542
+ if (isShapingElement(element)) {
543
+ fillShapingInBody(state, element);
544
+ continue;
545
+ }
546
+ pushChildRepeatFrame(state, element, elementPath);
547
+ }
548
+ }
549
+ function expandRow(row, totalStitches) {
550
+ validateRowInput(row, totalStitches);
551
+ if (totalStitches === 0) {
552
+ return [];
553
+ }
554
+ const result = new InstructionBuffer();
555
+ let used = 0;
556
+ const appendEdgeElements = (edges) => {
557
+ if (!edges) {
558
+ return;
559
+ }
560
+ for (const edge of edges) {
561
+ if (isEdgeShapingElement(edge)) {
562
+ result.append(createInstructionFromShaping(edge, edge.count));
563
+ used += edge.count * SHAPING_CONSUME[edge.shapingType];
564
+ } else {
565
+ result.append(edge);
566
+ used += edge.count;
567
+ }
568
+ }
569
+ };
570
+ appendEdgeElements(row.edgeStart);
571
+ const edgeEndCount = countEdgeSourceStitches(row.edgeEnd);
572
+ if (used + edgeEndCount > totalStitches) {
573
+ throw new Error("Edge stitches exceed total stitches for the row.");
574
+ }
575
+ const available = totalStitches - used - edgeEndCount;
576
+ const bodyState = {
577
+ available,
578
+ bodyStitchesUsed: 0,
579
+ frames: [createFrame(row.elements, "row.elements")],
580
+ result
581
+ };
582
+ runBodyExpansion(bodyState);
583
+ appendEdgeElements(row.edgeEnd);
584
+ return result.toArray();
585
+ }
586
+ function expandRowToStitches(row, totalStitches) {
587
+ const expanded = expandRow(row, totalStitches);
588
+ const final = [];
589
+ for (const instr of expanded) {
590
+ const last = final.at(-1);
591
+ if (last && canMergeInstructions(last, instr)) {
592
+ last.count += instr.count;
593
+ } else {
594
+ final.push(cloneInstruction(instr));
595
+ }
596
+ }
597
+ return final;
598
+ }
599
+ function countStitchesInInstructions(instructions) {
600
+ return instructions.reduce((sum, instruction) => sum + instruction.count, 0);
601
+ }
602
+ function expandRowToStitchTypes(row, totalStitches) {
603
+ const instructions = expandRow(row, totalStitches);
604
+ const totalCount = countStitchesInInstructions(instructions);
605
+ const stitches = new Array(totalCount);
606
+ let writeIndex = 0;
607
+ for (const instr of instructions) {
608
+ for (let i = 0; i < instr.count; i++) {
609
+ stitches[writeIndex] = instr.type;
610
+ writeIndex += 1;
611
+ }
612
+ }
613
+ return stitches;
614
+ }
615
+
616
+ // src/parser/repeatTermination.ts
617
+ function buildRepeatBlock(sequence, termination) {
618
+ switch (termination.mode) {
619
+ case "times":
620
+ return { sequence, times: termination.times, type: "repeat" };
621
+ case "until-stitches":
622
+ return termination.maxRepeats === void 0 ? { sequence, targetStitches: termination.targetStitches, type: "repeat" } : {
623
+ maxRepeats: termination.maxRepeats,
624
+ sequence,
625
+ targetStitches: termination.targetStitches,
626
+ type: "repeat"
627
+ };
628
+ case "until-remaining":
629
+ return termination.maxRepeats === void 0 ? { remainingStitches: termination.remainingStitches, sequence, type: "repeat" } : {
630
+ maxRepeats: termination.maxRepeats,
631
+ remainingStitches: termination.remainingStitches,
632
+ sequence,
633
+ type: "repeat"
634
+ };
635
+ case "to-end":
636
+ return termination.maxRepeats === void 0 && termination.allowPartial === void 0 ? { sequence, toEnd: true, type: "repeat" } : (() => {
637
+ const repeat = {
638
+ sequence,
639
+ toEnd: true,
640
+ type: "repeat"
641
+ };
642
+ if (termination.allowPartial === void 0) {
643
+ } else {
644
+ repeat.allowPartial = termination.allowPartial;
645
+ }
646
+ if (termination.maxRepeats === void 0) {
647
+ } else {
648
+ repeat.maxRepeats = termination.maxRepeats;
649
+ }
650
+ return repeat;
651
+ })();
652
+ }
653
+ }
654
+ function getRepeatTermination(repeat) {
655
+ if ("times" in repeat && repeat.times !== void 0) {
656
+ return { mode: "times", times: repeat.times };
657
+ }
658
+ if ("targetStitches" in repeat && repeat.targetStitches !== void 0) {
659
+ return {
660
+ maxRepeats: repeat.maxRepeats,
661
+ mode: "until-stitches",
662
+ targetStitches: repeat.targetStitches
663
+ };
664
+ }
665
+ if ("remainingStitches" in repeat && repeat.remainingStitches !== void 0) {
666
+ return {
667
+ maxRepeats: repeat.maxRepeats,
668
+ mode: "until-remaining",
669
+ remainingStitches: repeat.remainingStitches
670
+ };
671
+ }
672
+ return {
673
+ allowPartial: repeat.allowPartial,
674
+ maxRepeats: repeat.maxRepeats,
675
+ mode: "to-end"
676
+ };
677
+ }
678
+
679
+ // src/parser/stitchRegistryData.ts
680
+ var STITCH_TYPE_MAP = {
681
+ dec: "decrease",
682
+ decrease: "decrease",
683
+ inc: "increase",
684
+ increase: "increase",
685
+ k: "knit",
686
+ knit: "knit",
687
+ p: "purl",
688
+ purl: "purl"
689
+ };
690
+ var STITCH_ABBREVIATION_MAP = {
691
+ "k1-b": "knit",
692
+ k2tog: "decrease",
693
+ kfb: "increase",
694
+ kfbf: "increase",
695
+ "inv-l": "increase",
696
+ "inv-r": "increase",
697
+ m1: "increase",
698
+ "p1-b": "purl",
699
+ p2tog: "decrease",
700
+ pfb: "increase",
701
+ psso: "decrease",
702
+ s2kp: "decrease",
703
+ sk2p: "decrease",
704
+ skp: "decrease",
705
+ sl: "knit",
706
+ sl1: "knit",
707
+ slip: "knit",
708
+ ssp: "decrease",
709
+ ssk: "decrease",
710
+ tbl: "knit",
711
+ yfon: "increase",
712
+ yfrn: "increase",
713
+ yon: "increase",
714
+ yrn: "increase",
715
+ yo: "increase"
716
+ };
717
+ var STITCH_DEFAULT_COUNT_MAP = {
718
+ kfbf: 2
719
+ };
720
+ var STITCH_LOCKED_COUNT_VARIANTS = /* @__PURE__ */ new Set([
721
+ "k1-b",
722
+ "k2tog",
723
+ "kfbf",
724
+ "p1-b",
725
+ "p2tog",
726
+ "s2kp",
727
+ "sk2p",
728
+ "sl1"
729
+ ]);
730
+
731
+ // src/parser/rowDslSerialiser.ts
732
+ var STITCH_TYPE_TO_TOKEN = /* @__PURE__ */ (() => {
733
+ const map = {
734
+ decrease: "dec",
735
+ increase: "inc",
736
+ knit: "k",
737
+ purl: "p"
738
+ };
739
+ return map;
740
+ })();
741
+ function isLockedCountVariant(variant) {
742
+ return STITCH_LOCKED_COUNT_VARIANTS.has(variant);
743
+ }
744
+ function serialiseStitchInstruction(instr) {
745
+ if (instr.variant) {
746
+ if (isLockedCountVariant(instr.variant)) {
747
+ if (instr.count === 1) {
748
+ return instr.variant;
749
+ }
750
+ return `(${instr.variant}) *${instr.count}`;
751
+ }
752
+ return instr.count === 1 ? instr.variant : `${instr.variant}${instr.count}`;
753
+ }
754
+ const token = STITCH_TYPE_TO_TOKEN[instr.type] ?? instr.type;
755
+ return `${token}${instr.count}`;
756
+ }
757
+ function serialiseShapingElement(el) {
758
+ const typeToken = el.shapingType === "decrease" ? "dec" : "inc";
759
+ let dirToken = "";
760
+ if (el.direction !== void 0) {
761
+ dirToken = `-${el.direction}`;
762
+ }
763
+ return `shape-${typeToken}${dirToken}${el.count}`;
764
+ }
765
+ function serialiseMaxRepeats(maxRepeats) {
766
+ if (maxRepeats === void 0) {
767
+ return "";
768
+ }
769
+ return ` max ${maxRepeats} repeats`;
770
+ }
771
+ function serialiseRowElement(el) {
772
+ if (el.type === "shaping") {
773
+ return serialiseShapingElement(el);
774
+ }
775
+ if (el.type === "repeat") {
776
+ return serialiseRepeatBlock(el);
777
+ }
778
+ return serialiseStitchInstruction(el);
779
+ }
780
+ function serialiseRepeatBlock(block) {
781
+ const inner = block.sequence.map(serialiseRowElement).join(" ");
782
+ const body = `(${inner})`;
783
+ const termination = getRepeatTermination(block);
784
+ switch (termination.mode) {
785
+ case "times":
786
+ return `${body} *${termination.times}`;
787
+ case "until-stitches": {
788
+ const maxPart = serialiseMaxRepeats(termination.maxRepeats);
789
+ return `${body} until ${termination.targetStitches}${maxPart}`;
790
+ }
791
+ case "until-remaining": {
792
+ const maxPart = serialiseMaxRepeats(termination.maxRepeats);
793
+ return `${body} to last ${termination.remainingStitches}${maxPart}`;
794
+ }
795
+ case "to-end": {
796
+ const allowPartialPart = termination.allowPartial === true ? " allow partial" : "";
797
+ const maxPart = serialiseMaxRepeats(termination.maxRepeats);
798
+ return `${body}${allowPartialPart}${maxPart}`;
799
+ }
800
+ }
801
+ }
802
+ function serialiseEdgeElements(elements) {
803
+ return elements.map(serialiseRowElement).join(" ");
804
+ }
805
+ function rowToRowDsl(row) {
806
+ const parts = [];
807
+ if (row.edgeStart && row.edgeStart.length > 0) {
808
+ parts.push(`[${serialiseEdgeElements(row.edgeStart)}]s`);
809
+ }
810
+ for (const element of row.elements) {
811
+ parts.push(serialiseRowElement(element));
812
+ }
813
+ if (row.edgeEnd && row.edgeEnd.length > 0) {
814
+ parts.push(`[${serialiseEdgeElements(row.edgeEnd)}]e`);
815
+ }
816
+ return parts.join(" ");
817
+ }
818
+
819
+ // src/pattern/patternEngine.ts
820
+ var patternDebugLogger = globalThis.console;
821
+ function getCarriedStitches(mode, inputStitches, outputStitches) {
822
+ return mode === "shaped" ? outputStitches : inputStitches;
823
+ }
824
+ function isShapingEl(element) {
825
+ return element.type === "shaping";
826
+ }
827
+ function isRepeatEl(element) {
828
+ return element.type === "repeat";
829
+ }
830
+ function collectRowShaping(row) {
831
+ const ops = [];
832
+ function walkEdge(elements) {
833
+ for (const el of elements ?? []) {
834
+ if (isShapingEl(el)) {
835
+ ops.push({
836
+ count: el.count,
837
+ direction: el.direction,
838
+ shapingType: el.shapingType,
839
+ variant: el.variant
840
+ });
841
+ }
842
+ }
843
+ }
844
+ function walkElements(elements) {
845
+ for (const el of elements) {
846
+ if (isShapingEl(el)) {
847
+ ops.push({
848
+ count: el.count,
849
+ direction: el.direction,
850
+ shapingType: el.shapingType,
851
+ variant: el.variant
852
+ });
853
+ } else if (isRepeatEl(el)) {
854
+ walkElements(el.sequence);
855
+ }
856
+ }
857
+ }
858
+ walkEdge(row.edgeStart);
859
+ walkElements(row.elements);
860
+ walkEdge(row.edgeEnd);
861
+ return ops;
862
+ }
863
+ function compilePattern(pattern, initialStitches, options = {}) {
864
+ var _a;
865
+ const mode = options.mode ?? "shaped";
866
+ const rows = [];
867
+ let currentStitches = initialStitches;
868
+ const verticalRepeats = pattern.repeatRows ?? 1;
869
+ let rowNumber = 1;
870
+ for (let repeat = 0; repeat < verticalRepeats; repeat++) {
871
+ for (let sourceRowIndex = 0; sourceRowIndex < pattern.rows.length; sourceRowIndex += 1) {
872
+ const row = pattern.rows[sourceRowIndex];
873
+ const inputStitches = currentStitches;
874
+ const stitches = expandRowToStitches(row, inputStitches);
875
+ const outputStitches = countStitchesInInstructions(stitches);
876
+ const carriedStitches = getCarriedStitches(mode, inputStitches, outputStitches);
877
+ const shapingOperations = collectRowShaping(row);
878
+ rows.push({
879
+ carriedStitches,
880
+ hasShaping: shapingOperations.length > 0,
881
+ inputStitches,
882
+ normalisedRowDsl: rowToRowDsl(row),
883
+ outputStitches,
884
+ repeatIndex: repeat,
885
+ rowNumber,
886
+ shapingOperations,
887
+ sourceRowIndex,
888
+ stitchDelta: outputStitches - inputStitches,
889
+ stitches
890
+ });
891
+ currentStitches = carriedStitches;
892
+ rowNumber += 1;
893
+ }
894
+ }
895
+ return {
896
+ finalStitches: ((_a = rows.at(-1)) == null ? void 0 : _a.carriedStitches) ?? initialStitches,
897
+ initialStitches,
898
+ mode,
899
+ rows
900
+ };
901
+ }
902
+ function expandPattern(pattern, stitchesPerRow) {
903
+ return compilePattern(pattern, stitchesPerRow, { mode: "fixed" }).rows.map((row) => row.stitches);
904
+ }
905
+ function shapedExpandPattern(pattern, initialStitches) {
906
+ return compilePattern(pattern, initialStitches, { mode: "shaped" }).rows.map(
907
+ (row) => row.stitches
908
+ );
909
+ }
910
+ var stitchSymbols = {
911
+ knit: "K",
912
+ purl: "P",
913
+ increase: "+",
914
+ decrease: "-"
915
+ };
916
+ function printPattern(pattern, stitchesPerRow) {
917
+ const expanded = expandPattern(pattern, stitchesPerRow);
918
+ expanded.forEach((row, rowIndex) => {
919
+ const rowStr = row.map((stitch) => {
920
+ const symbol = stitchSymbols[stitch.type] ?? "?";
921
+ return symbol.repeat(stitch.count);
922
+ }).join(" ");
923
+ patternDebugLogger == null ? void 0 : patternDebugLogger.log(`Row ${rowIndex + 1}: ${rowStr}`);
924
+ });
925
+ }
926
+ function createRepeatRow(sequence, times) {
927
+ return {
928
+ elements: [
929
+ {
930
+ type: "repeat",
931
+ times,
932
+ sequence
933
+ }
934
+ ]
935
+ };
936
+ }
937
+
938
+ // src/public/errors.ts
939
+ var KnittingMathError = class extends Error {
940
+ code;
941
+ context;
942
+ constructor(name, code, message, context, cause) {
943
+ super(message);
944
+ this.name = name;
945
+ this.code = code;
946
+ this.context = context;
947
+ if (cause !== void 0) {
948
+ Object.defineProperty(this, "cause", {
949
+ configurable: true,
950
+ enumerable: false,
951
+ value: cause,
952
+ writable: true
953
+ });
954
+ }
955
+ }
956
+ };
957
+ var KnittingMathParseError = class extends KnittingMathError {
958
+ constructor(message, context, cause) {
959
+ super("KnittingMathParseError", "PARSE_ERROR", message, context, cause);
960
+ }
961
+ };
962
+ var KnittingMathValidationError = class extends KnittingMathError {
963
+ constructor(message, context, cause) {
964
+ super("KnittingMathValidationError", "VALIDATION_ERROR", message, context, cause);
965
+ }
966
+ };
967
+ var KnittingMathExpansionError = class extends KnittingMathError {
968
+ constructor(message, context, cause) {
969
+ super("KnittingMathExpansionError", "EXPANSION_ERROR", message, context, cause);
970
+ }
971
+ };
972
+ function getErrorMessage(error) {
973
+ if (error instanceof Error && error.message.length > 0) {
974
+ return error.message;
975
+ }
976
+ return "An unknown knitting-math error occurred.";
977
+ }
978
+ function ensureKnittingMathError(error, createError) {
979
+ if (error instanceof KnittingMathError) {
980
+ return error;
981
+ }
982
+ return createError(getErrorMessage(error), error);
983
+ }
984
+
985
+ // src/public/pattern.ts
986
+ function wrapExpansionError(error, operation) {
987
+ return ensureKnittingMathError(
988
+ error,
989
+ (message, cause) => new KnittingMathExpansionError(message, { operation }, cause)
990
+ );
991
+ }
992
+ function compilePattern2(pattern, initialStitches, options) {
993
+ try {
994
+ return compilePattern(pattern, initialStitches, options);
995
+ } catch (error) {
996
+ throw wrapExpansionError(error, "compilePattern");
997
+ }
998
+ }
999
+ function expandPattern2(pattern, stitchesPerRow) {
1000
+ try {
1001
+ return expandPattern(pattern, stitchesPerRow);
1002
+ } catch (error) {
1003
+ throw wrapExpansionError(error, "expandPattern");
1004
+ }
1005
+ }
1006
+ function shapedExpandPattern2(pattern, initialStitches) {
1007
+ try {
1008
+ return shapedExpandPattern(pattern, initialStitches);
1009
+ } catch (error) {
1010
+ throw wrapExpansionError(error, "shapedExpandPattern");
1011
+ }
1012
+ }
1013
+ function printPattern2(pattern, stitchesPerRow) {
1014
+ try {
1015
+ printPattern(pattern, stitchesPerRow);
1016
+ } catch (error) {
1017
+ throw wrapExpansionError(error, "printPattern");
1018
+ }
1019
+ }
1020
+
1021
+ // src/stitches/castOn.ts
1022
+ function calculateCastOn(input) {
1023
+ return calculateCastOnStitches({ ...input, ease: 0 });
1024
+ }
1025
+ function calculateCastOnStitches(input) {
1026
+ const { size, unit, gauge, ease = 0, easeUnit = unit } = input;
1027
+ const sizeInCm = normaliseMeasurement(size, unit);
1028
+ const easeCm = normaliseMeasurement(ease, easeUnit);
1029
+ const finalSizeInCm = sizeInCm + easeCm;
1030
+ if (finalSizeInCm <= 0) {
1031
+ throw new Error("Final size must be greater than 0 after applying ease.");
1032
+ }
1033
+ const stitches = finalSizeInCm * stitchesPerCm(gauge);
1034
+ return roundStitches(stitches);
1035
+ }
1036
+
1037
+ // src/parser/compatibilityNormaliser.ts
1038
+ function isWordBoundaryCharacter(char) {
1039
+ return char === void 0 || /[^a-z]/i.test(char);
1040
+ }
1041
+ function isIndexWithinBounds(input, index) {
1042
+ return index >= 0 && index < input.length;
1043
+ }
1044
+ function skipWhitespace(input, cursor) {
1045
+ let next = cursor;
1046
+ while (next < input.length && /\s/.test(input[next] ?? "")) {
1047
+ next += 1;
1048
+ }
1049
+ return next;
1050
+ }
1051
+ function hasRepeatKeywordAt(input, cursor) {
1052
+ const repeatKeyword = input.slice(cursor, cursor + 6);
1053
+ return repeatKeyword.toLowerCase() === "repeat" && isWordBoundaryCharacter(input[cursor + 6]);
1054
+ }
1055
+ function normaliseDirectionToken(direction) {
1056
+ const lowered = direction.toLowerCase();
1057
+ if (lowered === "l" || lowered === "left") {
1058
+ return "left";
1059
+ }
1060
+ if (lowered === "r" || lowered === "right") {
1061
+ return "right";
1062
+ }
1063
+ return "centre";
1064
+ }
1065
+ function normaliseDirectionalShapingPhrases(input) {
1066
+ let output = input;
1067
+ output = output.replaceAll(
1068
+ /\b(dec|decrease)\s+(left|right|centre|center|l|r|c)\s+(\d+)\b/gi,
1069
+ (_match, _op, direction, count) => `shape-dec-${normaliseDirectionToken(direction)}${count}`
1070
+ );
1071
+ output = output.replaceAll(
1072
+ /\b(inc|increase)\s+(left|right|centre|center|l|r|c)\s+(\d+)\b/gi,
1073
+ (_match, _op, direction, count) => `shape-inc-${normaliseDirectionToken(direction)}${count}`
1074
+ );
1075
+ return output;
1076
+ }
1077
+ function findStarCompatibilityBlockEnd(input, startIndex) {
1078
+ for (let i = startIndex + 1; i < input.length; i += 1) {
1079
+ if (input[i] === "*") {
1080
+ return i;
1081
+ }
1082
+ }
1083
+ return -1;
1084
+ }
1085
+ function findBracketCompatibilityBlockEnd(input, startIndex) {
1086
+ let depth = 0;
1087
+ for (let i = startIndex; i < input.length; i += 1) {
1088
+ if (!isIndexWithinBounds(input, i)) {
1089
+ break;
1090
+ }
1091
+ const char = input[i];
1092
+ if (char === "[") {
1093
+ depth += 1;
1094
+ continue;
1095
+ }
1096
+ if (char !== "]") {
1097
+ continue;
1098
+ }
1099
+ depth -= 1;
1100
+ if (depth !== 0) {
1101
+ continue;
1102
+ }
1103
+ const after = skipWhitespace(input, i + 1);
1104
+ return hasRepeatKeywordAt(input, after) ? i : -1;
1105
+ }
1106
+ return -1;
1107
+ }
1108
+ function findCompatibilityBlockEnd(input, startIndex) {
1109
+ if (!isIndexWithinBounds(input, startIndex)) {
1110
+ return -1;
1111
+ }
1112
+ const opener = input[startIndex];
1113
+ if (opener !== "*" && opener !== "[") {
1114
+ return -1;
1115
+ }
1116
+ if (opener === "[") {
1117
+ return findBracketCompatibilityBlockEnd(input, startIndex);
1118
+ }
1119
+ return findStarCompatibilityBlockEnd(input, startIndex);
1120
+ }
1121
+ function consumeNumberToken(input, cursor) {
1122
+ let nextCursor = cursor;
1123
+ while (nextCursor < input.length && /\d/.test(input[nextCursor] ?? "")) {
1124
+ nextCursor += 1;
1125
+ }
1126
+ if (nextCursor === cursor) {
1127
+ return { nextCursor };
1128
+ }
1129
+ return {
1130
+ nextCursor,
1131
+ value: Number.parseInt(input.slice(cursor, nextCursor), 10)
1132
+ };
1133
+ }
1134
+ function consumeOptionalStitchUnit(input, cursor) {
1135
+ const unitMatch = /^(stitches|stitch|sts|st)\b/i.exec(input.slice(cursor));
1136
+ return unitMatch ? cursor + unitMatch[0].length : cursor;
1137
+ }
1138
+ function parseToLastClause(input, cursor) {
1139
+ const afterLast = skipWhitespace(input, cursor);
1140
+ const numberToken = consumeNumberToken(input, afterLast);
1141
+ if (numberToken.value === void 0) {
1142
+ throw new Error("Expected a stitch count after 'to last' in repeat clause.");
1143
+ }
1144
+ if (numberToken.value <= 0) {
1145
+ throw new Error("Repeat remaining-stitches value must be greater than 0.");
1146
+ }
1147
+ const afterCount = skipWhitespace(input, numberToken.nextCursor);
1148
+ const nextIndex = consumeOptionalStitchUnit(input, afterCount);
1149
+ return {
1150
+ kind: "to-last",
1151
+ nextIndex,
1152
+ remainingStitches: numberToken.value
1153
+ };
1154
+ }
1155
+ function parseToClause(input, cursor) {
1156
+ const toKeyword = input.slice(cursor, cursor + 2);
1157
+ if (toKeyword.toLowerCase() !== "to" || !isWordBoundaryCharacter(input[cursor + 2])) {
1158
+ return {
1159
+ kind: "to-end",
1160
+ nextIndex: cursor
1161
+ };
1162
+ }
1163
+ let nextCursor = skipWhitespace(input, cursor + 2);
1164
+ const lastKeyword = input.slice(nextCursor, nextCursor + 4);
1165
+ if (lastKeyword.toLowerCase() === "last" && isWordBoundaryCharacter(input[nextCursor + 4])) {
1166
+ return parseToLastClause(input, nextCursor + 4);
1167
+ }
1168
+ const endKeyword = input.slice(nextCursor, nextCursor + 3);
1169
+ if (endKeyword.toLowerCase() !== "end" || !isWordBoundaryCharacter(input[nextCursor + 3])) {
1170
+ throw new Error("Expected 'end' after 'to' in repeat clause.");
1171
+ }
1172
+ nextCursor += 3;
1173
+ return {
1174
+ kind: "to-end",
1175
+ nextIndex: nextCursor
1176
+ };
1177
+ }
1178
+ function parseTimesClause(input, cursor) {
1179
+ const numberToken = consumeNumberToken(input, cursor);
1180
+ if (numberToken.value === void 0) {
1181
+ return {
1182
+ kind: "to-end",
1183
+ nextIndex: cursor
1184
+ };
1185
+ }
1186
+ if (numberToken.value <= 0) {
1187
+ throw new Error("Repeat times must be greater than 0.");
1188
+ }
1189
+ let nextCursor = skipWhitespace(input, numberToken.nextCursor);
1190
+ const timesKeyword = input.slice(nextCursor, nextCursor + 5);
1191
+ if (timesKeyword.toLowerCase() === "times" && isWordBoundaryCharacter(input[nextCursor + 5])) {
1192
+ nextCursor += 5;
1193
+ }
1194
+ return {
1195
+ kind: "times",
1196
+ nextIndex: nextCursor,
1197
+ times: numberToken.value
1198
+ };
1199
+ }
1200
+ function parseRepeatClause(input, opener, closingIndex) {
1201
+ let cursor = skipWhitespace(input, closingIndex + 1);
1202
+ if (!hasRepeatKeywordAt(input, cursor)) {
1203
+ throw new Error(`Expected 'repeat' after '${opener}...${opener === "*" ? "*" : "]"}' block.`);
1204
+ }
1205
+ cursor = skipWhitespace(input, cursor + 6);
1206
+ const toClause = parseToClause(input, cursor);
1207
+ if (toClause.kind === "to-last") {
1208
+ return toClause;
1209
+ }
1210
+ if (toClause.nextIndex !== cursor) {
1211
+ return toClause;
1212
+ }
1213
+ return parseTimesClause(input, cursor);
1214
+ }
1215
+ function renderCompletedClause(clause, completed) {
1216
+ if (clause.kind === "times") {
1217
+ return `(${completed}) * ${clause.times}`;
1218
+ }
1219
+ if (clause.kind === "to-last") {
1220
+ return `(${completed}) to last ${clause.remainingStitches} stitches`;
1221
+ }
1222
+ return `(${completed})`;
1223
+ }
1224
+ function completeFrame(frames) {
1225
+ const frame = frames.pop();
1226
+ const completed = frame.result;
1227
+ if (frames.length === 0) {
1228
+ return completed;
1229
+ }
1230
+ const parent = frames.at(-1);
1231
+ const pending = parent.pending;
1232
+ if (!pending) {
1233
+ throw new Error("Invalid normaliser state: missing pending replacement.");
1234
+ }
1235
+ parent.result += renderCompletedClause(pending.clause, completed);
1236
+ parent.index = pending.clause.nextIndex;
1237
+ parent.pending = void 0;
1238
+ return null;
1239
+ }
1240
+ function processLiteralChar(frame) {
1241
+ frame.result += frame.input[frame.index] ?? "";
1242
+ frame.index += 1;
1243
+ }
1244
+ function processBlockOpener(frame, frames) {
1245
+ const opener = frame.input[frame.index];
1246
+ if (opener !== "*" && opener !== "[") {
1247
+ processLiteralChar(frame);
1248
+ return;
1249
+ }
1250
+ const closingIndex = findCompatibilityBlockEnd(frame.input, frame.index);
1251
+ if (closingIndex === -1) {
1252
+ if (opener === "*") {
1253
+ throw new Error("Unclosed repeat block (*).");
1254
+ }
1255
+ processLiteralChar(frame);
1256
+ return;
1257
+ }
1258
+ const sequence = frame.input.slice(frame.index + 1, closingIndex).trim();
1259
+ if (sequence.length === 0) {
1260
+ if (opener === "*") {
1261
+ throw new Error("Repeat block sequence cannot be empty.");
1262
+ }
1263
+ frame.result += frame.input.slice(frame.index, closingIndex + 1);
1264
+ frame.index = closingIndex + 1;
1265
+ return;
1266
+ }
1267
+ const clause = parseRepeatClause(frame.input, opener, closingIndex);
1268
+ frame.pending = { clause };
1269
+ frames.push({
1270
+ input: sequence,
1271
+ index: 0,
1272
+ result: ""
1273
+ });
1274
+ }
1275
+ function normaliseCompatibilitySyntax(input) {
1276
+ const frames = [
1277
+ {
1278
+ input: normaliseDirectionalShapingPhrases(input),
1279
+ index: 0,
1280
+ result: ""
1281
+ }
1282
+ ];
1283
+ let completedResult = "";
1284
+ while (frames.length > 0) {
1285
+ const frame = frames.at(-1);
1286
+ if (!isIndexWithinBounds(frame.input, frame.index)) {
1287
+ const completed = completeFrame(frames);
1288
+ if (completed !== null) {
1289
+ completedResult = completed;
1290
+ break;
1291
+ }
1292
+ continue;
1293
+ }
1294
+ processBlockOpener(frame, frames);
1295
+ }
1296
+ return completedResult;
1297
+ }
1298
+
1299
+ // src/parser/rowLexer.ts
1300
+ var WORD_TOKEN_RE = /[a-z0-9-]+/iy;
1301
+ var COUNT_RE = /\d+/y;
1302
+ function readWordToken(input, index) {
1303
+ WORD_TOKEN_RE.lastIndex = index;
1304
+ const match = WORD_TOKEN_RE.exec(input);
1305
+ if (!match) {
1306
+ return null;
1307
+ }
1308
+ return {
1309
+ nextIndex: WORD_TOKEN_RE.lastIndex,
1310
+ value: match[0]
1311
+ };
1312
+ }
1313
+ function readCountToken(input, index) {
1314
+ COUNT_RE.lastIndex = index;
1315
+ const match = COUNT_RE.exec(input);
1316
+ if (!match) {
1317
+ return null;
1318
+ }
1319
+ return {
1320
+ nextIndex: COUNT_RE.lastIndex,
1321
+ value: match[0]
1322
+ };
1323
+ }
1324
+ function isDigitAt(input, index) {
1325
+ const char = input[index] ?? "";
1326
+ return /\d/.test(char);
1327
+ }
1328
+ function skipWhitespaceFrom(input, index) {
1329
+ let next = index;
1330
+ while (next < input.length && /\s/.test(input.charAt(next))) {
1331
+ next += 1;
1332
+ }
1333
+ return next;
1334
+ }
1335
+ function skipSeparatorsFrom(input, index) {
1336
+ let next = index;
1337
+ while (next < input.length && /[\s,]/.test(input.charAt(next))) {
1338
+ next += 1;
1339
+ }
1340
+ return next;
1341
+ }
1342
+
1343
+ // src/parser/stitchRegistry.ts
1344
+ var ERR_NON_EMPTY_TOKEN = "Token must be a non-empty string.";
1345
+ var ERR_DEFAULT_COUNT_INVALID = "Default count must be a positive integer.";
1346
+ var ERR_RESOLVE_OPTIONS_INVALID = "Resolve options must be an object when provided.";
1347
+ var ERR_RESOLVE_THROW_ON_UNKNOWN_INVALID = "Resolve option 'throwOnUnknown' must be a boolean when provided.";
1348
+ var InMemoryStitchRegistry = class {
1349
+ typeMap = /* @__PURE__ */ new Map();
1350
+ abbreviationMap = /* @__PURE__ */ new Map();
1351
+ defaultCounts = /* @__PURE__ */ new Map();
1352
+ lockedCountVariants = /* @__PURE__ */ new Set();
1353
+ /**
1354
+ * Normalise a required token for storage.
1355
+ *
1356
+ * @param token - The caller-provided token value.
1357
+ * @returns The trimmed, lower-cased token.
1358
+ * @throws Error when the token is not a non-empty string.
1359
+ */
1360
+ normaliseRequiredToken(token) {
1361
+ if (typeof token !== "string") {
1362
+ throw new Error(ERR_NON_EMPTY_TOKEN);
1363
+ }
1364
+ const normalised = token.trim().toLowerCase();
1365
+ if (normalised.length === 0) {
1366
+ throw new Error(ERR_NON_EMPTY_TOKEN);
1367
+ }
1368
+ return normalised;
1369
+ }
1370
+ /**
1371
+ * Normalise an optional lookup token.
1372
+ *
1373
+ * @param token - The caller-provided token value.
1374
+ * @returns The trimmed, lower-cased token, or null when blank.
1375
+ * @throws Error when the token is not a string.
1376
+ */
1377
+ normaliseLookupToken(token) {
1378
+ if (typeof token !== "string") {
1379
+ throw new Error(ERR_NON_EMPTY_TOKEN);
1380
+ }
1381
+ const normalised = token.trim().toLowerCase();
1382
+ if (normalised.length === 0) {
1383
+ return null;
1384
+ }
1385
+ return normalised;
1386
+ }
1387
+ /**
1388
+ * Determine whether a normalised token exists in either registry map.
1389
+ *
1390
+ * @param normalisedToken - A lower-cased token already normalised for lookup.
1391
+ * @returns True when the token is registered as either a type alias or abbreviation.
1392
+ */
1393
+ hasRegisteredToken(normalisedToken) {
1394
+ return this.typeMap.has(normalisedToken) || this.abbreviationMap.has(normalisedToken);
1395
+ }
1396
+ /**
1397
+ * Validate and read the strict unknown-token behaviour from optional resolve options.
1398
+ *
1399
+ * @param options - Optional resolve configuration.
1400
+ * @returns True when unknown tokens should throw instead of returning null.
1401
+ * @throws Error when the options value or throwOnUnknown flag has an invalid runtime type.
1402
+ */
1403
+ shouldThrowOnUnknown(options) {
1404
+ if (options === void 0) {
1405
+ return false;
1406
+ }
1407
+ if (typeof options !== "object" || options === null) {
1408
+ throw new Error(ERR_RESOLVE_OPTIONS_INVALID);
1409
+ }
1410
+ if (options.throwOnUnknown !== void 0 && typeof options.throwOnUnknown !== "boolean") {
1411
+ throw new Error(ERR_RESOLVE_THROW_ON_UNKNOWN_INVALID);
1412
+ }
1413
+ return options.throwOnUnknown === true;
1414
+ }
1415
+ /**
1416
+ * Check whether a normalised variant token is configured as locked-count.
1417
+ *
1418
+ * @param normalisedVariant - A lower-cased variant token, or null for blank input.
1419
+ * @returns True when the variant exists and is locked.
1420
+ */
1421
+ isLocked(normalisedVariant) {
1422
+ return normalisedVariant !== null && this.lockedCountVariants.has(normalisedVariant);
1423
+ }
1424
+ /**
1425
+ * Resolve a normalised token from the type alias map or abbreviation map.
1426
+ *
1427
+ * @param normalisedToken - A lower-cased token already normalised for lookup.
1428
+ * @returns The canonical stitch type, or null when unknown.
1429
+ */
1430
+ resolveKnownType(normalisedToken) {
1431
+ return this.typeMap.get(normalisedToken) ?? this.abbreviationMap.get(normalisedToken) ?? null;
1432
+ }
1433
+ /**
1434
+ * Build a descriptive strict-mode error suffix listing valid registered tokens.
1435
+ *
1436
+ * @returns A human-readable description of the currently registered token set.
1437
+ */
1438
+ describeExpectedTokens() {
1439
+ const allTokens = /* @__PURE__ */ new Set([...this.typeMap.keys(), ...this.abbreviationMap.keys()]);
1440
+ if (allTokens.size === 0) {
1441
+ return "Expected a valid KnittingPattern::StitchType token or registered abbreviation, but no stitch tokens are currently registered.";
1442
+ }
1443
+ return `Expected a valid KnittingPattern::StitchType token or registered abbreviation. Known stitch tokens: ${Array.from(
1444
+ allTokens
1445
+ ).sort().join(", ")}.`;
1446
+ }
1447
+ registerType(alias, canonicalType) {
1448
+ this.typeMap.set(this.normaliseRequiredToken(alias), canonicalType);
1449
+ }
1450
+ registerAbbreviation(abbreviation, canonicalType) {
1451
+ this.abbreviationMap.set(this.normaliseRequiredToken(abbreviation), canonicalType);
1452
+ }
1453
+ registerDefaultCount(variant, count) {
1454
+ if (!Number.isInteger(count) || count <= 0) {
1455
+ throw new Error(ERR_DEFAULT_COUNT_INVALID);
1456
+ }
1457
+ this.defaultCounts.set(this.normaliseRequiredToken(variant), count);
1458
+ }
1459
+ registerLockedCountVariant(variant) {
1460
+ this.lockedCountVariants.add(this.normaliseRequiredToken(variant));
1461
+ }
1462
+ isKnown(token) {
1463
+ const normalised = this.normaliseLookupToken(token);
1464
+ if (normalised === null) {
1465
+ return false;
1466
+ }
1467
+ return this.hasRegisteredToken(normalised);
1468
+ }
1469
+ isVariant(token) {
1470
+ const normalised = this.normaliseLookupToken(token);
1471
+ if (normalised === null) {
1472
+ return false;
1473
+ }
1474
+ return this.abbreviationMap.has(normalised);
1475
+ }
1476
+ resolve(token, options) {
1477
+ const throwOnUnknown = this.shouldThrowOnUnknown(options);
1478
+ const normalised = this.normaliseLookupToken(token);
1479
+ if (normalised === null) {
1480
+ if (throwOnUnknown) {
1481
+ throw new Error(`Unknown stitch token '${token}'. ${this.describeExpectedTokens()}`);
1482
+ }
1483
+ return null;
1484
+ }
1485
+ const resolved = this.resolveKnownType(normalised);
1486
+ if (resolved === null && throwOnUnknown) {
1487
+ throw new Error(`Unknown stitch token '${token}'. ${this.describeExpectedTokens()}`);
1488
+ }
1489
+ return resolved;
1490
+ }
1491
+ getDefaultCount(variant) {
1492
+ const normalised = this.normaliseLookupToken(variant);
1493
+ if (normalised === null) {
1494
+ return 1;
1495
+ }
1496
+ return this.defaultCounts.get(normalised) ?? 1;
1497
+ }
1498
+ isLockedCountVariant(variant) {
1499
+ const normalised = this.normaliseLookupToken(variant);
1500
+ return this.isLocked(normalised);
1501
+ }
1502
+ };
1503
+ function createDefaultStitchRegistry() {
1504
+ const registry = new InMemoryStitchRegistry();
1505
+ for (const [alias, canonicalType] of Object.entries(STITCH_TYPE_MAP)) {
1506
+ registry.registerType(alias, canonicalType);
1507
+ }
1508
+ for (const [abbr, canonicalType] of Object.entries(STITCH_ABBREVIATION_MAP)) {
1509
+ registry.registerAbbreviation(abbr, canonicalType);
1510
+ }
1511
+ for (const [variant, count] of Object.entries(STITCH_DEFAULT_COUNT_MAP)) {
1512
+ registry.registerDefaultCount(variant, count);
1513
+ }
1514
+ for (const variant of STITCH_LOCKED_COUNT_VARIANTS) {
1515
+ registry.registerLockedCountVariant(variant);
1516
+ }
1517
+ return registry;
1518
+ }
1519
+ function createEmptyStitchRegistry() {
1520
+ return new InMemoryStitchRegistry();
1521
+ }
1522
+
1523
+ // src/parser/rowAstParser.ts
1524
+ var SHAPING_TOKEN_RE = /^shape-(dec|decrease|inc|increase)(?:-(l|left|r|right|c|centre|center))?(\d+)$/;
1525
+ var COMPACT_STITCH_TOKEN_RE = /^([a-z-]+)(\d+)$/i;
1526
+ var RowDslParser = class {
1527
+ input;
1528
+ registry;
1529
+ index = 0;
1530
+ constructor(input, registry) {
1531
+ this.input = input;
1532
+ this.registry = registry ?? createDefaultStitchRegistry();
1533
+ }
1534
+ parseElements() {
1535
+ const elements = [];
1536
+ while (true) {
1537
+ this.skipSeparators();
1538
+ if (this.isDone()) {
1539
+ break;
1540
+ }
1541
+ const char = this.current();
1542
+ if (char === ")") {
1543
+ throw this.error("Unmatched group delimiter.");
1544
+ }
1545
+ if (char === "(") {
1546
+ elements.push(this.parseRepeat());
1547
+ continue;
1548
+ }
1549
+ elements.push(this.parseStitchInstruction());
1550
+ }
1551
+ return elements;
1552
+ }
1553
+ isDone() {
1554
+ return this.index >= this.input.length;
1555
+ }
1556
+ current() {
1557
+ return this.input[this.index] ?? "";
1558
+ }
1559
+ advance() {
1560
+ this.index += 1;
1561
+ }
1562
+ parseRepeat() {
1563
+ this.advance();
1564
+ const sequence = this.parseRepeatSequence();
1565
+ if (sequence.length === 0) {
1566
+ throw this.error("Repeat block sequence cannot be empty.");
1567
+ }
1568
+ this.skipWhitespace();
1569
+ const termination = this.parseRepeatTermination();
1570
+ return buildRepeatBlock(sequence, termination);
1571
+ }
1572
+ parseRepeatSequence() {
1573
+ const sequence = [];
1574
+ while (true) {
1575
+ this.skipSeparators();
1576
+ if (this.isDone()) {
1577
+ throw this.error("Unmatched group delimiter.");
1578
+ }
1579
+ if (this.current() === ")") {
1580
+ this.advance();
1581
+ break;
1582
+ }
1583
+ if (this.current() === "(") {
1584
+ sequence.push(this.parseRepeat());
1585
+ continue;
1586
+ }
1587
+ sequence.push(this.parseStitchInstruction());
1588
+ }
1589
+ return sequence;
1590
+ }
1591
+ parseRepeatTermination() {
1592
+ if (this.current() === "*" || this.current().toLowerCase() === "x") {
1593
+ return this.parseTimesTermination();
1594
+ }
1595
+ if (this.consumeKeyword("until")) {
1596
+ return this.parseUntilTermination();
1597
+ }
1598
+ if (this.consumeKeyword("to")) {
1599
+ return this.parseToLastTermination();
1600
+ }
1601
+ return {
1602
+ maxRepeats: this.parseOptionalMaxRepeats(),
1603
+ mode: "to-end"
1604
+ };
1605
+ }
1606
+ parseTimesTermination() {
1607
+ this.advance();
1608
+ this.skipWhitespace();
1609
+ const times = this.readCount();
1610
+ if (times <= 0) {
1611
+ throw this.error("Repeat times must be greater than 0.");
1612
+ }
1613
+ if (this.parseOptionalMaxRepeats() !== void 0) {
1614
+ throw this.error("Max repeats cannot be combined with fixed repeat times.");
1615
+ }
1616
+ return { mode: "times", times };
1617
+ }
1618
+ parseUntilTermination() {
1619
+ this.skipWhitespace();
1620
+ const untilCount = this.readCount();
1621
+ if (untilCount <= 0) {
1622
+ throw this.error("Repeat until-stitches value must be greater than 0.");
1623
+ }
1624
+ this.consumeOptionalStitchUnit();
1625
+ this.skipWhitespace();
1626
+ const maxRepeats = this.parseOptionalMaxRepeats();
1627
+ if (this.consumeKeyword("remain") || this.consumeKeyword("remaining") || this.consumeKeyword("left")) {
1628
+ return {
1629
+ maxRepeats,
1630
+ mode: "until-remaining",
1631
+ remainingStitches: untilCount
1632
+ };
1633
+ }
1634
+ return {
1635
+ maxRepeats,
1636
+ mode: "until-stitches",
1637
+ targetStitches: untilCount
1638
+ };
1639
+ }
1640
+ parseToLastTermination() {
1641
+ this.skipWhitespace();
1642
+ if (!this.consumeKeyword("last")) {
1643
+ throw this.error("Expected 'last' after 'to' in repeat clause.");
1644
+ }
1645
+ this.skipWhitespace();
1646
+ const remainingStitches = this.readCount();
1647
+ if (remainingStitches <= 0) {
1648
+ throw this.error("Repeat remaining-stitches value must be greater than 0.");
1649
+ }
1650
+ this.consumeOptionalStitchUnit();
1651
+ return {
1652
+ maxRepeats: this.parseOptionalMaxRepeats(),
1653
+ mode: "until-remaining",
1654
+ remainingStitches
1655
+ };
1656
+ }
1657
+ consumeOptionalStitchUnit() {
1658
+ this.skipWhitespace();
1659
+ if (this.startsWithKeyword("stitches")) {
1660
+ this.consumeKeyword("stitches");
1661
+ return;
1662
+ }
1663
+ if (this.startsWithKeyword("stitch")) {
1664
+ this.consumeKeyword("stitch");
1665
+ return;
1666
+ }
1667
+ if (this.startsWithKeyword("sts")) {
1668
+ this.consumeKeyword("sts");
1669
+ return;
1670
+ }
1671
+ if (this.startsWithKeyword("st")) {
1672
+ this.consumeKeyword("st");
1673
+ }
1674
+ }
1675
+ startsWithKeyword(keyword) {
1676
+ const segment = this.input.slice(this.index, this.index + keyword.length).toLowerCase();
1677
+ if (segment !== keyword.toLowerCase()) {
1678
+ return false;
1679
+ }
1680
+ const charAfter = this.input[this.index + keyword.length];
1681
+ return charAfter === void 0 || /[^a-z]/i.test(charAfter);
1682
+ }
1683
+ consumeKeyword(keyword) {
1684
+ if (!this.startsWithKeyword(keyword)) {
1685
+ return false;
1686
+ }
1687
+ this.index += keyword.length;
1688
+ return true;
1689
+ }
1690
+ parseOptionalMaxRepeats() {
1691
+ const saved = this.index;
1692
+ this.skipWhitespace();
1693
+ if (!this.consumeKeyword("max")) {
1694
+ this.index = saved;
1695
+ return void 0;
1696
+ }
1697
+ this.skipWhitespace();
1698
+ const maxRepeats = this.readCount();
1699
+ if (maxRepeats <= 0) {
1700
+ throw this.error("Max repeats must be greater than 0.");
1701
+ }
1702
+ this.skipWhitespace();
1703
+ if (this.startsWithKeyword("repeats")) {
1704
+ this.consumeKeyword("repeats");
1705
+ } else if (this.startsWithKeyword("repeat")) {
1706
+ this.consumeKeyword("repeat");
1707
+ }
1708
+ return maxRepeats;
1709
+ }
1710
+ parseStitchInstruction() {
1711
+ const token = this.readWord();
1712
+ const lowered = token.toLowerCase();
1713
+ const shaping = this.parseExplicitShapingToken(lowered);
1714
+ if (shaping) {
1715
+ return shaping;
1716
+ }
1717
+ const parsed = this.resolveStitchToken(lowered);
1718
+ if (!parsed.stitchType) {
1719
+ throw this.error(`Invalid stitch token: ${token}.`);
1720
+ }
1721
+ const count = this.resolveInstructionCount(parsed.variant, parsed.count);
1722
+ const finalCount = count ?? 1;
1723
+ if (finalCount <= 0) {
1724
+ throw this.error("Stitch count must be greater than 0.");
1725
+ }
1726
+ return {
1727
+ count: finalCount,
1728
+ type: parsed.stitchType,
1729
+ variant: parsed.variant
1730
+ };
1731
+ }
1732
+ /**
1733
+ * Parse explicit shaping tokens into first-class shaping elements.
1734
+ *
1735
+ * Supported forms:
1736
+ * - shape-dec2
1737
+ * - shape-inc3
1738
+ * - shape-dec-left2
1739
+ * - shape-inc-right1
1740
+ */
1741
+ parseExplicitShapingToken(loweredToken) {
1742
+ const match = SHAPING_TOKEN_RE.exec(loweredToken);
1743
+ if (!match) {
1744
+ return null;
1745
+ }
1746
+ const shapingTypeToken = match[1] ?? "";
1747
+ const directionToken = match[2];
1748
+ const countToken = match[3] ?? "";
1749
+ const shapingType = shapingTypeToken === "dec" || shapingTypeToken === "decrease" ? "decrease" : "increase";
1750
+ let direction;
1751
+ if (directionToken === void 0) {
1752
+ direction = void 0;
1753
+ } else if (directionToken === "l" || directionToken === "left") {
1754
+ direction = "left";
1755
+ } else if (directionToken === "r" || directionToken === "right") {
1756
+ direction = "right";
1757
+ } else {
1758
+ direction = "centre";
1759
+ }
1760
+ const count = Number.parseInt(countToken, 10);
1761
+ if (!Number.isInteger(count) || count <= 0) {
1762
+ throw this.error("Shaping count must be greater than 0.");
1763
+ }
1764
+ return {
1765
+ count,
1766
+ direction,
1767
+ shapingType,
1768
+ type: "shaping"
1769
+ };
1770
+ }
1771
+ applyResolvedToken(parsed, loweredToken) {
1772
+ if (this.registry.isVariant(loweredToken)) {
1773
+ parsed.stitchType = this.registry.resolve(loweredToken) ?? void 0;
1774
+ parsed.variant = loweredToken;
1775
+ const defaultCount = this.registry.getDefaultCount(loweredToken);
1776
+ if (defaultCount > 1) {
1777
+ parsed.count = defaultCount;
1778
+ }
1779
+ return;
1780
+ }
1781
+ parsed.stitchType = this.registry.resolve(loweredToken) ?? void 0;
1782
+ }
1783
+ parseCompactStitchToken(loweredToken) {
1784
+ const compactMatch = COMPACT_STITCH_TOKEN_RE.exec(loweredToken);
1785
+ if (!compactMatch) {
1786
+ return null;
1787
+ }
1788
+ const base = compactMatch[1] ?? "";
1789
+ const parsedCount = Number.parseInt(compactMatch[2] ?? "", 10);
1790
+ return { base, parsedCount };
1791
+ }
1792
+ applyCompactToken(parsed, base, parsedCount) {
1793
+ if (this.registry.isVariant(base)) {
1794
+ if (this.registry.isLockedCountVariant(base)) {
1795
+ throw this.error(
1796
+ `'${base}' does not accept an explicit count; use a repeat block to work it multiple times.`
1797
+ );
1798
+ }
1799
+ parsed.stitchType = this.registry.resolve(base) ?? void 0;
1800
+ parsed.variant = base;
1801
+ parsed.count = parsedCount;
1802
+ return;
1803
+ }
1804
+ const baseType = this.registry.resolve(base) ?? void 0;
1805
+ if (baseType) {
1806
+ parsed.stitchType = baseType;
1807
+ parsed.count = parsedCount;
1808
+ }
1809
+ }
1810
+ /**
1811
+ * Resolve a token into its stitch type, variant, and any implicit or compact count.
1812
+ *
1813
+ * @param loweredToken - The already lower-cased token text.
1814
+ * @returns Parsed token metadata used to build a stitch instruction.
1815
+ */
1816
+ resolveStitchToken(loweredToken) {
1817
+ const parsed = {};
1818
+ this.applyResolvedToken(parsed, loweredToken);
1819
+ if (!parsed.stitchType) {
1820
+ const compactToken = this.parseCompactStitchToken(loweredToken);
1821
+ if (compactToken) {
1822
+ this.applyCompactToken(parsed, compactToken.base, compactToken.parsedCount);
1823
+ }
1824
+ }
1825
+ return parsed;
1826
+ }
1827
+ /**
1828
+ * Finalise the stitch count for an instruction, enforcing locked-count variants.
1829
+ *
1830
+ * @param variant - The parsed stitch variant, when present.
1831
+ * @param baseCount - A count derived from defaults or compact token syntax.
1832
+ * @returns The explicit or inferred count, or undefined when no count is provided.
1833
+ */
1834
+ resolveInstructionCount(variant, baseCount) {
1835
+ if (variant !== void 0 && this.registry.isLockedCountVariant(variant)) {
1836
+ const saved = this.index;
1837
+ this.skipWhitespace();
1838
+ if (this.peekDigit()) {
1839
+ throw this.error(
1840
+ `'${variant}' does not accept an explicit count; use a repeat block to work it multiple times.`
1841
+ );
1842
+ }
1843
+ this.index = saved;
1844
+ return baseCount;
1845
+ }
1846
+ let count = baseCount;
1847
+ if (count === void 0) {
1848
+ const saved = this.index;
1849
+ this.skipWhitespace();
1850
+ if (this.peekDigit()) {
1851
+ count = this.readCount();
1852
+ } else {
1853
+ this.index = saved;
1854
+ }
1855
+ }
1856
+ return count;
1857
+ }
1858
+ /**
1859
+ * Read the next stitch-token word from the current parser cursor.
1860
+ *
1861
+ * @returns The parsed word token.
1862
+ * @throws Error when no word token starts at the current cursor.
1863
+ */
1864
+ readWord() {
1865
+ const token = readWordToken(this.input, this.index);
1866
+ if (!token) {
1867
+ throw this.error("Expected stitch instruction.");
1868
+ }
1869
+ this.index = token.nextIndex;
1870
+ return token.value;
1871
+ }
1872
+ /**
1873
+ * Read the next decimal count token from the current parser cursor.
1874
+ *
1875
+ * @returns The parsed numeric count.
1876
+ * @throws Error when no numeric token starts at the current cursor.
1877
+ */
1878
+ readCount() {
1879
+ const token = readCountToken(this.input, this.index);
1880
+ if (!token) {
1881
+ throw this.error("Expected a numeric count.");
1882
+ }
1883
+ this.index = token.nextIndex;
1884
+ return Number.parseInt(token.value, 10);
1885
+ }
1886
+ peekDigit() {
1887
+ return isDigitAt(this.input, this.index);
1888
+ }
1889
+ skipWhitespace() {
1890
+ this.index = skipWhitespaceFrom(this.input, this.index);
1891
+ }
1892
+ skipSeparators() {
1893
+ this.index = skipSeparatorsFrom(this.input, this.index);
1894
+ }
1895
+ error(message) {
1896
+ return new Error(`${message} At position ${this.index + 1}.`);
1897
+ }
1898
+ };
1899
+
1900
+ // src/parser/edgeParser.ts
1901
+ var EDGE_CONTENT_CAPTURE_FRAGMENT = String.raw`\[([^\]]*)\]`;
1902
+ var OPTIONAL_WHITESPACE_FRAGMENT = String.raw`\s*`;
1903
+ function buildEdgeRegex(side, syntax, marker) {
1904
+ const anchorStart = side === "start" ? "^" : "";
1905
+ const anchorEnd = side === "end" ? "$" : "";
1906
+ const label = side === "start" ? "edge-start" : "edge-end";
1907
+ const body = syntax === "named" ? `${label}${OPTIONAL_WHITESPACE_FRAGMENT}:${OPTIONAL_WHITESPACE_FRAGMENT}${EDGE_CONTENT_CAPTURE_FRAGMENT}` : `${EDGE_CONTENT_CAPTURE_FRAGMENT}${OPTIONAL_WHITESPACE_FRAGMENT}${marker}`;
1908
+ return new RegExp(
1909
+ `${anchorStart}${OPTIONAL_WHITESPACE_FRAGMENT}${body}${OPTIONAL_WHITESPACE_FRAGMENT}${anchorEnd}`,
1910
+ "i"
1911
+ );
1912
+ }
1913
+ var EDGE_END_NAMED_RE = buildEdgeRegex("end", "named", "e");
1914
+ var EDGE_END_SHORT_RE = buildEdgeRegex("end", "short", "e");
1915
+ var EDGE_START_NAMED_RE = buildEdgeRegex("start", "named", "s");
1916
+ var EDGE_START_SHORT_RE = buildEdgeRegex("start", "short", "s");
1917
+ var EDGE_END_PATTERNS = [EDGE_END_NAMED_RE, EDGE_END_SHORT_RE];
1918
+ var EDGE_START_PATTERNS = [EDGE_START_NAMED_RE, EDGE_START_SHORT_RE];
1919
+ function parseEdgeInstructions(content, registry) {
1920
+ if (content.trim().length === 0) {
1921
+ return [];
1922
+ }
1923
+ const parser = new RowDslParser(content, registry);
1924
+ const parsed = parser.parseElements();
1925
+ const stitches = [];
1926
+ for (const element of parsed) {
1927
+ if (element.type === "repeat") {
1928
+ throw new Error("Only stitch instructions are allowed inside edge instructions.");
1929
+ }
1930
+ stitches.push(element);
1931
+ }
1932
+ return stitches;
1933
+ }
1934
+ function findFirstMatch(input, patterns) {
1935
+ for (const regex of patterns) {
1936
+ const match = regex.exec(input);
1937
+ if (match) {
1938
+ return { match, regex };
1939
+ }
1940
+ }
1941
+ return null;
1942
+ }
1943
+ function extractEdge(input, patterns, registry) {
1944
+ const matchData = findFirstMatch(input, patterns);
1945
+ if (!matchData) {
1946
+ return null;
1947
+ }
1948
+ const content = matchData.match[1] ?? "";
1949
+ const edge = parseEdgeInstructions(content, registry);
1950
+ const body = input.replace(matchData.regex, "").trim();
1951
+ return { body, edge };
1952
+ }
1953
+ function extractEdgeEnd(input, registry) {
1954
+ const extracted = extractEdge(input, EDGE_END_PATTERNS, registry);
1955
+ if (extracted) {
1956
+ return { body: extracted.body, edgeEnd: extracted.edge };
1957
+ }
1958
+ return { body: input.trim() };
1959
+ }
1960
+ function extractEdgeStart(input, registry) {
1961
+ const extracted = extractEdge(input, EDGE_START_PATTERNS, registry);
1962
+ if (extracted) {
1963
+ return { body: extracted.body, edgeStart: extracted.edge };
1964
+ }
1965
+ return { body: input.trim() };
1966
+ }
1967
+
1968
+ // src/parser/rowDslAst.ts
1969
+ var EXACTLY_ONE = 1;
1970
+ var NO_EDGES = 0;
1971
+ var ERR_EXPECTED_ONE_BODY = "Row DSL AST must contain exactly one body element.";
1972
+ var ERR_MAX_ONE_EDGE_START = "Row DSL AST can contain at most one edge-start element.";
1973
+ var ERR_MAX_ONE_EDGE_END = "Row DSL AST can contain at most one edge-end element.";
1974
+ function countRowDslAstElements(ast) {
1975
+ const counts = {
1976
+ bodyCount: 0,
1977
+ edgeEndCount: 0,
1978
+ edgeStartCount: 0
1979
+ };
1980
+ for (const element of ast.elements) {
1981
+ if (element.type === "body") {
1982
+ counts.bodyCount += 1;
1983
+ continue;
1984
+ }
1985
+ if (element.type === "edge-start") {
1986
+ counts.edgeStartCount += 1;
1987
+ continue;
1988
+ }
1989
+ counts.edgeEndCount += 1;
1990
+ }
1991
+ return counts;
1992
+ }
1993
+ function assertRowDslAstShape(counts) {
1994
+ if (counts.bodyCount !== EXACTLY_ONE) {
1995
+ throw new Error(ERR_EXPECTED_ONE_BODY);
1996
+ }
1997
+ if (counts.edgeStartCount > EXACTLY_ONE) {
1998
+ throw new Error(ERR_MAX_ONE_EDGE_START);
1999
+ }
2000
+ if (counts.edgeEndCount > EXACTLY_ONE) {
2001
+ throw new Error(ERR_MAX_ONE_EDGE_END);
2002
+ }
2003
+ }
2004
+ function applyEdgeStitches(row, edgeType, stitches) {
2005
+ if (stitches.length <= NO_EDGES) {
2006
+ return;
2007
+ }
2008
+ if (edgeType === "edge-start") {
2009
+ row.edgeStart = stitches;
2010
+ return;
2011
+ }
2012
+ row.edgeEnd = stitches;
2013
+ }
2014
+ function extractEdgesAndBody(input, registry) {
2015
+ const { body: bodyWithoutEnd, edgeEnd } = extractEdgeEnd(input, registry);
2016
+ const { body, edgeStart } = extractEdgeStart(bodyWithoutEnd, registry);
2017
+ return { body, edgeEnd, edgeStart };
2018
+ }
2019
+ function buildRowDslAstElements(elements, edgeStart, edgeEnd) {
2020
+ const astElements = [];
2021
+ if (edgeStart && edgeStart.length > NO_EDGES) {
2022
+ astElements.push({ stitches: edgeStart, type: "edge-start" });
2023
+ }
2024
+ astElements.push({ elements, type: "body" });
2025
+ if (edgeEnd && edgeEnd.length > NO_EDGES) {
2026
+ astElements.push({ stitches: edgeEnd, type: "edge-end" });
2027
+ }
2028
+ return astElements;
2029
+ }
2030
+ function isRowDslAstWellFormed(ast) {
2031
+ const counts = countRowDslAstElements(ast);
2032
+ return counts.bodyCount === EXACTLY_ONE && counts.edgeStartCount <= EXACTLY_ONE && counts.edgeEndCount <= EXACTLY_ONE;
2033
+ }
2034
+ function parseRowDslAst(input, registry) {
2035
+ const normalised = input.trim();
2036
+ if (normalised.length <= NO_EDGES) {
2037
+ return {
2038
+ elements: [{ elements: [], type: "body" }]
2039
+ };
2040
+ }
2041
+ const { body, edgeEnd, edgeStart } = extractEdgesAndBody(normalised, registry);
2042
+ const parser = new RowDslParser(body, registry);
2043
+ const elements = parser.parseElements();
2044
+ return { elements: buildRowDslAstElements(elements, edgeStart, edgeEnd) };
2045
+ }
2046
+ function parseCompatibleRowDslAst(input, registry) {
2047
+ return parseRowDslAst(normaliseCompatibilitySyntax(input), registry);
2048
+ }
2049
+ function resolveRowDslAst(ast) {
2050
+ const counts = countRowDslAstElements(ast);
2051
+ assertRowDslAstShape(counts);
2052
+ const row = { elements: [] };
2053
+ for (const element of ast.elements) {
2054
+ switch (element.type) {
2055
+ case "body":
2056
+ row.elements = element.elements;
2057
+ break;
2058
+ case "edge-start":
2059
+ applyEdgeStitches(row, "edge-start", element.stitches);
2060
+ break;
2061
+ case "edge-end":
2062
+ applyEdgeStitches(row, "edge-end", element.stitches);
2063
+ break;
2064
+ }
2065
+ }
2066
+ return row;
2067
+ }
2068
+
2069
+ // src/parser/rowDsl.ts
2070
+ function parseWithAstParser(parser, input, registry) {
2071
+ return resolveRowDslAst(parser(input, registry));
2072
+ }
2073
+ function compileWithParser(parser, input, totalStitches, registry) {
2074
+ return expandRowToStitches(parser(input, registry), totalStitches);
2075
+ }
2076
+ function parseRowDsl(input, registry) {
2077
+ return parseWithAstParser(parseRowDslAst, input, registry);
2078
+ }
2079
+ function parseCompatibleRowDsl(input, registry) {
2080
+ return parseWithAstParser(parseCompatibleRowDslAst, input, registry);
2081
+ }
2082
+ function compileRowDsl(input, totalStitches, registry) {
2083
+ return compileWithParser(parseRowDsl, input, totalStitches, registry);
2084
+ }
2085
+ function compileCompatibleRowDsl(input, totalStitches, registry) {
2086
+ return compileWithParser(parseCompatibleRowDsl, input, totalStitches, registry);
2087
+ }
2088
+
2089
+ // src/public/parser.ts
2090
+ function wrapParseError(error, operation) {
2091
+ return ensureKnittingMathError(
2092
+ error,
2093
+ (message, cause) => new KnittingMathParseError(message, { operation }, cause)
2094
+ );
2095
+ }
2096
+ function wrapValidationError(error, operation) {
2097
+ return ensureKnittingMathError(
2098
+ error,
2099
+ (message, cause) => new KnittingMathValidationError(message, { operation }, cause)
2100
+ );
2101
+ }
2102
+ function wrapExpansionError2(error, operation) {
2103
+ return ensureKnittingMathError(
2104
+ error,
2105
+ (message, cause) => new KnittingMathExpansionError(message, { operation }, cause)
2106
+ );
2107
+ }
2108
+ function parseRowDsl2(input, registry) {
2109
+ try {
2110
+ return parseRowDsl(input, registry);
2111
+ } catch (error) {
2112
+ throw wrapParseError(error, "parseRowDsl");
2113
+ }
2114
+ }
2115
+ function parseCompatibleRowDsl2(input, registry) {
2116
+ try {
2117
+ return parseCompatibleRowDsl(input, registry);
2118
+ } catch (error) {
2119
+ throw wrapParseError(error, "parseCompatibleRowDsl");
2120
+ }
2121
+ }
2122
+ function compileRowDsl2(input, totalStitches, registry) {
2123
+ try {
2124
+ return compileRowDsl(input, totalStitches, registry);
2125
+ } catch (error) {
2126
+ throw wrapExpansionError2(error, "compileRowDsl");
2127
+ }
2128
+ }
2129
+ function compileCompatibleRowDsl2(input, totalStitches, registry) {
2130
+ try {
2131
+ return compileCompatibleRowDsl(input, totalStitches, registry);
2132
+ } catch (error) {
2133
+ throw wrapExpansionError2(error, "compileCompatibleRowDsl");
2134
+ }
2135
+ }
2136
+ function rowToRowDsl2(row) {
2137
+ try {
2138
+ return rowToRowDsl(row);
2139
+ } catch (error) {
2140
+ throw wrapValidationError(error, "rowToRowDsl");
2141
+ }
2142
+ }
2143
+ function parseRowDslAst2(input, registry) {
2144
+ try {
2145
+ return parseRowDslAst(input, registry);
2146
+ } catch (error) {
2147
+ throw wrapParseError(error, "parseRowDslAst");
2148
+ }
2149
+ }
2150
+ function parseCompatibleRowDslAst2(input, registry) {
2151
+ try {
2152
+ return parseCompatibleRowDslAst(input, registry);
2153
+ } catch (error) {
2154
+ throw wrapParseError(error, "parseCompatibleRowDslAst");
2155
+ }
2156
+ }
2157
+ function resolveRowDslAst2(ast) {
2158
+ try {
2159
+ return resolveRowDslAst(ast);
2160
+ } catch (error) {
2161
+ throw wrapValidationError(error, "resolveRowDslAst");
2162
+ }
2163
+ }
2164
+
2165
+ // src/shaping/distribution.ts
2166
+ function distributeEvenly(totalStitches, operations) {
2167
+ if (operations <= 0) {
2168
+ return { spacing: [] };
2169
+ }
2170
+ const baseSpacing = Math.floor(totalStitches / operations);
2171
+ const remainder = totalStitches % operations;
2172
+ const spacing = [];
2173
+ for (let i = 0; i < operations; i++) {
2174
+ const shouldAddExtra = Math.floor(i * remainder / operations) !== Math.floor((i + 1) * remainder / operations);
2175
+ spacing.push(baseSpacing + (shouldAddExtra ? 1 : 0));
2176
+ }
2177
+ return { spacing };
2178
+ }
2179
+
2180
+ // src/shaping/shapingRules.ts
2181
+ var SHAPING_RULES = {
2182
+ decrease: { consume: 2, produce: 1 },
2183
+ // k2tog - A decrease consumes 2 stitches and produces 1.
2184
+ increase: { consume: 1, produce: 2 }
2185
+ // M1 - An increase consumes 1 stitch and produces 2 stitches.
2186
+ };
2187
+
2188
+ // src/shaping/shaping.ts
2189
+ function getShapingPositions(length, count) {
2190
+ if (count <= 0 || length <= 0) {
2191
+ return [];
2192
+ }
2193
+ const { spacing } = distributeEvenly(length, count);
2194
+ const positions = [];
2195
+ let current = 0;
2196
+ for (const gap of spacing) {
2197
+ positions.push(current);
2198
+ current += gap;
2199
+ }
2200
+ return positions.filter((position) => position >= 0 && position < length);
2201
+ }
2202
+ function applyPhysicalShaping(stitches, shapingType, positions) {
2203
+ const rule = SHAPING_RULES[shapingType];
2204
+ const result = [];
2205
+ let i = 0;
2206
+ let positionIndex = 0;
2207
+ while (i < stitches.length) {
2208
+ if (positionIndex < positions.length && i === positions[positionIndex]) {
2209
+ for (let j = 0; j < rule.produce; j++) {
2210
+ result.push({
2211
+ isShaping: true,
2212
+ operationIndex: positionIndex,
2213
+ shapingType,
2214
+ sourceIndex: i,
2215
+ type: shapingType
2216
+ });
2217
+ }
2218
+ i += rule.consume;
2219
+ positionIndex++;
2220
+ } else {
2221
+ result.push(stitches[i]);
2222
+ i++;
2223
+ }
2224
+ }
2225
+ return result;
2226
+ }
2227
+ function compressStitches(stitches) {
2228
+ const result = [];
2229
+ for (const stitch of stitches) {
2230
+ const last = result.at(-1);
2231
+ if ((last == null ? void 0 : last.type) === stitch.type) {
2232
+ last.count++;
2233
+ } else {
2234
+ result.push({ type: stitch.type, count: 1 });
2235
+ }
2236
+ }
2237
+ return result;
2238
+ }
2239
+ function shapeRow(input) {
2240
+ const { row, totalStitches, shaping } = input;
2241
+ if (shaping.count < 0) {
2242
+ throw new Error("Shaping count must be 0 or greater.");
2243
+ }
2244
+ const expanded = expandRowToStitchTypes(row, totalStitches).map((type, sourceIndex) => ({
2245
+ sourceIndex,
2246
+ type
2247
+ }));
2248
+ const rule = SHAPING_RULES[shaping.type];
2249
+ if (shaping.count * rule.consume > expanded.length) {
2250
+ throw new Error("Cannot apply more shaping operations than stitches in the row.");
2251
+ }
2252
+ const shapingPositions = getShapingPositions(expanded.length, shaping.count);
2253
+ const shapedStitches = applyPhysicalShaping(expanded, shaping.type, shapingPositions);
2254
+ const stitches = compressStitches(shapedStitches);
2255
+ const resultingStitches = totalStitches + shaping.count * (rule.produce - rule.consume);
2256
+ return { shapedStitches, stitches, totalStitches: resultingStitches };
2257
+ }
2258
+ function groupShapedStitchesByOperation(shapedStitches) {
2259
+ const groups = /* @__PURE__ */ new Map();
2260
+ for (let outputIndex = 0; outputIndex < shapedStitches.length; outputIndex += 1) {
2261
+ const stitch = shapedStitches[outputIndex];
2262
+ if (!stitch.isShaping || stitch.operationIndex === void 0 || stitch.shapingType === void 0) {
2263
+ continue;
2264
+ }
2265
+ const existing = groups.get(stitch.operationIndex);
2266
+ if (existing) {
2267
+ if (existing.shapingType !== stitch.shapingType) {
2268
+ throw new Error("Shaping operation contains mixed shaping types.");
2269
+ }
2270
+ existing.stitches.push(stitch);
2271
+ existing.outputIndices.push(outputIndex);
2272
+ existing.sourceIndices.push(stitch.sourceIndex);
2273
+ continue;
2274
+ }
2275
+ groups.set(stitch.operationIndex, {
2276
+ operationIndex: stitch.operationIndex,
2277
+ outputIndices: [outputIndex],
2278
+ shapingType: stitch.shapingType,
2279
+ sourceIndices: [stitch.sourceIndex],
2280
+ stitches: [stitch]
2281
+ });
2282
+ }
2283
+ return Array.from(groups.values());
2284
+ }
2285
+
2286
+ // src/shaping/shapingDsl.ts
2287
+ var SHAPING_CLAUSE_RE = /^(?:shape|shaping)\s*:?\s*(increase|decrease)\s+(\d+)$/i;
2288
+ var TOTAL_CLAUSE_RE = /^(?:total|total-stitches|stitches)\s*:?\s*(\d+)$/i;
2289
+ var ROW_PREFIX_RE = /^row\s*:\s*/i;
2290
+ function parsePositiveInteger(input, fieldName) {
2291
+ const value = Number.parseInt(input, 10);
2292
+ if (!Number.isFinite(value) || Number.isNaN(value) || value < 0) {
2293
+ throw new Error(`${fieldName} must be a non-negative integer.`);
2294
+ }
2295
+ return value;
2296
+ }
2297
+ function hasAtLeastOneInstruction(row) {
2298
+ return row.elements.length > 0 || row.edgeStart !== void 0 && row.edgeStart.length > 0 || row.edgeEnd !== void 0 && row.edgeEnd.length > 0;
2299
+ }
2300
+ function parseShapingDslAst(input) {
2301
+ const normalised = input.trim();
2302
+ if (normalised.length === 0) {
2303
+ throw new Error("Shaping DSL input cannot be empty.");
2304
+ }
2305
+ const clauses = normalised.split(";").map((part) => part.trim()).filter((part) => part.length > 0);
2306
+ if (clauses.length === 0) {
2307
+ throw new Error("Shaping DSL input cannot be empty.");
2308
+ }
2309
+ const elements = [];
2310
+ for (const clause of clauses) {
2311
+ const shapingMatch = SHAPING_CLAUSE_RE.exec(clause);
2312
+ if (shapingMatch) {
2313
+ elements.push({
2314
+ shapingCount: parsePositiveInteger(shapingMatch[2] ?? "", "Shaping count"),
2315
+ shapingType: (shapingMatch[1] ?? "").toLowerCase(),
2316
+ type: "shaping"
2317
+ });
2318
+ continue;
2319
+ }
2320
+ const totalMatch = TOTAL_CLAUSE_RE.exec(clause);
2321
+ if (totalMatch) {
2322
+ elements.push({
2323
+ totalStitches: parsePositiveInteger(totalMatch[1] ?? "", "Total stitches"),
2324
+ type: "total"
2325
+ });
2326
+ continue;
2327
+ }
2328
+ const isExplicitRow = ROW_PREFIX_RE.test(clause);
2329
+ const rowClause = clause.replace(ROW_PREFIX_RE, "").trim();
2330
+ if (isExplicitRow && rowClause.length === 0) {
2331
+ throw new Error("Row clause cannot be empty.");
2332
+ }
2333
+ elements.push({ row: parseCompatibleRowDsl(rowClause), type: "row" });
2334
+ }
2335
+ return { elements };
2336
+ }
2337
+ function ensureResolvedShapingInput(row, shapingType, shapingCount, totalStitches) {
2338
+ if (!row) {
2339
+ throw new Error("Missing row clause.");
2340
+ }
2341
+ if (!hasAtLeastOneInstruction(row)) {
2342
+ throw new Error("Row must contain at least one instruction.");
2343
+ }
2344
+ if (shapingType === void 0 || shapingCount === void 0) {
2345
+ throw new Error("Missing shaping clause.");
2346
+ }
2347
+ if (totalStitches === void 0) {
2348
+ throw new Error("Missing total stitches clause.");
2349
+ }
2350
+ return { row, shapingCount, shapingType, totalStitches };
2351
+ }
2352
+ function validateFeasibleShaping(shapingType, shapingCount, totalStitches) {
2353
+ const rule = SHAPING_RULES[shapingType];
2354
+ const stichesConsumed = shapingCount * rule.consume;
2355
+ if (stichesConsumed > totalStitches) {
2356
+ throw new Error(
2357
+ `Cannot apply ${shapingCount} ${shapingType}(s): would require ${stichesConsumed} stitches, but only ${totalStitches} total stitches available.`
2358
+ );
2359
+ }
2360
+ const delta = shapingCount * (rule.produce - rule.consume);
2361
+ const finalCount = totalStitches + delta;
2362
+ if (finalCount < 0) {
2363
+ throw new Error("Shaping results in negative stitch count.");
2364
+ }
2365
+ }
2366
+ function resolveShapingDslAst(ast) {
2367
+ let row;
2368
+ let shapingType;
2369
+ let shapingCount;
2370
+ let totalStitches;
2371
+ for (const element of ast.elements) {
2372
+ switch (element.type) {
2373
+ case "row":
2374
+ if (row !== void 0) {
2375
+ throw new Error("Multiple row clauses detected. Only one row definition is allowed.");
2376
+ }
2377
+ row = element.row;
2378
+ break;
2379
+ case "shaping":
2380
+ if (shapingType !== void 0) {
2381
+ throw new Error("Shaping clause was provided more than once.");
2382
+ }
2383
+ shapingType = element.shapingType;
2384
+ shapingCount = element.shapingCount;
2385
+ break;
2386
+ case "total":
2387
+ if (totalStitches !== void 0) {
2388
+ throw new Error("Total stitches clause was provided more than once.");
2389
+ }
2390
+ totalStitches = element.totalStitches;
2391
+ break;
2392
+ }
2393
+ }
2394
+ const resolved = ensureResolvedShapingInput(row, shapingType, shapingCount, totalStitches);
2395
+ validateFeasibleShaping(resolved.shapingType, resolved.shapingCount, resolved.totalStitches);
2396
+ return {
2397
+ row: resolved.row,
2398
+ shaping: {
2399
+ type: resolved.shapingType,
2400
+ count: resolved.shapingCount
2401
+ },
2402
+ totalStitches: resolved.totalStitches
2403
+ };
2404
+ }
2405
+ function parseShapingDsl(input) {
2406
+ return resolveShapingDslAst(parseShapingDslAst(input));
2407
+ }
2408
+ function compileShapingDsl(input) {
2409
+ return shapeRow(parseShapingDsl(input));
2410
+ }
2411
+
2412
+ // src/shaping/shapingDebug.ts
2413
+ var DEFAULT_SYMBOLS = {
2414
+ decrease: "\\",
2415
+ increase: "/",
2416
+ knit: "K",
2417
+ purl: "P"
2418
+ };
2419
+ var FALLBACK_SYMBOL = "?";
2420
+ var debugLogger = globalThis.console;
2421
+ function renderShapingDebug(shapedStitches, options = {}) {
2422
+ const { cellWidth = 1, markerSymbol = "^", symbols: symbolOverrides } = options;
2423
+ const symbolMap = {
2424
+ ...DEFAULT_SYMBOLS,
2425
+ ...symbolOverrides
2426
+ };
2427
+ const cells = [];
2428
+ const markerCells = [];
2429
+ const seenOperations = /* @__PURE__ */ new Set();
2430
+ for (const stitch of shapedStitches) {
2431
+ const symbol = symbolMap[stitch.type] ?? FALLBACK_SYMBOL;
2432
+ cells.push(symbol.padEnd(cellWidth).slice(0, cellWidth));
2433
+ const operationIndex = stitch.operationIndex;
2434
+ const isFirstOfOperation = stitch.isShaping && operationIndex !== void 0 && !seenOperations.has(operationIndex);
2435
+ if (isFirstOfOperation) {
2436
+ seenOperations.add(operationIndex);
2437
+ }
2438
+ markerCells.push(
2439
+ isFirstOfOperation ? markerSymbol.padEnd(cellWidth).slice(0, cellWidth) : " ".repeat(cellWidth)
2440
+ );
2441
+ }
2442
+ const separator = cellWidth > 1 ? " " : "";
2443
+ const shapingTypes = new Set(
2444
+ shapedStitches.filter((s) => s.isShaping && s.shapingType).map((s) => s.shapingType)
2445
+ );
2446
+ const legendParts = [];
2447
+ for (const type of shapingTypes) {
2448
+ const sym = symbolMap[type] ?? FALLBACK_SYMBOL;
2449
+ legendParts.push(`${sym} = ${type}`);
2450
+ }
2451
+ legendParts.push(`${markerSymbol} = shaping position`);
2452
+ return {
2453
+ legend: legendParts.join(" | "),
2454
+ markers: markerCells.join(separator),
2455
+ row: cells.join(separator)
2456
+ };
2457
+ }
2458
+ function renderShapingDebugForInput(input, options = {}) {
2459
+ return renderShapingDebug(shapeRow(input).shapedStitches, options);
2460
+ }
2461
+ function printShapingDebug(shapedStitches, label, options = {}) {
2462
+ const { legend, markers, row } = renderShapingDebug(shapedStitches, options);
2463
+ if (label) {
2464
+ debugLogger == null ? void 0 : debugLogger.log(label);
2465
+ }
2466
+ debugLogger == null ? void 0 : debugLogger.log(row);
2467
+ debugLogger == null ? void 0 : debugLogger.log(markers);
2468
+ debugLogger == null ? void 0 : debugLogger.log(legend);
2469
+ }
2470
+
2471
+ // src/public/shaping.ts
2472
+ function wrapParseError2(error, operation) {
2473
+ return ensureKnittingMathError(
2474
+ error,
2475
+ (message, cause) => new KnittingMathParseError(message, { operation }, cause)
2476
+ );
2477
+ }
2478
+ function wrapValidationError2(error, operation) {
2479
+ return ensureKnittingMathError(
2480
+ error,
2481
+ (message, cause) => new KnittingMathValidationError(message, { operation }, cause)
2482
+ );
2483
+ }
2484
+ function wrapExpansionError3(error, operation) {
2485
+ return ensureKnittingMathError(
2486
+ error,
2487
+ (message, cause) => new KnittingMathExpansionError(message, { operation }, cause)
2488
+ );
2489
+ }
2490
+ function parseShapingDslAst2(input) {
2491
+ try {
2492
+ return parseShapingDslAst(input);
2493
+ } catch (error) {
2494
+ throw wrapParseError2(error, "parseShapingDslAst");
2495
+ }
2496
+ }
2497
+ function parseShapingDsl2(input) {
2498
+ try {
2499
+ return parseShapingDsl(input);
2500
+ } catch (error) {
2501
+ throw wrapValidationError2(error, "parseShapingDsl");
2502
+ }
2503
+ }
2504
+ function compileShapingDsl2(input) {
2505
+ try {
2506
+ return compileShapingDsl(input);
2507
+ } catch (error) {
2508
+ throw wrapExpansionError3(error, "compileShapingDsl");
2509
+ }
2510
+ }
2511
+ function shapeRow2(input) {
2512
+ try {
2513
+ return shapeRow(input);
2514
+ } catch (error) {
2515
+ throw wrapExpansionError3(error, "shapeRow");
2516
+ }
2517
+ }
2518
+ function groupShapedStitchesByOperation2(shapedStitches) {
2519
+ try {
2520
+ return groupShapedStitchesByOperation(shapedStitches);
2521
+ } catch (error) {
2522
+ throw wrapValidationError2(error, "groupShapedStitchesByOperation");
2523
+ }
2524
+ }
2525
+ export {
2526
+ CM_TO_INCH,
2527
+ CM_TO_MM,
2528
+ INCH_TO_CM,
2529
+ KnittingMathError,
2530
+ KnittingMathExpansionError,
2531
+ KnittingMathParseError,
2532
+ KnittingMathValidationError,
2533
+ MM_TO_CM,
2534
+ calculateCastOn,
2535
+ calculateCastOnStitches,
2536
+ cmToRows,
2537
+ cmToStitches,
2538
+ compileCompatibleRowDsl2 as compileCompatibleRowDsl,
2539
+ compilePattern2 as compilePattern,
2540
+ compileRowDsl2 as compileRowDsl,
2541
+ compileShapingDsl2 as compileShapingDsl,
2542
+ convertLength,
2543
+ countStitchesInInstructions,
2544
+ createDefaultStitchRegistry,
2545
+ createEmptyStitchRegistry,
2546
+ createRepeatRow,
2547
+ ensureKnittingMathError,
2548
+ expandPattern2 as expandPattern,
2549
+ expandRow,
2550
+ expandRowToStitchTypes,
2551
+ expandRowToStitches,
2552
+ groupShapedStitchesByOperation2 as groupShapedStitchesByOperation,
2553
+ isRowDslAstWellFormed,
2554
+ normaliseMeasurement,
2555
+ parseCompatibleRowDsl2 as parseCompatibleRowDsl,
2556
+ parseCompatibleRowDslAst2 as parseCompatibleRowDslAst,
2557
+ parseRowDsl2 as parseRowDsl,
2558
+ parseRowDslAst2 as parseRowDslAst,
2559
+ parseShapingDsl2 as parseShapingDsl,
2560
+ parseShapingDslAst2 as parseShapingDslAst,
2561
+ printPattern2 as printPattern,
2562
+ printShapingDebug,
2563
+ renderShapingDebug,
2564
+ renderShapingDebugForInput,
2565
+ resolveRowDslAst2 as resolveRowDslAst,
2566
+ roundStitches,
2567
+ rowToRowDsl2 as rowToRowDsl,
2568
+ rowsPerCm,
2569
+ shapeRow2 as shapeRow,
2570
+ shapedExpandPattern2 as shapedExpandPattern,
2571
+ stitchesPerCm
2572
+ };