|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let instance; |
|
|
export { instance as MacroCstWalker }; |
|
|
|
|
|
class MacroCstWalker { |
|
|
static #instance; |
|
|
static get instance() { return MacroCstWalker.#instance ?? (MacroCstWalker.#instance = new MacroCstWalker()); } |
|
|
|
|
|
constructor() { } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
evaluateDocument(options) { |
|
|
const { text, cst, env, resolveMacro } = options; |
|
|
|
|
|
if (typeof text !== 'string') { |
|
|
throw new Error('MacroCstWalker.evaluateDocument: text must be a string'); |
|
|
} |
|
|
if (!cst || typeof cst !== 'object' || !cst.children) { |
|
|
throw new Error('MacroCstWalker.evaluateDocument: cst must be a CstNode'); |
|
|
} |
|
|
if (typeof resolveMacro !== 'function') { |
|
|
throw new Error('MacroCstWalker.evaluateDocument: resolveMacro must be a function'); |
|
|
} |
|
|
|
|
|
|
|
|
const context = { text, env, resolveMacro }; |
|
|
const items = this.#collectDocumentItems(cst); |
|
|
|
|
|
if (items.length === 0) { |
|
|
return text; |
|
|
} |
|
|
|
|
|
let result = ''; |
|
|
let cursor = 0; |
|
|
|
|
|
|
|
|
for (const item of items) { |
|
|
if (item.startOffset > cursor) { |
|
|
result += text.slice(cursor, item.startOffset); |
|
|
} |
|
|
|
|
|
|
|
|
if (item.type === 'plaintext') { |
|
|
result += text.slice(item.startOffset, item.endOffset + 1); |
|
|
} else { |
|
|
result += this.#evaluateMacroNode(item.node, context); |
|
|
} |
|
|
|
|
|
cursor = item.endOffset + 1; |
|
|
} |
|
|
|
|
|
if (cursor < text.length) { |
|
|
result += text.slice(cursor); |
|
|
} |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#collectDocumentItems(cst) { |
|
|
const plaintextTokens = (cst.children.plaintext || []); |
|
|
const macroNodes = (cst.children.macro || []); |
|
|
|
|
|
|
|
|
const items = []; |
|
|
|
|
|
for (const token of plaintextTokens) { |
|
|
if (typeof token.startOffset !== 'number' || typeof token.endOffset !== 'number') { |
|
|
continue; |
|
|
} |
|
|
|
|
|
items.push({ |
|
|
type: 'plaintext', |
|
|
startOffset: token.startOffset, |
|
|
endOffset: token.endOffset, |
|
|
token, |
|
|
}); |
|
|
} |
|
|
|
|
|
for (const macroNode of macroNodes) { |
|
|
const children = macroNode.children || {}; |
|
|
const endToken = ((children['Macro.End'] || [])[0]); |
|
|
|
|
|
|
|
|
if (this.#isRecoveryToken(endToken)) { |
|
|
|
|
|
this.#flattenIncompleteMacro(macroNode, endToken, items); |
|
|
continue; |
|
|
} |
|
|
|
|
|
const range = this.#getMacroRange(macroNode); |
|
|
items.push({ |
|
|
type: 'macro', |
|
|
startOffset: range.startOffset, |
|
|
endOffset: range.endOffset, |
|
|
node: macroNode, |
|
|
}); |
|
|
} |
|
|
|
|
|
items.sort((a, b) => { |
|
|
if (a.startOffset !== b.startOffset) { |
|
|
return a.startOffset - b.startOffset; |
|
|
} |
|
|
return a.endOffset - b.endOffset; |
|
|
}); |
|
|
|
|
|
return items; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#evaluateMacroNode(macroNode, context) { |
|
|
const { text, env, resolveMacro } = context; |
|
|
|
|
|
const children = macroNode.children || {}; |
|
|
const identifierTokens = (children['Macro.identifier'] || []); |
|
|
const name = identifierTokens[0]?.image || ''; |
|
|
|
|
|
const range = this.#getMacroRange(macroNode); |
|
|
const startToken = ((children['Macro.Start'] || [])[0]); |
|
|
const endToken = ((children['Macro.End'] || [])[0]); |
|
|
|
|
|
const innerStart = startToken ? startToken.endOffset + 1 : range.startOffset; |
|
|
const innerEnd = endToken ? endToken.startOffset - 1 : range.endOffset; |
|
|
|
|
|
|
|
|
const argumentsNode = ((children.arguments || [])[0]); |
|
|
const argumentNodes = (argumentsNode?.children?.argument || []); |
|
|
|
|
|
|
|
|
const args = []; |
|
|
|
|
|
const evaluatedArguments = []; |
|
|
|
|
|
for (const argNode of argumentNodes) { |
|
|
const argValue = this.#evaluateArgumentNode(argNode, context); |
|
|
args.push(argValue); |
|
|
|
|
|
const location = this.#getArgumentLocation(argNode); |
|
|
if (location) { |
|
|
evaluatedArguments.push({ |
|
|
value: argValue, |
|
|
...location, |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
evaluatedArguments.sort((a, b) => a.startOffset - b.startOffset); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let rawInner = ''; |
|
|
if (innerStart <= innerEnd) { |
|
|
let cursor = innerStart; |
|
|
|
|
|
for (const entry of evaluatedArguments) { |
|
|
if (entry.startOffset > cursor) { |
|
|
rawInner += text.slice(cursor, entry.startOffset); |
|
|
} |
|
|
|
|
|
rawInner += entry.value; |
|
|
cursor = entry.endOffset + 1; |
|
|
} |
|
|
|
|
|
if (cursor <= innerEnd) { |
|
|
rawInner += text.slice(cursor, innerEnd + 1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const call = { |
|
|
name, |
|
|
args, |
|
|
rawInner, |
|
|
rawWithBraces: text.slice(range.startOffset, range.endOffset + 1), |
|
|
range, |
|
|
cstNode: macroNode, |
|
|
env, |
|
|
}; |
|
|
|
|
|
const value = resolveMacro(call); |
|
|
const stringValue = typeof value === 'string' ? value : String(value ?? ''); |
|
|
|
|
|
return stringValue; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#evaluateArgumentNode(argNode, context) { |
|
|
const location = this.#getArgumentLocation(argNode); |
|
|
if (!location) { |
|
|
return ''; |
|
|
} |
|
|
|
|
|
const { text } = context; |
|
|
|
|
|
const nestedMacros = ((argNode.children || {}).macro || []); |
|
|
|
|
|
|
|
|
if (nestedMacros.length === 0) { |
|
|
return text.slice(location.startOffset, location.endOffset + 1); |
|
|
} |
|
|
|
|
|
|
|
|
const nestedWithRange = nestedMacros.map(node => ({ |
|
|
node, |
|
|
range: this.#getMacroRange(node), |
|
|
})); |
|
|
|
|
|
nestedWithRange.sort((a, b) => a.range.startOffset - b.range.startOffset); |
|
|
|
|
|
let result = ''; |
|
|
let cursor = location.startOffset; |
|
|
|
|
|
for (const entry of nestedWithRange) { |
|
|
if (entry.range.startOffset < cursor) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
result += text.slice(cursor, entry.range.startOffset); |
|
|
result += this.#evaluateMacroNode(entry.node, context); |
|
|
cursor = entry.range.endOffset + 1; |
|
|
} |
|
|
|
|
|
if (cursor <= location.endOffset) { |
|
|
result += text.slice(cursor, location.endOffset + 1); |
|
|
} |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#getMacroRange(macroNode) { |
|
|
const startToken = (((macroNode.children || {})['Macro.Start'] || [])[0]); |
|
|
const endToken = (((macroNode.children || {})['Macro.End'] || [])[0]); |
|
|
|
|
|
if (startToken && endToken) { |
|
|
return { startOffset: startToken.startOffset, endOffset: endToken.endOffset }; |
|
|
} |
|
|
if (macroNode.location) { |
|
|
return { startOffset: macroNode.location.startOffset, endOffset: macroNode.location.endOffset }; |
|
|
} |
|
|
return { startOffset: 0, endOffset: 0 }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#flattenIncompleteMacro(macroNode, excludeToken, items) { |
|
|
const children = macroNode.children || {}; |
|
|
|
|
|
for (const key of Object.keys(children)) { |
|
|
for (const element of children[key] || []) { |
|
|
|
|
|
if (element === excludeToken) continue; |
|
|
|
|
|
|
|
|
if ('startOffset' in element && typeof element.startOffset === 'number') { |
|
|
items.push({ |
|
|
type: 'plaintext', |
|
|
startOffset: element.startOffset, |
|
|
endOffset: element.endOffset ?? element.startOffset, |
|
|
token: element, |
|
|
}); |
|
|
} |
|
|
|
|
|
else if ('children' in element) { |
|
|
const nestedChildren = element.children || {}; |
|
|
const nestedEnd = ((nestedChildren['Macro.End'] || [])[0]); |
|
|
const nestedStart = ((nestedChildren['Macro.Start'] || [])[0]); |
|
|
|
|
|
|
|
|
if (nestedStart && nestedEnd) { |
|
|
if (!this.#isRecoveryToken(nestedEnd)) { |
|
|
|
|
|
const range = this.#getMacroRange(element); |
|
|
items.push({ |
|
|
type: 'macro', |
|
|
startOffset: range.startOffset, |
|
|
endOffset: range.endOffset, |
|
|
node: element, |
|
|
}); |
|
|
} else { |
|
|
|
|
|
this.#flattenIncompleteMacro(element, nestedEnd, items); |
|
|
} |
|
|
} else { |
|
|
|
|
|
this.#flattenIncompleteMacro(element, excludeToken, items); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#isRecoveryToken(token) { |
|
|
return token?.isInsertedInRecovery === true |
|
|
|| typeof token?.startOffset !== 'number' |
|
|
|| Number.isNaN(token?.startOffset); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#getArgumentLocation(argNode) { |
|
|
const children = argNode.children || {}; |
|
|
let startOffset = Number.POSITIVE_INFINITY; |
|
|
let endOffset = Number.NEGATIVE_INFINITY; |
|
|
|
|
|
for (const key of Object.keys(children)) { |
|
|
for (const element of children[key] || []) { |
|
|
if (this.#isCstNode(element)) { |
|
|
const location = element.location; |
|
|
if (!location) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
if (location.startOffset < startOffset) { |
|
|
startOffset = location.startOffset; |
|
|
} |
|
|
if (location.endOffset > endOffset) { |
|
|
endOffset = location.endOffset; |
|
|
} |
|
|
} else if (element) { |
|
|
if (element.startOffset < startOffset) { |
|
|
startOffset = element.startOffset; |
|
|
} |
|
|
if (element.endOffset > endOffset) { |
|
|
endOffset = element.endOffset; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (!Number.isFinite(startOffset) || !Number.isFinite(endOffset)) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
return { startOffset, endOffset }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#isCstNode(value) { |
|
|
return !!value && typeof value === 'object' && 'name' in value && 'children' in value; |
|
|
} |
|
|
} |
|
|
|
|
|
instance = MacroCstWalker.instance; |
|
|
|