Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Math in Non-Default Text Fix #1111

Merged
merged 15 commits into from Feb 19, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/Options.js
Expand Up @@ -42,9 +42,9 @@ export type OptionsData = {
size?: number;
textSize?: number;
phantom?: boolean;
// TODO(#1009): Keep consistent with fontFamily/fontWeight. Ensure this has a
// string value.
fontFamily?: string | void;
font?: string;
oldTextFont?: boolean;
fontFamily?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does font differ from fontFamily? Maybe add some comments and/or be more specific about the types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Font families are a group of fonts, not a specific font. I'll add a comment.

fontWeight?: string;
fontShape?: string;
sizeMultiplier?: number;
Expand All @@ -64,7 +64,9 @@ class Options {
size: number;
textSize: number;
phantom: boolean;
fontFamily: string | void;
font: string;
oldTextFont: boolean;
fontFamily: string;
fontWeight: string;
fontShape: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we'd have properties like:

mathFont: string;
textFontFamily: string;
textFontWeight: string;
textFontShape: string;
mode: "text" | "math";

and then the API, would look something like:

.withMathFont(font: string);
.withTextFontFamily(family: string);
.withTextFontWeight(weight: string);
.withTextFontShape(shape: string);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, how would the old text fonts be handled? I.e. \rm \it. They behave similar to math fonts in that they change all aspects of the font.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot about \rm \it, but I think we could modify the suggested API in the following way:

.withTextFontFamily(family: string, isOldCommand: boolean = false);
.withTextFontWeight(weight: string, isOldCommand: boolean = false);
.withTextFontShape(shape: string, isOldCommand: boolean = false);

If isOldCommand is true, we would reset the other text font properties back to their defaults.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think withTextFontFamily/Weight/Shape would make sense with old font commands. Old font commands effectively change all 3, not just one. Additionally you'd lose the state when you exit the old font mode (i.e. \textbf{{\it hello} world}). They behave much more similar to the math commands, which is why I opted to leave the naming conventions generic.

Really wish LaTeX fonts weren't so confusing :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withTextFontFamily would look something like:

withTextFontFamily(family: string, isOldCommand: boolean) {
   if (isOldCommand) {
       this.extend({
           fontFamily: fontFamily || this.fontFontFamily,
           fontWeight: "",
           fontStyle: "",
      });
   } else {
        this.extend({
            fontFamily: fontFamily || this.fontFamily,
        });
   }
};

As for the question of losing state, {\rm hello} creates a new options object when processing that group which shouldn't affect the options object for the \textbf group.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'll give this more thought either later today or tomorrow. Thanks for the quick review.

sizeMultiplier: number;
Expand All @@ -82,7 +84,9 @@ class Options {
this.size = data.size || Options.BASESIZE;
this.textSize = data.textSize || this.size;
this.phantom = !!data.phantom;
this.fontFamily = data.fontFamily;
this.font = data.font || "";
this.oldTextFont = data.oldTextFont || false;
this.fontFamily = data.fontFamily || "";
this.fontWeight = data.fontWeight || '';
this.fontShape = data.fontShape || '';
this.sizeMultiplier = sizeMultipliers[this.size - 1];
Expand All @@ -101,6 +105,8 @@ class Options {
textSize: this.textSize,
color: this.color,
phantom: this.phantom,
font: this.font,
oldTextFont: this.oldTextFont,
fontFamily: this.fontFamily,
fontWeight: this.fontWeight,
fontShape: this.fontShape,
Expand Down Expand Up @@ -192,12 +198,37 @@ class Options {
});
}

/**
* Create a new options object when switching from text to math mode.
*/
withMathMode(): Options {
return this.extend({
font: 'mathit',
});
}

/**
* Creates a new options object with the given math font.
* @type {[type]}
*/
withFont(font: string): Options {
return this.extend({
font,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we aren't resetting the values of fontFamily, fontWeight, etc. I assume that font takes precedence.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Also, we can't safely reset the values of fontFamily/weight/etc.

Consider the following: \textbf{$\mathsf{\textsf{a}$}. This should render as a bold, sans-serif "a", but if we reset, we would get a regular sans-serif "a"

});
}

withOldTextFont(): Options {
return this.extend({
oldTextFont: true,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also reset the fontShape and fontWeight?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I actually thought the same thing until I broke the tests.

it("should render \\textsf{\\textbf{$\\mathrm{\\textsf{A}}$}} with the correct font", function() {
        const markup = katex.renderToString("\\textsf{\\textbf{$\\mathrm{\\textsf{A}}$}}");
        expect(markup).toContain("<span class=\"mord textsf textbf\">A</span>");
});

Basically when going from text mode, to math mode, then back to text mode, it should retain the font weight/shape from the first time it was in text mode. Hope that makes sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it not also retain the fontFamily? What happens in this case?

\textsf{\textbf{$\textbf{A}$}}

It feels like we need to:

  • store font styles separate for math and text modes
  • store which mode we're currently in

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch. This means I'll probably resolve #1112 in the process.

}

/**
* Create a new options objects with the give font.
*/
withFontFamily(fontFamily: ?string): Options {
withFontFamily(fontFamily: string): Options {
return this.extend({
fontFamily: fontFamily || this.fontFamily,
fontFamily,
});
}

Expand Down
2 changes: 2 additions & 0 deletions src/Parser.js
Expand Up @@ -494,6 +494,7 @@ export default class Parser {
this.consume();
return new ParseNode("styling", {
style: "text",
mathStart: true,
value: body,
}, "math");
} else if (func === "\\begin") {
Expand Down Expand Up @@ -559,6 +560,7 @@ export default class Parser {
} else {
return new ParseNode("font", {
font: style,
oldTextFont: true,
body: new ParseNode("ordgroup", body, this.mode),
}, this.mode);
}
Expand Down
33 changes: 17 additions & 16 deletions src/buildCommon.js
Expand Up @@ -33,7 +33,7 @@ const mainitLetters = [
const lookupSymbol = function(
value: string,
// TODO(#963): Use a union type for this.
fontFamily: string,
fontName: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does fontFamily in Options.js differ from fontName here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A fontFamily applies to a group of fonts. For example, the SansSerif family can be: regular, bold, or italic. In order to properly lookup the symbol, we need a specific font, not a font family.

mode: Mode,
): {value: string, metrics: ?CharacterMetrics} {
// Replace the value with its replaced value from symbol.js
Expand All @@ -42,7 +42,7 @@ const lookupSymbol = function(
}
return {
value: value,
metrics: fontMetrics.getCharacterMetrics(value, fontFamily, mode),
metrics: fontMetrics.getCharacterMetrics(value, fontName, mode),
};
};

Expand All @@ -58,12 +58,12 @@ const lookupSymbol = function(
*/
const makeSymbol = function(
value: string,
fontFamily: string,
fontName: string,
mode: Mode,
options?: Options,
classes?: string[],
): domTree.symbolNode {
const lookup = lookupSymbol(value, fontFamily, mode);
const lookup = lookupSymbol(value, fontName, mode);
const metrics = lookup.metrics;
value = lookup.value;

Expand All @@ -80,7 +80,7 @@ const makeSymbol = function(
// TODO(emily): Figure out a good way to only print this in development
typeof console !== "undefined" && console.warn(
"No character metrics for '" + value + "' in style '" +
fontFamily + "'");
fontName + "'");
symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, 0, classes);
}

Expand Down Expand Up @@ -117,7 +117,7 @@ const mathsym = function(
// text ordinal and is therefore not present as a symbol in the symbols
// table for text, as well as a special case for boldsymbol because it
// can be used for bold + and -
if ((options && options.fontFamily && options.fontFamily === "boldsymbol") &&
if ((options && options.font && options.font === "boldsymbol") &&
lookupSymbol(value, "Main-Bold", mode).metrics) {
return makeSymbol(value, "Main-Bold", mode, options,
classes.concat(["mathbf"]));
Expand Down Expand Up @@ -231,27 +231,28 @@ const makeOrd = function(

const classes = ["mord"];

const fontFamily = options.fontFamily;
if (fontFamily) {
// Math mode or Old font (i.e. \rm)
const isFont = mode === "math" || options.oldTextFont;
const fontOrFamily = isFont ? options.font : options.fontFamily;
if (fontOrFamily) {
let fontName;
let fontClasses;
if (fontFamily === "boldsymbol") {
if (fontOrFamily === "boldsymbol") {
const fontData = boldsymbol(value, mode, options, classes);
fontName = fontData.fontName;
fontClasses = [fontData.fontClass];
} else if (fontFamily === "mathit" ||
} else if (fontOrFamily === "mathit" ||
utils.contains(mainitLetters, value)) {
const fontData = mathit(value, mode, options, classes);
fontName = fontData.fontName;
fontClasses = [fontData.fontClass];
} else if (fontFamily.indexOf("math") !== -1 || mode === "math") {
// To support old font functions (i.e. \rm \sf etc.) or math mode.
fontName = fontMap[fontFamily].fontName;
fontClasses = [fontFamily];
} else if (isFont) {
fontName = fontMap[fontOrFamily].fontName;
fontClasses = [fontOrFamily];
} else {
fontName = retrieveTextFontName(fontFamily, options.fontWeight,
fontName = retrieveTextFontName(fontOrFamily, options.fontWeight,
options.fontShape);
fontClasses = [fontFamily, options.fontWeight, options.fontShape];
fontClasses = [fontOrFamily, options.fontWeight, options.fontShape];
}
if (lookupSymbol(value, fontName, mode).metrics) {
return makeSymbol(value, fontName, mode, options,
Expand Down
9 changes: 7 additions & 2 deletions src/buildHTML.js
Expand Up @@ -458,13 +458,18 @@ groupTypes.sizing = function(group, options) {
groupTypes.styling = function(group, options) {
// Style changes are handled in the TeXbook on pg. 442, Rule 3.
const newStyle = styleMap[group.value.style];
const newOptions = options.havingStyle(newStyle);
const newOptions = group.value.mathStart ?
options.havingStyle(newStyle).withMathMode() :
options.havingStyle(newStyle);
return sizingGroup(group.value.value, newOptions, options);
};

groupTypes.font = function(group, options) {
const font = group.value.font;
return buildGroup(group.value.body, options.withFontFamily(font));
const newOptions = group.value.oldTextFont ?
options.withFont(font).withOldTextFont() :
options.withFont(font);
return buildGroup(group.value.body, newOptions);
};

groupTypes.horizBrace = function(group, options) {
Expand Down
11 changes: 8 additions & 3 deletions src/buildMathML.js
Expand Up @@ -31,7 +31,7 @@ export const makeText = function(text, mode) {
* Returns the math variant as a string or null if none is required.
*/
const getVariant = function(group, options) {
const font = options.fontFamily;
const font = options.font;
if (!font) {
return null;
}
Expand Down Expand Up @@ -244,7 +244,10 @@ groupTypes.spacing = function(group) {

groupTypes.font = function(group, options) {
const font = group.value.font;
return buildGroup(group.value.body, options.withFontFamily(font));
const newOptions = group.value.oldTextFont ?
options.withFont(font).withOldTextFont() :
options.withFont(font);
return buildGroup(group.value.body, newOptions);
};

groupTypes.styling = function(group, options) {
Expand All @@ -259,7 +262,9 @@ groupTypes.styling = function(group, options) {
};

const newStyle = styleMap[group.value.style];
const newOptions = options.havingStyle(newStyle);
const newOptions = group.value.mathStart ?
options.havingStyle(newStyle).withMathMode() :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would've expected options.withMathMode() to be sufficient for this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow this. options.withMathMode() instead of options.havingStyle(newStyle)?

options.havingStyle(newStyle);

const inner = buildExpression(group.value.value, newOptions);

Expand Down
4 changes: 2 additions & 2 deletions src/functions/operatorname.js
Expand Up @@ -39,7 +39,7 @@ defineFunction({

// Consolidate Greek letter function names into symbol characters.
const temp = html.buildExpression(
group.value.value, options.withFontFamily("mathrm"), true);
group.value.value, options.withFont("mathrm"), true);

// All we want from temp are the letters. With them, we'll
// create a text operator similar to \tan or \cos.
Expand Down Expand Up @@ -69,7 +69,7 @@ defineFunction({
let output = [];
if (group.value.value.length > 0) {
const temp = mml.buildExpression(
group.value.value, options.withFontFamily("mathrm"));
group.value.value, options.withFont("mathrm"));

let word = temp.map(node => node.toText()).join("");

Expand Down
3 changes: 2 additions & 1 deletion test/__snapshots__/katex-spec.js.snap
Expand Up @@ -167,7 +167,8 @@ exports[`An implicit group parser within optional groups should work wwith old f
}
]
},
"font": "mathtt"
"font": "mathtt",
"oldTextFont": true
}
}
]
Expand Down
Binary file modified test/screenshotter/images/TextWithMath-chrome.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/screenshotter/images/TextWithMath-firefox.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion test/screenshotter/ss_data.yaml
Expand Up @@ -303,7 +303,12 @@ TextStacked:
\textsf{\textrm{\textbf{abc123}} \textbf{abc123} \textit{abc123}}\\
\textit{abc123 \textbf{abc123} \textsf{abc123}}\\
\end{matrix}
TextWithMath: \text{for $a < b$ and $ c < d $}.
TextWithMath:
\begin{matrix}
\text{for $a < b$ and $ c < d $}. \\
\textsf{for $a < b$ and $ c < d $}. \\
\textsf{for $a < b \textbf{ and } c < d $}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have some tests for the behavior of old fonts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update.

\end{matrix}
Unicode: \begin{matrix}\text{ÀàÇçÉéÏïÖöÛû} \\ \text{БГДЖЗЙЛФЦШЫЮЯ} \\ \text{여보세요} \\ \text{私はバナナです} \end{matrix}
Units: |
\begin{array}{ll}
Expand Down