@cancia/astro 0.7.0 → 0.9.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/{chunk-L2VKQJPY.js → chunk-2NVZWZPE.js} +62 -33
- package/dist/{chunk-22DJVJBR.js → chunk-DIV2FFYX.js} +1 -1
- package/dist/{chunk-UR5WC3RA.js → chunk-GBTFXQEA.js} +20 -6
- package/dist/chunk-GJIX7MWC.js +55 -0
- package/dist/{chunk-VFMOVGMC.js → chunk-HQIGNRIU.js} +2 -2
- package/dist/{chunk-BOIQNZAO.js → chunk-KHCFVVYV.js} +53 -1
- package/dist/{chunk-AGNPUTKS.js → chunk-LJSSPOSW.js} +2 -2
- package/dist/{chunk-GNWD7EL2.js → chunk-MXVOJRAM.js} +22 -39
- package/dist/{chunk-NG5GJME5.js → chunk-VL6FO446.js} +8 -1
- package/dist/{chunk-UMCBQXB6.js → chunk-WVHTCFE6.js} +17 -1
- package/dist/content.d.ts +24 -2
- package/dist/content.js +47 -4
- package/dist/endpoints/auth.js +7 -48
- package/dist/endpoints/lists.js +2 -2
- package/dist/endpoints/schemas.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +285 -71
- package/dist/loader/index.d.ts +24 -0
- package/dist/loader/index.js +4 -2
- package/dist/{portable-text-BikSqS9T.d.ts → portable-text-D74aIuHn.d.ts} +25 -1
- package/dist/richtext/CanciaRichRegion.astro +57 -0
- package/dist/richtext/index.d.ts +2 -2
- package/dist/richtext/index.js +7 -3
- package/dist/runtime.js +3 -3
- package/dist/schema/index.d.ts +47 -2
- package/dist/schema/index.js +9 -3
- package/dist/schema/loader.d.ts +23 -0
- package/dist/schema/loader.js +12 -0
- package/dist/storage/index.js +2 -2
- package/dist/transform/text-helper.d.ts +47 -0
- package/dist/transform/text-helper.js +64 -0
- package/package.json +10 -4
package/dist/endpoints/auth.js
CHANGED
|
@@ -1,55 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRateLimiter,
|
|
3
|
+
runAuth
|
|
4
|
+
} from "../chunk-GJIX7MWC.js";
|
|
5
|
+
|
|
1
6
|
// src/endpoints/auth.ts
|
|
2
7
|
import { getCanciaRuntime } from "virtual:cancia/runtime";
|
|
3
|
-
var
|
|
4
|
-
var MAX_FAILURES = 5;
|
|
5
|
-
var WINDOW_MS = 6e4;
|
|
6
|
-
function getIp(req) {
|
|
7
|
-
return req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
8
|
-
}
|
|
9
|
-
function isRateLimited(ip) {
|
|
10
|
-
const bucket = buckets.get(ip);
|
|
11
|
-
if (!bucket || Date.now() > bucket.resetAt) return false;
|
|
12
|
-
return bucket.failures >= MAX_FAILURES;
|
|
13
|
-
}
|
|
14
|
-
function recordFailure(ip) {
|
|
15
|
-
const now = Date.now();
|
|
16
|
-
const bucket = buckets.get(ip);
|
|
17
|
-
if (!bucket || now > bucket.resetAt) {
|
|
18
|
-
buckets.set(ip, { failures: 1, resetAt: now + WINDOW_MS });
|
|
19
|
-
} else {
|
|
20
|
-
bucket.failures += 1;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
8
|
+
var limiter = createRateLimiter();
|
|
23
9
|
async function POST({ request }) {
|
|
24
10
|
const { secret } = getCanciaRuntime();
|
|
25
|
-
|
|
26
|
-
if (isRateLimited(ip))
|
|
27
|
-
return new Response(
|
|
28
|
-
JSON.stringify({ error: "Too many attempts. Try again in a minute." }),
|
|
29
|
-
{ status: 429, headers: { "Content-Type": "application/json" } }
|
|
30
|
-
);
|
|
31
|
-
let token;
|
|
32
|
-
try {
|
|
33
|
-
const body = await request.json();
|
|
34
|
-
token = body?.token;
|
|
35
|
-
} catch {
|
|
36
|
-
return new Response(
|
|
37
|
-
JSON.stringify({ error: "Invalid request body" }),
|
|
38
|
-
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
if (!token || token !== secret) {
|
|
42
|
-
recordFailure(ip);
|
|
43
|
-
return new Response(
|
|
44
|
-
JSON.stringify({ error: "Invalid token" }),
|
|
45
|
-
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
buckets.delete(ip);
|
|
49
|
-
return new Response(
|
|
50
|
-
JSON.stringify({ ok: true }),
|
|
51
|
-
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
52
|
-
);
|
|
11
|
+
return runAuth({ request, secret, limiter });
|
|
53
12
|
}
|
|
54
13
|
export {
|
|
55
14
|
POST
|
package/dist/endpoints/lists.js
CHANGED
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
} from "../chunk-VGRG5DN7.js";
|
|
5
5
|
import {
|
|
6
6
|
makeListsRoutes
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-NG5GJME5.js";
|
|
7
|
+
} from "../chunk-DIV2FFYX.js";
|
|
9
8
|
import "../chunk-X6ZFFGIA.js";
|
|
9
|
+
import "../chunk-VL6FO446.js";
|
|
10
10
|
import "../chunk-7IA5B5CF.js";
|
|
11
11
|
|
|
12
12
|
// src/endpoints/lists.ts
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
makeSchemasRoute
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-HQIGNRIU.js";
|
|
4
|
+
import "../chunk-VL6FO446.js";
|
|
5
|
+
import "../chunk-WVHTCFE6.js";
|
|
6
|
+
import "../chunk-KHCFVVYV.js";
|
|
7
7
|
|
|
8
8
|
// src/endpoints/schemas.ts
|
|
9
9
|
import { getCanciaRuntime } from "virtual:cancia/runtime";
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { FieldDescription, FieldMeta, FieldMetaBase, FieldWidget, ListDescriptio
|
|
|
9
9
|
export { G as GitBackedContentPaths, a as GitBackedControls, b as GitBackedOptions, c as GitBackedStorage, S as SqliteV2Options, d as closeSqliteAdapterV2, e as createGitBackedAdapter, f as createJsonFileAdapter, g as createJsonFileAdapterV2, h as createSQLiteAdapter, i as createSqliteAdapterV2 } from './git-backed-DtiH52EI.js';
|
|
10
10
|
export { z } from 'zod';
|
|
11
11
|
import 'astro/loaders';
|
|
12
|
-
import './portable-text-
|
|
12
|
+
import './portable-text-D74aIuHn.js';
|
|
13
13
|
|
|
14
14
|
interface R2UploadHandlerOptions {
|
|
15
15
|
/** Cloudflare account ID (from R2 dashboard) */
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeSchemasRoute
|
|
3
|
+
} from "./chunk-HQIGNRIU.js";
|
|
1
4
|
import {
|
|
2
5
|
runPublish
|
|
3
6
|
} from "./chunk-5RCLBWRH.js";
|
|
4
7
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
createRateLimiter,
|
|
9
|
+
runAuth
|
|
10
|
+
} from "./chunk-GJIX7MWC.js";
|
|
7
11
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-NG5GJME5.js";
|
|
12
|
+
makeListsRoutes
|
|
13
|
+
} from "./chunk-DIV2FFYX.js";
|
|
11
14
|
import {
|
|
12
15
|
makeLocalUploadHandler,
|
|
13
16
|
makeR2UploadHandler,
|
|
14
17
|
makeUploadRoute,
|
|
15
18
|
setCanciaRuntime
|
|
16
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-LJSSPOSW.js";
|
|
17
20
|
import "./chunk-5IPHDIC6.js";
|
|
18
|
-
import {
|
|
19
|
-
defineField,
|
|
20
|
-
defineList,
|
|
21
|
-
describeList,
|
|
22
|
-
z
|
|
23
|
-
} from "./chunk-UMCBQXB6.js";
|
|
24
|
-
import {
|
|
25
|
-
canciaLoader
|
|
26
|
-
} from "./chunk-UR5WC3RA.js";
|
|
27
21
|
import {
|
|
28
22
|
createSQLiteAdapter
|
|
29
23
|
} from "./chunk-CJDIVWO3.js";
|
|
@@ -31,20 +25,31 @@ import {
|
|
|
31
25
|
closeSqliteAdapterV2,
|
|
32
26
|
createGitBackedAdapter,
|
|
33
27
|
createSqliteAdapterV2
|
|
34
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-MXVOJRAM.js";
|
|
35
29
|
import "./chunk-U7V53JX7.js";
|
|
30
|
+
import "./chunk-VL6FO446.js";
|
|
31
|
+
import {
|
|
32
|
+
canciaLoader
|
|
33
|
+
} from "./chunk-GBTFXQEA.js";
|
|
36
34
|
import {
|
|
37
35
|
createJsonFileAdapter,
|
|
38
36
|
createJsonFileAdapterV2
|
|
39
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-2NVZWZPE.js";
|
|
40
38
|
import {
|
|
41
39
|
RevConflictError
|
|
42
40
|
} from "./chunk-7IA5B5CF.js";
|
|
43
|
-
import
|
|
41
|
+
import {
|
|
42
|
+
defineField,
|
|
43
|
+
defineList,
|
|
44
|
+
describeList,
|
|
45
|
+
z
|
|
46
|
+
} from "./chunk-WVHTCFE6.js";
|
|
47
|
+
import "./chunk-KHCFVVYV.js";
|
|
44
48
|
|
|
45
49
|
// src/integration.ts
|
|
46
50
|
import { loadEnv } from "vite";
|
|
47
51
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
52
|
+
import { readFile } from "fs/promises";
|
|
48
53
|
import { isAbsolute, join as join2 } from "path";
|
|
49
54
|
|
|
50
55
|
// src/routes/content.ts
|
|
@@ -114,61 +119,10 @@ function makePublishRoute(deployHook, secret, hookOptions, dispatch, mode) {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
// src/routes/auth.ts
|
|
117
|
-
var buckets = /* @__PURE__ */ new Map();
|
|
118
|
-
var MAX_FAILURES = 5;
|
|
119
|
-
var WINDOW_MS = 6e4;
|
|
120
|
-
function getIp(req) {
|
|
121
|
-
return req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
122
|
-
}
|
|
123
|
-
function isRateLimited(ip) {
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
const bucket = buckets.get(ip);
|
|
126
|
-
if (!bucket || now > bucket.resetAt) return false;
|
|
127
|
-
return bucket.failures >= MAX_FAILURES;
|
|
128
|
-
}
|
|
129
|
-
function recordFailure(ip) {
|
|
130
|
-
const now = Date.now();
|
|
131
|
-
const bucket = buckets.get(ip);
|
|
132
|
-
if (!bucket || now > bucket.resetAt) {
|
|
133
|
-
buckets.set(ip, { failures: 1, resetAt: now + WINDOW_MS });
|
|
134
|
-
} else {
|
|
135
|
-
bucket.failures += 1;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function clearFailures(ip) {
|
|
139
|
-
buckets.delete(ip);
|
|
140
|
-
}
|
|
141
122
|
function makeAuthRoute(secret) {
|
|
123
|
+
const limiter = createRateLimiter();
|
|
142
124
|
return async function authRoute(req) {
|
|
143
|
-
|
|
144
|
-
if (isRateLimited(ip)) {
|
|
145
|
-
return new Response(
|
|
146
|
-
JSON.stringify({ error: "Too many attempts. Try again in a minute." }),
|
|
147
|
-
{ status: 429, headers: { "Content-Type": "application/json" } }
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
let token;
|
|
151
|
-
try {
|
|
152
|
-
const body = await req.json();
|
|
153
|
-
token = body?.token;
|
|
154
|
-
} catch {
|
|
155
|
-
return new Response(
|
|
156
|
-
JSON.stringify({ error: "Invalid request body" }),
|
|
157
|
-
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (!token || token !== secret) {
|
|
161
|
-
recordFailure(ip);
|
|
162
|
-
return new Response(
|
|
163
|
-
JSON.stringify({ error: "Invalid token" }),
|
|
164
|
-
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
clearFailures(ip);
|
|
168
|
-
return new Response(
|
|
169
|
-
JSON.stringify({ ok: true }),
|
|
170
|
-
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
171
|
-
);
|
|
125
|
+
return runAuth({ request: req, secret, limiter });
|
|
172
126
|
};
|
|
173
127
|
}
|
|
174
128
|
|
|
@@ -198,6 +152,211 @@ CANCIA_TOKEN=${token}
|
|
|
198
152
|
return token;
|
|
199
153
|
}
|
|
200
154
|
|
|
155
|
+
// src/transform/attribute-text.ts
|
|
156
|
+
import { parse } from "@astrojs/compiler";
|
|
157
|
+
var HELPER_NAME = "__canciaText";
|
|
158
|
+
var LINK_HELPER_NAME = "__canciaLink";
|
|
159
|
+
var LOADER_NAME = "__canciaContent";
|
|
160
|
+
var CONTENT_LOCAL = "__canciaCms";
|
|
161
|
+
var HELPER_MODULE = "virtual:cancia/text";
|
|
162
|
+
var RAW_TEXT = /* @__PURE__ */ new Set([
|
|
163
|
+
"script",
|
|
164
|
+
"style",
|
|
165
|
+
"pre",
|
|
166
|
+
"textarea",
|
|
167
|
+
"code",
|
|
168
|
+
"title",
|
|
169
|
+
"option",
|
|
170
|
+
"noscript",
|
|
171
|
+
"template"
|
|
172
|
+
]);
|
|
173
|
+
function byteToCharMap(source) {
|
|
174
|
+
let ascii = true;
|
|
175
|
+
for (let i = 0; i < source.length; i++) {
|
|
176
|
+
if (source.charCodeAt(i) > 127) {
|
|
177
|
+
ascii = false;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (ascii) return (b) => b;
|
|
182
|
+
const map = /* @__PURE__ */ new Map();
|
|
183
|
+
let byte = 0;
|
|
184
|
+
for (let char = 0; char < source.length; char++) {
|
|
185
|
+
map.set(byte, char);
|
|
186
|
+
const code = source.codePointAt(char);
|
|
187
|
+
byte += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
188
|
+
if (code >= 65536) char++;
|
|
189
|
+
}
|
|
190
|
+
map.set(byte, source.length);
|
|
191
|
+
return (b) => map.get(b) ?? b;
|
|
192
|
+
}
|
|
193
|
+
function quote(text) {
|
|
194
|
+
return JSON.stringify(text);
|
|
195
|
+
}
|
|
196
|
+
function findOpenAngle(node, src, at) {
|
|
197
|
+
const name = node.name ?? "";
|
|
198
|
+
const floor = Math.max(0, at - 4096);
|
|
199
|
+
for (let i = Math.min(at, src.length - 1); i >= floor; i--) {
|
|
200
|
+
if (src[i] !== "<") continue;
|
|
201
|
+
if (src.slice(i + 1, i + 1 + name.length) !== name) continue;
|
|
202
|
+
const boundary = src[i + 1 + name.length];
|
|
203
|
+
if (boundary === void 0 || /[\s/>]/.test(boundary)) return i;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function readAttr(openTag, name) {
|
|
208
|
+
const re = new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`);
|
|
209
|
+
return re.exec(openTag)?.[1] ?? null;
|
|
210
|
+
}
|
|
211
|
+
function findOpenTagEnd(src, lt) {
|
|
212
|
+
let quoteChar = null;
|
|
213
|
+
for (let i = lt; i < src.length; i++) {
|
|
214
|
+
const c = src[i];
|
|
215
|
+
if (quoteChar) {
|
|
216
|
+
if (c === quoteChar) quoteChar = null;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (c === '"' || c === "'") {
|
|
220
|
+
quoteChar = c;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (c === ">") return i + 1;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
async function transformAttributeText(source) {
|
|
228
|
+
if (!source.includes("data-cms")) {
|
|
229
|
+
return { code: source, changed: false, substituted: [] };
|
|
230
|
+
}
|
|
231
|
+
const toChar = byteToCharMap(source);
|
|
232
|
+
let ast;
|
|
233
|
+
try {
|
|
234
|
+
ast = (await parse(source, { position: true })).ast;
|
|
235
|
+
} catch {
|
|
236
|
+
return { code: source, changed: false, substituted: [] };
|
|
237
|
+
}
|
|
238
|
+
const edits = [];
|
|
239
|
+
const substituted = [];
|
|
240
|
+
function visit(node) {
|
|
241
|
+
if (node.type === "element" && RAW_TEXT.has((node.name ?? "").toLowerCase())) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (node.type === "element") {
|
|
245
|
+
const reported = node.position?.start?.offset;
|
|
246
|
+
if (reported !== void 0) {
|
|
247
|
+
const lt = findOpenAngle(node, source, toChar(reported));
|
|
248
|
+
if (lt !== null) {
|
|
249
|
+
const gt = findOpenTagEnd(source, lt);
|
|
250
|
+
if (gt !== null) {
|
|
251
|
+
const openTag = source.slice(lt, gt);
|
|
252
|
+
const key = readAttr(openTag, "data-cms");
|
|
253
|
+
if (key !== null) {
|
|
254
|
+
const declaredType = readAttr(openTag, "data-cms-type");
|
|
255
|
+
const isList = /\sdata-cms-list\s*=/.test(openTag);
|
|
256
|
+
if (declaredType === "link" && !isList) {
|
|
257
|
+
const kids = (node.children ?? []).filter(
|
|
258
|
+
(c) => c.type !== "text" || (c.value ?? "").trim() !== ""
|
|
259
|
+
);
|
|
260
|
+
const soleText = kids.length === 1 && kids[0].type === "text" ? kids[0] : null;
|
|
261
|
+
const authoredHref = readAttr(openTag, "href");
|
|
262
|
+
if (soleText && authoredHref !== null) {
|
|
263
|
+
const raw = soleText.value ?? "";
|
|
264
|
+
const literal = raw.replace(/\s+/g, " ").trim();
|
|
265
|
+
const s = soleText.position?.start?.offset;
|
|
266
|
+
const e = soleText.position?.end?.offset;
|
|
267
|
+
const hrefRe = /\shref\s*=\s*(["'])([^"']*)\1/.exec(openTag);
|
|
268
|
+
if (literal !== "" && s !== void 0 && e !== void 0 && hrefRe) {
|
|
269
|
+
const call = `${LINK_HELPER_NAME}(${CONTENT_LOCAL}, ${quote(key)}, ${quote(literal)}, ${quote(hrefRe[2])})`;
|
|
270
|
+
edits.push({
|
|
271
|
+
start: lt + hrefRe.index,
|
|
272
|
+
end: lt + hrefRe.index + hrefRe[0].length,
|
|
273
|
+
text: ` href={${call}.href}`
|
|
274
|
+
});
|
|
275
|
+
edits.push({
|
|
276
|
+
start: toChar(s),
|
|
277
|
+
end: toChar(e),
|
|
278
|
+
text: `{${call}.label}`
|
|
279
|
+
});
|
|
280
|
+
substituted.push({ key, line: node.position?.start?.line ?? 0 });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const typed = declaredType !== null;
|
|
285
|
+
if (!typed && !isList) {
|
|
286
|
+
const kids = (node.children ?? []).filter(
|
|
287
|
+
(c) => c.type !== "text" || (c.value ?? "").trim() !== ""
|
|
288
|
+
);
|
|
289
|
+
const soleText = kids.length === 1 && kids[0].type === "text" ? kids[0] : null;
|
|
290
|
+
if (soleText) {
|
|
291
|
+
const raw = soleText.value ?? "";
|
|
292
|
+
const literal = raw.replace(/\s+/g, " ").trim();
|
|
293
|
+
const s = soleText.position?.start?.offset;
|
|
294
|
+
const e = soleText.position?.end?.offset;
|
|
295
|
+
if (literal !== "" && s !== void 0 && e !== void 0) {
|
|
296
|
+
edits.push({
|
|
297
|
+
start: toChar(s),
|
|
298
|
+
end: toChar(e),
|
|
299
|
+
// Emitted as an EXPRESSION, not a spliced string: Astro
|
|
300
|
+
// escapes {expression} output, which is the XSS gate. A
|
|
301
|
+
// probe proved naive splicing executes a stored
|
|
302
|
+
// <script> tag.
|
|
303
|
+
text: `{${HELPER_NAME}(${CONTENT_LOCAL}, ${quote(key)}, ${quote(literal)})}`
|
|
304
|
+
});
|
|
305
|
+
substituted.push({
|
|
306
|
+
key,
|
|
307
|
+
line: node.position?.start?.line ?? 0
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const child of node.children ?? []) visit(child);
|
|
318
|
+
}
|
|
319
|
+
visit(ast);
|
|
320
|
+
if (edits.length === 0) {
|
|
321
|
+
return { code: source, changed: false, substituted: [] };
|
|
322
|
+
}
|
|
323
|
+
edits.sort((a, b) => b.start - a.start);
|
|
324
|
+
let code = source;
|
|
325
|
+
for (const edit of edits) {
|
|
326
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
code: injectImports(code, {
|
|
330
|
+
text: code.includes(`${HELPER_NAME}(`),
|
|
331
|
+
link: code.includes(`${LINK_HELPER_NAME}(`)
|
|
332
|
+
}),
|
|
333
|
+
changed: true,
|
|
334
|
+
substituted
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function injectImports(code, used) {
|
|
338
|
+
const names = [
|
|
339
|
+
LOADER_NAME,
|
|
340
|
+
used.text ? HELPER_NAME : null,
|
|
341
|
+
used.link ? LINK_HELPER_NAME : null
|
|
342
|
+
].filter(Boolean).join(", ");
|
|
343
|
+
const preamble = [
|
|
344
|
+
`import { ${names} } from ${quote(HELPER_MODULE)};`,
|
|
345
|
+
`const ${CONTENT_LOCAL} = await ${LOADER_NAME}();`
|
|
346
|
+
].join("\n");
|
|
347
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(code);
|
|
348
|
+
if (!fm) {
|
|
349
|
+
return `---
|
|
350
|
+
${preamble}
|
|
351
|
+
---
|
|
352
|
+
${code}`;
|
|
353
|
+
}
|
|
354
|
+
if (fm[1].includes(HELPER_MODULE)) return code;
|
|
355
|
+
const at = fm.index + fm[0].length - 3;
|
|
356
|
+
return `${code.slice(0, at)}${preamble}
|
|
357
|
+
${code.slice(at)}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
201
360
|
// src/integration.ts
|
|
202
361
|
function resolvePublish(opts, hasDeployHookEnv) {
|
|
203
362
|
const publish = opts.publish;
|
|
@@ -343,6 +502,11 @@ function canciaIntegration(opts = {}) {
|
|
|
343
502
|
injectRoute({ pattern: "/api/cancia/lists/[listName]/[id]", entrypoint: endpointPath("lists"), prerender: false });
|
|
344
503
|
const runtimeModulePath = fileURLToPath(new URL("./runtime.js", import.meta.url));
|
|
345
504
|
const RESOLVED_VIRTUAL_ID = "\0virtual:cancia/runtime";
|
|
505
|
+
const textHelperModulePath = fileURLToPath(
|
|
506
|
+
new URL("./transform/text-helper.js", import.meta.url)
|
|
507
|
+
);
|
|
508
|
+
const TEXT_VIRTUAL_ID = "virtual:cancia/text";
|
|
509
|
+
const RESOLVED_TEXT_VIRTUAL_ID = "\0virtual:cancia/text";
|
|
346
510
|
updateConfig({
|
|
347
511
|
vite: {
|
|
348
512
|
plugins: [
|
|
@@ -363,6 +527,56 @@ function canciaIntegration(opts = {}) {
|
|
|
363
527
|
`export { getCanciaRuntime };`
|
|
364
528
|
].join("\n");
|
|
365
529
|
}
|
|
530
|
+
},
|
|
531
|
+
// ---- plan 054: attribute-only authoring ---------------------
|
|
532
|
+
// Rewrites `<h1 data-cms="k">text</h1>` into a helper call before
|
|
533
|
+
// Astro's compiler reads the file, so a developer never writes a
|
|
534
|
+
// t() call, an import, or a repeated key.
|
|
535
|
+
//
|
|
536
|
+
// This works because Astro compiles the `source` ARGUMENT rather
|
|
537
|
+
// than reading from disk (deliberate since astro#3889), so an
|
|
538
|
+
// upstream transform is honoured.
|
|
539
|
+
//
|
|
540
|
+
// NOTE `enforce: "pre"` does NOT place this before Astro's own
|
|
541
|
+
// plugin — Astro prepends its own and mergeConfig appends ours
|
|
542
|
+
// (measured: ours at 20, astro:build at 9). It works because Vite
|
|
543
|
+
// CHAINS transform hooks and Astro's has an id filter. Do not
|
|
544
|
+
// write anything here that depends on plugin array position.
|
|
545
|
+
{
|
|
546
|
+
name: "vite-plugin-cancia-attribute-text",
|
|
547
|
+
enforce: "pre",
|
|
548
|
+
resolveId(id) {
|
|
549
|
+
if (id === TEXT_VIRTUAL_ID) return RESOLVED_TEXT_VIRTUAL_ID;
|
|
550
|
+
},
|
|
551
|
+
async load(id) {
|
|
552
|
+
if (id.endsWith(".astro") && !id.includes("?")) {
|
|
553
|
+
let source;
|
|
554
|
+
try {
|
|
555
|
+
source = await readFile(id, "utf8");
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const result = await transformAttributeText(source);
|
|
560
|
+
return result.changed ? result.code : null;
|
|
561
|
+
}
|
|
562
|
+
if (id !== RESOLVED_TEXT_VIRTUAL_ID) return;
|
|
563
|
+
const importUrl = pathToFileURL(textHelperModulePath).href;
|
|
564
|
+
return [
|
|
565
|
+
`import { __canciaText, __canciaLink, __canciaContent, configureTextHelper } from ${JSON.stringify(importUrl)};`,
|
|
566
|
+
`import { getCanciaRuntime } from "virtual:cancia/runtime";`,
|
|
567
|
+
`configureTextHelper({`,
|
|
568
|
+
` site: ${JSON.stringify(site)},`,
|
|
569
|
+
` lang: ${JSON.stringify(languages[0] ?? "en")},`,
|
|
570
|
+
// The read goes through the runtime so it uses whatever
|
|
571
|
+
// storage this project configured — no second adapter path.
|
|
572
|
+
` read: async () => {`,
|
|
573
|
+
` const rt = getCanciaRuntime();`,
|
|
574
|
+
` return rt.storageV2 ? rt.storageV2.kv.getAll(${JSON.stringify(site)}) : {};`,
|
|
575
|
+
` },`,
|
|
576
|
+
`});`,
|
|
577
|
+
`export { __canciaText, __canciaLink, __canciaContent };`
|
|
578
|
+
].join("\n");
|
|
579
|
+
}
|
|
366
580
|
}
|
|
367
581
|
]
|
|
368
582
|
}
|
package/dist/loader/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Loader } from 'astro/loaders';
|
|
2
2
|
import { a as CanciaStorageV2 } from '../types-BMlLS-OS.js';
|
|
3
|
+
import { ListSchema } from '../schema/index.js';
|
|
4
|
+
import 'zod';
|
|
5
|
+
import '../portable-text-D74aIuHn.js';
|
|
3
6
|
|
|
4
7
|
interface CanciaLoaderOptions {
|
|
5
8
|
/** The list name as declared in src/cms/schemas.ts. */
|
|
@@ -20,6 +23,27 @@ interface CanciaLoaderOptions {
|
|
|
20
23
|
* adapter. Ignored when `storage` is supplied.
|
|
21
24
|
*/
|
|
22
25
|
projectRoot?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The list's schema. Pass it to enable draft filtering: when the schema
|
|
28
|
+
* declares a `draftField`, entries flagged as drafts are omitted from the
|
|
29
|
+
* collection, so an unpublished entry never reaches the built site.
|
|
30
|
+
*
|
|
31
|
+
* Optional — omitting it keeps the pre-056 behaviour of emitting every
|
|
32
|
+
* stored entry. You are almost certainly already importing `schemas` in
|
|
33
|
+
* `src/content.config.ts` for the collection's `schema:`, so this is one
|
|
34
|
+
* more reference to the same object:
|
|
35
|
+
*
|
|
36
|
+
* loader: canciaLoader({ list: "blog", site: "x", schema: schemas.blog }),
|
|
37
|
+
* schema: schemas.blog.validator,
|
|
38
|
+
*/
|
|
39
|
+
schema?: Pick<ListSchema, "draftField">;
|
|
40
|
+
/**
|
|
41
|
+
* Emit drafts anyway, even when `schema.draftField` is set. The escape hatch
|
|
42
|
+
* for a preview deployment: build the same site with drafts visible by
|
|
43
|
+
* flipping one flag (e.g. `includeDrafts: import.meta.env.PREVIEW === "1"`),
|
|
44
|
+
* rather than maintaining a second content config.
|
|
45
|
+
*/
|
|
46
|
+
includeDrafts?: boolean;
|
|
23
47
|
}
|
|
24
48
|
declare function canciaLoader(opts: CanciaLoaderOptions): Loader;
|
|
25
49
|
|
package/dist/loader/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canciaLoader
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-GBTFXQEA.js";
|
|
4
|
+
import "../chunk-2NVZWZPE.js";
|
|
5
5
|
import "../chunk-7IA5B5CF.js";
|
|
6
|
+
import "../chunk-WVHTCFE6.js";
|
|
7
|
+
import "../chunk-KHCFVVYV.js";
|
|
6
8
|
export {
|
|
7
9
|
canciaLoader
|
|
8
10
|
};
|
|
@@ -113,5 +113,29 @@ declare const portableTextSubsetSchema: z.ZodArray<z.ZodObject<{
|
|
|
113
113
|
}, z.core.$strip>>;
|
|
114
114
|
}, z.core.$strip>>;
|
|
115
115
|
type PortableTextValue = z.infer<typeof portableTextSubsetSchema>;
|
|
116
|
+
/**
|
|
117
|
+
* Parse a stored rich-text value. NEVER throws.
|
|
118
|
+
*
|
|
119
|
+
* The KV surface stores every value as a string, so a rich document round-trips
|
|
120
|
+
* as JSON — exactly the precedent `link` set (see parseLinkValue). Four cases
|
|
121
|
+
* must all degrade gracefully rather than break a render:
|
|
122
|
+
*
|
|
123
|
+
* - valid PT-subset JSON → used as-is, with unsafe hrefs stripped
|
|
124
|
+
* - a bare string → ONE normal block containing that text. This is
|
|
125
|
+
* the migration path for a field promoted from
|
|
126
|
+
* `text` to `richtext`: it keeps rendering.
|
|
127
|
+
* - empty string → [] (deliberately empty — renders nothing, on
|
|
128
|
+
* purpose; NOT the same as "no override")
|
|
129
|
+
* - malformed / off-subset → [] rather than propagating bad data to the
|
|
130
|
+
* renderer
|
|
131
|
+
*
|
|
132
|
+
* Unsafe hrefs are dropped ON READ, not only rejected on write. Validation
|
|
133
|
+
* guards the save path, but a value can reach a renderer another way — an older
|
|
134
|
+
* row, a hand-edited DB, a different adapter — so the read is where the
|
|
135
|
+
* guarantee actually holds. Same rule, same reason, as parseLinkValue.
|
|
136
|
+
*/
|
|
137
|
+
declare function parseRichValue(raw: unknown): PortableTextValue;
|
|
138
|
+
/** Serialise a rich value for the KV store. Empty array -> "" (hidden). */
|
|
139
|
+
declare function serializeRichValue(value: PortableTextValue): string;
|
|
116
140
|
|
|
117
|
-
export { PT_DECORATORS as P, PT_LIST_ITEMS as a, PT_STYLES as b, type PortableTextValue as c, type PtBlock as d, type PtDecorator as e, type PtLinkMarkDef as f, type PtListItem as g, type PtSpan as h, type PtStyle as i, isSafeHref as j,
|
|
141
|
+
export { PT_DECORATORS as P, PT_LIST_ITEMS as a, PT_STYLES as b, type PortableTextValue as c, type PtBlock as d, type PtDecorator as e, type PtLinkMarkDef as f, type PtListItem as g, type PtSpan as h, type PtStyle as i, isSafeHref as j, portableTextSubsetSchema as k, ptBlockSchema as l, parseRichValue as p, serializeRichValue as s };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// <CanciaRichRegion> — an editable rich-text region on the INLINE surface
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// A slot the client can fill with real prose: paragraphs, h2/h3, blockquotes,
|
|
6
|
+
// bullet/number lists, strong/em, and links. They may ADD and DELETE paragraphs
|
|
7
|
+
// inside it. What they cannot do is invent a new section — the structure of the
|
|
8
|
+
// PAGE stays the developer's; the structure INSIDE a region is the client's.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// ---
|
|
12
|
+
// import CanciaRichRegion from "@cancia/astro/richtext/CanciaRichRegion.astro";
|
|
13
|
+
// const { tRich } = await reader.get();
|
|
14
|
+
// ---
|
|
15
|
+
// <CanciaRichRegion key="about.story" value={tRich("about.story")}>
|
|
16
|
+
// <p>The prose authored in the page. This is the fallback.</p>
|
|
17
|
+
// </CanciaRichRegion>
|
|
18
|
+
//
|
|
19
|
+
// THE FALLBACK IS THE SLOT. A rich fallback cannot be a JS string, so rather
|
|
20
|
+
// than serialise the markup into a Portable-Text literal at annotate time
|
|
21
|
+
// (lossy, unreadable, and it stops the source being the source of truth), the
|
|
22
|
+
// authored children ARE the fallback. An empty DB therefore renders
|
|
23
|
+
// byte-identically — the invariant that makes annotating a live client site
|
|
24
|
+
// safe.
|
|
25
|
+
//
|
|
26
|
+
// Three states, and the difference between the last two matters:
|
|
27
|
+
// value === null → no override. Render the authored children.
|
|
28
|
+
// value === [] → a stored, DELIBERATELY EMPTY value. Render nothing; the
|
|
29
|
+
// client removed this prose on purpose. Falling back to the
|
|
30
|
+
// authored children here would resurrect deleted content.
|
|
31
|
+
// value = blocks → render the stored document.
|
|
32
|
+
//
|
|
33
|
+
// Ships as a raw .astro under dist/richtext/ (an .astro file can't be a tsup
|
|
34
|
+
// entry) and is Vite-free at the package boundary.
|
|
35
|
+
// =============================================================================
|
|
36
|
+
import CanciaRichText from "./CanciaRichText.astro";
|
|
37
|
+
|
|
38
|
+
export interface Props {
|
|
39
|
+
/** Content key — what the toolbar edits and what the value is stored under. */
|
|
40
|
+
key: string;
|
|
41
|
+
/** The resolved value from `tRich(key)`. `null` means "no override". */
|
|
42
|
+
value?: Record<string, unknown>[] | null;
|
|
43
|
+
/** Element to render as the region wrapper. Default: `div`. */
|
|
44
|
+
as?: string;
|
|
45
|
+
class?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { key, value = null, as: Tag = "div", class: className } = Astro.props;
|
|
49
|
+
|
|
50
|
+
// `null` (no row) is the only state that shows the authored markup. An empty
|
|
51
|
+
// array is a real stored value and must render empty — see the header.
|
|
52
|
+
const hasOverride = Array.isArray(value);
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<Tag data-cms={key} data-cms-type="richtext" class={className}>
|
|
56
|
+
{hasOverride ? <CanciaRichText value={value} /> : <slot />}
|
|
57
|
+
</Tag>
|
package/dist/richtext/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as PtStyle, g as PtListItem, d as PtBlock } from '../portable-text-
|
|
2
|
-
export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTextValue, e as PtDecorator, f as PtLinkMarkDef, h as PtSpan, j as isSafeHref, p as
|
|
1
|
+
import { i as PtStyle, g as PtListItem, d as PtBlock } from '../portable-text-D74aIuHn.js';
|
|
2
|
+
export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTextValue, e as PtDecorator, f as PtLinkMarkDef, h as PtSpan, j as isSafeHref, p as parseRichValue, k as portableTextSubsetSchema, l as ptBlockSchema, s as serializeRichValue } from '../portable-text-D74aIuHn.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/** A row as the editor holds it: raw shorthand text + style + optional list kind. */
|