Improve MathML for classes (#1929)

* Improve MathML for classes

* Fix lint errors

* Fix another lint error

* Simplify MathML resulting from single character box

* Fix lint errors

* make some of the arrays in our html/mathml helper functions readonly
This commit is contained in:
Ron Kok
2019-07-04 22:14:05 -07:00
committed by Kevin Barabash
parent 19d9d83ad3
commit 2b83935064
9 changed files with 108 additions and 45 deletions

View File

@@ -68,7 +68,7 @@ export const buildExpression = function(
for (let i = 0; i < expression.length; i++) { for (let i = 0; i < expression.length; i++) {
const output = buildGroup(expression[i], options); const output = buildGroup(expression[i], options);
if (output instanceof DocumentFragment) { if (output instanceof DocumentFragment) {
const children: HtmlDomNode[] = output.children; const children: $ReadOnlyArray<HtmlDomNode> = output.children;
groups.push(...children); groups.push(...children);
} else { } else {
groups.push(output); groups.push(output);
@@ -154,6 +154,7 @@ const traverseNonSpaceNodes = function(
const node = nodes[i]; const node = nodes[i];
const partialGroup = checkPartialGroup(node); const partialGroup = checkPartialGroup(node);
if (partialGroup) { // Recursive DFS if (partialGroup) { // Recursive DFS
// $FlowFixMe: make nodes a $ReadOnlyArray by returning a new array
traverseNonSpaceNodes(partialGroup.children, callback, prev); traverseNonSpaceNodes(partialGroup.children, callback, prev);
continue; continue;
} }

View File

@@ -44,7 +44,7 @@ export const makeText = function(
* Wrap the given array of nodes in an <mrow> node if needed, i.e., * Wrap the given array of nodes in an <mrow> node if needed, i.e.,
* unless the array has length 1. Always returns a single node. * 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>): MathDomNode {
if (body.length === 1) { if (body.length === 1) {
return body[0]; return body[0];
} else { } else {
@@ -134,7 +134,19 @@ export const getVariant = function(
export const buildExpression = function( export const buildExpression = function(
expression: AnyParseNode[], expression: AnyParseNode[],
options: Options, 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 = []; const groups = [];
let lastGroup; let lastGroup;
for (let i = 0; i < expression.length; i++) { for (let i = 0; i < expression.length; i++) {
@@ -186,8 +198,9 @@ export const buildExpression = function(
export const buildExpressionRow = function( export const buildExpressionRow = function(
expression: AnyParseNode[], expression: AnyParseNode[],
options: Options, options: Options,
isOrdgroup?: boolean,
): MathDomNode { ): MathDomNode {
return makeRow(buildExpression(expression, options)); return makeRow(buildExpression(expression, options, isOrdgroup));
}; };
/** /**
@@ -197,7 +210,7 @@ export const buildExpressionRow = function(
export const buildGroup = function( export const buildGroup = function(
group: ?AnyParseNode, group: ?AnyParseNode,
options: Options, options: Options,
): MathDomNode { ): MathNode {
if (!group) { if (!group) {
return new mathMLTree.MathNode("mrow"); return new mathMLTree.MathNode("mrow");
} }

View File

@@ -3,6 +3,7 @@
import {binrelClass} from "./mclass"; import {binrelClass} from "./mclass";
import defineFunction from "../defineFunction"; import defineFunction from "../defineFunction";
import utils from "../utils";
import * as html from "../buildHTML"; import * as html from "../buildHTML";
import * as mml from "../buildMathML"; import * as mml from "../buildMathML";
@@ -71,6 +72,7 @@ defineFunction({
}, },
handler: ({parser}, args) => { handler: ({parser}, args) => {
const body = args[0]; const body = args[0];
const isCharacterBox = utils.isCharacterBox(body);
// amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the
// argument's bin|rel|ord status // argument's bin|rel|ord status
return { return {
@@ -85,6 +87,7 @@ defineFunction({
body, body,
}, },
], ],
isCharacterBox: isCharacterBox,
}; };
}, },
}); });

View File

@@ -2,6 +2,7 @@
import defineFunction, {ordargument} from "../defineFunction"; import defineFunction, {ordargument} from "../defineFunction";
import buildCommon from "../buildCommon"; import buildCommon from "../buildCommon";
import mathMLTree from "../mathMLTree"; import mathMLTree from "../mathMLTree";
import utils from "../utils";
import type {AnyParseNode} from "../parseNode"; import type {AnyParseNode} from "../parseNode";
import * as html from "../buildHTML"; import * as html from "../buildHTML";
@@ -17,8 +18,42 @@ function htmlBuilder(group: ParseNode<"mclass">, options) {
} }
function mathmlBuilder(group: ParseNode<"mclass">, options) { function mathmlBuilder(group: ParseNode<"mclass">, options) {
let node: mathMLTree.MathNode;
const inner = mml.buildExpression(group.body, options); const inner = mml.buildExpression(group.body, options);
if (group.mclass === "minner") {
return mathMLTree.newDocumentFragment(inner); 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 <mo> default space is 5/18 em, so <mrel> needs no action.
// Ref: https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo
}
return node;
} }
// Math class commands except \mathop // Math class commands except \mathop
@@ -38,6 +73,7 @@ defineFunction({
mode: parser.mode, mode: parser.mode,
mclass: "m" + funcName.substr(5), mclass: "m" + funcName.substr(5),
body: ordargument(body), body: ordargument(body),
isCharacterBox: utils.isCharacterBox(body),
}; };
}, },
htmlBuilder, htmlBuilder,
@@ -71,6 +107,7 @@ defineFunction({
mode: parser.mode, mode: parser.mode,
mclass: binrelClass(args[0]), mclass: binrelClass(args[0]),
body: [args[1]], body: [args[1]],
isCharacterBox: utils.isCharacterBox(args[1]),
}; };
}, },
}); });
@@ -118,6 +155,7 @@ defineFunction({
mode: parser.mode, mode: parser.mode,
mclass, mclass,
body: [supsub], body: [supsub],
isCharacterBox: utils.isCharacterBox(supsub),
}; };
}, },
htmlBuilder, htmlBuilder,

View File

@@ -16,7 +16,7 @@ defineFunctionBuilders({
["mord"], html.buildExpression(group.body, options, true), options); ["mord"], html.buildExpression(group.body, options, true), options);
}, },
mathmlBuilder(group, options) { mathmlBuilder(group, options) {
return mml.buildExpressionRow(group.body, options); return mml.buildExpressionRow(group.body, options, true);
}, },
}); });

View File

@@ -32,7 +32,9 @@ export interface MathDomNode extends VirtualNode {
} }
export type documentFragment = DocumentFragment<MathDomNode>; export type documentFragment = DocumentFragment<MathDomNode>;
export function newDocumentFragment(children: MathDomNode[]): documentFragment { export function newDocumentFragment(
children: $ReadOnlyArray<MathDomNode>
): documentFragment {
return new DocumentFragment(children); return new DocumentFragment(children);
} }
@@ -44,9 +46,9 @@ export function newDocumentFragment(children: MathDomNode[]): documentFragment {
export class MathNode implements MathDomNode { export class MathNode implements MathDomNode {
type: MathNodeType; type: MathNodeType;
attributes: {[string]: string}; attributes: {[string]: string};
children: MathDomNode[]; children: $ReadOnlyArray<MathDomNode>;
constructor(type: MathNodeType, children?: MathDomNode[]) { constructor(type: MathNodeType, children?: $ReadOnlyArray<MathDomNode>) {
this.type = type; this.type = type;
this.attributes = {}; this.attributes = {};
this.children = children || []; this.children = children || [];

View File

@@ -348,6 +348,7 @@ type ParseNodeTypes = {
loc?: ?SourceLocation, loc?: ?SourceLocation,
mclass: string, mclass: string,
body: AnyParseNode[], body: AnyParseNode[],
isCharacterBox: boolean,
|}, |},
"operatorname": {| "operatorname": {|
type: "operatorname", type: "operatorname",

View File

@@ -20,7 +20,7 @@ export interface VirtualNode {
*/ */
export class DocumentFragment<ChildType: VirtualNode> export class DocumentFragment<ChildType: VirtualNode>
implements HtmlDomNode, MathDomNode { implements HtmlDomNode, MathDomNode {
children: ChildType[]; children: $ReadOnlyArray<ChildType>;
// HtmlDomNode // HtmlDomNode
classes: string[]; classes: string[];
height: number; height: number;
@@ -28,7 +28,7 @@ export class DocumentFragment<ChildType: VirtualNode>
maxFontSize: number; maxFontSize: number;
style: CssStyle; // Never used; needed for satisfying interface. style: CssStyle; // Never used; needed for satisfying interface.
constructor(children: ChildType[]) { constructor(children: $ReadOnlyArray<ChildType>) {
this.children = children; this.children = children;
this.classes = []; this.classes = [];
this.height = 0; this.height = 0;

View File

@@ -8,15 +8,15 @@ exports[`A MathML builder \\html@mathml makes clean symbols 1`] = `
<mtext> <mtext>
© ©
</mtext> </mtext>
<mi mathvariant="normal"> <mo mathvariant="normal">
</mi> </mo>
<mi mathvariant="normal"> <mo mathvariant="normal">
</mi> </mo>
<mi mathvariant="normal"> <mo mathvariant="normal">
</mi> </mo>
<mtext> <mtext>
KaTeX KaTeX
</mtext> </mtext>
@@ -306,7 +306,10 @@ exports[`A MathML builder should make prime operators into <mo> nodes 1`] = `
<mi> <mi>
f f
</mi> </mi>
<mo mathvariant="normal"> <mo mathvariant="normal"
lspace="0em"
rspace="0em"
>
</mo> </mo>
</msup> </msup>
@@ -394,6 +397,7 @@ exports[`A MathML builder should render boldsymbol with the correct mathvariants
<math xmlns="http://www.w3.org/1998/Math/MathML"> <math xmlns="http://www.w3.org/1998/Math/MathML">
<semantics> <semantics>
<mrow> <mrow>
<mi>
<mrow> <mrow>
<mi mathvariant="bold-italic"> <mi mathvariant="bold-italic">
A A
@@ -420,6 +424,7 @@ exports[`A MathML builder should render boldsymbol with the correct mathvariants
+ +
</mo> </mo>
</mrow> </mrow>
</mi>
</mrow> </mrow>
<annotation encoding="application/x-tex"> <annotation encoding="application/x-tex">
\\boldsymbol{Ax2k\\omega\\Omega\\imath+} \\boldsymbol{Ax2k\\omega\\Omega\\imath+}