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

Shrinkwrap vlists in table-like CSS. #768

Merged
merged 2 commits into from
Jul 30, 2017
Merged

Conversation

kohler
Copy link
Collaborator

@kohler kohler commented Jul 18, 2017

This PR is not ready to merge, but I'd appreciate feedback on the approach is ready. @ronkok @kevinbarabash

Relevant commit message:

TeX and CSS treat line heights in fundamentally different ways. In
TeX, every character is treated as a box of its precise height and
depth; the line height (\baselineskip) applies after characters have
been assembled into lines. In CSS, in contrast, every character
creates a "line box" corresponding to the accompanying font. When
characters of different fonts and sizes are placed into the same
span, the resulting line box contains the line boxes of all children.

This is unfortunate because, for example, we want \frac{1}{2} to
behave in vertical spacing contexts like it is exactly as tall and
deep as the visible fraction (which is the TeX behavior). Given CSS
constraints, though, in most contexts the fraction has extra vertical
space: the line boxes for the numerator and denominator create
padding. For small boxes, this isn't so bad. To really see the
problem put a tall rule in the denominator of a fraction, or check
out the VerticalSpacing screenshotter test, which has way more space
than it should.

Solving this problem in CSS is difficult. There is no easy way to get
rid of the extra line boxes.

But there is a way, namely tables. A table-cell with vertical-align
top, bottom, or middle is ignored for the purposes of line height
calculation.

So in this commit, makeVList puts its contents into a
vertical-align:bottom table-cell (to clear unwanted line boxes), with
an extra row used to represent depth.

Many Chrome screenshotter tests change. This is because Chrome rounds
table dimensions to integral numbers of pixels, while it uses
sub-pixel positioning for non-table displayed tabs. That makes many
vlists a fraction of a pixel wider than they used to be.

@ronkok
Copy link
Collaborator

ronkok commented Jul 19, 2017

Wow. Enormously impressive.

Of course this needs lots of cross-browser testing, so I'll help with that. This solves a tough problem.

Now I have a question. There are couple of lines that contain a constant of 0.91:

child.style.top = (-child.strutSize * 0.91 + totalSize - child.bottom) + "em";
    vtable.style.top = (0.91 - vlist.height) + "em";

I expected that value to be 0.9, not 0.91, because the KaTeX fonts have an ascend value of 0.9. Is there any particular reason you used 0.91?

I'm glad you mentioned the extra space in the VerticalSpacing screenshotter test. When I was testing PR #670, I repeatedly saw this kind of extra space, and I'm glad that someone else has noticed it.

In an earlier thread, @gagern mentioned that any use of tables should be careful that the CSS resets table style. I don't think your CSS would conflict, but it is a good thing to keep in mind.

@kohler
Copy link
Collaborator Author

kohler commented Jul 19, 2017

The 0.91 was determined empirically, by comparing baseline-aligned rules in and out of vlists. I'd think it would be 0.9 (the line-height is 1.2em and the typographic ascender 0.8em, so you'd think 0.1em at the top + 0.8 ascender = 0.9em), but it's not.

I also added some comments, and thanks, added the table-layout: fixed I expect @gagern was referring to.

Compatibility could be an issue here. In particular, the vertical positioning in this checkin makes assumptions about (1) the ascender of a particular font, (2) the way line-boxes are calculated. These assumptions are new: the old code did produce different results on different browsers, but only in terms of the extra whitespace around KaTeX math, rather than within the math itself.

According to CSS 2.1 the ascender “should” be calculated using particular font metrics: sTypoAscender, sTypoDescender. But there might be differences on different platforms. So curious what happens on modern, KaTeX-supported browsers.

@edemaine
Copy link
Member

Sounds very cool! I've always been confused/annoyed by line boxes, so it's great to see a possible fix in this context.

Regarding @gagern's table comment, he may have also been referring to CSS code like this (taken from my own project), which changes the default style of all tables in a certain context. Both Markdown tables and KaTeX output could appear in this context (of a Markdown + LaTeX formatted message). But these rules are relevant only to tags like <table>, <tr>, <td>, not CSS-style tables. So I think no issue here either.

@ronkok
Copy link
Collaborator

ronkok commented Jul 20, 2017

I've tested this code on my box, on my phone. and on BrowserStack with various styles, sizes, and images. It works great on Edge, IE 9-11, iOS Safari, Mac Safari, Windows Chrome, Android Chrome, Opera, and Firefox.

Terrific stuff.

@kohler
Copy link
Collaborator Author

kohler commented Jul 20, 2017

Thanks @ronkok!!

I had, though, an idea to make the mechanism less sensitive to font ascender dimensions, namely a tall overflow:hidden strut. That makes many Firefox images match the screenshotter versions exactly and seems more robust.

However, Chrome versions don't match—in the horizontal direction. This was a surprise to me. It appears that Chrome rounds table-cell dimensions up to integral numbers of pixels, but does not do this for divs. Example diff attached. Maybe this is a blocker, maybe it's not.

arrays-chrome-diff

@edemaine
Copy link
Member

Annoying. Here's the Chrome issue.

@edemaine
Copy link
Member

Personally, I think this approach is still worth persuing. It's not a huge loss, and Chrome will hopefully eventually fix, while we gain... a lot, I think? Can you show some examples where this new approach to vlists is a significant improvement over the current one?

@ronkok
Copy link
Collaborator

ronkok commented Jul 20, 2017

Oh, I think this PR is a big win. The problem is inherent anytime a tall element is passed as an argument to makeVList. Below are two screen shots of the same HTML. The first screenshot is from the existing KaTeX master, and the second shot is from the current PR.
makevlist

That gap in the first shot is the problem. My work used a hack to mitigate it only for \fbox and \cancel. This PR resolves the problem entirely.

@kevinbarabash
Copy link
Member

@edemaine thanks for digging up that issue. It sounds like they want to fix it and are prioritizing it so that's promising.

@kohler since it looks like this bug is going to be fixed in Chrome and it doesn't exist in other browsers the rounding issues probably shouldn't be a block, but it'd be good to do some testing at smaller font sizes to see how much of an impact this has on layouts at those sizes.

@kohler
Copy link
Collaborator Author

kohler commented Jul 20, 2017

@kevinbarabash I've done some tests with 16px fonts, and the rounding issues are worse, but not dramatically worse. Three examples from screenshotter tests:

arrays-chrome-diff

sqrtroot-chrome-diff

primesuper-chrome-diff

@kohler
Copy link
Collaborator Author

kohler commented Jul 21, 2017

I further updated the CSS. By using two table-rows, and a table-cell with vertical-align: bottom, all reliance on line-height and font ascent can be avoided, and the code is simpler.

Only a small number of firefox test cases change. Most chrome test cases change, because of the rounding issue.

Assuming that the updated CSS works on other browsers—it appears to, but @ronkok, if you could help check that would be amazing—this PR is in my opinion ready to merge.

@ronkok
Copy link
Collaborator

ronkok commented Jul 21, 2017

I am now seeing problems in Safari, on both iOS and Mac. All other browsers (IE, Edge, Chrome, Firefox, Opera) look good. I do my Safari testing on BrowserStack, so it should be checked by someone with native equipment. Here's a sample of the problems:
safari

@kohler
Copy link
Collaborator Author

kohler commented Jul 21, 2017

Thanks @ronkok! That was a real problem: Safari needed an extra table-cell to position the table correctly. I've fixed that problem and things display well on local Safari, at least.

@kohler
Copy link
Collaborator Author

kohler commented Jul 21, 2017

@ronkok, did you know that accent placement was off on Safari? The \vec’s are rightwards-displaced even on master. The commit I just pushed is correct vertically though.

@ronkok
Copy link
Collaborator

ronkok commented Jul 21, 2017

Yes, the \vec horizontal problem is issue #735. It will need its own solution.

@ronkok
Copy link
Collaborator

ronkok commented Jul 21, 2017

@kohler, That last commit did the trick. This PR now works in Chrome, Firefox, Edge, IE 9-11, Safari, and Opera.

This problem has caused me many hours of frustration, so I'm very glad that you have solved it.

@kohler
Copy link
Collaborator Author

kohler commented Jul 27, 2017

Ping—can a girl get a review? @kevinbarabash

@kevinbarabash
Copy link
Member

@kohler sorry for the delay. I'll review this this weekend.

@kevinbarabash
Copy link
Member

These assumptions are new: the old code did produce different results on different browsers, but only in terms of the extra whitespace around KaTeX math, rather than within the math itself.

I noticed this when reviewing the screenshots. It seems like there is generally less extra padding which is a good thing imo.

But there might be differences on different platforms.

Awesome article.

Three examples from screenshotter tests:

I was concerned about vertical/horizontal rules getting too close to numbers/letters. The examples in the screenshot look alright.

}
}
const fontSizer = makeFontSizer(options, maxFontSize);
pstrutSize += 2;
Copy link
Member

Choose a reason for hiding this comment

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

This is so that it's a little taller than any of the children?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, taller than any child—or the height of any child.

}
}
const fontSizer = makeFontSizer(options, maxFontSize);
pstrutSize += 2;
const pstrut = makeSpan(["pstrut"], []);
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious about the name pstrut... what does the p represent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

“positioning strut”.

for (i = 0; i < children.length; i++) {
if (children[i].type === "elem") {
maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
const child = children[i].elem;
pstrutSize = Math.max(pstrutSize, child.maxFontSize, child.height);
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing this will have to be updated to support \rasiebox eventually.

childWrap.depth += shift;
childWrap.style.top = shift + "em";
const childWrap = makeSpan([], [pstrut, child]);
childWrap.style.top = (-pstrutSize - currPos - child.depth) + "em";
Copy link
Member

Choose a reason for hiding this comment

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

Just so I understand this correctly, the pstrut in the childWrap is taller than the child so that we avoid issues with the baseline alignment and can align the bottom of the child's glyph bounding box with the bottom of the pstrut? And then childWrap is position vertically so that child appears where we want it to.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Exactly.

const topStrut = makeSpan(["vlist-s"], [new domTree.symbolNode("\u200b")]);

rows = [makeSpan(["vlist-r"], [vlist, topStrut]),
makeSpan(["vlist-r"], [depthStrut])];
Copy link
Member

Choose a reason for hiding this comment

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

In what situations is the depthStrut necessary? I tried removing it from some fraction layouts and didn't notice a difference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The depthStrut signals to the browser how deep the box is. This isn't that relevant in normal layouts; KaTeX understands heights & depths. But it matters for layout of surrounding lines. That also might not matter given the current coding (which has separate struts done at the very outermost level), but I think it's good and useful to have the vlist coding produce a box of the correct height and depth.


const vtable = makeSpan(["vlist-t"], rows);
vtable.height = maxPos;
vtable.depth = Math.max(-minPos, 0);
Copy link
Member

Choose a reason for hiding this comment

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

I didn't realize that depth couldn't be negative. Why is this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In TeX it can be negative but usually isn't. The current code works, and I thought that it was producing the same results as the old code—should I check without the max-0?

@@ -564,16 +581,14 @@
}

// Lengthen the extensible arrows via padding.
.x-arrow > span > span {
text-align: center;
Copy link
Member

Choose a reason for hiding this comment

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

I see, this got moved down to line 591.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup.

@@ -564,16 +581,14 @@
}

// Lengthen the extensible arrows via padding.
.x-arrow > span > span {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't we have to be as specific as we were being before with these rules?

.vlist-s {
display: table-cell;
vertical-align: bottom;
font-size: 0.05em;
Copy link
Member

Choose a reason for hiding this comment

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

Is the 0.05em b/c we want the impact of the font size on the base line of elements with this class to be negligible?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Exactly.

@@ -232,8 +232,25 @@
& + .mop.mtight { margin-left: @thinspace; }
}

.vlist-t {
display: inline-table;
table-layout: fixed;
Copy link
Member

Choose a reason for hiding this comment

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

Reading https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout it says that table-layout: fixed bases the width of the table on the first row. It looks like the first row is always contains the content for the vlist. Is that right?

Copy link
Member

Choose a reason for hiding this comment

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

Apparently table-layout: fixed renders faster. I'm curious if the decision to use this was motivated by perf concerns or something else.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Motivated by the fact that the other table displays in katex.less have table-layout: fixed.

TeX and CSS treat line heights in fundamentally different ways. In
TeX, every character is treated as a box of its precise height and
depth; the line height (\baselineskip) applies after characters have
been assembled into lines. In CSS, in contrast, every character
creates a "line box" corresponding to the accompanying font. When
characters of different fonts and sizes are placed into the same
span, the resulting line box contains the line boxes of all children.

This is unfortunate because, for example, we want `\frac{1}{2}` to
behave in vertical spacing contexts like it is exactly as tall and
deep as the visible fraction (which is the TeX behavior). Given CSS
constraints, though, in most contexts the fraction has extra vertical
space: the line boxes for the numerator and denominator create
padding. For small boxes, this isn't so bad. To really see the
problem put a tall rule in the denominator of a fraction, or check
out the VerticalSpacing screenshotter test, which has way more space
than it should.

Solving this problem in CSS is difficult. There is no easy way to get
rid of the extra line boxes.

But there is *a* way, namely tables. A table-cell with vertical-align
top, bottom, or middle is ignored for the purposes of line height
calculation.

So in this commit, makeVList puts its contents into a
vertical-align:bottom table-cell (to clear unwanted line boxes), with
an extra row used to represent depth.

Many Chrome screenshotter tests change. This is because Chrome rounds
table dimensions to integral numbers of pixels, while it uses
sub-pixel positioning for non-table displayed tabs. That makes many
vlists a fraction of a pixel wider than they used to be.
@kohler
Copy link
Collaborator Author

kohler commented Jul 30, 2017

Inspired by your question, I changed the PR slightly to produce negative heights and depths when appropriate. Screenshotter tests OK.

Copy link
Member

@kevinbarabash kevinbarabash left a comment

Choose a reason for hiding this comment

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

LGTM

@kevinbarabash kevinbarabash merged commit 2da06d5 into KaTeX:master Jul 30, 2017
@kevinbarabash
Copy link
Member

@kohler thanks for the PR. I believe this unblocks #685 and changes to \sqrt previewed in #60. Is that right @edemaine and @ronkok?

@ronkok
Copy link
Collaborator

ronkok commented Jul 30, 2017

It certainly unblocks the \sqrt changes. I'll work up a PR today.

@ronkok
Copy link
Collaborator

ronkok commented Jul 30, 2017

I'm finding a problem on Safari. It occurs in iOS only, not Mac. The screenshot below is from BrowserStack's rendition of an iPad Pro, iOS 10.3
problem

That is trying to render part of the code: \( \sqrt A \, \sqrt{M^2} \, \sqrt{\vec F} \, \color{blue} \sqrt{\frac{\frac{\frac{c}{c}}{c}}{\frac{c}{c}} } + \sqrt{\frac{\frac{\frac{\frac{c}{c}}{\frac{c}{c}} }{c}}{\frac{c}{c}} } \, \sqrt{\frac{\frac{\frac{\frac{c}{c}}{\frac{c}{c}} }{c}}{\frac{c}{\frac{\frac{c}{c}}{\frac{c}{c}}}} } \)

Although I can't give you a screenshot, I am seeing the same problem on my iPod touch.

@kohler
Copy link
Collaborator Author

kohler commented Jul 30, 2017 via email

@ronkok
Copy link
Collaborator

ronkok commented Jul 30, 2017

I do pay for BrowserStack. For the short term, at least.

@kevinbarabash
Copy link
Member

Another way to test on iOS is use the simulator that comes with Xcode.

@kohler
Copy link
Collaborator Author

kohler commented Jul 30, 2017

This appears fine on master (which now has this PR). This image is from the iOS Simulator for IPad Pro (10.5-inch) – iOS 10.3 (14E8301)
screen shot 2017-07-30 at 6 30 56 pm

@ronkok
Copy link
Collaborator

ronkok commented Jul 30, 2017

I still see the problems. I see them only when the iPad is in portrait orientation, not landscape. That opens up a line of investigation. The page I'm looking at contains some CSS media queries for screen width. Among these are:

@media all {body {font-size: 20px;}}
@media (max-width:830px){body {font-size: 18px;}}
@media (max-width:740px){body {font-size: 16px;}}
@media (max-width:670px){body {font-size: 14px;}}

I'll investigate some more and let you know if I can pin this down further. Maybe not today, though.

@ronkok
Copy link
Collaborator

ronkok commented Aug 1, 2017

@kohler, I've prepared another test page here. This page has CSS body {font-size: 18px;} regardless of screen orientation. This page also shows makeVList problems on Safari - iOS.

That page, I should add, is running today's KaTeX master.

@kevinbarabash
Copy link
Member

Interestingly that test page looks good at 72px and then falls apart as the font size is decreased.

@kohler kohler deleted the shrinkwrap branch August 1, 2017 19:38
kohler added a commit to kohler/KaTeX that referenced this pull request Aug 1, 2017
Some delicate surgery is required on the `vlist-s` table-cell CSS.
Problem with KaTeX#768 reported by @ronkok; issue KaTeX#779. Fixes:

* `font-size: 1px` (rather than `font-size: 0.05em`) solves render
  issues with user overrides of the `.katex` `font-size`.
* Also fix a width issue.
kevinbarabash pushed a commit that referenced this pull request Aug 2, 2017
Some delicate surgery is required on the `vlist-s` table-cell CSS.
Problem with #768 reported by @ronkok; issue #779. Fixes:

* `font-size: 1px` (rather than `font-size: 0.05em`) solves render
  issues with user overrides of the `.katex` `font-size`.
* Also fix a width issue.
@ry-randall
Copy link
Member

ry-randall commented Feb 23, 2018

Just wanted to say thank you for this change. I'm working on a canvas renderer implementation, and this is a huge life saver.

@ronkok ronkok mentioned this pull request Apr 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants