Skip to content

Commit dba53e7

Browse files
fix(template-compiler): fix HTML serialization for SVGs (#2898)
Co-authored-by: Pierre-Marie Dartus <p.dartus@salesforce.com>
1 parent 6b3f6c5 commit dba53e7

File tree

53 files changed

+527
-101
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+527
-101
lines changed

packages/@lwc/engine-server/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
],
2727
"devDependencies": {
2828
"@lwc/engine-core": "2.19.1",
29-
"@lwc/shared": "2.19.1"
29+
"@lwc/rollup-plugin": "2.19.1",
30+
"@lwc/shared": "2.19.1",
31+
"@rollup/plugin-virtual": "^2.1.0",
32+
"parse5": "^6.0.1"
3033
},
3134
"publishConfig": {
3235
"access": "public"

packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import path from 'path';
1111
import { rollup } from 'rollup';
1212
// @ts-ignore
1313
import lwcRollupPlugin from '@lwc/rollup-plugin';
14-
import { isVoidElement } from '@lwc/shared';
14+
import { isVoidElement, HTML_NAMESPACE } from '@lwc/shared';
1515
import { testFixtureDir } from '@lwc/jest-utils-lwc-internals';
1616
import type * as lwc from '../index';
1717

@@ -77,7 +77,7 @@ function formatHTML(src: string): string {
7777
if (src.charAt(pos) === '<') {
7878
const tagNameMatch = src.slice(pos).match(/(\w+)/);
7979

80-
const isVoid = isVoidElement(tagNameMatch![0]);
80+
const isVoid = isVoidElement(tagNameMatch![0], HTML_NAMESPACE);
8181
const isClosing = src.charAt(pos + 1) === '/';
8282
const isComment =
8383
src.charAt(pos + 1) === '!' &&
@@ -96,7 +96,8 @@ function formatHTML(src: string): string {
9696

9797
res += getPadding() + src.slice(start, pos) + '\n';
9898

99-
if (!isClosing && !isVoid && !isComment) {
99+
const isSelfClosing = src.charAt(pos - 2) === '/';
100+
if (!isClosing && !isSelfClosing && !isVoid && !isComment) {
100101
depth++;
101102
}
102103
}

packages/@lwc/engine-server/src/__tests__/fixtures/svgs/expected.html

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@
33
<svg height="150" width="400">
44
<defs>
55
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
6-
<stop class="static" offset="0%" style="stop-color:rgb(255,255,0);stop-opacity:1">
7-
</stop>
8-
<stop class="static" offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1">
9-
</stop>
6+
<stop class="static" offset="0%" style="stop-color:rgb(255,255,0);stop-opacity:1"/>
7+
<stop class="static" offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1"/>
108
</linearGradient>
119
</defs>
12-
<ellipse class="static" cx="200" cy="70" rx="85" ry="55" fill="url(#grad1)">
10+
<ellipse class="static" cx="200" cy="70" rx="85" ry="55" fill="url(#grad1)"/>
1311
</svg>
1412
<svg class="static" height="150" width="400">
15-
<ellipse class="static" cx="200" cy="70" rx="85" ry="55" fill="url(#grad1)">
13+
<ellipse class="static" cx="200" cy="70" rx="85" ry="55" fill="url(#grad1)"/>
14+
</svg>
15+
<div>
16+
<svg xmlns="http://www.w3.org/2000/svg">
17+
<path/>
18+
<path/>
19+
</svg>
20+
</div>
21+
<svg xmlns="http://www.w3.org/2000/svg">
22+
<path/>
23+
<path/>
1624
</svg>
1725
</template>
1826
</x-svgs>

packages/@lwc/engine-server/src/__tests__/fixtures/svgs/modules/x/svgs/svgs.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,17 @@
1212
<svg class="static" height="150" width="400">
1313
<ellipse class="static" cx="200" cy="70" rx="85" ry="55" fill="url(#grad1)" />
1414
</svg>
15-
</template>
15+
16+
<div>
17+
<svg xmlns="http://www.w3.org/2000/svg">
18+
<path></path>
19+
<path></path>
20+
</svg>
21+
</div>
22+
23+
<svg xmlns="http://www.w3.org/2000/svg">
24+
<path></path>
25+
<path></path>
26+
</svg>
27+
28+
</template>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import path from 'path';
9+
import vm from 'vm';
10+
import { parseFragment, serialize } from 'parse5';
11+
import { rollup, RollupWarning } from 'rollup';
12+
import replace from '@rollup/plugin-replace';
13+
import virtual from '@rollup/plugin-virtual';
14+
import lwcRollupPlugin from '@lwc/rollup-plugin';
15+
import * as engineServer from '../index';
16+
17+
/**
18+
* The goal of these tests is to serialize the HTML, and then parse it with a real
19+
* HTML parser to ensure that the serialized content is correct. It's slightly more
20+
* robust than snapshots, which may have invalid/incorrect HTML.
21+
*/
22+
23+
jest.setTimeout(10_000 /* 10 seconds */);
24+
25+
// Compile a component to an HTML string, using the full LWC compilation pipeline
26+
async function compileComponent(tagName: string, componentName: string) {
27+
const modulesDir = path.resolve(__dirname, './modules');
28+
const componentPath = path.resolve(
29+
modulesDir,
30+
componentName,
31+
componentName.split('/')[1] + '.js'
32+
);
33+
34+
const warnings: RollupWarning[] = [];
35+
36+
const bundle = await rollup({
37+
input: '__entry__',
38+
external: ['lwc'],
39+
onwarn(warning) {
40+
warnings.push(warning);
41+
},
42+
plugins: [
43+
virtual({
44+
__entry__: `
45+
import { renderComponent } from 'lwc';
46+
import Component from ${JSON.stringify(componentPath)};
47+
export const component = renderComponent(${JSON.stringify(tagName)}, Component);
48+
`,
49+
}),
50+
lwcRollupPlugin({
51+
modules: [{ dir: modulesDir }],
52+
}),
53+
replace({
54+
preventAssignment: true,
55+
values: {
56+
'process.env.NODE_ENV': '"development"',
57+
},
58+
}),
59+
],
60+
});
61+
62+
const { output } = await bundle.generate({
63+
globals: {
64+
lwc: 'lwc',
65+
},
66+
format: 'iife',
67+
name: 'result',
68+
});
69+
const { code } = output[0];
70+
71+
const context = vm.createContext({
72+
lwc: engineServer,
73+
});
74+
vm.runInContext(code, context);
75+
const html = (context as any).result.component as string;
76+
77+
return {
78+
html,
79+
warnings,
80+
};
81+
}
82+
83+
// Parse the compiled HTML and re-serialize to validate it against a real HTML parser
84+
function parseAndReserialize(html: string): string {
85+
const parsed = parseFragment(html);
86+
return serialize(parsed);
87+
}
88+
89+
describe('html serialization', () => {
90+
it('serializes void HTML elements correctly', async () => {
91+
const { html, warnings } = await compileComponent('x-html-void', 'x/htmlVoid');
92+
const parsedHtml = parseAndReserialize(html);
93+
expect(parsedHtml).toEqual(
94+
'<x-html-void><input type="text" value="one"><input type="text" value="two"></x-html-void>'
95+
);
96+
expect(warnings.length).toEqual(0);
97+
});
98+
99+
it('serializes void HTML elements correctly with text in between', async () => {
100+
const { html, warnings } = await compileComponent(
101+
'x-html-void-adjacent-text',
102+
'x/htmlVoidAdjacentText'
103+
);
104+
const parsedHtml = parseAndReserialize(html);
105+
expect(parsedHtml).toEqual(
106+
'<x-html-void-adjacent-text>before<input type="text" value="one">middle<input type="text" value="two">after</x-html-void-adjacent-text>'
107+
);
108+
expect(warnings.length).toEqual(0);
109+
});
110+
111+
it('serializes SVG path elements correctly', async () => {
112+
const { html, warnings } = await compileComponent('x-svg-path', 'x/svgPath');
113+
const parsedHtml = parseAndReserialize(html);
114+
expect(parsedHtml).toEqual(
115+
'<x-svg-path><svg xmlns="http://www.w3.org/2000/svg"><path d="M10 10"></path><path d="M20 20"></path></svg></x-svg-path>'
116+
);
117+
expect(warnings.length).toEqual(0);
118+
});
119+
120+
it('serializes void HTML elements correctly in HTML namespace', async () => {
121+
const { html, warnings } = await compileComponent(
122+
'x-html-void-html-namespace',
123+
'x/htmlVoidHtmlNamespace'
124+
);
125+
const parsedHtml = parseAndReserialize(html);
126+
expect(parsedHtml).toEqual(
127+
'<x-html-void-html-namespace><div xmlns="http://www.w3.org/1999/xhtml"><input type="text" value="one"><input type="text" value="two"></div></x-html-void-html-namespace>'
128+
);
129+
expect(warnings.map((_) => _.message)).toEqual([
130+
'@lwc/rollup-plugin: LWC1057: xmlns is not valid attribute for div. For more information refer to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div',
131+
]);
132+
});
133+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template lwc:render-mode="light">
2+
<input type="text" value="one">
3+
<input type="text" value="two">
4+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
static renderMode = 'light';
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template lwc:render-mode="light">
2+
before
3+
<input type="text" value="one">
4+
middle
5+
<input type="text" value="two">
6+
after
7+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
static renderMode = 'light';
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template lwc:render-mode="light">
2+
<div xmlns="http://www.w3.org/1999/xhtml">
3+
<input type="text" value="one">
4+
<input type="text" value="two">
5+
</div>
6+
</template>

0 commit comments

Comments
 (0)