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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html-component-engine",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A Vite plugin for HTML component components with a lightweight static site compiler",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
- const componentRegex = /<Component\b([^>]*)>([\s\S]*?)<\/Component>/g;
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 {{ children }} placeholder with actual children content
61
- componentContent = componentContent.replace(/\{\{\s*children\s*\}\}/g, children);
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 || !attrs.src) continue;
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-]+)=["']([^"']*)["']/g;
240
+ const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
181
241
  let attrMatch;
182
242
  while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
183
- attrs[attrMatch[1]] = attrMatch[2];
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
@@ -11,11 +11,13 @@ export function parseSelfClosingComponentTag(tag) {
11
11
  if (!match) return null;
12
12
 
13
13
  const attrs = {};
14
- // Support hyphenated attributes like data-test, aria-label
15
- const attrRegex = /([\w-]+)="([^"]*)"/g;
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
- attrs[attrMatch[1]] = attrMatch[2];
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
  }