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
Changes from 9 commits
44a69b1
71b2f72
1daa348
4d82ce9
b7f9ac3
b2cf339
6e910f5
0eaaaf8
dfd9315
bb616d9
efb0089
5f9c761
59f3f7f
ecc69e9
32e4f29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,10 @@ | |
[include] | ||
|
||
[libs] | ||
|
||
|
||
./scripts/babel-nodes.js | ||
|
||
[lints] | ||
|
||
[options] | ||
|
||
[strict] |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`JavaScript output: transformed source code 1`] = ` | ||
"// To prevent warnings in console from react | ||
const test = 10; | ||
|
||
module.exports = <div data-first={true} data-second={true} data-positive={true} data-negative={false} data-check={true}><div data-one={true} data-two={true} /></div>;" | ||
`; | ||
|
||
exports[`html output: generated html 1`] = ` | ||
<div | ||
data-check={true} | ||
data-first={true} | ||
data-negative={false} | ||
data-positive={true} | ||
data-second={true} | ||
> | ||
<div | ||
data-one={true} | ||
data-two={true} | ||
/> | ||
</div> | ||
`; | ||
|
||
exports[`static html output: static html 1`] = `"<div data-first=\\"true\\" data-second=\\"true\\" data-positive=\\"true\\" data-negative=\\"false\\" data-check=\\"true\\"><div data-one=\\"true\\" data-two=\\"true\\"></div></div>"`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// To prevent warnings in console from react | ||
const test = 10; | ||
|
||
module.exports = pug` | ||
div( | ||
data-first | ||
data-second | ||
data-positive=true | ||
data-negative=false | ||
data-check | ||
) | ||
div(data-one data-two) | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import testHelper from './test-helper'; | ||
|
||
testHelper(__dirname + '/attributes-shorthand.input.js'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import React from 'react'; | ||
import {transform} from 'babel-core'; | ||
import renderer from 'react-test-renderer'; | ||
import transformReactPug from '../'; | ||
|
||
const transformationOptions = { | ||
babelrc: false, | ||
plugins: [transformReactPug], | ||
}; | ||
|
||
const transformer = code => { | ||
return transform(`pug\`${code}\``, transformationOptions).code; | ||
}; | ||
|
||
const ExpectedError = /Unescaped attributes/; | ||
|
||
test('throws error when pass string', () => { | ||
const wrapped = () => | ||
transformer(` | ||
div(name!="hello") | ||
`); | ||
|
||
expect(wrapped).toThrowError(ExpectedError); | ||
}); | ||
|
||
test('throws error when pass variable', () => { | ||
const wrapped = () => | ||
transformer(` | ||
- const variable = 'value' | ||
div(name!=variable.toString()) | ||
`); | ||
|
||
expect(wrapped).toThrowError(ExpectedError); | ||
}); | ||
|
||
test('does not throw error when pass variable or just string', () => { | ||
const wrapped = () => | ||
transformer(` | ||
- const variable = 'value' | ||
div#id.class( | ||
data-string="hello" data-variable=variable | ||
) | ||
div(class=['one', 'two']) | ||
`); | ||
|
||
expect(wrapped).not.toThrowError(ExpectedError); | ||
}); | ||
|
||
test('does not throw error when pass boolean variables', () => { | ||
const wrapped = () => | ||
transformer(` | ||
div(data-first data-second data-third) | ||
`); | ||
|
||
expect(wrapped).not.toThrowError(ExpectedError); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,14 @@ import t from '../babel-types'; | |
import {visitJsx, visitJsxExpressions} from '../visitors'; | ||
import {getInterpolationRefs} from '../utils/interpolation'; | ||
|
||
type PugAttribute = { | ||
name: string, | ||
val: string, | ||
mustEscape: boolean, | ||
}; | ||
|
||
type Attribute = JSXAttribute | JSXSpreadAttribute; | ||
|
||
/** | ||
* Get children nodes from the node, passing the node's | ||
* context to the children and generating JSX values. | ||
|
@@ -27,15 +35,22 @@ function getChildren(node: Object, context: Context): Array<JSXValue> { | |
* them into JSX attributes. | ||
* @param {Object} node - The node | ||
* @param {Context} context - The context | ||
* @returns {Array<JSXAttribute|JSXSpreadAttribute>} | ||
* @returns {Array<Attribute>} | ||
*/ | ||
function getAttributes( | ||
node: Object, | ||
context: Context, | ||
): Array<JSXAttribute | JSXSpreadAttribute> { | ||
const classes = []; | ||
const attrs = node.attrs | ||
.map(({name, val, mustEscape}) => { | ||
function getAttributes(node: Object, context: Context): Array<Attribute> { | ||
const classes: Array<Object> = []; | ||
const attrs: Array<Attribute> = node.attrs | ||
.map((node: PugAttribute): PugAttribute => { | ||
if (node.val === true) { | ||
return { | ||
...node, | ||
mustEscape: false, | ||
}; | ||
} | ||
|
||
return node; | ||
}) | ||
.map(({name, val, mustEscape}: PugAttribute): Attribute | null => { | ||
if (/\.\.\./.test(name) && val === true) { | ||
return t.jSXSpreadAttribute(parseExpression(name.substr(3), context)); | ||
} | ||
|
@@ -54,8 +69,19 @@ function getAttributes( | |
|
||
const expr = parseExpression(val === true ? 'true' : val, context); | ||
|
||
if (!mustEscape && (!t.isStringLiteral(expr) || /(\<\>\&)/.test(val))) { | ||
throw new Error('Unescaped attributes are not supported in react-pug'); | ||
if (!mustEscape) { | ||
const isStringViaAliases: boolean = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You shouldn't need to explicitly add the |
||
t.isStringLiteral(expr) && !['className', 'id'].includes(name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a list of two, I think I'd prefer just |
||
|
||
const isNotStringOrBoolean: boolean = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
!t.isStringLiteral(expr) && !t.isBooleanLiteral(expr); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 const isAllowedUnescapedAttribute = (
t.isBooleanLiteral(expr) ||
((name === 'className' || name === 'id') && t.isStringLiteral(expr))
);
if (!isAllowedUnescapedAttribute) { There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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:
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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, although you've dropped the |
||
|
||
if (isStringViaAliases || isNotStringOrBoolean) { | ||
throw context.error( | ||
'INVALID_EXPRESSION', | ||
'Unescaped attributes are not supported in react-pug', | ||
); | ||
} | ||
} | ||
|
||
if (expr == null) { | ||
|
@@ -79,20 +105,22 @@ function getAttributes( | |
return t.jSXAttribute(t.jSXIdentifier(name), jsxValue); | ||
}) | ||
.filter(Boolean); | ||
|
||
if (classes.length) { | ||
const value = classes.every(cls => t.isStringLiteral(cls)) | ||
? t.stringLiteral(classes.map(cls => (cls: any).value).join(' ')) | ||
: t.jSXExpressionContainer( | ||
t.callExpression( | ||
t.memberExpression( | ||
t.arrayExpression(classes), | ||
t.identifier('join'), | ||
t.callExpression( | ||
t.memberExpression( | ||
t.arrayExpression(classes), | ||
t.identifier('join'), | ||
), | ||
[t.stringLiteral(' ')], | ||
), | ||
[t.stringLiteral(' ')], | ||
), | ||
); | ||
); | ||
attrs.push(t.jSXAttribute(t.jSXIdentifier('className'), value)); | ||
} | ||
|
||
return attrs; | ||
} | ||
|
||
|
There was a problem hiding this comment.
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))
?There was a problem hiding this comment.
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 lostg
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.
There was a problem hiding this comment.
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:
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.