@mlightcad/mtext-parser 1.0.0 → 1.0.1

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/parser.js DELETED
@@ -1,1036 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MTextToken = exports.MTextContext = exports.TextScanner = exports.MTextParser = exports.MTextStroke = exports.MTextParagraphAlignment = exports.MTextLineAlignment = exports.TokenType = void 0;
4
- exports.rgb2int = rgb2int;
5
- exports.int2rgb = int2rgb;
6
- exports.caretDecode = caretDecode;
7
- exports.escapeDxfLineEndings = escapeDxfLineEndings;
8
- exports.hasInlineFormattingCodes = hasInlineFormattingCodes;
9
- /**
10
- * Token types used in MText parsing
11
- */
12
- var TokenType;
13
- (function (TokenType) {
14
- /** No token */
15
- TokenType[TokenType["NONE"] = 0] = "NONE";
16
- /** Word token with string data */
17
- TokenType[TokenType["WORD"] = 1] = "WORD";
18
- /** Stack token with [numerator, denominator, type] data */
19
- TokenType[TokenType["STACK"] = 2] = "STACK";
20
- /** Space token with no data */
21
- TokenType[TokenType["SPACE"] = 3] = "SPACE";
22
- /** Non-breaking space token with no data */
23
- TokenType[TokenType["NBSP"] = 4] = "NBSP";
24
- /** Tab token with no data */
25
- TokenType[TokenType["TABULATOR"] = 5] = "TABULATOR";
26
- /** New paragraph token with no data */
27
- TokenType[TokenType["NEW_PARAGRAPH"] = 6] = "NEW_PARAGRAPH";
28
- /** New column token with no data */
29
- TokenType[TokenType["NEW_COLUMN"] = 7] = "NEW_COLUMN";
30
- /** Wrap at dimension line token with no data */
31
- TokenType[TokenType["WRAP_AT_DIMLINE"] = 8] = "WRAP_AT_DIMLINE";
32
- /** Properties changed token with string data (full command) */
33
- TokenType[TokenType["PROPERTIES_CHANGED"] = 9] = "PROPERTIES_CHANGED";
34
- })(TokenType || (exports.TokenType = TokenType = {}));
35
- /**
36
- * Line alignment options for MText
37
- */
38
- var MTextLineAlignment;
39
- (function (MTextLineAlignment) {
40
- /** Align text to bottom */
41
- MTextLineAlignment[MTextLineAlignment["BOTTOM"] = 0] = "BOTTOM";
42
- /** Align text to middle */
43
- MTextLineAlignment[MTextLineAlignment["MIDDLE"] = 1] = "MIDDLE";
44
- /** Align text to top */
45
- MTextLineAlignment[MTextLineAlignment["TOP"] = 2] = "TOP";
46
- })(MTextLineAlignment || (exports.MTextLineAlignment = MTextLineAlignment = {}));
47
- /**
48
- * Paragraph alignment options for MText
49
- */
50
- var MTextParagraphAlignment;
51
- (function (MTextParagraphAlignment) {
52
- /** Default alignment */
53
- MTextParagraphAlignment[MTextParagraphAlignment["DEFAULT"] = 0] = "DEFAULT";
54
- /** Left alignment */
55
- MTextParagraphAlignment[MTextParagraphAlignment["LEFT"] = 1] = "LEFT";
56
- /** Right alignment */
57
- MTextParagraphAlignment[MTextParagraphAlignment["RIGHT"] = 2] = "RIGHT";
58
- /** Center alignment */
59
- MTextParagraphAlignment[MTextParagraphAlignment["CENTER"] = 3] = "CENTER";
60
- /** Justified alignment */
61
- MTextParagraphAlignment[MTextParagraphAlignment["JUSTIFIED"] = 4] = "JUSTIFIED";
62
- /** Distributed alignment */
63
- MTextParagraphAlignment[MTextParagraphAlignment["DISTRIBUTED"] = 5] = "DISTRIBUTED";
64
- })(MTextParagraphAlignment || (exports.MTextParagraphAlignment = MTextParagraphAlignment = {}));
65
- /**
66
- * Text stroke options for MText
67
- */
68
- var MTextStroke;
69
- (function (MTextStroke) {
70
- /** No stroke */
71
- MTextStroke[MTextStroke["NONE"] = 0] = "NONE";
72
- /** Underline stroke */
73
- MTextStroke[MTextStroke["UNDERLINE"] = 1] = "UNDERLINE";
74
- /** Overline stroke */
75
- MTextStroke[MTextStroke["OVERLINE"] = 2] = "OVERLINE";
76
- /** Strike-through stroke */
77
- MTextStroke[MTextStroke["STRIKE_THROUGH"] = 4] = "STRIKE_THROUGH";
78
- })(MTextStroke || (exports.MTextStroke = MTextStroke = {}));
79
- /**
80
- * Special character encoding mapping
81
- */
82
- const SPECIAL_CHAR_ENCODING = {
83
- c: 'Ø',
84
- d: '°',
85
- p: '±',
86
- };
87
- /**
88
- * Character to paragraph alignment mapping
89
- */
90
- const CHAR_TO_ALIGN = {
91
- l: MTextParagraphAlignment.LEFT,
92
- r: MTextParagraphAlignment.RIGHT,
93
- c: MTextParagraphAlignment.CENTER,
94
- j: MTextParagraphAlignment.JUSTIFIED,
95
- d: MTextParagraphAlignment.DISTRIBUTED,
96
- };
97
- /**
98
- * Convert RGB tuple to integer color value
99
- * @param rgb - RGB color tuple
100
- * @returns Integer color value
101
- */
102
- function rgb2int(rgb) {
103
- const [r, g, b] = rgb;
104
- return (b << 16) | (g << 8) | r;
105
- }
106
- /**
107
- * Convert integer color value to RGB tuple
108
- * @param value - Integer color value
109
- * @returns RGB color tuple
110
- */
111
- function int2rgb(value) {
112
- const r = value & 0xff;
113
- const g = (value >> 8) & 0xff;
114
- const b = (value >> 16) & 0xff;
115
- return [r, g, b];
116
- }
117
- /**
118
- * DXF stores some special characters using caret notation. This function
119
- * decodes this notation to normalize the representation of special characters
120
- * in the string.
121
- * see: https://en.wikipedia.org/wiki/Caret_notation
122
- * @param text - Text to decode
123
- * @returns Decoded text
124
- */
125
- function caretDecode(text) {
126
- return text.replace(/\^(.)/g, (_, c) => {
127
- const code = c.charCodeAt(0);
128
- // Handle space after caret
129
- if (code === 32) {
130
- // Space
131
- return '^';
132
- }
133
- // Handle control characters
134
- if (code === 73) {
135
- // Tab (^I)
136
- return '\t';
137
- }
138
- if (code === 74) {
139
- // Line Feed (^J)
140
- return '\n';
141
- }
142
- if (code === 77) {
143
- // Carriage Return (^M)
144
- return '';
145
- }
146
- // Handle all other characters
147
- return '▯';
148
- });
149
- }
150
- /**
151
- * Escape DXF line endings
152
- * @param text - Text to escape
153
- * @returns Escaped text
154
- */
155
- function escapeDxfLineEndings(text) {
156
- return text.replace(/\r\n|\r|\n/g, '\\P');
157
- }
158
- /**
159
- * Check if text contains inline formatting codes
160
- * @param text - Text to check
161
- * @returns True if text contains formatting codes
162
- */
163
- function hasInlineFormattingCodes(text) {
164
- return text.replace(/\\P/g, '').replace(/\\~/g, '').includes('\\');
165
- }
166
- /**
167
- * Main parser class for MText content
168
- */
169
- class MTextParser {
170
- /**
171
- * Creates a new MTextParser instance
172
- * @param content - The MText content to parse
173
- * @param ctx - Optional initial MText context
174
- * @param yieldPropertyCommands - Whether to yield property change commands
175
- */
176
- constructor(content, ctx, yieldPropertyCommands = false) {
177
- this.ctxStack = [];
178
- this.continueStroke = false;
179
- this.scanner = new TextScanner(caretDecode(content));
180
- this.ctx = ctx || new MTextContext();
181
- this.yieldPropertyCommands = yieldPropertyCommands;
182
- }
183
- /**
184
- * Push current context onto the stack
185
- */
186
- pushCtx() {
187
- this.ctxStack.push(this.ctx);
188
- }
189
- /**
190
- * Pop context from the stack
191
- */
192
- popCtx() {
193
- if (this.ctxStack.length > 0) {
194
- this.ctx = this.ctxStack.pop();
195
- }
196
- }
197
- /**
198
- * Parse stacking expression (numerator/denominator)
199
- * @returns Tuple of [TokenType.STACK, [numerator, denominator, type]]
200
- */
201
- parseStacking() {
202
- const scanner = new TextScanner(this.extractExpression(true));
203
- let numerator = '';
204
- let denominator = '';
205
- let stackingType = '';
206
- const getNextChar = () => {
207
- let c = scanner.peek();
208
- let escape = false;
209
- if (c.charCodeAt(0) < 32) {
210
- c = ' ';
211
- }
212
- if (c === '\\') {
213
- escape = true;
214
- scanner.consume(1);
215
- c = scanner.peek();
216
- }
217
- scanner.consume(1);
218
- return [c, escape];
219
- };
220
- const parseNumerator = () => {
221
- let word = '';
222
- while (scanner.hasData) {
223
- const [c, escape] = getNextChar();
224
- if (!escape && '^/#'.includes(c)) {
225
- return [word, c];
226
- }
227
- word += c;
228
- }
229
- return [word, ''];
230
- };
231
- const parseDenominator = () => {
232
- let word = '';
233
- while (scanner.hasData) {
234
- const [c, escape] = getNextChar();
235
- if (escape && c === ';') {
236
- word += ';';
237
- }
238
- else {
239
- word += c;
240
- }
241
- }
242
- return word;
243
- };
244
- [numerator, stackingType] = parseNumerator();
245
- if (stackingType) {
246
- denominator = parseDenominator();
247
- }
248
- return [TokenType.STACK, [numerator, denominator, stackingType]];
249
- }
250
- /**
251
- * Parse MText properties
252
- * @param cmd - The property command to parse
253
- */
254
- parseProperties(cmd) {
255
- const newCtx = this.ctx.copy();
256
- switch (cmd) {
257
- case 'L':
258
- newCtx.underline = true;
259
- this.continueStroke = true;
260
- break;
261
- case 'l':
262
- newCtx.underline = false;
263
- if (!newCtx.hasAnyStroke) {
264
- this.continueStroke = false;
265
- }
266
- break;
267
- case 'O':
268
- newCtx.overline = true;
269
- this.continueStroke = true;
270
- break;
271
- case 'o':
272
- newCtx.overline = false;
273
- if (!newCtx.hasAnyStroke) {
274
- this.continueStroke = false;
275
- }
276
- break;
277
- case 'K':
278
- newCtx.strikeThrough = true;
279
- this.continueStroke = true;
280
- break;
281
- case 'k':
282
- newCtx.strikeThrough = false;
283
- if (!newCtx.hasAnyStroke) {
284
- this.continueStroke = false;
285
- }
286
- break;
287
- case 'A':
288
- this.parseAlign(newCtx);
289
- break;
290
- case 'C':
291
- this.parseAciColor(newCtx);
292
- break;
293
- case 'c':
294
- this.parseRgbColor(newCtx);
295
- break;
296
- case 'H':
297
- this.parseHeight(newCtx);
298
- break;
299
- case 'W':
300
- this.parseWidth(newCtx);
301
- break;
302
- case 'Q':
303
- this.parseOblique(newCtx);
304
- break;
305
- case 'T':
306
- this.parseCharTracking(newCtx);
307
- break;
308
- case 'p':
309
- this.parseParagraphProperties(newCtx);
310
- break;
311
- case 'f':
312
- case 'F':
313
- this.parseFontProperties(newCtx);
314
- break;
315
- default:
316
- throw new Error(`Unknown command: ${cmd}`);
317
- }
318
- newCtx.continueStroke = this.continueStroke;
319
- this.ctx = newCtx;
320
- }
321
- /**
322
- * Parse alignment property
323
- * @param ctx - The context to update
324
- */
325
- parseAlign(ctx) {
326
- const char = this.scanner.get();
327
- if ('012'.includes(char)) {
328
- ctx.align = parseInt(char);
329
- }
330
- else {
331
- ctx.align = MTextLineAlignment.BOTTOM;
332
- }
333
- this.consumeOptionalTerminator();
334
- }
335
- /**
336
- * Parse height property
337
- * @param ctx - The context to update
338
- */
339
- parseHeight(ctx) {
340
- const expr = this.extractFloatExpression(true);
341
- if (expr) {
342
- try {
343
- if (expr.endsWith('x')) {
344
- // For height command, treat x suffix as absolute value
345
- ctx.capHeight = Math.abs(parseFloat(expr.slice(0, -1)));
346
- }
347
- else {
348
- ctx.capHeight = Math.abs(parseFloat(expr));
349
- }
350
- }
351
- catch (e) {
352
- // If parsing fails, treat the entire command as literal text
353
- this.scanner.consume(-expr.length); // Rewind to before the expression
354
- return;
355
- }
356
- }
357
- this.consumeOptionalTerminator();
358
- }
359
- /**
360
- * Parse width property
361
- * @param ctx - The context to update
362
- */
363
- parseWidth(ctx) {
364
- const expr = this.extractFloatExpression(true);
365
- if (expr) {
366
- try {
367
- if (expr.endsWith('x')) {
368
- // For width command, treat x suffix as absolute value
369
- ctx.widthFactor = Math.abs(parseFloat(expr.slice(0, -1)));
370
- }
371
- else {
372
- ctx.widthFactor = Math.abs(parseFloat(expr));
373
- }
374
- }
375
- catch (e) {
376
- // If parsing fails, treat the entire command as literal text
377
- this.scanner.consume(-expr.length); // Rewind to before the expression
378
- return;
379
- }
380
- }
381
- this.consumeOptionalTerminator();
382
- }
383
- /**
384
- * Parse character tracking property
385
- * @param ctx - The context to update
386
- */
387
- parseCharTracking(ctx) {
388
- const expr = this.extractFloatExpression(true);
389
- if (expr) {
390
- try {
391
- if (expr.endsWith('x')) {
392
- // For tracking command, treat x suffix as absolute value
393
- ctx.charTrackingFactor = Math.abs(parseFloat(expr.slice(0, -1)));
394
- }
395
- else {
396
- ctx.charTrackingFactor = Math.abs(parseFloat(expr));
397
- }
398
- }
399
- catch (e) {
400
- // If parsing fails, treat the entire command as literal text
401
- this.scanner.consume(-expr.length); // Rewind to before the expression
402
- return;
403
- }
404
- }
405
- this.consumeOptionalTerminator();
406
- }
407
- /**
408
- * Parse float value or factor
409
- * @param value - Current value to apply factor to
410
- * @returns New value
411
- */
412
- parseFloatValueOrFactor(value) {
413
- const expr = this.extractFloatExpression(true);
414
- if (expr) {
415
- if (expr.endsWith('x')) {
416
- const factor = parseFloat(expr.slice(0, -1));
417
- value *= Math.abs(factor);
418
- }
419
- else {
420
- value = Math.abs(parseFloat(expr));
421
- }
422
- }
423
- return value;
424
- }
425
- /**
426
- * Parse oblique angle property
427
- * @param ctx - The context to update
428
- */
429
- parseOblique(ctx) {
430
- const obliqueExpr = this.extractFloatExpression(false);
431
- if (obliqueExpr) {
432
- ctx.oblique = parseFloat(obliqueExpr);
433
- }
434
- this.consumeOptionalTerminator();
435
- }
436
- /**
437
- * Parse ACI color property
438
- * @param ctx - The context to update
439
- */
440
- parseAciColor(ctx) {
441
- const aciExpr = this.extractIntExpression();
442
- if (aciExpr) {
443
- const aci = parseInt(aciExpr);
444
- if (aci < 257) {
445
- ctx.aci = aci;
446
- ctx.rgb = null;
447
- }
448
- }
449
- this.consumeOptionalTerminator();
450
- }
451
- /**
452
- * Parse RGB color property
453
- * @param ctx - The context to update
454
- */
455
- parseRgbColor(ctx) {
456
- const rgbExpr = this.extractIntExpression();
457
- if (rgbExpr) {
458
- const value = parseInt(rgbExpr) & 0xffffff;
459
- const [b, g, r] = int2rgb(value);
460
- ctx.rgb = [r, g, b];
461
- }
462
- this.consumeOptionalTerminator();
463
- }
464
- /**
465
- * Extract float expression from scanner
466
- * @param relative - Whether to allow relative values (ending in 'x')
467
- * @returns Extracted expression
468
- */
469
- extractFloatExpression(relative = false) {
470
- const pattern = relative
471
- ? /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?x?/
472
- : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/;
473
- const match = this.scanner.tail.match(pattern);
474
- if (match) {
475
- const result = match[0];
476
- this.scanner.consume(result.length);
477
- return result;
478
- }
479
- return '';
480
- }
481
- /**
482
- * Extract integer expression from scanner
483
- * @returns Extracted expression
484
- */
485
- extractIntExpression() {
486
- const match = this.scanner.tail.match(/^\d+/);
487
- if (match) {
488
- const result = match[0];
489
- this.scanner.consume(result.length);
490
- return result;
491
- }
492
- return '';
493
- }
494
- /**
495
- * Extract expression until semicolon or end
496
- * @param escape - Whether to handle escaped semicolons
497
- * @returns Extracted expression
498
- */
499
- extractExpression(escape = false) {
500
- const stop = this.scanner.find(';', escape);
501
- if (stop < 0) {
502
- const expr = this.scanner.tail;
503
- this.scanner.consume(expr.length);
504
- return expr;
505
- }
506
- // Check if the semicolon is escaped by looking at the previous character
507
- const prevChar = this.scanner.peek(stop - this.scanner.currentIndex - 1);
508
- const isEscaped = prevChar === '\\';
509
- const expr = this.scanner.tail.slice(0, stop - this.scanner.currentIndex + (isEscaped ? 1 : 0));
510
- this.scanner.consume(expr.length + 1);
511
- return expr;
512
- }
513
- /**
514
- * Parse font properties
515
- * @param ctx - The context to update
516
- */
517
- parseFontProperties(ctx) {
518
- const parts = this.extractExpression().split('|');
519
- if (parts.length > 0 && parts[0]) {
520
- const name = parts[0];
521
- let style = 'Regular';
522
- let weight = 400;
523
- for (const part of parts.slice(1)) {
524
- if (part.startsWith('b1')) {
525
- weight = 700;
526
- }
527
- else if (part.startsWith('i1')) {
528
- style = 'Italic';
529
- }
530
- }
531
- ctx.fontFace = {
532
- family: name,
533
- style,
534
- weight,
535
- };
536
- }
537
- }
538
- /**
539
- * Parse paragraph properties from the MText content
540
- * Handles properties like indentation, alignment, and tab stops
541
- * @param ctx - The context to update
542
- */
543
- parseParagraphProperties(ctx) {
544
- const scanner = new TextScanner(this.extractExpression());
545
- /** Current indentation value */
546
- let indent = ctx.paragraph.indent;
547
- /** Left margin value */
548
- let left = ctx.paragraph.left;
549
- /** Right margin value */
550
- let right = ctx.paragraph.right;
551
- /** Current paragraph alignment */
552
- let align = ctx.paragraph.align;
553
- /** Array of tab stop positions and types */
554
- let tabStops = [];
555
- /**
556
- * Parse a floating point number from the scanner's current position
557
- * Handles optional sign, decimal point, and scientific notation
558
- * @returns The parsed float value, or 0 if no valid number is found
559
- */
560
- const parseFloatValue = () => {
561
- const match = scanner.tail.match(/^[+-]?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?/);
562
- if (match) {
563
- const value = parseFloat(match[0]);
564
- scanner.consume(match[0].length);
565
- while (scanner.peek() === ',') {
566
- scanner.consume(1);
567
- }
568
- return value;
569
- }
570
- return 0;
571
- };
572
- while (scanner.hasData) {
573
- const cmd = scanner.get();
574
- switch (cmd) {
575
- case 'i': // Indentation
576
- indent = parseFloatValue();
577
- break;
578
- case 'l': // Left margin
579
- left = parseFloatValue();
580
- break;
581
- case 'r': // Right margin
582
- right = parseFloatValue();
583
- break;
584
- case 'x': // Skip
585
- break;
586
- case 'q': {
587
- // Alignment
588
- const adjustment = scanner.get();
589
- align = CHAR_TO_ALIGN[adjustment] || MTextParagraphAlignment.DEFAULT;
590
- while (scanner.peek() === ',') {
591
- scanner.consume(1);
592
- }
593
- break;
594
- }
595
- case 't': // Tab stops
596
- tabStops = [];
597
- while (scanner.hasData) {
598
- const type = scanner.peek();
599
- if (type === 'r' || type === 'c') {
600
- scanner.consume(1);
601
- const value = parseFloatValue();
602
- tabStops.push(type + value.toString());
603
- }
604
- else {
605
- const value = parseFloatValue();
606
- if (!isNaN(value)) {
607
- tabStops.push(value);
608
- }
609
- else {
610
- scanner.consume(1);
611
- }
612
- }
613
- }
614
- break;
615
- }
616
- }
617
- ctx.paragraph = {
618
- indent,
619
- left,
620
- right,
621
- align,
622
- tab_stops: tabStops,
623
- };
624
- }
625
- /**
626
- * Consume optional terminator (semicolon)
627
- */
628
- consumeOptionalTerminator() {
629
- if (this.scanner.peek() === ';') {
630
- this.scanner.consume(1);
631
- }
632
- }
633
- /**
634
- * Parse MText content into tokens
635
- * @yields MTextToken objects
636
- */
637
- *parse() {
638
- const wordToken = TokenType.WORD;
639
- const spaceToken = TokenType.SPACE;
640
- let followupToken = null;
641
- const nextToken = () => {
642
- let word = '';
643
- while (this.scanner.hasData) {
644
- let escape = false;
645
- let letter = this.scanner.peek();
646
- const cmdStartIndex = this.scanner.currentIndex;
647
- // Handle control characters first
648
- if (letter.charCodeAt(0) < 32) {
649
- this.scanner.consume(1); // Always consume the control character
650
- if (letter === '\t') {
651
- return [TokenType.TABULATOR, null];
652
- }
653
- if (letter === '\n') {
654
- return [TokenType.NEW_PARAGRAPH, null];
655
- }
656
- letter = ' ';
657
- }
658
- if (letter === '\\') {
659
- if ('\\{}'.includes(this.scanner.peek(1))) {
660
- escape = true;
661
- this.scanner.consume(1);
662
- letter = this.scanner.peek();
663
- }
664
- else {
665
- if (word) {
666
- return [wordToken, word];
667
- }
668
- this.scanner.consume(1);
669
- const cmd = this.scanner.get();
670
- switch (cmd) {
671
- case '~':
672
- return [TokenType.NBSP, null];
673
- case 'P':
674
- return [TokenType.NEW_PARAGRAPH, null];
675
- case 'N':
676
- return [TokenType.NEW_COLUMN, null];
677
- case 'X':
678
- return [TokenType.WRAP_AT_DIMLINE, null];
679
- case 'S':
680
- return this.parseStacking();
681
- default:
682
- if (cmd) {
683
- try {
684
- this.parseProperties(cmd);
685
- if (this.yieldPropertyCommands) {
686
- return [
687
- TokenType.PROPERTIES_CHANGED,
688
- this.scanner.tail.slice(cmdStartIndex, this.scanner.currentIndex),
689
- ];
690
- }
691
- // After processing a property command, continue with normal parsing
692
- continue;
693
- }
694
- catch (e) {
695
- const commandText = this.scanner.tail.slice(cmdStartIndex, this.scanner.currentIndex);
696
- word += commandText;
697
- }
698
- }
699
- }
700
- continue;
701
- }
702
- }
703
- if (letter === '%' && this.scanner.peek(1) === '%') {
704
- const code = this.scanner.peek(2).toLowerCase();
705
- const specialChar = SPECIAL_CHAR_ENCODING[code];
706
- if (specialChar) {
707
- this.scanner.consume(3);
708
- word += specialChar;
709
- continue;
710
- }
711
- else {
712
- // Skip invalid special character codes
713
- this.scanner.consume(3);
714
- continue;
715
- }
716
- }
717
- if (letter === ' ') {
718
- if (word) {
719
- this.scanner.consume(1);
720
- followupToken = spaceToken;
721
- return [wordToken, word];
722
- }
723
- this.scanner.consume(1);
724
- return [spaceToken, null];
725
- }
726
- if (!escape) {
727
- if (letter === '{') {
728
- if (word) {
729
- return [wordToken, word];
730
- }
731
- this.scanner.consume(1);
732
- this.pushCtx();
733
- continue;
734
- }
735
- else if (letter === '}') {
736
- if (word) {
737
- return [wordToken, word];
738
- }
739
- this.scanner.consume(1);
740
- this.popCtx();
741
- continue;
742
- }
743
- }
744
- this.scanner.consume(1);
745
- if (letter.charCodeAt(0) >= 32) {
746
- word += letter;
747
- }
748
- }
749
- if (word) {
750
- return [wordToken, word];
751
- }
752
- return [TokenType.NONE, null];
753
- };
754
- while (true) {
755
- const [type, data] = nextToken();
756
- if (type) {
757
- yield new MTextToken(type, this.ctx, data);
758
- if (followupToken) {
759
- yield new MTextToken(followupToken, this.ctx, null);
760
- followupToken = null;
761
- }
762
- }
763
- else {
764
- break;
765
- }
766
- }
767
- }
768
- }
769
- exports.MTextParser = MTextParser;
770
- /**
771
- * Text scanner for parsing MText content
772
- */
773
- class TextScanner {
774
- /**
775
- * Create a new text scanner
776
- * @param text - The text to scan
777
- */
778
- constructor(text) {
779
- this.text = text;
780
- this.textLen = text.length;
781
- this._index = 0;
782
- }
783
- /**
784
- * Get the current index in the text
785
- */
786
- get currentIndex() {
787
- return this._index;
788
- }
789
- /**
790
- * Check if the scanner has reached the end of the text
791
- */
792
- get isEmpty() {
793
- return this._index >= this.textLen;
794
- }
795
- /**
796
- * Check if there is more text to scan
797
- */
798
- get hasData() {
799
- return this._index < this.textLen;
800
- }
801
- /**
802
- * Get the next character and advance the index
803
- * @returns The next character, or empty string if at end
804
- */
805
- get() {
806
- if (this.isEmpty) {
807
- return '';
808
- }
809
- const char = this.text[this._index];
810
- this._index++;
811
- return char;
812
- }
813
- /**
814
- * Advance the index by the specified count
815
- * @param count - Number of characters to advance
816
- */
817
- consume(count = 1) {
818
- if (count < 1)
819
- throw new Error(`Invalid consume count: ${count}`);
820
- this._index = Math.min(this._index + count, this.textLen);
821
- }
822
- /**
823
- * Look at a character without advancing the index
824
- * @param offset - Offset from current position
825
- * @returns The character at the offset position, or empty string if out of bounds
826
- */
827
- peek(offset = 0) {
828
- if (offset < 0)
829
- throw new Error(`Invalid peek offset: ${offset}`);
830
- const index = this._index + offset;
831
- if (index >= this.textLen) {
832
- return '';
833
- }
834
- return this.text[index];
835
- }
836
- /**
837
- * Find the next occurrence of a character
838
- * @param char - The character to find
839
- * @param escape - Whether to handle escaped characters
840
- * @returns Index of the character, or -1 if not found
841
- */
842
- find(char, escape = false) {
843
- let index = this._index;
844
- while (index < this.textLen) {
845
- if (escape && this.text[index] === '\\') {
846
- if (index + 1 < this.textLen) {
847
- if (this.text[index + 1] === char) {
848
- return index + 1;
849
- }
850
- index += 2;
851
- continue;
852
- }
853
- index++;
854
- continue;
855
- }
856
- if (this.text[index] === char) {
857
- return index;
858
- }
859
- index++;
860
- }
861
- return -1;
862
- }
863
- /**
864
- * Get the remaining text from the current position
865
- */
866
- get tail() {
867
- return this.text.slice(this._index);
868
- }
869
- /**
870
- * Check if the next character is a space
871
- */
872
- isNextSpace() {
873
- return this.peek() === ' ';
874
- }
875
- /**
876
- * Consume spaces until a non-space character is found
877
- * @returns Number of spaces consumed
878
- */
879
- consumeSpaces() {
880
- let count = 0;
881
- while (this.isNextSpace()) {
882
- this.consume();
883
- count++;
884
- }
885
- return count;
886
- }
887
- }
888
- exports.TextScanner = TextScanner;
889
- /**
890
- * MText context class for managing text formatting state
891
- */
892
- class MTextContext {
893
- constructor() {
894
- this._stroke = 0;
895
- /** Whether to continue stroke formatting */
896
- this.continueStroke = false;
897
- this._aci = 7;
898
- /** RGB color value, or null if using ACI */
899
- this.rgb = null;
900
- /** Line alignment */
901
- this.align = MTextLineAlignment.BOTTOM;
902
- /** Font face properties */
903
- this.fontFace = { family: '', style: 'Regular', weight: 400 };
904
- /** Capital letter height */
905
- this.capHeight = 1.0;
906
- /** Character width factor */
907
- this.widthFactor = 1.0;
908
- /** Character tracking factor */
909
- this.charTrackingFactor = 1.0;
910
- /** Oblique angle */
911
- this.oblique = 0.0;
912
- /** Paragraph properties */
913
- this.paragraph = {
914
- indent: 0,
915
- left: 0,
916
- right: 0,
917
- align: MTextParagraphAlignment.DEFAULT,
918
- tab_stops: [],
919
- };
920
- }
921
- /**
922
- * Get the ACI color value
923
- */
924
- get aci() {
925
- return this._aci;
926
- }
927
- /**
928
- * Set the ACI color value
929
- * @param value - ACI color value (0-256)
930
- * @throws Error if value is out of range
931
- */
932
- set aci(value) {
933
- if (value >= 0 && value <= 256) {
934
- this._aci = value;
935
- this.rgb = null;
936
- }
937
- else {
938
- throw new Error('ACI not in range [0, 256]');
939
- }
940
- }
941
- /**
942
- * Get whether text is underlined
943
- */
944
- get underline() {
945
- return Boolean(this._stroke & MTextStroke.UNDERLINE);
946
- }
947
- /**
948
- * Set whether text is underlined
949
- * @param value - Whether to underline
950
- */
951
- set underline(value) {
952
- this._setStrokeState(MTextStroke.UNDERLINE, value);
953
- }
954
- /**
955
- * Get whether text has strike-through
956
- */
957
- get strikeThrough() {
958
- return Boolean(this._stroke & MTextStroke.STRIKE_THROUGH);
959
- }
960
- /**
961
- * Set whether text has strike-through
962
- * @param value - Whether to strike through
963
- */
964
- set strikeThrough(value) {
965
- this._setStrokeState(MTextStroke.STRIKE_THROUGH, value);
966
- }
967
- /**
968
- * Get whether text has overline
969
- */
970
- get overline() {
971
- return Boolean(this._stroke & MTextStroke.OVERLINE);
972
- }
973
- /**
974
- * Set whether text has overline
975
- * @param value - Whether to overline
976
- */
977
- set overline(value) {
978
- this._setStrokeState(MTextStroke.OVERLINE, value);
979
- }
980
- /**
981
- * Check if any stroke formatting is active
982
- */
983
- get hasAnyStroke() {
984
- return Boolean(this._stroke);
985
- }
986
- /**
987
- * Set the state of a stroke type
988
- * @param stroke - The stroke type to set
989
- * @param state - Whether to enable or disable the stroke
990
- */
991
- _setStrokeState(stroke, state = true) {
992
- if (state) {
993
- this._stroke |= stroke;
994
- }
995
- else {
996
- this._stroke &= ~stroke;
997
- }
998
- }
999
- /**
1000
- * Create a copy of this context
1001
- * @returns A new context with the same properties
1002
- */
1003
- copy() {
1004
- const ctx = new MTextContext();
1005
- ctx._stroke = this._stroke;
1006
- ctx.continueStroke = this.continueStroke;
1007
- ctx._aci = this._aci;
1008
- ctx.rgb = this.rgb;
1009
- ctx.align = this.align;
1010
- ctx.fontFace = { ...this.fontFace };
1011
- ctx.capHeight = this.capHeight;
1012
- ctx.widthFactor = this.widthFactor;
1013
- ctx.charTrackingFactor = this.charTrackingFactor;
1014
- ctx.oblique = this.oblique;
1015
- ctx.paragraph = { ...this.paragraph };
1016
- return ctx;
1017
- }
1018
- }
1019
- exports.MTextContext = MTextContext;
1020
- /**
1021
- * Token class for MText parsing
1022
- */
1023
- class MTextToken {
1024
- /**
1025
- * Create a new MText token
1026
- * @param type - The token type
1027
- * @param ctx - The text context at this token
1028
- * @param data - Optional token data
1029
- */
1030
- constructor(type, ctx, data) {
1031
- this.type = type;
1032
- this.ctx = ctx;
1033
- this.data = data;
1034
- }
1035
- }
1036
- exports.MTextToken = MTextToken;