mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-05 03:08:40 +00:00
Support \( and fix instant mode switching for $, \(, \text (#1213)
* Fix #1212 by supporting `\(...\)` in addition to `$...$` inline math nested inside `\text`. This turned out to be harder than one would think because of the way `$` was being handled specially, while `\(` needed to be handled as a function, which immediately consumed the `\(` token, which meant that the first following token got consumed in text mode instead of math mode (unlike `$` where we could switch the mode before consumption). * Added new `consumeMode` setting for functions which tells the parser to switch modes just for the consumption of the command token. * Now that we're working with functions, move all the `$` handling code into `functions/math.js`. Somewhat bizarrely, we can define `$` as a function (nearly identical to `\(`) and it all works (just like we can have single-character macros). This move removed a lot of messy stuff from `Parser.js`: `ParsedDollar`, `ParsedFuncOrArgOrDollar`, `newDollar`, `assertFuncOrArg`, and at least three explicit checks for `if (... === "$")`. * Moved the `consume()` for the command token from `parseSymbol` to `parseGivenFunction`. This ended up not being strictly necessary, but seems conceptually cleaner. * Partially address #1027 by setting `consumeMode: "text"` for `\text` commands. As a result, `\text` now instantly switches the mode to `"text"`, so even an unbraced macro argument will properly detect text mode. I added a test for this, which is a slightly weaker form of #1027.
This commit is contained in:
@@ -16,7 +16,7 @@ import Settings from "./Settings";
|
||||
import { Token } from "./Token";
|
||||
|
||||
import type { Mode, ArgType, BreakToken } from "./types";
|
||||
import type { FunctionContext, FunctionSpec } from "./defineFunction" ;
|
||||
import type { FunctionContext, FunctionSpec } from "./defineFunction";
|
||||
import type { EnvSpec } from "./defineEnvironment";
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,7 @@ import type { EnvSpec } from "./defineEnvironment";
|
||||
*
|
||||
* The earlier functions return ParseNodes.
|
||||
* The later functions (which are called deeper in the parse) sometimes return
|
||||
* ParsedFuncOrArgOrDollar, which contain a ParseNode as well as some data about
|
||||
* ParsedFuncOrArg, which contain a ParseNode as well as some data about
|
||||
* whether the parsed object is a function which is missing some arguments, or a
|
||||
* standalone object which can be used as an argument to another function.
|
||||
*/
|
||||
@@ -63,13 +63,6 @@ type ParsedArg = {|
|
||||
result: ParseNode,
|
||||
token: Token,
|
||||
|};
|
||||
type ParsedDollar = {|
|
||||
// Math mode switch
|
||||
type: "$",
|
||||
result: "$",
|
||||
token: Token,
|
||||
|};
|
||||
type ParsedFuncOrArgOrDollar = ParsedFunc | ParsedArg | ParsedDollar;
|
||||
type ParsedFuncOrArg = ParsedFunc | ParsedArg;
|
||||
|
||||
function newArgument(result: ParseNode, token: Token): ParsedArg {
|
||||
@@ -80,17 +73,6 @@ function newFunction(token: Token): ParsedFunc {
|
||||
return {type: "fn", result: token.text, token};
|
||||
}
|
||||
|
||||
function newDollar(token: Token): ParsedDollar {
|
||||
return {type: "$", result: "$", token};
|
||||
}
|
||||
|
||||
function assertFuncOrArg(parsed: ParsedFuncOrArgOrDollar): ParsedFuncOrArg {
|
||||
if (parsed.type === "$") {
|
||||
throw new ParseError("Unexpected $", parsed.token);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export default class Parser {
|
||||
mode: Mode;
|
||||
gullet: MacroExpander;
|
||||
@@ -294,20 +276,19 @@ export default class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
const arg = assertFuncOrArg(group);
|
||||
if (arg.type === "fn") {
|
||||
if (group.type === "fn") {
|
||||
// ^ and _ have a greediness, so handle interactions with functions'
|
||||
// greediness
|
||||
const funcGreediness = functions[arg.result].greediness;
|
||||
const funcGreediness = functions[group.result].greediness;
|
||||
if (funcGreediness > Parser.SUPSUB_GREEDINESS) {
|
||||
return this.parseGivenFunction(group);
|
||||
} else {
|
||||
throw new ParseError(
|
||||
"Got function '" + arg.result + "' with no arguments " +
|
||||
"Got function '" + group.result + "' with no arguments " +
|
||||
"as " + name, symbolToken);
|
||||
}
|
||||
} else {
|
||||
return arg.result;
|
||||
return group.result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,25 +436,7 @@ export default class Parser {
|
||||
|
||||
const func = start.result;
|
||||
|
||||
if (func === "$") {
|
||||
if (this.mode === "math") {
|
||||
throw new ParseError("$ within math mode");
|
||||
}
|
||||
const outerMode = this.mode;
|
||||
this.switchMode("math");
|
||||
// Expand next symbol now that we're in math mode.
|
||||
this.consume();
|
||||
const body = this.parseExpression(false, "$");
|
||||
// We can't expand the next symbol after the $ until after
|
||||
// switching modes back. So don't consume within expect.
|
||||
this.expect("$", false);
|
||||
this.switchMode(outerMode);
|
||||
this.consume();
|
||||
return new ParseNode("styling", {
|
||||
style: "text",
|
||||
value: body,
|
||||
}, "math");
|
||||
} else if (func === "\\begin") {
|
||||
if (func === "\\begin") {
|
||||
// begin...end is similar to left...right
|
||||
const begin = this.parseGivenFunction(start);
|
||||
const envName = begin.value.name;
|
||||
@@ -524,10 +487,9 @@ export default class Parser {
|
||||
* non-nullable result.
|
||||
*/
|
||||
parseGivenFunction(
|
||||
baseGroup: ParsedFuncOrArgOrDollar,
|
||||
baseGroup: ParsedFuncOrArg,
|
||||
breakOnTokenText?: BreakToken,
|
||||
): ParseNode {
|
||||
baseGroup = assertFuncOrArg(baseGroup);
|
||||
if (baseGroup.type === "fn") {
|
||||
const func = baseGroup.result;
|
||||
const funcData = functions[func];
|
||||
@@ -542,10 +504,21 @@ export default class Parser {
|
||||
baseGroup.token);
|
||||
}
|
||||
|
||||
// Consume the command token after possibly switching to the
|
||||
// mode specified by the function (for instant mode switching),
|
||||
// and then immediately switch back.
|
||||
if (funcData.consumeMode) {
|
||||
const oldMode = this.mode;
|
||||
this.switchMode(funcData.consumeMode);
|
||||
this.consume();
|
||||
this.switchMode(oldMode);
|
||||
} else {
|
||||
this.consume();
|
||||
}
|
||||
const {args, optArgs} = this.parseArguments(func, funcData);
|
||||
const token = baseGroup.token;
|
||||
const result =
|
||||
this.callFunction(func, args, optArgs, token, breakOnTokenText);
|
||||
const result = this.callFunction(
|
||||
func, args, optArgs, token, breakOnTokenText);
|
||||
return new ParseNode(result.type, result, this.mode);
|
||||
} else {
|
||||
return baseGroup.result;
|
||||
@@ -632,7 +605,6 @@ export default class Parser {
|
||||
}
|
||||
}
|
||||
let argNode: ParseNode;
|
||||
arg = assertFuncOrArg(arg);
|
||||
if (arg.type === "fn") {
|
||||
const argGreediness =
|
||||
functions[arg.result].greediness;
|
||||
@@ -658,7 +630,7 @@ export default class Parser {
|
||||
parseGroupOfType(
|
||||
type: ArgType, // Used to describe the mode in error messages.
|
||||
optional: boolean,
|
||||
): ?ParsedFuncOrArgOrDollar {
|
||||
): ?ParsedFuncOrArg {
|
||||
// Handle `original` argTypes
|
||||
if (type === "original") {
|
||||
type = this.mode;
|
||||
@@ -861,7 +833,7 @@ export default class Parser {
|
||||
* If `mode` is present, switches to that mode while parsing the group,
|
||||
* and switches back after.
|
||||
*/
|
||||
parseGroup(optional?: boolean, mode?: Mode): ?ParsedFuncOrArgOrDollar {
|
||||
parseGroup(optional?: boolean, mode?: Mode): ?ParsedFuncOrArg {
|
||||
const outerMode = this.mode;
|
||||
const firstToken = this.nextToken;
|
||||
// Try to parse an open brace
|
||||
@@ -936,14 +908,15 @@ export default class Parser {
|
||||
* Parse a single symbol out of the string. Here, we handle both the functions
|
||||
* we have defined, as well as the single character symbols
|
||||
*/
|
||||
parseSymbol(): ?ParsedFuncOrArgOrDollar {
|
||||
parseSymbol(): ?ParsedFuncOrArg {
|
||||
const nucleus = this.nextToken;
|
||||
let text = nucleus.text;
|
||||
|
||||
if (functions[text]) {
|
||||
this.consume();
|
||||
// If there exists a function with this name, we return the function and
|
||||
// say that it is a function.
|
||||
// If there exists a function with this name, we return the
|
||||
// function and say that it is a function.
|
||||
// The token will be consumed later in parseGivenFunction
|
||||
// (after possibly switching modes).
|
||||
return newFunction(nucleus);
|
||||
} else if (/^\\verb[^a-zA-Z]/.test(text)) {
|
||||
this.consume();
|
||||
@@ -964,8 +937,6 @@ export default class Parser {
|
||||
body: arg,
|
||||
star: star,
|
||||
}, "text"), nucleus);
|
||||
} else if (text === "$") {
|
||||
return newDollar(nucleus);
|
||||
}
|
||||
// At this point, we should have a symbol, possibly with accents.
|
||||
// First expand any accented base symbol according to unicodeSymbols,
|
||||
|
@@ -2,11 +2,11 @@
|
||||
import {groupTypes as htmlGroupTypes} from "./buildHTML";
|
||||
import {groupTypes as mathmlGroupTypes} from "./buildMathML";
|
||||
|
||||
import type Parser from "./Parser" ;
|
||||
import type ParseNode from "./ParseNode" ;
|
||||
import type Parser from "./Parser";
|
||||
import type ParseNode from "./ParseNode";
|
||||
import type Options from "./Options";
|
||||
import type {ArgType, BreakToken} from "./types" ;
|
||||
import type {Token} from "./Token" ;
|
||||
import type {ArgType, BreakToken, Mode} from "./types";
|
||||
import type {Token} from "./Token";
|
||||
|
||||
/** Context provided to function handlers for error messages. */
|
||||
export type FunctionContext = {|
|
||||
@@ -70,6 +70,14 @@ export type FunctionPropSpec = {
|
||||
|
||||
// Must be true if the function is an infix operator.
|
||||
infix?: boolean,
|
||||
|
||||
// Switch to the specified mode while consuming the command token.
|
||||
// This is useful for commands that switch between math and text mode,
|
||||
// for making sure that a switch happens early enough. Note that the
|
||||
// mode is switched immediately back to its original value after consuming
|
||||
// the command token, so that the argument parsing and/or function handler
|
||||
// can easily access the old mode while doing their own mode switching.
|
||||
consumeMode?: ?Mode,
|
||||
};
|
||||
|
||||
type FunctionDefSpec = {|
|
||||
@@ -119,6 +127,7 @@ export type FunctionSpec = {|
|
||||
allowedInMath: boolean,
|
||||
numOptionalArgs: number,
|
||||
infix: boolean,
|
||||
consumeMode: ?Mode,
|
||||
// Must be specified unless it's handled directly in the parser.
|
||||
handler: ?FunctionHandler,
|
||||
|};
|
||||
@@ -149,6 +158,7 @@ export default function defineFunction({
|
||||
: props.allowedInMath,
|
||||
numOptionalArgs: props.numOptionalArgs || 0,
|
||||
infix: !!props.infix,
|
||||
consumeMode: props.consumeMode,
|
||||
handler: handler,
|
||||
};
|
||||
for (let i = 0; i < names.length; ++i) {
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
_functions,
|
||||
} from "./defineFunction";
|
||||
|
||||
import type {FunctionPropSpec, FunctionHandler} from "./defineFunction" ;
|
||||
import type {FunctionPropSpec, FunctionHandler} from "./defineFunction";
|
||||
|
||||
// WARNING: New functions should be added to src/functions and imported here.
|
||||
|
||||
@@ -33,6 +33,8 @@ import "./functions/color";
|
||||
|
||||
import "./functions/text";
|
||||
|
||||
import "./functions/math";
|
||||
|
||||
import "./functions/enclose";
|
||||
|
||||
import "./functions/overline";
|
||||
|
44
src/functions/math.js
Normal file
44
src/functions/math.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// @flow
|
||||
import defineFunction from "../defineFunction";
|
||||
import ParseError from "../ParseError";
|
||||
|
||||
// Switching from text mode back to math mode
|
||||
defineFunction({
|
||||
names: ["\\(", "$"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
allowedInMath: false,
|
||||
consumeMode: "math",
|
||||
},
|
||||
handler(context, args) {
|
||||
const {funcName, parser} = context;
|
||||
const outerMode = parser.mode;
|
||||
parser.switchMode("math");
|
||||
const close = (funcName === "\\(" ? "\\)" : "$");
|
||||
const body = parser.parseExpression(false, close);
|
||||
// We can't expand the next symbol after the closing $ until after
|
||||
// switching modes back. So don't consume within expect.
|
||||
parser.expect(close, false);
|
||||
parser.switchMode(outerMode);
|
||||
parser.consume();
|
||||
return {
|
||||
type: "styling",
|
||||
style: "text",
|
||||
value: body,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Check for extra closing math delimiters
|
||||
defineFunction({
|
||||
names: ["\\)", "\\]"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
allowedInMath: false,
|
||||
},
|
||||
handler(context, args) {
|
||||
throw new ParseError(`Mismatched ${context.funcName}`);
|
||||
},
|
||||
});
|
@@ -35,6 +35,7 @@ defineFunction({
|
||||
argTypes: ["text"],
|
||||
greediness: 2,
|
||||
allowedInText: true,
|
||||
consumeMode: "text",
|
||||
},
|
||||
handler(context, args) {
|
||||
const body = args[0];
|
||||
|
@@ -23,4 +23,5 @@ export type ArgType = "color" | "size" | "url" | "original" | Mode;
|
||||
// LaTeX display style.
|
||||
export type StyleStr = "text" | "display";
|
||||
|
||||
export type BreakToken = "]" | "}" | "$";
|
||||
// Allowable token text for "break" arguments in parser
|
||||
export type BreakToken = "]" | "}" | "$" | "\\)";
|
||||
|
@@ -812,7 +812,6 @@ describe("A text parser", function() {
|
||||
const badTextExpression = "\\text{a b%}";
|
||||
const badFunctionExpression = "\\text{\\sqrt{x}}";
|
||||
const mathTokenAfterText = "\\text{sin}^2";
|
||||
const textWithEmbeddedMath = "\\text{graph: $y = mx + b$}";
|
||||
|
||||
it("should not fail", function() {
|
||||
expect(textExpression).toParse();
|
||||
@@ -872,7 +871,40 @@ describe("A text parser", function() {
|
||||
});
|
||||
|
||||
it("should parse math within text group", function() {
|
||||
expect(textWithEmbeddedMath).toParse();
|
||||
expect("\\text{graph: $y = mx + b$}").toParse();
|
||||
expect("\\text{graph: \\(y = mx + b\\)}").toParse();
|
||||
});
|
||||
|
||||
it("should parse math within text within math within text", function() {
|
||||
expect("\\text{hello $x + \\text{world $y$} + z$}").toParse();
|
||||
expect("\\text{hello \\(x + \\text{world $y$} + z\\)}").toParse();
|
||||
expect("\\text{hello $x + \\text{world \\(y\\)} + z$}").toParse();
|
||||
expect("\\text{hello \\(x + \\text{world \\(y\\)} + z\\)}").toParse();
|
||||
});
|
||||
|
||||
it("should forbid \\( within math mode", function() {
|
||||
expect("\\(").toNotParse();
|
||||
expect("\\text{$\\(x\\)$}").toNotParse();
|
||||
});
|
||||
|
||||
it("should forbid $ within math mode", function() {
|
||||
expect("$x$").toNotParse();
|
||||
expect("\\text{\\($x$\\)}").toNotParse();
|
||||
});
|
||||
|
||||
it("should detect unbalanced \\)", function() {
|
||||
expect("\\)").toNotParse();
|
||||
expect("\\text{\\)}").toNotParse();
|
||||
});
|
||||
|
||||
it("should detect unbalanced $", function() {
|
||||
expect("$").toNotParse();
|
||||
expect("\\text{$}").toNotParse();
|
||||
});
|
||||
|
||||
it("should not mix $ and \\(..\\)", function() {
|
||||
expect("\\text{$x\\)}").toNotParse();
|
||||
expect("\\text{\\(x$}").toNotParse();
|
||||
});
|
||||
|
||||
it("should parse spacing functions", function() {
|
||||
@@ -2824,8 +2856,13 @@ describe("A macro expander", function() {
|
||||
{"\\mode": "\\TextOrMath{text}{math}"});
|
||||
});
|
||||
|
||||
// TODO(edemaine): This doesn't work yet. Parses like `\text math`,
|
||||
// which doesn't even treat all four letters as an argument.
|
||||
it("\\TextOrMath should work in a macro passed to \\text", function() {
|
||||
compareParseTree("\\text\\mode", "\\text t",
|
||||
{"\\mode": "\\TextOrMath{t}{m}"});
|
||||
});
|
||||
|
||||
// TODO(edemaine): This doesn't work yet. Parses like `\text text`,
|
||||
// which doesn't treat all four letters as an argument.
|
||||
//it("\\TextOrMath should work in a macro passed to \\text", function() {
|
||||
// compareParseTree("\\text\\mode", "\\text{text}",
|
||||
// {"\\mode": "\\TextOrMath{text}{math}"});
|
||||
|
Reference in New Issue
Block a user