227 lines
6.1 KiB
TypeScript
227 lines
6.1 KiB
TypeScript
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
|
|
}
|