@caretcms/caretize 0.1.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/README.md +70 -0
- package/dist/backup.d.ts +30 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +89 -0
- package/dist/backup.js.map +1 -0
- package/dist/bind-collection.d.ts +56 -0
- package/dist/bind-collection.d.ts.map +1 -0
- package/dist/bind-collection.js +140 -0
- package/dist/bind-collection.js.map +1 -0
- package/dist/bind-route.d.ts +40 -0
- package/dist/bind-route.d.ts.map +1 -0
- package/dist/bind-route.js +150 -0
- package/dist/bind-route.js.map +1 -0
- package/dist/cli-args.d.ts +30 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +118 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +356 -0
- package/dist/cli.js.map +1 -0
- package/dist/detect.d.ts +76 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +237 -0
- package/dist/detect.js.map +1 -0
- package/dist/discover.d.ts +13 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +84 -0
- package/dist/discover.js.map +1 -0
- package/dist/frontmatter.d.ts +26 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +52 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/identifiers.d.ts +26 -0
- package/dist/identifiers.d.ts.map +1 -0
- package/dist/identifiers.js +34 -0
- package/dist/identifiers.js.map +1 -0
- package/dist/import-wrap.d.ts +56 -0
- package/dist/import-wrap.d.ts.map +1 -0
- package/dist/import-wrap.js +149 -0
- package/dist/import-wrap.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/name.d.ts +49 -0
- package/dist/name.d.ts.map +1 -0
- package/dist/name.js +164 -0
- package/dist/name.js.map +1 -0
- package/dist/output.d.ts +30 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +110 -0
- package/dist/output.js.map +1 -0
- package/dist/parse.d.ts +86 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +92 -0
- package/dist/parse.js.map +1 -0
- package/dist/plan.d.ts +46 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +76 -0
- package/dist/plan.js.map +1 -0
- package/dist/preflight.d.ts +19 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +95 -0
- package/dist/preflight.js.map +1 -0
- package/dist/prop-hoist.d.ts +67 -0
- package/dist/prop-hoist.d.ts.map +1 -0
- package/dist/prop-hoist.js +232 -0
- package/dist/prop-hoist.js.map +1 -0
- package/dist/props.d.ts +38 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +116 -0
- package/dist/props.js.map +1 -0
- package/dist/report.d.ts +42 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +35 -0
- package/dist/report.js.map +1 -0
- package/dist/resolve.d.ts +15 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +35 -0
- package/dist/resolve.js.map +1 -0
- package/dist/run.d.ts +52 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +213 -0
- package/dist/run.js.map +1 -0
- package/dist/splice.d.ts +43 -0
- package/dist/splice.d.ts.map +1 -0
- package/dist/splice.js +90 -0
- package/dist/splice.js.map +1 -0
- package/dist/usage.d.ts +90 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +249 -0
- package/dist/usage.js.map +1 -0
- package/dist/wrap.d.ts +72 -0
- package/dist/wrap.d.ts.map +1 -0
- package/dist/wrap.js +170 -0
- package/dist/wrap.js.map +1 -0
- package/dist/write.d.ts +28 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +37 -0
- package/dist/write.js.map +1 -0
- package/package.json +54 -0
package/dist/run.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run orchestration. The atomicity model is "validate everything in memory,
|
|
3
|
+
* then write": every file's tagged output is computed and verified (re-parses +
|
|
4
|
+
* is pure-insertion-reversible) BEFORE a single byte hits disk. If any file
|
|
5
|
+
* fails verification, the run aborts having written nothing. During the write
|
|
6
|
+
* phase, backups are taken per file; if a write throws, everything written so
|
|
7
|
+
* far is restored from those backups.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { parseAstro } from "./parse.js";
|
|
12
|
+
import { applyTags } from "./write.js";
|
|
13
|
+
import { wrapConst } from "./wrap.js";
|
|
14
|
+
import { wrapImport } from "./import-wrap.js";
|
|
15
|
+
import { hoistPropLiterals, verifyHoistResult } from "./prop-hoist.js";
|
|
16
|
+
import { writeBackup, restoreBackups } from "./backup.js";
|
|
17
|
+
/** Apply the right editable() wrap for a target's origin. */
|
|
18
|
+
function applyWrap(source, t) {
|
|
19
|
+
return t.origin === "import"
|
|
20
|
+
? wrapImport(source, t.varName, t.key)
|
|
21
|
+
: wrapConst(source, t.varName, t.key);
|
|
22
|
+
}
|
|
23
|
+
/** Compute and verify a single file's tagged output. No disk access. */
|
|
24
|
+
export async function prepareFile(relPath, source, items) {
|
|
25
|
+
const base = {
|
|
26
|
+
relPath, source, output: source, inserted: [], tagCount: 0, ok: true,
|
|
27
|
+
};
|
|
28
|
+
if (items.length === 0)
|
|
29
|
+
return base;
|
|
30
|
+
const { output, inserted, failures } = applyTags(source, items);
|
|
31
|
+
if (failures.length > 0) {
|
|
32
|
+
return { ...base, ok: false, reason: `could not place ${failures.length} tag(s)` };
|
|
33
|
+
}
|
|
34
|
+
// Gate 1: the result must still be valid Astro.
|
|
35
|
+
try {
|
|
36
|
+
await parseAstro(output);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return { ...base, ok: false, reason: `output failed to re-parse: ${err.message}` };
|
|
40
|
+
}
|
|
41
|
+
// Gate 2: pure-insertion — stripping the inserted attrs restores the original.
|
|
42
|
+
let restored = output;
|
|
43
|
+
for (const attr of inserted) {
|
|
44
|
+
const idx = restored.indexOf(attr);
|
|
45
|
+
if (idx < 0)
|
|
46
|
+
return { ...base, ok: false, reason: "inserted attribute not found on verify" };
|
|
47
|
+
restored = restored.slice(0, idx) + restored.slice(idx + attr.length);
|
|
48
|
+
}
|
|
49
|
+
if (restored !== source) {
|
|
50
|
+
return { ...base, ok: false, reason: "output is not a pure insertion of the original" };
|
|
51
|
+
}
|
|
52
|
+
return { relPath, source, output, inserted, tagCount: items.length, ok: true };
|
|
53
|
+
}
|
|
54
|
+
/** True when every character of `needle` appears in `haystack` in order. */
|
|
55
|
+
function isSubsequence(needle, haystack) {
|
|
56
|
+
let i = 0;
|
|
57
|
+
for (let j = 0; j < haystack.length && i < needle.length; j++) {
|
|
58
|
+
if (haystack[j] === needle[i])
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
return i === needle.length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Compute and verify a single file's editable()-wrapped output (Tier-1). Same
|
|
65
|
+
* atomicity contract as prepareFile, so the result feeds commitRun unchanged.
|
|
66
|
+
*
|
|
67
|
+
* Gate 2 here is a SUBSEQUENCE check rather than strip-by-string: the inserted
|
|
68
|
+
* `await editable(…, ` / `)` spans contain characters (`)`, commas) that also
|
|
69
|
+
* occur elsewhere, so verifying "original is a subsequence of output" is the
|
|
70
|
+
* robust way to prove pure insertion (no original char deleted or reordered).
|
|
71
|
+
*/
|
|
72
|
+
export async function prepareWrapFile(relPath, source, targets) {
|
|
73
|
+
const base = {
|
|
74
|
+
relPath, source, output: source, inserted: [], tagCount: 0, ok: true,
|
|
75
|
+
};
|
|
76
|
+
if (targets.length === 0)
|
|
77
|
+
return base;
|
|
78
|
+
let output = source;
|
|
79
|
+
const inserted = [];
|
|
80
|
+
for (const t of targets) {
|
|
81
|
+
const result = applyWrap(output, t);
|
|
82
|
+
if (!result.ok)
|
|
83
|
+
return { ...base, ok: false, reason: `${t.varName}: ${result.reason}` };
|
|
84
|
+
if (result.alreadyWrapped)
|
|
85
|
+
continue;
|
|
86
|
+
inserted.push(`editable(${JSON.stringify(t.key)}) around ${t.varName}`);
|
|
87
|
+
output = result.output;
|
|
88
|
+
}
|
|
89
|
+
if (inserted.length === 0)
|
|
90
|
+
return base; // nothing applicable (already wrapped)
|
|
91
|
+
// Gate 1: the result must still be valid Astro.
|
|
92
|
+
try {
|
|
93
|
+
await parseAstro(output);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return { ...base, ok: false, reason: `output failed to re-parse: ${err.message}` };
|
|
97
|
+
}
|
|
98
|
+
// Gate 2: pure insertion — every original character survives, in order.
|
|
99
|
+
if (!isSubsequence(source, output)) {
|
|
100
|
+
return { ...base, ok: false, reason: "output is not a pure insertion of the original" };
|
|
101
|
+
}
|
|
102
|
+
return { relPath, source, output, inserted, tagCount: inserted.length, ok: true };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Compute and verify a file's output with BOTH passes: attribute tags and
|
|
106
|
+
* editable() wraps. Tags are applied first (offset-based, in the template), then
|
|
107
|
+
* wraps (content-scan, in the frontmatter) — so the frontmatter insertions never
|
|
108
|
+
* shift the template tag offsets. Same atomicity gates; one PreparedFile out.
|
|
109
|
+
*/
|
|
110
|
+
export async function prepareFileFull(relPath, source, items, targets, hoistTargets = []) {
|
|
111
|
+
const base = {
|
|
112
|
+
relPath, source, output: source, inserted: [], tagCount: 0, ok: true,
|
|
113
|
+
};
|
|
114
|
+
if (items.length === 0 && targets.length === 0 && hoistTargets.length === 0)
|
|
115
|
+
return base;
|
|
116
|
+
let output = source;
|
|
117
|
+
const inserted = [];
|
|
118
|
+
// Pass 1: attribute tags (template).
|
|
119
|
+
if (items.length > 0) {
|
|
120
|
+
const tagged = applyTags(source, items);
|
|
121
|
+
if (tagged.failures.length > 0) {
|
|
122
|
+
return { ...base, ok: false, reason: `could not place ${tagged.failures.length} tag(s)` };
|
|
123
|
+
}
|
|
124
|
+
output = tagged.output;
|
|
125
|
+
inserted.push(...tagged.inserted);
|
|
126
|
+
}
|
|
127
|
+
// Pass 2: editable() wraps (frontmatter) — wrapConst for consts, wrapImport
|
|
128
|
+
// for default data imports. Both are pure insertion.
|
|
129
|
+
let wrapCount = 0;
|
|
130
|
+
for (const t of targets) {
|
|
131
|
+
const wrapped = applyWrap(output, t);
|
|
132
|
+
if (!wrapped.ok)
|
|
133
|
+
return { ...base, ok: false, reason: `${t.varName}: ${wrapped.reason}` };
|
|
134
|
+
if (wrapped.alreadyWrapped)
|
|
135
|
+
continue;
|
|
136
|
+
inserted.push(`editable(${JSON.stringify(t.key)}) around ${t.varName}`);
|
|
137
|
+
output = wrapped.output;
|
|
138
|
+
wrapCount++;
|
|
139
|
+
}
|
|
140
|
+
if (output === source && hoistTargets.length === 0)
|
|
141
|
+
return base; // nothing changed
|
|
142
|
+
// Passes 1+2 are pure insertion: verify the running output is a subsequence of
|
|
143
|
+
// the original BEFORE the (deletion-bearing) hoist pass runs.
|
|
144
|
+
const afterWraps = output;
|
|
145
|
+
if (!isSubsequence(source, afterWraps)) {
|
|
146
|
+
return { ...base, ok: false, reason: "output is not a pure insertion of the original" };
|
|
147
|
+
}
|
|
148
|
+
// Pass 3 (LAST): prop-hoist rewrite. Self-locating, so it runs after the
|
|
149
|
+
// offset-based tag pass. Verified by an inverse gate against `afterWraps`.
|
|
150
|
+
let hoistCount = 0;
|
|
151
|
+
if (hoistTargets.length > 0) {
|
|
152
|
+
const h = hoistPropLiterals(afterWraps, hoistTargets);
|
|
153
|
+
if (!h.ok)
|
|
154
|
+
return { ...base, ok: false, reason: `hoist: ${h.reason}` };
|
|
155
|
+
if (h.propsRewritten > 0) {
|
|
156
|
+
if (!verifyHoistResult(afterWraps, h.output, hoistTargets, h.addedImport)) {
|
|
157
|
+
return { ...base, ok: false, reason: "hoist not reversible to pre-hoist source" };
|
|
158
|
+
}
|
|
159
|
+
output = h.output;
|
|
160
|
+
hoistCount = h.propsRewritten;
|
|
161
|
+
inserted.push(...h.constsDeclared.map((c) => `editable() hoist → const ${c}`));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (output === source)
|
|
165
|
+
return base; // nothing actually changed
|
|
166
|
+
// Gate 1: still valid Astro.
|
|
167
|
+
try {
|
|
168
|
+
await parseAstro(output);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
return { ...base, ok: false, reason: `output failed to re-parse: ${err.message}` };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
relPath, source, output, inserted,
|
|
175
|
+
tagCount: items.length + wrapCount + hoistCount, ok: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Write verified files to disk with backups. Throws (after rolling back) if any
|
|
180
|
+
* prepared file failed verification or any write fails.
|
|
181
|
+
*/
|
|
182
|
+
export function commitRun(rootDir, prepared, stamp) {
|
|
183
|
+
const toWrite = prepared.filter((p) => p.tagCount > 0);
|
|
184
|
+
const bad = toWrite.find((p) => !p.ok);
|
|
185
|
+
if (bad) {
|
|
186
|
+
throw new Error(`refusing to write: ${bad.relPath} failed verification (${bad.reason})`);
|
|
187
|
+
}
|
|
188
|
+
const written = [];
|
|
189
|
+
const backups = [];
|
|
190
|
+
const thisRun = [];
|
|
191
|
+
try {
|
|
192
|
+
for (const file of toWrite) {
|
|
193
|
+
const backupAbs = writeBackup(rootDir, file.relPath, stamp);
|
|
194
|
+
backups.push(backupAbs);
|
|
195
|
+
thisRun.push({ relPath: file.relPath, backupAbs });
|
|
196
|
+
writeFileSync(resolve(rootDir, file.relPath), file.output, "utf8");
|
|
197
|
+
written.push(file.relPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
// Roll back exactly the files THIS run backed up — never "the latest
|
|
202
|
+
// stamp", which could be a previous run's backups if we died before
|
|
203
|
+
// writing our first one.
|
|
204
|
+
restoreBackups(rootDir, thisRun);
|
|
205
|
+
throw new Error(`write failed, rolled back: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
return { written, backups };
|
|
208
|
+
}
|
|
209
|
+
/** Convenience: read a file from disk for preparation. */
|
|
210
|
+
export function readSource(rootDir, relPath) {
|
|
211
|
+
return readFileSync(resolve(rootDir, relPath), "utf8");
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=run.js.map
|
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
|
package/dist/splice.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte-correct splice core for caretize.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists at all: `@astrojs/compiler` reports node positions as
|
|
5
|
+
* **0-based UTF-8 byte offsets** (see `Point.offset` in its AST types), but
|
|
6
|
+
* JavaScript string indexing counts UTF-16 code units. The two diverge the
|
|
7
|
+
* moment a file contains a multibyte character (em-dash, curly quote,
|
|
8
|
+
* box-drawing glyph, emoji) before an element. Splicing at a byte offset into a
|
|
9
|
+
* JS string therefore lands in the wrong place and silently corrupts the tag.
|
|
10
|
+
*
|
|
11
|
+
* The rule, proven by spike against the example corpus: do ALL offset
|
|
12
|
+
* arithmetic and splicing on a UTF-8 `Buffer`, never on a string. Everything in
|
|
13
|
+
* this module takes and returns `Buffer`s for that reason.
|
|
14
|
+
*
|
|
15
|
+
* We use the compiler AST for DETECTION only. Writing is pure insertion of an
|
|
16
|
+
* attribute just before the `>` of an opening tag — never AST reprinting — so
|
|
17
|
+
* the user's formatting, comments, and whitespace are preserved byte-for-byte.
|
|
18
|
+
*/
|
|
19
|
+
export interface OpenTagEnd {
|
|
20
|
+
/** Byte offset at which to insert an attribute (just before `>` or `/>`). */
|
|
21
|
+
insertAt: number;
|
|
22
|
+
/** Byte offset of the closing `>` itself. */
|
|
23
|
+
gtOffset: number;
|
|
24
|
+
/** Whether the tag is self-closing (`<img ... />`). */
|
|
25
|
+
selfClosing: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Given the byte offset of an opening tag's `<`, find where its opening tag
|
|
29
|
+
* ends. Scans forward to the `>` that is not inside a quoted attribute value or
|
|
30
|
+
* a `{...}` expression (Astro attributes can hold JS expressions and template
|
|
31
|
+
* literals that legitimately contain `>`), so we don't stop early.
|
|
32
|
+
*
|
|
33
|
+
* Returns null if `startOffset` isn't a `<` or no closing `>` is found.
|
|
34
|
+
*/
|
|
35
|
+
export declare function findOpenTagEnd(buf: Buffer, startOffset: number): OpenTagEnd | null;
|
|
36
|
+
/**
|
|
37
|
+
* Insert `attribute` (e.g. ` data-caret="pages::home::headline"`, including its
|
|
38
|
+
* leading space) at the given byte offset, returning a new buffer. Pure
|
|
39
|
+
* insertion: removing the inserted bytes yields the original exactly, which is
|
|
40
|
+
* the reversibility invariant the writer relies on for backups/undo.
|
|
41
|
+
*/
|
|
42
|
+
export declare function spliceAttribute(buf: Buffer, insertAt: number, attribute: string): Buffer;
|
|
43
|
+
//# sourceMappingURL=splice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splice.d.ts","sourceRoot":"","sources":["../src/splice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAaH,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,GAClB,UAAU,GAAG,IAAI,CA0CnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,CAOR"}
|
package/dist/splice.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte-correct splice core for caretize.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists at all: `@astrojs/compiler` reports node positions as
|
|
5
|
+
* **0-based UTF-8 byte offsets** (see `Point.offset` in its AST types), but
|
|
6
|
+
* JavaScript string indexing counts UTF-16 code units. The two diverge the
|
|
7
|
+
* moment a file contains a multibyte character (em-dash, curly quote,
|
|
8
|
+
* box-drawing glyph, emoji) before an element. Splicing at a byte offset into a
|
|
9
|
+
* JS string therefore lands in the wrong place and silently corrupts the tag.
|
|
10
|
+
*
|
|
11
|
+
* The rule, proven by spike against the example corpus: do ALL offset
|
|
12
|
+
* arithmetic and splicing on a UTF-8 `Buffer`, never on a string. Everything in
|
|
13
|
+
* this module takes and returns `Buffer`s for that reason.
|
|
14
|
+
*
|
|
15
|
+
* We use the compiler AST for DETECTION only. Writing is pure insertion of an
|
|
16
|
+
* attribute just before the `>` of an opening tag — never AST reprinting — so
|
|
17
|
+
* the user's formatting, comments, and whitespace are preserved byte-for-byte.
|
|
18
|
+
*/
|
|
19
|
+
// ASCII byte constants (all the structural characters we scan for are ASCII,
|
|
20
|
+
// so single-byte comparisons on the UTF-8 buffer are safe).
|
|
21
|
+
const LT = 0x3c; // <
|
|
22
|
+
const GT = 0x3e; // >
|
|
23
|
+
const SLASH = 0x2f; // /
|
|
24
|
+
const LBRACE = 0x7b; // {
|
|
25
|
+
const RBRACE = 0x7d; // }
|
|
26
|
+
const DQUOTE = 0x22; // "
|
|
27
|
+
const SQUOTE = 0x27; // '
|
|
28
|
+
const BACKTICK = 0x60; // `
|
|
29
|
+
/**
|
|
30
|
+
* Given the byte offset of an opening tag's `<`, find where its opening tag
|
|
31
|
+
* ends. Scans forward to the `>` that is not inside a quoted attribute value or
|
|
32
|
+
* a `{...}` expression (Astro attributes can hold JS expressions and template
|
|
33
|
+
* literals that legitimately contain `>`), so we don't stop early.
|
|
34
|
+
*
|
|
35
|
+
* Returns null if `startOffset` isn't a `<` or no closing `>` is found.
|
|
36
|
+
*/
|
|
37
|
+
export function findOpenTagEnd(buf, startOffset) {
|
|
38
|
+
let i = startOffset;
|
|
39
|
+
if (buf[i] !== LT)
|
|
40
|
+
return null;
|
|
41
|
+
i++;
|
|
42
|
+
let braceDepth = 0;
|
|
43
|
+
while (i < buf.length) {
|
|
44
|
+
const b = buf[i];
|
|
45
|
+
// Skip quoted attribute values (only when not inside an expression).
|
|
46
|
+
if (braceDepth === 0 && (b === DQUOTE || b === SQUOTE || b === BACKTICK)) {
|
|
47
|
+
const quote = b;
|
|
48
|
+
i++;
|
|
49
|
+
while (i < buf.length && buf[i] !== quote)
|
|
50
|
+
i++;
|
|
51
|
+
i++; // step past the closing quote
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (b === LBRACE) {
|
|
55
|
+
braceDepth++;
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (b === RBRACE) {
|
|
60
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (braceDepth === 0 && b === GT) {
|
|
65
|
+
const selfClosing = buf[i - 1] === SLASH;
|
|
66
|
+
return {
|
|
67
|
+
insertAt: selfClosing ? i - 1 : i,
|
|
68
|
+
gtOffset: i,
|
|
69
|
+
selfClosing,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Insert `attribute` (e.g. ` data-caret="pages::home::headline"`, including its
|
|
78
|
+
* leading space) at the given byte offset, returning a new buffer. Pure
|
|
79
|
+
* insertion: removing the inserted bytes yields the original exactly, which is
|
|
80
|
+
* the reversibility invariant the writer relies on for backups/undo.
|
|
81
|
+
*/
|
|
82
|
+
export function spliceAttribute(buf, insertAt, attribute) {
|
|
83
|
+
const attrBuf = Buffer.from(attribute, "utf8");
|
|
84
|
+
return Buffer.concat([
|
|
85
|
+
buf.subarray(0, insertAt),
|
|
86
|
+
attrBuf,
|
|
87
|
+
buf.subarray(insertAt),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=splice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splice.js","sourceRoot":"","sources":["../src/splice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,6EAA6E;AAC7E,4DAA4D;AAC5D,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI;AACrB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI;AACrB,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI;AACxB,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,IAAI;AACzB,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,IAAI;AACzB,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,IAAI;AACzB,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,IAAI;AACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,IAAI;AAW3B;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,WAAmB;IAEnB,IAAI,CAAC,GAAG,WAAW,CAAC;IACpB,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC/B,CAAC,EAAE,CAAC;IAEJ,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAEjB,qEAAqE;QACrE,IAAI,UAAU,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC;YACzE,MAAM,KAAK,GAAG,CAAC,CAAC;YAChB,CAAC,EAAE,CAAC;YACJ,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK;gBAAE,CAAC,EAAE,CAAC;YAC/C,CAAC,EAAE,CAAC,CAAC,8BAA8B;YACnC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,MAAM,EAAE,CAAC;YACjB,UAAU,EAAE,CAAC;YACb,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,MAAM,EAAE,CAAC;YACjB,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;YACzC,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QAED,IAAI,UAAU,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;YACjC,MAAM,WAAW,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC;YACzC,OAAO;gBACL,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjC,QAAQ,EAAE,CAAC;gBACX,WAAW;aACZ,CAAC;QACJ,CAAC;QAED,CAAC,EAAE,CAAC;IACN,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAW,EACX,QAAgB,EAChB,SAAiB;IAEjB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC;QACzB,OAAO;QACP,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;KACvB,CAAC,CAAC;AACL,CAAC"}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage safety classifier — the single safety primitive both wrap tiers depend on.
|
|
3
|
+
*
|
|
4
|
+
* `editable()` stega-encodes EVERY string leaf of the value it wraps (see
|
|
5
|
+
* `@caretcms/core`'s encodeLeaves). An encoded string is invisible and harmless
|
|
6
|
+
* when it renders as visible TEXT, but it CORRUPTS anything it lands in as an
|
|
7
|
+
* attribute — a broken href/src/class/id/datetime. So a literal const is only
|
|
8
|
+
* safe to wrap when every field that flows out of it reaches the DOM as text and
|
|
9
|
+
* never as a native-element attribute.
|
|
10
|
+
*
|
|
11
|
+
* This module answers exactly that, from the `@astrojs/compiler` AST:
|
|
12
|
+
* - native element (`type:"element"`) + a DYNAMIC attribute that references a
|
|
13
|
+
* field of the value → UNSAFE.
|
|
14
|
+
* - native element text position (`{item.title}`) → safe.
|
|
15
|
+
* - component (`type:"component"`) attribute that references a field → a prop
|
|
16
|
+
* hand-off; reported so a cross-file pass can verify the child (Tier-2),
|
|
17
|
+
* and — depending on `componentHandoffUnsafe` — either tolerated (Tier-1,
|
|
18
|
+
* preserving prior behavior) or treated as unprovable/UNSAFE (Tier-2 child).
|
|
19
|
+
*
|
|
20
|
+
* The bias is conservative throughout: anything we cannot positively prove is
|
|
21
|
+
* text-only is reported UNSAFE. False negatives (a safe const left unwrapped)
|
|
22
|
+
* are acceptable; a false positive (encoding into an attribute) is not. In line
|
|
23
|
+
* with that, the loop-variable scan over-approximates — within an expression
|
|
24
|
+
* that mentions the const, EVERY `.map()`/`.flatMap()` binding is treated as
|
|
25
|
+
* carrying a field of it, so nested and chained loops can never smuggle a field
|
|
26
|
+
* into an attribute unnoticed.
|
|
27
|
+
*/
|
|
28
|
+
import type { AstroNode } from "./parse.js";
|
|
29
|
+
export interface Handoff {
|
|
30
|
+
/** Component the value (or a field of it) is passed to. */
|
|
31
|
+
component: string;
|
|
32
|
+
/** Prop name it is passed as. */
|
|
33
|
+
prop: string;
|
|
34
|
+
}
|
|
35
|
+
export interface UsageVerdict {
|
|
36
|
+
safe: boolean;
|
|
37
|
+
/** Populated when `safe` is false. */
|
|
38
|
+
reason?: string;
|
|
39
|
+
/** Component prop hand-offs of this const, for an optional cross-file check. */
|
|
40
|
+
handoffs: Handoff[];
|
|
41
|
+
}
|
|
42
|
+
export interface ClassifyOptions {
|
|
43
|
+
/**
|
|
44
|
+
* When true, a field flowing into a component prop counts as UNSAFE (we can't
|
|
45
|
+
* prove how the component renders it). Tier-2's child analysis sets this so a
|
|
46
|
+
* re-pass to a grandchild is conservatively skipped. Tier-1 leaves it false to
|
|
47
|
+
* preserve its established "component prop ≈ text" behavior.
|
|
48
|
+
*/
|
|
49
|
+
componentHandoffUnsafe?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* When true, the `set:html` / `set:text` directives count as TEXT sinks, not
|
|
52
|
+
* native attributes. They render the value as element CONTENT, where stega
|
|
53
|
+
* survives harmlessly — so a field reaching them is safe to encode. Off by
|
|
54
|
+
* default (the wrap tiers stay conservative); the prop-hoist child check sets
|
|
55
|
+
* it so a `<p set:html={prop} />` rich field can be hoisted.
|
|
56
|
+
*/
|
|
57
|
+
htmlDirectivesSafe?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Loop-binding identifiers introduced by `.map()`/`.flatMap()` callbacks in a JS
|
|
61
|
+
* fragment. With `receiver`, only loops over that exact variable are read; with
|
|
62
|
+
* no receiver, EVERY map/flatMap is read (the conservative scan used during
|
|
63
|
+
* classification, so chained/nested loops can't hide a field).
|
|
64
|
+
*/
|
|
65
|
+
export declare function mapParamIdents(js: string, receiver?: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Classify how a literal const `varName` is consumed in a parsed template.
|
|
68
|
+
*
|
|
69
|
+
* For every tag node, the field-carrying identifiers in scope are `varName`
|
|
70
|
+
* itself plus the binding variables of any ancestor expression that mentions
|
|
71
|
+
* `varName` and loops. A dynamic attribute referencing one of those names is the
|
|
72
|
+
* encoded value escaping into a sink: a native element → UNSAFE; a component → a
|
|
73
|
+
* recorded prop hand-off (and UNSAFE too under `componentHandoffUnsafe`). Text
|
|
74
|
+
* positions (`{item.x}` as an element's child) are never attributes, so they
|
|
75
|
+
* never trip this — exactly the leaves `editable()` can safely encode.
|
|
76
|
+
*/
|
|
77
|
+
export declare function classifyConstUsage(root: AstroNode, varName: string, opts?: ClassifyOptions): UsageVerdict;
|
|
78
|
+
/**
|
|
79
|
+
* Does the template render any of `names` in a BODY-reachable position — a text
|
|
80
|
+
* expression (`{title}`) or a `set:html`/`set:text` content sink — that is NOT
|
|
81
|
+
* inside the document `<head>`?
|
|
82
|
+
*
|
|
83
|
+
* `classifyConstUsage` proves a field never lands in a corrupting attribute, but
|
|
84
|
+
* a field that renders ONLY into `<head>` (a layout's `<title>{title}</title>`)
|
|
85
|
+
* is text-safe yet has no click-to-edit target. The prop-hoist tier requires a
|
|
86
|
+
* positive body sink so it doesn't mint `editable()` bindings the editor can't
|
|
87
|
+
* surface (and, incidentally, won't hoist a prop the child never renders at all).
|
|
88
|
+
*/
|
|
89
|
+
export declare function rendersOutsideHead(root: AstroNode, names: string[]): boolean;
|
|
90
|
+
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAW,MAAM,YAAY,CAAC;AAGrD,MAAM,WAAW,OAAO;IACtB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AA8ED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,eAAoB,GACzB,YAAY,CAqCd;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CA4B5E"}
|