-
Notifications
You must be signed in to change notification settings - Fork 439
Expand file tree
/
Copy pathformat-html.ts
More file actions
128 lines (107 loc) · 4.66 KB
/
format-html.ts
File metadata and controls
128 lines (107 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { HTML_NAMESPACE, isVoidElement } from '@lwc/shared';
/**
* Naive HTML fragment formatter.
*
* This is a replacement for Prettier HTML formatting. Prettier formatting is too aggressive for
* fixture testing. It not only indent the HTML code but also fixes HTML issues. For testing we want
* to make sure that the fixture file is as close as possible to what the engine produces.
* @param src the original HTML fragment.
* @returns the formatter HTML fragment.
*/
export function formatHTML(src: string): string {
let res = '';
let pos = 0;
let depth = 0;
const getPadding = () => {
return ' '.repeat(depth);
};
while (pos < src.length) {
// Consume element tags and comments.
if (src.charAt(pos) === '<') {
const tagNameMatch = src.slice(pos).match(/([\w-]+)/);
const posAfterTagName = pos + 1 + tagNameMatch![0].length; // +1 to account for '<'
// Special handling for `<style>` tags – these are not encoded, so we may hit '<' or '>'
// inside the text content. So we just serialize it as-is.
if (tagNameMatch![0] === 'style') {
const styleMatch = src.slice(pos).match(/<style([\s\S]*?)>([\s\S]*?)<\/style>/);
if (styleMatch) {
// opening tag
const [, attrs, textContent] = styleMatch;
res += getPadding() + `<style${attrs}>` + '\n';
depth++;
res += getPadding() + textContent + '\n';
depth--;
res += getPadding() + '</style>' + '\n';
continue;
}
}
const isVoid = isVoidElement(tagNameMatch![0], HTML_NAMESPACE);
const isClosing = src.charAt(pos + 1) === '/';
const isComment =
src.charAt(pos + 1) === '!' &&
src.charAt(pos + 2) === '-' &&
src.charAt(pos + 3) === '-';
const start = pos;
while (src.charAt(pos++) !== '>') {
// Keep advancing until consuming the closing tag.
}
const isSelfClosing = src.charAt(pos - 2) === '/';
// Adjust current depth and print the element tag or comment.
if (isClosing) {
depth--;
} else if (!isComment) {
// Offsets to account for '>' or '/>'
const endPos = isSelfClosing ? pos - 2 : pos - 1;
// Trim to account for whitespace at the beginning
const attributesRaw = src.slice(posAfterTagName, endPos).trim();
const attributesReordered = attributesRaw
? ' ' + reorderAttributes(attributesRaw)
: '';
src =
src.substring(0, posAfterTagName) + attributesReordered + src.substring(endPos);
}
res += getPadding() + src.slice(start, pos) + '\n';
if (!isClosing && !isSelfClosing && !isVoid && !isComment) {
depth++;
}
}
// Consume text content.
const start = pos;
while (src.charAt(pos) !== '<' && pos < src.length) {
pos++;
}
if (start !== pos) {
res += getPadding() + src.slice(start, pos) + '\n';
}
}
return res.trim();
}
function reorderAttributes(attributesRaw: string) {
// If we have an odd number of quotes, we haven't parsed the attributes
// correctly, so we just avoid trying to sort them. This is mostly to paper
// over the `attribute-dynamic-escape` fixture.
const numQuotes = attributesRaw.match(/"/g)?.length || 0;
if (numQuotes % 2 !== 0) return attributesRaw;
const matches = [...attributesRaw.matchAll(/([:\w-]+)(="([^"]*)")?/gi)];
const results = matches
.map(([_whole, name, equalsQuotedValue, value]) => {
// TODO [#4714]: Scope token classes render in an inconsistent order for static vs dynamic classes
if (name === 'class' && value) {
// sort classes to ignore differences, e.g. `class="a b"` vs `class="b a"`
value = value.split(' ').sort().join(' ');
}
return name + (equalsQuotedValue ? `="${value}"` : '');
})
.sort()
.join(' ');
if (results.length !== attributesRaw.length) {
throw new Error('HTML auto-formatting failed due to unexpected whitespaces');
}
return results;
}