Skip to content

Commit 2226ae2

Browse files
committed
fix(strategy): error when minifying inline CSS style placeholders #1
1 parent 11a9f6b commit 2226ae2

File tree

3 files changed

+110
-24
lines changed

3 files changed

+110
-24
lines changed

src/minifyHTMLLiterals.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export interface Result {
172172
export function defaultGenerateSourceMap(
173173
ms: MagicStringLike,
174174
fileName: string
175-
): SourceMap {
175+
) {
176176
return ms.generateMap({
177177
file: `${fileName}.map`,
178178
source: fileName,
@@ -188,20 +188,20 @@ export function defaultGenerateSourceMap(
188188
* @param template the template to check
189189
* @returns true if the template should be minified
190190
*/
191-
export function defaultShouldMinify(template: Template): boolean {
191+
export function defaultShouldMinify(template: Template) {
192192
return !!template.tag && template.tag.toLowerCase().includes('html');
193193
}
194194

195195
/**
196196
* The default validation.
197197
*/
198198
export const defaultValidation: Validation = {
199-
ensurePlaceholderValid(placeholder: any) {
199+
ensurePlaceholderValid(placeholder) {
200200
if (typeof placeholder !== 'string' || !placeholder.length) {
201201
throw new Error('getPlaceholder() must return a non-empty string');
202202
}
203203
},
204-
ensureHTMLPartsValid(parts: TemplatePart[], htmlParts: string[]) {
204+
ensureHTMLPartsValid(parts, htmlParts) {
205205
if (parts.length !== htmlParts.length) {
206206
throw new Error(
207207
'splitHTMLByPlaceholder() must return same number of strings as template parts'

src/strategy.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ export const defaultMinifyOptions: Options = {
7070
* The default strategy. This uses <code>html-minifier</code> to minify HTML.
7171
*/
7272
export const defaultStrategy: Strategy<Options> = {
73-
getPlaceholder(parts: TemplatePart[]): string {
73+
getPlaceholder(parts) {
74+
// Using @ and (); will cause the expression not to be removed in CSS.
75+
// However, sometimes the semicolon can be removed (ex: inline styles).
76+
// In those cases, we want to make sure that the HTML splitting also
77+
// accounts for the missing semicolon.
7478
const suffix = '();';
7579
let placeholder = '@TEMPLATE_EXPRESSION';
7680
while (parts.some(part => part.text.includes(placeholder + suffix))) {
@@ -79,13 +83,49 @@ export const defaultStrategy: Strategy<Options> = {
7983

8084
return placeholder + suffix;
8185
},
82-
combineHTMLStrings(parts: TemplatePart[], placeholder: string): string {
86+
combineHTMLStrings(parts, placeholder) {
8387
return parts.map(part => part.text).join(placeholder);
8488
},
85-
minifyHTML(html: string, options?: Options): string {
86-
return minify(html, options);
89+
minifyHTML(html, options = {}) {
90+
let minifyCSSOptions: any;
91+
if (options.minifyCSS) {
92+
if (
93+
options.minifyCSS !== true &&
94+
typeof options.minifyCSS !== 'function'
95+
) {
96+
minifyCSSOptions = { ...options.minifyCSS };
97+
} else {
98+
minifyCSSOptions = {};
99+
}
100+
} else {
101+
minifyCSSOptions = false;
102+
}
103+
104+
if (minifyCSSOptions && typeof minifyCSSOptions.level === 'undefined') {
105+
minifyCSSOptions.level = {
106+
1: {
107+
transform(_property: string, value: string) {
108+
if (
109+
value.startsWith('@TEMPLATE_EXPRESSION') &&
110+
!value.endsWith(';')
111+
) {
112+
// The CSS minifier has removed the semicolon from the placeholder
113+
// and we need to add it back.
114+
return `${value};`;
115+
}
116+
}
117+
}
118+
};
119+
}
120+
121+
return minify(html, {
122+
...options,
123+
minifyCSS: minifyCSSOptions
124+
});
87125
},
88-
splitHTMLByPlaceholder(html: string, placeholder: string): string[] {
126+
splitHTMLByPlaceholder(html, placeholder) {
127+
// Make the last character (a semicolon) optional. See above.
128+
// return html.split(new RegExp(`${placeholder}?`, 'g'));
89129
return html.split(placeholder);
90130
}
91131
};

test/strategy.spec.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,69 @@ describe('strategy', () => {
1919
];
2020

2121
describe('getPlaceholder()', () => {
22-
it('should return "@TEMPLATE_EXPRESSION();"', () => {
22+
it('should return a string with @ and () in it with no spaces', () => {
2323
const placeholder = defaultStrategy.getPlaceholder(parts);
24-
expect(placeholder).to.equal('@TEMPLATE_EXPRESSION();');
24+
expect(placeholder.indexOf('@')).to.equal(0, 'should start with @');
25+
expect(placeholder).to.include('()', 'should contain function parens');
2526
});
2627

2728
it('should append "_" if placeholder exists in templates', () => {
28-
expect(
29-
defaultStrategy.getPlaceholder([
30-
{ text: '@TEMPLATE_EXPRESSION();', start: 0, end: 19 }
31-
])
32-
).to.equal('@TEMPLATE_EXPRESSION_();');
29+
const regularPlaceholder = defaultStrategy.getPlaceholder(parts);
30+
const oneUnderscore = defaultStrategy.getPlaceholder([
31+
{ text: regularPlaceholder, start: 0, end: regularPlaceholder.length }
32+
]);
33+
34+
expect(oneUnderscore).not.to.equal(regularPlaceholder);
35+
expect(oneUnderscore).to.include('_');
36+
37+
const twoUnderscores = defaultStrategy.getPlaceholder([
38+
{
39+
text: regularPlaceholder,
40+
start: 0,
41+
end: regularPlaceholder.length
42+
},
43+
{
44+
text: oneUnderscore,
45+
start: regularPlaceholder.length,
46+
end: regularPlaceholder.length + oneUnderscore.length
47+
}
48+
]);
49+
50+
expect(twoUnderscores).not.to.equal(regularPlaceholder);
51+
expect(twoUnderscores).not.to.equal(oneUnderscore);
52+
expect(twoUnderscores).to.include('__');
53+
});
54+
55+
it('should return a value that is preserved by html-minifier when splitting', () => {
56+
const placeholder = defaultStrategy.getPlaceholder(parts);
57+
const minHtml = defaultStrategy.minifyHTML(
58+
`
59+
<style>
60+
${placeholder}
61+
62+
p {
63+
${placeholder}
64+
color: ${placeholder}
65+
}
66+
67+
div {
68+
width: ${placeholder}
69+
}
70+
</style>
71+
<p style="color: ${placeholder}">
72+
${placeholder}
73+
</p>
74+
<div id="${placeholder}" class="with ${placeholder}"></div>
75+
`,
76+
defaultMinifyOptions
77+
);
78+
79+
console.log(minHtml);
3380

81+
// 8 placeholders, 9 parts
3482
expect(
35-
defaultStrategy.getPlaceholder([
36-
{ text: '@TEMPLATE_EXPRESSION();', start: 0, end: 19 },
37-
{ text: '@TEMPLATE_EXPRESSION_();', start: 19, end: 38 }
38-
])
39-
).to.equal('@TEMPLATE_EXPRESSION__();');
83+
defaultStrategy.splitHTMLByPlaceholder(minHtml, placeholder)
84+
).to.have.lengthOf(9);
4085
});
4186
});
4287

@@ -51,11 +96,12 @@ describe('strategy', () => {
5196

5297
describe('minifyHTML()', () => {
5398
it('should call minify() with html and options', () => {
99+
const placeholder = defaultStrategy.getPlaceholder(parts);
54100
const html = `
55-
<style>@TEMPLATE_EXPRESSION();</style>
56-
<h1 class="heading">@TEMPLATE_EXPRESSION();</h1>
101+
<style>${placeholder}</style>
102+
<h1 class="heading">${placeholder}</h1>
57103
<ul>
58-
<li>@TEMPLATE_EXPRESSION();</li>
104+
<li>${placeholder}</li>
59105
</ul>
60106
`;
61107

0 commit comments

Comments
 (0)