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

Glimmer: fix newlines rendering in non-mustache attributes #8667

Closed
wants to merge 27 commits into from

Conversation

kangax
Copy link
Contributor

@kangax kangax commented Jun 29, 2020

Fixes #8648

It appears that hardline "primitive" can't be rendered within an attribute in Prettier and results in \n output as [object Object].

When working on a fix for this, I found out that Glimmer parser treats attribute values with mustaches differently: they're a ConcatStatement rather than a single TextNode if the attribute value is a plain string.

This PR changes TextNode output behavior to only trim it if it's the only node within an attribute (as opposed to ConcatStatement).

  • I’ve added tests to confirm my change works.
  • (If the change is user-facing) I’ve added my changes to changelog_unreleased/*/pr-XXXX.md file following changelog_unreleased/TEMPLATE.md.
  • I’ve read the contributing guidelines.

Try the playground for this PR

@kangax
Copy link
Contributor Author

kangax commented Jun 29, 2020

@dcyriller I saw you were an author of this part of code. Let me know what you think! My first PR to prettier so might be rough around the edges :)

@fisker
Copy link
Member

fisker commented Jun 30, 2020

We don't trim attribute in html

Prettier pr-8667
Playground link

--parser html

Input:

<a title="
attr
"></a>

Output:

<a
  title="
attr
"
></a>

// they're a ConcatStatement rather than a single TextNode
// if the attribute value is a plain string, we only trim it and avoid inserting hardlines
// since they're not rendered correctly within attributes
if (isParentOfSomeType(path, ["AttrNode"])) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This conditionnal will match <htmlElement attribute="textNode">'s TextNode but not <htmlElement attribute="textNodeInConcatStatement {{mustache}}">'s.

I would suggest to use instead:

Suggested change
if (isParentOfSomeType(path, ["AttrNode"])) {
if (path.stack.includes("attributes")) {

to match both.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If you apply this suggestion, you'll notice that you can return return concat([trimmedValue]); from line 234 and get rid of the big else statement.

Doing that, we'd split the TextNode printer in two: one for TextNodes nested in attributes, one for other TextNodes. We'd be able later to print differently attributes whether they be class / style / srcset / other.

Copy link
Contributor Author

@kangax kangax Jun 30, 2020

Choose a reason for hiding this comment

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

This conditionnal will match <htmlElement attribute="textNode">'s TextNode but not <htmlElement attribute="textNodeInConcatStatement {{mustache}}">'s.

Correct and that's the whole point — presence of MustacheStatement makes it ConcatStatement which is handled properly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is handled correctly in the sense it doesn’t have the bug. But a TextNode in a ConcatStatement should probably be formatted no differently than a simple TextNode (ie. trimmed / not trimmed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dcyriller just tried your suggestion and remembered why I didn't do that. It breaks mustache handling in attributes:

    <div
    -   class='a-first-class
    -   {{if this.optionOne 'optionOne'}}
    -
    -    {{if this.optionTwo 'optionTwo'}}
    -
    -    {{if this.optionThree 'optionThree'}}
    -
    -    {{if this.optionFour 'optionFour'}}
    -
    -    {{if this.optionFive 'optionFive'}}
    -
    -    {{this.class}}'
    +   class='a-first-class {{if this.optionOne 'optionOne'}} {{
    +     if this.optionTwo 'optionTwo'
    +   }} {{if this.optionThree 'optionThree'}} {{
    +     if this.optionFour 'optionFour'
    +   }} {{if this.optionFive 'optionFive'}} {{this.class}}'
        ...attributes
      >
      </div>

Copy link
Collaborator

Choose a reason for hiding this comment

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

This ☝️ would actually be an improvement - if applied to only class names.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is something I wanted to discuss with you too. In #6207 they ask to format classes with newlines (but not extra newlines) whereas I feel like ☝️ option is not bad. We could consider that ticket an improvement or bring it up for more discussion.

@dcyriller
Copy link
Collaborator

@fisker handlebars printer for attributes is not yet splitted: we could rewrite whitespaces in class and style attributes, print scrset in a custom way and do not process other attributes. This is what html printer does? What do you think?

@fisker
Copy link
Member

fisker commented Jun 30, 2020

@dcyriller I'm not familiar with handlebars, html didn't trim attribute, I think we should not do trim in handlebars either? And yes, html format style class and srcset.

@kangax
Copy link
Contributor Author

kangax commented Jun 30, 2020

@fisker @dcyriller Ah, I missed the fact that Prettier treats attribute values differently depending on an attribute. I initially skipped trimming entirely but then noticed a failing glimmer test that checked for class=" smth " being trimmed to class="smth". I then saw in the playground that class attribute does indeed get trimmed in, at least, html printer.

Looking at something like this:

<input
class="
class"
style="
foo:bar"
srcset="
srcset"
title="
title"
alt="
alt"
unspecified="
unspecified"
/>

I see that "html" printer only trims class and style?

<input
  class="class"
  style="foo: bar;"
  srcset="
srcset"
  title="
title"
  alt="
alt"
  unspecified="
unspecified"
/>

JSX printer seems to leave everything untouched:

<input
  class="
class"
  style="
foo:bar"
  srcset="
srcset"
  title="
title"
  alt="
alt"
  unspecified="
unspecified"
/>;

I can skip trimming for now entirely, so that we get consistent behavior and fix this bug, then work on splitting printer behavior depending on an attribute?

@fisker
Copy link
Member

fisker commented Jun 30, 2020

srcset has own parser

Prettier 2.0.5
Playground link

--parser html

Input:

<img
class="
class"
style="
foo:bar"
srcset="
srcset------------------------------------------------------------ 1x,
        srcset_2  2x"
title="
title"
alt="
alt"
unspecified="
unspecified"
/>

Output:

<img
  class="class"
  style="foo: bar;"
  srcset="
    srcset------------------------------------------------------------ 1x,
    srcset_2                                                           2x
  "
  title="
title"
  alt="
alt"
  unspecified="
unspecified"
/>

@kangax
Copy link
Contributor Author

kangax commented Jun 30, 2020

We can look at cleanAttributeValue from my html-minifier (haven't worked on it in a while, forgot the rules) for some inspiration :) https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L257-L328

@kangax
Copy link
Contributor Author

kangax commented Jul 13, 2020

@dcyriller would you like me to wait until you merge your PR and then we rebase this one on top? or should we merge this as is and then follow up with yours?

.replace(/^\s+/g, leadingSpace)
.replace(/\s+$/, trailingSpace);

// Glimmer parser treats attribute values with mustaches differently
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment might removed - it doesn't explain how prettier printer works (but the undelying ast).

@@ -406,7 +406,6 @@ sometimes{{nogaps}}areimportant<Hello></Hello>
<div class="a list of classes">

</div>
Copy link
Collaborator

Choose a reason for hiding this comment

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

This change might be reverted

@dcyriller
Copy link
Collaborator

dcyriller commented Jul 16, 2020

@dcyriller would you like me to wait until you merge your PR and then we rebase this one on top? or should we merge this as is and then follow up with yours?

@fisker what do you think?

IMHO if this is merged, I can rebase and add more tests on #8677 (as the bug would be fixed)

@fisker
Copy link
Member

fisker commented Jul 16, 2020

@dcyriller This PR prints trimmed attribute values, it's not okay right? So we can't merge this one.

@dcyriller
Copy link
Collaborator

The other one - to format only class names - is ready 👍.

@kangax
Copy link
Contributor Author

kangax commented Jul 17, 2020

@dcyriller ok, waiting for yours to merge before we go ahead with this one

@Alonski
Copy link
Contributor

Alonski commented Jul 29, 2020

@kangax This was merged :)
#8677

@kangax
Copy link
Contributor Author

kangax commented Aug 3, 2020

@Alonski @dcyriller any idea why the tests are failing on CI? everything passes locally, ran yarn test -u and so on

@fisker
Copy link
Member

fisker commented Aug 3, 2020

@kangax the test fails because you changed the AST, need fix it in clean, to test locally, AST_COMPARE=1 yarn test

@@ -6,7 +6,7 @@ module.exports = function (ast, newNode) {

// (Glimmer/HTML) ignore TextNode whitespace
if (ast.type === "TextNode") {
const trimmed = ast.chars.trim();
const trimmed = ast.chars.trim().replace(/\s+/g, " ");
Copy link
Member

Choose a reason for hiding this comment

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

Please, only do nessary change, should check attribute name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@fisker actually I don't quite understand why we need to do this. Could you explain? Why are we "cleaning" AST and when does this happen? Also, why does the test fail now if we only changed Prettier output and not the underlying AST? Checking for attribute==class will involve some duplication from Glimmer's printer which I'd rather avoid. I'm also not sure if this place is performance critical?

Copy link
Member

Choose a reason for hiding this comment

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

This only runs when comparing the AST during test, so performance is not very important, but make sure the formated / original code have the same AST is very important.

Copy link
Member

Choose a reason for hiding this comment

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

Also, why does the test fail now if we only changed Prettier output and not the underlying AST?

You changed the output, it parse to different ASTs.

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 not familiar with glimmer AST , but I checked failed tests on CI , the attribute with TextNode have "name": "class", can we use it ?.

Base automatically changed from master to main January 23, 2021 17:13
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handlebars: newline in an attribute incorrectly rendered as [object Object]
5 participants