@cancia/astro 0.7.0 → 0.8.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-UR5WC3RA.js → chunk-3B3CXOMK.js} +19 -5
- package/dist/{chunk-22DJVJBR.js → chunk-DIV2FFYX.js} +1 -1
- package/dist/{chunk-VFMOVGMC.js → chunk-HQIGNRIU.js} +2 -2
- package/dist/{chunk-BOIQNZAO.js → chunk-KHCFVVYV.js} +53 -1
- 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 +45 -2
- 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 +276 -15
- package/dist/loader/index.d.ts +24 -0
- package/dist/loader/index.js +3 -1
- 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/schema/index.d.ts +45 -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/transform/text-helper.d.ts +47 -0
- package/dist/transform/text-helper.js +64 -0
- package/package.json +9 -3
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createJsonFileAdapterV2
|
|
3
3
|
} from "./chunk-L2VKQJPY.js";
|
|
4
|
+
import {
|
|
5
|
+
isDraft
|
|
6
|
+
} from "./chunk-WVHTCFE6.js";
|
|
4
7
|
|
|
5
8
|
// src/loader/index.ts
|
|
6
9
|
import { join } from "path";
|
|
7
10
|
function makeId(locale, entryId) {
|
|
8
11
|
return `${locale}/${entryId}`;
|
|
9
12
|
}
|
|
10
|
-
async function syncOnce(ctx, lists, list, site) {
|
|
13
|
+
async function syncOnce(ctx, lists, list, site, schema, includeDrafts) {
|
|
11
14
|
ctx.store.clear();
|
|
12
|
-
const
|
|
15
|
+
const all = await lists.list(site, list);
|
|
16
|
+
const drafts = schema?.draftField && !includeDrafts ? all.filter((e) => isDraft(schema, e.data)) : [];
|
|
17
|
+
const draftIds = new Set(drafts.map((e) => `${e.locale}/${e.id}`));
|
|
18
|
+
const entries = draftIds.size ? all.filter((e) => !draftIds.has(`${e.locale}/${e.id}`)) : all;
|
|
13
19
|
for (const entry of entries) {
|
|
14
20
|
const id = makeId(entry.locale, entry.id);
|
|
15
21
|
const data = {
|
|
@@ -24,8 +30,9 @@ async function syncOnce(ctx, lists, list, site) {
|
|
|
24
30
|
digest: entry._rev
|
|
25
31
|
});
|
|
26
32
|
}
|
|
33
|
+
const skipped = drafts.length ? ` (${drafts.length} draft${drafts.length === 1 ? "" : "s"} skipped)` : "";
|
|
27
34
|
ctx.logger.info(
|
|
28
|
-
`cancia: loaded ${entries.length} entr${entries.length === 1 ? "y" : "ies"} from list "${list}"`
|
|
35
|
+
`cancia: loaded ${entries.length} entr${entries.length === 1 ? "y" : "ies"} from list "${list}"${skipped}`
|
|
29
36
|
);
|
|
30
37
|
}
|
|
31
38
|
function canciaLoader(opts) {
|
|
@@ -41,7 +48,7 @@ function canciaLoader(opts) {
|
|
|
41
48
|
projectRoot: opts.projectRoot ?? process.cwd()
|
|
42
49
|
});
|
|
43
50
|
const { lists } = storage;
|
|
44
|
-
await syncOnce(ctx, lists, opts.list, opts.site);
|
|
51
|
+
await syncOnce(ctx, lists, opts.list, opts.site, opts.schema, opts.includeDrafts);
|
|
45
52
|
if (ctx.watcher && !watcherAttached) {
|
|
46
53
|
watcherAttached = true;
|
|
47
54
|
const root = opts.projectRoot ?? process.cwd();
|
|
@@ -53,7 +60,14 @@ function canciaLoader(opts) {
|
|
|
53
60
|
pendingTimer = null;
|
|
54
61
|
inflight = inflight.catch(() => {
|
|
55
62
|
}).then(
|
|
56
|
-
() => syncOnce(
|
|
63
|
+
() => syncOnce(
|
|
64
|
+
latestCtx,
|
|
65
|
+
lists,
|
|
66
|
+
opts.list,
|
|
67
|
+
opts.site,
|
|
68
|
+
opts.schema,
|
|
69
|
+
opts.includeDrafts
|
|
70
|
+
).catch((err) => {
|
|
57
71
|
latestCtx.logger.error(`cancia: resync failed \u2014 ${err.message}`);
|
|
58
72
|
})
|
|
59
73
|
);
|
|
@@ -53,6 +53,56 @@ var ptBlockSchema = z.object({
|
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
var portableTextSubsetSchema = z.array(ptBlockSchema);
|
|
56
|
+
function parseRichValue(raw) {
|
|
57
|
+
if (raw == null) return [];
|
|
58
|
+
let candidate = raw;
|
|
59
|
+
if (typeof raw === "string") {
|
|
60
|
+
const trimmed = raw.trim();
|
|
61
|
+
if (trimmed === "") return [];
|
|
62
|
+
if (trimmed.startsWith("[")) {
|
|
63
|
+
try {
|
|
64
|
+
candidate = JSON.parse(trimmed);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
return [textBlock(raw)];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(candidate)) return [];
|
|
73
|
+
const sanitised = candidate.map(stripUnsafeHrefs);
|
|
74
|
+
const parsed = portableTextSubsetSchema.safeParse(sanitised);
|
|
75
|
+
return parsed.success ? parsed.data : [];
|
|
76
|
+
}
|
|
77
|
+
function serializeRichValue(value) {
|
|
78
|
+
return value.length === 0 ? "" : JSON.stringify(value);
|
|
79
|
+
}
|
|
80
|
+
function textBlock(text) {
|
|
81
|
+
return {
|
|
82
|
+
_type: "block",
|
|
83
|
+
_key: "legacy",
|
|
84
|
+
style: "normal",
|
|
85
|
+
markDefs: [],
|
|
86
|
+
children: [{ _type: "span", _key: "legacy0", text, marks: [] }]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function stripUnsafeHrefs(block) {
|
|
90
|
+
if (!block || typeof block !== "object") return block;
|
|
91
|
+
const b = block;
|
|
92
|
+
if (!Array.isArray(b.markDefs)) return block;
|
|
93
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
94
|
+
const markDefs = b.markDefs.filter((def) => {
|
|
95
|
+
const href = def?.href;
|
|
96
|
+
if (typeof href === "string" && isSafeHref(href)) return true;
|
|
97
|
+
if (typeof def?._key === "string") dropped.add(def._key);
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
if (dropped.size === 0) return block;
|
|
101
|
+
const children = Array.isArray(b.children) ? b.children.map(
|
|
102
|
+
(span) => Array.isArray(span?.marks) ? { ...span, marks: span.marks.filter((m) => !dropped.has(m)) } : span
|
|
103
|
+
) : b.children;
|
|
104
|
+
return { ...b, markDefs, children };
|
|
105
|
+
}
|
|
56
106
|
|
|
57
107
|
export {
|
|
58
108
|
PT_STYLES,
|
|
@@ -60,5 +110,7 @@ export {
|
|
|
60
110
|
PT_DECORATORS,
|
|
61
111
|
isSafeHref,
|
|
62
112
|
ptBlockSchema,
|
|
63
|
-
portableTextSubsetSchema
|
|
113
|
+
portableTextSubsetSchema,
|
|
114
|
+
parseRichValue,
|
|
115
|
+
serializeRichValue
|
|
64
116
|
};
|
|
@@ -32,8 +32,15 @@ async function loadListSchema(projectRoot, listName, overridePath) {
|
|
|
32
32
|
const schemas = await loadSchemas(projectRoot, overridePath);
|
|
33
33
|
return schemas[listName] ?? null;
|
|
34
34
|
}
|
|
35
|
+
function clearSchemaCache() {
|
|
36
|
+
_cache = null;
|
|
37
|
+
_cachedPath = null;
|
|
38
|
+
_cachedMtime = 0;
|
|
39
|
+
}
|
|
35
40
|
|
|
36
41
|
export {
|
|
42
|
+
resolveSchemasPath,
|
|
37
43
|
loadSchemas,
|
|
38
|
-
loadListSchema
|
|
44
|
+
loadListSchema,
|
|
45
|
+
clearSchemaCache
|
|
39
46
|
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isSafeHref,
|
|
3
3
|
portableTextSubsetSchema
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KHCFVVYV.js";
|
|
5
5
|
|
|
6
6
|
// src/schema/index.ts
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
function isDraft(schema, data) {
|
|
9
|
+
if (!schema.draftField) return false;
|
|
10
|
+
return data[schema.draftField] === true;
|
|
11
|
+
}
|
|
8
12
|
function defineList(opts) {
|
|
9
13
|
return {
|
|
10
14
|
label: opts.label,
|
|
@@ -12,6 +16,7 @@ function defineList(opts) {
|
|
|
12
16
|
titleField: opts.titleField,
|
|
13
17
|
bodyField: opts.bodyField,
|
|
14
18
|
slugField: opts.slugField,
|
|
19
|
+
draftField: opts.draftField,
|
|
15
20
|
fields: opts.fields,
|
|
16
21
|
validator: z.object(opts.fields)
|
|
17
22
|
};
|
|
@@ -23,6 +28,15 @@ var defineField = {
|
|
|
23
28
|
email: (o) => z.string().email().meta({ widget: "email", ...o }),
|
|
24
29
|
datetime: (o) => z.string().datetime().meta({ widget: "datetime", ...o }),
|
|
25
30
|
checkbox: (o) => z.boolean().meta({ widget: "checkbox", ...o }),
|
|
31
|
+
/**
|
|
32
|
+
* The draft flag for a list's `draftField`. A checkbox that DEFAULTS to
|
|
33
|
+
* false, which is what makes it safe to add to an existing list: every
|
|
34
|
+
* stored entry that predates the field validates and reads as published
|
|
35
|
+
* rather than vanishing from the built site.
|
|
36
|
+
*
|
|
37
|
+
* Label defaults to "Draft" so the affordance reads the same on every list.
|
|
38
|
+
*/
|
|
39
|
+
draft: (o) => z.boolean().default(false).meta({ widget: "checkbox", label: "Draft", ...o }),
|
|
26
40
|
number: (o) => {
|
|
27
41
|
let n = z.number();
|
|
28
42
|
if (o?.min != null) n = n.min(o.min);
|
|
@@ -167,6 +181,7 @@ function describeList(name, schema) {
|
|
|
167
181
|
titleField: schema.titleField,
|
|
168
182
|
bodyField: schema.bodyField,
|
|
169
183
|
slugField: schema.slugField,
|
|
184
|
+
draftField: schema.draftField,
|
|
170
185
|
fields
|
|
171
186
|
};
|
|
172
187
|
}
|
|
@@ -202,6 +217,7 @@ function isExternalHref(href) {
|
|
|
202
217
|
|
|
203
218
|
export {
|
|
204
219
|
z,
|
|
220
|
+
isDraft,
|
|
205
221
|
defineList,
|
|
206
222
|
defineField,
|
|
207
223
|
slugify,
|
package/dist/content.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ interface LinkValue {
|
|
|
9
9
|
}
|
|
10
10
|
/** Resolve a link field, falling back to the values authored in the page. */
|
|
11
11
|
type TranslateLink = (key: string, fallback: LinkValue) => LinkValue;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a rich-text region (plan 051).
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` when there is NO stored override — which the region renders as
|
|
16
|
+
* "show the markup authored in the page". That is deliberate and load-bearing:
|
|
17
|
+
* a rich fallback cannot be a JS string, so the authored markup IS the fallback,
|
|
18
|
+
* which is what keeps an empty DB rendering byte-identically.
|
|
19
|
+
*
|
|
20
|
+
* `[]` is a different answer from `null`: it means a stored, deliberately empty
|
|
21
|
+
* value — the client removed the prose on purpose — and renders nothing.
|
|
22
|
+
*/
|
|
23
|
+
type TranslateRich = (key: string) => RichValue | null;
|
|
24
|
+
/** A Portable-Text subset document. Structurally typed to keep this subpath Vite-free. */
|
|
25
|
+
type RichValue = Record<string, unknown>[];
|
|
12
26
|
interface ContentReaderOptions {
|
|
13
27
|
/** Site id (the `?site=` query param and the storage `site` column). */
|
|
14
28
|
site: string;
|
|
@@ -57,10 +71,18 @@ interface ContentReader {
|
|
|
57
71
|
* authored in the page. Never throws; drops an unsafe href.
|
|
58
72
|
*/
|
|
59
73
|
makeTLink(cms: ContentMap): TranslateLink;
|
|
60
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Build a rich-text resolver over a content map. Returns `null` when no
|
|
76
|
+
* override is stored, which a rich region renders as "show the markup
|
|
77
|
+
* authored in the page" — the reason an empty DB still renders identically.
|
|
78
|
+
* `[]` means a stored, deliberately empty value and renders nothing.
|
|
79
|
+
*/
|
|
80
|
+
makeRich(cms: ContentMap): TranslateRich;
|
|
81
|
+
/** Sugar: `const { t, tLink, tRich, cms } = await reader.get()` in frontmatter. */
|
|
61
82
|
get(): Promise<{
|
|
62
83
|
t: Translate;
|
|
63
84
|
tLink: TranslateLink;
|
|
85
|
+
tRich: TranslateRich;
|
|
64
86
|
cms: ContentMap;
|
|
65
87
|
}>;
|
|
66
88
|
}
|
|
@@ -71,4 +93,4 @@ interface ContentReader {
|
|
|
71
93
|
*/
|
|
72
94
|
declare function createContentReader(opts: ContentReaderOptions): ContentReader;
|
|
73
95
|
|
|
74
|
-
export { type ContentMap, type ContentReader, type ContentReaderOptions, type LinkValue, type Translate, type TranslateLink, createContentReader };
|
|
96
|
+
export { type ContentMap, type ContentReader, type ContentReaderOptions, type LinkValue, type RichValue, type Translate, type TranslateLink, type TranslateRich, createContentReader };
|
package/dist/content.js
CHANGED
|
@@ -37,6 +37,46 @@ function parseLink(raw, fallback) {
|
|
|
37
37
|
}
|
|
38
38
|
return { label: raw, href: fallback.href };
|
|
39
39
|
}
|
|
40
|
+
function parseRich(raw) {
|
|
41
|
+
if (raw === void 0) return null;
|
|
42
|
+
const trimmed = raw.trim();
|
|
43
|
+
if (trimmed === "") return [];
|
|
44
|
+
if (!trimmed.startsWith("[")) {
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
_type: "block",
|
|
48
|
+
_key: "legacy",
|
|
49
|
+
style: "normal",
|
|
50
|
+
markDefs: [],
|
|
51
|
+
children: [{ _type: "span", _key: "legacy0", text: raw, marks: [] }]
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(trimmed);
|
|
57
|
+
if (!Array.isArray(parsed)) return [];
|
|
58
|
+
return parsed.map(stripUnsafeMarkDefs);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function stripUnsafeMarkDefs(block) {
|
|
64
|
+
if (!block || typeof block !== "object") return block;
|
|
65
|
+
const b = block;
|
|
66
|
+
if (!Array.isArray(b.markDefs)) return block;
|
|
67
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
68
|
+
const markDefs = b.markDefs.filter((def) => {
|
|
69
|
+
const href = def?.href;
|
|
70
|
+
if (typeof href === "string" && isSafeHref(href)) return true;
|
|
71
|
+
if (typeof def?._key === "string") dropped.add(def._key);
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
if (dropped.size === 0) return block;
|
|
75
|
+
const children = Array.isArray(b.children) ? b.children.map(
|
|
76
|
+
(span) => Array.isArray(span?.marks) ? { ...span, marks: span.marks.filter((m) => !dropped.has(m)) } : span
|
|
77
|
+
) : b.children;
|
|
78
|
+
return { ...b, markDefs, children };
|
|
79
|
+
}
|
|
40
80
|
function createContentReader(opts) {
|
|
41
81
|
const site = opts.site;
|
|
42
82
|
const lang = opts.lang ?? "en";
|
|
@@ -73,11 +113,14 @@ function createContentReader(opts) {
|
|
|
73
113
|
function makeTLink(cms) {
|
|
74
114
|
return (key, fallback) => parseLink(cms[`${key}.${lang}`], fallback);
|
|
75
115
|
}
|
|
116
|
+
function makeRich(cms) {
|
|
117
|
+
return (key) => parseRich(cms[`${key}.${lang}`]);
|
|
118
|
+
}
|
|
76
119
|
async function get() {
|
|
77
120
|
const cms = await getCMS();
|
|
78
|
-
return { t: makeT(cms), tLink: makeTLink(cms), cms };
|
|
121
|
+
return { t: makeT(cms), tLink: makeTLink(cms), tRich: makeRich(cms), cms };
|
|
79
122
|
}
|
|
80
|
-
return { getCMS, makeT, makeTLink, get };
|
|
123
|
+
return { getCMS, makeT, makeTLink, makeRich, get };
|
|
81
124
|
}
|
|
82
125
|
export {
|
|
83
126
|
createContentReader
|
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,13 +1,12 @@
|
|
|
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
8
|
makeListsRoutes
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import {
|
|
8
|
-
makeSchemasRoute
|
|
9
|
-
} from "./chunk-VFMOVGMC.js";
|
|
10
|
-
import "./chunk-NG5GJME5.js";
|
|
9
|
+
} from "./chunk-DIV2FFYX.js";
|
|
11
10
|
import {
|
|
12
11
|
makeLocalUploadHandler,
|
|
13
12
|
makeR2UploadHandler,
|
|
@@ -15,15 +14,6 @@ import {
|
|
|
15
14
|
setCanciaRuntime
|
|
16
15
|
} from "./chunk-AGNPUTKS.js";
|
|
17
16
|
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
17
|
import {
|
|
28
18
|
createSQLiteAdapter
|
|
29
19
|
} from "./chunk-CJDIVWO3.js";
|
|
@@ -33,6 +23,10 @@ import {
|
|
|
33
23
|
createSqliteAdapterV2
|
|
34
24
|
} from "./chunk-GNWD7EL2.js";
|
|
35
25
|
import "./chunk-U7V53JX7.js";
|
|
26
|
+
import "./chunk-VL6FO446.js";
|
|
27
|
+
import {
|
|
28
|
+
canciaLoader
|
|
29
|
+
} from "./chunk-3B3CXOMK.js";
|
|
36
30
|
import {
|
|
37
31
|
createJsonFileAdapter,
|
|
38
32
|
createJsonFileAdapterV2
|
|
@@ -40,11 +34,18 @@ import {
|
|
|
40
34
|
import {
|
|
41
35
|
RevConflictError
|
|
42
36
|
} from "./chunk-7IA5B5CF.js";
|
|
43
|
-
import
|
|
37
|
+
import {
|
|
38
|
+
defineField,
|
|
39
|
+
defineList,
|
|
40
|
+
describeList,
|
|
41
|
+
z
|
|
42
|
+
} from "./chunk-WVHTCFE6.js";
|
|
43
|
+
import "./chunk-KHCFVVYV.js";
|
|
44
44
|
|
|
45
45
|
// src/integration.ts
|
|
46
46
|
import { loadEnv } from "vite";
|
|
47
47
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
48
|
+
import { readFile } from "fs/promises";
|
|
48
49
|
import { isAbsolute, join as join2 } from "path";
|
|
49
50
|
|
|
50
51
|
// src/routes/content.ts
|
|
@@ -198,6 +199,211 @@ CANCIA_TOKEN=${token}
|
|
|
198
199
|
return token;
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
// src/transform/attribute-text.ts
|
|
203
|
+
import { parse } from "@astrojs/compiler";
|
|
204
|
+
var HELPER_NAME = "__canciaText";
|
|
205
|
+
var LINK_HELPER_NAME = "__canciaLink";
|
|
206
|
+
var LOADER_NAME = "__canciaContent";
|
|
207
|
+
var CONTENT_LOCAL = "__canciaCms";
|
|
208
|
+
var HELPER_MODULE = "virtual:cancia/text";
|
|
209
|
+
var RAW_TEXT = /* @__PURE__ */ new Set([
|
|
210
|
+
"script",
|
|
211
|
+
"style",
|
|
212
|
+
"pre",
|
|
213
|
+
"textarea",
|
|
214
|
+
"code",
|
|
215
|
+
"title",
|
|
216
|
+
"option",
|
|
217
|
+
"noscript",
|
|
218
|
+
"template"
|
|
219
|
+
]);
|
|
220
|
+
function byteToCharMap(source) {
|
|
221
|
+
let ascii = true;
|
|
222
|
+
for (let i = 0; i < source.length; i++) {
|
|
223
|
+
if (source.charCodeAt(i) > 127) {
|
|
224
|
+
ascii = false;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (ascii) return (b) => b;
|
|
229
|
+
const map = /* @__PURE__ */ new Map();
|
|
230
|
+
let byte = 0;
|
|
231
|
+
for (let char = 0; char < source.length; char++) {
|
|
232
|
+
map.set(byte, char);
|
|
233
|
+
const code = source.codePointAt(char);
|
|
234
|
+
byte += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
235
|
+
if (code >= 65536) char++;
|
|
236
|
+
}
|
|
237
|
+
map.set(byte, source.length);
|
|
238
|
+
return (b) => map.get(b) ?? b;
|
|
239
|
+
}
|
|
240
|
+
function quote(text) {
|
|
241
|
+
return JSON.stringify(text);
|
|
242
|
+
}
|
|
243
|
+
function findOpenAngle(node, src, at) {
|
|
244
|
+
const name = node.name ?? "";
|
|
245
|
+
const floor = Math.max(0, at - 4096);
|
|
246
|
+
for (let i = Math.min(at, src.length - 1); i >= floor; i--) {
|
|
247
|
+
if (src[i] !== "<") continue;
|
|
248
|
+
if (src.slice(i + 1, i + 1 + name.length) !== name) continue;
|
|
249
|
+
const boundary = src[i + 1 + name.length];
|
|
250
|
+
if (boundary === void 0 || /[\s/>]/.test(boundary)) return i;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function readAttr(openTag, name) {
|
|
255
|
+
const re = new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`);
|
|
256
|
+
return re.exec(openTag)?.[1] ?? null;
|
|
257
|
+
}
|
|
258
|
+
function findOpenTagEnd(src, lt) {
|
|
259
|
+
let quoteChar = null;
|
|
260
|
+
for (let i = lt; i < src.length; i++) {
|
|
261
|
+
const c = src[i];
|
|
262
|
+
if (quoteChar) {
|
|
263
|
+
if (c === quoteChar) quoteChar = null;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (c === '"' || c === "'") {
|
|
267
|
+
quoteChar = c;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (c === ">") return i + 1;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
async function transformAttributeText(source) {
|
|
275
|
+
if (!source.includes("data-cms")) {
|
|
276
|
+
return { code: source, changed: false, substituted: [] };
|
|
277
|
+
}
|
|
278
|
+
const toChar = byteToCharMap(source);
|
|
279
|
+
let ast;
|
|
280
|
+
try {
|
|
281
|
+
ast = (await parse(source, { position: true })).ast;
|
|
282
|
+
} catch {
|
|
283
|
+
return { code: source, changed: false, substituted: [] };
|
|
284
|
+
}
|
|
285
|
+
const edits = [];
|
|
286
|
+
const substituted = [];
|
|
287
|
+
function visit(node) {
|
|
288
|
+
if (node.type === "element" && RAW_TEXT.has((node.name ?? "").toLowerCase())) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (node.type === "element") {
|
|
292
|
+
const reported = node.position?.start?.offset;
|
|
293
|
+
if (reported !== void 0) {
|
|
294
|
+
const lt = findOpenAngle(node, source, toChar(reported));
|
|
295
|
+
if (lt !== null) {
|
|
296
|
+
const gt = findOpenTagEnd(source, lt);
|
|
297
|
+
if (gt !== null) {
|
|
298
|
+
const openTag = source.slice(lt, gt);
|
|
299
|
+
const key = readAttr(openTag, "data-cms");
|
|
300
|
+
if (key !== null) {
|
|
301
|
+
const declaredType = readAttr(openTag, "data-cms-type");
|
|
302
|
+
const isList = /\sdata-cms-list\s*=/.test(openTag);
|
|
303
|
+
if (declaredType === "link" && !isList) {
|
|
304
|
+
const kids = (node.children ?? []).filter(
|
|
305
|
+
(c) => c.type !== "text" || (c.value ?? "").trim() !== ""
|
|
306
|
+
);
|
|
307
|
+
const soleText = kids.length === 1 && kids[0].type === "text" ? kids[0] : null;
|
|
308
|
+
const authoredHref = readAttr(openTag, "href");
|
|
309
|
+
if (soleText && authoredHref !== null) {
|
|
310
|
+
const raw = soleText.value ?? "";
|
|
311
|
+
const literal = raw.replace(/\s+/g, " ").trim();
|
|
312
|
+
const s = soleText.position?.start?.offset;
|
|
313
|
+
const e = soleText.position?.end?.offset;
|
|
314
|
+
const hrefRe = /\shref\s*=\s*(["'])([^"']*)\1/.exec(openTag);
|
|
315
|
+
if (literal !== "" && s !== void 0 && e !== void 0 && hrefRe) {
|
|
316
|
+
const call = `${LINK_HELPER_NAME}(${CONTENT_LOCAL}, ${quote(key)}, ${quote(literal)}, ${quote(hrefRe[2])})`;
|
|
317
|
+
edits.push({
|
|
318
|
+
start: lt + hrefRe.index,
|
|
319
|
+
end: lt + hrefRe.index + hrefRe[0].length,
|
|
320
|
+
text: ` href={${call}.href}`
|
|
321
|
+
});
|
|
322
|
+
edits.push({
|
|
323
|
+
start: toChar(s),
|
|
324
|
+
end: toChar(e),
|
|
325
|
+
text: `{${call}.label}`
|
|
326
|
+
});
|
|
327
|
+
substituted.push({ key, line: node.position?.start?.line ?? 0 });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const typed = declaredType !== null;
|
|
332
|
+
if (!typed && !isList) {
|
|
333
|
+
const kids = (node.children ?? []).filter(
|
|
334
|
+
(c) => c.type !== "text" || (c.value ?? "").trim() !== ""
|
|
335
|
+
);
|
|
336
|
+
const soleText = kids.length === 1 && kids[0].type === "text" ? kids[0] : null;
|
|
337
|
+
if (soleText) {
|
|
338
|
+
const raw = soleText.value ?? "";
|
|
339
|
+
const literal = raw.replace(/\s+/g, " ").trim();
|
|
340
|
+
const s = soleText.position?.start?.offset;
|
|
341
|
+
const e = soleText.position?.end?.offset;
|
|
342
|
+
if (literal !== "" && s !== void 0 && e !== void 0) {
|
|
343
|
+
edits.push({
|
|
344
|
+
start: toChar(s),
|
|
345
|
+
end: toChar(e),
|
|
346
|
+
// Emitted as an EXPRESSION, not a spliced string: Astro
|
|
347
|
+
// escapes {expression} output, which is the XSS gate. A
|
|
348
|
+
// probe proved naive splicing executes a stored
|
|
349
|
+
// <script> tag.
|
|
350
|
+
text: `{${HELPER_NAME}(${CONTENT_LOCAL}, ${quote(key)}, ${quote(literal)})}`
|
|
351
|
+
});
|
|
352
|
+
substituted.push({
|
|
353
|
+
key,
|
|
354
|
+
line: node.position?.start?.line ?? 0
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const child of node.children ?? []) visit(child);
|
|
365
|
+
}
|
|
366
|
+
visit(ast);
|
|
367
|
+
if (edits.length === 0) {
|
|
368
|
+
return { code: source, changed: false, substituted: [] };
|
|
369
|
+
}
|
|
370
|
+
edits.sort((a, b) => b.start - a.start);
|
|
371
|
+
let code = source;
|
|
372
|
+
for (const edit of edits) {
|
|
373
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
code: injectImports(code, {
|
|
377
|
+
text: code.includes(`${HELPER_NAME}(`),
|
|
378
|
+
link: code.includes(`${LINK_HELPER_NAME}(`)
|
|
379
|
+
}),
|
|
380
|
+
changed: true,
|
|
381
|
+
substituted
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function injectImports(code, used) {
|
|
385
|
+
const names = [
|
|
386
|
+
LOADER_NAME,
|
|
387
|
+
used.text ? HELPER_NAME : null,
|
|
388
|
+
used.link ? LINK_HELPER_NAME : null
|
|
389
|
+
].filter(Boolean).join(", ");
|
|
390
|
+
const preamble = [
|
|
391
|
+
`import { ${names} } from ${quote(HELPER_MODULE)};`,
|
|
392
|
+
`const ${CONTENT_LOCAL} = await ${LOADER_NAME}();`
|
|
393
|
+
].join("\n");
|
|
394
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(code);
|
|
395
|
+
if (!fm) {
|
|
396
|
+
return `---
|
|
397
|
+
${preamble}
|
|
398
|
+
---
|
|
399
|
+
${code}`;
|
|
400
|
+
}
|
|
401
|
+
if (fm[1].includes(HELPER_MODULE)) return code;
|
|
402
|
+
const at = fm.index + fm[0].length - 3;
|
|
403
|
+
return `${code.slice(0, at)}${preamble}
|
|
404
|
+
${code.slice(at)}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
201
407
|
// src/integration.ts
|
|
202
408
|
function resolvePublish(opts, hasDeployHookEnv) {
|
|
203
409
|
const publish = opts.publish;
|
|
@@ -343,6 +549,11 @@ function canciaIntegration(opts = {}) {
|
|
|
343
549
|
injectRoute({ pattern: "/api/cancia/lists/[listName]/[id]", entrypoint: endpointPath("lists"), prerender: false });
|
|
344
550
|
const runtimeModulePath = fileURLToPath(new URL("./runtime.js", import.meta.url));
|
|
345
551
|
const RESOLVED_VIRTUAL_ID = "\0virtual:cancia/runtime";
|
|
552
|
+
const textHelperModulePath = fileURLToPath(
|
|
553
|
+
new URL("./transform/text-helper.js", import.meta.url)
|
|
554
|
+
);
|
|
555
|
+
const TEXT_VIRTUAL_ID = "virtual:cancia/text";
|
|
556
|
+
const RESOLVED_TEXT_VIRTUAL_ID = "\0virtual:cancia/text";
|
|
346
557
|
updateConfig({
|
|
347
558
|
vite: {
|
|
348
559
|
plugins: [
|
|
@@ -363,6 +574,56 @@ function canciaIntegration(opts = {}) {
|
|
|
363
574
|
`export { getCanciaRuntime };`
|
|
364
575
|
].join("\n");
|
|
365
576
|
}
|
|
577
|
+
},
|
|
578
|
+
// ---- plan 054: attribute-only authoring ---------------------
|
|
579
|
+
// Rewrites `<h1 data-cms="k">text</h1>` into a helper call before
|
|
580
|
+
// Astro's compiler reads the file, so a developer never writes a
|
|
581
|
+
// t() call, an import, or a repeated key.
|
|
582
|
+
//
|
|
583
|
+
// This works because Astro compiles the `source` ARGUMENT rather
|
|
584
|
+
// than reading from disk (deliberate since astro#3889), so an
|
|
585
|
+
// upstream transform is honoured.
|
|
586
|
+
//
|
|
587
|
+
// NOTE `enforce: "pre"` does NOT place this before Astro's own
|
|
588
|
+
// plugin — Astro prepends its own and mergeConfig appends ours
|
|
589
|
+
// (measured: ours at 20, astro:build at 9). It works because Vite
|
|
590
|
+
// CHAINS transform hooks and Astro's has an id filter. Do not
|
|
591
|
+
// write anything here that depends on plugin array position.
|
|
592
|
+
{
|
|
593
|
+
name: "vite-plugin-cancia-attribute-text",
|
|
594
|
+
enforce: "pre",
|
|
595
|
+
resolveId(id) {
|
|
596
|
+
if (id === TEXT_VIRTUAL_ID) return RESOLVED_TEXT_VIRTUAL_ID;
|
|
597
|
+
},
|
|
598
|
+
async load(id) {
|
|
599
|
+
if (id.endsWith(".astro") && !id.includes("?")) {
|
|
600
|
+
let source;
|
|
601
|
+
try {
|
|
602
|
+
source = await readFile(id, "utf8");
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
const result = await transformAttributeText(source);
|
|
607
|
+
return result.changed ? result.code : null;
|
|
608
|
+
}
|
|
609
|
+
if (id !== RESOLVED_TEXT_VIRTUAL_ID) return;
|
|
610
|
+
const importUrl = pathToFileURL(textHelperModulePath).href;
|
|
611
|
+
return [
|
|
612
|
+
`import { __canciaText, __canciaLink, __canciaContent, configureTextHelper } from ${JSON.stringify(importUrl)};`,
|
|
613
|
+
`import { getCanciaRuntime } from "virtual:cancia/runtime";`,
|
|
614
|
+
`configureTextHelper({`,
|
|
615
|
+
` site: ${JSON.stringify(site)},`,
|
|
616
|
+
` lang: ${JSON.stringify(languages[0] ?? "en")},`,
|
|
617
|
+
// The read goes through the runtime so it uses whatever
|
|
618
|
+
// storage this project configured — no second adapter path.
|
|
619
|
+
` read: async () => {`,
|
|
620
|
+
` const rt = getCanciaRuntime();`,
|
|
621
|
+
` return rt.storageV2 ? rt.storageV2.kv.getAll(${JSON.stringify(site)}) : {};`,
|
|
622
|
+
` },`,
|
|
623
|
+
`});`,
|
|
624
|
+
`export { __canciaText, __canciaLink, __canciaContent };`
|
|
625
|
+
].join("\n");
|
|
626
|
+
}
|
|
366
627
|
}
|
|
367
628
|
]
|
|
368
629
|
}
|
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
|
@@ -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. */
|
package/dist/richtext/index.js
CHANGED
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
PT_LIST_ITEMS,
|
|
4
4
|
PT_STYLES,
|
|
5
5
|
isSafeHref,
|
|
6
|
+
parseRichValue,
|
|
6
7
|
portableTextSubsetSchema,
|
|
7
|
-
ptBlockSchema
|
|
8
|
-
|
|
8
|
+
ptBlockSchema,
|
|
9
|
+
serializeRichValue
|
|
10
|
+
} from "../chunk-KHCFVVYV.js";
|
|
9
11
|
|
|
10
12
|
// src/richtext/markdown.ts
|
|
11
13
|
function keyGen(prefix) {
|
|
@@ -134,9 +136,11 @@ export {
|
|
|
134
136
|
PT_STYLES,
|
|
135
137
|
blockToRow,
|
|
136
138
|
isSafeHref,
|
|
139
|
+
parseRichValue,
|
|
137
140
|
portableTextSubsetSchema,
|
|
138
141
|
portableTextToRows,
|
|
139
142
|
ptBlockSchema,
|
|
140
143
|
rowToBlock,
|
|
141
|
-
rowsToPortableText
|
|
144
|
+
rowsToPortableText,
|
|
145
|
+
serializeRichValue
|
|
142
146
|
};
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export { z } from 'zod';
|
|
3
|
-
export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTextValue, d as PtBlock, e as PtDecorator, f as PtLinkMarkDef, g as PtListItem, h as PtSpan, i as PtStyle, j as isSafeHref, p as
|
|
3
|
+
export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTextValue, d as PtBlock, e as PtDecorator, f as PtLinkMarkDef, g as PtListItem, h as PtSpan, i as PtStyle, j as isSafeHref, p as parseRichValue, k as portableTextSubsetSchema, l as ptBlockSchema, s as serializeRichValue } from '../portable-text-D74aIuHn.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Widget hint the modal form uses to pick an input element. The Zod type
|
|
@@ -82,6 +82,21 @@ interface ListSchemaOptions<TFields extends Record<string, z.ZodTypeAny>> {
|
|
|
82
82
|
* Defaults to the entry ID. The server validates slug uniqueness on save.
|
|
83
83
|
*/
|
|
84
84
|
slugField?: keyof TFields & string;
|
|
85
|
+
/**
|
|
86
|
+
* Field name holding the entry's draft flag. When set, `canciaLoader`
|
|
87
|
+
* omits entries whose value is truthy from the content collection, so a
|
|
88
|
+
* draft never reaches the built site.
|
|
89
|
+
*
|
|
90
|
+
* The field MUST be a boolean with a default (`defineField.draft()` gives
|
|
91
|
+
* you exactly that) — an `undefined` draft flag reads as published, which
|
|
92
|
+
* is the safe direction for the pre-existing entries of a list that adds
|
|
93
|
+
* `draftField` later.
|
|
94
|
+
*
|
|
95
|
+
* Draft is deliberately per-list opt-in rather than an implicit field on
|
|
96
|
+
* every list: a list of office locations or nav links has no meaningful
|
|
97
|
+
* draft state, and a phantom "Draft" checkbox on every form is noise.
|
|
98
|
+
*/
|
|
99
|
+
draftField?: keyof TFields & string;
|
|
85
100
|
/** Field definitions. */
|
|
86
101
|
fields: TFields;
|
|
87
102
|
}
|
|
@@ -91,10 +106,24 @@ interface ListSchema<TFields extends Record<string, z.ZodTypeAny> = Record<strin
|
|
|
91
106
|
titleField: string;
|
|
92
107
|
bodyField: string | undefined;
|
|
93
108
|
slugField: string | undefined;
|
|
109
|
+
draftField: string | undefined;
|
|
94
110
|
fields: TFields;
|
|
95
111
|
/** Zod object schema combining all fields, for one-shot validation. */
|
|
96
112
|
validator: z.ZodObject<TFields>;
|
|
97
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* True when `entry.data` is a draft under `schema.draftField`.
|
|
116
|
+
*
|
|
117
|
+
* The single source of truth for "is this entry a draft" — the loader, the
|
|
118
|
+
* toolbar preview rows and any user-side filter all call THIS, so a draft is
|
|
119
|
+
* decided identically everywhere. Vite-free and dependency-free by design.
|
|
120
|
+
*
|
|
121
|
+
* Reads as published (`false`) when the list declares no draftField, when the
|
|
122
|
+
* field is absent, or when it is `undefined` — every ambiguous case resolves
|
|
123
|
+
* to "published", matching the field's own default and keeping entries that
|
|
124
|
+
* predate the draftField visible.
|
|
125
|
+
*/
|
|
126
|
+
declare function isDraft(schema: Pick<ListSchema, "draftField">, data: Record<string, unknown>): boolean;
|
|
98
127
|
declare function defineList<TFields extends Record<string, z.ZodTypeAny>>(opts: ListSchemaOptions<TFields>): ListSchema<TFields>;
|
|
99
128
|
/**
|
|
100
129
|
* One field as seen by the modal form. Plain JSON, no Zod refs.
|
|
@@ -154,6 +183,15 @@ declare const defineField: {
|
|
|
154
183
|
email: (o?: PlainFieldOptions) => z.ZodString;
|
|
155
184
|
datetime: (o?: PlainFieldOptions) => z.ZodString;
|
|
156
185
|
checkbox: (o?: PlainFieldOptions) => z.ZodBoolean;
|
|
186
|
+
/**
|
|
187
|
+
* The draft flag for a list's `draftField`. A checkbox that DEFAULTS to
|
|
188
|
+
* false, which is what makes it safe to add to an existing list: every
|
|
189
|
+
* stored entry that predates the field validates and reads as published
|
|
190
|
+
* rather than vanishing from the built site.
|
|
191
|
+
*
|
|
192
|
+
* Label defaults to "Draft" so the affordance reads the same on every list.
|
|
193
|
+
*/
|
|
194
|
+
draft: (o?: PlainFieldOptions) => z.ZodDefault<z.ZodBoolean>;
|
|
157
195
|
number: (o?: PlainFieldOptions & {
|
|
158
196
|
min?: number;
|
|
159
197
|
max?: number;
|
|
@@ -266,6 +304,11 @@ interface ListDescription {
|
|
|
266
304
|
titleField: string;
|
|
267
305
|
bodyField?: string;
|
|
268
306
|
slugField?: string;
|
|
307
|
+
/**
|
|
308
|
+
* Field holding the draft flag, when the list opts in. The toolbar uses it
|
|
309
|
+
* to badge unpublished rows; the loader uses it to omit them from the build.
|
|
310
|
+
*/
|
|
311
|
+
draftField?: string;
|
|
269
312
|
fields: FieldDescription[];
|
|
270
313
|
}
|
|
271
314
|
/**
|
|
@@ -317,4 +360,4 @@ declare function isExternalHref(href: string): boolean;
|
|
|
317
360
|
/** Map of list name → schema. What users export from src/cms/schemas.ts. */
|
|
318
361
|
type SchemasModule = Record<string, ListSchema>;
|
|
319
362
|
|
|
320
|
-
export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type LinkValue, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList, isExternalHref, parseLinkValue, serializeLinkValue, slugify };
|
|
363
|
+
export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type LinkValue, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList, isDraft, isExternalHref, parseLinkValue, serializeLinkValue, slugify };
|
package/dist/schema/index.js
CHANGED
|
@@ -2,20 +2,23 @@ import {
|
|
|
2
2
|
defineField,
|
|
3
3
|
defineList,
|
|
4
4
|
describeList,
|
|
5
|
+
isDraft,
|
|
5
6
|
isExternalHref,
|
|
6
7
|
parseLinkValue,
|
|
7
8
|
serializeLinkValue,
|
|
8
9
|
slugify,
|
|
9
10
|
z
|
|
10
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-WVHTCFE6.js";
|
|
11
12
|
import {
|
|
12
13
|
PT_DECORATORS,
|
|
13
14
|
PT_LIST_ITEMS,
|
|
14
15
|
PT_STYLES,
|
|
15
16
|
isSafeHref,
|
|
17
|
+
parseRichValue,
|
|
16
18
|
portableTextSubsetSchema,
|
|
17
|
-
ptBlockSchema
|
|
18
|
-
|
|
19
|
+
ptBlockSchema,
|
|
20
|
+
serializeRichValue
|
|
21
|
+
} from "../chunk-KHCFVVYV.js";
|
|
19
22
|
export {
|
|
20
23
|
PT_DECORATORS,
|
|
21
24
|
PT_LIST_ITEMS,
|
|
@@ -23,12 +26,15 @@ export {
|
|
|
23
26
|
defineField,
|
|
24
27
|
defineList,
|
|
25
28
|
describeList,
|
|
29
|
+
isDraft,
|
|
26
30
|
isExternalHref,
|
|
27
31
|
isSafeHref,
|
|
28
32
|
parseLinkValue,
|
|
33
|
+
parseRichValue,
|
|
29
34
|
portableTextSubsetSchema,
|
|
30
35
|
ptBlockSchema,
|
|
31
36
|
serializeLinkValue,
|
|
37
|
+
serializeRichValue,
|
|
32
38
|
slugify,
|
|
33
39
|
z
|
|
34
40
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ListSchema, SchemasModule } from './index.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
import '../portable-text-D74aIuHn.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the absolute path to the user's schemas.ts. Returns null if no
|
|
7
|
+
* file exists at the expected location.
|
|
8
|
+
*/
|
|
9
|
+
declare function resolveSchemasPath(projectRoot: string, overridePath?: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Load (and cache) the user's schemas module. Returns an empty map if no
|
|
12
|
+
* schemas.ts exists — the inline editor still works for KV edits without
|
|
13
|
+
* any lists defined.
|
|
14
|
+
*/
|
|
15
|
+
declare function loadSchemas(projectRoot: string, overridePath?: string): Promise<SchemasModule>;
|
|
16
|
+
/**
|
|
17
|
+
* Get a single list schema, or null if it doesn't exist.
|
|
18
|
+
*/
|
|
19
|
+
declare function loadListSchema(projectRoot: string, listName: string, overridePath?: string): Promise<ListSchema | null>;
|
|
20
|
+
/** For tests — drop the in-memory cache. */
|
|
21
|
+
declare function clearSchemaCache(): void;
|
|
22
|
+
|
|
23
|
+
export { clearSchemaCache, loadListSchema, loadSchemas, resolveSchemasPath };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ContentMap } from '../content.js';
|
|
2
|
+
|
|
3
|
+
/** Test/dev hook — a content change must not be served from a stale cache. */
|
|
4
|
+
declare function clearTextCache(): void;
|
|
5
|
+
interface TextHelperConfig {
|
|
6
|
+
site: string;
|
|
7
|
+
lang: string;
|
|
8
|
+
/** Reads the whole content map. Injected so this module imports no adapter. */
|
|
9
|
+
read: () => Promise<ContentMap>;
|
|
10
|
+
}
|
|
11
|
+
declare function configureTextHelper(next: TextHelperConfig): void;
|
|
12
|
+
/**
|
|
13
|
+
* Load the content map. Awaited once per page, in frontmatter.
|
|
14
|
+
*
|
|
15
|
+
* NEVER throws or rejects. A content read must not break a build — the same rule
|
|
16
|
+
* createContentReader holds — so every failure resolves to an empty map, and
|
|
17
|
+
* every call site then falls back to the text authored in the page. A site whose
|
|
18
|
+
* database is unreachable renders exactly as its source says.
|
|
19
|
+
*/
|
|
20
|
+
declare function __canciaContent(): Promise<ContentMap>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve one annotated element's text.
|
|
23
|
+
*
|
|
24
|
+
* `??` is load-bearing, not style: a stored "" means DELIBERATELY EMPTY (the
|
|
25
|
+
* client removed this text on purpose) and must NOT fall through to the authored
|
|
26
|
+
* fallback, or deleted content reappears on every build.
|
|
27
|
+
*/
|
|
28
|
+
declare function __canciaText(cms: ContentMap, key: string, fallback: string): string;
|
|
29
|
+
interface LinkValue {
|
|
30
|
+
label: string;
|
|
31
|
+
href: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a link field — label AND href from one stored value.
|
|
35
|
+
*
|
|
36
|
+
* A link stores `{label, href}` as JSON so a new label can never be saved
|
|
37
|
+
* against a stale href. Both halves have an authored fallback (the text, and the
|
|
38
|
+
* `href=` already in the markup), which is why a link needs no t() call either.
|
|
39
|
+
*
|
|
40
|
+
* An unsafe href is DROPPED on read, falling back to the authored one — the same
|
|
41
|
+
* rule parseLinkValue holds, and for the same reason: validation guards the
|
|
42
|
+
* write path, but a value can reach a renderer from an older row or a
|
|
43
|
+
* hand-edited DB, so the read is where the guarantee has to hold.
|
|
44
|
+
*/
|
|
45
|
+
declare function __canciaLink(cms: ContentMap, key: string, labelFallback: string, hrefFallback: string): LinkValue;
|
|
46
|
+
|
|
47
|
+
export { type LinkValue, type TextHelperConfig, __canciaContent, __canciaLink, __canciaText, clearTextCache, configureTextHelper };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/transform/text-helper.ts
|
|
2
|
+
var contentPromise = null;
|
|
3
|
+
function clearTextCache() {
|
|
4
|
+
contentPromise = null;
|
|
5
|
+
}
|
|
6
|
+
var config = null;
|
|
7
|
+
function configureTextHelper(next) {
|
|
8
|
+
config = next;
|
|
9
|
+
contentPromise = null;
|
|
10
|
+
}
|
|
11
|
+
async function __canciaContent() {
|
|
12
|
+
if (!config) return {};
|
|
13
|
+
try {
|
|
14
|
+
if (!contentPromise) contentPromise = config.read();
|
|
15
|
+
return await contentPromise;
|
|
16
|
+
} catch {
|
|
17
|
+
contentPromise = null;
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function lang() {
|
|
22
|
+
return config?.lang ?? "en";
|
|
23
|
+
}
|
|
24
|
+
function __canciaText(cms, key, fallback) {
|
|
25
|
+
return cms[`${key}.${lang()}`] ?? fallback;
|
|
26
|
+
}
|
|
27
|
+
function isSafeHref(href) {
|
|
28
|
+
const trimmed = href.trim();
|
|
29
|
+
if (trimmed === "") return true;
|
|
30
|
+
if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return true;
|
|
31
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
|
|
32
|
+
if (scheme) {
|
|
33
|
+
const firstSep = trimmed.search(/[/?#]/);
|
|
34
|
+
if (firstSep === -1 || scheme[1].length < firstSep) return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function __canciaLink(cms, key, labelFallback, hrefFallback) {
|
|
39
|
+
const fallback = { label: labelFallback, href: hrefFallback };
|
|
40
|
+
const raw = cms[`${key}.${lang()}`];
|
|
41
|
+
if (raw === void 0) return fallback;
|
|
42
|
+
if (raw === "") return { label: "", href: "" };
|
|
43
|
+
const trimmed = raw.trim();
|
|
44
|
+
if (trimmed.startsWith("{")) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(trimmed);
|
|
47
|
+
const href = String(parsed.href ?? "");
|
|
48
|
+
return {
|
|
49
|
+
label: String(parsed.label ?? "") || labelFallback,
|
|
50
|
+
href: href && isSafeHref(href) ? href : hrefFallback
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { label: raw, href: hrefFallback };
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
__canciaContent,
|
|
60
|
+
__canciaLink,
|
|
61
|
+
__canciaText,
|
|
62
|
+
clearTextCache,
|
|
63
|
+
configureTextHelper
|
|
64
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cancia/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Astro integration for Cancia CMS — inline editing with zero separate server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/schema/index.d.ts",
|
|
21
21
|
"import": "./dist/schema/index.js"
|
|
22
22
|
},
|
|
23
|
+
"./schema-loader": {
|
|
24
|
+
"types": "./dist/schema/loader.d.ts",
|
|
25
|
+
"import": "./dist/schema/loader.js"
|
|
26
|
+
},
|
|
23
27
|
"./loader": {
|
|
24
28
|
"types": "./dist/loader/index.d.ts",
|
|
25
29
|
"import": "./dist/loader/index.js"
|
|
@@ -40,7 +44,8 @@
|
|
|
40
44
|
"types": "./dist/richtext/index.d.ts",
|
|
41
45
|
"import": "./dist/richtext/index.js"
|
|
42
46
|
},
|
|
43
|
-
"./richtext/CanciaRichText.astro": "./dist/richtext/CanciaRichText.astro"
|
|
47
|
+
"./richtext/CanciaRichText.astro": "./dist/richtext/CanciaRichText.astro",
|
|
48
|
+
"./richtext/CanciaRichRegion.astro": "./dist/richtext/CanciaRichRegion.astro"
|
|
44
49
|
},
|
|
45
50
|
"files": [
|
|
46
51
|
"dist"
|
|
@@ -68,7 +73,8 @@
|
|
|
68
73
|
},
|
|
69
74
|
"dependencies": {
|
|
70
75
|
"astro-portabletext": "^0.13.0",
|
|
71
|
-
"zod": "^4.4.3"
|
|
76
|
+
"zod": "^4.4.3",
|
|
77
|
+
"@astrojs/compiler": "^2.10.0"
|
|
72
78
|
},
|
|
73
79
|
"scripts": {
|
|
74
80
|
"build": "tsup",
|