@jxsuite/runtime 5.2.0 → 6.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/runtime",
3
- "version": "5.2.0",
3
+ "version": "6.0.0",
4
4
  "description": "JSON-native reactive web component runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "upgrade": "bun update --latest && bun install"
34
34
  },
35
35
  "dependencies": {
36
- "@jxsuite/schema": "^4.0.0",
36
+ "@jxsuite/schema": "^4.1.0",
37
37
  "@vue/reactivity": "3.5.44"
38
38
  },
39
39
  "devDependencies": {
package/src/css.ts CHANGED
@@ -122,7 +122,8 @@ export function isKeyframesAtRule(atKey: string): boolean {
122
122
  /**
123
123
  * Resolve one nested style key against its parent selector.
124
124
  *
125
- * `&` splices, `:`/`.`/`[` concatenate, anything else is a descendant. Extracted because the same
125
+ * A key with an `&` anywhere splices the scope in for each one (`& > li`, `.wrap &`), else
126
+ * `:`/`.`/`[` concatenate, and anything else is a descendant. Extracted because the same
126
127
  * four-branch decision was written THREE times in `applyStyle` — the top-level nested loop,
127
128
  * `emitNested`'s recursion and `emitMediaNested` — and three copies of one decision is three
128
129
  * chances for a selector to mean different things depending on how deeply it was nested.
@@ -143,8 +144,9 @@ export function resolveNestedSelector(scope: string, key: string): string {
143
144
 
144
145
  /** One member of a scope against one member of a key — the four-branch decision itself. */
145
146
  function resolveOneNestedSelector(scope: string, key: string): string {
146
- if (key.startsWith("&")) {
147
- return key.replaceAll("&", scope);
147
+ const spliced = spliceNestingSelector(key, scope);
148
+ if (spliced !== null) {
149
+ return spliced;
148
150
  }
149
151
  if (key.startsWith("[") || key.startsWith(":") || key.startsWith(".")) {
150
152
  return `${scope}${key}`;
@@ -152,6 +154,70 @@ function resolveOneNestedSelector(scope: string, key: string): string {
152
154
  return `${scope} ${key}`;
153
155
  }
154
156
 
157
+ /**
158
+ * Where a selector member holds the nesting selector `&`: outside quoted strings and attribute
159
+ * brackets, where an `&` is text (`[title="R&D"]`) and not the scope.
160
+ *
161
+ * The scope is wherever `&` stands, not only at the start. `.wrap &` is the element when it sits
162
+ * inside a `.wrap`, and `:root &` is the element under the root. Splicing only a LEADING `&` read
163
+ * such a key as a compound and emitted `#box.wrap &`: a selector with a raw `&`, which the browser
164
+ * discards, so the rule never applied anywhere (spec.md §9.2).
165
+ *
166
+ * @param {string} member - One member of a selector list
167
+ * @returns {number[]} The indexes of its `&` characters, in order
168
+ */
169
+ function nestingIndexes(member: string): number[] {
170
+ const found: number[] = [];
171
+ let quote: string | null = null;
172
+ let brackets = 0;
173
+ for (let i = 0; i < member.length; i += 1) {
174
+ const ch = member[i]!;
175
+ if (quote) {
176
+ if (ch === "\\") {
177
+ i += 1;
178
+ } else if (ch === quote) {
179
+ quote = null;
180
+ }
181
+ continue;
182
+ }
183
+ if (ch === "\\") {
184
+ i += 1;
185
+ } else if (ch === '"' || ch === "'") {
186
+ quote = ch;
187
+ } else if (ch === "[") {
188
+ brackets += 1;
189
+ } else if (ch === "]") {
190
+ brackets = Math.max(0, brackets - 1);
191
+ } else if (ch === "&" && brackets === 0) {
192
+ found.push(i);
193
+ }
194
+ }
195
+ return found;
196
+ }
197
+
198
+ /**
199
+ * Replace every nesting `&` of a selector member with `scope`, or null when it holds none: the one
200
+ * rule every resolver of a nested key shares, so `.wrap &` means the same in every emitter.
201
+ *
202
+ * @param {string} member - One member of a selector list
203
+ * @param {string} scope - What `&` stands for
204
+ * @returns {string | null}
205
+ */
206
+ export function spliceNestingSelector(member: string, scope: string): string | null {
207
+ return nestingIndexes(member).length > 0 ? spliceScope(member, scope) : null;
208
+ }
209
+
210
+ /** Replace every nesting `&` of `member` ({@link nestingIndexes}) with `scope`. */
211
+ function spliceScope(member: string, scope: string): string {
212
+ let out = "";
213
+ let from = 0;
214
+ for (const at of nestingIndexes(member)) {
215
+ out += member.slice(from, at) + scope;
216
+ from = at + 1;
217
+ }
218
+ return out + member.slice(from);
219
+ }
220
+
155
221
  /**
156
222
  * Split a selector list on its top-level commas.
157
223
  *
@@ -609,7 +675,11 @@ function withoutKeyframes(block: JxStyle): JxStyle {
609
675
  * {@link CssRuleTarget}, so it is decided once, here.
610
676
  */
611
677
  function compoundsOntoScope(key: string): boolean {
612
- const rest = key.startsWith("&") ? key.slice(1) : key;
678
+ /* With an `&` anywhere, what follows the last one decides: `.wrap &:hover` is the element in a
679
+ context, `.wrap & span` something inside it. */
680
+ const nesting = nestingIndexes(key);
681
+ const last = nesting.at(-1);
682
+ const rest = last === undefined ? key : key.slice(last + 1);
613
683
  return rest === "" || rest.startsWith(":") || rest.startsWith(".") || rest.startsWith("[");
614
684
  }
615
685
 
package/src/pointer.ts CHANGED
@@ -95,6 +95,32 @@ export function refAccessor(base: string, path: string): string {
95
95
  return expr;
96
96
  }
97
97
 
98
+ /**
99
+ * A JavaScript expression reading `path` from `base` that answers `undefined` through a missing
100
+ * step instead of throwing: `s.user?.name`, where {@link refAccessor} emits `s.user.name`.
101
+ *
102
+ * Same member forms as `refAccessor` (dotted where a segment is an identifier, bracketed and
103
+ * JSON-quoted where it is not), with every step after the first optional. That is what the
104
+ * interpreter's {@link readPath} already answers for a missing intermediate, so a compiled read of
105
+ * state a server call or a fetch fills in later agrees with it while the value is still `null`. The
106
+ * accessor stays as it is for the reads that predate this, because making every compiled read
107
+ * optional is a change to what every compiled site does at runtime (see the equivalence tests).
108
+ *
109
+ * @param {string} base - Receiver expression, e.g. `state`
110
+ * @param {string} path
111
+ * @returns {string}
112
+ */
113
+ export function refReader(base: string, path: string): string {
114
+ let expr = base;
115
+ for (const [index, key] of refSegments(path).entries()) {
116
+ const optional = index === 0 ? "" : "?.";
117
+ expr += IDENTIFIER.test(key)
118
+ ? `${index === 0 ? "." : optional}${key}`
119
+ : `${optional}[${JSON.stringify(key)}]`;
120
+ }
121
+ return expr;
122
+ }
123
+
98
124
  /**
99
125
  * An object-literal key: bare where it is an identifier, JSON-quoted where it is not.
100
126
  *
package/src/runtime.ts CHANGED
@@ -4307,6 +4307,7 @@ export {
4307
4307
  resolveAtQuery,
4308
4308
  resolveNestedSelector,
4309
4309
  schemeSelectors,
4310
+ spliceNestingSelector,
4310
4311
  splitSelectorList,
4311
4312
  transposeCanvasOverlaySelector,
4312
4313
  transposeCanvasPopoverSelector,