@cancia/astro 0.0.2 → 0.2.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-BOIQNZAO.js +64 -0
- package/dist/{chunk-KL2YMBFV.js → chunk-IIGDU5SV.js} +1 -1
- package/dist/{chunk-RKHJNQ6R.js → chunk-MCHQV6Y7.js} +62 -1
- package/dist/chunk-SXKZ2WUL.js +305 -0
- package/dist/endpoints/schemas.js +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -3
- package/dist/portable-text-BikSqS9T.d.ts +117 -0
- package/dist/richtext/CanciaRichText.astro +44 -0
- package/dist/richtext/Link.astro +25 -0
- package/dist/richtext/index.d.ts +23 -0
- package/dist/richtext/index.js +142 -0
- package/dist/schema/index.d.ts +100 -2
- package/dist/schema/index.js +17 -1
- package/dist/storage/index.d.ts +110 -1
- package/dist/storage/index.js +5 -1
- package/package.json +8 -2
- package/dist/chunk-AE4SIY24.js +0 -63
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/schema/portable-text.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var PT_STYLES = ["normal", "h2", "h3", "blockquote"];
|
|
4
|
+
var PT_LIST_ITEMS = ["bullet", "number"];
|
|
5
|
+
var PT_DECORATORS = ["strong", "em"];
|
|
6
|
+
function isSafeHref(href) {
|
|
7
|
+
if (typeof href !== "string") return false;
|
|
8
|
+
const trimmed = href.trim();
|
|
9
|
+
if (trimmed === "") return false;
|
|
10
|
+
if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return true;
|
|
11
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
|
|
12
|
+
if (schemeMatch) {
|
|
13
|
+
const beforeColon = trimmed.slice(0, schemeMatch[1].length);
|
|
14
|
+
const firstSep = trimmed.search(/[/?#]/);
|
|
15
|
+
const colonIndex = beforeColon.length;
|
|
16
|
+
if (firstSep === -1 || colonIndex < firstSep) return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
var ptLinkMarkDefSchema = z.object({
|
|
21
|
+
_type: z.literal("link"),
|
|
22
|
+
_key: z.string(),
|
|
23
|
+
href: z.string().refine(isSafeHref, "unsafe or unsupported URL scheme")
|
|
24
|
+
});
|
|
25
|
+
var ptSpanSchema = z.object({
|
|
26
|
+
_type: z.literal("span"),
|
|
27
|
+
_key: z.string(),
|
|
28
|
+
text: z.string(),
|
|
29
|
+
marks: z.array(z.string())
|
|
30
|
+
});
|
|
31
|
+
var ptBlockSchema = z.object({
|
|
32
|
+
_type: z.literal("block"),
|
|
33
|
+
_key: z.string(),
|
|
34
|
+
style: z.enum(PT_STYLES),
|
|
35
|
+
listItem: z.enum(PT_LIST_ITEMS).optional(),
|
|
36
|
+
level: z.number().int().positive().optional(),
|
|
37
|
+
markDefs: z.array(ptLinkMarkDefSchema).default([]),
|
|
38
|
+
children: z.array(ptSpanSchema)
|
|
39
|
+
}).superRefine((block, ctx) => {
|
|
40
|
+
const defKeys = new Set(block.markDefs.map((d) => d._key));
|
|
41
|
+
const decorators = new Set(PT_DECORATORS);
|
|
42
|
+
for (const span of block.children) {
|
|
43
|
+
for (const mark of span.marks) {
|
|
44
|
+
if (decorators.has(mark)) continue;
|
|
45
|
+
if (!defKeys.has(mark)) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
message: `Mark "${mark}" is not an allowed decorator and does not reference a link markDef`,
|
|
49
|
+
path: ["children"]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
var portableTextSubsetSchema = z.array(ptBlockSchema);
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
PT_STYLES,
|
|
59
|
+
PT_LIST_ITEMS,
|
|
60
|
+
PT_DECORATORS,
|
|
61
|
+
isSafeHref,
|
|
62
|
+
ptBlockSchema,
|
|
63
|
+
portableTextSubsetSchema
|
|
64
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
portableTextSubsetSchema
|
|
3
|
+
} from "./chunk-BOIQNZAO.js";
|
|
4
|
+
|
|
1
5
|
// src/schema/index.ts
|
|
2
6
|
import { z } from "zod";
|
|
3
7
|
function defineList(opts) {
|
|
@@ -26,13 +30,56 @@ var defineField = {
|
|
|
26
30
|
},
|
|
27
31
|
slug: (o) => z.string().regex(/^[a-z0-9-]+$/).meta({ widget: "slug", ...o }),
|
|
28
32
|
image: (o) => z.string().url().meta({ widget: "image", ...o }),
|
|
29
|
-
select: (o) => z.enum(o.options).meta({ widget: "select", ...o })
|
|
33
|
+
select: (o) => z.enum(o.options).meta({ widget: "select", ...o }),
|
|
34
|
+
/**
|
|
35
|
+
* A repeatable list of a single member type. `member` is any Zod type,
|
|
36
|
+
* typically another `defineField.*` (so it keeps its own `.meta({ widget })`
|
|
37
|
+
* and recursion carries labels/widgets into each row).
|
|
38
|
+
*
|
|
39
|
+
* bullets: f.array(f.text({ label: "Point" }), { label: "Key points" }),
|
|
40
|
+
* faqs: f.array(f.object({ q: f.text(), a: f.textarea() })),
|
|
41
|
+
*/
|
|
42
|
+
array: (member, o) => z.array(member).meta({ widget: "array", ...o }),
|
|
43
|
+
/**
|
|
44
|
+
* A nested group of named sub-fields, rendered as a collapsible fieldset.
|
|
45
|
+
*
|
|
46
|
+
* socials: f.object({ linkedin: f.url(), twitter: f.url() }, { label: "Socials" }),
|
|
47
|
+
*/
|
|
48
|
+
object: (shape, o) => z.object(shape).meta({ widget: "object", ...o }),
|
|
49
|
+
/**
|
|
50
|
+
* A pointer to an entry in another list. Stored as a plain string — the
|
|
51
|
+
* target entry's id. Per the v0.1.0 design (D5) there is NO integrity
|
|
52
|
+
* backend: dangling ids are tolerated and surfaced by the editor, never
|
|
53
|
+
* prevented. `list` is the target list's name (a key in the schemas module).
|
|
54
|
+
*
|
|
55
|
+
* author: f.reference({ list: "people", label: "Author" }),
|
|
56
|
+
* related: f.array(f.reference({ list: "posts" }), { label: "Related" }),
|
|
57
|
+
*/
|
|
58
|
+
reference: (o) => z.string().meta({ widget: "reference", ...o }),
|
|
59
|
+
/**
|
|
60
|
+
* A constrained rich-text body. Stored as a Portable-Text SUBSET array (D4):
|
|
61
|
+
* block styles normal/h2/h3/blockquote, bullet/number lists, strong/em marks,
|
|
62
|
+
* and link annotations — nothing else. The toolbar renders a structured block
|
|
63
|
+
* editor (approach B) over it, and `<CanciaRichText>` renders it to HTML.
|
|
64
|
+
*
|
|
65
|
+
* body: f.richtext({ label: "Body" }),
|
|
66
|
+
*
|
|
67
|
+
* The JSON-Schema type of this field is "array"; the explicit
|
|
68
|
+
* `widget: "richtext"` meta is what distinguishes it from a plain array in
|
|
69
|
+
* describeProperty.
|
|
70
|
+
*/
|
|
71
|
+
richtext: (o) => portableTextSubsetSchema.meta({ widget: "richtext", ...o })
|
|
30
72
|
};
|
|
73
|
+
function slugify(input) {
|
|
74
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
75
|
+
}
|
|
31
76
|
function humanise(name) {
|
|
32
77
|
return name.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
|
|
33
78
|
}
|
|
34
79
|
function inferWidget(prop, label) {
|
|
35
80
|
if (prop.widget) return prop.widget;
|
|
81
|
+
if (prop.type === "array") return "array";
|
|
82
|
+
if (prop.type === "object") return "object";
|
|
36
83
|
if (prop.enum) return "select";
|
|
37
84
|
if (prop.type === "boolean") return "checkbox";
|
|
38
85
|
if (prop.type === "number" || prop.type === "integer") return "number";
|
|
@@ -64,6 +111,19 @@ function describeProperty(name, prop, required) {
|
|
|
64
111
|
else if (prop.options) desc.options = prop.options;
|
|
65
112
|
if (prop.pattern !== void 0) desc.pattern = prop.pattern;
|
|
66
113
|
if (prop.source !== void 0) desc.source = prop.source;
|
|
114
|
+
if (desc.widget === "reference" && prop.list !== void 0) desc.referenceList = prop.list;
|
|
115
|
+
if (desc.widget === "richtext") {
|
|
116
|
+
return desc;
|
|
117
|
+
}
|
|
118
|
+
if (desc.widget === "array" && prop.items) {
|
|
119
|
+
desc.of = describeProperty("", prop.items, true);
|
|
120
|
+
}
|
|
121
|
+
if (desc.widget === "object" && prop.properties) {
|
|
122
|
+
const subRequired = new Set(prop.required ?? []);
|
|
123
|
+
desc.fields = Object.keys(prop.properties).map(
|
|
124
|
+
(subName) => describeProperty(subName, prop.properties[subName], subRequired.has(subName))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
67
127
|
return desc;
|
|
68
128
|
}
|
|
69
129
|
function describeList(name, schema) {
|
|
@@ -90,5 +150,6 @@ export {
|
|
|
90
150
|
z,
|
|
91
151
|
defineList,
|
|
92
152
|
defineField,
|
|
153
|
+
slugify,
|
|
93
154
|
describeList
|
|
94
155
|
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/storage/sqlite.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
var require2 = createRequire(import.meta.url);
|
|
4
|
+
var _db = null;
|
|
5
|
+
function createDB(dbPath) {
|
|
6
|
+
const Database = require2("better-sqlite3");
|
|
7
|
+
const sqlite = new Database(dbPath);
|
|
8
|
+
sqlite.pragma("journal_mode = WAL");
|
|
9
|
+
sqlite.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS cancia_content (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
site TEXT NOT NULL,
|
|
13
|
+
key TEXT NOT NULL,
|
|
14
|
+
lang TEXT NOT NULL,
|
|
15
|
+
value TEXT NOT NULL,
|
|
16
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
17
|
+
UNIQUE(site, key, lang)
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
return {
|
|
21
|
+
get: sqlite.prepare(
|
|
22
|
+
"SELECT value FROM cancia_content WHERE site=? AND key=? AND lang=?"
|
|
23
|
+
),
|
|
24
|
+
set: sqlite.prepare(
|
|
25
|
+
`INSERT INTO cancia_content (site, key, lang, value, updated_at)
|
|
26
|
+
VALUES (?, ?, ?, ?, unixepoch())
|
|
27
|
+
ON CONFLICT(site, key, lang) DO UPDATE SET value=excluded.value, updated_at=unixepoch()`
|
|
28
|
+
),
|
|
29
|
+
getAll: sqlite.prepare(
|
|
30
|
+
"SELECT key, lang, value FROM cancia_content WHERE site=?"
|
|
31
|
+
),
|
|
32
|
+
delete: sqlite.prepare(
|
|
33
|
+
"DELETE FROM cancia_content WHERE site=? AND key=? AND lang=?"
|
|
34
|
+
)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function getDB(dbPath) {
|
|
38
|
+
if (!_db) _db = createDB(dbPath);
|
|
39
|
+
return _db;
|
|
40
|
+
}
|
|
41
|
+
function createSQLiteAdapter(dbPath) {
|
|
42
|
+
const path = dbPath ?? process.cwd() + "/cancia.db";
|
|
43
|
+
return {
|
|
44
|
+
async get(site, key, lang) {
|
|
45
|
+
const row = getDB(path).get.get(site, key, lang);
|
|
46
|
+
return row?.value ?? null;
|
|
47
|
+
},
|
|
48
|
+
async set(site, key, lang, value) {
|
|
49
|
+
getDB(path).set.run(site, key, lang, value);
|
|
50
|
+
},
|
|
51
|
+
async getAll(site) {
|
|
52
|
+
const rows = getDB(path).getAll.all(site);
|
|
53
|
+
return Object.fromEntries(rows.map((r) => [`${r.key}.${r.lang}`, r.value]));
|
|
54
|
+
},
|
|
55
|
+
async delete(site, key, lang) {
|
|
56
|
+
getDB(path).delete.run(site, key, lang);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/storage/github-client.ts
|
|
62
|
+
function toBase64(text) {
|
|
63
|
+
if (typeof Buffer !== "undefined") {
|
|
64
|
+
return Buffer.from(text, "utf-8").toString("base64");
|
|
65
|
+
}
|
|
66
|
+
const bytes = new TextEncoder().encode(text);
|
|
67
|
+
let binary = "";
|
|
68
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
69
|
+
return btoa(binary);
|
|
70
|
+
}
|
|
71
|
+
function createGitHubClient(opts) {
|
|
72
|
+
const { repo, branch, token, committer } = opts;
|
|
73
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
74
|
+
const apiBase = (opts.apiBase ?? "https://api.github.com").replace(/\/$/, "");
|
|
75
|
+
if (!doFetch) {
|
|
76
|
+
throw new Error("createGitHubClient: no fetch available (pass opts.fetch)");
|
|
77
|
+
}
|
|
78
|
+
const headers = () => ({
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
Accept: "application/vnd.github+json",
|
|
81
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
82
|
+
});
|
|
83
|
+
const contentsUrl = (path) => {
|
|
84
|
+
const encoded = path.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
85
|
+
return `${apiBase}/repos/${repo}/contents/${encoded}`;
|
|
86
|
+
};
|
|
87
|
+
async function getFileSha(path) {
|
|
88
|
+
const url = `${contentsUrl(path)}?ref=${encodeURIComponent(branch)}`;
|
|
89
|
+
const res = await doFetch(url, { method: "GET", headers: headers() });
|
|
90
|
+
if (res.status === 404) return null;
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const detail = await res.text().catch(() => "");
|
|
93
|
+
throw new Error(`GitHub getFileSha ${path} failed: ${res.status} ${detail}`);
|
|
94
|
+
}
|
|
95
|
+
const body = await res.json();
|
|
96
|
+
return body.sha ?? null;
|
|
97
|
+
}
|
|
98
|
+
async function putFile(file, message) {
|
|
99
|
+
const sha = await getFileSha(file.path);
|
|
100
|
+
const payload = {
|
|
101
|
+
message,
|
|
102
|
+
content: toBase64(file.content),
|
|
103
|
+
branch
|
|
104
|
+
};
|
|
105
|
+
if (sha) payload.sha = sha;
|
|
106
|
+
if (committer) payload.committer = committer;
|
|
107
|
+
const res = await doFetch(contentsUrl(file.path), {
|
|
108
|
+
method: "PUT",
|
|
109
|
+
headers: headers(),
|
|
110
|
+
body: JSON.stringify(payload)
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const detail = await res.text().catch(() => "");
|
|
114
|
+
throw new Error(`GitHub commit ${file.path} failed: ${res.status} ${detail}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function commitFiles(files, message) {
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
await putFile(file, message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { getFileSha, commitFiles };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/storage/git-backed.ts
|
|
126
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
127
|
+
import { join, relative } from "path";
|
|
128
|
+
function createGitBackedAdapter(opts) {
|
|
129
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
130
|
+
const branch = opts.branch ?? "main";
|
|
131
|
+
const debounceMs = opts.debounceMs ?? 3e3;
|
|
132
|
+
const commitMessage = opts.commitMessage ?? "Cancia: content update";
|
|
133
|
+
const warn = opts.warn ?? ((m) => console.warn(m));
|
|
134
|
+
const onError = opts.onError ?? ((m, e) => console.error(m, e));
|
|
135
|
+
const kvPath = opts.contentPaths?.kvPath ?? join(projectRoot, "cancia-content.json");
|
|
136
|
+
const pagesPath = opts.contentPaths?.pagesPath ?? join(projectRoot, ".cancia", "pages.json");
|
|
137
|
+
const listsDir = opts.contentPaths?.listsDir ?? join(projectRoot, ".cancia", "lists");
|
|
138
|
+
const token = opts.token ?? process.env.CANCIA_GITHUB_TOKEN ?? "";
|
|
139
|
+
let client = null;
|
|
140
|
+
if (opts.client) {
|
|
141
|
+
client = opts.client;
|
|
142
|
+
} else if (token) {
|
|
143
|
+
client = createGitHubClient({
|
|
144
|
+
repo: opts.repo,
|
|
145
|
+
branch,
|
|
146
|
+
token,
|
|
147
|
+
committer: opts.committer,
|
|
148
|
+
fetch: opts.fetch,
|
|
149
|
+
apiBase: opts.apiBase
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const gitEnabled = client !== null;
|
|
153
|
+
if (!gitEnabled) {
|
|
154
|
+
warn(
|
|
155
|
+
"[cancia] Git-backed storage: no GitHub token (CANCIA_GITHUB_TOKEN) \u2014 running local-only. Edits save to disk but are NOT committed/pushed."
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
159
|
+
let timer = null;
|
|
160
|
+
let flushing = null;
|
|
161
|
+
let rerunRequested = false;
|
|
162
|
+
function toRepoPath(absPath) {
|
|
163
|
+
return relative(projectRoot, absPath).split("\\").join("/");
|
|
164
|
+
}
|
|
165
|
+
function markDirty(absPath) {
|
|
166
|
+
dirty.add(absPath);
|
|
167
|
+
}
|
|
168
|
+
function markListDirty(listName, site) {
|
|
169
|
+
const siteDir = join(listsDir, listName, site);
|
|
170
|
+
if (!existsSync(siteDir)) return;
|
|
171
|
+
const walk = (dir) => {
|
|
172
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
173
|
+
const full = join(dir, entry.name);
|
|
174
|
+
if (entry.isDirectory()) walk(full);
|
|
175
|
+
else if (entry.isFile()) markDirty(full);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
walk(siteDir);
|
|
179
|
+
}
|
|
180
|
+
function scheduleFlush() {
|
|
181
|
+
if (!gitEnabled) return;
|
|
182
|
+
if (timer) clearTimeout(timer);
|
|
183
|
+
timer = setTimeout(() => {
|
|
184
|
+
timer = null;
|
|
185
|
+
void runFlush();
|
|
186
|
+
}, debounceMs);
|
|
187
|
+
}
|
|
188
|
+
async function runFlush() {
|
|
189
|
+
if (flushing) {
|
|
190
|
+
rerunRequested = true;
|
|
191
|
+
return flushing;
|
|
192
|
+
}
|
|
193
|
+
flushing = doFlush().finally(() => {
|
|
194
|
+
flushing = null;
|
|
195
|
+
if (rerunRequested) {
|
|
196
|
+
rerunRequested = false;
|
|
197
|
+
void runFlush();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return flushing;
|
|
201
|
+
}
|
|
202
|
+
async function doFlush() {
|
|
203
|
+
if (!client || dirty.size === 0) return;
|
|
204
|
+
const batch = [...dirty];
|
|
205
|
+
const files = [];
|
|
206
|
+
for (const abs of batch) {
|
|
207
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) continue;
|
|
208
|
+
files.push({ path: toRepoPath(abs), content: readFileSync(abs, "utf-8") });
|
|
209
|
+
}
|
|
210
|
+
if (files.length === 0) {
|
|
211
|
+
for (const abs of batch) dirty.delete(abs);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await client.commitFiles(files, commitMessage);
|
|
216
|
+
for (const abs of batch) dirty.delete(abs);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
onError(
|
|
219
|
+
"[cancia] Git-backed storage: commit failed \u2014 data saved locally, will retry on next flush.",
|
|
220
|
+
err
|
|
221
|
+
);
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function flush() {
|
|
226
|
+
if (!gitEnabled) return;
|
|
227
|
+
if (timer) {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
timer = null;
|
|
230
|
+
}
|
|
231
|
+
await runFlush();
|
|
232
|
+
}
|
|
233
|
+
const kv = {
|
|
234
|
+
get: (site, key, lang) => opts.local.kv.get(site, key, lang),
|
|
235
|
+
getAll: (site) => opts.local.kv.getAll(site),
|
|
236
|
+
async set(site, key, lang, value) {
|
|
237
|
+
await opts.local.kv.set(site, key, lang, value);
|
|
238
|
+
markDirty(kvPath);
|
|
239
|
+
scheduleFlush();
|
|
240
|
+
},
|
|
241
|
+
async delete(site, key, lang) {
|
|
242
|
+
await opts.local.kv.delete(site, key, lang);
|
|
243
|
+
markDirty(kvPath);
|
|
244
|
+
scheduleFlush();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const pages = {
|
|
248
|
+
get: (site, route) => opts.local.pages.get(site, route),
|
|
249
|
+
list: (site) => opts.local.pages.list(site),
|
|
250
|
+
async set(site, route, meta, rev) {
|
|
251
|
+
const result = await opts.local.pages.set(site, route, meta, rev);
|
|
252
|
+
markDirty(pagesPath);
|
|
253
|
+
scheduleFlush();
|
|
254
|
+
return result;
|
|
255
|
+
},
|
|
256
|
+
async delete(site, route) {
|
|
257
|
+
await opts.local.pages.delete(site, route);
|
|
258
|
+
markDirty(pagesPath);
|
|
259
|
+
scheduleFlush();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const lists = {
|
|
263
|
+
list: (site, listName, locale) => opts.local.lists.list(site, listName, locale),
|
|
264
|
+
get: (site, listName, id, locale) => opts.local.lists.get(site, listName, id, locale),
|
|
265
|
+
translations: (site, listName) => opts.local.lists.translations(site, listName),
|
|
266
|
+
async create(site, listName, data, locale, id) {
|
|
267
|
+
const entry = await opts.local.lists.create(site, listName, data, locale, id);
|
|
268
|
+
markListDirty(listName, site);
|
|
269
|
+
scheduleFlush();
|
|
270
|
+
return entry;
|
|
271
|
+
},
|
|
272
|
+
async update(site, listName, id, locale, data, rev) {
|
|
273
|
+
const entry = await opts.local.lists.update(site, listName, id, locale, data, rev);
|
|
274
|
+
markListDirty(listName, site);
|
|
275
|
+
scheduleFlush();
|
|
276
|
+
return entry;
|
|
277
|
+
},
|
|
278
|
+
async delete(site, listName, id, locale) {
|
|
279
|
+
await opts.local.lists.delete(site, listName, id, locale);
|
|
280
|
+
markListDirty(listName, site);
|
|
281
|
+
scheduleFlush();
|
|
282
|
+
},
|
|
283
|
+
async reorder(site, listName, ids) {
|
|
284
|
+
await opts.local.lists.reorder(site, listName, ids);
|
|
285
|
+
markListDirty(listName, site);
|
|
286
|
+
scheduleFlush();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const git = {
|
|
290
|
+
flush,
|
|
291
|
+
get gitEnabled() {
|
|
292
|
+
return gitEnabled;
|
|
293
|
+
},
|
|
294
|
+
pendingPaths() {
|
|
295
|
+
return [...dirty].map(toRepoPath);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
return { kv, pages, lists, git };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
createSQLiteAdapter,
|
|
303
|
+
createGitHubClient,
|
|
304
|
+
createGitBackedAdapter
|
|
305
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
makeSchemasRoute
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-IIGDU5SV.js";
|
|
4
4
|
import "../chunk-NG5GJME5.js";
|
|
5
|
-
import "../chunk-
|
|
5
|
+
import "../chunk-MCHQV6Y7.js";
|
|
6
|
+
import "../chunk-BOIQNZAO.js";
|
|
6
7
|
|
|
7
8
|
// src/endpoints/schemas.ts
|
|
8
9
|
import { getCanciaRuntime } from "virtual:cancia/runtime";
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ import { U as UploadHandler } from './upload-DwCGjXbz.js';
|
|
|
5
5
|
export { m as makeLocalUploadHandler } from './upload-DwCGjXbz.js';
|
|
6
6
|
export { CanciaLoaderOptions, canciaLoader } from './loader/index.js';
|
|
7
7
|
export { FieldDescription, FieldMeta, FieldMetaBase, FieldWidget, ListDescription, ListSchema, SchemasModule, defineField, defineList, describeList } from './schema/index.js';
|
|
8
|
-
export { createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter } from './storage/index.js';
|
|
8
|
+
export { GitBackedContentPaths, GitBackedControls, GitBackedOptions, GitBackedStorage, createGitBackedAdapter, createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter } from './storage/index.js';
|
|
9
9
|
export { z } from 'zod';
|
|
10
10
|
import 'astro/loaders';
|
|
11
|
+
import './portable-text-BikSqS9T.js';
|
|
11
12
|
|
|
12
13
|
interface R2UploadHandlerOptions {
|
|
13
14
|
/** Cloudflare account ID (from R2 dashboard) */
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-22DJVJBR.js";
|
|
4
4
|
import {
|
|
5
5
|
makeSchemasRoute
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-IIGDU5SV.js";
|
|
7
7
|
import "./chunk-NG5GJME5.js";
|
|
8
8
|
import {
|
|
9
9
|
setCanciaRuntime
|
|
@@ -13,13 +13,14 @@ import {
|
|
|
13
13
|
defineList,
|
|
14
14
|
describeList,
|
|
15
15
|
z
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-MCHQV6Y7.js";
|
|
17
17
|
import {
|
|
18
18
|
canciaLoader
|
|
19
19
|
} from "./chunk-337LJIKX.js";
|
|
20
20
|
import {
|
|
21
|
+
createGitBackedAdapter,
|
|
21
22
|
createSQLiteAdapter
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-SXKZ2WUL.js";
|
|
23
24
|
import {
|
|
24
25
|
createJsonFileAdapter,
|
|
25
26
|
createJsonFileAdapterV2
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
import {
|
|
28
29
|
RevConflictError
|
|
29
30
|
} from "./chunk-7IA5B5CF.js";
|
|
31
|
+
import "./chunk-BOIQNZAO.js";
|
|
30
32
|
import {
|
|
31
33
|
detectImageType,
|
|
32
34
|
isValidSite
|
|
@@ -644,6 +646,7 @@ export {
|
|
|
644
646
|
RevConflictError,
|
|
645
647
|
canciaIntegration,
|
|
646
648
|
canciaLoader,
|
|
649
|
+
createGitBackedAdapter,
|
|
647
650
|
createJsonFileAdapter,
|
|
648
651
|
createJsonFileAdapterV2,
|
|
649
652
|
createSQLiteAdapter,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** The four block styles the subset permits. */
|
|
4
|
+
declare const PT_STYLES: readonly ["normal", "h2", "h3", "blockquote"];
|
|
5
|
+
type PtStyle = (typeof PT_STYLES)[number];
|
|
6
|
+
/** The two list kinds the subset permits (absent = not a list item). */
|
|
7
|
+
declare const PT_LIST_ITEMS: readonly ["bullet", "number"];
|
|
8
|
+
type PtListItem = (typeof PT_LIST_ITEMS)[number];
|
|
9
|
+
/** The two decorators (marks that are not annotation keys) the subset permits. */
|
|
10
|
+
declare const PT_DECORATORS: readonly ["strong", "em"];
|
|
11
|
+
type PtDecorator = (typeof PT_DECORATORS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* Whether a link href uses a SAFE scheme. This is the XSS gate for the only
|
|
14
|
+
* annotation the subset carries: astro-portabletext renders `href` straight
|
|
15
|
+
* into an <a href> without scheme filtering, so a link authored as
|
|
16
|
+
* `javascript:alert(1)` or `data:text/html,…` would be a live injection vector
|
|
17
|
+
* — exactly what the "no raw HTML" design (D4) exists to prevent. Only allow:
|
|
18
|
+
* - http(s), mailto:, tel: absolute URLs
|
|
19
|
+
* - root-relative (`/…`), fragment (`#…`), and relative (`./…`, `../…`, or a
|
|
20
|
+
* bare path) links that carry NO scheme at all.
|
|
21
|
+
* Everything with any other scheme (javascript:, data:, vbscript:, file:, …)
|
|
22
|
+
* is rejected. Exported so the parser and tests share one definition.
|
|
23
|
+
*/
|
|
24
|
+
declare function isSafeHref(href: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* A link annotation. Stored in a block's `markDefs`; a span references it by
|
|
27
|
+
* putting the markDef's `_key` in its `marks` array. `href` is the only
|
|
28
|
+
* carried property (no title/target — kept minimal and sanitizable), and it is
|
|
29
|
+
* constrained to safe schemes (see isSafeHref) so a stored value can never
|
|
30
|
+
* carry a javascript:/data: XSS payload.
|
|
31
|
+
*/
|
|
32
|
+
declare const ptLinkMarkDefSchema: z.ZodObject<{
|
|
33
|
+
_type: z.ZodLiteral<"link">;
|
|
34
|
+
_key: z.ZodString;
|
|
35
|
+
href: z.ZodString;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
type PtLinkMarkDef = z.infer<typeof ptLinkMarkDefSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* A text span. `marks` entries are EITHER a decorator ("strong"/"em") OR a
|
|
40
|
+
* markDef `_key` present in the parent block's `markDefs`. We validate the
|
|
41
|
+
* decorator-or-string shape here; the cross-reference (every non-decorator mark
|
|
42
|
+
* resolves to a markDef) is enforced by the refine on the block below.
|
|
43
|
+
*/
|
|
44
|
+
declare const ptSpanSchema: z.ZodObject<{
|
|
45
|
+
_type: z.ZodLiteral<"span">;
|
|
46
|
+
_key: z.ZodString;
|
|
47
|
+
text: z.ZodString;
|
|
48
|
+
marks: z.ZodArray<z.ZodString>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
type PtSpan = z.infer<typeof ptSpanSchema>;
|
|
51
|
+
/**
|
|
52
|
+
* A single block. `listItem`/`level` are present only for list rows. Every
|
|
53
|
+
* span mark that is not a decorator must reference a link markDef by key —
|
|
54
|
+
* that guard is what keeps arbitrary annotations out of the stored value.
|
|
55
|
+
*/
|
|
56
|
+
declare const ptBlockSchema: z.ZodObject<{
|
|
57
|
+
_type: z.ZodLiteral<"block">;
|
|
58
|
+
_key: z.ZodString;
|
|
59
|
+
style: z.ZodEnum<{
|
|
60
|
+
normal: "normal";
|
|
61
|
+
h2: "h2";
|
|
62
|
+
h3: "h3";
|
|
63
|
+
blockquote: "blockquote";
|
|
64
|
+
}>;
|
|
65
|
+
listItem: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
number: "number";
|
|
67
|
+
bullet: "bullet";
|
|
68
|
+
}>>;
|
|
69
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
markDefs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
71
|
+
_type: z.ZodLiteral<"link">;
|
|
72
|
+
_key: z.ZodString;
|
|
73
|
+
href: z.ZodString;
|
|
74
|
+
}, z.core.$strip>>>;
|
|
75
|
+
children: z.ZodArray<z.ZodObject<{
|
|
76
|
+
_type: z.ZodLiteral<"span">;
|
|
77
|
+
_key: z.ZodString;
|
|
78
|
+
text: z.ZodString;
|
|
79
|
+
marks: z.ZodArray<z.ZodString>;
|
|
80
|
+
}, z.core.$strip>>;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
type PtBlock = z.infer<typeof ptBlockSchema>;
|
|
83
|
+
/**
|
|
84
|
+
* The stored rich-text value: an array of subset blocks. Attach
|
|
85
|
+
* `.meta({ widget: "richtext" })` in defineField so describeList can identify
|
|
86
|
+
* it (its JSON-Schema type is "array", so the explicit widget is the only
|
|
87
|
+
* reliable signal — see the guard in describeProperty).
|
|
88
|
+
*/
|
|
89
|
+
declare const portableTextSubsetSchema: z.ZodArray<z.ZodObject<{
|
|
90
|
+
_type: z.ZodLiteral<"block">;
|
|
91
|
+
_key: z.ZodString;
|
|
92
|
+
style: z.ZodEnum<{
|
|
93
|
+
normal: "normal";
|
|
94
|
+
h2: "h2";
|
|
95
|
+
h3: "h3";
|
|
96
|
+
blockquote: "blockquote";
|
|
97
|
+
}>;
|
|
98
|
+
listItem: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
number: "number";
|
|
100
|
+
bullet: "bullet";
|
|
101
|
+
}>>;
|
|
102
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
103
|
+
markDefs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
104
|
+
_type: z.ZodLiteral<"link">;
|
|
105
|
+
_key: z.ZodString;
|
|
106
|
+
href: z.ZodString;
|
|
107
|
+
}, z.core.$strip>>>;
|
|
108
|
+
children: z.ZodArray<z.ZodObject<{
|
|
109
|
+
_type: z.ZodLiteral<"span">;
|
|
110
|
+
_key: z.ZodString;
|
|
111
|
+
text: z.ZodString;
|
|
112
|
+
marks: z.ZodArray<z.ZodString>;
|
|
113
|
+
}, z.core.$strip>>;
|
|
114
|
+
}, z.core.$strip>>;
|
|
115
|
+
type PortableTextValue = z.infer<typeof portableTextSubsetSchema>;
|
|
116
|
+
|
|
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, ptBlockSchema as k, portableTextSubsetSchema as p };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// <CanciaRichText> — render a Cancia rich-text (Portable-Text SUBSET) value
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Renders the stored PT-subset array (see schema/portable-text.ts) to real
|
|
6
|
+
// HTML using astro-portabletext. The subset — normal/h2/h3/blockquote blocks,
|
|
7
|
+
// bullet/number lists, strong/em marks, link annotations — is a strict subset
|
|
8
|
+
// of Portable Text, so astro-portabletext's DEFAULT components already cover
|
|
9
|
+
// every case; we only override `link` to add rel/target hardening for external
|
|
10
|
+
// hrefs. There is no raw HTML in the value (D4), so nothing is dangerouslySet.
|
|
11
|
+
//
|
|
12
|
+
// Usage in a page/template:
|
|
13
|
+
// ---
|
|
14
|
+
// import CanciaRichText from "@cancia/astro/richtext/CanciaRichText.astro";
|
|
15
|
+
// const { body } = entry.data;
|
|
16
|
+
// ---
|
|
17
|
+
// <CanciaRichText value={body} />
|
|
18
|
+
//
|
|
19
|
+
// This file ships as a raw .astro under dist/richtext/ (an .astro file can't be
|
|
20
|
+
// a tsup entry) and is Vite-free at the package boundary — importing it does
|
|
21
|
+
// NOT pull in the Cancia integration.
|
|
22
|
+
// =============================================================================
|
|
23
|
+
import { PortableText } from "astro-portabletext";
|
|
24
|
+
import Link from "./Link.astro";
|
|
25
|
+
|
|
26
|
+
export interface Props {
|
|
27
|
+
/** The stored PT-subset value. Undefined / empty renders nothing. */
|
|
28
|
+
value?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { value } = Astro.props;
|
|
32
|
+
|
|
33
|
+
// Normalise: only render for a non-empty array of blocks. undefined, null, or
|
|
34
|
+
// [] all render nothing (empty-value handling — [] vs undefined both no-op).
|
|
35
|
+
const blocks = Array.isArray(value) ? value : [];
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
blocks.length > 0 && (
|
|
40
|
+
<div class="cancia-richtext">
|
|
41
|
+
<PortableText value={blocks} components={{ mark: { link: Link } }} />
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Link mark for <CanciaRichText>
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// astro-portabletext hands a mark component the resolved markDef on `node`. For
|
|
6
|
+
// the subset's only annotation (link) that markDef is { _type:"link", href }.
|
|
7
|
+
// We render an <a> and harden EXTERNAL hrefs (anything with a scheme) with
|
|
8
|
+
// rel="noopener noreferrer" + target="_blank"; in-page/relative links stay
|
|
9
|
+
// plain. The href is a plain string from the validated value — never HTML.
|
|
10
|
+
// =============================================================================
|
|
11
|
+
const { node } = Astro.props as {
|
|
12
|
+
node: { markDef?: { href?: string } };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const href = node?.markDef?.href ?? "#";
|
|
16
|
+
const isExternal = /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("//");
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
isExternal ? (
|
|
21
|
+
<a href={href} rel="noopener noreferrer" target="_blank"><slot /></a>
|
|
22
|
+
) : (
|
|
23
|
+
<a href={href}><slot /></a>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { i as PtStyle, g as PtListItem, d as PtBlock } from '../portable-text-BikSqS9T.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 portableTextSubsetSchema, k as ptBlockSchema } from '../portable-text-BikSqS9T.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/** A row as the editor holds it: raw shorthand text + style + optional list kind. */
|
|
6
|
+
interface RichTextRow {
|
|
7
|
+
text: string;
|
|
8
|
+
style: PtStyle;
|
|
9
|
+
listItem?: PtListItem;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Turn one editor row into a PT-subset block. Empty text still yields a block
|
|
13
|
+
* with a single empty span (PT convention) so the row survives a round-trip.
|
|
14
|
+
*/
|
|
15
|
+
declare function rowToBlock(row: RichTextRow): PtBlock;
|
|
16
|
+
/** Serialise a list of editor rows into a PT-subset value (block array). */
|
|
17
|
+
declare function rowsToPortableText(rows: RichTextRow[]): PtBlock[];
|
|
18
|
+
/** Turn a PT-subset block back into an editable row (text + style + list). */
|
|
19
|
+
declare function blockToRow(block: PtBlock): RichTextRow;
|
|
20
|
+
/** Deserialise a PT-subset value into editor rows. */
|
|
21
|
+
declare function portableTextToRows(value: PtBlock[]): RichTextRow[];
|
|
22
|
+
|
|
23
|
+
export { PtBlock, PtListItem, PtStyle, type RichTextRow, blockToRow, portableTextToRows, rowToBlock, rowsToPortableText };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PT_DECORATORS,
|
|
3
|
+
PT_LIST_ITEMS,
|
|
4
|
+
PT_STYLES,
|
|
5
|
+
isSafeHref,
|
|
6
|
+
portableTextSubsetSchema,
|
|
7
|
+
ptBlockSchema
|
|
8
|
+
} from "../chunk-BOIQNZAO.js";
|
|
9
|
+
|
|
10
|
+
// src/richtext/markdown.ts
|
|
11
|
+
function keyGen(prefix) {
|
|
12
|
+
let n = 0;
|
|
13
|
+
return () => `${prefix}${(n++).toString(36)}`;
|
|
14
|
+
}
|
|
15
|
+
function parseInline(input) {
|
|
16
|
+
const segments = [];
|
|
17
|
+
let plain = "";
|
|
18
|
+
let i = 0;
|
|
19
|
+
const flushPlain = () => {
|
|
20
|
+
if (plain) {
|
|
21
|
+
segments.push({ text: plain, marks: [] });
|
|
22
|
+
plain = "";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
while (i < input.length) {
|
|
26
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
27
|
+
plain += input[i + 1];
|
|
28
|
+
i += 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (input[i] === "[") {
|
|
32
|
+
const close = input.indexOf("]", i + 1);
|
|
33
|
+
if (close !== -1 && input[close + 1] === "(") {
|
|
34
|
+
const paren = input.indexOf(")", close + 2);
|
|
35
|
+
if (paren !== -1) {
|
|
36
|
+
const text = input.slice(i + 1, close);
|
|
37
|
+
const href = input.slice(close + 2, paren);
|
|
38
|
+
flushPlain();
|
|
39
|
+
segments.push({ text, marks: [], href });
|
|
40
|
+
i = paren + 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (input[i] === "*" && input[i + 1] === "*") {
|
|
46
|
+
const end = input.indexOf("**", i + 2);
|
|
47
|
+
if (end !== -1 && end > i + 2) {
|
|
48
|
+
flushPlain();
|
|
49
|
+
segments.push({ text: input.slice(i + 2, end), marks: ["strong"] });
|
|
50
|
+
i = end + 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (input[i] === "*") {
|
|
55
|
+
const end = input.indexOf("*", i + 1);
|
|
56
|
+
if (end !== -1 && end > i + 1) {
|
|
57
|
+
flushPlain();
|
|
58
|
+
segments.push({ text: input.slice(i + 1, end), marks: ["em"] });
|
|
59
|
+
i = end + 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
plain += input[i];
|
|
64
|
+
i += 1;
|
|
65
|
+
}
|
|
66
|
+
flushPlain();
|
|
67
|
+
return segments;
|
|
68
|
+
}
|
|
69
|
+
function rowToBlock(row) {
|
|
70
|
+
const spanKey = keyGen("s");
|
|
71
|
+
const linkKey = keyGen("l");
|
|
72
|
+
const segments = parseInline(row.text);
|
|
73
|
+
const markDefs = [];
|
|
74
|
+
const children = [];
|
|
75
|
+
for (const seg of segments) {
|
|
76
|
+
const marks = [...seg.marks];
|
|
77
|
+
if (seg.href !== void 0 && isSafeHref(seg.href)) {
|
|
78
|
+
const _key = linkKey();
|
|
79
|
+
markDefs.push({ _type: "link", _key, href: seg.href });
|
|
80
|
+
marks.push(_key);
|
|
81
|
+
}
|
|
82
|
+
children.push({ _type: "span", _key: spanKey(), text: seg.text, marks });
|
|
83
|
+
}
|
|
84
|
+
if (children.length === 0) {
|
|
85
|
+
children.push({ _type: "span", _key: spanKey(), text: "", marks: [] });
|
|
86
|
+
}
|
|
87
|
+
const style = PT_STYLES.includes(row.style) ? row.style : "normal";
|
|
88
|
+
const block = {
|
|
89
|
+
_type: "block",
|
|
90
|
+
_key: keyGen("b")(),
|
|
91
|
+
style,
|
|
92
|
+
markDefs,
|
|
93
|
+
children
|
|
94
|
+
};
|
|
95
|
+
if (row.listItem && PT_LIST_ITEMS.includes(row.listItem)) {
|
|
96
|
+
block.listItem = row.listItem;
|
|
97
|
+
block.level = 1;
|
|
98
|
+
}
|
|
99
|
+
return block;
|
|
100
|
+
}
|
|
101
|
+
function rowsToPortableText(rows) {
|
|
102
|
+
return rows.map(rowToBlock);
|
|
103
|
+
}
|
|
104
|
+
function escapeShorthand(text) {
|
|
105
|
+
return text.replace(/([\\*\[\]])/g, "\\$1");
|
|
106
|
+
}
|
|
107
|
+
function spanToShorthand(span, linkHrefByKey) {
|
|
108
|
+
let out = escapeShorthand(span.text);
|
|
109
|
+
const href = span.marks.map((m) => linkHrefByKey.get(m)).find((h) => h !== void 0);
|
|
110
|
+
if (href !== void 0) out = `[${out}](${href})`;
|
|
111
|
+
if (span.marks.includes("strong")) out = `**${out}**`;
|
|
112
|
+
if (span.marks.includes("em")) out = `*${out}*`;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function blockToRow(block) {
|
|
116
|
+
const linkHrefByKey = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const def of block.markDefs ?? []) {
|
|
118
|
+
if (def._type === "link") linkHrefByKey.set(def._key, def.href);
|
|
119
|
+
}
|
|
120
|
+
const text = (block.children ?? []).map((span) => spanToShorthand(span, linkHrefByKey)).join("");
|
|
121
|
+
const row = {
|
|
122
|
+
text,
|
|
123
|
+
style: PT_STYLES.includes(block.style) ? block.style : "normal"
|
|
124
|
+
};
|
|
125
|
+
if (block.listItem && PT_LIST_ITEMS.includes(block.listItem)) row.listItem = block.listItem;
|
|
126
|
+
return row;
|
|
127
|
+
}
|
|
128
|
+
function portableTextToRows(value) {
|
|
129
|
+
return value.map(blockToRow);
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
PT_DECORATORS,
|
|
133
|
+
PT_LIST_ITEMS,
|
|
134
|
+
PT_STYLES,
|
|
135
|
+
blockToRow,
|
|
136
|
+
isSafeHref,
|
|
137
|
+
portableTextSubsetSchema,
|
|
138
|
+
portableTextToRows,
|
|
139
|
+
ptBlockSchema,
|
|
140
|
+
rowToBlock,
|
|
141
|
+
rowsToPortableText
|
|
142
|
+
};
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,5 +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 portableTextSubsetSchema, k as ptBlockSchema } from '../portable-text-BikSqS9T.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Widget hint the modal form uses to pick an input element. The Zod type
|
|
@@ -15,7 +16,7 @@ export { z } from 'zod';
|
|
|
15
16
|
* z.boolean() → "checkbox"
|
|
16
17
|
* z.enum([...]) → "select"
|
|
17
18
|
*/
|
|
18
|
-
type FieldWidget = "text" | "textarea" | "url" | "email" | "datetime" | "number" | "checkbox" | "select" | "image" | "slug";
|
|
19
|
+
type FieldWidget = "text" | "textarea" | "url" | "email" | "datetime" | "number" | "checkbox" | "select" | "image" | "slug" | "array" | "object" | "reference" | "richtext";
|
|
19
20
|
/**
|
|
20
21
|
* Fields common to every widget's metadata. All optional.
|
|
21
22
|
*/
|
|
@@ -51,6 +52,15 @@ type FieldMeta = (FieldMetaBase & {
|
|
|
51
52
|
}) | (FieldMetaBase & {
|
|
52
53
|
widget: "slug";
|
|
53
54
|
source?: string;
|
|
55
|
+
}) | (FieldMetaBase & {
|
|
56
|
+
widget: "array";
|
|
57
|
+
}) | (FieldMetaBase & {
|
|
58
|
+
widget: "object";
|
|
59
|
+
}) | (FieldMetaBase & {
|
|
60
|
+
widget: "reference";
|
|
61
|
+
list: string;
|
|
62
|
+
}) | (FieldMetaBase & {
|
|
63
|
+
widget: "richtext";
|
|
54
64
|
});
|
|
55
65
|
interface ListSchemaOptions<TFields extends Record<string, z.ZodTypeAny>> {
|
|
56
66
|
/** Display label for the list (e.g. "Blog Posts"). */
|
|
@@ -106,6 +116,15 @@ interface FieldDescription {
|
|
|
106
116
|
pattern?: string;
|
|
107
117
|
/** For slug widgets: the field name to auto-generate the slug from. */
|
|
108
118
|
source?: string;
|
|
119
|
+
/**
|
|
120
|
+
* For array widgets: the description of a single item. The item has no name
|
|
121
|
+
* of its own (arrays are keyed by position), so `of.name` is "".
|
|
122
|
+
*/
|
|
123
|
+
of?: FieldDescription;
|
|
124
|
+
/** For object widgets: the sub-field descriptions, in declared order. */
|
|
125
|
+
fields?: FieldDescription[];
|
|
126
|
+
/** For reference widgets: the name of the list whose entries this points at. */
|
|
127
|
+
referenceList?: string;
|
|
109
128
|
}
|
|
110
129
|
/**
|
|
111
130
|
* Typed field constructors. Each returns the underlying Zod type with a
|
|
@@ -148,6 +167,71 @@ declare const defineField: {
|
|
|
148
167
|
}) => z.ZodEnum<{
|
|
149
168
|
[x: string]: string;
|
|
150
169
|
}>;
|
|
170
|
+
/**
|
|
171
|
+
* A repeatable list of a single member type. `member` is any Zod type,
|
|
172
|
+
* typically another `defineField.*` (so it keeps its own `.meta({ widget })`
|
|
173
|
+
* and recursion carries labels/widgets into each row).
|
|
174
|
+
*
|
|
175
|
+
* bullets: f.array(f.text({ label: "Point" }), { label: "Key points" }),
|
|
176
|
+
* faqs: f.array(f.object({ q: f.text(), a: f.textarea() })),
|
|
177
|
+
*/
|
|
178
|
+
array: <TMember extends z.ZodTypeAny>(member: TMember, o?: FieldMetaBase) => z.ZodArray<TMember>;
|
|
179
|
+
/**
|
|
180
|
+
* A nested group of named sub-fields, rendered as a collapsible fieldset.
|
|
181
|
+
*
|
|
182
|
+
* socials: f.object({ linkedin: f.url(), twitter: f.url() }, { label: "Socials" }),
|
|
183
|
+
*/
|
|
184
|
+
object: <TShape extends Record<string, z.ZodTypeAny>>(shape: TShape, o?: FieldMetaBase) => z.ZodObject<{ -readonly [P in keyof TShape]: TShape[P]; }, z.core.$strip>;
|
|
185
|
+
/**
|
|
186
|
+
* A pointer to an entry in another list. Stored as a plain string — the
|
|
187
|
+
* target entry's id. Per the v0.1.0 design (D5) there is NO integrity
|
|
188
|
+
* backend: dangling ids are tolerated and surfaced by the editor, never
|
|
189
|
+
* prevented. `list` is the target list's name (a key in the schemas module).
|
|
190
|
+
*
|
|
191
|
+
* author: f.reference({ list: "people", label: "Author" }),
|
|
192
|
+
* related: f.array(f.reference({ list: "posts" }), { label: "Related" }),
|
|
193
|
+
*/
|
|
194
|
+
reference: (o: FieldMetaBase & {
|
|
195
|
+
list: string;
|
|
196
|
+
}) => z.ZodString;
|
|
197
|
+
/**
|
|
198
|
+
* A constrained rich-text body. Stored as a Portable-Text SUBSET array (D4):
|
|
199
|
+
* block styles normal/h2/h3/blockquote, bullet/number lists, strong/em marks,
|
|
200
|
+
* and link annotations — nothing else. The toolbar renders a structured block
|
|
201
|
+
* editor (approach B) over it, and `<CanciaRichText>` renders it to HTML.
|
|
202
|
+
*
|
|
203
|
+
* body: f.richtext({ label: "Body" }),
|
|
204
|
+
*
|
|
205
|
+
* The JSON-Schema type of this field is "array"; the explicit
|
|
206
|
+
* `widget: "richtext"` meta is what distinguishes it from a plain array in
|
|
207
|
+
* describeProperty.
|
|
208
|
+
*/
|
|
209
|
+
richtext: (o?: PlainFieldOptions) => z.ZodArray<z.ZodObject<{
|
|
210
|
+
_type: z.ZodLiteral<"block">;
|
|
211
|
+
_key: z.ZodString;
|
|
212
|
+
style: z.ZodEnum<{
|
|
213
|
+
normal: "normal";
|
|
214
|
+
h2: "h2";
|
|
215
|
+
h3: "h3";
|
|
216
|
+
blockquote: "blockquote";
|
|
217
|
+
}>;
|
|
218
|
+
listItem: z.ZodOptional<z.ZodEnum<{
|
|
219
|
+
number: "number";
|
|
220
|
+
bullet: "bullet";
|
|
221
|
+
}>>;
|
|
222
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
223
|
+
markDefs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
224
|
+
_type: z.ZodLiteral<"link">;
|
|
225
|
+
_key: z.ZodString;
|
|
226
|
+
href: z.ZodString;
|
|
227
|
+
}, z.core.$strip>>>;
|
|
228
|
+
children: z.ZodArray<z.ZodObject<{
|
|
229
|
+
_type: z.ZodLiteral<"span">;
|
|
230
|
+
_key: z.ZodString;
|
|
231
|
+
text: z.ZodString;
|
|
232
|
+
marks: z.ZodArray<z.ZodString>;
|
|
233
|
+
}, z.core.$strip>>;
|
|
234
|
+
}, z.core.$strip>>;
|
|
151
235
|
};
|
|
152
236
|
interface ListDescription {
|
|
153
237
|
name: string;
|
|
@@ -158,8 +242,22 @@ interface ListDescription {
|
|
|
158
242
|
slugField?: string;
|
|
159
243
|
fields: FieldDescription[];
|
|
160
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Turn arbitrary text into a URL-safe slug: lowercase, spaces → hyphens,
|
|
247
|
+
* strip anything outside [a-z0-9-], collapse runs of hyphens, trim leading
|
|
248
|
+
* and trailing hyphens. Matches the slug field regex `^[a-z0-9-]+$`.
|
|
249
|
+
*
|
|
250
|
+
* Used by the toolbar to auto-derive a slug from a title as the editor types
|
|
251
|
+
* (plan 025). Kept here (not in the toolbar) so it is unit-tested alongside the
|
|
252
|
+
* schema helpers and stays the single definition of "what a slug looks like".
|
|
253
|
+
*
|
|
254
|
+
* slugify("Hello, World!") → "hello-world"
|
|
255
|
+
* slugify(" Multiple spaces ") → "multiple-spaces"
|
|
256
|
+
* slugify("Café déjà vu") → "caf-dj-vu" (non-ASCII stripped)
|
|
257
|
+
*/
|
|
258
|
+
declare function slugify(input: string): string;
|
|
161
259
|
declare function describeList(name: string, schema: ListSchema): ListDescription;
|
|
162
260
|
/** Map of list name → schema. What users export from src/cms/schemas.ts. */
|
|
163
261
|
type SchemasModule = Record<string, ListSchema>;
|
|
164
262
|
|
|
165
|
-
export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList };
|
|
263
|
+
export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList, slugify };
|
package/dist/schema/index.js
CHANGED
|
@@ -2,11 +2,27 @@ import {
|
|
|
2
2
|
defineField,
|
|
3
3
|
defineList,
|
|
4
4
|
describeList,
|
|
5
|
+
slugify,
|
|
5
6
|
z
|
|
6
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-MCHQV6Y7.js";
|
|
8
|
+
import {
|
|
9
|
+
PT_DECORATORS,
|
|
10
|
+
PT_LIST_ITEMS,
|
|
11
|
+
PT_STYLES,
|
|
12
|
+
isSafeHref,
|
|
13
|
+
portableTextSubsetSchema,
|
|
14
|
+
ptBlockSchema
|
|
15
|
+
} from "../chunk-BOIQNZAO.js";
|
|
7
16
|
export {
|
|
17
|
+
PT_DECORATORS,
|
|
18
|
+
PT_LIST_ITEMS,
|
|
19
|
+
PT_STYLES,
|
|
8
20
|
defineField,
|
|
9
21
|
defineList,
|
|
10
22
|
describeList,
|
|
23
|
+
isSafeHref,
|
|
24
|
+
portableTextSubsetSchema,
|
|
25
|
+
ptBlockSchema,
|
|
26
|
+
slugify,
|
|
11
27
|
z
|
|
12
28
|
};
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -17,4 +17,113 @@ declare function createJsonFileAdapterV2(opts?: JsonFileV2Options): CanciaStorag
|
|
|
17
17
|
|
|
18
18
|
declare function createSQLiteAdapter(dbPath?: string): CanciaStorage;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/** Minimal fetch signature — matches the global `fetch` we depend on. */
|
|
21
|
+
type FetchLike = (input: string, init?: {
|
|
22
|
+
method?: string;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
body?: string;
|
|
25
|
+
}) => Promise<{
|
|
26
|
+
ok: boolean;
|
|
27
|
+
status: number;
|
|
28
|
+
json(): Promise<unknown>;
|
|
29
|
+
text(): Promise<string>;
|
|
30
|
+
}>;
|
|
31
|
+
interface GitHubCommitter {
|
|
32
|
+
name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
}
|
|
35
|
+
interface GitHubClientOptions {
|
|
36
|
+
/** "owner/name" */
|
|
37
|
+
repo: string;
|
|
38
|
+
/** Branch to commit onto, e.g. "main". */
|
|
39
|
+
branch: string;
|
|
40
|
+
/** Fine-grained PAT with contents:write on the one repo. */
|
|
41
|
+
token: string;
|
|
42
|
+
/** Optional committer identity. GitHub uses the token's user if omitted. */
|
|
43
|
+
committer?: GitHubCommitter;
|
|
44
|
+
/** Injectable fetch (defaults to global fetch) — tests mock this. */
|
|
45
|
+
fetch?: FetchLike;
|
|
46
|
+
/** API base — defaults to https://api.github.com. Overridable for tests. */
|
|
47
|
+
apiBase?: string;
|
|
48
|
+
}
|
|
49
|
+
interface CommitFile {
|
|
50
|
+
/** Repo-relative path, forward slashes, no leading slash. */
|
|
51
|
+
path: string;
|
|
52
|
+
/** Raw file content (UTF-8 text). Encoded to base64 before PUT. */
|
|
53
|
+
content: string;
|
|
54
|
+
}
|
|
55
|
+
interface GitHubClient {
|
|
56
|
+
/** Current blob sha for `path`, or null if the file doesn't exist yet. */
|
|
57
|
+
getFileSha(path: string): Promise<string | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Commit each file to the branch. Updates pass the current sha; creates omit
|
|
60
|
+
* it. Resolves once every PUT succeeds; rejects (without partial silence) if
|
|
61
|
+
* any PUT fails so the caller can keep the batch dirty and retry.
|
|
62
|
+
*/
|
|
63
|
+
commitFiles(files: CommitFile[], message: string): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
declare function createGitHubClient(opts: GitHubClientOptions): GitHubClient;
|
|
66
|
+
|
|
67
|
+
interface GitBackedContentPaths {
|
|
68
|
+
/** KV file path. Default <projectRoot>/cancia-content.json. */
|
|
69
|
+
kvPath?: string;
|
|
70
|
+
/** Pages file path. Default <projectRoot>/.cancia/pages.json. */
|
|
71
|
+
pagesPath?: string;
|
|
72
|
+
/** Lists directory. Default <projectRoot>/.cancia/lists. */
|
|
73
|
+
listsDir?: string;
|
|
74
|
+
}
|
|
75
|
+
interface GitBackedOptions {
|
|
76
|
+
/** The wrapped local adapter — the on-disk source of truth. */
|
|
77
|
+
local: CanciaStorageV2;
|
|
78
|
+
/** "owner/name" of the GitHub repo whose builds carry the content. */
|
|
79
|
+
repo: string;
|
|
80
|
+
/** Branch to commit onto. Default "main". */
|
|
81
|
+
branch?: string;
|
|
82
|
+
/**
|
|
83
|
+
* GitHub PAT. Reads CANCIA_GITHUB_TOKEN if omitted. When absent entirely the
|
|
84
|
+
* adapter runs in local-only mode (disk writes only; no commits) + warns once.
|
|
85
|
+
*/
|
|
86
|
+
token?: string;
|
|
87
|
+
/** Optional committer identity for commits. */
|
|
88
|
+
committer?: GitHubCommitter;
|
|
89
|
+
/**
|
|
90
|
+
* Project root the local adapter writes under — needed to turn absolute
|
|
91
|
+
* on-disk paths into repo-relative commit paths. Default process.cwd().
|
|
92
|
+
*/
|
|
93
|
+
projectRoot?: string;
|
|
94
|
+
/** Override where the local adapter's content lives (must match `local`). */
|
|
95
|
+
contentPaths?: GitBackedContentPaths;
|
|
96
|
+
/** Quiet window (ms) before a flush fires. Default 3000. */
|
|
97
|
+
debounceMs?: number;
|
|
98
|
+
/** Commit message for content updates. */
|
|
99
|
+
commitMessage?: string;
|
|
100
|
+
/** Injected GitHub client (tests pass a mock). Overrides token/fetch. */
|
|
101
|
+
client?: GitHubClient;
|
|
102
|
+
/** Injected fetch, forwarded to the default GitHub client. */
|
|
103
|
+
fetch?: FetchLike;
|
|
104
|
+
/** API base override, forwarded to the default GitHub client (tests). */
|
|
105
|
+
apiBase?: string;
|
|
106
|
+
/** Warn sink (tests capture). Default console.warn. */
|
|
107
|
+
warn?: (msg: string) => void;
|
|
108
|
+
/** Error sink for push failures (tests capture). Default console.error. */
|
|
109
|
+
onError?: (msg: string, err: unknown) => void;
|
|
110
|
+
}
|
|
111
|
+
/** The extra control surface the git adapter adds on top of CanciaStorageV2. */
|
|
112
|
+
interface GitBackedControls {
|
|
113
|
+
/**
|
|
114
|
+
* Force any pending dirty files to commit now, bypassing the debounce.
|
|
115
|
+
* Resolves once the flush completes (or rejects if the push failed — the
|
|
116
|
+
* files stay dirty for the next flush). For tests + graceful shutdown.
|
|
117
|
+
*/
|
|
118
|
+
flush(): Promise<void>;
|
|
119
|
+
/** True if git commits are active (token present). */
|
|
120
|
+
readonly gitEnabled: boolean;
|
|
121
|
+
/** Snapshot of currently-dirty repo-relative paths (for tests/inspection). */
|
|
122
|
+
pendingPaths(): string[];
|
|
123
|
+
}
|
|
124
|
+
type GitBackedStorage = CanciaStorageV2 & {
|
|
125
|
+
git: GitBackedControls;
|
|
126
|
+
};
|
|
127
|
+
declare function createGitBackedAdapter(opts: GitBackedOptions): GitBackedStorage;
|
|
128
|
+
|
|
129
|
+
export { CanciaStorage, CanciaStorageV2, type CommitFile, type GitBackedContentPaths, type GitBackedControls, type GitBackedOptions, type GitBackedStorage, type GitHubClient, type GitHubClientOptions, type GitHubCommitter, createGitBackedAdapter, createGitHubClient, createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter };
|
package/dist/storage/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createGitBackedAdapter,
|
|
3
|
+
createGitHubClient,
|
|
2
4
|
createSQLiteAdapter
|
|
3
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-SXKZ2WUL.js";
|
|
4
6
|
import {
|
|
5
7
|
createJsonFileAdapter,
|
|
6
8
|
createJsonFileAdapterV2
|
|
@@ -10,6 +12,8 @@ import {
|
|
|
10
12
|
} from "../chunk-7IA5B5CF.js";
|
|
11
13
|
export {
|
|
12
14
|
RevConflictError,
|
|
15
|
+
createGitBackedAdapter,
|
|
16
|
+
createGitHubClient,
|
|
13
17
|
createJsonFileAdapter,
|
|
14
18
|
createJsonFileAdapterV2,
|
|
15
19
|
createSQLiteAdapter
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cancia/astro",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Astro integration for Cancia CMS — inline editing with zero separate server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
"./storage": {
|
|
28
28
|
"types": "./dist/storage/index.d.ts",
|
|
29
29
|
"import": "./dist/storage/index.js"
|
|
30
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"./richtext": {
|
|
32
|
+
"types": "./dist/richtext/index.d.ts",
|
|
33
|
+
"import": "./dist/richtext/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./richtext/CanciaRichText.astro": "./dist/richtext/CanciaRichText.astro"
|
|
31
36
|
},
|
|
32
37
|
"files": [
|
|
33
38
|
"dist"
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
"vitest": "^4.1.9"
|
|
55
60
|
},
|
|
56
61
|
"dependencies": {
|
|
62
|
+
"astro-portabletext": "^0.13.0",
|
|
57
63
|
"zod": "^4.4.3"
|
|
58
64
|
},
|
|
59
65
|
"scripts": {
|
package/dist/chunk-AE4SIY24.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// src/storage/sqlite.ts
|
|
2
|
-
import { createRequire } from "module";
|
|
3
|
-
var require2 = createRequire(import.meta.url);
|
|
4
|
-
var _db = null;
|
|
5
|
-
function createDB(dbPath) {
|
|
6
|
-
const Database = require2("better-sqlite3");
|
|
7
|
-
const sqlite = new Database(dbPath);
|
|
8
|
-
sqlite.pragma("journal_mode = WAL");
|
|
9
|
-
sqlite.exec(`
|
|
10
|
-
CREATE TABLE IF NOT EXISTS cancia_content (
|
|
11
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
-
site TEXT NOT NULL,
|
|
13
|
-
key TEXT NOT NULL,
|
|
14
|
-
lang TEXT NOT NULL,
|
|
15
|
-
value TEXT NOT NULL,
|
|
16
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
17
|
-
UNIQUE(site, key, lang)
|
|
18
|
-
)
|
|
19
|
-
`);
|
|
20
|
-
return {
|
|
21
|
-
get: sqlite.prepare(
|
|
22
|
-
"SELECT value FROM cancia_content WHERE site=? AND key=? AND lang=?"
|
|
23
|
-
),
|
|
24
|
-
set: sqlite.prepare(
|
|
25
|
-
`INSERT INTO cancia_content (site, key, lang, value, updated_at)
|
|
26
|
-
VALUES (?, ?, ?, ?, unixepoch())
|
|
27
|
-
ON CONFLICT(site, key, lang) DO UPDATE SET value=excluded.value, updated_at=unixepoch()`
|
|
28
|
-
),
|
|
29
|
-
getAll: sqlite.prepare(
|
|
30
|
-
"SELECT key, lang, value FROM cancia_content WHERE site=?"
|
|
31
|
-
),
|
|
32
|
-
delete: sqlite.prepare(
|
|
33
|
-
"DELETE FROM cancia_content WHERE site=? AND key=? AND lang=?"
|
|
34
|
-
)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function getDB(dbPath) {
|
|
38
|
-
if (!_db) _db = createDB(dbPath);
|
|
39
|
-
return _db;
|
|
40
|
-
}
|
|
41
|
-
function createSQLiteAdapter(dbPath) {
|
|
42
|
-
const path = dbPath ?? process.cwd() + "/cancia.db";
|
|
43
|
-
return {
|
|
44
|
-
async get(site, key, lang) {
|
|
45
|
-
const row = getDB(path).get.get(site, key, lang);
|
|
46
|
-
return row?.value ?? null;
|
|
47
|
-
},
|
|
48
|
-
async set(site, key, lang, value) {
|
|
49
|
-
getDB(path).set.run(site, key, lang, value);
|
|
50
|
-
},
|
|
51
|
-
async getAll(site) {
|
|
52
|
-
const rows = getDB(path).getAll.all(site);
|
|
53
|
-
return Object.fromEntries(rows.map((r) => [`${r.key}.${r.lang}`, r.value]));
|
|
54
|
-
},
|
|
55
|
-
async delete(site, key, lang) {
|
|
56
|
-
getDB(path).delete.run(site, key, lang);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export {
|
|
62
|
-
createSQLiteAdapter
|
|
63
|
-
};
|