diff --git a/src/Parser.js b/src/Parser.js index 0ac2a93f..31fa2859 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -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), diff --git a/src/buildCommon.js b/src/buildCommon.js index 28d88df5..88f13402 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -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( group: ParseNode, 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( 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); } diff --git a/src/buildMathML.js b/src/buildMathML.js index 79e22a1f..20c5891e 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -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 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 s } else if (group.type === 'mn' && diff --git a/src/functions/symbolsOrd.js b/src/functions/symbolsOrd.js index ab29b81f..a818e4bf 100644 --- a/src/functions/symbolsOrd.js +++ b/src/functions/symbolsOrd.js @@ -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; diff --git a/src/functions/text.js b/src/functions/text.js index a210bbc7..5f257062 100644 --- a/src/functions/text.js +++ b/src/functions/text.js @@ -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); }, }); diff --git a/src/mathMLTree.js b/src/mathMLTree.js index ce301a8e..b67bee7f 100644 --- a/src/mathMLTree.js +++ b/src/mathMLTree.js @@ -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. */ diff --git a/src/symbols.js b/src/symbols.js index c4b5c6db..3ed95bdb 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -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", "---"); diff --git a/test/__snapshots__/mathml-spec.js.snap b/test/__snapshots__/mathml-spec.js.snap index 4cbaaca0..f2c60d4c 100644 --- a/test/__snapshots__/mathml-spec.js.snap +++ b/test/__snapshots__/mathml-spec.js.snap @@ -1,5 +1,86 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`A MathML builder \\text fonts become mathvariant 1`] = ` + + + + + + roman + + + + italic + + + + bold + + + + + italic + + + + + bold + + + + ss + + + + italic + + + + bold + + + + + italic + + + + + bold + + + + + tt + + + + italic + + + + bold + + + + + italic + + + + + bold + + + + + \\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}}} + + + + +`; + exports[`A MathML builder accents turn into in MathML 1`] = ` @@ -57,6 +138,52 @@ exports[`A MathML builder accents turn into in MathML 1`] `; +exports[`A MathML builder ligatures render properly 1`] = ` + + + + + + “‘Hi—-”’ + + + − + + + − + + + \`\`‘Hi----''’ + + + + \`\` + + + ‘Hi + + + --- + + + - + + + '' + + + ’ + + + + + \\text{\`\`\`Hi----'''}--\\texttt{\`\`\`Hi----'''}\\text{\\tt \`\`\`Hi----'''} + + + + +`; + exports[`A MathML builder normal spaces render normally 1`] = ` diff --git a/test/mathml-spec.js b/test/mathml-spec.js index 3af5faa1..a1f55196 100644 --- a/test/mathml-spec.js +++ b/test/mathml-spec.js @@ -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(); + }); }); diff --git a/test/screenshotter/images/DashesAndQuotes-chrome.png b/test/screenshotter/images/DashesAndQuotes-chrome.png index ea21f698..f73664f5 100644 Binary files a/test/screenshotter/images/DashesAndQuotes-chrome.png and b/test/screenshotter/images/DashesAndQuotes-chrome.png differ diff --git a/test/screenshotter/images/DashesAndQuotes-firefox.png b/test/screenshotter/images/DashesAndQuotes-firefox.png index ca308aee..1dbc83d4 100644 Binary files a/test/screenshotter/images/DashesAndQuotes-firefox.png and b/test/screenshotter/images/DashesAndQuotes-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 367db384..0a8cf6d1 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -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} +