|
|
|
|
|
|
|
|
|
|
|
|
|
import functions from "./functions"; |
|
import symbols from "./symbols"; |
|
import Lexer from "./Lexer"; |
|
import {Token} from "./Token"; |
|
import type {Mode} from "./types"; |
|
import ParseError from "./ParseError"; |
|
import Namespace from "./Namespace"; |
|
import macros from "./macros"; |
|
|
|
import type {MacroContextInterface, MacroDefinition, MacroExpansion, MacroArg} |
|
from "./defineMacro"; |
|
import type Settings from "./Settings"; |
|
|
|
|
|
|
|
export const implicitCommands = { |
|
"^": true, |
|
"_": true, |
|
"\\limits": true, |
|
"\\nolimits": true, |
|
}; |
|
|
|
export default class MacroExpander implements MacroContextInterface { |
|
settings: Settings; |
|
expansionCount: number; |
|
lexer: Lexer; |
|
macros: Namespace<MacroDefinition>; |
|
stack: Token[]; |
|
mode: Mode; |
|
|
|
constructor(input: string, settings: Settings, mode: Mode) { |
|
this.settings = settings; |
|
this.expansionCount = 0; |
|
this.feed(input); |
|
|
|
this.macros = new Namespace(macros, settings.macros); |
|
this.mode = mode; |
|
this.stack = []; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
feed(input: string) { |
|
this.lexer = new Lexer(input, this.settings); |
|
} |
|
|
|
|
|
|
|
|
|
switchMode(newMode: Mode) { |
|
this.mode = newMode; |
|
} |
|
|
|
|
|
|
|
|
|
beginGroup() { |
|
this.macros.beginGroup(); |
|
} |
|
|
|
|
|
|
|
|
|
endGroup() { |
|
this.macros.endGroup(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
endGroups() { |
|
this.macros.endGroups(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
future(): Token { |
|
if (this.stack.length === 0) { |
|
this.pushToken(this.lexer.lex()); |
|
} |
|
return this.stack[this.stack.length - 1]; |
|
} |
|
|
|
|
|
|
|
|
|
popToken(): Token { |
|
this.future(); |
|
return this.stack.pop(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
pushToken(token: Token) { |
|
this.stack.push(token); |
|
} |
|
|
|
|
|
|
|
|
|
pushTokens(tokens: Token[]) { |
|
this.stack.push(...tokens); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
scanArgument(isOptional: boolean): ?Token { |
|
let start; |
|
let end; |
|
let tokens; |
|
if (isOptional) { |
|
this.consumeSpaces(); |
|
if (this.future().text !== "[") { |
|
return null; |
|
} |
|
start = this.popToken(); |
|
({tokens, end} = this.consumeArg(["]"])); |
|
} else { |
|
({tokens, start, end} = this.consumeArg()); |
|
} |
|
|
|
|
|
this.pushToken(new Token("EOF", end.loc)); |
|
|
|
this.pushTokens(tokens); |
|
return start.range(end, ""); |
|
} |
|
|
|
|
|
|
|
|
|
consumeSpaces() { |
|
for (;;) { |
|
const token = this.future(); |
|
if (token.text === " ") { |
|
this.stack.pop(); |
|
} else { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
consumeArg(delims?: ?string[]): MacroArg { |
|
|
|
|
|
|
|
|
|
|
|
|
|
const tokens: Token[] = []; |
|
const isDelimited = delims && delims.length > 0; |
|
if (!isDelimited) { |
|
|
|
|
|
|
|
|
|
this.consumeSpaces(); |
|
} |
|
const start = this.future(); |
|
let tok; |
|
let depth = 0; |
|
let match = 0; |
|
do { |
|
tok = this.popToken(); |
|
tokens.push(tok); |
|
if (tok.text === "{") { |
|
++depth; |
|
} else if (tok.text === "}") { |
|
--depth; |
|
if (depth === -1) { |
|
throw new ParseError("Extra }", tok); |
|
} |
|
} else if (tok.text === "EOF") { |
|
throw new ParseError("Unexpected end of input in a macro argument" + |
|
", expected '" + (delims && isDelimited ? delims[match] : "}") + |
|
"'", tok); |
|
} |
|
if (delims && isDelimited) { |
|
if ((depth === 0 || (depth === 1 && delims[match] === "{")) && |
|
tok.text === delims[match]) { |
|
++match; |
|
if (match === delims.length) { |
|
|
|
tokens.splice(-match, match); |
|
break; |
|
} |
|
} else { |
|
match = 0; |
|
} |
|
} |
|
} while (depth !== 0 || isDelimited); |
|
|
|
|
|
if (start.text === "{" && tokens[tokens.length - 1].text === "}") { |
|
tokens.pop(); |
|
tokens.shift(); |
|
} |
|
tokens.reverse(); |
|
return {tokens, start, end: tok}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
consumeArgs(numArgs: number, delimiters?: string[][]): Token[][] { |
|
if (delimiters) { |
|
if (delimiters.length !== numArgs + 1) { |
|
throw new ParseError( |
|
"The length of delimiters doesn't match the number of args!"); |
|
} |
|
const delims = delimiters[0]; |
|
for (let i = 0; i < delims.length; i++) { |
|
const tok = this.popToken(); |
|
if (delims[i] !== tok.text) { |
|
throw new ParseError( |
|
"Use of the macro doesn't match its definition", tok); |
|
} |
|
} |
|
} |
|
|
|
const args: Token[][] = []; |
|
for (let i = 0; i < numArgs; i++) { |
|
args.push(this.consumeArg(delimiters && delimiters[i + 1]).tokens); |
|
} |
|
return args; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
countExpansion(amount: number): void { |
|
this.expansionCount += amount; |
|
if (this.expansionCount > this.settings.maxExpand) { |
|
throw new ParseError("Too many expansions: infinite loop or " + |
|
"need to increase maxExpand setting"); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expandOnce(expandableOnly?: boolean): number | boolean { |
|
const topToken = this.popToken(); |
|
const name = topToken.text; |
|
const expansion = !topToken.noexpand ? this._getExpansion(name) : null; |
|
if (expansion == null || (expandableOnly && expansion.unexpandable)) { |
|
if (expandableOnly && expansion == null && |
|
name[0] === "\\" && !this.isDefined(name)) { |
|
throw new ParseError("Undefined control sequence: " + name); |
|
} |
|
this.pushToken(topToken); |
|
return false; |
|
} |
|
this.countExpansion(1); |
|
let tokens = expansion.tokens; |
|
const args = this.consumeArgs(expansion.numArgs, expansion.delimiters); |
|
if (expansion.numArgs) { |
|
|
|
tokens = tokens.slice(); |
|
for (let i = tokens.length - 1; i >= 0; --i) { |
|
let tok = tokens[i]; |
|
if (tok.text === "#") { |
|
if (i === 0) { |
|
throw new ParseError( |
|
"Incomplete placeholder at end of macro body", |
|
tok); |
|
} |
|
tok = tokens[--i]; |
|
if (tok.text === "#") { |
|
tokens.splice(i + 1, 1); |
|
} else if (/^[1-9]$/.test(tok.text)) { |
|
|
|
tokens.splice(i, 2, ...args[+tok.text - 1]); |
|
} else { |
|
throw new ParseError( |
|
"Not a valid argument number", |
|
tok); |
|
} |
|
} |
|
} |
|
} |
|
|
|
this.pushTokens(tokens); |
|
return tokens.length; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expandAfterFuture(): Token { |
|
this.expandOnce(); |
|
return this.future(); |
|
} |
|
|
|
|
|
|
|
|
|
expandNextToken(): Token { |
|
for (;;) { |
|
if (this.expandOnce() === false) { |
|
const token = this.stack.pop(); |
|
|
|
|
|
if (token.treatAsRelax) { |
|
token.text = "\\relax"; |
|
} |
|
return token; |
|
} |
|
} |
|
|
|
|
|
|
|
throw new Error(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
expandMacro(name: string): Token[] | void { |
|
return this.macros.has(name) |
|
? this.expandTokens([new Token(name)]) : undefined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
expandTokens(tokens: Token[]): Token[] { |
|
const output = []; |
|
const oldStackLength = this.stack.length; |
|
this.pushTokens(tokens); |
|
while (this.stack.length > oldStackLength) { |
|
|
|
if (this.expandOnce(true) === false) { |
|
const token = this.stack.pop(); |
|
if (token.treatAsRelax) { |
|
|
|
token.noexpand = false; |
|
token.treatAsRelax = false; |
|
} |
|
output.push(token); |
|
} |
|
} |
|
|
|
|
|
this.countExpansion(output.length); |
|
return output; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
expandMacroAsText(name: string): string | void { |
|
const tokens = this.expandMacro(name); |
|
if (tokens) { |
|
return tokens.map((token) => token.text).join(""); |
|
} else { |
|
return tokens; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
_getExpansion(name: string): ?MacroExpansion { |
|
const definition = this.macros.get(name); |
|
if (definition == null) { |
|
return definition; |
|
} |
|
|
|
|
|
if (name.length === 1) { |
|
const catcode = this.lexer.catcodes[name]; |
|
if (catcode != null && catcode !== 13) { |
|
return; |
|
} |
|
} |
|
const expansion = |
|
typeof definition === "function" ? definition(this) : definition; |
|
if (typeof expansion === "string") { |
|
let numArgs = 0; |
|
if (expansion.indexOf("#") !== -1) { |
|
const stripped = expansion.replace(/##/g, ""); |
|
while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { |
|
++numArgs; |
|
} |
|
} |
|
const bodyLexer = new Lexer(expansion, this.settings); |
|
const tokens = []; |
|
let tok = bodyLexer.lex(); |
|
while (tok.text !== "EOF") { |
|
tokens.push(tok); |
|
tok = bodyLexer.lex(); |
|
} |
|
tokens.reverse(); |
|
const expanded = {tokens, numArgs}; |
|
return expanded; |
|
} |
|
|
|
return expansion; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isDefined(name: string): boolean { |
|
return this.macros.has(name) || |
|
functions.hasOwnProperty(name) || |
|
symbols.math.hasOwnProperty(name) || |
|
symbols.text.hasOwnProperty(name) || |
|
implicitCommands.hasOwnProperty(name); |
|
} |
|
|
|
|
|
|
|
|
|
isExpandable(name: string): boolean { |
|
const macro = this.macros.get(name); |
|
return macro != null ? typeof macro === "string" |
|
|| typeof macro === "function" || !macro.unexpandable |
|
: functions.hasOwnProperty(name) && !functions[name].primitive; |
|
} |
|
} |
|
|