diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html index 034653a3d0..9c2e17ba24 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html @@ -6,7 +6,7 @@
-
+
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/error.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/expected.html new file mode 100644 index 0000000000..374977ed1a --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/expected.html @@ -0,0 +1,58 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/index.js b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/index.js new file mode 100644 index 0000000000..50ed0122e3 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/index.js @@ -0,0 +1,3 @@ +export const tagName = 'x-foo'; +export { default } from 'x/foo'; +export * from 'x/foo'; \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/modules/x/foo/foo.html b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/modules/x/foo/foo.html new file mode 100644 index 0000000000..b3b8dea008 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/modules/x/foo/foo.html @@ -0,0 +1,29 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/modules/x/foo/foo.js b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/modules/x/foo/foo.js new file mode 100644 index 0000000000..dc92b10396 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/style-class-whitespace/modules/x/foo/foo.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Foo extends LightningElement {} \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/svgs/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/svgs/expected.html index cce82eed18..452511dc85 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/svgs/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/svgs/expected.html @@ -3,8 +3,8 @@ - - + + diff --git a/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/index.spec.js b/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/index.spec.js new file mode 100644 index 0000000000..9b20bf0379 --- /dev/null +++ b/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/index.spec.js @@ -0,0 +1,54 @@ +import { createElement } from 'lwc'; +import Component from 'x/component'; + +describe('style and class whitespace normalization', () => { + it('should normalize style whitespace', async () => { + const elm = createElement('x-component', { is: Component }); + document.body.appendChild(elm); + await Promise.resolve(); + + const actual = [...elm.shadowRoot.querySelectorAll('[style]')].map((elm) => + elm.getAttribute('style') + ); + expect(actual).toEqual([ + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red !important;', + 'color: red;', + 'color: red; background-color: aqua;', + 'color: red; background-color: aqua;', + '--its-a-tab: red;', + '--its-a-tab-and-a-space: red;', + ]); + }); + it('should normalize class whitespace', async () => { + const elm = createElement('x-component', { is: Component }); + document.body.appendChild(elm); + await Promise.resolve(); + + const actual = [...elm.shadowRoot.querySelectorAll('[class]')].map((elm) => + elm.getAttribute('class') + ); + expect(actual).toEqual([ + 'boo', + 'boo', + 'foo bar', + 'foo bar baz', + 'foo bar', + 'foo bar', + 'foo bar', + 'foo bar', + ]); + }); +}); diff --git a/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/x/component/component.html b/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/x/component/component.html new file mode 100644 index 0000000000..b3b8dea008 --- /dev/null +++ b/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/x/component/component.html @@ -0,0 +1,29 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/x/component/component.js b/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/x/component/component.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-karma/test/rendering/style-class-whitespace/x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/class/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/class/expected.js index beb75cafce..8fc54b1237 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/class/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/class/expected.js @@ -3,8 +3,8 @@ import _implicitScopedStylesheets from "./class.scoped.css?scoped=true"; import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; const $fragment1 = parseFragment`
`; const $fragment2 = parseFragment`
`; -const $fragment3 = parseFragment`
`; -const $fragment4 = parseFragment`
`; +const $fragment3 = parseFragment`
`; +const $fragment4 = parseFragment`
`; function tmpl($api, $cmp, $slotset, $ctx) { const { st: api_static_fragment } = $api; return [ diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/class/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/class/expected.js index 3812591a20..05ad7cbeb9 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/class/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/class/expected.js @@ -1,7 +1,7 @@ import _implicitStylesheets from "./class.css"; import _implicitScopedStylesheets from "./class.scoped.css?scoped=true"; import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; -const $fragment1 = parseFragment`
`; +const $fragment1 = parseFragment`
`; function tmpl($api, $cmp, $slotset, $ctx) { const { st: api_static_fragment } = $api; return [api_static_fragment($fragment1, 1)]; diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/actual.html index bd48f90a38..edcab753e3 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/actual.html +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/actual.html @@ -1,3 +1,4 @@ \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/ast.json index 5baf4a7a42..c911073558 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/ast.json +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/ast.json @@ -4,10 +4,10 @@ "location": { "startLine": 1, "startColumn": 1, - "endLine": 3, + "endLine": 4, "endColumn": 12, "start": 0, - "end": 111, + "end": 200, "startTag": { "startLine": 1, "startColumn": 1, @@ -17,12 +17,12 @@ "end": 10 }, "endTag": { - "startLine": 3, + "startLine": 4, "startColumn": 1, - "endLine": 3, + "endLine": 4, "endColumn": 12, - "start": 100, - "end": 111 + "start": 189, + "end": 200 } }, "directives": [], @@ -77,6 +77,57 @@ "directives": [], "listeners": [], "children": [] + }, + { + "type": "Element", + "name": "div", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 3, + "startColumn": 5, + "endLine": 3, + "endColumn": 89, + "start": 104, + "end": 188, + "startTag": { + "startLine": 3, + "startColumn": 5, + "endLine": 3, + "endColumn": 83, + "start": 104, + "end": 182 + }, + "endTag": { + "startLine": 3, + "startColumn": 83, + "endLine": 3, + "endColumn": 89, + "start": 182, + "end": 188 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "style", + "value": { + "type": "Literal", + "value": "background: blue !IMPORTANT; color: red; opacity: 0.5 !IMPORTANT" + }, + "location": { + "startLine": 3, + "startColumn": 10, + "endLine": 3, + "endColumn": 82, + "start": 109, + "end": 181 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [] } ] } diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/expected.js index 2f22338383..cf4c1f0daf 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-important/expected.js @@ -1,10 +1,14 @@ import _implicitStylesheets from "./style-important.css"; import _implicitScopedStylesheets from "./style-important.scoped.css?scoped=true"; import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; -const $fragment1 = parseFragment`
`; +const $fragment1 = parseFragment`
`; +const $fragment2 = parseFragment`
`; function tmpl($api, $cmp, $slotset, $ctx) { const { st: api_static_fragment } = $api; - return [api_static_fragment($fragment1, 1)]; + return [ + api_static_fragment($fragment1, 1), + api_static_fragment($fragment2, 3), + ]; /*LWC compiler vX.X.X*/ } export default registerTemplate(tmpl); diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/actual.html index f6a49725bf..33b57464cd 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/actual.html +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/actual.html @@ -1,4 +1,5 @@ diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/ast.json index 4ff1eb228a..4f82c17414 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/ast.json +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/ast.json @@ -4,10 +4,10 @@ "location": { "startLine": 1, "startColumn": 1, - "endLine": 4, + "endLine": 5, "endColumn": 12, "start": 0, - "end": 178, + "end": 257, "startTag": { "startLine": 1, "startColumn": 1, @@ -17,12 +17,12 @@ "end": 10 }, "endTag": { - "startLine": 4, + "startLine": 5, "startColumn": 1, - "endLine": 4, + "endLine": 5, "endColumn": 12, - "start": 167, - "end": 178 + "start": 246, + "end": 257 } }, "directives": [], @@ -128,6 +128,57 @@ "directives": [], "listeners": [], "children": [] + }, + { + "type": "Element", + "name": "section", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 4, + "startColumn": 5, + "endLine": 4, + "endColumn": 79, + "start": 171, + "end": 245, + "startTag": { + "startLine": 4, + "startColumn": 5, + "endLine": 4, + "endColumn": 69, + "start": 171, + "end": 235 + }, + "endTag": { + "startLine": 4, + "startColumn": 69, + "endLine": 4, + "endColumn": 79, + "start": 235, + "end": 245 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "style", + "value": { + "type": "Literal", + "value": "font-size:\t12px;color:red;margin:10px 5px 10px" + }, + "location": { + "startLine": 4, + "startColumn": 14, + "endLine": 4, + "endColumn": 68, + "start": 180, + "end": 234 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [] } ] } diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/expected.js index 8470ed22b5..3cc947fa80 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/base/style-static/expected.js @@ -1,13 +1,15 @@ import _implicitStylesheets from "./style-static.css"; import _implicitScopedStylesheets from "./style-static.scoped.css?scoped=true"; import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; -const $fragment1 = parseFragment`
`; -const $fragment2 = parseFragment`
`; +const $fragment1 = parseFragment`
`; +const $fragment2 = parseFragment`
`; +const $fragment3 = parseFragment`
`; function tmpl($api, $cmp, $slotset, $ctx) { const { st: api_static_fragment } = $api; return [ api_static_fragment($fragment1, 1), api_static_fragment($fragment2, 3), + api_static_fragment($fragment3, 5), ]; /*LWC compiler vX.X.X*/ } diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-for-each/static-content-optimization/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-for-each/static-content-optimization/expected.js index efad491408..075e43bb21 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-for-each/static-content-optimization/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-for-each/static-content-optimization/expected.js @@ -1,7 +1,7 @@ import _implicitStylesheets from "./static-content-optimization.css"; import _implicitScopedStylesheets from "./static-content-optimization.scoped.css?scoped=true"; import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; -const $fragment1 = parseFragment`${"t6"}${"t8"}
`; +const $fragment1 = parseFragment`${"t6"}${"t8"}`; const stc0 = { key: 0, }; diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/svg/linear-gradient/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/svg/linear-gradient/expected.js index 679c6028e2..6178579622 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/svg/linear-gradient/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/svg/linear-gradient/expected.js @@ -1,7 +1,7 @@ import _implicitStylesheets from "./linear-gradient.css"; import _implicitScopedStylesheets from "./linear-gradient.scoped.css?scoped=true"; import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; -const $fragment1 = parseFragment``; +const $fragment1 = parseFragment``; function tmpl($api, $cmp, $slotset, $ctx) { const { gid: api_scoped_id, diff --git a/packages/@lwc/template-compiler/src/codegen/helpers.ts b/packages/@lwc/template-compiler/src/codegen/helpers.ts index 2badd1b939..47ada2f26a 100644 --- a/packages/@lwc/template-compiler/src/codegen/helpers.ts +++ b/packages/@lwc/template-compiler/src/codegen/helpers.ts @@ -238,6 +238,17 @@ export function parseStyleText(cssText: string): { [name: string]: string } { return styleMap; } +export function normalizeStyleAttribute(style: string): string { + const styleMap = parseStyleText(style); + + const styles = Object.entries(styleMap).map(([key, value]) => { + value = value.replace(IMPORTANT_FLAG, ' !important').trim(); + return `${key}: ${value};`; + }); + + return styles.join(' '); +} + const IMPORTANT_FLAG = /\s*!\s*important\s*$/i; // Given a map of CSS property keys to values, return an array AST like: diff --git a/packages/@lwc/template-compiler/src/codegen/static-element-serializer.ts b/packages/@lwc/template-compiler/src/codegen/static-element-serializer.ts index 977433d6d7..06c22ef612 100644 --- a/packages/@lwc/template-compiler/src/codegen/static-element-serializer.ts +++ b/packages/@lwc/template-compiler/src/codegen/static-element-serializer.ts @@ -21,6 +21,7 @@ import { isText, } from '../shared/ast'; import { hasDynamicText, isContiguousText, transformStaticChildren } from './static-element'; +import { normalizeStyleAttribute } from './helpers'; import type CodeGen from './codegen'; // Implementation based on the parse5 serializer: https://github.com/inikulin/parse5/blob/master/packages/parse5/lib/serializer/index.ts @@ -45,6 +46,10 @@ function templateStringEscape(str: string): string { return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'); } +function normalizeWhitespace(str: string): string { + return str.trim().replace(/\s+/g, ' '); +} + function serializeAttrs(element: Element, codeGen: CodeGen): string { /** * 0: styleToken in existing class attr @@ -76,10 +81,17 @@ function serializeAttrs(element: Element, codeGen: CodeGen): string { // The token is only needed when the class attribute is static. // The token will be injected at runtime for expressions in parseFragmentFn. if (!hasExpression) { + if (typeof v === 'string') { + v = normalizeWhitespace(v); + } v += '${0}'; } } + if (name === 'style' && !hasExpression && typeof v === 'string') { + v = normalizeStyleAttribute(v); + } + // `spellcheck` string values are specially handled to massage them into booleans. // For backwards compat with non-static-optimized templates, we also treat any non-`"false"` // value other than the valueless format (e.g. `
`) as `"true"`,