@sprlab/wccompiler 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,109 +1,114 @@
1
- /**
2
- * Template Normalizer — pre-processes WCC template HTML before DOM parsing.
3
- *
4
- * Handles two transformations:
5
- * 1. PascalCase tags → kebab-case (e.g. <WccBadge> → <wcc-badge>)
6
- * 2. Self-closing custom elements → open+close (e.g. <wcc-badge /> → <wcc-badge></wcc-badge>)
7
- *
8
- * Must run BEFORE linkedom/jsdom parsing because HTML parsers:
9
- * - Lowercase all tag names (losing PascalCase word boundaries)
10
- * - Don't recognize self-closing syntax for non-void elements
11
- */
12
-
13
- // HTML void elements that are legitimately self-closing
14
- const VOID_ELEMENTS = new Set([
15
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
16
- 'link', 'meta', 'param', 'source', 'track', 'wbr',
17
- ]);
18
-
19
- /**
20
- * Convert a PascalCase tag name to kebab-case.
21
- * e.g. "WccBadge" → "wcc-badge", "WccCardHeader" → "wcc-card-header"
22
- *
23
- * Only converts if the name starts with an uppercase letter (PascalCase).
24
- *
25
- * @param {string} name
26
- * @returns {string}
27
- */
28
- export function pascalToKebab(name) {
29
- // Insert hyphen before each uppercase letter that follows a lowercase letter or digit
30
- return name
31
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
32
- .toLowerCase();
33
- }
34
-
35
- /**
36
- * Check if a tag name is PascalCase (starts with uppercase, has at least
37
- * one more uppercase letter indicating a word boundary).
38
- *
39
- * @param {string} name
40
- * @returns {boolean}
41
- */
42
- export function isPascalCase(name) {
43
- return /^[A-Z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/.test(name);
44
- }
45
-
46
- /**
47
- * Normalize a WCC template string:
48
- * 1. Convert PascalCase tags to kebab-case (validated against importMap if provided)
49
- * 2. Expand self-closing custom elements to open+close pairs
50
- *
51
- * @param {string} html — Raw template HTML
52
- * @param {object} [options]
53
- * @param {Map<string, string>} [options.importMap] — PascalCase identifier → kebab-case tag
54
- * @param {string} [options.fileName] — Source file for error messages
55
- * @returns {string} — Normalized HTML ready for DOM parsing
56
- * @throws {Error} with code 'UNRESOLVED_COMPONENT' if PascalCase tag has no matching import
57
- */
58
- export function normalizeTemplate(html, options) {
59
- const { importMap, fileName } = options || {};
60
-
61
- // Match opening tags (including self-closing): <TagName ...> or <TagName ... />
62
- // Also match closing tags: </TagName>
63
- //
64
- // Regex breakdown:
65
- // < — opening angle bracket
66
- // (\/?)? — optional slash (closing tag)
67
- // ([A-Za-z][\w-]*) — tag name
68
- // ((?:\s[^>]*)?) — attributes (anything that's not >)
69
- // (\s*\/)? — optional self-closing slash
70
- // > — closing angle bracket
71
- const TAG_RE = /<(\/?)([A-Za-z][\w-]*)((?:\s[^>]*?)?)(\/?)>/g;
72
-
73
- return html.replace(TAG_RE, (match, closingSlash, tagName, attrs, selfClosing) => {
74
- let normalizedTag = tagName;
75
-
76
- // Step 1: Convert PascalCase to kebab-case
77
- if (isPascalCase(tagName)) {
78
- if (importMap) {
79
- // Validate against the import map
80
- if (importMap.has(tagName)) {
81
- normalizedTag = importMap.get(tagName);
82
- } else {
83
- const error = new Error(
84
- `Unresolved component '<${tagName}>' in '${fileName || 'unknown'}'. Did you forget to import it?`
85
- );
86
- error.code = 'UNRESOLVED_COMPONENT';
87
- throw error;
88
- }
89
- } else {
90
- // Backward compatible: convert all PascalCase to kebab-case
91
- normalizedTag = pascalToKebab(tagName);
92
- }
93
- }
94
-
95
- // Step 2: Handle self-closing tags
96
- if (selfClosing === '/') {
97
- // If it's a void element, keep it self-closing
98
- if (VOID_ELEMENTS.has(normalizedTag.toLowerCase())) {
99
- return `<${closingSlash}${normalizedTag}${attrs} />`;
100
- }
101
- // Otherwise expand to open+close pair (trim trailing whitespace from attrs)
102
- const trimmedAttrs = attrs.trimEnd();
103
- return `<${normalizedTag}${trimmedAttrs}></${normalizedTag}>`;
104
- }
105
-
106
- // Regular open or close tag just replace the name
107
- return `<${closingSlash}${normalizedTag}${attrs}>`;
108
- });
109
- }
1
+ /**
2
+ * Template Normalizer — pre-processes WCC template HTML before DOM parsing.
3
+ *
4
+ * Handles two transformations:
5
+ * 1. PascalCase tags → kebab-case (e.g. <WccBadge> → <wcc-badge>)
6
+ * 2. Self-closing custom elements → open+close (e.g. <wcc-badge /> → <wcc-badge></wcc-badge>)
7
+ *
8
+ * Must run BEFORE linkedom/jsdom parsing because HTML parsers:
9
+ * - Lowercase all tag names (losing PascalCase word boundaries)
10
+ * - Don't recognize self-closing syntax for non-void elements
11
+ */
12
+
13
+ // HTML void elements that are legitimately self-closing
14
+ const VOID_ELEMENTS = new Set([
15
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
16
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
17
+ ]);
18
+
19
+ /**
20
+ * Convert a PascalCase tag name to kebab-case.
21
+ * e.g. "WccBadge" → "wcc-badge", "WccCardHeader" → "wcc-card-header"
22
+ *
23
+ * Only converts if the name starts with an uppercase letter (PascalCase).
24
+ *
25
+ * @param {string} name
26
+ * @returns {string}
27
+ */
28
+ export function pascalToKebab(name) {
29
+ // Insert hyphen before each uppercase letter that follows a lowercase letter or digit
30
+ return name
31
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
32
+ .toLowerCase();
33
+ }
34
+
35
+ /**
36
+ * Check if a tag name is PascalCase (starts with uppercase, has at least
37
+ * one more uppercase letter indicating a word boundary).
38
+ *
39
+ * @param {string} name
40
+ * @returns {boolean}
41
+ */
42
+ export function isPascalCase(name) {
43
+ return /^[A-Z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/.test(name);
44
+ }
45
+
46
+ /**
47
+ * Normalize a WCC template string:
48
+ * 1. Convert PascalCase tags to kebab-case (validated against importMap if provided)
49
+ * 2. Expand self-closing custom elements to open+close pairs
50
+ *
51
+ * @param {string} html — Raw template HTML
52
+ * @param {object} [options]
53
+ * @param {Map<string, string>} [options.importMap] — PascalCase identifier → kebab-case tag
54
+ * @param {string} [options.fileName] — Source file for error messages
55
+ * @returns {string} — Normalized HTML ready for DOM parsing
56
+ * @throws {Error} with code 'UNRESOLVED_COMPONENT' if PascalCase tag has no matching import
57
+ */
58
+ export function normalizeTemplate(html, options) {
59
+ const { importMap, fileName } = options || {};
60
+
61
+ // Match opening tags (including self-closing): <TagName ...> or <TagName ... />
62
+ // Also match closing tags: </TagName>
63
+ //
64
+ // Regex breakdown:
65
+ // < — opening angle bracket
66
+ // (\/?)? — optional slash (closing tag)
67
+ // ([A-Za-z][\w-]*) — tag name
68
+ // ((?:\s[^>]*)?) — attributes (anything that's not >)
69
+ // (\s*\/)? — optional self-closing slash
70
+ // > — closing angle bracket
71
+ const TAG_RE = /<(\/?)([A-Za-z][\w-]*)((?:\s[^>]*?)?)(\/?)>/g;
72
+
73
+ return html.replace(TAG_RE, (match, closingSlash, tagName, attrs, selfClosing) => {
74
+ // Guard: preserve <component> tags as-is — this is a compiler directive, not a custom element
75
+ if (tagName.toLowerCase() === 'component') {
76
+ return match;
77
+ }
78
+
79
+ let normalizedTag = tagName;
80
+
81
+ // Step 1: Convert PascalCase to kebab-case
82
+ if (isPascalCase(tagName)) {
83
+ if (importMap) {
84
+ // Validate against the import map
85
+ if (importMap.has(tagName)) {
86
+ normalizedTag = importMap.get(tagName);
87
+ } else {
88
+ const error = new Error(
89
+ `Unresolved component '<${tagName}>' in '${fileName || 'unknown'}'. Did you forget to import it?`
90
+ );
91
+ error.code = 'UNRESOLVED_COMPONENT';
92
+ throw error;
93
+ }
94
+ } else {
95
+ // Backward compatible: convert all PascalCase to kebab-case
96
+ normalizedTag = pascalToKebab(tagName);
97
+ }
98
+ }
99
+
100
+ // Step 2: Handle self-closing tags
101
+ if (selfClosing === '/') {
102
+ // If it's a void element, keep it self-closing
103
+ if (VOID_ELEMENTS.has(normalizedTag.toLowerCase())) {
104
+ return `<${closingSlash}${normalizedTag}${attrs} />`;
105
+ }
106
+ // Otherwise expand to open+close pair (trim trailing whitespace from attrs)
107
+ const trimmedAttrs = attrs.trimEnd();
108
+ return `<${normalizedTag}${trimmedAttrs}></${normalizedTag}>`;
109
+ }
110
+
111
+ // Regular open or close tag — just replace the name
112
+ return `<${closingSlash}${normalizedTag}${attrs}>`;
113
+ });
114
+ }