import { SyntaxNode } from './node' // Parse string contents into fragments, interpolations, and escape sequences. export const parseString = (input: string, from: number, to: number, parser: any): SyntaxNode => { const stringNode = new SyntaxNode('String', from, to) const content = input.slice(from, to) const firstChar = content[0] // double quotes: no interpolation or escapes if (firstChar === '"') { const fragment = new SyntaxNode('DoubleQuote', from, to) stringNode.add(fragment) return stringNode } // curlies: interpolation but no escapes if (firstChar === '{') { parseCurlyString(stringNode, input, from, to, parser) return stringNode } // single-quotes: interpolation and escapes if (firstChar === "'") { parseSingleQuoteString(stringNode, input, from, to, parser) return stringNode } throw `Unknown string type starting with: ${firstChar}` } const parseSingleQuoteString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => { let pos = from + 1 // skip opening ' let fragmentStart = pos while (pos < to - 1) { // -1 to skip closing ' const char = input[pos] if (char === '\\' && pos + 1 < to - 1) { if (pos > fragmentStart) { const frag = new SyntaxNode('StringFragment', fragmentStart, pos) stringNode.add(frag) } const escNode = new SyntaxNode('EscapeSeq', pos, pos + 2) stringNode.add(escNode) pos += 2 fragmentStart = pos continue } if (char === '$') { if (pos > fragmentStart) { const frag = new SyntaxNode('StringFragment', fragmentStart, pos) stringNode.add(frag) } pos++ // skip $ if (input[pos] === '(') { const interpStart = pos - 1 // Include the $ const exprResult = parseInterpolationExpr(input, pos, parser) const interpNode = new SyntaxNode('Interpolation', interpStart, exprResult.endPos) interpNode.add(exprResult.node) stringNode.add(interpNode) pos = exprResult.endPos } else { const interpStart = pos - 1 const identEnd = findIdentifierEnd(input, pos, to - 1) const identNode = new SyntaxNode('FunctionCallOrIdentifier', pos, identEnd) const innerIdent = new SyntaxNode('Identifier', pos, identEnd) identNode.add(innerIdent) const interpNode = new SyntaxNode('Interpolation', interpStart, identEnd) interpNode.add(identNode) stringNode.add(interpNode) pos = identEnd } fragmentStart = pos continue } pos++ } if (pos > fragmentStart && fragmentStart < to - 1) { const frag = new SyntaxNode('StringFragment', fragmentStart, pos) stringNode.add(frag) } } const parseCurlyString = (stringNode: SyntaxNode, input: string, from: number, to: number, parser: any) => { let pos = from + 1 // skip opening { let fragmentStart = from // include the opening { in the fragment let depth = 1 while (pos < to && depth > 0) { const char = input[pos] // track nesting if (char === '{') { depth++ pos++ continue } if (char === '}') { depth-- if (depth === 0) { const frag = new SyntaxNode('CurlyString', fragmentStart, pos + 1) stringNode.add(frag) break } pos++ continue } if (char === '\\' && pos + 1 < to && input[pos + 1] === '$') { if (pos > fragmentStart) { const frag = new SyntaxNode('CurlyString', fragmentStart, pos) stringNode.add(frag) } const escapedFrag = new SyntaxNode('CurlyString', pos + 1, pos + 2) stringNode.add(escapedFrag) pos += 2 // skip \ and $ fragmentStart = pos continue } if (char === '$') { if (pos > fragmentStart) { const frag = new SyntaxNode('CurlyString', fragmentStart, pos) stringNode.add(frag) } pos++ // skip $ if (input[pos] === '(') { const interpStart = pos - 1 const exprResult = parseInterpolationExpr(input, pos, parser) const interpNode = new SyntaxNode('Interpolation', interpStart, exprResult.endPos) interpNode.add(exprResult.node) stringNode.add(interpNode) pos = exprResult.endPos } else { const interpStart = pos - 1 const identEnd = findIdentifierEnd(input, pos, to) const identNode = new SyntaxNode('FunctionCallOrIdentifier', pos, identEnd) const innerIdent = new SyntaxNode('Identifier', pos, identEnd) identNode.add(innerIdent) const interpNode = new SyntaxNode('Interpolation', interpStart, identEnd) interpNode.add(identNode) stringNode.add(interpNode) pos = identEnd } fragmentStart = pos continue } pos++ } } const parseInterpolationExpr = (input: string, pos: number, parser: any): { node: SyntaxNode, endPos: number } => { let depth = 1 let start = pos let end = pos + 1 // start after opening ( while (end < input.length && depth > 0) { if (input[end] === '(') depth++ if (input[end] === ')') { depth-- if (depth === 0) break } end++ } const exprContent = input.slice(start + 1, end) // Content between ( and ) const closeParen = end end++ // move past closing ) const exprNode = parser.parse(exprContent) const innerNode = exprNode.firstChild || exprNode const offset = start + 1 // position where exprContent starts in input adjustNodePositions(innerNode, offset) const parenNode = new SyntaxNode('ParenExpr', start, closeParen + 1) parenNode.add(innerNode) return { node: parenNode, endPos: end } } const adjustNodePositions = (node: SyntaxNode, offset: number) => { node.from += offset node.to += offset for (const child of node.children) { adjustNodePositions(child, offset) } } const findIdentifierEnd = (input: string, pos: number, maxPos: number): number => { let end = pos while (end < maxPos) { const char = input[end]! // Stop at non-identifier characters if (!/[a-z0-9\-?]/.test(char)) { break } end++ } return end }