@litsx/jsx-authoring 0.1.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/src/index.js ADDED
@@ -0,0 +1,878 @@
1
+ import MagicString from "magic-string";
2
+
3
+ const PREFIX_TO_KIND = {
4
+ "@": "event",
5
+ ".": "prop",
6
+ "?": "bool",
7
+ };
8
+
9
+ const KIND_TO_PREFIX = {
10
+ event: "@",
11
+ prop: ".",
12
+ bool: "?",
13
+ };
14
+
15
+ const ATTR_NAME_CHAR = /[\w:-]/;
16
+ const TAG_NAME_START_CHAR = /[A-Za-z]/;
17
+ const TAG_NAME_CHAR = /[\w:.-]/;
18
+ const MACRO_NAME_START_CHAR = /[A-Za-z$_]/;
19
+ const MACRO_NAME_CHAR = /[A-Za-z0-9$_]/;
20
+
21
+ function isWhitespace(char) {
22
+ return char === " " || char === "\t" || char === "\n" || char === "\r";
23
+ }
24
+
25
+ function isReservedVirtualAttributeName(name) {
26
+ return /^__litsx_(event|prop|bool)_/.test(name);
27
+ }
28
+
29
+ function sanitizeIdentifierTailChar(char) {
30
+ return /[A-Za-z0-9$_]/.test(char) ? char : "_";
31
+ }
32
+
33
+ function encodeEditorVirtualAttributeName(name) {
34
+ const prefix = name[0];
35
+ const localName = name.slice(1);
36
+ const encodedPrefix = prefix === "@" ? "e" : prefix === "." ? "p" : "b";
37
+ return `${encodedPrefix}${Array.from(localName, sanitizeIdentifierTailChar).join("")}`;
38
+ }
39
+
40
+ function encodeEditorStaticHoistName(originalName, macroName) {
41
+ return `$${macroName}`;
42
+ }
43
+
44
+ function scanQuotedString(sourceText, start, quote) {
45
+ let index = start + 1;
46
+
47
+ while (index < sourceText.length) {
48
+ const char = sourceText[index];
49
+ if (char === "\\") {
50
+ index += 2;
51
+ continue;
52
+ }
53
+ if (char === quote) {
54
+ return index + 1;
55
+ }
56
+ index += 1;
57
+ }
58
+
59
+ return index;
60
+ }
61
+
62
+ function scanLineComment(sourceText, start) {
63
+ let index = start + 2;
64
+ while (index < sourceText.length && sourceText[index] !== "\n") {
65
+ index += 1;
66
+ }
67
+ return index;
68
+ }
69
+
70
+ function scanBlockComment(sourceText, start) {
71
+ let index = start + 2;
72
+ while (index < sourceText.length) {
73
+ if (sourceText[index] === "*" && sourceText[index + 1] === "/") {
74
+ return index + 2;
75
+ }
76
+ index += 1;
77
+ }
78
+ return index;
79
+ }
80
+
81
+ function scanTemplateLiteral(sourceText, start) {
82
+ let index = start + 1;
83
+
84
+ while (index < sourceText.length) {
85
+ const char = sourceText[index];
86
+ if (char === "\\") {
87
+ index += 2;
88
+ continue;
89
+ }
90
+ if (char === "`") {
91
+ return index + 1;
92
+ }
93
+ if (char === "$" && sourceText[index + 1] === "{") {
94
+ index = scanBalancedBraces(sourceText, index + 1);
95
+ continue;
96
+ }
97
+ index += 1;
98
+ }
99
+
100
+ return index;
101
+ }
102
+
103
+ function scanBalancedBraces(sourceText, start) {
104
+ let depth = 0;
105
+ let index = start;
106
+
107
+ while (index < sourceText.length) {
108
+ const char = sourceText[index];
109
+ const next = sourceText[index + 1];
110
+
111
+ if (char === "'" || char === "\"") {
112
+ index = scanQuotedString(sourceText, index, char);
113
+ continue;
114
+ }
115
+
116
+ if (char === "`") {
117
+ index = scanTemplateLiteral(sourceText, index);
118
+ continue;
119
+ }
120
+
121
+ if (char === "/" && next === "/") {
122
+ index = scanLineComment(sourceText, index);
123
+ continue;
124
+ }
125
+
126
+ if (char === "/" && next === "*") {
127
+ index = scanBlockComment(sourceText, index);
128
+ continue;
129
+ }
130
+
131
+ if (char === "{") {
132
+ depth += 1;
133
+ index += 1;
134
+ continue;
135
+ }
136
+
137
+ if (char === "}") {
138
+ depth -= 1;
139
+ index += 1;
140
+ if (depth <= 0) {
141
+ return index;
142
+ }
143
+ continue;
144
+ }
145
+
146
+ index += 1;
147
+ }
148
+
149
+ return index;
150
+ }
151
+
152
+ function scanBalancedBracesWithJsx(sourceText, start, replacements, encodeAttributeName) {
153
+ let depth = 0;
154
+ let index = start;
155
+
156
+ while (index < sourceText.length) {
157
+ const char = sourceText[index];
158
+ const next = sourceText[index + 1];
159
+
160
+ if (char === "'" || char === "\"") {
161
+ index = scanQuotedString(sourceText, index, char);
162
+ continue;
163
+ }
164
+
165
+ if (char === "`") {
166
+ index = scanTemplateLiteral(sourceText, index);
167
+ continue;
168
+ }
169
+
170
+ if (char === "/" && next === "/") {
171
+ index = scanLineComment(sourceText, index);
172
+ continue;
173
+ }
174
+
175
+ if (char === "/" && next === "*") {
176
+ index = scanBlockComment(sourceText, index);
177
+ continue;
178
+ }
179
+
180
+ if (char === "<" && isLikelyJsxTagStart(sourceText, index)) {
181
+ index = scanJsxElement(sourceText, index, replacements, encodeAttributeName);
182
+ continue;
183
+ }
184
+
185
+ if (char === "{") {
186
+ depth += 1;
187
+ index += 1;
188
+ continue;
189
+ }
190
+
191
+ if (char === "}") {
192
+ depth -= 1;
193
+ index += 1;
194
+ if (depth <= 0) {
195
+ return index;
196
+ }
197
+ continue;
198
+ }
199
+
200
+ index += 1;
201
+ }
202
+
203
+ return index;
204
+ }
205
+
206
+ function trimTrailingWhitespaceAndComments(sourceText) {
207
+ let text = sourceText;
208
+ let changed = true;
209
+
210
+ while (changed) {
211
+ changed = false;
212
+
213
+ const trimmedWhitespace = text.replace(/\s+$/u, "");
214
+ if (trimmedWhitespace !== text) {
215
+ text = trimmedWhitespace;
216
+ changed = true;
217
+ }
218
+
219
+ const trimmedLineComment = text.replace(/\/\/[^\n\r]*$/u, "");
220
+ if (trimmedLineComment !== text) {
221
+ text = trimmedLineComment;
222
+ changed = true;
223
+ continue;
224
+ }
225
+
226
+ const trimmedBlockComment = text.replace(/\/\*[\s\S]*?\*\/$/u, "");
227
+ if (trimmedBlockComment !== text) {
228
+ text = trimmedBlockComment;
229
+ changed = true;
230
+ }
231
+ }
232
+
233
+ return text;
234
+ }
235
+
236
+ function previousSignificantIndex(sourceText, start) {
237
+ let index = start - 1;
238
+ while (index >= 0 && isWhitespace(sourceText[index])) {
239
+ index -= 1;
240
+ }
241
+ return index;
242
+ }
243
+
244
+ function readPreviousWord(sourceText, endIndex) {
245
+ let index = endIndex;
246
+ while (index >= 0 && /[A-Za-z]/.test(sourceText[index])) {
247
+ index -= 1;
248
+ }
249
+ return sourceText.slice(index + 1, endIndex + 1);
250
+ }
251
+
252
+ function isLikelyJsxTagStart(sourceText, index) {
253
+ const next = sourceText[index + 1];
254
+ if (!TAG_NAME_START_CHAR.test(next || "")) {
255
+ return false;
256
+ }
257
+
258
+ const previousIndex = previousSignificantIndex(sourceText, index);
259
+ if (previousIndex < 0) {
260
+ return true;
261
+ }
262
+
263
+ const previousChar = sourceText[previousIndex];
264
+ if ("=({[,!?:;>&|".includes(previousChar)) {
265
+ return true;
266
+ }
267
+
268
+ const previousWord = readPreviousWord(sourceText, previousIndex);
269
+ return ["return", "case", "throw", "yield", "else"].includes(previousWord);
270
+ }
271
+
272
+ function readJsxTagName(sourceText, start) {
273
+ let index = start + 1;
274
+ const isClosing = sourceText[index] === "/";
275
+
276
+ if (isClosing) {
277
+ index += 1;
278
+ }
279
+
280
+ if (!TAG_NAME_START_CHAR.test(sourceText[index] || "")) {
281
+ return null;
282
+ }
283
+
284
+ const nameStart = index;
285
+
286
+ while (index < sourceText.length && TAG_NAME_CHAR.test(sourceText[index])) {
287
+ index += 1;
288
+ }
289
+
290
+ return {
291
+ name: sourceText.slice(nameStart, index),
292
+ isClosing,
293
+ end: index,
294
+ };
295
+ }
296
+
297
+ function scanJsxTag(sourceText, start, replacements, encodeAttributeName) {
298
+ const tag = readJsxTagName(sourceText, start);
299
+
300
+ if (!tag) {
301
+ return {
302
+ end: start + 1,
303
+ tagName: null,
304
+ isClosing: false,
305
+ selfClosing: false,
306
+ };
307
+ }
308
+
309
+ let index = tag.end;
310
+
311
+ if (tag.isClosing) {
312
+ while (index < sourceText.length) {
313
+ if (sourceText[index] === ">") {
314
+ return {
315
+ end: index + 1,
316
+ tagName: tag.name,
317
+ isClosing: true,
318
+ selfClosing: false,
319
+ };
320
+ }
321
+
322
+ index += 1;
323
+ }
324
+
325
+ return {
326
+ end: index,
327
+ tagName: tag.name,
328
+ isClosing: true,
329
+ selfClosing: false,
330
+ };
331
+ }
332
+
333
+ function scanAttributeValue(valueStart) {
334
+ let valueIndex = valueStart;
335
+
336
+ while (valueIndex < sourceText.length && isWhitespace(sourceText[valueIndex])) {
337
+ valueIndex += 1;
338
+ }
339
+
340
+ if (valueIndex >= sourceText.length) {
341
+ return valueIndex;
342
+ }
343
+
344
+ const valueChar = sourceText[valueIndex];
345
+ if (valueChar === "{") {
346
+ return scanBalancedBracesWithJsx(
347
+ sourceText,
348
+ valueIndex,
349
+ replacements,
350
+ encodeAttributeName
351
+ );
352
+ }
353
+
354
+ if (valueChar === "'" || valueChar === "\"") {
355
+ return scanQuotedString(sourceText, valueIndex, valueChar);
356
+ }
357
+
358
+ while (
359
+ valueIndex < sourceText.length &&
360
+ !isWhitespace(sourceText[valueIndex]) &&
361
+ sourceText[valueIndex] !== ">" &&
362
+ !(sourceText[valueIndex] === "/" && sourceText[valueIndex + 1] === ">")
363
+ ) {
364
+ valueIndex += 1;
365
+ }
366
+
367
+ return valueIndex;
368
+ }
369
+
370
+ while (index < sourceText.length) {
371
+ const char = sourceText[index];
372
+ const next = sourceText[index + 1];
373
+
374
+ if (char === ">") {
375
+ return {
376
+ end: index + 1,
377
+ tagName: tag.name,
378
+ isClosing: false,
379
+ selfClosing: false,
380
+ };
381
+ }
382
+
383
+ if (char === "/" && next === ">") {
384
+ return {
385
+ end: index + 2,
386
+ tagName: tag.name,
387
+ isClosing: false,
388
+ selfClosing: true,
389
+ };
390
+ }
391
+
392
+ if (isWhitespace(char)) {
393
+ index += 1;
394
+ continue;
395
+ }
396
+
397
+ if (char === "{") {
398
+ index = scanBalancedBracesWithJsx(
399
+ sourceText,
400
+ index,
401
+ replacements,
402
+ encodeAttributeName
403
+ );
404
+ continue;
405
+ }
406
+
407
+ if (char === "'" || char === "\"") {
408
+ index = scanQuotedString(sourceText, index, char);
409
+ continue;
410
+ }
411
+
412
+ if (Object.hasOwn(PREFIX_TO_KIND, char) && ATTR_NAME_CHAR.test(next || "")) {
413
+ const attrStart = index;
414
+ index += 1;
415
+
416
+ while (index < sourceText.length && ATTR_NAME_CHAR.test(sourceText[index])) {
417
+ index += 1;
418
+ }
419
+
420
+ const originalName = sourceText.slice(attrStart, index);
421
+ replacements.push({
422
+ start: attrStart,
423
+ end: index,
424
+ originalName,
425
+ replacement: encodeAttributeName(originalName),
426
+ });
427
+
428
+ while (index < sourceText.length && isWhitespace(sourceText[index])) {
429
+ index += 1;
430
+ }
431
+ if (sourceText[index] === "=") {
432
+ index = scanAttributeValue(index + 1);
433
+ }
434
+ continue;
435
+ }
436
+
437
+ const attrStart = index;
438
+ while (
439
+ index < sourceText.length &&
440
+ !isWhitespace(sourceText[index]) &&
441
+ sourceText[index] !== "=" &&
442
+ sourceText[index] !== ">" &&
443
+ !(sourceText[index] === "/" && sourceText[index + 1] === ">")
444
+ ) {
445
+ index += 1;
446
+ }
447
+
448
+ if (index === attrStart) {
449
+ index += 1;
450
+ continue;
451
+ }
452
+
453
+ while (index < sourceText.length && isWhitespace(sourceText[index])) {
454
+ index += 1;
455
+ }
456
+ if (sourceText[index] === "=") {
457
+ index = scanAttributeValue(index + 1);
458
+ }
459
+ }
460
+
461
+ return {
462
+ end: index,
463
+ tagName: tag.name,
464
+ isClosing: false,
465
+ selfClosing: false,
466
+ };
467
+ }
468
+
469
+ function scanJsxElement(sourceText, start, replacements, encodeAttributeName) {
470
+ const openingTag = scanJsxTag(sourceText, start, replacements, encodeAttributeName);
471
+
472
+ if (
473
+ openingTag.isClosing ||
474
+ openingTag.selfClosing ||
475
+ !openingTag.tagName
476
+ ) {
477
+ return openingTag.end;
478
+ }
479
+
480
+ let index = openingTag.end;
481
+
482
+ while (index < sourceText.length) {
483
+ const char = sourceText[index];
484
+ const next = sourceText[index + 1];
485
+
486
+ if (char === "'" || char === "\"") {
487
+ index = scanQuotedString(sourceText, index, char);
488
+ continue;
489
+ }
490
+
491
+ if (char === "`") {
492
+ index = scanTemplateLiteral(sourceText, index);
493
+ continue;
494
+ }
495
+
496
+ if (char === "/" && next === "/") {
497
+ index = scanLineComment(sourceText, index);
498
+ continue;
499
+ }
500
+
501
+ if (char === "/" && next === "*") {
502
+ index = scanBlockComment(sourceText, index);
503
+ continue;
504
+ }
505
+
506
+ if (char === "{") {
507
+ index = scanBalancedBracesWithJsx(
508
+ sourceText,
509
+ index,
510
+ replacements,
511
+ encodeAttributeName
512
+ );
513
+ continue;
514
+ }
515
+
516
+ if (char === "<") {
517
+ const nestedTag = readJsxTagName(sourceText, index);
518
+
519
+ if (!nestedTag) {
520
+ index += 1;
521
+ continue;
522
+ }
523
+
524
+ if (nestedTag.isClosing && nestedTag.name === openingTag.tagName) {
525
+ return scanJsxTag(sourceText, index, replacements, encodeAttributeName).end;
526
+ }
527
+
528
+ if (!nestedTag.isClosing) {
529
+ index = scanJsxElement(sourceText, index, replacements, encodeAttributeName);
530
+ continue;
531
+ }
532
+ }
533
+
534
+ index += 1;
535
+ }
536
+
537
+ return index;
538
+ }
539
+
540
+ export function encodeVirtualAttributeName(name) {
541
+ const prefix = name[0];
542
+ const localName = name.slice(1);
543
+ const kind = PREFIX_TO_KIND[prefix];
544
+
545
+ if (!kind) {
546
+ return name;
547
+ }
548
+
549
+ return `__litsx_${kind}_${localName}`;
550
+ }
551
+
552
+ export function decodeVirtualAttributeName(name) {
553
+ const match = /^__litsx_(event|prop|bool)_(.+)$/.exec(name);
554
+
555
+ if (!match) {
556
+ return null;
557
+ }
558
+
559
+ const [, kind, localName] = match;
560
+ return `${KIND_TO_PREFIX[kind]}${localName}`;
561
+ }
562
+
563
+ export function decodeVirtualStaticHoistName(name) {
564
+ const match = /^__litsx_static_([A-Za-z$_][A-Za-z0-9$_]*)$/.exec(name);
565
+
566
+ if (!match) {
567
+ return null;
568
+ }
569
+
570
+ return `^${match[1]}`;
571
+ }
572
+
573
+ export function remapVirtualText(text) {
574
+ if (typeof text !== "string") {
575
+ return text;
576
+ }
577
+
578
+ return text
579
+ .replace(/__litsx_(event|prop|bool)_[\w:-]+/g, (name) => (
580
+ decodeVirtualAttributeName(name) ?? name
581
+ ))
582
+ .replace(/__litsx_static_[A-Za-z$_][A-Za-z0-9$_]*/g, (name) => (
583
+ decodeVirtualStaticHoistName(name) ?? name
584
+ ));
585
+ }
586
+
587
+ export function looksLikeLitsxJsx(sourceText) {
588
+ return (
589
+ /<[\w.-]+[^>]*\s(?:[@.?][\w:-]+)/m.test(sourceText) ||
590
+ /(?:^|[;{}]\s*)\^[A-Za-z$_][A-Za-z0-9$_]*/m.test(sourceText) ||
591
+ /^\s*\^[A-Za-z$_][A-Za-z0-9$_]*/m.test(sourceText)
592
+ );
593
+ }
594
+
595
+ function isLikelyStaticMacroStart(sourceText, index) {
596
+ const next = sourceText[index + 1];
597
+ if (!MACRO_NAME_START_CHAR.test(next || "")) {
598
+ return false;
599
+ }
600
+
601
+ const prefix = trimTrailingWhitespaceAndComments(sourceText.slice(0, index));
602
+ if (!prefix) {
603
+ return true;
604
+ }
605
+
606
+ const previousChar = prefix[prefix.length - 1];
607
+ return previousChar === ";" || previousChar === "{" || previousChar === "}";
608
+ }
609
+
610
+ function scanStaticMacro(sourceText, start, replacements, encodeMacroName) {
611
+ let index = start + 1;
612
+
613
+ while (index < sourceText.length && MACRO_NAME_CHAR.test(sourceText[index])) {
614
+ index += 1;
615
+ }
616
+
617
+ const originalName = sourceText.slice(start, index);
618
+ const macroName = originalName.slice(1);
619
+
620
+ if (macroName === "mixins") {
621
+ return index;
622
+ }
623
+
624
+ replacements.push({
625
+ start,
626
+ end: index,
627
+ originalName,
628
+ replacement: encodeMacroName(originalName, macroName),
629
+ });
630
+
631
+ return index;
632
+ }
633
+
634
+ export function createVirtualLitsxJsxSource(sourceText, options = {}) {
635
+ const strategy = options.strategy === "editor" ? "editor" : "compiler";
636
+ const includeSourceMap = options.sourceMap === true;
637
+ const encodeAttributeName =
638
+ strategy === "editor"
639
+ ? encodeEditorVirtualAttributeName
640
+ : encodeVirtualAttributeName;
641
+ const encodeMacroName =
642
+ strategy === "editor"
643
+ ? encodeEditorStaticHoistName
644
+ : (_originalName, macroName) => `__litsx_static_${macroName}`;
645
+
646
+ if (!sourceText || typeof sourceText !== "string") {
647
+ return {
648
+ code: sourceText,
649
+ map: null,
650
+ replacements: [],
651
+ };
652
+ }
653
+
654
+ if (strategy === "compiler" && sourceText.includes("__litsx_")) {
655
+ return {
656
+ code: sourceText,
657
+ map: null,
658
+ replacements: [],
659
+ collision: true,
660
+ };
661
+ }
662
+
663
+ if (!looksLikeLitsxJsx(sourceText)) {
664
+ return {
665
+ code: sourceText,
666
+ map: null,
667
+ replacements: [],
668
+ };
669
+ }
670
+
671
+ const replacements = [];
672
+ let index = 0;
673
+ let braceDepth = 0;
674
+
675
+ while (index < sourceText.length) {
676
+ const char = sourceText[index];
677
+ const next = sourceText[index + 1];
678
+
679
+ if (char === "'" || char === "\"") {
680
+ index = scanQuotedString(sourceText, index, char);
681
+ continue;
682
+ }
683
+
684
+ if (char === "`") {
685
+ index = scanTemplateLiteral(sourceText, index);
686
+ continue;
687
+ }
688
+
689
+ if (char === "/" && next === "/") {
690
+ index = scanLineComment(sourceText, index);
691
+ continue;
692
+ }
693
+
694
+ if (char === "/" && next === "*") {
695
+ index = scanBlockComment(sourceText, index);
696
+ continue;
697
+ }
698
+
699
+ if (char === "<" && isLikelyJsxTagStart(sourceText, index)) {
700
+ index = scanJsxElement(sourceText, index, replacements, encodeAttributeName);
701
+ continue;
702
+ }
703
+
704
+ if (char === "^" && isLikelyStaticMacroStart(sourceText, index)) {
705
+ index = scanStaticMacro(sourceText, index, replacements, encodeMacroName);
706
+ continue;
707
+ }
708
+
709
+ if (char === "{") {
710
+ braceDepth += 1;
711
+ index += 1;
712
+ continue;
713
+ }
714
+
715
+ if (char === "}") {
716
+ braceDepth = Math.max(0, braceDepth - 1);
717
+ index += 1;
718
+ continue;
719
+ }
720
+
721
+ index += 1;
722
+ }
723
+
724
+ if (!replacements.length) {
725
+ return {
726
+ code: sourceText,
727
+ map: null,
728
+ replacements: [],
729
+ };
730
+ }
731
+
732
+ let lastIndex = 0;
733
+ let transformed = "";
734
+
735
+ for (const replacement of replacements) {
736
+ transformed += sourceText.slice(lastIndex, replacement.start);
737
+ transformed += replacement.replacement;
738
+ lastIndex = replacement.end;
739
+ }
740
+
741
+ transformed += sourceText.slice(lastIndex);
742
+
743
+ return {
744
+ code: transformed,
745
+ map: includeSourceMap
746
+ ? createVirtualLitsxJsxSourceMap(sourceText, replacements, {
747
+ sourceFileName: options.sourceFileName,
748
+ })
749
+ : null,
750
+ replacements,
751
+ };
752
+ }
753
+
754
+ export function createVirtualLitsxJsxSourceMap(
755
+ sourceText,
756
+ replacements = [],
757
+ options = {}
758
+ ) {
759
+ const editable = new MagicString(sourceText);
760
+ applyVirtualAttributeReplacements(editable, replacements);
761
+
762
+ return editable.generateMap({
763
+ hires: true,
764
+ source: options.sourceFileName,
765
+ includeContent: true,
766
+ });
767
+ }
768
+
769
+ function findReplacementByVirtualPosition(position, replacements) {
770
+ let originalCursor = 0;
771
+ let virtualCursor = 0;
772
+
773
+ for (const replacement of replacements) {
774
+ const untouchedLength = replacement.start - originalCursor;
775
+ const replacementVirtualStart = virtualCursor + untouchedLength;
776
+ const replacementVirtualEnd =
777
+ replacementVirtualStart + replacement.replacement.length;
778
+
779
+ if (position >= replacementVirtualStart && position < replacementVirtualEnd) {
780
+ return {
781
+ replacement,
782
+ virtualStart: replacementVirtualStart,
783
+ virtualEnd: replacementVirtualEnd,
784
+ };
785
+ }
786
+
787
+ originalCursor = replacement.end;
788
+ virtualCursor = replacementVirtualEnd;
789
+ }
790
+
791
+ return null;
792
+ }
793
+
794
+ export function mapOriginalPositionToVirtual(position, replacements = []) {
795
+ if (!replacements.length) {
796
+ return position;
797
+ }
798
+
799
+ let offset = 0;
800
+
801
+ for (const replacement of replacements) {
802
+ if (position < replacement.start) {
803
+ break;
804
+ }
805
+
806
+ const originalLength = replacement.end - replacement.start;
807
+ const replacementLength = replacement.replacement.length;
808
+
809
+ if (position < replacement.end) {
810
+ return replacement.start + offset;
811
+ }
812
+
813
+ offset += replacementLength - originalLength;
814
+ }
815
+
816
+ return position + offset;
817
+ }
818
+
819
+ export function remapTextSpanToOriginal(span, replacements = []) {
820
+ if (!span || !replacements.length) {
821
+ return span;
822
+ }
823
+
824
+ const startMapping = findReplacementByVirtualPosition(span.start, replacements);
825
+ if (startMapping) {
826
+ return {
827
+ start: startMapping.replacement.start,
828
+ length: startMapping.replacement.end - startMapping.replacement.start,
829
+ };
830
+ }
831
+
832
+ let originalStart = span.start;
833
+ let originalEnd = span.start + span.length;
834
+
835
+ for (const replacement of replacements) {
836
+ const originalLength = replacement.end - replacement.start;
837
+ const replacementLength = replacement.replacement.length;
838
+ const delta = originalLength - replacementLength;
839
+ const virtualStart = mapOriginalPositionToVirtual(replacement.start, replacements);
840
+ const virtualEnd = virtualStart + replacementLength;
841
+
842
+ if (virtualEnd <= span.start) {
843
+ originalStart += delta;
844
+ originalEnd += delta;
845
+ continue;
846
+ }
847
+
848
+ if (virtualStart < span.start) {
849
+ originalStart = replacement.start;
850
+ }
851
+
852
+ if (virtualStart < span.start + span.length) {
853
+ originalEnd += delta;
854
+ }
855
+ }
856
+
857
+ return {
858
+ start: originalStart,
859
+ length: Math.max(0, originalEnd - originalStart),
860
+ };
861
+ }
862
+
863
+ export function remapVirtualPositionToOriginal(position, replacements = []) {
864
+ const span = remapTextSpanToOriginal({ start: position, length: 0 }, replacements);
865
+ return span.start;
866
+ }
867
+
868
+ export const mapVirtualPositionToOriginal = remapVirtualPositionToOriginal;
869
+
870
+ export function applyVirtualAttributeReplacements(editable, replacements = []) {
871
+ for (const replacement of replacements) {
872
+ editable.overwrite(replacement.start, replacement.end, replacement.replacement);
873
+ }
874
+ }
875
+
876
+ export {
877
+ isReservedVirtualAttributeName,
878
+ };