@c9up/aurora 0.1.27 → 0.1.29
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/browser.js +5 -0
- package/dist/html.js +5 -0
- package/dist/http.js +5 -0
- package/dist/hydrate.js +5 -0
- package/dist/liveClient.js +5 -0
- package/dist/liveServer.d.ts +6 -1
- package/dist/render.js +5 -0
- package/dist/rpc.js +5 -0
- package/dist/server/renderPage.js +23 -7
- package/dist/ssr.js +50 -15
- package/package.json +2 -2
- package/src/browser.ts +5 -0
- package/src/html.ts +5 -0
- package/src/http.ts +5 -0
- package/src/hydrate.ts +5 -0
- package/src/liveClient.ts +5 -0
- package/src/liveServer.ts +9 -1
- package/src/render.ts +5 -0
- package/src/rpc.ts +5 -0
- package/src/server/renderPage.ts +23 -7
- package/src/ssr.ts +48 -13
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Browser DX helpers — navigation + a typed `localStorage` wrapper.
|
|
3
8
|
*
|
package/dist/html.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Tagged-template HTML parser.
|
|
3
8
|
*
|
package/dist/http.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* `HttpClient` — a small typed wrapper over `fetch` so call sites read
|
|
3
8
|
* `await http.get<User>("/auth/me")` instead of hand-rolling headers,
|
package/dist/hydrate.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Hydration — adopt SSR-rendered HTML in the browser without rebuilding
|
|
3
8
|
* the DOM.
|
package/dist/liveClient.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Live client runtime (Stage 5) — the thin browser side of live components.
|
|
3
8
|
*
|
package/dist/liveServer.d.ts
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
import type { LiveRouter } from "./liveRouter.js";
|
|
13
13
|
/** The slice of the host HTTP router this needs. */
|
|
14
14
|
export interface LiveHttpRouter {
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The handler returns `void | Promise<void>`, not `unknown`: that is what an
|
|
17
|
+
* HTTP handler returns in ream and in AdonisJS, and the wider shape made
|
|
18
|
+
* ream's own Router fail to satisfy this interface.
|
|
19
|
+
*/
|
|
20
|
+
post(path: string, handler: (ctx: LiveHttpContext) => void | Promise<void>): unknown;
|
|
16
21
|
}
|
|
17
22
|
/** The slice of the host HTTP context this needs (Ream's HttpContext satisfies it). */
|
|
18
23
|
export interface LiveHttpContext {
|
package/dist/render.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Render a `TemplateResult` to the DOM and keep it reactive.
|
|
3
8
|
*
|
package/dist/rpc.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint — aurora's thin binding
|
|
3
8
|
* over the agnostic {@link https://github.com/C9up/comet | @c9up/comet} client.
|
|
@@ -140,13 +140,29 @@ function escapeAttr(value) {
|
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
142
|
* Escape a JSON payload for safe embedding inside a `<script>` block.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
143
|
+
*
|
|
144
|
+
* The HTML parser ends the script on `</script>` and reinterprets `<!--` /
|
|
145
|
+
* `-->` as comment markers, whatever the JSON quoting says — so those
|
|
146
|
+
* characters must not survive literally. They are escaped as \uXXXX, which is
|
|
147
|
+
* valid JSON: a backslash escape like `\!` or `\>` is NOT, and made
|
|
148
|
+
* `JSON.parse` throw on the client the moment a prop contained a comment
|
|
149
|
+
* marker, taking the whole page's hydration with it.
|
|
146
150
|
*/
|
|
147
151
|
function escapeJsonForScript(value) {
|
|
148
|
-
return JSON.stringify(value)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (c) => {
|
|
153
|
+
switch (c) {
|
|
154
|
+
case "<":
|
|
155
|
+
return "\\u003c";
|
|
156
|
+
case ">":
|
|
157
|
+
return "\\u003e";
|
|
158
|
+
case "&":
|
|
159
|
+
return "\\u0026";
|
|
160
|
+
// Line separators are valid in JSON strings but terminate a JS line,
|
|
161
|
+
// so a script block carrying them raw is a syntax error.
|
|
162
|
+
case "\u2028":
|
|
163
|
+
return "\\u2028";
|
|
164
|
+
default:
|
|
165
|
+
return "\\u2029";
|
|
166
|
+
}
|
|
167
|
+
});
|
|
152
168
|
}
|
package/dist/ssr.js
CHANGED
|
@@ -46,6 +46,7 @@ function stringifyTemplateResult(result) {
|
|
|
46
46
|
// segment. This three-step coordination is why the loop holds a
|
|
47
47
|
// `pendingClosingQuote` flag.
|
|
48
48
|
let pendingClosingQuote = false;
|
|
49
|
+
const scanner = new TagScanner();
|
|
49
50
|
for (let i = 0; i < strings.length; i++) {
|
|
50
51
|
let segment = strings[i];
|
|
51
52
|
if (pendingClosingQuote) {
|
|
@@ -59,11 +60,14 @@ function stringifyTemplateResult(result) {
|
|
|
59
60
|
pendingClosingQuote = true;
|
|
60
61
|
}
|
|
61
62
|
out += segment;
|
|
63
|
+
scanner.consume(segment);
|
|
62
64
|
if (i < values.length && !skipValue) {
|
|
63
65
|
const value = values[i];
|
|
64
|
-
const inAttr =
|
|
66
|
+
const inAttr = scanner.insideTag;
|
|
65
67
|
if (inAttr) {
|
|
66
|
-
|
|
68
|
+
const rendered = stringifyValue(value, true);
|
|
69
|
+
out += rendered;
|
|
70
|
+
scanner.consume(rendered);
|
|
67
71
|
}
|
|
68
72
|
else {
|
|
69
73
|
// Text-region slot — ALWAYS wrap in boundary markers so the SSR
|
|
@@ -77,9 +81,13 @@ function stringifyTemplateResult(result) {
|
|
|
77
81
|
// (collapseMarkerRanges) so paths align exactly; the range also
|
|
78
82
|
// anchors scalar text updates and nested-template swaps. Same
|
|
79
83
|
// part-marker approach as lit-html / Solid.
|
|
84
|
+
const rendered = stringifyValue(value, false);
|
|
80
85
|
out += `<!--${SLOT_START}-->`;
|
|
81
|
-
out +=
|
|
86
|
+
out += rendered;
|
|
82
87
|
out += `<!--${SLOT_END}-->`;
|
|
88
|
+
// A text-region value may itself carry markup (a nested template
|
|
89
|
+
// or a SafeString), so it has to move the scanner too.
|
|
90
|
+
scanner.consume(rendered);
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
}
|
|
@@ -89,20 +97,47 @@ function stringifyTemplateResult(result) {
|
|
|
89
97
|
const SLOT_START = "$";
|
|
90
98
|
const SLOT_END = "/$";
|
|
91
99
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
100
|
+
* Tracks whether the cursor sits inside a tag, scanning FORWARD as the output
|
|
101
|
+
* grows.
|
|
102
|
+
*
|
|
103
|
+
* The obvious version walked backwards looking for the nearest `<` or `>`, but
|
|
104
|
+
* a `>` inside a quoted attribute value — `title="a > b"` — reads as the end of
|
|
105
|
+
* the tag, so the next interpolation is treated as a text slot and gets wrapped
|
|
106
|
+
* in `<!--$-->` markers INSIDE an attribute. That corrupts the markup and
|
|
107
|
+
* desyncs every following slot path at hydration. Quotes are what disambiguate,
|
|
108
|
+
* and they can only be resolved by reading forward.
|
|
109
|
+
*
|
|
110
|
+
* State is carried across appends instead of re-derived, so the whole render
|
|
111
|
+
* stays linear.
|
|
96
112
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
class TagScanner {
|
|
114
|
+
#inTag = false;
|
|
115
|
+
/** The quote character currently open inside a tag, or empty. */
|
|
116
|
+
#quote = "";
|
|
117
|
+
/** Feed everything appended since the last call. */
|
|
118
|
+
consume(chunk) {
|
|
119
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
120
|
+
const c = chunk[i];
|
|
121
|
+
if (this.#quote !== "") {
|
|
122
|
+
if (c === this.#quote)
|
|
123
|
+
this.#quote = "";
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (this.#inTag) {
|
|
127
|
+
if (c === '"' || c === "'")
|
|
128
|
+
this.#quote = c;
|
|
129
|
+
else if (c === ">")
|
|
130
|
+
this.#inTag = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (c === "<")
|
|
134
|
+
this.#inTag = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** True when the cursor is inside a tag — an attribute region. */
|
|
138
|
+
get insideTag() {
|
|
139
|
+
return this.#inTag;
|
|
104
140
|
}
|
|
105
|
-
return false;
|
|
106
141
|
}
|
|
107
142
|
function stringifyValue(value, inAttribute) {
|
|
108
143
|
if (value === null || value === undefined || value === false)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@c9up/comet": "^0.1.0",
|
|
45
|
-
"@c9up/ream": "^0.
|
|
45
|
+
"@c9up/ream": "^0.2.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
48
48
|
"@c9up/comet": {
|
package/src/browser.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Browser DX helpers — navigation + a typed `localStorage` wrapper.
|
|
3
8
|
*
|
package/src/html.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Tagged-template HTML parser.
|
|
3
8
|
*
|
package/src/http.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* `HttpClient` — a small typed wrapper over `fetch` so call sites read
|
|
3
8
|
* `await http.get<User>("/auth/me")` instead of hand-rolling headers,
|
package/src/hydrate.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Hydration — adopt SSR-rendered HTML in the browser without rebuilding
|
|
3
8
|
* the DOM.
|
package/src/liveClient.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Live client runtime (Stage 5) — the thin browser side of live components.
|
|
3
8
|
*
|
package/src/liveServer.ts
CHANGED
|
@@ -14,7 +14,15 @@ import type { LiveRouter } from "./liveRouter.js";
|
|
|
14
14
|
|
|
15
15
|
/** The slice of the host HTTP router this needs. */
|
|
16
16
|
export interface LiveHttpRouter {
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The handler returns `void | Promise<void>`, not `unknown`: that is what an
|
|
19
|
+
* HTTP handler returns in ream and in AdonisJS, and the wider shape made
|
|
20
|
+
* ream's own Router fail to satisfy this interface.
|
|
21
|
+
*/
|
|
22
|
+
post(
|
|
23
|
+
path: string,
|
|
24
|
+
handler: (ctx: LiveHttpContext) => void | Promise<void>,
|
|
25
|
+
): unknown;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
/** The slice of the host HTTP context this needs (Ream's HttpContext satisfies it). */
|
package/src/render.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Render a `TemplateResult` to the DOM and keep it reactive.
|
|
3
8
|
*
|
package/src/rpc.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// This file uses browser globals. The reference pulls the DOM lib in for
|
|
3
|
+
// THIS file whatever `lib` the consumer configured, so a Node app
|
|
4
|
+
// typechecking against our sources does not trip over `window` —
|
|
5
|
+
// `types: "./src/index.ts"` means every consumer reads them.
|
|
1
6
|
/**
|
|
2
7
|
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint — aurora's thin binding
|
|
3
8
|
* over the agnostic {@link https://github.com/C9up/comet | @c9up/comet} client.
|
package/src/server/renderPage.ts
CHANGED
|
@@ -278,13 +278,29 @@ function escapeAttr(value: string): string {
|
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
280
|
* Escape a JSON payload for safe embedding inside a `<script>` block.
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
281
|
+
*
|
|
282
|
+
* The HTML parser ends the script on `</script>` and reinterprets `<!--` /
|
|
283
|
+
* `-->` as comment markers, whatever the JSON quoting says — so those
|
|
284
|
+
* characters must not survive literally. They are escaped as \uXXXX, which is
|
|
285
|
+
* valid JSON: a backslash escape like `\!` or `\>` is NOT, and made
|
|
286
|
+
* `JSON.parse` throw on the client the moment a prop contained a comment
|
|
287
|
+
* marker, taking the whole page's hydration with it.
|
|
284
288
|
*/
|
|
285
289
|
function escapeJsonForScript(value: unknown): string {
|
|
286
|
-
return JSON.stringify(value)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (c) => {
|
|
291
|
+
switch (c) {
|
|
292
|
+
case "<":
|
|
293
|
+
return "\\u003c";
|
|
294
|
+
case ">":
|
|
295
|
+
return "\\u003e";
|
|
296
|
+
case "&":
|
|
297
|
+
return "\\u0026";
|
|
298
|
+
// Line separators are valid in JSON strings but terminate a JS line,
|
|
299
|
+
// so a script block carrying them raw is a syntax error.
|
|
300
|
+
case "\u2028":
|
|
301
|
+
return "\\u2028";
|
|
302
|
+
default:
|
|
303
|
+
return "\\u2029";
|
|
304
|
+
}
|
|
305
|
+
});
|
|
290
306
|
}
|
package/src/ssr.ts
CHANGED
|
@@ -50,6 +50,7 @@ function stringifyTemplateResult(result: TemplateResult): string {
|
|
|
50
50
|
// segment. This three-step coordination is why the loop holds a
|
|
51
51
|
// `pendingClosingQuote` flag.
|
|
52
52
|
let pendingClosingQuote = false;
|
|
53
|
+
const scanner = new TagScanner();
|
|
53
54
|
for (let i = 0; i < strings.length; i++) {
|
|
54
55
|
let segment = strings[i];
|
|
55
56
|
if (pendingClosingQuote) {
|
|
@@ -63,11 +64,14 @@ function stringifyTemplateResult(result: TemplateResult): string {
|
|
|
63
64
|
pendingClosingQuote = true;
|
|
64
65
|
}
|
|
65
66
|
out += segment;
|
|
67
|
+
scanner.consume(segment);
|
|
66
68
|
if (i < values.length && !skipValue) {
|
|
67
69
|
const value = values[i];
|
|
68
|
-
const inAttr =
|
|
70
|
+
const inAttr = scanner.insideTag;
|
|
69
71
|
if (inAttr) {
|
|
70
|
-
|
|
72
|
+
const rendered = stringifyValue(value, true);
|
|
73
|
+
out += rendered;
|
|
74
|
+
scanner.consume(rendered);
|
|
71
75
|
} else {
|
|
72
76
|
// Text-region slot — ALWAYS wrap in boundary markers so the SSR
|
|
73
77
|
// node structure matches the client template, which keeps exactly
|
|
@@ -80,9 +84,13 @@ function stringifyTemplateResult(result: TemplateResult): string {
|
|
|
80
84
|
// (collapseMarkerRanges) so paths align exactly; the range also
|
|
81
85
|
// anchors scalar text updates and nested-template swaps. Same
|
|
82
86
|
// part-marker approach as lit-html / Solid.
|
|
87
|
+
const rendered = stringifyValue(value, false);
|
|
83
88
|
out += `<!--${SLOT_START}-->`;
|
|
84
|
-
out +=
|
|
89
|
+
out += rendered;
|
|
85
90
|
out += `<!--${SLOT_END}-->`;
|
|
91
|
+
// A text-region value may itself carry markup (a nested template
|
|
92
|
+
// or a SafeString), so it has to move the scanner too.
|
|
93
|
+
scanner.consume(rendered);
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
}
|
|
@@ -94,18 +102,45 @@ const SLOT_START = "$";
|
|
|
94
102
|
const SLOT_END = "/$";
|
|
95
103
|
|
|
96
104
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
105
|
+
* Tracks whether the cursor sits inside a tag, scanning FORWARD as the output
|
|
106
|
+
* grows.
|
|
107
|
+
*
|
|
108
|
+
* The obvious version walked backwards looking for the nearest `<` or `>`, but
|
|
109
|
+
* a `>` inside a quoted attribute value — `title="a > b"` — reads as the end of
|
|
110
|
+
* the tag, so the next interpolation is treated as a text slot and gets wrapped
|
|
111
|
+
* in `<!--$-->` markers INSIDE an attribute. That corrupts the markup and
|
|
112
|
+
* desyncs every following slot path at hydration. Quotes are what disambiguate,
|
|
113
|
+
* and they can only be resolved by reading forward.
|
|
114
|
+
*
|
|
115
|
+
* State is carried across appends instead of re-derived, so the whole render
|
|
116
|
+
* stays linear.
|
|
101
117
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
class TagScanner {
|
|
119
|
+
#inTag = false;
|
|
120
|
+
/** The quote character currently open inside a tag, or empty. */
|
|
121
|
+
#quote = "";
|
|
122
|
+
|
|
123
|
+
/** Feed everything appended since the last call. */
|
|
124
|
+
consume(chunk: string): void {
|
|
125
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
126
|
+
const c = chunk[i];
|
|
127
|
+
if (this.#quote !== "") {
|
|
128
|
+
if (c === this.#quote) this.#quote = "";
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (this.#inTag) {
|
|
132
|
+
if (c === '"' || c === "'") this.#quote = c;
|
|
133
|
+
else if (c === ">") this.#inTag = false;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (c === "<") this.#inTag = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** True when the cursor is inside a tag — an attribute region. */
|
|
141
|
+
get insideTag(): boolean {
|
|
142
|
+
return this.#inTag;
|
|
107
143
|
}
|
|
108
|
-
return false;
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
function stringifyValue(value: unknown, inAttribute: boolean): string {
|