Fix interaction between styles and sizes.#719
Conversation
|
(Rebased.) |
|
c2aaf90 fixes a longstanding bug in |
|
I'm going to re-commit with some changes to the mechanism, because of corner cases in the interaction between sizes and styles. Here's the issue. Consider:
These commands have no equivalent in TeX, but these work, and have the following result: Note that In this commit, This means that TeX like I'm not sure there's a more coherent solution than this (maybe you can think of one), but this seems most TeXlike. |
|
After playing with more examples, including |
|
The
This is so that both the sizes glyphs and the spaces between them is consistent no matter where the sizing command is used, is that right? |
That is my assumption too, and I share your surprise. The main change here is that, e.g.,
Yes. For what it's worth, and it's up to you, but this will probably conflict with #519. Whatever gets merged first I'm happy to help resolve conflicts. |
|
Hi @kevinbarabash any other comments on this? |
|
Ah, OK, I figured. Probably #670 wants to go first, as @kevinbarabash has been working on that for a while. |
|
#670 has landed. I don't see any conflicts, but perhaps tests need to be rerun? |
|
The new PR is ready, including necessary changes to the stretchy functions (#670). I also added a screenshotter improvement. |
| /** | ||
| * Create a new options object with the given style. | ||
| * Return an options object with the given style. If `this.style === style`, | ||
| * returns `this`. |
There was a problem hiding this comment.
This seems functionally equivalent as long as we don't mutate these objects. Can you add a warning somewhere not to mutate these objects and to only use the functions provided to create new Option instances from existing ones?
There was a problem hiding this comment.
Sure.
FWIW it would also be OK to use the old .with* names. The .having* names were very useful during auditing.
| return this.extend({ | ||
| size: size, | ||
| }); | ||
| Options.prototype.havingCrampedStyle = function() { |
There was a problem hiding this comment.
buildHTML uses it for convenience.
There was a problem hiding this comment.
Thanks. I totally missed that file b/c github auto-collapsed it.
There was a problem hiding this comment.
I'll try to review the changes to buildHTML.js over the next couple of days.
|
|
||
| const Style = require("./Style"); | ||
|
|
||
| const sizeStyleMap = [ // [textsize, scriptsize, scriptscriptsize] |
There was a problem hiding this comment.
So these numbers are chosen by applying 0.7x and 0.5x the base size and rounding to an available KaTeX size. But your question inspired an experiment.
In old KaTeX, scriptsize is always 0.7x the size of textsize, and scriptscriptsize is 0.5x the size of textsize. This also holds in LaTeX at 10pt:
\documentclass[10pt]{article}
\usepackage[utf8]{inputenc}
\showboxbreadth=\maxdimen
\showboxdepth=\maxdimen
\begin{document}
\tracingonline=1
$a^{b^c}$\showlists
\end{document}
shows
\OML/cmm/m/it/10 a
\hbox(5.17056+0.0)x7.7638, shifted -3.62892
.\OML/cmm/m/it/7 b
.\hbox(2.15277+0.0)x3.74713, shifted -3.01779
..\OML/cmm/m/it/5 c
But at other point sizes these ratios don't hold precisely, since LaTeX was originally built for a fixed palette of fonts at specific sizes. For instance, 11pt gives sizes 10.95/8/6, and 12pt gives 12/8/6.
FWIW here are the sizes LaTeX uses at base font size 10pt, compared with the PR's current sizes:
| TeX command | TeX Sizes | Current PR sizes |
|---|---|---|
\tiny |
5/5/5 | 5/5/5 |
\scriptsize |
7/5/5 | 7/5/5 |
\footnotesize |
8/6/5 | 8/7/5 [NB KaTeX has no 6pt] |
\small |
9/6/5 | 9/7/5 |
\normalsize |
10/7/5 | 10/7/5 |
\large |
12/8/6 | 12/8/7 |
\Large |
14.4/10/7 | 14.4/10/7 |
\LARGE |
17.28/12/10 | 17.3/12/9 |
\huge |
20.74/14.4/12 | 20.7/14.4/10 |
\Huge |
24.88/20.74/17.28 | 24.9/17.3/12 |
So some differences.
So what to do? We could keep the current sizes (& maybe document them). Or we could implement these more TeX-precise sizes in KaTeX, with or without the currently-missing 6pt font. We can also delay this choice (i.e. leave the current sizes in and then change them later).
There was a problem hiding this comment.
My understanding of KaTeX fonts is that we only have 10pt size fonts and we scale them appropriately. What would prevent us from scaling that font to 6pt when necessary?
There was a problem hiding this comment.
I agree this shouldn't be blocking, but if we defer please add a comment in the code.
There was a problem hiding this comment.
There is nothing preventing us from scaling the font to 6pt. But because we do things with CSS here, it would mean that “normalsize”, which has been “size5” in CSS classes for a while, would become “size6”. Probably doesn't matter.
| }; | ||
|
|
||
| function sizeAtStyle(size, style) { | ||
| return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; |
There was a problem hiding this comment.
Is this equivalent to the following?
return sizeStyleMap[size - 1][style.size - 1];
There was a problem hiding this comment.
No because style.size might be 0 (DISPLAY).
It would be equivalent to sizeStyleMap[size - 1][Math.max(0, style.size - 1)] but I find this easier to understand
There was a problem hiding this comment.
For some reason I thought the sizes where started counting from 1. I think my reason for confusion is that I was conflating sizing command sizes with style command sizes. :( The fact that we're accessing values in sizeStyleMap should've clued me in.
| // Ensure style is at least `\textstyle`. | ||
| let style = this.style; | ||
| if (style.size > 1) { | ||
| style = style.cramped ? Style.TEXT.cramp() : Style.TEXT; |
There was a problem hiding this comment.
I'm curious why you chose not reset the cramped-ness?
There was a problem hiding this comment.
(I don't have a crisp response. :) )
| * Create a new options object with the same style, size, and color. This is | ||
| * used so that parent style and size changes are handled correctly. | ||
| * Return the CSS sizing classes required to switch from enclosing options | ||
| * `oldOptions` to `this`. Returns an array of classes. |
| return new ParseNode("sizing", { | ||
| // Figure out what size to use based on the list of functions above | ||
| size: "size" + (utils.indexOf(sizeFuncs, func) + 1), | ||
| size: utils.indexOf(sizeFuncs, func) + 1, |
| // this. | ||
| node.setAttribute( | ||
| "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); | ||
| node.setAttribute("mathsize", newOptions.sizeMultiplier + "em"); |
There was a problem hiding this comment.
Does the comment above still apply?
There was a problem hiding this comment.
I have no idea and I have no way to check and it's MathML :(
There was a problem hiding this comment.
This should be checkable by rendering a expression with nested sizing commands and verify that the mathsize values in the MathML tree in the DOM match the sizes we use for same elements in visible HTML nodes that KaTeX renders.
| inners.push(makeInner(top, font, mode)); | ||
|
|
||
| // Finally, build the vlist | ||
| const inner = buildCommon.makeVList(inners, "bottom", depth, options); |
There was a problem hiding this comment.
Why are we reset the base style here?
There was a problem hiding this comment.
Because stacked delimiters are taken from the extension font, and in old TeX, the extension font was only used at size 10.
However, there is a wrinkle: In AMSLaTeX, the extension font is available (and used) at smaller sizes too. I could see changing the code to do that instead.
There was a problem hiding this comment.
The delimiters look pretty good in the screenshots as is.
| } | ||
| }); | ||
| return deferred.promise; | ||
| } |
There was a problem hiding this comment.
These changes seem unrelated to the rest of the PR. Were you using this to compare the sizing changes with LaTeX output?
There was a problem hiding this comment.
Yes. I'm happy to take them out of the PR but shrug it's all going in the same place eventually
There was a problem hiding this comment.
@gagern is probably the most experienced with the screenshotter, so he might want to look at this part of the PR (or separate the PR into two).
There was a problem hiding this comment.
It's definitely preferable to to have these as separate commits. Even though it's all going to the same place, smaller commits will take less time to review and if one review needs revision it doesn't hold up the other.
…zes. Rather than having both `textstyle` CSS classes and `size5` CSS classes affect the font size (and step on each other), implement sizes more the way TeX does: a command like `\displaystyle` changes the current size. This is actually a simplification, since now only `size` affects the size. Simplifies CSS and computation. Many screenshotter tests change; they change to be more like TeX. For instance, `\sqrt` fixes some discrepancies in size treatment. Also: Remove the `Options.withX()` methods in favor of `.havingX()`, which might return the same `options`. Remove `Style.cls()` and `Style.reset()`. Remove `Options.reset()`. You should never modify an `Options`; they should change only by the `havingX()` methods.
|
I took out the screenshotter diff and updated some comments as requested. |
|
I'm going to check in another commit that adds the 6pt size and implements TeX's size relationships for scriptstyle/scriptscriptstyle. I also gave KaTeX's sizes more significant digits. |
At every size level. Also make the sizes match TeX to the last decimal.
|
The TeX size relationships are in. |
|
Hi @kevinbarabash any comments? |
|
@kohler sorry. I haven't reviewed changes to buildHTML.js. I'll have time this weekend. |
k4b7
left a comment
There was a problem hiding this comment.
There are a few nits which may or may not make sense to change. Mostly questions so that I understand what's going on. This is a complex change and I want to make sure I'm not missing something before merging this.
| newOptions = options.withStyle(style.sup()); | ||
| sup = buildGroup(group.value.sup, newOptions); | ||
| supmid = makeSpan([style.reset(), style.sup().cls()], | ||
| [sup], newOptions); |
There was a problem hiding this comment.
I'm curious why the wrapping isn't necessary anymore.
There was a problem hiding this comment.
It looks like it's being handled by buildGroup now that we're passing parent options.
| if (!isCharacterBox(group.value.base)) { | ||
| subShift = base.depth + newOptions.style.metrics.subDrop | ||
| * newOptions.sizeMultiplier / options.sizeMultiplier; | ||
| } |
There was a problem hiding this comment.
Moving this up here to avoid unnecessary calculations when there isn't both sub and sup parts is a nice optimization.
There was a problem hiding this comment.
I'm okay with keeping this change as it makes reviewing easier.
| let sub; | ||
| const base = buildGroup(group.value.base, options); | ||
| let supm; | ||
| let subm; |
There was a problem hiding this comment.
Why the name change from sub and sup? Keeping those variables names would've reduced the number of changed lines.
There was a problem hiding this comment.
So, in the old code, sub and sup were not decorated with the classes necessary to change size, whereas supmid and submid were so wrapped.
In the new code, we don't need both versions, because we create the wrapped versions in one step. These are more like the mid versions, so I kept a suffix.
| [options.style.reset(), Style.TEXT.cls(), "frac-line"]); | ||
| // Manually set the height of the line because its height is | ||
| // created in CSS | ||
| mid.height = ruleWidth; |
There was a problem hiding this comment.
I see makeLineSpan takes care of setting the height.
| @@ -450,20 +436,16 @@ groupTypes.genfrac = function(group, options) { | |||
| const dstyle = style.fracDen(); | |||
| let newOptions; | |||
There was a problem hiding this comment.
I wish we weren't reusing this identifier for different options within the frac layout, but since this is the way the code was I guess we'll have to change it later.
| groupTypes.llap = function(group, options) { | ||
| const inner = makeSpan( | ||
| ["inner"], [buildGroup(group.value.body, options.reset())]); | ||
| ["inner"], [buildGroup(group.value.body, options)]); |
There was a problem hiding this comment.
The reset() isn't necessary anymore here b/c buildGroup handles that now.
There was a problem hiding this comment.
No: The .reset() isn't necessary any more because Options objects are immutable.
There was a problem hiding this comment.
I was mixing up options.reset() with the reset- CSS classes.
| const makeLineSpan = function(className, options) { | ||
| const baseOptions = options.havingBaseStyle(); | ||
| const line = makeSpan( | ||
| [className].concat(baseOptions.sizingClasses(options)), |
There was a problem hiding this comment.
Are adding the sizing classes necessary here? The span has no children and we're already setting its height explicitly.
There was a problem hiding this comment.
The sizing classes were on rules previously too, I think.
| inner[i].maxFontSize = fontSize; | ||
| } else if (inner[i].classes[pos + 1] === "reset-" + group.value.size) { | ||
| inner[i].classes.push("sizing", "reset-size" + baseOptions.size, | ||
| "size" + options.size); |
There was a problem hiding this comment.
Could we not using options.sizingClasses(baseOptions) here?
| // this. | ||
| node.setAttribute( | ||
| "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); | ||
| node.setAttribute("mathsize", newOptions.sizeMultiplier + "em"); |
There was a problem hiding this comment.
This should be checkable by rendering a expression with nested sizing commands and verify that the mathsize values in the MathML tree in the DOM match the sizes we use for same elements in visible HTML nodes that KaTeX renders.
|
|
||
| return new buildCommon.makeFragment(inner); | ||
| const newOptions = options.havingStyle(newStyle); | ||
| return sizingGroup(group.value.value, newOptions, options); |
There was a problem hiding this comment.
I really like how you unified the handle of style groups and sizing groups.
|
Thanks for the comments, I checked in some changes. The MathML values look correct to me in a couple of examples, so I think it's OK. |
|
Hi @kevinbarabash, any more comments or requests on this? (I have some other changes blocked here.) |
|
@kohler thanks for making those changes and for checking the MathML output. The output looks good so if there are things that I've missed they must be minor. |
Corrects a bug in KaTeX#719.

Rather than having both
textstyleCSS classes andsize5CSS classes affect the font size (and step on each other), implement sizes more the way TeX does: a command like\displaystylechanges the current size.Remove
displaystyle/textstyleCSS classes entirely, since they do nothing now.This is actually a simplification, since now only
sizeaffects the size. Simplifies CSS and computation. Many screenshotter tests change; most of them (all?) change to be more like TeX.