Fix ligatures vs. \tt (#1379)

* Fix ligatures not always triggering

Example: \text{\rm --} would not cause ligature but should

* Remove ligatures in \texttt mode

* Add screenshot tests

* Fix MathML ligatures

* Fix type

* Handle \text... fonts in MathML building

* Remove leftover console deubgging
This commit is contained in:
Erik Demaine
2018-06-04 20:29:25 -04:00
committed by Kevin Barabash
parent 4f9851fb0c
commit 1e645198f7
12 changed files with 232 additions and 29 deletions

View File

@@ -198,6 +198,9 @@ export default class Parser {
}
body.push(atom);
}
if (this.mode === "text") {
this.formLigatures(body);
}
return this.handleInfixNodes(body);
}
@@ -870,9 +873,6 @@ export default class Parser {
this.gullet.endGroup();
// Make sure we get a close brace
this.expect(optional ? "]" : "}");
if (mode === "text") {
this.formLigatures(expression);
}
return newArgument(
new ParseNode(
"ordgroup", expression, this.mode, firstToken, lastToken),

View File

@@ -7,7 +7,7 @@
import domTree from "./domTree";
import fontMetrics from "./fontMetrics";
import symbols from "./symbols";
import symbols, {ligatures} from "./symbols";
import utils from "./utils";
import {wideCharacterFont} from "./wide-character";
import {calculateSize} from "./units";
@@ -226,7 +226,7 @@ const makeOrd = function<NODETYPE: "spacing" | "mathord" | "textord">(
group: ParseNode<NODETYPE>,
options: Options,
type: "mathord" | "textord",
): domTree.symbolNode {
): domTree.symbolNode | domTree.documentFragment {
const mode = group.mode;
const value = group.value;
@@ -260,9 +260,19 @@ const makeOrd = function<NODETYPE: "spacing" | "mathord" | "textord">(
options.fontShape);
fontClasses = [fontOrFamily, options.fontWeight, options.fontShape];
}
if (lookupSymbol(value, fontName, mode).metrics) {
return makeSymbol(value, fontName, mode, options,
classes.concat(fontClasses));
} else if (ligatures.hasOwnProperty(value) &&
fontName.substr(0, 10) === "Typewriter") {
// Deconstruct ligatures in monospace fonts (\texttt, \tt).
const parts = [];
for (let i = 0; i < value.length; i++) {
parts.push(makeSymbol(value[i], fontName, mode, options,
classes.concat(fontClasses)));
}
return makeFragment(parts);
} else {
return mathDefault(value, mode, options, classes, type);
}

View File

@@ -8,7 +8,7 @@ import buildCommon from "./buildCommon";
import fontMetrics from "./fontMetrics";
import mathMLTree from "./mathMLTree";
import ParseError from "./ParseError";
import symbols from "./symbols";
import symbols, {ligatures} from "./symbols";
import utils from "./utils";
import {_mathmlGroupBuilders as groupBuilders} from "./defineFunction";
@@ -16,11 +16,13 @@ import {_mathmlGroupBuilders as groupBuilders} from "./defineFunction";
* Takes a symbol and converts it into a MathML text node after performing
* optional replacement from symbols.js.
*/
export const makeText = function(text, mode) {
if (symbols[mode][text] && symbols[mode][text].replace) {
if (text.charCodeAt(0) !== 0xD835) {
text = symbols[mode][text].replace;
}
export const makeText = function(text, mode, options) {
if (symbols[mode][text] && symbols[mode][text].replace &&
text.charCodeAt(0) !== 0xD835 &&
!(ligatures.hasOwnProperty(text) && options &&
((options.fontFamily && options.fontFamily.substr(4, 2) === "tt") ||
(options.font && options.font.substr(4, 2) === "tt")))) {
text = symbols[mode][text].replace;
}
return new mathMLTree.TextNode(text);
@@ -42,6 +44,31 @@ export const makeRow = function(body) {
* Returns the math variant as a string or null if none is required.
*/
export const getVariant = function(group, options) {
// Handle \text... font specifiers as best we can.
// MathML has a limited list of allowable mathvariant specifiers; see
// https://www.w3.org/TR/MathML3/chapter3.html#presm.commatt
if (options.fontFamily === "texttt") {
return "monospace";
} else if (options.fontFamily === "textsf") {
if (options.fontShape === "textit" &&
options.fontWeight === "textbf") {
return "sans-serif-bold-italic";
} else if (options.fontShape === "textit") {
return "sans-serif-italic";
} else if (options.fontWeight === "textbf") {
return "bold-sans-serif";
} else {
return "sans-serif";
}
} else if (options.fontShape === "textit" &&
options.fontWeight === "textbf") {
return "bold-italic";
} else if (options.fontShape === "textit") {
return "italic";
} else if (options.fontWeight === "textbf") {
return "bold";
}
const font = options.font;
if (!font) {
return null;
@@ -82,7 +109,9 @@ export const buildExpression = function(expression, options) {
for (let i = 0; i < expression.length; i++) {
const group = buildGroup(expression[i], options);
// Concatenate adjacent <mtext>s
if (group.type === 'mtext' && lastGroup && lastGroup.type === 'mtext') {
if (group.type === 'mtext' && lastGroup && lastGroup.type === 'mtext'
&& group.getAttribute('mathvariant') ===
lastGroup.getAttribute('mathvariant')) {
lastGroup.children.push(...group.children);
// Concatenate adjacent <mn>s
} else if (group.type === 'mn' &&

View File

@@ -22,7 +22,7 @@ defineFunctionBuilders({
mathmlBuilder(group, options) {
const node = new mathMLTree.MathNode(
"mi",
[mml.makeText(group.value, group.mode)]);
[mml.makeText(group.value, group.mode, options)]);
const variant = mml.getVariant(group, options) || "italic";
if (variant !== defaultVariant[node.type]) {
@@ -38,8 +38,7 @@ defineFunctionBuilders({
return buildCommon.makeOrd(group, options, "textord");
},
mathmlBuilder(group, options) {
const text = mml.makeText(group.value, group.mode);
const text = mml.makeText(group.value, group.mode, options);
const variant = mml.getVariant(group, options) || "normal";
let node;

View File

@@ -20,6 +20,20 @@ const textFontShapes = {
"\\textit": "textit",
};
const optionsWithFont = (group, options) => {
const font = group.value.font;
// Checks if the argument is a font family or a font style.
if (!font) {
return options;
} else if (textFontFamilies[font]) {
return options.withTextFontFamily(textFontFamilies[font]);
} else if (textFontWeights[font]) {
return options.withTextFontWeight(textFontWeights[font]);
} else {
return options.withTextFontShape(textFontShapes[font]);
}
};
defineFunction({
type: "text",
names: [
@@ -46,23 +60,13 @@ defineFunction({
}, parser.mode);
},
htmlBuilder(group, options) {
const font = group.value.font;
// Checks if the argument is a font family or a font style.
let newOptions;
if (!font) {
newOptions = options;
} else if (textFontFamilies[font]) {
newOptions = options.withTextFontFamily(textFontFamilies[font]);
} else if (textFontWeights[font]) {
newOptions = options.withTextFontWeight(textFontWeights[font]);
} else {
newOptions = options.withTextFontShape(textFontShapes[font]);
}
const newOptions = optionsWithFont(group, options);
const inner = html.buildExpression(group.value.body, newOptions, true);
buildCommon.tryCombineChars(inner);
return buildCommon.makeSpan(["mord", "text"], inner, newOptions);
},
mathmlBuilder(group, options) {
return mml.buildExpressionRow(group.value.body, options);
const newOptions = optionsWithFont(group, options);
return mml.buildExpressionRow(group.value.body, newOptions);
},
});

View File

@@ -50,6 +50,13 @@ export class MathNode {
this.attributes[name] = value;
}
/**
* Gets an attribute on a MathML node.
*/
getAttribute(name: string): string {
return this.attributes[name];
}
/**
* Converts the math node into a MathML-namespaced DOM element.
*/

View File

@@ -700,6 +700,14 @@ defineSymbol(text, main, accent, "\u00a8", '\\"'); // diaresis
defineSymbol(text, main, accent, "\u02dd", "\\H"); // double acute
defineSymbol(text, main, accent, "\u25ef", "\\textcircled"); // \bigcirc glyph
// These ligatures are detected and created in Parser.js's `formLigatures`.
export const ligatures = {
"--": true,
"---": true,
"``": true,
"''": true,
};
defineSymbol(text, main, textord, "\u2013", "--");
defineSymbol(text, main, textord, "\u2013", "\\textendash");
defineSymbol(text, main, textord, "\u2014", "---");

View File

@@ -1,5 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`A MathML builder \\text fonts become mathvariant 1`] = `
<math>
<semantics>
<mrow>
<mtext>
roman
</mtext>
<mrow>
<mtext mathvariant="italic">
italic
</mtext>
<mrow>
<mtext mathvariant="bold-italic">
bold
</mtext>
<mtext>
</mtext>
<mtext mathvariant="bold-italic">
italic
</mtext>
</mrow>
</mrow>
<mtext mathvariant="bold">
bold
</mtext>
<mrow>
<mtext mathvariant="sans-serif">
ss
</mtext>
<mrow>
<mtext mathvariant="sans-serif-italic">
italic
</mtext>
<mrow>
<mtext mathvariant="sans-serif-bold-italic">
bold
</mtext>
<mtext>
</mtext>
<mtext mathvariant="sans-serif-bold-italic">
italic
</mtext>
</mrow>
</mrow>
<mtext mathvariant="bold-sans-serif">
bold
</mtext>
</mrow>
<mrow>
<mtext mathvariant="monospace">
tt
</mtext>
<mrow>
<mtext mathvariant="monospace">
italic
</mtext>
<mrow>
<mtext mathvariant="monospace">
bold
</mtext>
<mtext>
</mtext>
<mtext mathvariant="monospace">
italic
</mtext>
</mrow>
</mrow>
<mtext mathvariant="monospace">
bold
</mtext>
</mrow>
</mrow>
<annotation encoding="application/x-tex">
\\text{roman\\textit{italic\\textbf{bold italic}}\\textbf{bold}\\textsf{ss\\textit{italic\\textbf{bold italic}}\\textbf{bold}}\\texttt{tt\\textit{italic\\textbf{bold italic}}\\textbf{bold}}}
</annotation>
</semantics>
</math>
`;
exports[`A MathML builder accents turn into <mover accent="true"> in MathML 1`] = `
<math>
@@ -57,6 +138,52 @@ exports[`A MathML builder accents turn into <mover accent="true"> in MathML 1`]
`;
exports[`A MathML builder ligatures render properly 1`] = `
<math>
<semantics>
<mrow>
<mtext>
Hi—-”’
</mtext>
<mo>
</mo>
<mo>
</mo>
<mtext mathvariant="monospace">
\`\`Hi----&#x27;&#x27;
</mtext>
<mrow>
<mtext>
\`\`
</mtext>
<mtext mathvariant="monospace">
Hi
</mtext>
<mtext>
---
</mtext>
<mtext mathvariant="monospace">
-
</mtext>
<mtext>
&#x27;&#x27;
</mtext>
<mtext mathvariant="monospace">
</mtext>
</mrow>
</mrow>
<annotation encoding="application/x-tex">
\\text{\`\`\`Hi----&#x27;&#x27;&#x27;}--\\texttt{\`\`\`Hi----&#x27;&#x27;&#x27;}\\text{\\tt \`\`\`Hi----&#x27;&#x27;&#x27;}
</annotation>
</semantics>
</math>
`;
exports[`A MathML builder normal spaces render normally 1`] = `
<math>

View File

@@ -118,4 +118,18 @@ describe("A MathML builder", function() {
"\\mkern1mu\\mkern3mu\\mkern4mu\\mkern5mu" +
"\\mkern-1mu\\mkern-3mu\\mkern-4mu\\mkern-5mu")).toMatchSnapshot();
});
it('ligatures render properly', () => {
expect(getMathML("\\text{```Hi----'''}--" +
"\\texttt{```Hi----'''}" +
"\\text{\\tt ```Hi----'''}")).toMatchSnapshot();
});
it('\\text fonts become mathvariant', () => {
expect(getMathML("\\text{" +
"roman\\textit{italic\\textbf{bold italic}}\\textbf{bold}" +
"\\textsf{ss\\textit{italic\\textbf{bold italic}}\\textbf{bold}}" +
"\\texttt{tt\\textit{italic\\textbf{bold italic}}\\textbf{bold}}}"))
.toMatchSnapshot();
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -76,7 +76,12 @@ Colors:
ColorImplicit: bl{ack\color{red}red\textcolor{green}{green}red\color{blue}blue}black
ColorSpacing: \textcolor{red}{\displaystyle \int x} + 1
Colorbox: a \colorbox{teal} B \fcolorbox{blue}{red}{C} e+\colorbox{teal}x
DashesAndQuotes: \text{``a'' b---c -- d----`e'-{-}-f}--``x''
DashesAndQuotes: |
\begin{array}{l}
\text{``a'' b---c -- d----`e'-{-}-f} -- \\
\text{\it ``a'' b---c -- d----`e'-{-}-f} ``x'' \\
\text{\tt ``a''---} \texttt{``a''---} \mathtt{--} \\
\end{array}
DeepFontSizing:
tex: |
a^{\big| x^{\big(}}_{\Big\uparrow} +