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

Handle shorthands in attributes correctly #33

Merged
merged 15 commits into from Mar 20, 2018

Conversation

ezhlobo
Copy link
Member

@ezhlobo ezhlobo commented Feb 12, 2018

It adds supporting of shorthands in attributes. Initial issue #1 (Fails on boolean attributes).

Now following syntax won't throw an error:

div(first second third)

But this one will throw:

div(
  first!="stirng" 
  second!=variable
)

Checklist:

  • Use context.error instead of new Error to keep well-described errors
  • Update pug-lexer and remove workaround to keep mustEscape correct

@ezhlobo
Copy link
Member Author

ezhlobo commented Feb 12, 2018

The main reason why I created this PR is to get feedback about my direction.

@@ -57,8 +67,8 @@ function getAttributes(

const expr = parseExpression(val === true ? 'true' : val, context);

if (!mustEscape && (!t.isStringLiteral(expr) || /(\<\>\&)/.test(val))) {
Copy link
Member

Choose a reason for hiding this comment

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

Why are you removing the (!t.isStringLiteral(expr) || /(\<\>\&)/.test(val))?

Copy link
Member Author

Choose a reason for hiding this comment

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

@ForbesLindesay because it looks impossible to achieve with basic tests. Maybe we can hack it somehow... Otherwise val starts and ends with " when it's a string.

And filtering off <>& looks not obvious for me, why not <script>... or something like that? Probably we lost g flag for regex?

Anyway I'm going to use different conditions. All use cases are captured in tests. Let's discuss something, if you want to add to those tests.

Copy link
Member

Choose a reason for hiding this comment

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

The case this was capturing, although arguably not very important, is:

div(data-whatever!="some string that does not contain any special characters")

The idea being that it might make it slightly easier when converting from html based pug templates to react pug templates. Arguably that might not be super important though.

'use strict';

// To prevent warnings in console from react
const Custom = () => null
Copy link
Member

Choose a reason for hiding this comment

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

You could also render an input(checked) so we can actually see html output in the test.

Copy link
Member Author

@ezhlobo ezhlobo Mar 5, 2018

Choose a reason for hiding this comment

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

@ForbesLindesay I started using data- attributes to keep html output. Let me know if you think that it does not appropriate.

@ForbesLindesay
Copy link
Member

Looks good. Ping me once you've fixed build & made the changes you want to make.

@ezhlobo
Copy link
Member Author

ezhlobo commented Mar 5, 2018

@ForbesLindesay I rewrote my solution completely (left only test cases), need your attention one more time.

Also it looks like I need your help with publishing new version of pug-lexer (after #2956 and #2957). Otherwise we need to support a workaround in our codebase.

throw new Error('Unescaped attributes are not supported in react-pug');
if (!mustEscape) {
const isStringViaAliases: boolean =
t.isStringLiteral(expr) && !['className', 'id'].includes(name);
Copy link
Member

Choose a reason for hiding this comment

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

For a list of two, I think I'd prefer just (name === 'className' || name === 'id') - it takes less time to mentally parse.

if (!mustEscape && (!t.isStringLiteral(expr) || /(\<\>\&)/.test(val))) {
throw new Error('Unescaped attributes are not supported in react-pug');
if (!mustEscape) {
const isStringViaAliases: boolean =
Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't need to explicitly add the : boolean here; flow should infer it from context.

const isStringViaAliases: boolean =
t.isStringLiteral(expr) && !['className', 'id'].includes(name);

const isNotStringOrBoolean: boolean =
Copy link
Member

Choose a reason for hiding this comment

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

ditto

@ForbesLindesay
Copy link
Member

I don't have a strong opinion on whether we should support div(data-whatever!="some string that does not contain any special characters") or not.

Please can you address the other small comments though.

@ezhlobo
Copy link
Member Author

ezhlobo commented Mar 6, 2018

@ForbesLindesay thank you for your review. I addressed all your comments and removed my workaround because there is new version of pug-lexer (that made it for us).

The question about checking tags in scripts is still open. I would suggest to create a separate issue and discuss it there, because anyway it did not work as expected before this PR. Does it work for you? We can actually remember all use cases of unescaped values and think up how to handle them best...

t.isStringLiteral(expr) && name !== 'className' && name !== 'id';

const isNotStringOrBoolean =
!t.isStringLiteral(expr) && !t.isBooleanLiteral(expr);
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 just wondering if this lot can be clarified. It seems like the allowed case is just:

t.isBooleanLiteral(expr) ||
((name === 'className' || name === 'id') && t.isStringLiteral(expr))

It seems simpler written like that to me, rather than having multiple references to isStringLiteral. How about we just use:

const isAllowedUnescapedAttribute = (
  t.isBooleanLiteral(expr) ||
  ((name === 'className' || name === 'id') && t.isStringLiteral(expr))
);
if (!isAllowedUnescapedAttribute) {

Copy link
Member Author

Choose a reason for hiding this comment

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

@ForbesLindesay I agree that my naming can confuse, but it was created with following reason in mind: all not obvious conditions should be commented. Variables are more convenient than just comments, so I used them.

isAllowedUnescapedAttribute sounds not obvious because we don't use such naming through this plugin and pug (I mean it does not respect spirit of mustEscape). It's better to keep shouldBeEscaped, allowedToBeEscaped.

Let me show how did I achieve my code:

When attribute is marked that we can skip its escaping, we should always show a message about unsupported feature. Two exceptions, where we should not show a message:

  1. if attribute is a string not from aliases (not from className and id)
  2. if attribute is not primitive (in pug it's everything besides string and booleans)

If I convert it to the code:

if (!mustEscape) {
  const isStringNotFromAliases = 
    t.isStringLiteral(expr) && name !== 'className' && name !== 'id';

  const isNotPrimitive =
    !t.isStringLiteral(expr) && !t.isBooleanLiteral(expr);

  if (isStringNotFromAliases || isNotPrimitive) {
    throw context.error(
      'INVALID_EXPRESSION',
      'Unescaped attributes are not supported in react-pug',
    );
  }
}

This snippet is what I suggest to leave. But I don't mind to make it much more compact by refactoring javascript, not the logic. If this is the case, then could you suggest any concrete solution that I can use? Maybe you can help me with optimizing the logic?

Copy link
Member

Choose a reason for hiding this comment

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

I'd much rather see a whitelist of cases where it is ok to have unescaped attributes:

  1. className and id if the value is a string literal
  2. any attribute if the value is a boolean

As this is much easier for me to interpret. When I read your code, I have to mentally figure out which are the allowed cases. Since the allowed cases are so narrow, I never really feel like I need to figure out what the non-allowed cases are.

allowedToBeEscaped doesn't make sense because everything can be escaped. mustEscape is fine, but we already have that with a subtly different meaning. I think something like escapingNotRequired or canSkipEscaping makes more sense here.

I agree that variable names are generally preferable to comments, but it's even better if the code makes sense without needing to read the variable names. I think you've ended up with much more complex code here in an effort to give names to things.

Copy link
Member Author

Choose a reason for hiding this comment

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

@ForbesLindesay you are right that understanding whitelist is much better here. But I still need your help with figuring out the right way. Does following code look better:

if (!mustEscape) {
  const canSkipEscaping =
    (name === 'className' || name === 'id') && t.isStringLiteral(expr);

  if (!canSkipEscaping) {
    throw context.error(
      'INVALID_EXPRESSION',
      'Unescaped attributes are not supported in react-pug',
    );
  }
}

I noticed that we should not check is the value boolean or not. We don't allow to write != constructions at all. Could you please take a look at tests in this PR and tell me is everything appropriate there?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, although you've dropped the BooleanLiteral case there. Is that intentional?

@ForbesLindesay
Copy link
Member

I'm not sure if it worked for me or not, I haven't actually made use of that bit anyway.

@ezhlobo
Copy link
Member Author

ezhlobo commented Mar 20, 2018

@ForbesLindesay hey, I pushed updates according to our discussion. Need your attention one more time.

you've dropped the BooleanLiteral case there. Is that intentional?

Yes. Now all boolean attributes come to us with mustEscape: true, so it is not necessary for us.

But the following example will throw an error and it's intentionally:

div(name!=true)

@ForbesLindesay ForbesLindesay merged commit 3fb864e into pugjs:master Mar 20, 2018
@ForbesLindesay
Copy link
Member

Thanks :)

@ezhlobo
Copy link
Member Author

ezhlobo commented Mar 20, 2018

Thank you very much for your time and patience

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.

None yet

2 participants