@adecore/editor-core 0.39.0 → 0.40.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.
Files changed (53) hide show
  1. package/dist/block-folds.js +5 -2
  2. package/dist/bracket-tracker.js +4 -3
  3. package/dist/brackets.d.ts +2 -12
  4. package/dist/brackets.js +3 -15
  5. package/dist/closer-tracker.js +0 -1
  6. package/dist/doc-comments.js +0 -1
  7. package/dist/document.d.ts +2 -0
  8. package/dist/document.js +33 -35
  9. package/dist/edit-regions.js +3 -4
  10. package/dist/enter.d.ts +2 -0
  11. package/dist/enter.js +11 -9
  12. package/dist/fold-roles.js +2 -2
  13. package/dist/lexical-folds.d.ts +0 -1
  14. package/dist/lexical-folds.js +1 -4
  15. package/dist/lexical.d.ts +11 -5
  16. package/dist/lexical.js +13 -17
  17. package/dist/line-commands.js +7 -15
  18. package/dist/line-text.d.ts +2 -0
  19. package/dist/line-text.js +4 -0
  20. package/dist/markup-regions.js +2 -3
  21. package/dist/paste.js +2 -7
  22. package/dist/structure.js +2 -1
  23. package/dist/typing-context.js +5 -15
  24. package/package.json +2 -2
  25. package/src/block-folds.ts +39 -2
  26. package/src/bracket-tracker.ts +55 -12
  27. package/src/brackets.ts +35 -42
  28. package/src/closer-tracker.ts +6 -1
  29. package/src/comments.ts +103 -0
  30. package/src/doc-comments.ts +19 -1
  31. package/src/document.ts +505 -36
  32. package/src/edit-regions.ts +26 -4
  33. package/src/enter.ts +104 -9
  34. package/src/fold-roles.ts +34 -2
  35. package/src/index.ts +2 -0
  36. package/src/languages.ts +11 -0
  37. package/src/lexical-folds.ts +21 -5
  38. package/src/lexical.ts +31 -23
  39. package/src/line-commands.ts +81 -17
  40. package/src/line-text.ts +7 -0
  41. package/src/markup-regions.ts +101 -3
  42. package/src/offsets.ts +10 -0
  43. package/src/paste.ts +20 -7
  44. package/src/php-markup.ts +23 -0
  45. package/src/preserve-case.ts +12 -0
  46. package/src/rope.ts +70 -0
  47. package/src/script-source.ts +10 -0
  48. package/src/search.ts +38 -0
  49. package/src/structure.ts +29 -1
  50. package/src/text-span.ts +53 -0
  51. package/src/typing-context.ts +65 -41
  52. package/src/typing-handlers.ts +28 -0
  53. package/src/words.ts +10 -0
@@ -1,4 +1,5 @@
1
1
  import type { EditSource } from './edit-source.ts';
2
+ import { whitespaceOf } from './line-text.ts';
2
3
 
3
4
  const JSX_LANGUAGES = /^(typescriptreact|javascriptreact|tsx|jsx|javascript|js|mjs|cjs)$/i;
4
5
  const NAME = /[\w$.:-]/;
@@ -39,93 +40,128 @@ class JsxScanner {
39
40
  let at = from;
40
41
  let previous = '';
41
42
  let word = '';
43
+
42
44
  while (at < text.length) {
43
45
  if (--this.budget < 0) {
44
46
  return -1;
45
47
  }
48
+
46
49
  const char = text[at]!;
47
50
  const next = text[at + 1];
51
+
48
52
  if (/\s/.test(char)) {
49
53
  at++;
54
+
50
55
  continue;
51
56
  }
57
+
52
58
  if (char === '/' && next === '/') {
53
59
  const end = text.indexOf('\n', at);
54
60
  at = end < 0 ? text.length : end;
61
+
55
62
  continue;
56
63
  }
64
+
57
65
  if (char === '/' && next === '*') {
58
66
  const end = text.indexOf('*/', at + 2);
59
67
  at = end < 0 ? text.length : end + 2;
68
+
60
69
  continue;
61
70
  }
71
+
62
72
  if (char === '"' || char === "'") {
63
73
  at = this.quoted(at);
64
74
  previous = char;
65
75
  word = '';
76
+
66
77
  continue;
67
78
  }
79
+
68
80
  if (char === '`') {
69
81
  at = this.template(at);
82
+
70
83
  if (at < 0) {
71
84
  return -1;
72
85
  }
86
+
73
87
  previous = char;
74
88
  word = '';
89
+
75
90
  continue;
76
91
  }
92
+
77
93
  const expressionStart = previous === '' || EXPRESSION_STARTS.includes(previous) || EXPRESSION_KEYWORDS.test(word);
94
+
78
95
  if (char === '/' && expressionStart) {
79
96
  at = this.regex(at);
80
97
  previous = '/';
81
98
  word = '';
99
+
82
100
  continue;
83
101
  }
102
+
84
103
  if (char === '<' && expressionStart && next !== undefined && START_OF_NAME.test(next)) {
85
104
  const end = this.element(at, true);
105
+
86
106
  if (end >= 0) {
87
107
  at = end;
88
108
  previous = ')';
89
109
  word = '';
110
+
90
111
  continue;
91
112
  }
92
113
  }
114
+
93
115
  const closes = CLOSERS[char];
116
+
94
117
  if (closes !== undefined) {
95
118
  const end = this.code(at + 1, closes);
119
+
96
120
  if (end < 0) {
97
121
  return -1;
98
122
  }
123
+
99
124
  at = end;
100
125
  previous = closes;
101
126
  word = '';
127
+
102
128
  continue;
103
129
  }
130
+
104
131
  if (char === '}' || char === ')' || char === ']') {
105
132
  if (closer === char) {
106
133
  return at + 1;
107
134
  }
135
+
108
136
  if (closer !== null) {
109
137
  return -1;
110
138
  }
139
+
111
140
  at++;
112
141
  previous = char;
113
142
  word = '';
143
+
114
144
  continue;
115
145
  }
146
+
116
147
  if (/[\w$]/.test(char)) {
117
148
  const start = at;
149
+
118
150
  while (at < text.length && /[\w$]/.test(text[at]!)) {
119
151
  at++;
120
152
  }
153
+
121
154
  word = text.slice(start, at);
122
155
  previous = word.slice(-1);
156
+
123
157
  continue;
124
158
  }
159
+
125
160
  previous = char;
126
161
  word = '';
127
162
  at++;
128
163
  }
164
+
129
165
  return closer === null ? text.length : -1;
130
166
  }
131
167
 
@@ -133,15 +169,18 @@ class JsxScanner {
133
169
  const { text } = this;
134
170
  const quote = text[from]!;
135
171
  let at = from + 1;
172
+
136
173
  while (at < text.length && text[at] !== quote && text[at] !== '\n') {
137
174
  at += text[at] === '\\' ? 2 : 1;
138
175
  }
176
+
139
177
  return Math.min(text.length, at + 1);
140
178
  }
141
179
 
142
180
  private template(from: number): number {
143
181
  const { text } = this;
144
182
  let at = from + 1;
183
+
145
184
  while (at < text.length) {
146
185
  if (text[at] === '\\') {
147
186
  at += 2;
@@ -149,6 +188,7 @@ class JsxScanner {
149
188
  return at + 1;
150
189
  } else if (text[at] === '$' && text[at + 1] === '{') {
151
190
  at = this.code(at + 2, '}');
191
+
152
192
  if (at < 0) {
153
193
  return -1;
154
194
  }
@@ -156,6 +196,7 @@ class JsxScanner {
156
196
  at++;
157
197
  }
158
198
  }
199
+
159
200
  return text.length;
160
201
  }
161
202
 
@@ -163,8 +204,10 @@ class JsxScanner {
163
204
  const { text } = this;
164
205
  let at = from + 1;
165
206
  let inClass = false;
207
+
166
208
  while (at < text.length && text[at] !== '\n') {
167
209
  const char = text[at]!;
210
+
168
211
  if (char === '\\') {
169
212
  at++;
170
213
  } else if (char === '[') {
@@ -174,8 +217,10 @@ class JsxScanner {
174
217
  } else if (char === '/' && !inClass) {
175
218
  return at + 1;
176
219
  }
220
+
177
221
  at++;
178
222
  }
223
+
179
224
  return from + 1;
180
225
  }
181
226
 
@@ -186,15 +231,19 @@ class JsxScanner {
186
231
  this.depth++;
187
232
  const end = this.parseElement(from);
188
233
  this.depth--;
234
+
189
235
  if (end < 0) {
190
236
  this.children.length = children;
237
+
191
238
  if (previousEnd !== undefined) {
192
239
  this.children[children - 1]!.to = previousEnd;
193
240
  }
194
241
  }
242
+
195
243
  if (end >= 0 && outermost && this.depth === 0) {
196
244
  this.ranges.push({ from, to: end });
197
245
  }
246
+
198
247
  return end;
199
248
  }
200
249
 
@@ -202,7 +251,9 @@ class JsxScanner {
202
251
  if (!this.collectChildren || from >= to) {
203
252
  return;
204
253
  }
254
+
205
255
  const previous = this.children.at(-1);
256
+
206
257
  if (previous !== undefined && previous.to === from) {
207
258
  previous.to = to;
208
259
  } else {
@@ -212,72 +263,100 @@ class JsxScanner {
212
263
 
213
264
  private skipSpace(from: number): number {
214
265
  let at = from;
266
+
215
267
  while (at < this.text.length && /\s/.test(this.text[at]!)) {
216
268
  at++;
217
269
  }
270
+
218
271
  return at;
219
272
  }
220
273
 
221
274
  private parseElement(from: number): number {
222
275
  const { text } = this;
223
276
  let at = from + 1;
277
+
224
278
  while (at < text.length && NAME.test(text[at]!)) {
225
279
  at++;
226
280
  }
281
+
227
282
  const name = text.slice(from + 1, at);
283
+
228
284
  for (;;) {
229
285
  at = this.skipSpace(at);
230
286
  const char = text[at];
287
+
231
288
  if (char === undefined || --this.budget < 0) {
232
289
  return -1;
233
290
  }
291
+
234
292
  if (char === '/' && text[at + 1] === '/') {
235
293
  const end = text.indexOf('\n', at + 2);
294
+
236
295
  if (end < 0) {
237
296
  return -1;
238
297
  }
298
+
239
299
  at = end + 1;
300
+
240
301
  continue;
241
302
  }
303
+
242
304
  if (char === '/' && text[at + 1] === '*') {
243
305
  const end = text.indexOf('*/', at + 2);
306
+
244
307
  if (end < 0) {
245
308
  return -1;
246
309
  }
310
+
247
311
  at = end + 2;
312
+
248
313
  continue;
249
314
  }
315
+
250
316
  if (char === '/' && text[at + 1] === '>') {
251
317
  return at + 2;
252
318
  }
319
+
253
320
  if (char === '>') {
254
321
  at++;
322
+
255
323
  break;
256
324
  }
325
+
257
326
  if (char === '{') {
258
327
  at = this.code(at + 1, '}');
328
+
259
329
  if (at < 0) {
260
330
  return -1;
261
331
  }
332
+
262
333
  continue;
263
334
  }
335
+
264
336
  if (!/[A-Za-z_$]/.test(char)) {
265
337
  return -1;
266
338
  }
339
+
267
340
  while (at < text.length && NAME.test(text[at]!)) {
268
341
  at++;
269
342
  }
343
+
270
344
  at = this.skipSpace(at);
345
+
271
346
  if (text[at] !== '=') {
272
347
  continue;
273
348
  }
349
+
274
350
  at = this.skipSpace(at + 1);
275
351
  const value = text[at];
352
+
276
353
  if (value === '"' || value === "'") {
277
354
  const close = text.indexOf(value, at + 1);
355
+
278
356
  if (close < 0) {
279
357
  return -1;
280
358
  }
359
+
281
360
  this.values.push({ from: at, to: close + 1 });
282
361
  at = close + 1;
283
362
  } else if (value === '{') {
@@ -287,34 +366,43 @@ class JsxScanner {
287
366
  } else {
288
367
  return -1;
289
368
  }
369
+
290
370
  if (at < 0) {
291
371
  return -1;
292
372
  }
293
373
  }
374
+
294
375
  for (;;) {
295
376
  const start = at;
377
+
296
378
  while (at < text.length && text[at] !== '<' && text[at] !== '{') {
297
379
  at++;
298
380
  }
381
+
299
382
  this.child(start, text[at] === '<' ? at + 1 : at);
383
+
300
384
  if (at >= text.length || --this.budget < 0) {
301
385
  return -1;
302
386
  }
387
+
303
388
  if (text[at] === '{') {
304
389
  this.child(at, at + 1);
305
390
  const from = at;
306
391
  at = this.code(at + 1, '}');
392
+
307
393
  if (at >= 0 && /^\{\s*\/\*/.test(text.slice(from, from + 20))) {
308
394
  this.child(from + 1, at);
309
395
  }
310
396
  } else if (text[at + 1] === '/') {
311
397
  const closing = /^<\/\s*([\w$.:-]*)\s*>/.exec(text.slice(at, at + 200));
398
+
312
399
  return closing !== null && closing[1] === name ? at + closing[0].length : -1;
313
400
  } else if (START_OF_NAME.test(text[at + 1] ?? '')) {
314
401
  at = this.parseElement(at);
315
402
  } else {
316
403
  return -1;
317
404
  }
405
+
318
406
  if (at < 0) {
319
407
  return -1;
320
408
  }
@@ -326,6 +414,7 @@ class JsxScanner {
326
414
  export function jsxRanges(text: string): { from: number; to: number }[] {
327
415
  const scanner = new JsxScanner(text);
328
416
  scanner.scan();
417
+
329
418
  return scanner.ranges;
330
419
  }
331
420
 
@@ -333,9 +422,11 @@ export function jsxRanges(text: string): { from: number; to: number }[] {
333
422
  function insideAny(ranges: readonly { from: number; to: number }[], offset: number): boolean {
334
423
  let low = 0;
335
424
  let high = ranges.length - 1;
425
+
336
426
  while (low <= high) {
337
427
  const middle = (low + high) >> 1;
338
428
  const range = ranges[middle]!;
429
+
339
430
  if (offset < range.from) {
340
431
  high = middle - 1;
341
432
  } else if (offset >= range.to) {
@@ -344,6 +435,7 @@ function insideAny(ranges: readonly { from: number; to: number }[], offset: numb
344
435
  return true;
345
436
  }
346
437
  }
438
+
347
439
  return false;
348
440
  }
349
441
 
@@ -351,10 +443,10 @@ function insideAny(ranges: readonly { from: number; to: number }[], offset: numb
351
443
  export function jsxChildren(text: string): (offset: number) => boolean {
352
444
  const scanner = new JsxScanner(text, true);
353
445
  scanner.scan();
446
+
354
447
  return (offset) => insideAny(scanner.children, offset);
355
448
  }
356
449
 
357
- /* Whether a language is one whose scripts can hold JSX. */
358
450
  export function hasJsx(language: string): boolean {
359
451
  return JSX_LANGUAGES.test(language);
360
452
  }
@@ -363,6 +455,7 @@ export function hasJsx(language: string): boolean {
363
455
  export function isJsxAttributeValue(text: string, offset: number): boolean {
364
456
  const scanner = new JsxScanner(text);
365
457
  scanner.scan();
458
+
366
459
  return scanner.values.some((value) => value.from === offset);
367
460
  }
368
461
 
@@ -373,19 +466,24 @@ export function isJsxAttributeValue(text: string, offset: number): boolean {
373
466
  */
374
467
  export function markupGuard(source: EditSource, language: string): (line: number) => boolean {
375
468
  const id = language.toLowerCase();
469
+
376
470
  if (id === 'vue') {
377
471
  return (line) => {
378
472
  const region = source.region(line);
473
+
379
474
  return (region !== 'script' && region !== 'style') || /^\s*<\/?(?:script|style|template)\b/.test(source.line(line).text);
380
475
  };
381
476
  }
477
+
382
478
  if (hasJsx(id)) {
383
479
  const ranges = jsxRanges(source.slice(0, source.line(source.lineCount - 1).end));
480
+
384
481
  return (line) => {
385
482
  const bounds = source.line(line);
386
- const start = bounds.start + (/^[\t ]*/.exec(bounds.text)?.[0].length ?? 0);
387
- return insideAny(ranges, start);
483
+
484
+ return insideAny(ranges, bounds.start + whitespaceOf(bounds.text).length);
388
485
  };
389
486
  }
487
+
390
488
  return () => false;
391
489
  }
package/src/offsets.ts CHANGED
@@ -3,38 +3,48 @@ import type { TextEdit } from './types.ts';
3
3
  /* An offset after simultaneous edits. An edit that inserts at it leaves it in front of the new text when `stayBefore`. */
4
4
  export function mapOffset(offset: number, edits: readonly TextEdit[], stayBefore = false): number {
5
5
  let delta = 0;
6
+
6
7
  for (const edit of edits) {
7
8
  if (offset < edit.from || (stayBefore && offset === edit.from && edit.to === edit.from)) {
8
9
  break;
9
10
  }
11
+
10
12
  if (offset < edit.to) {
11
13
  return edit.from + delta + edit.text.length;
12
14
  }
15
+
13
16
  delta += edit.text.length - (edit.to - edit.from);
14
17
  }
18
+
15
19
  return offset + delta;
16
20
  }
17
21
 
18
22
  /* Reuse this index across selections while retaining both endpoint affinities. */
19
23
  export function offsetMapper(edits: readonly TextEdit[]): (offset: number, stayBefore?: boolean) => number {
20
24
  const deltas = new Float64Array(edits.length + 1);
25
+
21
26
  for (let index = 0; index < edits.length; index++) {
22
27
  const edit = edits[index]!;
23
28
  deltas[index + 1] = deltas[index]! + edit.text.length - (edit.to - edit.from);
24
29
  }
30
+
25
31
  return (offset, stayBefore = false) => {
26
32
  let low = 0,
27
33
  high = edits.length;
34
+
28
35
  while (low < high) {
29
36
  const middle = (low + high) >>> 1;
30
37
  const edit = edits[middle]!;
38
+
31
39
  if (edit.to < offset || (edit.to === offset && !(stayBefore && edit.from === edit.to))) {
32
40
  low = middle + 1;
33
41
  } else {
34
42
  high = middle;
35
43
  }
36
44
  }
45
+
37
46
  const edit = edits[low];
47
+
38
48
  return edit !== undefined && edit.from <= offset && offset < edit.to ? edit.from + deltas[low]! + edit.text.length : offset + deltas[low]!;
39
49
  };
40
50
  }
package/src/paste.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { EditPlan, EditSource } from './edit-source.ts';
2
- import { indentText, isBlank, whitespaceOf } from './line-text.ts';
2
+ import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.ts';
3
3
  import { mapOffset } from './offsets.ts';
4
4
  import { indentationColumn } from './structure.ts';
5
5
  import type { Selection, TextEdit } from './types.ts';
@@ -23,7 +23,9 @@ function perCaretLines(text: string, carets: number): string[] | null {
23
23
  if (carets < 2) {
24
24
  return null;
25
25
  }
26
+
26
27
  const lines = linesOf(text);
28
+
27
29
  return lines.length === carets ? lines : null;
28
30
  }
29
31
 
@@ -36,9 +38,11 @@ function perCaretLines(text: string, carets: number): string[] | null {
36
38
  function reindent(source: EditSource, from: number, text: string, options: PasteOptions): string {
37
39
  const newline = text.includes('\r\n') ? '\r\n' : '\n';
38
40
  const lines = text.split(/\r?\n/);
41
+
39
42
  if (lines.length < 2) {
40
43
  return text;
41
44
  }
45
+
42
46
  const line = source.line(source.lineAt(from));
43
47
  const before = source.slice(line.start, from);
44
48
  const caretColumn = indentationColumn(before, options.tabSize);
@@ -50,25 +54,32 @@ function reindent(source: EditSource, from: number, text: string, options: Paste
50
54
  const widths = lines.map((entry) => (isBlank(entry) ? null : indentationColumn(whitespaceOf(entry), options.tabSize)));
51
55
  const measured = widths.filter((width, index): width is number => width !== null && !(anchored && index === 0));
52
56
  const trailingBreak = isBlank(lines.at(-1)!);
57
+
53
58
  if (measured.length > 0) {
54
59
  // A reduce, not a spread: a paste of a few hundred thousand lines is more arguments than V8 takes in one call.
55
60
  const delta = target - measured.reduce((least, width) => Math.min(least, width));
61
+
56
62
  for (const [index, entry] of lines.entries()) {
57
63
  const width = widths[index];
64
+
58
65
  if (width === null || width === undefined || (anchored && index === 0)) {
59
66
  continue;
60
67
  }
68
+
61
69
  // The first line follows the indentation already in front of the caret.
62
70
  const indent = Math.max(0, width + delta - (index === 0 ? caretColumn : 0));
71
+
63
72
  if (indent !== width) {
64
73
  lines[index] = indentText(indent, options) + entry.slice(whitespaceOf(entry).length);
65
74
  }
66
75
  }
67
76
  }
77
+
68
78
  if (trailingBreak) {
69
79
  // What follows the paste stays under the indentation of the line it was on.
70
80
  lines[lines.length - 1] = whitespaceOf(before);
71
81
  }
82
+
72
83
  return lines.join(newline);
73
84
  }
74
85
 
@@ -78,29 +89,30 @@ function reindent(source: EditSource, from: number, text: string, options: Paste
78
89
  * the line it lands on, when it lands in code and not in a string, a comment or a heredoc.
79
90
  */
80
91
  export function planPaste(source: EditSource, selections: readonly Selection[], text: string, options: PasteOptions): EditPlan {
81
- const ordered = selections.map((selection, index) => ({
82
- from: Math.min(selection.anchor, selection.head),
83
- to: Math.max(selection.anchor, selection.head),
84
- index
85
- }));
86
- ordered.sort((left, right) => left.from - right.from);
92
+ const ordered = selections.map((selection, index) => ({ ...rangeOf(selection), index })).sort((left, right) => left.from - right.from);
87
93
  const segments = perCaretLines(text, selections.length);
88
94
  const newline = text.includes('\r\n') ? '\r\n' : '\n';
89
95
  const edits: TextEdit[] = [];
96
+
90
97
  if (options.wholeLines && selections.every((selection) => selection.anchor === selection.head)) {
91
98
  const lineEdits = new Map<number, string>();
99
+
92
100
  for (const [position, { from }] of ordered.entries()) {
93
101
  const start = source.line(source.lineAt(from)).start;
94
102
  const piece = segments?.[position] ?? text;
95
103
  lineEdits.set(start, (lineEdits.get(start) ?? '') + (/\r?\n$/.test(piece) ? piece : piece + newline));
96
104
  }
105
+
97
106
  for (const [start, insert] of lineEdits) {
98
107
  edits.push({ from: start, to: start, text: insert });
99
108
  }
109
+
100
110
  return { edits, selections: selections.map((selection) => ({ anchor: mapOffset(selection.anchor, edits), head: mapOffset(selection.head, edits) })) };
101
111
  }
112
+
102
113
  const carets: Selection[] = new Array<Selection>(selections.length);
103
114
  let shift = 0;
115
+
104
116
  for (const [position, { from, to, index }] of ordered.entries()) {
105
117
  const piece = segments?.[position] ?? text;
106
118
  const inserted = options.reindent && segments === null && source.context(from).mode === 'code' ? reindent(source, from, piece, options) : piece;
@@ -109,5 +121,6 @@ export function planPaste(source: EditSource, selections: readonly Selection[],
109
121
  carets[index] = { anchor: head, head };
110
122
  shift += inserted.length - (to - from);
111
123
  }
124
+
112
125
  return { edits, selections: carets };
113
126
  }
package/src/php-markup.ts CHANGED
@@ -12,9 +12,11 @@ function finishTag(state: TypingContext, selfClosing: boolean, script: ScriptLex
12
12
  state.markup = !selfClosing && state.element !== undefined ? 'raw' : undefined;
13
13
  state.markupQuote = undefined;
14
14
  state.embed = state.markup === 'raw' && state.element === 'script' ? script.initial() : undefined;
15
+
15
16
  if (state.embed !== undefined) {
16
17
  state.embed.embedStart = start;
17
18
  }
19
+
18
20
  if (selfClosing) {
19
21
  state.element = undefined;
20
22
  }
@@ -23,33 +25,42 @@ function finishTag(state: TypingContext, selfClosing: boolean, script: ScriptLex
23
25
  export function scanPhpMarkup(state: TypingContext, text: string, from: number, end: number, offset: number, script: ScriptLexer): number {
24
26
  for (let at = from; at < end; at++) {
25
27
  const char = text[at]!;
28
+
26
29
  if (char === '<' && text[at + 1] === '?') {
27
30
  PHP.lastIndex = at;
28
31
  const open = PHP.exec(text);
32
+
29
33
  if (open !== null) {
30
34
  if (state.markup === 'equals') {
31
35
  state.markup = 'value';
32
36
  }
37
+
33
38
  state.mode = 'code';
34
39
  state.expression = true;
35
40
  state.word = '';
41
+
36
42
  return at + open[0].length - 1;
37
43
  }
38
44
  }
45
+
39
46
  if (state.markup === 'comment') {
40
47
  if (text.startsWith('-->', at) && at + 3 <= end) {
41
48
  state.markup = undefined;
42
49
  at += 2;
43
50
  }
51
+
44
52
  continue;
45
53
  }
54
+
46
55
  if (state.markup === 'value' && state.markupQuote !== undefined) {
47
56
  if (char === state.markupQuote) {
48
57
  state.markup = 'tag';
49
58
  state.markupQuote = undefined;
50
59
  }
60
+
51
61
  continue;
52
62
  }
63
+
53
64
  if (char === '<') {
54
65
  if (state.markup !== 'raw' && text.startsWith('<!--', at)) {
55
66
  // HTML also closes these two comments immediately, without a later -->.
@@ -57,33 +68,44 @@ export function scanPhpMarkup(state: TypingContext, text: string, from: number,
57
68
  state.markup = empty > 0 && at + empty <= end ? undefined : 'comment';
58
69
  state.element = undefined;
59
70
  at += (empty || 4) - 1;
71
+
60
72
  continue;
61
73
  }
74
+
62
75
  TAG.lastIndex = at;
63
76
  const tag = TAG.exec(text);
64
77
  const name = tag?.[2]?.toLowerCase();
78
+
65
79
  if (state.markup === 'raw' && (tag?.[1] !== '/' || name !== state.element)) {
66
80
  if (state.embed !== undefined) {
67
81
  at = script.step(state.embed, text, at, end, offset);
68
82
  }
83
+
69
84
  continue;
70
85
  }
86
+
71
87
  state.embed = undefined;
72
88
  state.element = tag?.[1] !== '/' && (name === 'script' || name === 'style' || name === 'textarea' || name === 'title') ? name : undefined;
73
89
  state.markup = tag === null ? undefined : 'tag';
74
90
  state.markupQuote = undefined;
91
+
75
92
  if (tag !== null) {
76
93
  at += tag[0].length - 1;
77
94
  }
95
+
78
96
  continue;
79
97
  }
98
+
80
99
  if (state.embed !== undefined) {
81
100
  at = script.step(state.embed, text, at, end, offset);
101
+
82
102
  continue;
83
103
  }
104
+
84
105
  if (state.markup === undefined || state.markup === 'raw') {
85
106
  continue;
86
107
  }
108
+
87
109
  if (char === '>') {
88
110
  finishTag(state, false, script, offset + at + 1);
89
111
  } else if (char === '/' && text[at + 1] === '>' && at + 2 <= end) {
@@ -100,5 +122,6 @@ export function scanPhpMarkup(state: TypingContext, text: string, from: number,
100
122
  state.markup = 'equals';
101
123
  }
102
124
  }
125
+
103
126
  return end - 1;
104
127
  }