@cancia/astro 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ };
@@ -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,20 +16,52 @@ 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";
19
+ type FieldWidget = "text" | "textarea" | "url" | "email" | "datetime" | "number" | "checkbox" | "select" | "image" | "slug" | "array" | "object" | "reference" | "richtext";
19
20
  /**
20
- * Metadata attached to a Zod field via `.meta({...})`. All optional.
21
+ * Fields common to every widget's metadata. All optional.
21
22
  */
22
- interface FieldMeta {
23
+ interface FieldMetaBase {
23
24
  /** Display label for the field. Defaults to humanised field name. */
24
25
  label?: string;
25
26
  /** Help text shown under the input. */
26
27
  description?: string;
27
- /** Override widget choice. */
28
- widget?: FieldWidget;
29
28
  /** Placeholder text in the input. */
30
29
  placeholder?: string;
31
30
  }
31
+ /**
32
+ * Metadata attached to a Zod field via `.meta({...})`.
33
+ *
34
+ * This is a widget-keyed discriminated union: each widget variant only
35
+ * permits the options that are legal for it. It exists at the type level
36
+ * only — Zod's `.meta()` accepts any object (its `GlobalMeta` carries an
37
+ * index signature), so this union is what `defineField` uses to type its
38
+ * per-widget option parameters. Passing an illegal option to a
39
+ * `defineField.*` helper is therefore a compile error, while bare
40
+ * `z.string().meta({ widget: "textarea" })` calls stay valid.
41
+ *
42
+ * Later plans extend this union with array / reference / richtext variants.
43
+ */
44
+ type FieldMeta = (FieldMetaBase & {
45
+ widget?: "text" | "textarea" | "url" | "email" | "number" | "checkbox" | "datetime";
46
+ }) | (FieldMetaBase & {
47
+ widget: "select";
48
+ options?: string[];
49
+ }) | (FieldMetaBase & {
50
+ widget: "image";
51
+ alt?: boolean;
52
+ }) | (FieldMetaBase & {
53
+ widget: "slug";
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";
64
+ });
32
65
  interface ListSchemaOptions<TFields extends Record<string, z.ZodTypeAny>> {
33
66
  /** Display label for the list (e.g. "Blog Posts"). */
34
67
  label: string;
@@ -79,7 +112,127 @@ interface FieldDescription {
79
112
  /** For number widgets: min/max. */
80
113
  min?: number;
81
114
  max?: number;
115
+ /** For string widgets: a regex pattern (source, no delimiters) the value must match. */
116
+ pattern?: string;
117
+ /** For slug widgets: the field name to auto-generate the slug from. */
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;
82
128
  }
129
+ /**
130
+ * Typed field constructors. Each returns the underlying Zod type with a
131
+ * correctly-typed `.meta()` already attached, so schemas read declaratively:
132
+ *
133
+ * import { defineField as f } from "@cancia/astro/schema";
134
+ *
135
+ * fields: {
136
+ * title: f.text({ label: "Title" }),
137
+ * slug: f.slug({ source: "title", label: "URL slug" }),
138
+ * status: f.select({ options: ["draft", "published"], label: "Status" }),
139
+ * }
140
+ *
141
+ * The option parameter of each helper is derived from the matching FieldMeta
142
+ * variant, so illegal options (e.g. passing `source` to `text`) are compile
143
+ * errors. `defineField` is purely additive — bare `z.string().meta({...})`
144
+ * declarations keep working unchanged.
145
+ */
146
+ /** Options accepted by the plain string/number/etc. widgets (no extras). */
147
+ type PlainFieldOptions = FieldMetaBase;
148
+ declare const defineField: {
149
+ text: (o?: PlainFieldOptions) => z.ZodString;
150
+ textarea: (o?: PlainFieldOptions) => z.ZodString;
151
+ url: (o?: PlainFieldOptions) => z.ZodString;
152
+ email: (o?: PlainFieldOptions) => z.ZodString;
153
+ datetime: (o?: PlainFieldOptions) => z.ZodString;
154
+ checkbox: (o?: PlainFieldOptions) => z.ZodBoolean;
155
+ number: (o?: PlainFieldOptions & {
156
+ min?: number;
157
+ max?: number;
158
+ }) => z.ZodNumber;
159
+ slug: (o: FieldMetaBase & {
160
+ source?: string;
161
+ }) => z.ZodString;
162
+ image: (o?: FieldMetaBase & {
163
+ alt?: boolean;
164
+ }) => z.ZodString;
165
+ select: (o: FieldMetaBase & {
166
+ options: string[];
167
+ }) => z.ZodEnum<{
168
+ [x: string]: string;
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>>;
235
+ };
83
236
  interface ListDescription {
84
237
  name: string;
85
238
  label: string;
@@ -89,8 +242,22 @@ interface ListDescription {
89
242
  slugField?: string;
90
243
  fields: FieldDescription[];
91
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;
92
259
  declare function describeList(name: string, schema: ListSchema): ListDescription;
93
260
  /** Map of list name → schema. What users export from src/cms/schemas.ts. */
94
261
  type SchemasModule = Record<string, ListSchema>;
95
262
 
96
- export { type FieldDescription, type FieldMeta, type FieldWidget, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineList, describeList };
263
+ export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList, slugify };
@@ -1,10 +1,28 @@
1
1
  import {
2
+ defineField,
2
3
  defineList,
3
4
  describeList,
5
+ slugify,
4
6
  z
5
- } from "../chunk-QAM5VKAF.js";
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";
6
16
  export {
17
+ PT_DECORATORS,
18
+ PT_LIST_ITEMS,
19
+ PT_STYLES,
20
+ defineField,
7
21
  defineList,
8
22
  describeList,
23
+ isSafeHref,
24
+ portableTextSubsetSchema,
25
+ ptBlockSchema,
26
+ slugify,
9
27
  z
10
28
  };
@@ -0,0 +1,20 @@
1
+ import { C as CanciaStorage, a as CanciaStorageV2 } from '../types-BMlLS-OS.js';
2
+ export { b as CanciaKVStore, c as CanciaListStore, d as CanciaPageStore, L as ListEntry, P as PageMeta, e as PageRecord, f as PageSEO, R as Rev, g as RevConflictError } from '../types-BMlLS-OS.js';
3
+
4
+ declare function createJsonFileAdapter(filePath?: string): CanciaStorage;
5
+
6
+ interface JsonFileV2Options {
7
+ /** Project root. Defaults to process.cwd(). */
8
+ projectRoot?: string;
9
+ /** Override the KV file path. Defaults to <root>/cancia-content.json. */
10
+ kvPath?: string;
11
+ /** Override the pages file path. Defaults to <root>/.cancia/pages.json. */
12
+ pagesPath?: string;
13
+ /** Override the lists directory. Defaults to <root>/.cancia/lists. */
14
+ listsDir?: string;
15
+ }
16
+ declare function createJsonFileAdapterV2(opts?: JsonFileV2Options): CanciaStorageV2;
17
+
18
+ declare function createSQLiteAdapter(dbPath?: string): CanciaStorage;
19
+
20
+ export { CanciaStorage, CanciaStorageV2, createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter };
@@ -0,0 +1,16 @@
1
+ import {
2
+ createSQLiteAdapter
3
+ } from "../chunk-AE4SIY24.js";
4
+ import {
5
+ createJsonFileAdapter,
6
+ createJsonFileAdapterV2
7
+ } from "../chunk-ST44VULL.js";
8
+ import {
9
+ RevConflictError
10
+ } from "../chunk-7IA5B5CF.js";
11
+ export {
12
+ RevConflictError,
13
+ createJsonFileAdapter,
14
+ createJsonFileAdapterV2,
15
+ createSQLiteAdapter
16
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cancia/astro",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Astro integration for Cancia CMS — inline editing with zero separate server",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,7 +23,16 @@
23
23
  "./loader": {
24
24
  "types": "./dist/loader/index.d.ts",
25
25
  "import": "./dist/loader/index.js"
26
- }
26
+ },
27
+ "./storage": {
28
+ "types": "./dist/storage/index.d.ts",
29
+ "import": "./dist/storage/index.js"
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"
27
36
  },
28
37
  "files": [
29
38
  "dist"
@@ -46,13 +55,16 @@
46
55
  "better-sqlite3": "^11.0.0",
47
56
  "tsup": "^8.0.0",
48
57
  "typescript": "^5.4.0",
49
- "vite": "^8.0.0"
58
+ "vite": "^8.0.0",
59
+ "vitest": "^4.1.9"
50
60
  },
51
61
  "dependencies": {
62
+ "astro-portabletext": "^0.13.0",
52
63
  "zod": "^4.4.3"
53
64
  },
54
65
  "scripts": {
55
66
  "build": "tsup",
56
- "typecheck": "tsc --noEmit"
67
+ "typecheck": "tsc --noEmit",
68
+ "test": "vitest run"
57
69
  }
58
70
  }