@ryusenkei/maezae 0.0.0-stage → 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/body.js ADDED
@@ -0,0 +1,679 @@
1
+ import { z } from "zod";
2
+ // ../body/src/components.ts
3
+ /** 予約の名前(ブロック) */
4
+ const RESERVED_BLOCK_NAMES = /* @__PURE__ */ new Set(["block"]);
5
+ /** 予約の名前(インライン) */
6
+ const RESERVED_INLINE_NAMES = /* @__PURE__ */ new Set([
7
+ "mark",
8
+ "sup",
9
+ "sub",
10
+ "link"
11
+ ]);
12
+ // ../body/src/library.ts
13
+ /** 注のコンポーネントの名前(本文からの算出 citesOf が参照) */
14
+ const CITE = "cite";
15
+ const https = z.string().regex(/^https:\/\//, "URL は https://");
16
+ const caption = z.string().min(1, "注記が未入力");
17
+ const datum = z.object({
18
+ label: z.string(),
19
+ value: z.number()
20
+ });
21
+ const color = z.union([
22
+ z.literal(1),
23
+ z.literal(2),
24
+ z.literal(3),
25
+ z.literal(4),
26
+ z.literal(5),
27
+ z.literal(6),
28
+ z.literal(7),
29
+ z.literal(8)
30
+ ]);
31
+ const chart = z.union([
32
+ z.object({
33
+ chart: z.literal("bar"),
34
+ data: z.array(datum),
35
+ unit: z.string(),
36
+ color: color.optional(),
37
+ caption
38
+ }),
39
+ z.object({
40
+ chart: z.literal("line"),
41
+ labels: z.array(z.string()),
42
+ series: z.array(z.object({
43
+ name: z.string(),
44
+ values: z.array(z.number())
45
+ })),
46
+ unit: z.string(),
47
+ caption
48
+ }),
49
+ z.object({
50
+ chart: z.literal("pie"),
51
+ data: z.array(datum),
52
+ unit: z.string(),
53
+ caption
54
+ }),
55
+ z.object({
56
+ chart: z.literal("heat"),
57
+ cols: z.array(z.string()),
58
+ rows: z.array(z.object({
59
+ label: z.string(),
60
+ values: z.array(z.number())
61
+ })),
62
+ unit: z.string(),
63
+ color: color.optional(),
64
+ caption
65
+ })
66
+ ]);
67
+ const optional = (attrs, keys) => Object.fromEntries(keys.filter((k) => attrs[k] !== void 0).map((k) => [k, attrs[k]]));
68
+ const plain = (rich) => rich.map((i) => typeof i === "string" ? i : "text" in i ? i.text ?? "" : "").join("");
69
+ /** 核のコンポーネント(chart の Markdown は既定の JSON) */
70
+ const CORE_COMPONENTS = [
71
+ {
72
+ name: CITE,
73
+ inline: true,
74
+ description: "注(出典の ID を参照、出典は記事に結び付けが必要)",
75
+ example: ":cite{#出典の ID}",
76
+ props: () => z.object({ id: z.string().min(1, "注の出典が未入力") })
77
+ },
78
+ {
79
+ name: "pending",
80
+ inline: true,
81
+ description: "未定の値(方針のページで値が決まっていない箇所)",
82
+ example: ":pending[未定]",
83
+ props: () => z.object({})
84
+ },
85
+ {
86
+ name: "speech",
87
+ description: "発言(会話の一言を大きく引く、1段落)",
88
+ example: ":::speech\n大きく引く一言\n:::",
89
+ markdown: {
90
+ to: (p) => ({ content: [{
91
+ type: "paragraph",
92
+ content: [p.text]
93
+ }] }),
94
+ from: (_, content) => ({ text: content[0]?.type === "paragraph" ? plain(content[0].content) : "" })
95
+ },
96
+ props: () => z.object({ text: z.string().min(1, "発言が未入力") })
97
+ },
98
+ {
99
+ name: "chart",
100
+ description: "グラフ(中は JSON、chart は bar・line・pie・heat、caption は必須)",
101
+ example: ":::chart\n```json\n{\"chart\": \"bar\", \"unit\": \"%\", \"data\": [{\"label\": \"A\", \"value\": 40}], \"caption\": \"説明\"}\n```\n:::",
102
+ props: () => chart
103
+ },
104
+ {
105
+ name: "diagram",
106
+ description: "図(中は Mermaid、alt は図の内容の説明、caption は必須)",
107
+ example: ":::diagram{alt=\"内容の説明\" caption=\"説明\"}\n```mermaid\nflowchart LR\n A --> B\n```\n:::",
108
+ markdown: {
109
+ to: (p) => ({
110
+ attributes: {
111
+ alt: p.alt,
112
+ caption: p.caption
113
+ },
114
+ content: [{
115
+ type: "code",
116
+ lang: "mermaid",
117
+ code: p.source
118
+ }]
119
+ }),
120
+ from: (attrs, content) => ({
121
+ source: content[0]?.type === "code" ? content[0].code : "",
122
+ alt: attrs.alt ?? "",
123
+ caption: attrs.caption ?? ""
124
+ })
125
+ },
126
+ props: () => z.object({
127
+ source: z.string().min(1, "図の記法が未入力"),
128
+ alt: z.string().min(1, "図の代替テキストが未入力"),
129
+ caption
130
+ })
131
+ },
132
+ {
133
+ name: "gallery",
134
+ description: "複数の画像を2列で配置(画像は2枚以上)",
135
+ example: ":::gallery\n![](asset/<画像の ID> \"説明\")\n\n![](asset/<画像の ID> \"説明\")\n:::",
136
+ markdown: {
137
+ to: (p) => ({ content: p.items.map((i) => ({
138
+ type: "image",
139
+ media: i.media,
140
+ ...i.caption ? { caption: i.caption } : {}
141
+ })) }),
142
+ from: (_, content) => ({ items: content.flatMap((b) => b.type === "image" ? [{
143
+ media: b.media,
144
+ ...b.caption ? { caption: b.caption } : {}
145
+ }] : []) })
146
+ },
147
+ props: (media) => z.object({ items: z.array(z.object({
148
+ media,
149
+ caption: z.string().optional()
150
+ })).min(2, "ギャラリーの画像は2枚以上") })
151
+ },
152
+ {
153
+ name: "link-card",
154
+ description: "外部リンクのカード(https のみ、site・desc・画像は任意)",
155
+ example: ":::link-card{href=\"https://…\" title=\"ページの題\" site=\"サイト名\" desc=\"説明\"}\n:::",
156
+ markdown: {
157
+ to: (p) => {
158
+ const { href, title, site, desc, image } = p;
159
+ return {
160
+ attributes: {
161
+ href,
162
+ title,
163
+ ...site ? { site } : {},
164
+ ...desc ? { desc } : {}
165
+ },
166
+ content: image ? [{
167
+ type: "image",
168
+ media: image
169
+ }] : []
170
+ };
171
+ },
172
+ from: (attrs, content) => ({
173
+ href: attrs.href,
174
+ title: attrs.title,
175
+ ...optional(attrs, ["site", "desc"]),
176
+ ...content[0]?.type === "image" ? { image: content[0].media } : {}
177
+ })
178
+ },
179
+ props: (media) => z.object({
180
+ href: https,
181
+ title: z.string().min(1, "リンクのタイトルが未入力"),
182
+ site: z.string().optional(),
183
+ desc: z.string().optional(),
184
+ image: media.optional()
185
+ })
186
+ },
187
+ {
188
+ name: "links",
189
+ description: "外部リンクの一覧(1件1行、リンクの title はサイト名、label は小見出しで既定は「参考」)",
190
+ example: ":::links{label=\"参考\"}\n- [ページの題](https://… \"サイト名\")\n:::",
191
+ markdown: {
192
+ to: (p) => {
193
+ const { label, items } = p;
194
+ const content = [{
195
+ type: "list",
196
+ items: items.map((i) => ({ content: [{
197
+ text: i.title,
198
+ link: {
199
+ href: i.href,
200
+ ...i.site ? { title: i.site } : {}
201
+ }
202
+ }] }))
203
+ }];
204
+ return {
205
+ attributes: label ? { label } : void 0,
206
+ content
207
+ };
208
+ },
209
+ from: (attrs, content) => ({
210
+ ...optional(attrs, ["label"]),
211
+ items: content[0]?.type === "list" ? content[0].items.flatMap((i) => i.content.flatMap((x) => typeof x !== "string" && "link" in x && x.link ? [{
212
+ href: x.link.href,
213
+ title: x.text,
214
+ ...x.link.title ? { site: x.link.title } : {}
215
+ }] : [])) : []
216
+ })
217
+ },
218
+ props: () => z.object({
219
+ label: z.string().optional(),
220
+ items: z.array(z.object({
221
+ href: https,
222
+ title: z.string().min(1, "リンクのタイトルが未入力"),
223
+ site: z.string().optional()
224
+ }))
225
+ })
226
+ }
227
+ ];
228
+ /** 名前から核のコンポーネント(なければ undefined) */
229
+ const coreComponentOf = (name) => CORE_COMPONENTS.find((c) => c.name === name);
230
+ // ../body/src/declared.ts
231
+ const NAME = /^[A-Za-z][A-Za-z0-9-]*$/;
232
+ const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
233
+ const propSchema = z.lazy(() => z.object({
234
+ type: z.enum([
235
+ "string",
236
+ "number",
237
+ "integer",
238
+ "boolean",
239
+ "array",
240
+ "object"
241
+ ]),
242
+ title: z.string().min(1).optional(),
243
+ description: z.string().optional(),
244
+ enum: z.array(z.string().min(1)).min(1).optional(),
245
+ items: propSchema.optional(),
246
+ properties: z.record(z.string().regex(KEY, "引数の名前は英字で始まる英数字"), propSchema).optional(),
247
+ required: z.array(z.string()).optional()
248
+ }).strict().superRefine((p, ctx) => {
249
+ if (p.enum && p.type !== "string") ctx.addIssue({
250
+ code: "custom",
251
+ message: "選択肢 enum は文字列の引数だけ",
252
+ path: ["enum"]
253
+ });
254
+ if (p.type === "array" !== (p.items !== void 0)) ctx.addIssue({
255
+ code: "custom",
256
+ message: "配列の引数は items が必要(ほかの型は不可)",
257
+ path: ["items"]
258
+ });
259
+ if ((p.properties || p.required) && p.type !== "object") ctx.addIssue({
260
+ code: "custom",
261
+ message: "properties・required はオブジェクトの引数だけ",
262
+ path: ["properties"]
263
+ });
264
+ for (const key of p.required ?? []) if (!p.properties?.[key]) ctx.addIssue({
265
+ code: "custom",
266
+ message: `必須の引数 ${key} が properties にない`,
267
+ path: ["required"]
268
+ });
269
+ }));
270
+ const declaredComponent = z.object({
271
+ name: z.string().regex(NAME, "部品の名前は英字で始まる英数字とハイフン"),
272
+ label: z.string().min(1, "部品の表示名が未入力"),
273
+ description: z.string().optional(),
274
+ inline: z.boolean().optional(),
275
+ props: z.object({
276
+ type: z.literal("object"),
277
+ properties: z.record(z.string().regex(KEY, "引数の名前は英字で始まる英数字"), propSchema),
278
+ required: z.array(z.string()).optional()
279
+ }).strict()
280
+ }).strict().superRefine((d, ctx) => {
281
+ if (coreComponentOf(d.name) || RESERVED_BLOCK_NAMES.has(d.name) || RESERVED_INLINE_NAMES.has(d.name)) ctx.addIssue({
282
+ code: "custom",
283
+ message: `部品の名前 ${d.name} はコアの部品・予約の名前と重複`,
284
+ path: ["name"]
285
+ });
286
+ for (const key of d.props.required ?? []) if (!d.props.properties[key]) ctx.addIssue({
287
+ code: "custom",
288
+ message: `必須の引数 ${key} が properties にない`,
289
+ path: ["props", "required"]
290
+ });
291
+ if (d.inline) {
292
+ for (const [key, p] of Object.entries(d.props.properties)) if (p.type !== "string") ctx.addIssue({
293
+ code: "custom",
294
+ message: `インラインの部品の引数は文字列だけ: ${key}`,
295
+ path: [
296
+ "props",
297
+ "properties",
298
+ key
299
+ ]
300
+ });
301
+ }
302
+ });
303
+ z.array(declaredComponent).superRefine((list, ctx) => {
304
+ const seen = /* @__PURE__ */ new Set();
305
+ list.forEach((d, i) => {
306
+ if (seen.has(d.name)) ctx.addIssue({
307
+ code: "custom",
308
+ message: `部品の名前 ${d.name} が重複`,
309
+ path: [i, "name"]
310
+ });
311
+ seen.add(d.name);
312
+ });
313
+ });
314
+ /** 1つの引数の検査(label はメッセージの項目名) */
315
+ function propZod(p, label) {
316
+ const wrong = { error: (iss) => iss.input === void 0 ? `${label}が未入力` : `${label}の形が不正` };
317
+ switch (p.type) {
318
+ case "string": return p.enum ? z.enum(p.enum, wrong) : z.string(wrong);
319
+ case "number": return z.number(wrong);
320
+ case "integer": return z.number(wrong).int(`${label}は整数`);
321
+ case "boolean": return z.boolean(wrong);
322
+ case "array": return z.array(propZod(p.items, label), wrong);
323
+ case "object": return objectZod(p.properties ?? {}, p.required ?? [], wrong);
324
+ }
325
+ }
326
+ /** オブジェクトの引数の検査(必須でない引数は省略可、宣言にない引数は除去) */
327
+ const objectZod = (properties, required, error) => z.object(Object.fromEntries(Object.entries(properties).map(([key, p]) => [key, required.includes(key) ? propZod(p, p.title ?? key) : propZod(p, p.title ?? key).optional()])), error);
328
+ const propsCache = /* @__PURE__ */ new WeakMap();
329
+ /** 宣言した部品の引数の検査(宣言ごとに作成を再利用) */
330
+ function declaredPropsZod(d) {
331
+ let schema = propsCache.get(d);
332
+ if (!schema) propsCache.set(d, schema = objectZod(d.props.properties, d.props.required ?? [], { error: () => `${d.label}の引数の形が不正` }));
333
+ return schema;
334
+ }
335
+ // ../body/src/description.ts
336
+ /** meta description の長さの上限(検索結果で省略されにくい字数の目安) */
337
+ const DESCRIPTION_MAX = 120;
338
+ /**
339
+ * 説明を meta description の長さに調整
340
+ * - 上限を超える場合: 上限までの最後の句点まで
341
+ * - 句点がない場合: 上限で切って省略の記号
342
+ */
343
+ function descriptionOf(excerpt, max = 120) {
344
+ if ([...excerpt].length <= max) return excerpt;
345
+ const head = [...excerpt].slice(0, max).join("");
346
+ const end = head.lastIndexOf("。");
347
+ return end > 0 ? head.slice(0, end + 1) : `${[...head].slice(0, max - 1).join("")}…`;
348
+ }
349
+ // ../body/src/derive.ts
350
+ /** 読了時間の既定の分速(字/分) */
351
+ const READING_SPEED = 500;
352
+ const listRich = (list) => list.items.flatMap((i) => [i.content, ...i.children ? listRich(i.children) : []]);
353
+ /** 本文にある断片の列(段落・見出し・リスト・引用・表のセル) */
354
+ function richIn(body) {
355
+ return body.flatMap((b) => {
356
+ switch (b.type) {
357
+ case "paragraph":
358
+ case "heading": return [b.content];
359
+ case "list": return listRich(b);
360
+ case "quote": return [...b.content];
361
+ case "table": return [...b.head ?? [], ...b.rows.flat()];
362
+ default: return [];
363
+ }
364
+ });
365
+ }
366
+ /** 断片の列の文字(装飾・リンクは文字、インラインのコンポーネントは中の文字) */
367
+ const plainOf = (rich) => rich.map((i) => typeof i === "string" ? i : i.text ?? "").join("");
368
+ /** 本文の注が参照する出典の ID(出現の順、重複あり) */
369
+ function citesOf(body) {
370
+ return richIn(body).flatMap((rich) => rich.flatMap((i) => typeof i === "object" && "component" in i && i.component === "cite" && i.props?.id ? [i.props.id] : []));
371
+ }
372
+ /** 本文の導入の文字(最初の見出しまでの段落をつなげたもの、導入がなければ空) */
373
+ function excerptOf(body) {
374
+ const end = body.findIndex((b) => b.type === "heading");
375
+ return (end < 0 ? body : body.slice(0, end)).flatMap((b) => b.type === "paragraph" ? [plainOf(b.content).replaceAll("\n", "").trim()] : []).filter(Boolean).join("");
376
+ }
377
+ /** 字数に数えない項目(ブロック・コンポーネント・表示の種類・装飾の名前、保存の形に依存する文字) */
378
+ const UNREAD = /* @__PURE__ */ new Set([
379
+ "type",
380
+ "name",
381
+ "component",
382
+ "variant",
383
+ "marks"
384
+ ]);
385
+ const chars = (v) => typeof v === "string" ? v.length : Array.isArray(v) ? v.reduce((n, x) => n + chars(x), 0) : v && typeof v === "object" ? Object.entries(v).reduce((n, [k, x]) => n + (UNREAD.has(k) ? 0 : chars(x)), 0) : 0;
386
+ /** 読了時間(分、本文の文字列の字数を分速 speed で割った概算、1分以上) */
387
+ const readMinOf = (body, speed = 500) => Math.max(1, Math.round(chars(body) / speed));
388
+ const hrefs = (v) => Array.isArray(v) ? v.flatMap(hrefs) : v && typeof v === "object" ? Object.entries(v).flatMap(([k, x]) => k === "href" && typeof x === "string" ? [x] : hrefs(x)) : [];
389
+ /** 本文にあるサイト内へのリンク先(`/` で始まる href、重複なし) */
390
+ const linksOf = (body) => [...new Set(hrefs(body).filter((h) => h.startsWith("/")))];
391
+ // ../body/src/href.ts
392
+ const SAFE_HREF = /^(?:https?:\/\/|mailto:|#|\/(?![/\\]))/i;
393
+ /** 本文のリンク先として許可する形か(保存の検査とサイトの出力で共通) */
394
+ const isSafeHref = (href) => SAFE_HREF.test(href);
395
+ /** 許可しないリンク先の問題の文言 */
396
+ const unsafeHrefMessage = (href) => `リンク先 ${href} は使えない(https://・http://・mailto:・/ で始まるサイト内のパス・# で始まるページ内のリンクだけ)`;
397
+ // ../body/src/profile.ts
398
+ const MARK_LABELS = {
399
+ strong: "太字",
400
+ em: "斜体",
401
+ code: "コード",
402
+ strike: "取り消し線",
403
+ mark: "蛍光ペン",
404
+ sup: "上付き",
405
+ sub: "下付き"
406
+ };
407
+ /** 本文を設定で検査(合わない箇所をすべて返却) */
408
+ function checkBody(body, profile) {
409
+ const problems = [];
410
+ const allowedMarks = new Set(profile.marks);
411
+ body.forEach((block, index) => {
412
+ const add = (message) => problems.push({
413
+ index,
414
+ message
415
+ });
416
+ const rich = (r, plain = false) => {
417
+ for (const inline of r) {
418
+ if (typeof inline === "string") continue;
419
+ if ("component" in inline) {
420
+ if (plain) add("この場所ではインラインのコンポーネントは使えない");
421
+ else if (!profile.components.includes(inline.component)) add(`インラインのコンポーネント ${inline.component} は使えない`);
422
+ continue;
423
+ }
424
+ if (plain && (inline.marks?.length || inline.link)) add("この場所では装飾・リンクは使えない");
425
+ for (const m of inline.marks ?? []) if (!allowedMarks.has(m)) add(`${MARK_LABELS[m]}は使えない`);
426
+ if (profile.combineMarks === false && (inline.marks?.length ?? 0) + (inline.link ? 1 : 0) > 1) add("装飾の重ね掛け・装飾つきのリンクは使えない");
427
+ if (inline.link?.rel && !profile.linkRels?.includes(inline.link.rel)) add(`リンクの rel=${inline.link.rel} は使えない`);
428
+ if (inline.link && !isSafeHref(inline.link.href)) add(unsafeHrefMessage(inline.link.href));
429
+ }
430
+ };
431
+ const variant = (v, allowed = []) => {
432
+ if (v && !allowed.includes(v)) add(`表示の種類 ${v} は使えない`);
433
+ };
434
+ const b = block;
435
+ switch (b.type) {
436
+ case "paragraph":
437
+ variant(b.variant);
438
+ rich(b.content);
439
+ break;
440
+ case "heading":
441
+ if (!profile.headings.includes(b.level)) add(`見出し${b.level}は使えない(使える見出し: ${profile.headings.join("・")})`);
442
+ variant(b.variant);
443
+ rich(b.content, profile.plain?.includes("heading"));
444
+ break;
445
+ case "list": {
446
+ const p = profile.list;
447
+ if (!p) {
448
+ add("リストは使えない");
449
+ break;
450
+ }
451
+ if (b.ordered && !p.ordered) add("番号のリストは使えない");
452
+ if (!p.nested && b.items.some((i) => i.children)) add("入れ子のリストは使えない");
453
+ variant(b.variant, p.variants);
454
+ b.items.forEach((i) => rich(i.content));
455
+ break;
456
+ }
457
+ case "quote": {
458
+ const p = profile.quote;
459
+ if (!p) {
460
+ add("引用は使えない");
461
+ break;
462
+ }
463
+ if (!p.multiple && b.content.length > 1) add("引用は1段落まで");
464
+ if (!p.cite && b.cite) add("引用の出典は使えない");
465
+ if (b.cite?.href && !isSafeHref(b.cite.href)) add(unsafeHrefMessage(b.cite.href));
466
+ variant(b.variant);
467
+ b.content.forEach((r) => rich(r));
468
+ break;
469
+ }
470
+ case "code":
471
+ if (!profile.code) add("コードは使えない");
472
+ else if (!profile.code.caption && b.caption) add("コードの説明は使えない");
473
+ else if (b.lang && profile.code.componentLangs?.[b.lang]) add(`\`\`\`${b.lang} のコードは使えない(:::${profile.code.componentLangs[b.lang]} で囲む)`);
474
+ variant(b.variant);
475
+ break;
476
+ case "image":
477
+ if (!profile.image) add("画像は使えない");
478
+ variant(b.variant, profile.image?.variants);
479
+ break;
480
+ case "table": {
481
+ const p = profile.table;
482
+ if (!p) {
483
+ add("表は使えない");
484
+ break;
485
+ }
486
+ if (p.head === "required" && !b.head) add("表は見出しの行が必須");
487
+ if (p.caption === "required" && !b.caption) add("表は説明(caption)が必須");
488
+ if (p.caption === "none" && b.caption) add("表の説明は使えない");
489
+ variant(b.variant);
490
+ b.head?.forEach((r) => rich(r, profile.plain?.includes("tableHead")));
491
+ b.rows.forEach((row) => row.forEach((r) => rich(r)));
492
+ break;
493
+ }
494
+ case "divider":
495
+ if (!profile.divider) add("区切りは使えない");
496
+ break;
497
+ case "html":
498
+ if (!profile.html) add("HTML のブロックは使えない");
499
+ break;
500
+ case "component": if (!profile.components.includes(b.name)) add(`コンポーネント ${b.name} は使えない`);
501
+ }
502
+ });
503
+ return problems;
504
+ }
505
+ // ../body/src/schema.ts
506
+ const MARKS = [
507
+ "strong",
508
+ "em",
509
+ "code",
510
+ "strike",
511
+ "mark",
512
+ "sup",
513
+ "sub"
514
+ ];
515
+ const CITE_TEXT = /:cite\{#([^\s}]+)\}/g;
516
+ const liftCites = (text) => text.split(CITE_TEXT).flatMap((piece, i) => i % 2 ? [{
517
+ component: CITE,
518
+ props: { id: piece }
519
+ }] : piece ? [piece] : []);
520
+ const link = z.object({
521
+ href: z.string().min(1, "リンク先が未入力"),
522
+ title: z.string().optional(),
523
+ rel: z.string().optional()
524
+ });
525
+ const text = z.object({
526
+ text: z.string(),
527
+ marks: z.array(z.enum(MARKS)).optional(),
528
+ link: link.optional()
529
+ });
530
+ /** 部品の props の検査(名前で核のコンポーネント、なければ宣言した部品、どちらもないか種類が違えば null) */
531
+ function propsOf(name, inline, media, declared) {
532
+ const core = coreComponentOf(name);
533
+ if (core) return Boolean(core.inline) === inline ? core.props(media) : null;
534
+ const d = declared.find((c) => c.name === name);
535
+ return d && Boolean(d.inline) === inline ? declaredPropsZod(d) : null;
536
+ }
537
+ const inlineComponentOf = (declared) => z.object({
538
+ component: z.string().min(1),
539
+ props: z.record(z.string(), z.string()).optional(),
540
+ text: z.string().optional()
541
+ }).superRefine((c, ctx) => {
542
+ const props = propsOf(c.component, true, z.never(), declared);
543
+ if (!props) return void ctx.addIssue({
544
+ code: "custom",
545
+ message: `インラインのコンポーネント ${c.component} はない`
546
+ });
547
+ const r = props.safeParse(c.props ?? {});
548
+ if (!r.success) for (const issue of r.error.issues) ctx.addIssue({
549
+ code: "custom",
550
+ message: issue.message,
551
+ path: ["props", ...issue.path]
552
+ });
553
+ });
554
+ /** インラインの1要素(component を持つ要素は部品としてだけ検査、union で文字の形に落ちて部品の検査を素通りしないため) */
555
+ function inlineItemOf(declared) {
556
+ const component = inlineComponentOf(declared);
557
+ const plain = z.union([z.string(), text]);
558
+ return z.unknown().transform((value, ctx) => {
559
+ const r = (typeof value === "object" && value !== null && "component" in value ? component : plain).safeParse(value);
560
+ if (r.success) return r.data;
561
+ for (const issue of r.error.issues) ctx.addIssue({
562
+ code: "custom",
563
+ message: issue.message,
564
+ path: issue.path
565
+ });
566
+ return z.NEVER;
567
+ });
568
+ }
569
+ const richOf = (cites, declared) => z.array(inlineItemOf(declared)).transform((items) => cites ? items.flatMap((item) => typeof item === "string" ? liftCites(item) : [item]) : items);
570
+ const variant = z.string().optional();
571
+ /** 本文の1ブロックの検査(profile は本文の設定、media は画像の参照の検査、declared は導入先が宣言した部品) */
572
+ function blockSchemaOf(profile, media, declared = []) {
573
+ const rich = richOf(profile.components.includes(CITE), declared);
574
+ const list = z.object({
575
+ type: z.literal("list"),
576
+ ordered: z.boolean().optional(),
577
+ items: z.array(z.object({
578
+ content: rich,
579
+ get children() {
580
+ return list.optional();
581
+ }
582
+ })),
583
+ variant
584
+ });
585
+ const heading = z.object({
586
+ type: z.literal("heading"),
587
+ level: z.union([
588
+ z.literal(1),
589
+ z.literal(2),
590
+ z.literal(3),
591
+ z.literal(4),
592
+ z.literal(5),
593
+ z.literal(6)
594
+ ]),
595
+ content: rich,
596
+ id: z.string().optional(),
597
+ variant
598
+ });
599
+ return z.discriminatedUnion("type", [
600
+ z.object({
601
+ type: z.literal("paragraph"),
602
+ content: rich,
603
+ variant
604
+ }),
605
+ heading.refine((h) => h.content.some((i) => typeof i === "string" ? i.trim() : "text" in i && i.text?.trim()), "見出しが未入力"),
606
+ list,
607
+ z.object({
608
+ type: z.literal("quote"),
609
+ content: z.array(rich),
610
+ cite: z.object({
611
+ label: z.string(),
612
+ href: z.string().optional()
613
+ }).optional(),
614
+ variant
615
+ }),
616
+ z.object({
617
+ type: z.literal("code"),
618
+ code: z.string(),
619
+ lang: z.string().optional(),
620
+ caption: z.string().optional(),
621
+ variant
622
+ }),
623
+ z.object({
624
+ type: z.literal("image"),
625
+ media,
626
+ alt: z.string().optional(),
627
+ caption: z.string().optional(),
628
+ variant
629
+ }),
630
+ z.object({
631
+ type: z.literal("table"),
632
+ head: z.array(rich).optional(),
633
+ rows: z.array(z.array(rich)),
634
+ caption: z.string().optional(),
635
+ variant
636
+ }),
637
+ z.object({ type: z.literal("divider") }),
638
+ z.object({
639
+ type: z.literal("html"),
640
+ html: z.string()
641
+ }),
642
+ z.object({
643
+ type: z.literal("component"),
644
+ name: z.string().min(1),
645
+ props: z.unknown()
646
+ })
647
+ ]).transform((block, ctx) => {
648
+ let out = block;
649
+ if (out.type === "component") {
650
+ const props = propsOf(out.name, false, media, declared);
651
+ if (!props) {
652
+ ctx.addIssue({
653
+ code: "custom",
654
+ message: `コンポーネント ${out.name} はない`
655
+ });
656
+ return z.NEVER;
657
+ }
658
+ const r = props.safeParse(out.props);
659
+ if (!r.success) {
660
+ for (const issue of r.error.issues) ctx.addIssue({
661
+ code: "custom",
662
+ message: issue.message,
663
+ path: ["props", ...issue.path]
664
+ });
665
+ return z.NEVER;
666
+ }
667
+ out = {
668
+ ...out,
669
+ props: r.data
670
+ };
671
+ }
672
+ for (const p of checkBody([out], profile)) ctx.addIssue({
673
+ code: "custom",
674
+ message: p.message
675
+ });
676
+ return out;
677
+ });
678
+ }
679
+ export { DESCRIPTION_MAX, READING_SPEED, blockSchemaOf, citesOf, descriptionOf, excerptOf, isSafeHref, linksOf, plainOf, readMinOf };