Use JS for spacing between atoms instead of CSS#1070
Conversation
Summary: This is the first step towards creating an intermediate representation that can be used to generate HTML, SVG, and Canvas commands for rendering. By generating spans that contain the width of the spaces instead of relying on CSS sibling rules we'll be able to one day replaces the spans with intermeidate 'Glue' nodes (in a later PR). An added benefit of this approach is that is enables us to programmatically change the values for thinspace, mediumspace, and thickspace which will allow us to implement the \setlength command. Test Plan: - npm test - dockers/Screenshotter/screenshotter.sh --verify
|
There are some failing tests for this change that I have to work through, in particular: |
|
Somethings have improved in the screenshots:
Somethings are worse:
Somethings are neutral:
|
|
All tests are passing now, but there's still a bit of cleanup work that I'd like to do first. In particular:
|
| if (groups[i].value === "\u0338") { | ||
| groups[i].style.position = "absolute"; | ||
| // TODO(kevinb) fix this for Safari by switching to a non-combining | ||
| // character for \not. |
There was a problem hiding this comment.
I think TODO makes the most sense in a separate PR since font changes are involved.
src/buildHTML.js
Outdated
| // TODO(kevinb) fix this for Safari by switching to a non-combining | ||
| // character for \not. | ||
| // 0.194440 is the width for the \not character | ||
| groups[i].style.paddingLeft = `${1.0 - 0.194440}em`; |
There was a problem hiding this comment.
Should grab the width metric for this character from fontMetricsData.js.
| } | ||
| } | ||
|
|
||
| // Process \\not commands within the group. |
There was a problem hiding this comment.
I'm happy how this became a lot simpler.
| calculateSize(html.spacings[lastChildType]["mclose"], options); | ||
| glue.style.marginRight = `${dimension}em`; | ||
| inner.push(glue); | ||
| } |
There was a problem hiding this comment.
I wish that delimsizing.js didn't have to know about this. I was thinking about moving space inserting to be a post-processing step on the HTML tree, but then we don't have access to sizing/styling data so I think we'll just have to live with this.
|
This is ready for review now. |
|
@kohler if you have time, could you look at this PR since you did original work to fix the spacing? |
ry-randall
left a comment
There was a problem hiding this comment.
Love that this is the first step towards an IR
src/buildHTML.js
Outdated
|
|
||
| const thinspace = { | ||
| number: 3, | ||
| unit: "mu", |
There was a problem hiding this comment.
What was the reasoning for switching from em to mu? Although, looking at whole numbers is definitely cleaner.
There was a problem hiding this comment.
I originally used em and had comments and math. This seemed a lot simpler and maps directly to LaTeX definitions.
| return text; | ||
| }; | ||
|
|
||
| const makeGlue = (measurement: Measurement, options: Options): domTree.span => { |
There was a problem hiding this comment.
I think a comment here would be useful. The term glue might be confusing to people.
| export const buildExpression = function(expression, options, isRealGroup) { | ||
| // Parse expressions into `groups`. | ||
| const groups = []; | ||
| const rawGroups = []; |
There was a problem hiding this comment.
What was the reasoning for the rename?
There was a problem hiding this comment.
I thought about insert spaces into this array, but inserts mess with indices and I thought the code would be cleaner copying non space groups over to a new array as the spaces were being inserted.
| const output = buildGroup(group, options); | ||
| if (output instanceof domTree.documentFragment) { | ||
| Array.prototype.push.apply(groups, output.children); | ||
| rawGroups.push(...output.children); |
| @thickspace: 0.27778em; // 5mu | ||
|
|
||
| // These spacings apply in textstyle and displaystyle. | ||
| .mord { |
| } | ||
|
|
||
| // Process \\not commands within the group. | ||
| // TODO(kevinb): Handle multiple \\not commands in a row. |
There was a problem hiding this comment.
Assuming this comment handled by this PR?
|
|
||
| // Process \\not commands within the group. | ||
| // TODO(kevinb): Handle multiple \\not commands in a row. | ||
| // TODO(kevinb): Handle \\not{abc} correctly. The \\not should appear over |
There was a problem hiding this comment.
See the new screenshots for Not. :)
src/buildHTML.js
Outdated
| unit: "mu", | ||
| }; | ||
|
|
||
| export const spacings = { |
There was a problem hiding this comment.
This all looking super clean 👍 . Since this is just data with no dependencies. Perhaps it could be extracted outside of buildHTML into it's own module? Maybe a spacing.js?
| Array.prototype.push.apply(groups, spaces); | ||
| break; | ||
| // spacing (e.g., "add thick space between mord and mrel"). | ||
| const nonSpaces = |
There was a problem hiding this comment.
The comment above indicates that this is spacing, but the variable name is nonSpaces. Is that intentional?
There was a problem hiding this comment.
I'll expand on this comment. Basically, we want to look at the relationship between non space groups to determine what spaces to insert. This means we want to disregard explicit spaces when determining which implicit spaces to add.
src/buildHTML.js
Outdated
| }, | ||
| }; | ||
|
|
||
| const tightSpacings = { |
There was a problem hiding this comment.
What's the reasoning for distinguishing these from spacings?
There was a problem hiding this comment.
The spacings are different when we're in \scriptstyle or \scriptscriptstyle. I'm open to suggestions of other ways to organize these alternate values.
|
@rrandallcainc thanks for the review. I'll make the requested changes this evening. |
| break; | ||
| // spacing (e.g., "add thick space between mord and mrel"). | ||
| const nonSpaces = | ||
| rawGroups.filter(group => group && group.classes[0] !== "mspace"); |
There was a problem hiding this comment.
I think we can remove using dummy spans in the enclosing element like:
https://github.com/Khan/KaTeX/blob/732f2a84ccb000099d29fb1f61dd02e611fab64b/src/functions/href.js#L34-L65
and handle spacing here.
There was a problem hiding this comment.
Good catch. I'm guessing that this diff breaks \href spacing which means we should probably have a test for it.
There was a problem hiding this comment.
I should probably update getTypeOfDomTree to be able to get the class of the rightmost or leftmost element in the tree.
There was a problem hiding this comment.
@ylemkimon as for removing the dummy spans, it's still possible that the anchor wraps a fragment. makeAnchor always returns a single element even when it has multiple children so I believe we still need to do this otherwise I don't see how we'd communicate there being atoms of different classes at either end of the anchor's content.
|
@rrandallcainc this is ready for another pass. |
| classes = [first]; | ||
| } else { // Case 3: both ends have different types. | ||
| // TODO(kevinb): figure out a better way to communicate this | ||
| // information to buildHTML.js#buildExpression. |
There was a problem hiding this comment.
I think we can just return new buildCommon.makeAnchor(href, [], elements, options) in makeAnchor()
There was a problem hiding this comment.
I was confused at first, but this makes sense given your comments in getTypeOfDomTree.
There was a problem hiding this comment.
Currently spacings around \href are duplicated as dummy spans are created.
src/buildHTML.js
Outdated
| // Return math atom class (mclass) of a domTree. | ||
| export const getTypeOfDomTree = function(node) { | ||
| export const getTypeOfDomTree = function(node, side = "right") { | ||
| if (node instanceof domTree.documentFragment) { |
There was a problem hiding this comment.
and get leftmost or rightmost children type for node instanceof domTree.anchor too.
There was a problem hiding this comment.
Also I think we can define a special class name for span that encloses elements without affecting spacings.
There was a problem hiding this comment.
I ran into some issues with spans so I'm going to leave them out for now.
kohler
left a comment
There was a problem hiding this comment.
I gave this a once-over (no time for in depth) and it looks good!
src/buildHTML.js
Outdated
| // Otherwise, put any spaces back at the end of the groups. | ||
| Array.prototype.push.apply(groups, spaces); | ||
| break; | ||
| // Ignore explicit spaces (e.g., \;, \,) when determine what implicit |
src/buildHTML.js
Outdated
| const right = getTypeOfDomTree(nonSpaces[j + 1], "left"); | ||
|
|
||
| // We use buildExpression inside of sizingGroup, but it returns a | ||
| // document fragement of elements. sizingGroup sets `isRealGroup` |
|
@kohler thanks! I'll fix all the nits before merging. |
|
@kevinbarabash This is looking good. I'll do one more pass tomorrow morning. EDIT: Did it now |
| return text; | ||
| }; | ||
|
|
||
| // Glue is a concept from TeX which is a flexible space between elements in |
|
Thanks everyone for reviewing this. This should make #687 much more tractable now. |
|
@kevinbarabash Thanks for implementing. Would extracting these: https://github.com/Khan/KaTeX/blob/master/static/katex.less#L217 follow a similar pattern? |
|
@rrandallcainc yup. Anywhere where we create a span using one of those styles could be replaced with a call to |
| // leftmost node in the fragment. | ||
| // 'mtight' indicates that the node is script or scriptscript style. | ||
| export const isLeftTight = function(node) { | ||
| if (node instanceof domTree.documentFragment) { |
There was a problem hiding this comment.
Can the node be a domTree.documentFragment? Aren't they flattened at the start of buildExpression?
There was a problem hiding this comment.
@kevinbarabash It seems that node's children can be a domTree.documentFragment, e.g., \phantom{x}^2.
| for (let i = 0; i < nonSpaces.length; i++) { | ||
| if (isBin(nonSpaces[i])) { | ||
| if (isBinLeftCanceller(nonSpaces[i - 1], isRealGroup) | ||
| || isBinRightCanceller(nonSpaces[i + 1], isRealGroup)) { |
There was a problem hiding this comment.
I think these functions should take domTree.documentFragment and domTree.anchor into account
There was a problem hiding this comment.
domTree.anchor yes, but as noted in your previous comment, documentFragments get flattened.
| } | ||
|
|
||
| const glue = buildCommon.makeGlue( | ||
| spacings[left][right], glueOptions); |
There was a problem hiding this comment.
Is using spacings[left][right] instead of space an intended behavior?
There was a problem hiding this comment.
Yes. Thanks for catching this. It's obvious that we don't have any tests for the "tight" situation.


This is the first step towards creating an intermediate representation
that can be used to generate HTML, SVG, and Canvas commands for rendering.
By generating spans that contain the width of the spaces instead of
relying on CSS sibling rules we'll be able to one day replaces the spans
with intermeidate 'Glue' nodes (in a later PR). This was described in #376 (comment).
An added benefit of this approach is that is enables us to programmatically
change the values for thinspace, mediumspace, and thickspace which will
allow us to implement the \setlength command.