dalila 1.9.25 → 1.10.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.
package/dist/cli/check.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { buildRouteTree, injectHtmlPathTemplates, findFile, findProjectRoot, extractParamKeys, } from './routes-generator.js';
4
+ const VOID_HTML_TAGS = new Set([
5
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
6
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
7
+ ]);
4
8
  // ============================================================================
5
9
  // TypeScript Compiler API (dynamic import)
6
10
  // ============================================================================
@@ -406,6 +410,161 @@ function extractRootIdentifiers(expr) {
406
410
  }
407
411
  return result;
408
412
  }
413
+ function findContainingRange(offset, ranges) {
414
+ for (const range of ranges) {
415
+ if (offset < range.start)
416
+ break;
417
+ if (offset >= range.start && offset < range.end)
418
+ return range;
419
+ }
420
+ return null;
421
+ }
422
+ function isInsideRange(offset, ranges) {
423
+ return findContainingRange(offset, ranges) !== null;
424
+ }
425
+ function hasRawMarkerAttribute(attrs) {
426
+ let i = 0;
427
+ while (i < attrs.length) {
428
+ while (i < attrs.length && /\s/.test(attrs[i]))
429
+ i++;
430
+ if (i >= attrs.length)
431
+ break;
432
+ if (attrs[i] === '/') {
433
+ i++;
434
+ continue;
435
+ }
436
+ const nameStart = i;
437
+ while (i < attrs.length && !/[\s=\/]/.test(attrs[i]))
438
+ i++;
439
+ if (i === nameStart) {
440
+ i++;
441
+ continue;
442
+ }
443
+ const name = attrs.slice(nameStart, i).toLowerCase();
444
+ if (name === 'd-pre' || name === 'd-raw')
445
+ return true;
446
+ while (i < attrs.length && /\s/.test(attrs[i]))
447
+ i++;
448
+ if (attrs[i] !== '=')
449
+ continue;
450
+ i++; // skip '='
451
+ while (i < attrs.length && /\s/.test(attrs[i]))
452
+ i++;
453
+ if (i >= attrs.length)
454
+ break;
455
+ const quote = attrs[i];
456
+ if (quote === '"' || quote === "'") {
457
+ i++;
458
+ while (i < attrs.length && attrs[i] !== quote)
459
+ i++;
460
+ if (i < attrs.length)
461
+ i++;
462
+ continue;
463
+ }
464
+ while (i < attrs.length && !/\s/.test(attrs[i]))
465
+ i++;
466
+ }
467
+ return false;
468
+ }
469
+ function extractRawTemplateRanges(html, options = {}) {
470
+ const ranges = [];
471
+ const stack = [];
472
+ const lowerHtml = html.toLowerCase();
473
+ let i = 0;
474
+ while (i < html.length) {
475
+ // script/style contents are raw text in HTML parsing; ignore any
476
+ // "<...>" sequences inside them to avoid false tag detection.
477
+ const scriptLike = stack[stack.length - 1];
478
+ if (scriptLike && (scriptLike.tagName === 'script' || scriptLike.tagName === 'style')) {
479
+ const closeNeedle = `</${scriptLike.tagName}`;
480
+ const closeIndex = lowerHtml.indexOf(closeNeedle, i);
481
+ if (closeIndex === -1)
482
+ break;
483
+ if (closeIndex > i) {
484
+ i = closeIndex;
485
+ continue;
486
+ }
487
+ }
488
+ if (html[i] !== '<') {
489
+ i++;
490
+ continue;
491
+ }
492
+ let j = i + 1;
493
+ let inString = null;
494
+ let escaped = false;
495
+ while (j < html.length) {
496
+ const ch = html[j];
497
+ if (inString) {
498
+ if (escaped) {
499
+ escaped = false;
500
+ }
501
+ else if (ch === '\\') {
502
+ escaped = true;
503
+ }
504
+ else if (ch === inString) {
505
+ inString = null;
506
+ }
507
+ j++;
508
+ continue;
509
+ }
510
+ if (ch === '"' || ch === "'") {
511
+ inString = ch;
512
+ j++;
513
+ continue;
514
+ }
515
+ if (ch === '>')
516
+ break;
517
+ j++;
518
+ }
519
+ if (j >= html.length)
520
+ break;
521
+ const fullTag = html.slice(i, j + 1);
522
+ const inner = html.slice(i + 1, j).trim();
523
+ const isClosingTag = inner.startsWith('/');
524
+ const normalized = isClosingTag ? inner.slice(1).trim() : inner;
525
+ const nameMatch = /^([a-zA-Z][\w:-]*)/.exec(normalized);
526
+ if (!nameMatch) {
527
+ i = j + 1;
528
+ continue;
529
+ }
530
+ const tagName = nameMatch[1].toLowerCase();
531
+ const attrs = normalized.slice(nameMatch[1].length);
532
+ if (isClosingTag) {
533
+ for (let stackIdx = stack.length - 1; stackIdx >= 0; stackIdx--) {
534
+ if (stack[stackIdx].tagName !== tagName)
535
+ continue;
536
+ const entry = stack.splice(stackIdx, 1)[0];
537
+ if (entry.isRaw) {
538
+ ranges.push({ start: entry.start, end: i + fullTag.length });
539
+ }
540
+ break;
541
+ }
542
+ i = j + 1;
543
+ continue;
544
+ }
545
+ const isRawTag = (options.includePreCode && (tagName === 'pre' || tagName === 'code'))
546
+ || tagName === 'd-pre'
547
+ || tagName === 'd-raw'
548
+ || hasRawMarkerAttribute(attrs);
549
+ const isSelfClosingTag = VOID_HTML_TAGS.has(tagName);
550
+ if (isSelfClosingTag) {
551
+ if (isRawTag) {
552
+ ranges.push({ start: i, end: i + fullTag.length });
553
+ }
554
+ i = j + 1;
555
+ continue;
556
+ }
557
+ stack.push({ tagName, isRaw: isRawTag, start: i });
558
+ i = j + 1;
559
+ }
560
+ for (const entry of stack) {
561
+ if (entry.isRaw) {
562
+ ranges.push({ start: entry.start, end: html.length });
563
+ }
564
+ }
565
+ ranges.sort((a, b) => a.start - b.start);
566
+ return ranges;
567
+ }
409
568
  /**
410
569
  * Extract all template identifiers from HTML content.
411
570
  *
@@ -413,7 +572,7 @@ function extractRootIdentifiers(expr) {
413
572
  * 1. Text interpolation `{expr}` — only outside HTML tags
414
573
  * 2. Context-binding directives `d-*="value"` — specific set
415
574
  */
416
- function extractTemplateIdentifiers(html) {
575
+ function extractTemplateIdentifiers(html, rawRanges = [], interpolationRawRanges = rawRanges) {
417
576
  const identifiers = [];
418
577
  const lines = html.split('\n');
419
578
  const lineOffsets = [];
@@ -439,6 +598,13 @@ function extractTemplateIdentifiers(html) {
439
598
  let tagQuote = null;
440
599
  let i = 0;
441
600
  while (i < html.length) {
601
+ if (!inTag) {
602
+ const rawRange = findContainingRange(i, interpolationRawRanges);
603
+ if (rawRange) {
604
+ i = rawRange.end;
605
+ continue;
606
+ }
607
+ }
442
608
  const ch = html[i];
443
609
  if (!inTag && ch === '<') {
444
610
  inTag = true;
@@ -524,6 +690,8 @@ function extractTemplateIdentifiers(html) {
524
690
  DIRECTIVE_RE.lastIndex = 0;
525
691
  let match;
526
692
  while ((match = DIRECTIVE_RE.exec(html))) {
693
+ if (isInsideRange(match.index, rawRanges))
694
+ continue;
527
695
  const directive = match[1];
528
696
  const value = match[3].trim();
529
697
  if (!value)
@@ -544,11 +712,16 @@ function extractTemplateIdentifiers(html) {
544
712
  }
545
713
  return identifiers;
546
714
  }
547
- function extractLoopRanges(html) {
715
+ function extractLoopRanges(html, rawRanges = []) {
548
716
  const ranges = [];
549
717
  const stack = [];
550
718
  let i = 0;
551
719
  while (i < html.length) {
720
+ const rawRange = findContainingRange(i, rawRanges);
721
+ if (rawRange) {
722
+ i = rawRange.end;
723
+ continue;
724
+ }
552
725
  if (html[i] !== '<') {
553
726
  i++;
554
727
  continue;
@@ -591,7 +764,7 @@ function extractLoopRanges(html) {
591
764
  i = j + 1;
592
765
  continue;
593
766
  }
594
- const tagName = nameMatch[1];
767
+ const tagName = nameMatch[1].toLowerCase();
595
768
  const attrs = normalized.slice(tagName.length);
596
769
  const isSelfClosingTag = !isClosingTag && /\/\s*$/.test(normalized);
597
770
  if (isClosingTag) {
@@ -699,8 +872,10 @@ const LOOP_FORCED_CHECK_SOURCES = new Set([
699
872
  'd-virtual-overscan',
700
873
  ]);
701
874
  function checkHtmlContent(html, filePath, validIdentifiers, diagnostics) {
702
- const ids = extractTemplateIdentifiers(html);
703
- const loopRanges = extractLoopRanges(html);
875
+ const rawRanges = extractRawTemplateRanges(html);
876
+ const interpolationRawRanges = extractRawTemplateRanges(html, { includePreCode: true });
877
+ const ids = extractTemplateIdentifiers(html, rawRanges, interpolationRawRanges);
878
+ const loopRanges = extractLoopRanges(html, rawRanges);
704
879
  for (const id of ids) {
705
880
  if (validIdentifiers.has(id.name))
706
881
  continue;
@@ -15,7 +15,8 @@ export interface BindOptions {
15
15
  */
16
16
  events?: string[];
17
17
  /**
18
- * Selectors for elements where text interpolation should be skipped
18
+ * Selectors for elements where text interpolation should be skipped.
19
+ * `d-pre` / `d-raw` subtrees are always skipped regardless of this option.
19
20
  */
20
21
  rawTextSelectors?: string;
21
22
  /**
@@ -103,6 +103,73 @@ function isWarnAsErrorEnabledForActiveScope() {
103
103
  }
104
104
  return false;
105
105
  }
106
+ const RAW_BLOCK_SELECTORS = '[d-pre], [d-raw], d-pre, d-raw, [data-dalila-raw]';
107
+ const RAW_BLOCK_STYLE_ID = 'dalila-raw-block-default-styles';
108
+ function ensureRawBlockDefaultStyles() {
109
+ if (typeof document === 'undefined')
110
+ return;
111
+ if (document.getElementById(RAW_BLOCK_STYLE_ID))
112
+ return;
113
+ const style = document.createElement('style');
114
+ style.id = RAW_BLOCK_STYLE_ID;
115
+ style.textContent = `
116
+ [data-dalila-raw] { white-space: pre-wrap; }
117
+ d-pre[data-dalila-raw], d-raw[data-dalila-raw] { display: block; }
118
+ `.trim();
119
+ if (document.head) {
120
+ document.head.appendChild(style);
121
+ return;
122
+ }
123
+ document.documentElement?.appendChild(style);
124
+ }
125
+ function elementDepth(el) {
126
+ let depth = 0;
127
+ let cursor = el.parentElement;
128
+ while (cursor) {
129
+ depth += 1;
130
+ cursor = cursor.parentElement;
131
+ }
132
+ return depth;
133
+ }
134
+ function collectRawBlocks(root) {
135
+ const boundary = root.closest('[data-dalila-internal-bound]');
136
+ const matches = [];
137
+ if (root.matches(RAW_BLOCK_SELECTORS)) {
138
+ matches.push(root);
139
+ }
140
+ matches.push(...Array.from(root.querySelectorAll(RAW_BLOCK_SELECTORS)));
141
+ const inScope = matches.filter((el) => {
142
+ const bound = el.closest('[data-dalila-internal-bound]');
143
+ return bound === boundary;
144
+ });
145
+ inScope.sort((a, b) => elementDepth(a) - elementDepth(b));
146
+ const roots = [];
147
+ for (const el of inScope) {
148
+ if (roots.some((parent) => parent.contains(el)))
149
+ continue;
150
+ roots.push(el);
151
+ }
152
+ return roots;
153
+ }
154
+ function materializeRawBlocks(root) {
155
+ const rawBlocks = collectRawBlocks(root);
156
+ if (rawBlocks.length > 0) {
157
+ ensureRawBlockDefaultStyles();
158
+ }
159
+ for (const block of rawBlocks) {
160
+ if (block.hasAttribute('data-dalila-raw'))
161
+ continue;
162
+ const source = block.innerHTML;
163
+ block.setAttribute('data-dalila-raw', '');
164
+ block.textContent = source;
165
+ }
166
+ }
167
+ function isInsideRawBlock(el, boundary) {
168
+ const rawRoot = el.closest(RAW_BLOCK_SELECTORS);
169
+ if (!rawRoot)
170
+ return false;
171
+ return rawRoot.closest('[data-dalila-internal-bound]') === boundary;
172
+ }
106
173
  function createBindScanPlan(root) {
107
174
  const boundary = root.closest('[data-dalila-internal-bound]');
108
175
  const elements = [];
@@ -208,16 +275,19 @@ function resolveSelectorFromIndex(plan, selector) {
208
275
  return null;
209
276
  }
210
277
  function qsaFromPlan(plan, selector) {
278
+ const boundary = plan.root.closest('[data-dalila-internal-bound]');
211
279
  const cacheable = !selector.includes('[');
212
280
  if (cacheable) {
213
281
  const cached = plan.selectorCache.get(selector);
214
282
  if (cached) {
215
- return cached.filter(el => el === plan.root || plan.root.contains(el));
283
+ return cached.filter((el) => (el === plan.root || plan.root.contains(el))
284
+ && !isInsideRawBlock(el, boundary));
216
285
  }
217
286
  }
218
287
  const indexed = resolveSelectorFromIndex(plan, selector);
219
288
  const source = indexed ?? plan.elements.filter(el => el.matches(selector));
220
- const matches = source.filter(el => el === plan.root || plan.root.contains(el));
289
+ const matches = source.filter((el) => (el === plan.root || plan.root.contains(el))
290
+ && !isInsideRawBlock(el, boundary));
221
291
  if (cacheable)
222
292
  plan.selectorCache.set(selector, matches);
223
293
  return matches;
@@ -239,7 +309,9 @@ function qsaIncludingRoot(root, selector) {
239
309
  const boundary = root.closest('[data-dalila-internal-bound]');
240
310
  return out.filter(el => {
241
311
  const bound = el.closest('[data-dalila-internal-bound]');
242
- return bound === boundary;
312
+ if (bound !== boundary)
313
+ return false;
314
+ return !isInsideRawBlock(el, boundary);
243
315
  });
244
316
  }
245
317
  /**
@@ -1486,6 +1558,8 @@ function createInterpolationTemplatePlan(root, rawTextSelectors) {
1486
1558
  const parent = node.parentElement;
1487
1559
  if (parent && parent.closest(rawTextSelectors))
1488
1560
  continue;
1561
+ if (parent && parent.closest(RAW_BLOCK_SELECTORS))
1562
+ continue;
1489
1563
  if (parent) {
1490
1564
  const bound = parent.closest('[data-dalila-internal-bound]');
1491
1565
  if (bound !== textBoundary)
@@ -3381,6 +3455,9 @@ export function bind(root, ctx, options = {}) {
3381
3455
  warnAsErrorScopes.add(templateScope);
3382
3456
  }
3383
3457
  linkScopeToDom(templateScope, root, describeBindRoot(root));
3458
+ // Harden raw code-example blocks early so nested markup is rendered as
3459
+ // inert text before any directive/event pass runs.
3460
+ materializeRawBlocks(root);
3384
3461
  const bindScanPlan = createBindScanPlan(root);
3385
3462
  let bindCompleted = false;
3386
3463
  let disposed = false;
@@ -12,6 +12,10 @@ import { bind, warnSecurityRuntime } from './bind.js';
12
12
  import { defineComponent } from './component.js';
13
13
  import { hasExecutableHtmlSinkPattern, setElementInnerHTML } from './html-sinks.js';
14
14
  // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+ const RAW_BLOCK_SELECTORS = '[d-pre], [d-raw], d-pre, d-raw, [data-dalila-raw]';
18
+ // ============================================================================
15
19
  // Error Boundary Component
16
20
  // ============================================================================
17
21
  /**
@@ -97,6 +101,10 @@ export function bindBoundary(root, ctx, cleanups, options = {}) {
97
101
  continue;
98
102
  if (el.closest('[data-dalila-internal-bound]') !== boundary)
99
103
  continue;
104
+ // Raw blocks are inert by contract: d-* directives must not run inside.
105
+ const rawRoot = el.closest(RAW_BLOCK_SELECTORS);
106
+ if (rawRoot && rawRoot.closest('[data-dalila-internal-bound]') === boundary)
107
+ continue;
100
108
  for (const nested of Array.from(el.querySelectorAll('[d-boundary]'))) {
101
109
  consumedNested.add(nested);
102
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.9.25",
3
+ "version": "1.10.0",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -335,6 +335,52 @@ function createImportMapScript(dalilaPath, sourceDirPath = '/src/') {
335
335
  return ` <script type="importmap">\n${payload}\n </script>`;
336
336
  }
337
337
 
338
+ function mergeImportMapIntoHtml(html, dalilaPath, sourceDirPath = '/src/') {
339
+ const importMapPattern = /<script\b[^>]*type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/i;
340
+ const match = html.match(importMapPattern);
341
+ if (!match) {
342
+ return {
343
+ html,
344
+ merged: false,
345
+ script: '',
346
+ };
347
+ }
348
+
349
+ const existingPayload = match[1]?.trim() || '{}';
350
+ let importMap;
351
+ try {
352
+ importMap = JSON.parse(existingPayload);
353
+ } catch {
354
+ return {
355
+ html,
356
+ merged: false,
357
+ script: '',
358
+ };
359
+ }
360
+
361
+ const existingImports = importMap && typeof importMap.imports === 'object' && importMap.imports !== null
362
+ ? importMap.imports
363
+ : {};
364
+ const mergedImportMap = {
365
+ ...importMap,
366
+ imports: {
367
+ ...createImportMapEntries(dalilaPath, sourceDirPath),
368
+ ...existingImports,
369
+ },
370
+ };
371
+ const payload = JSON.stringify(mergedImportMap, null, 2)
372
+ .split('\n')
373
+ .map(line => ` ${line}`)
374
+ .join('\n');
375
+ const script = ` <script type="importmap">\n${payload}\n </script>`;
376
+
377
+ return {
378
+ html: html.replace(importMapPattern, ''),
379
+ merged: true,
380
+ script,
381
+ };
382
+ }
383
+
338
384
  // ============================================================================
339
385
  // Preload Script Detection (auto-inject for persist() with preload: true)
340
386
  // ============================================================================
@@ -536,11 +582,15 @@ function injectBindings(html, requestPath) {
536
582
  const normalizedPath = normalizeHtmlRequestPath(requestPath);
537
583
  // Different paths for dalila repo vs user projects
538
584
  const dalilaPath = isDalilaRepo ? '/dist' : '/node_modules/dalila/dist';
539
- const importMap = createImportMapScript(dalilaPath, buildProjectSourceDirPath(projectDir));
585
+ const sourceDirPath = buildProjectSourceDirPath(projectDir);
586
+ const mergedImportMap = mergeImportMapIntoHtml(html, dalilaPath, sourceDirPath);
587
+ const importMap = mergedImportMap.merged
588
+ ? mergedImportMap.script
589
+ : createImportMapScript(dalilaPath, sourceDirPath);
540
590
 
541
591
  // For user projects, inject import map + HMR script
542
592
  if (!isDalilaRepo) {
543
- let output = addLoadingAttributes(html);
593
+ let output = addLoadingAttributes(mergedImportMap.html);
544
594
 
545
595
  // Smart HMR script for user projects
546
596
  const hmrScript = `
@@ -692,7 +742,12 @@ function injectBindings(html, requestPath) {
692
742
  };
693
743
  </script>`;
694
744
 
695
- output = injectHeadFragments(output, buildUserProjectHeadAdditions(projectDir, dalilaPath), {
745
+ const headAdditions = buildUserProjectHeadAdditions(projectDir, dalilaPath)
746
+ .filter((fragment) => !mergedImportMap.merged || !/type=["']importmap["']/i.test(fragment));
747
+ if (importMap) {
748
+ headAdditions.push(importMap);
749
+ }
750
+ output = injectHeadFragments(output, headAdditions, {
696
751
  beforeModule: true,
697
752
  beforeStyles: true,
698
753
  });
@@ -1353,6 +1408,7 @@ module.exports = {
1353
1408
  safeDecodeUrlPath,
1354
1409
  createImportMapEntries,
1355
1410
  createImportMapScript,
1411
+ mergeImportMapIntoHtml,
1356
1412
  detectPreloadScripts,
1357
1413
  buildUserProjectHeadAdditions,
1358
1414
  injectHeadFragments,