-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildHTML
uses it for convenience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I totally missed that file b/c github auto-collapsed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try to review the changes to buildHTML.js
over the next couple of days.
src/Options.js
Outdated
@@ -5,6 +5,25 @@ | |||
* `.reset` functions. | |||
*/ | |||
|
|||
const Style = require("./Style"); | |||
|
|||
const sizeStyleMap = [ // [textsize, scriptsize, scriptscriptsize] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where do these numbers come from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -58,22 +68,69 @@ Options.prototype.extend = function(extension) { | |||
return new Options(data); | |||
}; | |||
|
|||
function sizeAtStyle(size, style) { | |||
return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this equivalent to the following?
return sizeStyleMap[size - 1][style.size - 1];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/Options.js
Outdated
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious why you chose not reset the cramped-ness?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It didn't hurt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice comment.
@@ -483,7 +483,7 @@ Parser.prototype.parseImplicitGroup = function() { | |||
const body = this.parseExpression(false); | |||
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice cleanup
@@ -489,8 +490,7 @@ groupTypes.sizing = function(group, options) { | |||
// in, so we can't reset the size to normal before changing it. Now | |||
// that we're passing an options parameter we should be able to fix | |||
// this. | |||
node.setAttribute( | |||
"mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); | |||
node.setAttribute("mathsize", newOptions.sizeMultiplier + "em"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the comment above still apply?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea and I have no way to check and it's MathML :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -317,10 +311,11 @@ const makeStackedDelim = function(delim, heightTotal, center, options, mode, | |||
inners.push(makeInner(top, font, mode)); | |||
|
|||
// Finally, build the vlist | |||
const inner = buildCommon.makeVList(inners, "bottom", depth, options); | |||
const newOptions = options.havingBaseStyle(Style.TEXT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we reset the base style here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The delimiters look pretty good in the screenshots as is.
} | ||
}); | ||
return deferred.promise; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious why the wrapping isn't necessary anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -770,18 +748,18 @@ groupTypes.spacing = function(group, options) { | |||
|
|||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reset()
isn't necessary anymore here b/c buildGroup
handles that now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No: The .reset()
isn't necessary any more because Options
objects are immutable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sizing classes were on rules previously too, I think.
src/buildHTML.js
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we not using options.sizingClasses(baseOptions)
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could.
@@ -489,8 +490,7 @@ groupTypes.sizing = function(group, options) { | |||
// in, so we can't reset the size to normal before changing it. Now | |||
// that we're passing an options parameter we should be able to fix | |||
// this. | |||
node.setAttribute( | |||
"mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); | |||
node.setAttribute("mathsize", newOptions.sizeMultiplier + "em"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
textstyle
CSS classes andsize5
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.Remove
displaystyle/textstyle
CSS classes entirely, since they do nothing now.This is actually a simplification, since now only
size
affects the size. Simplifies CSS and computation. Many screenshotter tests change; most of them (all?) change to be more like TeX.