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 #533, #534, #541. #567
Conversation
CLA signature looks good 👍 |
I have a fix for #4 that's based on this commit, and will submit it for review after hearing back. |
44fdf40
to
e95dda4
Compare
Hi, any chance I could talk to a contributor about the approach taken by this PR? KaTeX's approach to TeX operator spacing uses CSS adjacency rules, as in But in some cases KaTeX doesn't ensure this. Style changes, size changes, and spaces introduce spurious spans into the HTML output. These spans mess up operator spacing: TeX considers two boxes adjacent (and space them accordingly), but in CSS the spans aren't adjacent. The same root cause is behind #533, #541, #136, and to some extent #4. The approach taken here—eliminating spurious spans—seems like a good fit for current KaTeX, although it is not a trivial change. I've enjoyed working on these features and others but if there's no chance of merge I should do something else. |
@kohler definitely interested in this PR. I'll try to do a first past this weekend. |
} | ||
|
||
.minner { | ||
& + .mop.mtight { margin-left: @thinspace; } |
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've checked these changes against https://books.google.ca/books?id=rstheMEAf2gC&pg=PA257&lpg=PA257&dq=texbook+ord+bin+rel+op+punct+spacing+rules&source=bl&ots=TU_zSmaVkr&sig=4ndj36478T4s9EI7OgrIAwwlUws&hl=en&sa=X&ved=0ahUKEwjltL3-zMfQAhUC9IMKHZhVCK0Q6AEIHTAA#v=onepage&q=texbook%20ord%20bin%20rel%20op%20punct%20spacing%20rules&f=false. They look good.
@@ -36,6 +37,7 @@ Cases: | | |||
Colors: | |||
tex: \blue{a}\color{#0f0}{b}\color{red}{c} | |||
nolatex: different syntax and different scope | |||
ColorSpacing: \color{black}{\displaystyle \int x} + 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.
Can you change the color to something other than black?
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!
@@ -106,6 +109,7 @@ Sqrt: | | |||
^{\sqrt{\sqrt{\sqrt{x}}}}} | |||
SqrtRoot: | | |||
1+\sqrt[3]{2}+\sqrt[1923^234]{2^{2^{2^{2^{2^{2^{2^{2^{2^{2^{2^2}}}}}}}}}}} | |||
StyleSwitching: a\cdot b\scriptstyle a\cdot b |
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 you add some style switches at boundaries between atom types, e.g. a\cdot\textstyle b
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.
Yep.
@@ -30,14 +30,22 @@ var createClass = function(classes) { | |||
* an inline style. It also contains information about its height, depth, and | |||
* maxFontSize. | |||
*/ | |||
function span(classes, children, height, depth, maxFontSize, style) { | |||
function span(classes, children, 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.
nice cleanup, I didn't realize we weren't using using half of the parameters to domTree.span
.
@@ -35,7 +35,7 @@ var mainitLetters = [ | |||
* Correctly pulls out metrics for the character, and optionally takes a list of | |||
* classes to be attached to the node. | |||
*/ | |||
var makeSymbol = function(value, style, mode, color, classes) { | |||
var makeSymbol = function(value, style, mode, options, 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.
Can you rename style
to fontFamily
?
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.
OK
var atoms = group.value.value; | ||
return getTypeOfGroup(atoms[atoms.length - 1]); | ||
} else if (group.type === "font") { | ||
return getTypeOfGroup(group.value.body); |
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.
Shouldn't we get the rightmost atom of the group.value.body
for `fonts?
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. Font bodies are not arrays—unlike for style, etc. You can see this both in the creation code (in functions.js
) and in groupTypes.font
, which calls buildGroup()
on the body rather than buildExpression
on the body array.
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.
Right b/c styles apply to everything which follows them whereas the font contains a single group. It's been a while since I've looked at the code – can you tell?
node = groups[i] = makeSpan(node.classes, [node]); | ||
} | ||
buildCommon.prependChildren(node, | ||
groups.splice(lastSpace, i - lastSpace)); |
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 found this kind of hard to follow. I think maybe doing two passes would make this more understandable, in the first pass we could add divide groups
up into set of arrays and the second pass could create the spans. This should avoid the need for prependChildren
.
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've tried a different one-pass arrangement.
newOptions = options.withStyle(nstyle); | ||
var numer = buildGroup(group.value.numer, newOptions); | ||
var numerreset = makeSpan([style.reset(), nstyle.cls()], | ||
[numer], 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.
Is newOptions
necessary for the numerreset
? The .reset-X
rules don't seem to interact with .tight
. Am I missing something?
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 doubt the newOptions are necessary, but neither are they harmful.
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.
In that case it's probably best to either always pass options
. There are a few places where it's not being passed, groupTypes.spacing
, groupTypes.sqrt
, groupTypes.array
, and buildExpression
. Can you add it to those call sites.
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.
Ugh. I will try this, but if any tests fail, I will ask you to tolerate the inconsistency, which is really no different from (e.g.) previous commits, which didn't always pass options.color()
in to makeSpan
.
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.
Yeah no look let me push back on this. There are makeSpan
calls in delimiter.js
, etc., that as of this commit have no options
to pass. How about I put a TODO to improve API consistency.
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.
A TODO sounds good.
@@ -684,14 +736,14 @@ groupTypes.spacing = function(group, options, prev) { | |||
// things has an entry in the symbols table, so these will be turned | |||
// into appropriate outputs. | |||
return makeSpan( | |||
["mord", "mspace"], | |||
["mspace"], |
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 did "mord"
disappear?
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 “mspace” nodes are no longer considered atoms. They are not atoms and TeX doesn't treat them as such. So they shouldn't affect operator spacing. If they were mord
they'd affect operator spacing (e.g. if followed by an mbin
)
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.
That makes sense.
[buildCommon.mathsym(group.value, group.mode)] | ||
); | ||
} else { | ||
// Other kinds of spaces are of arbitrary width. We use CSS to | ||
// generate these. | ||
return makeSpan( | ||
["mord", "mspace", | ||
["mspace", |
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.
Same.
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.
Same :)
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.
👍
group.value.body, fontName, "math", options.getColor(), | ||
["op-symbol", large ? "large-op" : "small-op", "mop"]); | ||
group.value.body, fontName, "math", options, | ||
["mop", "op-symbol", large ? "large-op" : "small-op"]); |
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... this order for the classes is better. It would be nice if the param order between makeSpan
and makeSymbol
was more similar. Can you add a TODO to clean that up later (there's already enough changes in this PR)?
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.
OK
fontSize = fontSize * style.sizeMultiplier; | ||
|
||
// Add size-resetting classes to the inner list and set maxFontSize | ||
// manually. |
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 to handle a size change inside of a color group? Can you update this comment to include why we're doing 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.
The old code set maxFontSize manually as well. In case you meant the pos
stuff, I've updated the comment.
} else if (inner[i].classes[pos + 1] === "reset-" + group.value.size) { | ||
inner[i].classes[pos + 1] = "reset-" + 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.
It seems like this is going to result in a lot of reset-
styles being added. Is that the case? It seems like this is unavoidable.
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 will add more reset-
styles than the previous code will; on the other hand, it will add fewer nested <span>
s. I don't see a way to avoid the additional classes, short of adding more passes and/or arguments to the builder functions. That would be another PR.
group.value.value, options.withStyle(newStyle), prev); | ||
group.value.value, newOptions, prev); | ||
|
||
// Add size-resetting classes to the inner list. |
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.
size-resetting
or style-resetting
?
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.
Good catch
e95dda4
to
45d6f6f
Compare
OK @kevinbarabash I’ve addressed your comments! Let me know if you'd like anything else. |
} else { | ||
i = j; | ||
} | ||
} |
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 is still really hard to follow. I was thinking something more along the lines of the following (if I understand the code above correctly):
var spans = [];
var children = [];
for (i = 0; i < groups.length; i++) {
var child = groups[i];
if (isSpace(child && children.length > 0) {
spans.push(makeSpan(children[0].classes, children));
children = [];
} else {
children.push(child);
}
}
if (children.length > 0) {
spans.push(makeSpan(children[0].classes, children));
}
return spans;
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 doesn't do the same thing. Aside from the obvious bugs (the isSpace(child)
case doesn’t actually put the current child anywhere), by introducing spurious spans, it would screw up class adjacency.
I think the code is pretty simple and you should take it as is.
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 following code should work but is longer. If you really won't take the splice
solution i'll do this.
var spaces = [];
var nextGroups = [];
for (i = 0; i < groups.length; i++) {
group = groups[i];
if (isSpace(group)) {
spaces.push(group);
} else {
if (spaces.length > 0) {
if (group instanceof domTree.symbolNode) {
group = makeSpan(group.classes, [group]);
}
buildCommon.prependChildren(group, spaces);
spaces = [];
}
nextGroups.push(group);
}
}
if (spaces.length > 0) {
Array.prototype.push.apply(nextGroups, spaces);
}
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 for catching my mistake with isSpace(child
.
This is better. I was confused as to what was being prepended. buildCommon.prependChildren(group, spaces);
makes that really clear. Question about the instanceof domTree.symbolNode
check: if it fails is it possible that the group
being passed into prependChildren
is not a span?
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.
OK. I still don't find the original confusing. Would
var spaces = groups.splice(i, j - i);
make it clearer. This isn't a huge deal but creating additional arrays isn't free; in the normal case where there are no spaces my code doesn't do any array allocation
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.
As for instanceof domTree.symbolNode
, as the immediately previous comment says,
// At this point `groups` consists entirely of `symbolNode`s and `span`s.
because the documentFragments have been spliced in
Not just the current color. This will facilitate applying options to built nodes in a standardized way, rather than changing all callsites.
Specifically, infer style from a class on the *current* element, rather than the parent element. Use "mtight" class to denote elements with tight spacing (scriptstyle or scriptscriptstyle). Apply that class automatically based on options.
- KaTeX#534: Implement getTypeOfGroup for font groups. - KaTeX#533, KaTeX#541: Improve the ways spaces are applied to lists. Since CSS adjacency implements mathematical spacing, it's incorrect to introduce "convenience spans" for spaces and display changes into the generated HTML -- those spans break adjacency. Apply display changes directly, and shift space spans into adjacent atoms. Requires updates to two screenshotter tests, LimitControls and SupSubLeftAlignReset. The new results for these tests are closer to TeX output than the old results. Also requires updates to Jasmine tests, since those assumed output structures that have changed.
This is so the size commands don't hide the types of their enclosed atoms. Addresses KaTeX#136. This slightly changes the vertical position of the Sizing test. Not sure the vertical position matters, so change the test.
45d6f6f
to
e9899d8
Compare
@kohler thanks for updating the loop. I left a minor nit in the inline comments. I understand the desire to keep performance in check. TBH, performance isn't something I've put a whole lot of thought into :(, but it's one of the selling points of KaTeX so I wouldn't want to do anything that adversely affects it. We should probably have some sort of performance tests that we can run on each build to avoid perf regressions. |
I noticed that too. Can you run texcmp.sh to see how it compares against LaTeX's output? See https://github.com/Khan/KaTeX/blob/e6de31d2d622f01aae6d465cf332a268332942f4/dockers/texcmp/README.md for details. |
It doesn't work, either on local or docker. At least this diff is needed then I could get local to work
Docker looks like some latex packages are missing from the docker The diff is attached; there are font differences clearly; the baselines are equal, meaning the output of this PR is better than the previous |
@kohler thanks for checking the output for the Sizing test. Hmm... I'm thinking the issue with |
\displaystyle
is present in the coloured block #533, Got wrong space of LaTeX "\!=\!=\!="? #541: Improve the ways spaces are applied to lists. SinceCSS adjacency implements mathematical spacing, it's incorrect to
introduce "convenience spans" for spaces and display changes into
the generated HTML -- those spans break adjacency. Apply display
changes directly, and shift space spans into adjacent atoms.
Requires updates to two screenshotter tests, LimitControls and
SupSubLeftAlignReset. The new results for these tests are closer
to TeX output than the old results.
Also requires updates to Jasmine tests, since those assumed output
structures that have changed.