Skip to content

Use JS for spacing between atoms instead of CSS#1070

Merged
k4b7 merged 14 commits intomasterfrom
js_spacing
Jan 25, 2018
Merged

Use JS for spacing between atoms instead of CSS#1070
k4b7 merged 14 commits intomasterfrom
js_spacing

Conversation

@k4b7
Copy link
Member

@k4b7 k4b7 commented Jan 16, 2018

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.

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
@k4b7
Copy link
Member Author

k4b7 commented Jan 16, 2018

There are some failing tests for this change that I have to work through, in particular:

BinCancellation BoldSymbol Not OperatorName OverUnderset SizingBaseline StrikeThrough StrikeThroughColor

@k4b7
Copy link
Member Author

k4b7 commented Jan 21, 2018

Somethings have improved in the screenshots:

  • spaces between operators and strike through items is closer to what LaTeX does

Somethings are worse:

  • \not is in the wrong position when followed by a group
  • spaces around + in SizingBaseline now depend on the sizing, but shouldn't
  • spaces around dot operators in StyleSwitchings should be symmetric but aren't

Somethings are neutral:

  • changes to OverUnderset are minimal

@k4b7
Copy link
Member Author

k4b7 commented Jan 22, 2018

not

\not is now looking right on for Chrome and Firefox. We need to change the glyph to a non-combining character to fix Safari.

@k4b7
Copy link
Member Author

k4b7 commented Jan 22, 2018

sizingbaseline

SizingBaseline seems to be better too, but not perfect.

@k4b7
Copy link
Member Author

k4b7 commented Jan 22, 2018

All tests are passing now, but there's still a bit of cleanup work that I'd like to do first. In particular:

  • extract helper function for generating glue
  • do the TODO for handling styling commands the same as sizing commands in buildExpression

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.
Copy link
Member Author

Choose a reason for hiding this comment

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

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`;
Copy link
Member Author

Choose a reason for hiding this comment

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

Should grab the width metric for this character from fontMetricsData.js.

}
}

// Process \\not commands within the group.
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm happy how this became a lot simpler.

calculateSize(html.spacings[lastChildType]["mclose"], options);
glue.style.marginRight = `${dimension}em`;
inner.push(glue);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

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.

@k4b7
Copy link
Member Author

k4b7 commented Jan 22, 2018

This is ready for review now.

@k4b7
Copy link
Member Author

k4b7 commented Jan 22, 2018

@kohler if you have time, could you look at this PR since you did original work to fix the spacing?

Copy link
Member

@ry-randall ry-randall left a comment

Choose a reason for hiding this comment

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

Love that this is the first step towards an IR

src/buildHTML.js Outdated

const thinspace = {
number: 3,
unit: "mu",
Copy link
Member

Choose a reason for hiding this comment

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

What was the reasoning for switching from em to mu? Although, looking at whole numbers is definitely cleaner.

Copy link
Member Author

Choose a reason for hiding this comment

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

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 => {
Copy link
Member

Choose a reason for hiding this comment

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

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 = [];
Copy link
Member

Choose a reason for hiding this comment

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

What was the reasoning for the rename?

Copy link
Member Author

Choose a reason for hiding this comment

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

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);
Copy link
Member

Choose a reason for hiding this comment

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

👍

@thickspace: 0.27778em; // 5mu

// These spacings apply in textstyle and displaystyle.
.mord {
Copy link
Member

Choose a reason for hiding this comment

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

❤️ Love that this is gone

}

// Process \\not commands within the group.
// TODO(kevinb): Handle multiple \\not commands in a row.
Copy link
Member

Choose a reason for hiding this comment

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

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
Copy link
Member

Choose a reason for hiding this comment

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

Same as above

Copy link
Member Author

Choose a reason for hiding this comment

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

See the new screenshots for Not. :)

src/buildHTML.js Outdated
unit: "mu",
};

export const spacings = {
Copy link
Member

Choose a reason for hiding this comment

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

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 =
Copy link
Member

Choose a reason for hiding this comment

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

The comment above indicates that this is spacing, but the variable name is nonSpaces. Is that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

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

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 = {
Copy link
Member

Choose a reason for hiding this comment

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

What's the reasoning for distinguishing these from spacings?

Copy link
Member Author

Choose a reason for hiding this comment

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

The spacings are different when we're in \scriptstyle or \scriptscriptstyle. I'm open to suggestions of other ways to organize these alternate values.

@k4b7
Copy link
Member Author

k4b7 commented Jan 23, 2018

@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");
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

@k4b7 k4b7 Jan 23, 2018

Choose a reason for hiding this comment

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

Good catch. I'm guessing that this diff breaks \href spacing which means we should probably have a test for it.

Copy link
Member Author

@k4b7 k4b7 Jan 23, 2018

Choose a reason for hiding this comment

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

I should probably update getTypeOfDomTree to be able to get the class of the rightmost or leftmost element in the tree.

Copy link
Member Author

Choose a reason for hiding this comment

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

@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.

@k4b7
Copy link
Member Author

k4b7 commented Jan 24, 2018

@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.
Copy link
Member

@ylemkimon ylemkimon Jan 24, 2018

Choose a reason for hiding this comment

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

I think we can just return new buildCommon.makeAnchor(href, [], elements, options) in makeAnchor()

Copy link
Member Author

Choose a reason for hiding this comment

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

I was confused at first, but this makes sense given your comments in getTypeOfDomTree.

Copy link
Member

Choose a reason for hiding this comment

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

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) {
Copy link
Member

@ylemkimon ylemkimon Jan 24, 2018

Choose a reason for hiding this comment

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

and get leftmost or rightmost children type for node instanceof domTree.anchor too.

Copy link
Member

Choose a reason for hiding this comment

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

Also I think we can define a special class name for span that encloses elements without affecting spacings.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call.

Copy link
Member Author

Choose a reason for hiding this comment

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

I ran into some issues with spans so I'm going to leave them out for now.

Copy link
Collaborator

@kohler kohler left a comment

Choose a reason for hiding this comment

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

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

“when determining”

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`
Copy link
Collaborator

Choose a reason for hiding this comment

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

“fragment”

@k4b7
Copy link
Member Author

k4b7 commented Jan 24, 2018

@kohler thanks!

I'll fix all the nits before merging.

@ry-randall
Copy link
Member

ry-randall commented Jan 25, 2018

@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
Copy link
Member

Choose a reason for hiding this comment

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

Cool, didn't know this!

Copy link
Member

@ry-randall ry-randall left a comment

Choose a reason for hiding this comment

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

LGTM

@k4b7
Copy link
Member Author

k4b7 commented Jan 25, 2018

Thanks everyone for reviewing this. This should make #687 much more tractable now.

@k4b7 k4b7 deleted the js_spacing branch January 25, 2018 04:09
@ry-randall
Copy link
Member

@kevinbarabash Thanks for implementing. Would extracting these: https://github.com/Khan/KaTeX/blob/master/static/katex.less#L217 follow a similar pattern?

@k4b7
Copy link
Member Author

k4b7 commented Jan 25, 2018

@rrandallcainc yup. Anywhere where we create a span using one of those styles could be replaced with a call to makeGlue.

// 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) {
Copy link
Member

Choose a reason for hiding this comment

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

Can the node be a domTree.documentFragment? Aren't they flattened at the start of buildExpression?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch.

Copy link
Member

Choose a reason for hiding this comment

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

@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)) {
Copy link
Member

Choose a reason for hiding this comment

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

I think these functions should take domTree.documentFragment and domTree.anchor into account

Copy link
Member Author

Choose a reason for hiding this comment

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

domTree.anchor yes, but as noted in your previous comment, documentFragments get flattened.

}

const glue = buildCommon.makeGlue(
spacings[left][right], glueOptions);
Copy link
Member

Choose a reason for hiding this comment

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

Is using spacings[left][right] instead of space an intended behavior?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. Thanks for catching this. It's obvious that we don't have any tests for the "tight" situation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Tracking in #1102.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants