|
|
|
|
|
import functions from "./functions"; |
|
import MacroExpander, {implicitCommands} from "./MacroExpander"; |
|
import symbols, {ATOMS, extraLatin} from "./symbols"; |
|
import {validUnit} from "./units"; |
|
import {supportedCodepoint} from "./unicodeScripts"; |
|
import ParseError from "./ParseError"; |
|
import {combiningDiacriticalMarksEndRegex} from "./Lexer"; |
|
import Settings from "./Settings"; |
|
import SourceLocation from "./SourceLocation"; |
|
import {uSubsAndSups, unicodeSubRegEx} from "./unicodeSupOrSub"; |
|
import {Token} from "./Token"; |
|
|
|
|
|
import unicodeAccents from "./unicodeAccents"; |
|
import unicodeSymbols from "./unicodeSymbols"; |
|
|
|
import type {ParseNode, AnyParseNode, SymbolParseNode, UnsupportedCmdParseNode} |
|
from "./parseNode"; |
|
import type {Atom, Group} from "./symbols"; |
|
import type {Mode, ArgType, BreakToken} from "./types"; |
|
import type {FunctionContext, FunctionSpec} from "./defineFunction"; |
|
import type {EnvSpec} from "./defineEnvironment"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default class Parser { |
|
mode: Mode; |
|
gullet: MacroExpander; |
|
settings: Settings; |
|
leftrightDepth: number; |
|
nextToken: ?Token; |
|
|
|
constructor(input: string, settings: Settings) { |
|
|
|
this.mode = "math"; |
|
|
|
|
|
this.gullet = new MacroExpander(input, settings, this.mode); |
|
|
|
this.settings = settings; |
|
|
|
this.leftrightDepth = 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
expect(text: string, consume?: boolean = true) { |
|
if (this.fetch().text !== text) { |
|
throw new ParseError( |
|
`Expected '${text}', got '${this.fetch().text}'`, this.fetch() |
|
); |
|
} |
|
if (consume) { |
|
this.consume(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
consume() { |
|
this.nextToken = null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
fetch(): Token { |
|
if (this.nextToken == null) { |
|
this.nextToken = this.gullet.expandNextToken(); |
|
} |
|
return this.nextToken; |
|
} |
|
|
|
|
|
|
|
|
|
switchMode(newMode: Mode) { |
|
this.mode = newMode; |
|
this.gullet.switchMode(newMode); |
|
} |
|
|
|
|
|
|
|
|
|
parse(): AnyParseNode[] { |
|
if (!this.settings.globalGroup) { |
|
|
|
|
|
this.gullet.beginGroup(); |
|
} |
|
|
|
|
|
|
|
|
|
if (this.settings.colorIsTextColor) { |
|
this.gullet.macros.set("\\color", "\\textcolor"); |
|
} |
|
|
|
try { |
|
|
|
const parse = this.parseExpression(false); |
|
|
|
|
|
this.expect("EOF"); |
|
|
|
|
|
if (!this.settings.globalGroup) { |
|
this.gullet.endGroup(); |
|
} |
|
|
|
return parse; |
|
|
|
|
|
} finally { |
|
this.gullet.endGroups(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
subparse(tokens: Token[]): AnyParseNode[] { |
|
|
|
const oldToken = this.nextToken; |
|
this.consume(); |
|
|
|
|
|
this.gullet.pushToken(new Token("}")); |
|
this.gullet.pushTokens(tokens); |
|
const parse = this.parseExpression(false); |
|
this.expect("}"); |
|
|
|
|
|
this.nextToken = oldToken; |
|
|
|
return parse; |
|
} |
|
|
|
static endOfExpression: string[] = ["}", "\\endgroup", "\\end", "\\right", "&"]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseExpression( |
|
breakOnInfix: boolean, |
|
breakOnTokenText?: BreakToken, |
|
): AnyParseNode[] { |
|
const body = []; |
|
|
|
|
|
while (true) { |
|
|
|
if (this.mode === "math") { |
|
this.consumeSpaces(); |
|
} |
|
const lex = this.fetch(); |
|
if (Parser.endOfExpression.indexOf(lex.text) !== -1) { |
|
break; |
|
} |
|
if (breakOnTokenText && lex.text === breakOnTokenText) { |
|
break; |
|
} |
|
if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { |
|
break; |
|
} |
|
const atom = this.parseAtom(breakOnTokenText); |
|
if (!atom) { |
|
break; |
|
} else if (atom.type === "internal") { |
|
continue; |
|
} |
|
body.push(atom); |
|
} |
|
if (this.mode === "text") { |
|
this.formLigatures(body); |
|
} |
|
return this.handleInfixNodes(body); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleInfixNodes(body: AnyParseNode[]): AnyParseNode[] { |
|
let overIndex = -1; |
|
let funcName; |
|
|
|
for (let i = 0; i < body.length; i++) { |
|
if (body[i].type === "infix") { |
|
if (overIndex !== -1) { |
|
throw new ParseError( |
|
"only one infix operator per group", |
|
body[i].token); |
|
} |
|
overIndex = i; |
|
funcName = body[i].replaceWith; |
|
} |
|
} |
|
|
|
if (overIndex !== -1 && funcName) { |
|
let numerNode; |
|
let denomNode; |
|
|
|
const numerBody = body.slice(0, overIndex); |
|
const denomBody = body.slice(overIndex + 1); |
|
|
|
if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { |
|
numerNode = numerBody[0]; |
|
} else { |
|
numerNode = {type: "ordgroup", mode: this.mode, body: numerBody}; |
|
} |
|
|
|
if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { |
|
denomNode = denomBody[0]; |
|
} else { |
|
denomNode = {type: "ordgroup", mode: this.mode, body: denomBody}; |
|
} |
|
|
|
let node; |
|
if (funcName === "\\\\abovefrac") { |
|
node = this.callFunction(funcName, |
|
[numerNode, body[overIndex], denomNode], []); |
|
} else { |
|
node = this.callFunction(funcName, [numerNode, denomNode], []); |
|
} |
|
return [node]; |
|
} else { |
|
return body; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
handleSupSubscript( |
|
name: string, |
|
): AnyParseNode { |
|
const symbolToken = this.fetch(); |
|
const symbol = symbolToken.text; |
|
this.consume(); |
|
this.consumeSpaces(); |
|
const group = this.parseGroup(name); |
|
|
|
if (!group) { |
|
throw new ParseError( |
|
"Expected group after '" + symbol + "'", |
|
symbolToken |
|
); |
|
} |
|
|
|
return group; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
formatUnsupportedCmd(text: string): UnsupportedCmdParseNode { |
|
const textordArray = []; |
|
|
|
for (let i = 0; i < text.length; i++) { |
|
textordArray.push({type: "textord", mode: "text", text: text[i]}); |
|
} |
|
|
|
const textNode = { |
|
type: "text", |
|
mode: this.mode, |
|
body: textordArray, |
|
}; |
|
|
|
const colorNode = { |
|
type: "color", |
|
mode: this.mode, |
|
color: this.settings.errorColor, |
|
body: [textNode], |
|
}; |
|
|
|
return colorNode; |
|
} |
|
|
|
|
|
|
|
|
|
parseAtom(breakOnTokenText?: BreakToken): ?AnyParseNode { |
|
|
|
|
|
const base = this.parseGroup("atom", breakOnTokenText); |
|
|
|
|
|
if (this.mode === "text") { |
|
return base; |
|
} |
|
|
|
|
|
|
|
let superscript; |
|
let subscript; |
|
while (true) { |
|
|
|
this.consumeSpaces(); |
|
|
|
|
|
const lex = this.fetch(); |
|
|
|
if (lex.text === "\\limits" || lex.text === "\\nolimits") { |
|
|
|
if (base && base.type === "op") { |
|
const limits = lex.text === "\\limits"; |
|
base.limits = limits; |
|
base.alwaysHandleSupSub = true; |
|
} else if (base && base.type === "operatorname") { |
|
if (base.alwaysHandleSupSub) { |
|
base.limits = lex.text === "\\limits"; |
|
} |
|
} else { |
|
throw new ParseError( |
|
"Limit controls must follow a math operator", |
|
lex); |
|
} |
|
this.consume(); |
|
} else if (lex.text === "^") { |
|
|
|
if (superscript) { |
|
throw new ParseError("Double superscript", lex); |
|
} |
|
superscript = this.handleSupSubscript("superscript"); |
|
} else if (lex.text === "_") { |
|
|
|
if (subscript) { |
|
throw new ParseError("Double subscript", lex); |
|
} |
|
subscript = this.handleSupSubscript("subscript"); |
|
} else if (lex.text === "'") { |
|
|
|
if (superscript) { |
|
throw new ParseError("Double superscript", lex); |
|
} |
|
const prime = {type: "textord", mode: this.mode, text: "\\prime"}; |
|
|
|
|
|
const primes = [prime]; |
|
this.consume(); |
|
|
|
while (this.fetch().text === "'") { |
|
|
|
primes.push(prime); |
|
this.consume(); |
|
} |
|
|
|
|
|
if (this.fetch().text === "^") { |
|
primes.push(this.handleSupSubscript("superscript")); |
|
} |
|
|
|
superscript = {type: "ordgroup", mode: this.mode, body: primes}; |
|
} else if (uSubsAndSups[lex.text]) { |
|
|
|
|
|
|
|
|
|
const isSub = unicodeSubRegEx.test(lex.text); |
|
const subsupTokens = []; |
|
subsupTokens.push(new Token(uSubsAndSups[lex.text])); |
|
this.consume(); |
|
|
|
while (true) { |
|
const token = this.fetch().text; |
|
if (!(uSubsAndSups[token])) { break; } |
|
if (unicodeSubRegEx.test(token) !== isSub) { break; } |
|
subsupTokens.unshift(new Token(uSubsAndSups[token])); |
|
this.consume(); |
|
} |
|
|
|
const body = this.subparse(subsupTokens); |
|
if (isSub) { |
|
subscript = {type: "ordgroup", mode: "math", body}; |
|
} else { |
|
superscript = {type: "ordgroup", mode: "math", body}; |
|
} |
|
} else { |
|
|
|
break; |
|
} |
|
} |
|
|
|
|
|
|
|
if (superscript || subscript) { |
|
|
|
return { |
|
type: "supsub", |
|
mode: this.mode, |
|
base: base, |
|
sup: superscript, |
|
sub: subscript, |
|
}; |
|
} else { |
|
|
|
return base; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
parseFunction( |
|
breakOnTokenText?: BreakToken, |
|
name?: string, |
|
): ?AnyParseNode { |
|
const token = this.fetch(); |
|
const func = token.text; |
|
const funcData = functions[func]; |
|
if (!funcData) { |
|
return null; |
|
} |
|
this.consume(); |
|
|
|
if (name && name !== "atom" && !funcData.allowedInArgument) { |
|
throw new ParseError( |
|
"Got function '" + func + "' with no arguments" + |
|
(name ? " as " + name : ""), token); |
|
} else if (this.mode === "text" && !funcData.allowedInText) { |
|
throw new ParseError( |
|
"Can't use function '" + func + "' in text mode", token); |
|
} else if (this.mode === "math" && funcData.allowedInMath === false) { |
|
throw new ParseError( |
|
"Can't use function '" + func + "' in math mode", token); |
|
} |
|
|
|
const {args, optArgs} = this.parseArguments(func, funcData); |
|
return this.callFunction(func, args, optArgs, token, breakOnTokenText); |
|
} |
|
|
|
|
|
|
|
|
|
callFunction( |
|
name: string, |
|
args: AnyParseNode[], |
|
optArgs: (?AnyParseNode)[], |
|
token?: Token, |
|
breakOnTokenText?: BreakToken, |
|
): AnyParseNode { |
|
const context: FunctionContext = { |
|
funcName: name, |
|
parser: this, |
|
token, |
|
breakOnTokenText, |
|
}; |
|
const func = functions[name]; |
|
if (func && func.handler) { |
|
return func.handler(context, args, optArgs); |
|
} else { |
|
throw new ParseError(`No function handler for ${name}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
parseArguments( |
|
func: string, |
|
funcData: FunctionSpec<*> | EnvSpec<*>, |
|
): { |
|
args: AnyParseNode[], |
|
optArgs: (?AnyParseNode)[], |
|
} { |
|
const totalArgs = funcData.numArgs + funcData.numOptionalArgs; |
|
if (totalArgs === 0) { |
|
return {args: [], optArgs: []}; |
|
} |
|
|
|
const args = []; |
|
const optArgs = []; |
|
|
|
for (let i = 0; i < totalArgs; i++) { |
|
let argType = funcData.argTypes && funcData.argTypes[i]; |
|
const isOptional = i < funcData.numOptionalArgs; |
|
|
|
if ((funcData.primitive && argType == null) || |
|
|
|
(funcData.type === "sqrt" && i === 1 && optArgs[0] == null)) { |
|
argType = "primitive"; |
|
} |
|
|
|
const arg = this.parseGroupOfType(`argument to '${func}'`, |
|
argType, isOptional); |
|
if (isOptional) { |
|
optArgs.push(arg); |
|
} else if (arg != null) { |
|
args.push(arg); |
|
} else { |
|
throw new ParseError("Null argument, please report this as a bug"); |
|
} |
|
} |
|
|
|
return {args, optArgs}; |
|
} |
|
|
|
|
|
|
|
|
|
parseGroupOfType( |
|
name: string, |
|
type: ?ArgType, |
|
optional: boolean, |
|
): ?AnyParseNode { |
|
switch (type) { |
|
case "color": |
|
return this.parseColorGroup(optional); |
|
case "size": |
|
return this.parseSizeGroup(optional); |
|
case "url": |
|
return this.parseUrlGroup(optional); |
|
case "math": |
|
case "text": |
|
return this.parseArgumentGroup(optional, type); |
|
case "hbox": { |
|
|
|
|
|
const group = this.parseArgumentGroup(optional, "text"); |
|
return group != null ? { |
|
type: "styling", |
|
mode: group.mode, |
|
body: [group], |
|
style: "text", |
|
} : null; |
|
} |
|
case "raw": { |
|
const token = this.parseStringGroup("raw", optional); |
|
return token != null ? { |
|
type: "raw", |
|
mode: "text", |
|
string: token.text, |
|
} : null; |
|
} |
|
case "primitive": { |
|
if (optional) { |
|
throw new ParseError("A primitive argument cannot be optional"); |
|
} |
|
const group = this.parseGroup(name); |
|
if (group == null) { |
|
throw new ParseError("Expected group as " + name, this.fetch()); |
|
} |
|
return group; |
|
} |
|
case "original": |
|
case null: |
|
case undefined: |
|
return this.parseArgumentGroup(optional); |
|
default: |
|
throw new ParseError( |
|
"Unknown group type as " + name, this.fetch()); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
consumeSpaces() { |
|
while (this.fetch().text === " ") { |
|
this.consume(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
parseStringGroup( |
|
modeName: ArgType, |
|
optional: boolean, |
|
): ?Token { |
|
const argToken = this.gullet.scanArgument(optional); |
|
if (argToken == null) { |
|
return null; |
|
} |
|
let str = ""; |
|
let nextToken; |
|
while ((nextToken = this.fetch()).text !== "EOF") { |
|
str += nextToken.text; |
|
this.consume(); |
|
} |
|
this.consume(); |
|
argToken.text = str; |
|
return argToken; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
parseRegexGroup( |
|
regex: RegExp, |
|
modeName: string, |
|
): Token { |
|
const firstToken = this.fetch(); |
|
let lastToken = firstToken; |
|
let str = ""; |
|
let nextToken; |
|
while ((nextToken = this.fetch()).text !== "EOF" && |
|
regex.test(str + nextToken.text)) { |
|
lastToken = nextToken; |
|
str += lastToken.text; |
|
this.consume(); |
|
} |
|
if (str === "") { |
|
throw new ParseError( |
|
"Invalid " + modeName + ": '" + firstToken.text + "'", |
|
firstToken); |
|
} |
|
return firstToken.range(lastToken, str); |
|
} |
|
|
|
|
|
|
|
|
|
parseColorGroup(optional: boolean): ?ParseNode<"color-token"> { |
|
const res = this.parseStringGroup("color", optional); |
|
if (res == null) { |
|
return null; |
|
} |
|
const match = (/^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i).exec(res.text); |
|
if (!match) { |
|
throw new ParseError("Invalid color: '" + res.text + "'", res); |
|
} |
|
let color = match[0]; |
|
if (/^[0-9a-f]{6}$/i.test(color)) { |
|
|
|
|
|
|
|
color = "#" + color; |
|
} |
|
return { |
|
type: "color-token", |
|
mode: this.mode, |
|
color, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
parseSizeGroup(optional: boolean): ?ParseNode<"size"> { |
|
let res; |
|
let isBlank = false; |
|
|
|
this.gullet.consumeSpaces(); |
|
if (!optional && this.gullet.future().text !== "{") { |
|
res = this.parseRegexGroup( |
|
/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); |
|
} else { |
|
res = this.parseStringGroup("size", optional); |
|
} |
|
if (!res) { |
|
return null; |
|
} |
|
if (!optional && res.text.length === 0) { |
|
|
|
|
|
|
|
res.text = "0pt"; |
|
isBlank = true; |
|
} |
|
const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); |
|
if (!match) { |
|
throw new ParseError("Invalid size: '" + res.text + "'", res); |
|
} |
|
const data = { |
|
number: +(match[1] + match[2]), |
|
unit: match[3], |
|
}; |
|
if (!validUnit(data)) { |
|
throw new ParseError("Invalid unit: '" + data.unit + "'", res); |
|
} |
|
return { |
|
type: "size", |
|
mode: this.mode, |
|
value: data, |
|
isBlank, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
parseUrlGroup(optional: boolean): ?ParseNode<"url"> { |
|
this.gullet.lexer.setCatcode("%", 13); |
|
this.gullet.lexer.setCatcode("~", 12); |
|
const res = this.parseStringGroup("url", optional); |
|
this.gullet.lexer.setCatcode("%", 14); |
|
this.gullet.lexer.setCatcode("~", 13); |
|
if (res == null) { |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
const url = res.text.replace(/\\([#$%&~_^{}])/g, '$1'); |
|
return { |
|
type: "url", |
|
mode: this.mode, |
|
url, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
parseArgumentGroup(optional: boolean, mode?: Mode): ?ParseNode<"ordgroup"> { |
|
const argToken = this.gullet.scanArgument(optional); |
|
if (argToken == null) { |
|
return null; |
|
} |
|
const outerMode = this.mode; |
|
if (mode) { |
|
this.switchMode(mode); |
|
} |
|
|
|
this.gullet.beginGroup(); |
|
const expression = this.parseExpression(false, "EOF"); |
|
|
|
this.expect("EOF"); |
|
this.gullet.endGroup(); |
|
const result = { |
|
type: "ordgroup", |
|
mode: this.mode, |
|
loc: argToken.loc, |
|
body: expression, |
|
}; |
|
|
|
if (mode) { |
|
this.switchMode(outerMode); |
|
} |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseGroup( |
|
name: string, |
|
breakOnTokenText?: BreakToken, |
|
): ?AnyParseNode { |
|
const firstToken = this.fetch(); |
|
const text = firstToken.text; |
|
|
|
let result; |
|
|
|
if (text === "{" || text === "\\begingroup") { |
|
this.consume(); |
|
const groupEnd = text === "{" ? "}" : "\\endgroup"; |
|
|
|
this.gullet.beginGroup(); |
|
|
|
const expression = this.parseExpression(false, groupEnd); |
|
const lastToken = this.fetch(); |
|
this.expect(groupEnd); |
|
this.gullet.endGroup(); |
|
result = { |
|
type: "ordgroup", |
|
mode: this.mode, |
|
loc: SourceLocation.range(firstToken, lastToken), |
|
body: expression, |
|
|
|
|
|
|
|
|
|
semisimple: text === "\\begingroup" || undefined, |
|
}; |
|
} else { |
|
|
|
|
|
result = this.parseFunction(breakOnTokenText, name) || |
|
this.parseSymbol(); |
|
if (result == null && text[0] === "\\" && |
|
!implicitCommands.hasOwnProperty(text)) { |
|
if (this.settings.throwOnError) { |
|
throw new ParseError( |
|
"Undefined control sequence: " + text, firstToken); |
|
} |
|
result = this.formatUnsupportedCmd(text); |
|
this.consume(); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
formLigatures(group: AnyParseNode[]) { |
|
let n = group.length - 1; |
|
for (let i = 0; i < n; ++i) { |
|
const a = group[i]; |
|
|
|
const v = a.text; |
|
if (v === "-" && group[i + 1].text === "-") { |
|
if (i + 1 < n && group[i + 2].text === "-") { |
|
group.splice(i, 3, { |
|
type: "textord", |
|
mode: "text", |
|
loc: SourceLocation.range(a, group[i + 2]), |
|
text: "---", |
|
}); |
|
n -= 2; |
|
} else { |
|
group.splice(i, 2, { |
|
type: "textord", |
|
mode: "text", |
|
loc: SourceLocation.range(a, group[i + 1]), |
|
text: "--", |
|
}); |
|
n -= 1; |
|
} |
|
} |
|
if ((v === "'" || v === "`") && group[i + 1].text === v) { |
|
group.splice(i, 2, { |
|
type: "textord", |
|
mode: "text", |
|
loc: SourceLocation.range(a, group[i + 1]), |
|
text: v + v, |
|
}); |
|
n -= 1; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
parseSymbol(): ?AnyParseNode { |
|
const nucleus = this.fetch(); |
|
let text = nucleus.text; |
|
|
|
if (/^\\verb[^a-zA-Z]/.test(text)) { |
|
this.consume(); |
|
let arg = text.slice(5); |
|
const star = (arg.charAt(0) === "*"); |
|
if (star) { |
|
arg = arg.slice(1); |
|
} |
|
|
|
|
|
if (arg.length < 2 || arg.charAt(0) !== arg.slice(-1)) { |
|
throw new ParseError(`\\verb assertion failed -- |
|
please report what input caused this bug`); |
|
} |
|
arg = arg.slice(1, -1); |
|
return { |
|
type: "verb", |
|
mode: "text", |
|
body: arg, |
|
star, |
|
}; |
|
} |
|
|
|
|
|
if (unicodeSymbols.hasOwnProperty(text[0]) && |
|
!symbols[this.mode][text[0]]) { |
|
|
|
if (this.settings.strict && this.mode === "math") { |
|
this.settings.reportNonstrict("unicodeTextInMathMode", |
|
`Accented Unicode text character "${text[0]}" used in ` + |
|
`math mode`, nucleus); |
|
} |
|
text = unicodeSymbols[text[0]] + text.slice(1); |
|
} |
|
|
|
const match = combiningDiacriticalMarksEndRegex.exec(text); |
|
if (match) { |
|
text = text.substring(0, match.index); |
|
if (text === 'i') { |
|
text = '\u0131'; |
|
} else if (text === 'j') { |
|
text = '\u0237'; |
|
} |
|
} |
|
|
|
let symbol: AnyParseNode; |
|
if (symbols[this.mode][text]) { |
|
if (this.settings.strict && this.mode === 'math' && |
|
extraLatin.indexOf(text) >= 0) { |
|
this.settings.reportNonstrict("unicodeTextInMathMode", |
|
`Latin-1/Unicode text character "${text[0]}" used in ` + |
|
`math mode`, nucleus); |
|
} |
|
const group: Group = symbols[this.mode][text].group; |
|
const loc = SourceLocation.range(nucleus); |
|
let s: SymbolParseNode; |
|
if (ATOMS.hasOwnProperty(group)) { |
|
|
|
const family: Atom = group; |
|
s = { |
|
type: "atom", |
|
mode: this.mode, |
|
family, |
|
loc, |
|
text, |
|
}; |
|
} else { |
|
|
|
s = { |
|
type: group, |
|
mode: this.mode, |
|
loc, |
|
text, |
|
}; |
|
} |
|
|
|
symbol = s; |
|
} else if (text.charCodeAt(0) >= 0x80) { |
|
if (this.settings.strict) { |
|
if (!supportedCodepoint(text.charCodeAt(0))) { |
|
this.settings.reportNonstrict("unknownSymbol", |
|
`Unrecognized Unicode character "${text[0]}"` + |
|
` (${text.charCodeAt(0)})`, nucleus); |
|
} else if (this.mode === "math") { |
|
this.settings.reportNonstrict("unicodeTextInMathMode", |
|
`Unicode text character "${text[0]}" used in math mode`, |
|
nucleus); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
symbol = { |
|
type: "textord", |
|
mode: "text", |
|
loc: SourceLocation.range(nucleus), |
|
text, |
|
}; |
|
} else { |
|
return null; |
|
} |
|
this.consume(); |
|
|
|
if (match) { |
|
for (let i = 0; i < match[0].length; i++) { |
|
const accent: string = match[0][i]; |
|
if (!unicodeAccents[accent]) { |
|
throw new ParseError(`Unknown accent ' ${accent}'`, nucleus); |
|
} |
|
const command = unicodeAccents[accent][this.mode] || |
|
unicodeAccents[accent].text; |
|
if (!command) { |
|
throw new ParseError( |
|
`Accent ${accent} unsupported in ${this.mode} mode`, |
|
nucleus); |
|
} |
|
symbol = { |
|
type: "accent", |
|
mode: this.mode, |
|
loc: SourceLocation.range(nucleus), |
|
label: command, |
|
isStretchy: false, |
|
isShifty: true, |
|
|
|
base: symbol, |
|
}; |
|
} |
|
} |
|
|
|
return symbol; |
|
} |
|
} |
|
|