|
3 | 3 | applySourceEdits, |
4 | 4 | CodePlaceholderCompileError, |
5 | 5 | createCodePlaceholderCompilationContext, |
6 | | - isOffsetInRanges, |
| 6 | + createOffsetRangeLookup, |
| 7 | + partitionPoint, |
7 | 8 | type SourceEdit, |
8 | 9 | } from '@/lib/execution/code-placeholders/shared' |
9 | 10 | import type { |
@@ -357,27 +358,32 @@ function collectAnnexBHtmlCommentRanges( |
357 | 358 | source: string, |
358 | 359 | sourceFile: ts.SourceFile |
359 | 360 | ): AnnexBHtmlCommentRange[] { |
360 | | - const protectedRanges = [ |
| 361 | + const isProtected = createOffsetRangeLookup([ |
361 | 362 | ...collectStandardCommentRanges(source, sourceFile), |
362 | 363 | ...collectJavaScriptLiteralRanges(sourceFile), |
363 | | - ] |
| 364 | + ]) |
364 | 365 | const ranges: AnnexBHtmlCommentRange[] = [] |
365 | 366 |
|
| 367 | + // Reused across lines: searching from every line start rescans to EOF when no marker exists. |
| 368 | + let nextOpenMarker = source.indexOf('<!--') |
366 | 369 | let lineStart = 0 |
367 | 370 | while (lineStart < source.length) { |
368 | 371 | const newline = source.indexOf('\n', lineStart) |
369 | 372 | const lineEnd = newline === -1 ? source.length : newline |
370 | 373 | const leadingWhitespace = /^\s*/.exec(source.slice(lineStart, lineEnd))?.[0].length ?? 0 |
371 | 374 | const closeMarker = lineStart + leadingWhitespace |
372 | | - if (source.startsWith('-->', closeMarker) && !isOffsetInRanges(closeMarker, protectedRanges)) { |
| 375 | + if (source.startsWith('-->', closeMarker) && !isProtected(closeMarker)) { |
373 | 376 | ranges.push({ start: closeMarker, end: lineEnd, markerLength: 3 }) |
374 | 377 | lineStart = newline === -1 ? source.length : newline + 1 |
375 | 378 | continue |
376 | 379 | } |
377 | 380 |
|
378 | | - let openMarker = source.indexOf('<!--', lineStart) |
| 381 | + if (nextOpenMarker !== -1 && nextOpenMarker < lineStart) { |
| 382 | + nextOpenMarker = source.indexOf('<!--', lineStart) |
| 383 | + } |
| 384 | + let openMarker = nextOpenMarker |
379 | 385 | while (openMarker >= 0 && openMarker < lineEnd) { |
380 | | - if (!isOffsetInRanges(openMarker, protectedRanges)) { |
| 386 | + if (!isProtected(openMarker)) { |
381 | 387 | ranges.push({ start: openMarker, end: lineEnd, markerLength: 4 }) |
382 | 388 | break |
383 | 389 | } |
@@ -714,10 +720,10 @@ export async function compileJavaScriptPlaceholders( |
714 | 720 | ts.ScriptKind.JS |
715 | 721 | ) |
716 | 722 | if (!input.analysisOnly) assertSyntacticallyValidJavaScript(source, input.code, sourceFile) |
717 | | - const commentRanges: Array<[number, number]> = [ |
| 723 | + const isInComment = createOffsetRangeLookup([ |
718 | 724 | ...collectStandardCommentRanges(parserSource, sourceFile), |
719 | 725 | ...htmlCommentRanges.map(({ start, end }) => [start, end] as [number, number]), |
720 | | - ] |
| 726 | + ]) |
721 | 727 | const itemByRange = new Map( |
722 | 728 | sentinelOccurrences.map((item) => [`${item.occurrence.start}:${item.occurrence.end}`, item]) |
723 | 729 | ) |
@@ -757,12 +763,19 @@ export async function compileJavaScriptPlaceholders( |
757 | 763 | if (ts.isTemplateExpression(node.template)) { |
758 | 764 | for (const span of node.template.templateSpans) visit(span.expression) |
759 | 765 | } |
760 | | - const nestedEdits = edits.splice(nestedEditStart) |
| 766 | + // A stable sort: applySourceEdits keeps input order among edits with equal bounds. |
| 767 | + const nestedEdits = edits |
| 768 | + .splice(nestedEditStart) |
| 769 | + .sort((left, right) => left.start - right.start) |
761 | 770 | const transformedNodeText = (child: ts.Node): string => { |
762 | 771 | const childStart = child.getStart(sourceFile) |
763 | 772 | const childEnd = child.getEnd() |
764 | 773 | const childEdits = nestedEdits |
765 | | - .filter((edit) => edit.start >= childStart && edit.end <= childEnd) |
| 774 | + .slice( |
| 775 | + partitionPoint(nestedEdits, (edit) => edit.start < childStart), |
| 776 | + partitionPoint(nestedEdits, (edit) => edit.start <= childEnd) |
| 777 | + ) |
| 778 | + .filter((edit) => edit.end <= childEnd) |
766 | 779 | .map((edit) => ({ |
767 | 780 | ...edit, |
768 | 781 | start: edit.start - childStart, |
@@ -873,7 +886,7 @@ export async function compileJavaScriptPlaceholders( |
873 | 886 |
|
874 | 887 | if (ts.isIdentifier(node)) { |
875 | 888 | const item = itemByRange.get(`${node.getStart(sourceFile)}:${node.getEnd()}`) |
876 | | - if (!item || isOffsetInRanges(item.occurrence.start, commentRanges)) return |
| 889 | + if (!item || isInComment(item.occurrence.start)) return |
877 | 890 | const resolved = context.resolve(item.occurrence) |
878 | 891 | if (!resolved) return |
879 | 892 | if (isDeclarationIdentifier(node) || isWriteIdentifier(node)) { |
@@ -919,7 +932,7 @@ export async function compileJavaScriptPlaceholders( |
919 | 932 | visit(sourceFile) |
920 | 933 |
|
921 | 934 | for (const item of sentinelOccurrences) { |
922 | | - if (isOffsetInRanges(item.occurrence.start, commentRanges)) continue |
| 935 | + if (isInComment(item.occurrence.start)) continue |
923 | 936 | if (!context.hasValue(item.occurrence.name) || consumed.has(item.occurrence)) continue |
924 | 937 | rejectUnsupported(item, 'this JavaScript syntax position') |
925 | 938 | } |
|
0 commit comments