diff --git a/src/buildHTML.js b/src/buildHTML.js index 2822eec8..eeb27e1b 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -68,7 +68,7 @@ export const buildExpression = function( for (let i = 0; i < expression.length; i++) { const output = buildGroup(expression[i], options); if (output instanceof DocumentFragment) { - const children: HtmlDomNode[] = output.children; + const children: $ReadOnlyArray = output.children; groups.push(...children); } else { groups.push(output); @@ -154,6 +154,7 @@ const traverseNonSpaceNodes = function( const node = nodes[i]; const partialGroup = checkPartialGroup(node); if (partialGroup) { // Recursive DFS + // $FlowFixMe: make nodes a $ReadOnlyArray by returning a new array traverseNonSpaceNodes(partialGroup.children, callback, prev); continue; } diff --git a/src/buildMathML.js b/src/buildMathML.js index d4e2cb96..70bc0dd4 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -44,7 +44,7 @@ export const makeText = function( * Wrap the given array of nodes in an node if needed, i.e., * unless the array has length 1. Always returns a single node. */ -export const makeRow = function(body: MathDomNode[]): MathDomNode { +export const makeRow = function(body: $ReadOnlyArray): MathDomNode { if (body.length === 1) { return body[0]; } else { @@ -134,7 +134,19 @@ export const getVariant = function( export const buildExpression = function( expression: AnyParseNode[], options: Options, -): MathDomNode[] { + isOrdgroup?: boolean, +): MathNode[] { + if (expression.length === 1) { + const group = buildGroup(expression[0], options); + if (isOrdgroup && group instanceof MathNode && group.type === "mo") { + // When TeX writers want to suppress spacing on an operator, + // they often put the operator by itself inside braces. + group.setAttribute("lspace", "0em"); + group.setAttribute("rspace", "0em"); + } + return [group]; + } + const groups = []; let lastGroup; for (let i = 0; i < expression.length; i++) { @@ -186,8 +198,9 @@ export const buildExpression = function( export const buildExpressionRow = function( expression: AnyParseNode[], options: Options, + isOrdgroup?: boolean, ): MathDomNode { - return makeRow(buildExpression(expression, options)); + return makeRow(buildExpression(expression, options, isOrdgroup)); }; /** @@ -197,7 +210,7 @@ export const buildExpressionRow = function( export const buildGroup = function( group: ?AnyParseNode, options: Options, -): MathDomNode { +): MathNode { if (!group) { return new mathMLTree.MathNode("mrow"); } diff --git a/src/functions/font.js b/src/functions/font.js index f6f0743a..005182d2 100644 --- a/src/functions/font.js +++ b/src/functions/font.js @@ -3,6 +3,7 @@ import {binrelClass} from "./mclass"; import defineFunction from "../defineFunction"; +import utils from "../utils"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; @@ -71,6 +72,7 @@ defineFunction({ }, handler: ({parser}, args) => { const body = args[0]; + const isCharacterBox = utils.isCharacterBox(body); // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the // argument's bin|rel|ord status return { @@ -85,6 +87,7 @@ defineFunction({ body, }, ], + isCharacterBox: isCharacterBox, }; }, }); diff --git a/src/functions/mclass.js b/src/functions/mclass.js index 6f63c483..af18d7df 100644 --- a/src/functions/mclass.js +++ b/src/functions/mclass.js @@ -2,6 +2,7 @@ import defineFunction, {ordargument} from "../defineFunction"; import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; +import utils from "../utils"; import type {AnyParseNode} from "../parseNode"; import * as html from "../buildHTML"; @@ -17,8 +18,42 @@ function htmlBuilder(group: ParseNode<"mclass">, options) { } function mathmlBuilder(group: ParseNode<"mclass">, options) { + let node: mathMLTree.MathNode; const inner = mml.buildExpression(group.body, options); - return mathMLTree.newDocumentFragment(inner); + + if (group.mclass === "minner") { + return mathMLTree.newDocumentFragment(inner); + } else if (group.mclass === "mord") { + if (group.isCharacterBox) { + node = inner[0]; + node.type = "mi"; + } else { + node = new mathMLTree.MathNode("mi", inner); + } + } else { + if (group.isCharacterBox) { + node = inner[0]; + node.type = "mo"; + } else { + node = new mathMLTree.MathNode("mo", inner); + } + + // Set spacing based on what is the most likely adjacent atom type. + // See TeXbook p170. + if (group.mclass === "mbin") { + node.attributes.lspace = "0.22em"; // medium space + node.attributes.rspace = "0.22em"; + } else if (group.mclass === "mpunct") { + node.attributes.lspace = "0em"; + node.attributes.rspace = "0.17em"; // thinspace + } else if (group.mclass === "mopen" || group.mclass === "mclose") { + node.attributes.lspace = "0em"; + node.attributes.rspace = "0em"; + } + // MathML default space is 5/18 em, so needs no action. + // Ref: https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo + } + return node; } // Math class commands except \mathop @@ -38,6 +73,7 @@ defineFunction({ mode: parser.mode, mclass: "m" + funcName.substr(5), body: ordargument(body), + isCharacterBox: utils.isCharacterBox(body), }; }, htmlBuilder, @@ -71,6 +107,7 @@ defineFunction({ mode: parser.mode, mclass: binrelClass(args[0]), body: [args[1]], + isCharacterBox: utils.isCharacterBox(args[1]), }; }, }); @@ -118,6 +155,7 @@ defineFunction({ mode: parser.mode, mclass, body: [supsub], + isCharacterBox: utils.isCharacterBox(supsub), }; }, htmlBuilder, diff --git a/src/functions/ordgroup.js b/src/functions/ordgroup.js index 72b25b25..f3555781 100644 --- a/src/functions/ordgroup.js +++ b/src/functions/ordgroup.js @@ -16,7 +16,7 @@ defineFunctionBuilders({ ["mord"], html.buildExpression(group.body, options, true), options); }, mathmlBuilder(group, options) { - return mml.buildExpressionRow(group.body, options); + return mml.buildExpressionRow(group.body, options, true); }, }); diff --git a/src/mathMLTree.js b/src/mathMLTree.js index 610fea61..ba1dbde5 100644 --- a/src/mathMLTree.js +++ b/src/mathMLTree.js @@ -32,7 +32,9 @@ export interface MathDomNode extends VirtualNode { } export type documentFragment = DocumentFragment; -export function newDocumentFragment(children: MathDomNode[]): documentFragment { +export function newDocumentFragment( + children: $ReadOnlyArray +): documentFragment { return new DocumentFragment(children); } @@ -44,9 +46,9 @@ export function newDocumentFragment(children: MathDomNode[]): documentFragment { export class MathNode implements MathDomNode { type: MathNodeType; attributes: {[string]: string}; - children: MathDomNode[]; + children: $ReadOnlyArray; - constructor(type: MathNodeType, children?: MathDomNode[]) { + constructor(type: MathNodeType, children?: $ReadOnlyArray) { this.type = type; this.attributes = {}; this.children = children || []; diff --git a/src/parseNode.js b/src/parseNode.js index 5b2f0c4b..1fe2a6f4 100644 --- a/src/parseNode.js +++ b/src/parseNode.js @@ -348,6 +348,7 @@ type ParseNodeTypes = { loc?: ?SourceLocation, mclass: string, body: AnyParseNode[], + isCharacterBox: boolean, |}, "operatorname": {| type: "operatorname", diff --git a/src/tree.js b/src/tree.js index 0d982848..940a44d0 100644 --- a/src/tree.js +++ b/src/tree.js @@ -20,7 +20,7 @@ export interface VirtualNode { */ export class DocumentFragment implements HtmlDomNode, MathDomNode { - children: ChildType[]; + children: $ReadOnlyArray; // HtmlDomNode classes: string[]; height: number; @@ -28,7 +28,7 @@ export class DocumentFragment maxFontSize: number; style: CssStyle; // Never used; needed for satisfying interface. - constructor(children: ChildType[]) { + constructor(children: $ReadOnlyArray) { this.children = children; this.classes = []; this.height = 0; diff --git a/test/__snapshots__/mathml-spec.js.snap b/test/__snapshots__/mathml-spec.js.snap index c2975a3f..283f907d 100644 --- a/test/__snapshots__/mathml-spec.js.snap +++ b/test/__snapshots__/mathml-spec.js.snap @@ -8,15 +8,15 @@ exports[`A MathML builder \\html@mathml makes clean symbols 1`] = ` © - + ≠ - - + + ∉ - - + + ≘ - + KaTeX @@ -306,7 +306,10 @@ exports[`A MathML builder should make prime operators into nodes 1`] = ` f - + @@ -394,32 +397,34 @@ exports[`A MathML builder should render boldsymbol with the correct mathvariants - - - A - - - x - - - 2 - - - k - - - ω - - - Ω - - - ı - - - + - - + + + + A + + + x + + + 2 + + + k + + + ω + + + Ω + + + ı + + + + + + + \\boldsymbol{Ax2k\\omega\\Omega\\imath+}