import ts from '@typescript/typescript6' import { applySourceEdits, CodePlaceholderCompileError, createCodePlaceholderCompilationContext, isOffsetInRanges, type SourceEdit, } from '@/lib/execution/code-placeholders/shared' import type { CodePlaceholderOccurrence, CompiledCodePlaceholders, InternalCompileCodePlaceholdersInput, ResolvedCodePlaceholderOccurrence, } from '@/lib/execution/code-placeholders/types' interface SentinelOccurrence { occurrence: CodePlaceholderOccurrence sentinel: string } const SENTINEL_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' interface DecodedJavaScriptSyntax { identifierNames: string[] values: string[] } interface AnnexBHtmlCommentRange { start: number end: number markerLength: 3 | 4 } function collectDecodedSyntax(code: string): DecodedJavaScriptSyntax { const sourceFile = ts.createSourceFile( 'user-code-original.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS ) const identifierNames: string[] = [] const values: string[] = [] const visit = (node: ts.Node): void => { const isTemplateToken = node.kind === ts.SyntaxKind.TemplateHead || node.kind === ts.SyntaxKind.TemplateMiddle || node.kind === ts.SyntaxKind.TemplateTail if (ts.isIdentifier(node) || ts.isStringLiteralLike(node) || isTemplateToken) { const text: unknown = Reflect.get(node, 'text') if (typeof text === 'string' && text) values.push(text) if (ts.isIdentifier(node) && node.text) identifierNames.push(node.text) const rawText: unknown = Reflect.get(node, 'rawText') if (typeof rawText === 'string' && rawText) values.push(rawText) } ts.forEachChild(node, visit) } visit(sourceFile) return { identifierNames, values } } function collectForbiddenSentinels( values: readonly string[], lengths: ReadonlySet ): Map> { const forbidden = new Map>() for (const value of values) { for (const match of value.matchAll(/(?=(\$[0-9A-Za-z]*\$))/g)) { const sentinel = match[1] if (!lengths.has(sentinel.length)) continue const entries = forbidden.get(sentinel.length) ?? new Set() entries.add(sentinel) forbidden.set(sentinel.length, entries) } } return forbidden } function encodeSentinelIndex(index: number, length: number): string | undefined { const characters = new Array(length).fill(SENTINEL_ALPHABET[0]) let remaining = index for (let cursor = length - 1; cursor >= 0 && remaining > 0; cursor -= 1) { characters[cursor] = SENTINEL_ALPHABET[remaining % SENTINEL_ALPHABET.length] remaining = Math.floor(remaining / SENTINEL_ALPHABET.length) } return remaining === 0 ? characters.join('') : undefined } function createSentinel( forbidden: ReadonlyMap>, length: number, nextCandidateByLength: Map ): string { const payloadLength = length - 2 let candidateIndex = nextCandidateByLength.get(length) ?? 0 for (;;) { const encoded = encodeSentinelIndex(candidateIndex, payloadLength) if (!encoded) break candidateIndex += 1 const sentinel = `$${encoded}$` if (!forbidden.get(length)?.has(sentinel)) { nextCandidateByLength.set(length, candidateIndex) return sentinel } } throw new CodePlaceholderCompileError('Unable to allocate a collision-free parser sentinel') } function createSentinelSource( code: string, occurrences: CodePlaceholderOccurrence[], decodedValues: readonly string[] ): { source: string; sentinelOccurrences: SentinelOccurrence[] } { const nextCandidateByLength = new Map() const lengths = new Set(occurrences.map((occurrence) => occurrence.end - occurrence.start)) const forbidden = collectForbiddenSentinels([code, ...decodedValues], lengths) const sentinelOccurrences = occurrences.map((occurrence) => ({ occurrence, sentinel: createSentinel(forbidden, occurrence.end - occurrence.start, nextCandidateByLength), })) let cursor = 0 let source = '' for (const item of sentinelOccurrences) { source += code.slice(cursor, item.occurrence.start) source += item.sentinel cursor = item.occurrence.end } return { source: source + code.slice(cursor), sentinelOccurrences } } function collectRegularExpressionRanges(sourceFile: ts.SourceFile): Array<[number, number]> { const ranges: Array<[number, number]> = [] const visit = (node: ts.Node): void => { if (node.kind === ts.SyntaxKind.RegularExpressionLiteral) { ranges.push([node.getStart(sourceFile), node.getEnd()]) return } ts.forEachChild(node, visit) } visit(sourceFile) return ranges } function maskSourceRanges(source: string, ranges: ReadonlyArray<[number, number]>): string { const edits = ranges.map(([start, end]) => ({ start, end, text: source.slice(start, end).replace(/[^\r\n]/g, ' '), })) return applySourceEdits(source, edits) } function collectStandardCommentRanges( source: string, sourceFile: ts.SourceFile ): Array<[number, number]> { const scannerSource = maskSourceRanges(source, collectRegularExpressionRanges(sourceFile)) const scanner = ts.createScanner( ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, scannerSource ) const ranges: Array<[number, number]> = [] for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) { if ( token === ts.SyntaxKind.SingleLineCommentTrivia || token === ts.SyntaxKind.MultiLineCommentTrivia || token === ts.SyntaxKind.ShebangTrivia ) { ranges.push([scanner.getTokenPos(), scanner.getTextPos()]) } } return ranges } function collectJavaScriptLiteralRanges(sourceFile: ts.SourceFile): Array<[number, number]> { const ranges: Array<[number, number]> = [] const visit = (node: ts.Node): void => { const isTemplateToken = node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateHead || node.kind === ts.SyntaxKind.TemplateMiddle || node.kind === ts.SyntaxKind.TemplateTail if ( ts.isStringLiteralLike(node) || isTemplateToken || node.kind === ts.SyntaxKind.RegularExpressionLiteral ) { ranges.push([node.getStart(sourceFile), node.getEnd()]) } ts.forEachChild(node, visit) } visit(sourceFile) return ranges } function collectAnnexBHtmlCommentRanges( source: string, sourceFile: ts.SourceFile ): AnnexBHtmlCommentRange[] { const protectedRanges = [ ...collectStandardCommentRanges(source, sourceFile), ...collectJavaScriptLiteralRanges(sourceFile), ] const ranges: AnnexBHtmlCommentRange[] = [] let lineStart = 0 while (lineStart < source.length) { const newline = source.indexOf('\n', lineStart) const lineEnd = newline === -1 ? source.length : newline const leadingWhitespace = /^\s*/.exec(source.slice(lineStart, lineEnd))?.[0].length ?? 0 const closeMarker = lineStart + leadingWhitespace if (source.startsWith('-->', closeMarker) && !isOffsetInRanges(closeMarker, protectedRanges)) { ranges.push({ start: closeMarker, end: lineEnd, markerLength: 3 }) lineStart = newline === -1 ? source.length : newline + 1 continue } let openMarker = source.indexOf('