diff --git a/src/__tests__/__snapshots__/attributes-transformation.test.js.snap b/src/__tests__/__snapshots__/attributes-transformation.test.js.snap index 16e0245..7d61f30 100644 --- a/src/__tests__/__snapshots__/attributes-transformation.test.js.snap +++ b/src/__tests__/__snapshots__/attributes-transformation.test.js.snap @@ -10,10 +10,10 @@ module.exports = (
-
+
-
+
-
-
-
-
-
+
+
+
+
+
@@ -54,7 +51,7 @@ exports[`html output: generated html 1`] = ` className="one two" />
`; -exports[`static html output: static html 1`] = `"
"`; +exports[`static html output: static html 1`] = `"
"`; diff --git a/src/__tests__/__snapshots__/option-attribute-alias.test.js.snap b/src/__tests__/__snapshots__/option-attribute-alias.test.js.snap new file mode 100644 index 0000000..e0c682c --- /dev/null +++ b/src/__tests__/__snapshots__/option-attribute-alias.test.js.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Expect an error to be thrown 1`] = ` +"/src/__tests__/option-attribute-alias.input.js:13 + 11| p.b1.b2(class=\\"b3 b4\\") + 12| p.c1(className=\\"c2 c3\\") + > 13| p.d1(class=classes) + 14| p.e1(class=['e2', 'e3']) + 15| p.f1(class=['f2', ...classesArray]) + 16| p(class=$.Red) + +We can't use expressions in shorthands, use \\"className\\" instead of \\"class\\"" +`; + +exports[`JavaScript output: transformed source code 1`] = ` +"const $ = { + Red: \\"color-red\\" +}; +const classes = \\"d2 d3\\"; +const classesArray = [\\"f3\\", $.Red]; +const showK = true; +const showL = false; + +const handleClick = () => {}; + +const svgGroup = \\"\\"; +module.exports = ( + +); +" +`; + +exports[`html output: generated html 1`] = ` + +`; + +exports[`static html output: static html 1`] = `"

"`; diff --git a/src/__tests__/__snapshots__/option-class-attribute.test.js.snap b/src/__tests__/__snapshots__/option-class-attribute.test.js.snap index 67af569..e78e9b9 100644 --- a/src/__tests__/__snapshots__/option-class-attribute.test.js.snap +++ b/src/__tests__/__snapshots__/option-class-attribute.test.js.snap @@ -16,7 +16,7 @@ module.exports = ( return (

); })} diff --git a/src/__tests__/option-attribute-alias.input.js b/src/__tests__/option-attribute-alias.input.js new file mode 100644 index 0000000..3f3fffe --- /dev/null +++ b/src/__tests__/option-attribute-alias.input.js @@ -0,0 +1,27 @@ +const $ = {Red: 'color-red'}; +const classes = 'd2 d3'; +const classesArray = ['f3', $.Red]; +const showK = true; +const showL = false; +const handleClick = () => {}; +const svgGroup = ''; + +module.exports = pug` + div.a1 + p.b1.b2(class="b3 b4") + p.c1(className="c2 c3") + p.d1(class=classes) + p.e1(class=['e2', 'e3']) + p.f1(class=['f2', ...classesArray]) + p(class=$.Red) + p(class=${`g1 ${$.Red}`}) + p.i1(class=${`i2 ${$.Red}`}) + p.j1( + class="j2 j3", + className="j4 j5") + p(class=(showK && "k1")) + p.l1(class=(showL ? "l2" : "")) + a.m1(@click=handleClick) + svg.n1(@html={ __html: "" }) + svg.o1(@html={ __html: svgGroup }) +`; diff --git a/src/__tests__/option-attribute-alias.test.js b/src/__tests__/option-attribute-alias.test.js new file mode 100644 index 0000000..4b33573 --- /dev/null +++ b/src/__tests__/option-attribute-alias.test.js @@ -0,0 +1,11 @@ +import testHelper, {testCompileError} from './test-helper'; + +testCompileError(__dirname + '/option-attribute-alias.input.js'); + +testHelper(__dirname + '/option-attribute-alias.input.js', { + attributeAlias: { + class: 'className', + '@click': 'onClick', + '@html': 'dangerouslySetInnerHTML', + }, +}); diff --git a/src/context.js b/src/context.js index 1d62807..2c223c3 100644 --- a/src/context.js +++ b/src/context.js @@ -15,6 +15,9 @@ type Variable = { type Options = { classAttribute: string, + attributeAlias: { + [string]: string, + }, }; class Context { diff --git a/src/index.js b/src/index.js index 550f80b..8b37c04 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ import {setBabelTypes} from './lib/babel-types'; const DEFAULT_OPTIONS = { classAttribute: 'className', + attributeAlias: {}, }; export default function(babel) { diff --git a/src/utils/get-class-name-value.js b/src/utils/get-class-name-value.js index 3b8f597..4816a20 100644 --- a/src/utils/get-class-name-value.js +++ b/src/utils/get-class-name-value.js @@ -2,92 +2,136 @@ import t from '../lib/babel-types'; -function getPlainShorthandValue(classes: Array): string | null { - if (classes.length) { - return classes - .map(item => item.value) - .filter(Boolean) - .join(' '); - } +function isParsableLiteral(expr: Expression): boolean { + return ( + t.isStringLiteral(expr) || + t.isNumericLiteral(expr) || + t.isBooleanLiteral(expr) || + t.isNullLiteral(expr) + ); +} - return null; +function flattenAndFilterAttributeExpressions( + classes: Array, +): Array { + return [].concat( + ...classes.map(item => { + if (t.isArrayExpression(item)) { + /*:: item = ((item: any): ArrayExpression) */ + return (item.elements: any).filter( + item => item && !t.isSpreadElement(item), + ); + } else { + return item; + } + }), + ); } -function getPlainClassNameValue( - classes: Array, -): string | ArrayExpression | CallExpression | null | Array { - if (classes.every(item => t.isStringLiteral(item))) { - return classes - .map(item => item.value) - .filter(Boolean) - .join(' '); - } +function combineParsableLiteralsAndExpressions( + classes: Array, +): Array | Array> { + const literalOfClasses = classes.map(item => isParsableLiteral(item)); - if (classes.every(item => t.isArrayExpression(item))) { - return classes.reduce((all, item) => all.concat(item.elements), []); + const result = []; + const len = classes.length; + + let i = 0; + let lookLiteral = true; + + while (i < len) { + const nextDifferentIndex = literalOfClasses.indexOf(!lookLiteral, i); + const start = i; + const end = nextDifferentIndex < 0 ? len : nextDifferentIndex; + + result.push(classes.slice(start, end)); + + lookLiteral = !lookLiteral; + i = end; } - if (Array.isArray(classes)) { - return classes[0]; + return result; +} + +function getValueOfLiterals(literals: Array): string { + if (literals.length < 1) { + return ''; } - return null; + return literals + .map(item => (item: any).value) + .filter(Boolean) + .join(' '); } -function mergeStringWithClassName( - shorthand: string | null, - attribute: string | ArrayExpression | CallExpression | null | Array, -) { - // There are several branches: - // - when attribute exists - // - when shorthand only exists - // - otherwise - - if (attribute) { - if (typeof attribute === 'string') { - if (shorthand) { - return t.stringLiteral(shorthand + ' ' + attribute); - } - return t.stringLiteral(attribute); - } +function getMergedJSXExpression( + classes: Array, +): StringLiteral | JSXExpressionContainer { + const combined = combineParsableLiteralsAndExpressions(classes); - if (Array.isArray(attribute)) { - if (shorthand) { - return t.jSXExpressionContainer( - t.arrayExpression([t.stringLiteral(shorthand)].concat(attribute)), - ); - } - return t.jSXExpressionContainer(t.arrayExpression(attribute)); - } + if (combined.length === 1) { + const value = getValueOfLiterals((combined[0]: any)); + return t.stringLiteral(value); + } - if (shorthand) { - return t.jSXExpressionContainer( - t.binaryExpression('+', t.stringLiteral(shorthand + ' '), attribute), - ); + if (combined.length === 2) { + if (combined[0].length === 0 && combined[1].length === 1) { + return t.jSXExpressionContainer(combined[1][0]); } + } - return t.jSXExpressionContainer(attribute); + // Keep combined items in odd + if (combined.length % 2 === 0) { + combined.push(([]: Array)); } - if (shorthand) { - if (typeof shorthand === 'string') { - return t.stringLiteral(shorthand); + const quasis = []; + const expressions = []; + const len = combined.length; + + for (let i = 0; i < len; ++i) { + let items = combined[i]; + const itemsLen = items.length; + const isQuasi = i % 2 === 0; + const isFirst = i === 0; + const isLast = len - i <= 1; + + if (isQuasi) { + /*:: items = ((items: any): Array) */ + const value = getValueOfLiterals(items); + const raw = value + ? (isFirst ? '' : ' ') + value + (isLast ? '' : ' ') + : ''; + const cooked = raw; + + quasis.push(t.templateElement({raw, cooked}, isLast)); + } else { + expressions.push(items[0]); + + if (itemsLen > 1) { + for (let j = 1; j < itemsLen; ++j) { + const raw = ' '; + const cooked = ' '; + quasis.push(t.templateElement({raw, cooked}, false)); + expressions.push(items[j]); + } + } } - - return t.jSXExpressionContainer(shorthand); } - return null; + return t.jSXExpressionContainer(t.templateLiteral(quasis, expressions)); } function getClassNameValue( classesViaShorthand: Array, - classesViaAttribute: Array, + classesViaAttribute: Array, ): any { - const shorthandValue = getPlainShorthandValue(classesViaShorthand); - const attributeValue = getPlainClassNameValue(classesViaAttribute); + const attrs = flattenAndFilterAttributeExpressions([ + ...classesViaShorthand, + ...classesViaAttribute, + ]); - return mergeStringWithClassName(shorthandValue, attributeValue); + return getMergedJSXExpression(attrs); } export default getClassNameValue; diff --git a/src/visitors/Tag.js b/src/visitors/Tag.js index e2ccb11..3d56f9e 100644 --- a/src/visitors/Tag.js +++ b/src/visitors/Tag.js @@ -45,21 +45,15 @@ function getAttributes(node: Object, context: Context): Array { const attrs: Array = node.attrs .map( ({name, val, mustEscape}: PugAttribute): Attribute | null => { - if (/\.\.\./.test(name) && val === true) { + if (/^\.\.\./.test(name)) { + if (!val) { + throw new Error('spread attributes must not have a value'); + } return t.jSXSpreadAttribute(parseExpression(name.substr(3), context)); } - // TODO: Need to drop all aliases for attributes - switch (name) { - case 'for': - name = 'htmlFor'; - break; - case 'maxlength': - name = 'maxLength'; - break; - } - const expr = parseExpression(String(val), context); + const attrName = context._options.attributeAlias[name] || name; if (!mustEscape) { const canSkipEscaping = @@ -77,7 +71,7 @@ function getAttributes(node: Object, context: Context): Array { return null; } - if (name === 'class') { + if (attrName === 'class') { if (!t.isStringLiteral(expr)) { throw context.error( 'INVALID_EXPRESSION', @@ -87,11 +81,13 @@ function getAttributes(node: Object, context: Context): Array { ); } - classesViaShorthand.push(expr); - return null; + if (!context._options.attributeAlias[name]) { + classesViaShorthand.push(expr); + return null; + } } - if (name === context._options.classAttribute) { + if (attrName === context._options.classAttribute) { classesViaAttribute.push(expr); return null; } @@ -101,11 +97,7 @@ function getAttributes(node: Object, context: Context): Array { t.asJSXElement(expr) || t.jSXExpressionContainer(expr); - if (/\.\.\./.test(name)) { - throw new Error('spread attributes must not have a value'); - } - - return t.jSXAttribute(t.jSXIdentifier(name), jsxValue); + return t.jSXAttribute(t.jSXIdentifier(attrName), jsxValue); }, ) .filter(Boolean); @@ -137,6 +129,7 @@ function getAttributesAndChildren( } { const children = getChildren(node, context); + // TODO Implement node.attributeBlocks if (node.attributeBlocks.length) { throw new Error('Attribute blocks are not yet supported in react-pug'); }