@tsrx/core 0.1.66 → 0.1.68

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
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.66",
6
+ "version": "0.1.68",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -80,7 +80,7 @@
80
80
  "esrap": "^2.3.2",
81
81
  "magic-string": "^0.30.18",
82
82
  "zimmerframe": "^1.1.2",
83
- "@tsrx/runtime": "0.1.5"
83
+ "@tsrx/runtime": "0.1.6"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@types/node": "^24.3.0",
@@ -422,18 +422,14 @@ function can_render_dynamic_content(element, check_classes = false) {
422
422
  function get_possible_element_siblings(node, direction, adjacent_only) {
423
423
  /** @type {Map<AST.TSRXJSXElement, boolean>} */
424
424
  const siblings = new Map();
425
- const parent = get_element_parent(node);
425
+ const container = get_sibling_container(node);
426
426
 
427
- if (!parent) {
427
+ if (container === null) {
428
428
  return siblings;
429
429
  }
430
430
 
431
- // Get the container that holds the siblings
432
- const container = node_children(parent);
433
431
  const node_index = container.indexOf(node);
434
432
 
435
- if (node_index === -1) return siblings;
436
-
437
433
  // Determine which siblings to check based on direction
438
434
  let start, end, step;
439
435
  if (direction === FORWARD) {
@@ -532,9 +528,8 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
532
528
  sibling_matched = true;
533
529
  } else {
534
530
  // Check if there are any elements after this component that could match the remaining selectors
535
- const parent = get_element_parent(node);
536
- if (parent) {
537
- const container = node_children(parent);
531
+ const container = get_sibling_container(node);
532
+ if (container !== null) {
538
533
  const component_index = container.indexOf(possible_sibling);
539
534
 
540
535
  // For adjacent combinator, only check immediate next element
@@ -578,6 +573,38 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
578
573
  return true;
579
574
  }
580
575
  }
576
+ /**
577
+ * The children list an element sits in — that of the nearest ancestor on its
578
+ * path whose children include it, which is its parent element, a fragment
579
+ * (an authored `<>…</>`, a control-flow branch's output, or the synthetic
580
+ * root of a style scope), or `null` when the element has no container.
581
+ *
582
+ * Sibling combinators (`+`, `~`) read this list. Ancestor combinators keep
583
+ * using {@link get_element_parent}: a fragment is not an element, and a
584
+ * scope's root fragment stands for the container that a scoped block never
585
+ * styles.
586
+ *
587
+ * @param {AST.TSRXElementNode} node
588
+ * @returns {AST.Node[] | null}
589
+ */
590
+ function get_sibling_container(node) {
591
+ const path = node.metadata?.path;
592
+ if (!path || !path.length) {
593
+ return null;
594
+ }
595
+
596
+ let i = path.length;
597
+
598
+ while (i--) {
599
+ const children = node_children(path[i]);
600
+ if (children.includes(node)) {
601
+ return children;
602
+ }
603
+ }
604
+
605
+ return null;
606
+ }
607
+
581
608
  /**
582
609
  * @param {AST.TSRXElementNode} node
583
610
  * @returns {AST.TSRXJSXElement | null}
package/src/index.js CHANGED
@@ -252,6 +252,7 @@ export {
252
252
  // Analyze
253
253
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
254
254
  export { prune_css as pruneCss } from './analyze/prune.js';
255
+ export { create_scope_root as createScopeRoot } from './transform/jsx/style-scopes.js';
255
256
  export {
256
257
  TSRX_DO_WHILE_STATEMENT_ERROR,
257
258
  TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
@@ -446,7 +446,9 @@ function prepare_scope(own, render_items, holder, state) {
446
446
  }
447
447
  const hash = sheets.length > 0 ? sheets[0][1].hash : null;
448
448
  const refs = collect_style_ref_attributes(own);
449
- const elements = collect_css_prunable_elements(render_items, [], ctx);
449
+ const elements = collect_css_prunable_elements(render_items, [], ctx, [
450
+ create_scope_root(render_items),
451
+ ]);
450
452
 
451
453
  /** @type {AST.CSS.StyleSheet | null} */
452
454
  let first_sheet = null;
@@ -566,12 +568,34 @@ export function apply_css_definition_metadata(
566
568
  }
567
569
  }
568
570
 
571
+ /**
572
+ * The root of a scope's ancestor paths: a fragment holding the list's items,
573
+ * so `prune_css` can find the siblings of a top-level item for `+` and `~`
574
+ * combinators. It is not an element, so ancestor combinators never match it —
575
+ * the container of a scope is exactly what a scoped block does not style.
576
+ *
577
+ * @param {AST.Node[]} items
578
+ * @returns {AST.Node}
579
+ */
580
+ export function create_scope_root(items) {
581
+ return /** @type {AST.Node} */ (
582
+ /** @type {unknown} */ ({
583
+ type: 'JSXFragment',
584
+ openingFragment: { type: 'JSXOpeningFragment' },
585
+ closingFragment: { type: 'JSXClosingFragment' },
586
+ children: items,
587
+ metadata: { path: [] },
588
+ })
589
+ );
590
+ }
591
+
569
592
  /**
570
593
  * Pruning runs before the walker stamps paths onto template nodes, so each
571
594
  * collected element gets its ancestor chain (`metadata.path`) here —
572
595
  * descendant/sibling selector matching in `prune_css` reads it. The scope's
573
596
  * own elements and those of nested scopes are collected; a component
574
- * boundary stops the walk.
597
+ * boundary stops the walk. Callers that collect a scope's items themselves
598
+ * should seed `path` with {@link create_scope_root} for the same reason.
575
599
  *
576
600
  * @param {AST.Node | AST.Node[]} value
577
601
  * @param {AST.TSRXJSXElement[]} [elements]
@@ -0,0 +1,16 @@
1
+ {
2
+ "elements": {
3
+ "a scope": ["scope"],
4
+ "b": ["scope"],
5
+ "c": ["scope"],
6
+ "wrap": ["scope"],
7
+ "x": ["scope"],
8
+ "y": ["scope"],
9
+ "d": ["scope"],
10
+ "e": ["scope"],
11
+ "g": ["scope"]
12
+ },
13
+ "cssOrder": ["scope"],
14
+ "pruned": [".f + .g"],
15
+ "classMaps": {}
16
+ }
@@ -0,0 +1,42 @@
1
+ // Sibling combinators between the items of one scope, and between the items
2
+ // of a branch fragment: both siblings carry the scope hash, so the rules are
3
+ // kept. Only the descendant chain through `.wrap` had a parent element to find
4
+ // siblings in before; the others sit at the top of their list.
5
+ export function SiblingCombinators({ open }: { open: boolean }) @{
6
+ <>
7
+ <style>
8
+ .scope {
9
+ --label: scope;
10
+ }
11
+ .a + .b {
12
+ color: red;
13
+ }
14
+ .a ~ .c {
15
+ color: blue;
16
+ }
17
+ .wrap .x + .y {
18
+ color: green;
19
+ }
20
+ .d + .e {
21
+ color: black;
22
+ }
23
+ .f + .g {
24
+ color: gray;
25
+ }
26
+ </style>
27
+ <div class="a scope">{'a'}</div>
28
+ <div class="b">{'b'}</div>
29
+ <div class="c">{'c'}</div>
30
+ <div class="wrap">
31
+ <span class="x">{'x'}</span>
32
+ <span class="y">{'y'}</span>
33
+ </div>
34
+ @if (open) {
35
+ <>
36
+ <i class="d">{'d'}</i>
37
+ <i class="e">{'e'}</i>
38
+ </>
39
+ }
40
+ <i class="g">{'g'}</i>
41
+ </>
42
+ }