html-component-engine 0.1.3 → 0.1.4
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.
- package/package.json +1 -1
- package/src/engine/compiler.js +95 -9
- package/src/engine/utils.js +5 -3
package/package.json
CHANGED
package/src/engine/compiler.js
CHANGED
|
@@ -29,8 +29,9 @@ export async function compileHtml(html, root, projectRoot = null) {
|
|
|
29
29
|
* Also supports legacy: <Component name="...">...children...</Component>
|
|
30
30
|
*/
|
|
31
31
|
async function processComponentsWithChildren(html, root, projectRoot) {
|
|
32
|
-
// Regex to match non-self-closing <Component ...>...</Component>
|
|
33
|
-
|
|
32
|
+
// Regex to match ONLY non-self-closing <Component ...>...</Component>
|
|
33
|
+
// Prevents accidentally matching <Component ... /> as an opening tag.
|
|
34
|
+
const componentRegex = /<Component\b(?![^>]*\/\s*>)([^>]*)>([\s\S]*?)<\/Component>/g;
|
|
34
35
|
|
|
35
36
|
let result = html;
|
|
36
37
|
let matches = [...html.matchAll(componentRegex)];
|
|
@@ -57,8 +58,15 @@ async function processComponentsWithChildren(html, root, projectRoot) {
|
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
// Replace
|
|
61
|
-
|
|
61
|
+
// Replace children and named slots
|
|
62
|
+
const { defaultChildren, namedSlots } = extractSlots(children);
|
|
63
|
+
componentContent = componentContent.replace(/\{\{\s*children\s*\}\}/g, defaultChildren);
|
|
64
|
+
for (const [slotName, slotContent] of Object.entries(namedSlots)) {
|
|
65
|
+
componentContent = componentContent.replace(
|
|
66
|
+
new RegExp(`\\{\\{\\s*slot:${slotName}\\s*\\}\\}`, 'g'),
|
|
67
|
+
slotContent,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
62
70
|
|
|
63
71
|
// Replace props with {{key}} placeholders
|
|
64
72
|
for (const [key, value] of Object.entries(attrs)) {
|
|
@@ -86,9 +94,10 @@ async function processSelfClosingComponents(html, root, projectRoot) {
|
|
|
86
94
|
for (const match of matches) {
|
|
87
95
|
const tag = match[0];
|
|
88
96
|
const attrs = parseSelfClosingComponentTag(tag);
|
|
89
|
-
if (!attrs
|
|
97
|
+
if (!attrs) continue;
|
|
90
98
|
|
|
91
|
-
const name = attrs.src;
|
|
99
|
+
const name = attrs.src || attrs.name;
|
|
100
|
+
if (!name) continue;
|
|
92
101
|
let componentContent = await loadComponent(name, root, projectRoot, attrs);
|
|
93
102
|
|
|
94
103
|
if (componentContent === null) {
|
|
@@ -130,7 +139,7 @@ async function processSelfClosingComponents(html, root, projectRoot) {
|
|
|
130
139
|
*/
|
|
131
140
|
async function loadComponent(name, root, projectRoot, attrs = {}) {
|
|
132
141
|
// Normalize name for path construction (handle both / and \)
|
|
133
|
-
const normalizedName = name.replace(/\\/g, '/');
|
|
142
|
+
const normalizedName = name.replace(/\\/g, '/').replace(/\.html$/i, '');
|
|
134
143
|
|
|
135
144
|
// root = srcRoot (e.g., example/src)
|
|
136
145
|
// projectRoot = project root (e.g., example)
|
|
@@ -165,11 +174,62 @@ async function loadComponent(name, root, projectRoot, attrs = {}) {
|
|
|
165
174
|
continue;
|
|
166
175
|
}
|
|
167
176
|
}
|
|
177
|
+
|
|
178
|
+
// Try case-insensitive HTML lookup to support mixed-case component tags
|
|
179
|
+
try {
|
|
180
|
+
const caseInsensitivePath = await findCaseInsensitivePath(componentPath);
|
|
181
|
+
if (caseInsensitivePath) {
|
|
182
|
+
const content = await fs.readFile(caseInsensitivePath, 'utf8');
|
|
183
|
+
return content;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Try case-insensitive JS component lookup
|
|
190
|
+
try {
|
|
191
|
+
const jsPath = componentPath.replace('.html', '.js');
|
|
192
|
+
const caseInsensitiveJsPath = await findCaseInsensitivePath(jsPath);
|
|
193
|
+
if (caseInsensitiveJsPath) {
|
|
194
|
+
const componentModule = await import(pathToFileURL(caseInsensitiveJsPath));
|
|
195
|
+
const componentExport = componentModule.default || componentModule;
|
|
196
|
+
|
|
197
|
+
if (typeof componentExport === 'function') {
|
|
198
|
+
const props = { ...attrs };
|
|
199
|
+
delete props.src;
|
|
200
|
+
delete props.name;
|
|
201
|
+
return componentExport(props);
|
|
202
|
+
} else {
|
|
203
|
+
return String(componentExport);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// ignore
|
|
208
|
+
}
|
|
168
209
|
}
|
|
169
210
|
|
|
170
211
|
return null;
|
|
171
212
|
}
|
|
172
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Find a file path in a case-insensitive way
|
|
216
|
+
* @param {string} targetPath
|
|
217
|
+
* @returns {Promise<string|null>}
|
|
218
|
+
*/
|
|
219
|
+
async function findCaseInsensitivePath(targetPath) {
|
|
220
|
+
const dir = path.dirname(targetPath);
|
|
221
|
+
const base = path.basename(targetPath).toLowerCase();
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const entries = await fs.readdir(dir);
|
|
225
|
+
const matched = entries.find((entry) => entry.toLowerCase() === base);
|
|
226
|
+
if (!matched) return null;
|
|
227
|
+
return path.join(dir, matched);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
173
233
|
/**
|
|
174
234
|
* Parse attributes from an attribute string
|
|
175
235
|
* @param {string} attrsStr - Attribute string like ' class="foo" id="bar"'
|
|
@@ -177,14 +237,40 @@ async function loadComponent(name, root, projectRoot, attrs = {}) {
|
|
|
177
237
|
*/
|
|
178
238
|
function parseAttributes(attrsStr) {
|
|
179
239
|
const attrs = {};
|
|
180
|
-
const attrRegex = /([\w-]+)
|
|
240
|
+
const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
|
|
181
241
|
let attrMatch;
|
|
182
242
|
while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
|
|
183
|
-
|
|
243
|
+
const key = attrMatch[1];
|
|
244
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? 'true';
|
|
245
|
+
attrs[key] = value;
|
|
184
246
|
}
|
|
185
247
|
return attrs;
|
|
186
248
|
}
|
|
187
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Extract named slots from component children content
|
|
252
|
+
* Supported syntax:
|
|
253
|
+
* <template slot="actions">...</template>
|
|
254
|
+
* {{ slot:actions }}
|
|
255
|
+
* @param {string} children
|
|
256
|
+
* @returns {{defaultChildren: string, namedSlots: Record<string, string>}}
|
|
257
|
+
*/
|
|
258
|
+
function extractSlots(children) {
|
|
259
|
+
const namedSlots = {};
|
|
260
|
+
const slotRegex = /<template\s+slot=["']([\w-]+)["']\s*>([\s\S]*?)<\/template>/g;
|
|
261
|
+
|
|
262
|
+
let defaultChildren = children;
|
|
263
|
+
defaultChildren = defaultChildren.replace(slotRegex, (_, slotName, slotContent) => {
|
|
264
|
+
namedSlots[slotName] = slotContent;
|
|
265
|
+
return '';
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
defaultChildren,
|
|
270
|
+
namedSlots,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
188
274
|
/**
|
|
189
275
|
* Inline CSS from <link> tags into <style> tags
|
|
190
276
|
* @param {string} html - The HTML content
|
package/src/engine/utils.js
CHANGED
|
@@ -11,11 +11,13 @@ export function parseSelfClosingComponentTag(tag) {
|
|
|
11
11
|
if (!match) return null;
|
|
12
12
|
|
|
13
13
|
const attrs = {};
|
|
14
|
-
// Support hyphenated attributes
|
|
15
|
-
const attrRegex = /([\w-]+)
|
|
14
|
+
// Support hyphenated attributes with both quote styles and boolean attributes
|
|
15
|
+
const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
|
|
16
16
|
let attrMatch;
|
|
17
17
|
while ((attrMatch = attrRegex.exec(match[1])) !== null) {
|
|
18
|
-
|
|
18
|
+
const key = attrMatch[1];
|
|
19
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? 'true';
|
|
20
|
+
attrs[key] = value;
|
|
19
21
|
}
|
|
20
22
|
return attrs;
|
|
21
23
|
}
|