From 578877764d7697dc673e61ed766a796b988caa76 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 17 Jul 2018 22:49:50 -0400 Subject: [PATCH] \@binrel command and refactor (#1487) * \@binrel command and refactor New `\@binrel{x}{y}` command renders `y` as if it's a bin/rel/ord like `x`. This is equivalent to AMSTeX's `\binrel@{x}\binrel@@{y}`. It will hopefully be useful for macros, such as `\pmb` (#1418). Also refactor the (already duplicated) code for implementing this functionality into buildCommon.binrelClass. * Add tests; fix flow error * Another test * Move binrelClass to functions/mclass.js * Remove expensive array destructuring polyfill --- src/functions/font.js | 13 ++++------- src/functions/mclass.js | 51 ++++++++++++++++++++++++++++++----------- test/katex-spec.js | 17 ++++++++++++++ 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/functions/font.js b/src/functions/font.js index d63c5d96..3a495d43 100644 --- a/src/functions/font.js +++ b/src/functions/font.js @@ -1,6 +1,7 @@ // @flow // TODO(kevinb): implement \\sl and \\sc +import {binrelClass} from "./mclass"; import defineFunction from "../defineFunction"; import ParseNode from "../ParseNode"; @@ -69,17 +70,11 @@ defineFunction({ }, handler: ({parser}, args) => { const body = args[0]; - // amsbsy.sty's \boldsymbol inherits the argument's bin|rel|ord status - // (similar to \stackrel in functions/mclass.js) - let mclass = "mord"; - const atomType = (body.type === "ordgroup" && body.value.length ? - body.value[0].type : body.type); - if (/^(bin|rel)$/.test(atomType)) { - mclass = "m" + atomType; - } + // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the + // argument's bin|rel|ord status return new ParseNode("mclass", { type: "mclass", - mclass, + mclass: binrelClass(body), value: [ new ParseNode("font", { type: "font", diff --git a/src/functions/mclass.js b/src/functions/mclass.js index 82e4030d..7d9b06f2 100644 --- a/src/functions/mclass.js +++ b/src/functions/mclass.js @@ -3,6 +3,7 @@ import defineFunction, {ordargument} from "../defineFunction"; import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; import ParseNode from "../ParseNode"; +import type {AnyParseNode} from "../ParseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; @@ -41,6 +42,37 @@ defineFunction({ mathmlBuilder, }); +export const binrelClass = (arg: AnyParseNode) => { + // \binrel@ spacing varies with (bin|rel|ord) of the atom in the argument. + // (by rendering separately and with {}s before and after, and measuring + // the change in spacing). We'll do roughly the same by detecting the + // atom type directly. + const atomType = (arg.type === "ordgroup" && + arg.value.length ? arg.value[0].type : arg.type); + if (/^(bin|rel)$/.test(atomType)) { + return "m" + atomType; + } else { + return "mord"; + } +}; + +// \@binrel{x}{y} renders like y but as mbin/mrel/mord if x is mbin/mrel/mord. +// This is equivalent to \binrel@{x}\binrel@@{y} in AMSTeX. +defineFunction({ + type: "mclass", + names: ["\\@binrel"], + props: { + numArgs: 2, + }, + handler({parser}, args) { + return new ParseNode("mclass", { + type: "mclass", + mclass: binrelClass(args[0]), + value: [args[1]], + }, parser.mode); + }, +}); + // Build a relation or stacked op by placing one symbol on top of another defineFunction({ type: "mclass", @@ -52,21 +84,12 @@ defineFunction({ const baseArg = args[1]; const shiftedArg = args[0]; - let mclass = "mrel"; // default. May change below. + let mclass; if (funcName !== "\\stackrel") { - // LaTeX applies \binrel spacing to \overset and \underset. \binrel - // spacing varies with (bin|rel|ord) of the atom in the argument. - // We'll do the same. - const atomType = (baseArg.type === "ordgroup" && - baseArg.value.length ? baseArg.value[0].type : baseArg.type); - if (/^(bin|rel)$/.test(atomType)) { - mclass = "m" + atomType; - } else { - // This may capture some instances in which the baseArg is more - // than just a single symbol. Say a \overset inside an \overset. - // TODO: A more comprehensive way to determine the baseArg type. - mclass = "mord"; - } + // LaTeX applies \binrel spacing to \overset and \underset. + mclass = binrelClass(baseArg); + } else { + mclass = "mrel"; // for \stackrel } const baseOp = new ParseNode("op", { diff --git a/test/katex-spec.js b/test/katex-spec.js index 46b17502..e213c1ec 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -2999,6 +2999,23 @@ describe("\\tag support", function() { }); }); +describe("\\@binrel automatic bin/rel/ord", () => { + it("should generate proper class", () => { + expect("L\\@binrel+xR").toParseLike("L\\mathbin xR"); + expect("L\\@binrel=xR").toParseLike("L\\mathrel xR"); + expect("L\\@binrel xxR").toParseLike("L\\mathord xR"); + expect("L\\@binrel{+}{x}R").toParseLike("L\\mathbin{{x}}R"); + expect("L\\@binrel{=}{x}R").toParseLike("L\\mathrel{{x}}R"); + expect("L\\@binrel{x}{x}R").toParseLike("L\\mathord{{x}}R"); + }); + + it("should base on just first character in group", () => { + expect("L\\@binrel{+x}xR").toParseLike("L\\mathbin xR"); + expect("L\\@binrel{=x}xR").toParseLike("L\\mathrel xR"); + expect("L\\@binrel{xx}xR").toParseLike("L\\mathord xR"); + }); +}); + describe("A parser taking String objects", function() { it("should not fail on an empty String object", function() { expect(new String("")).toParse();