@cfbender/cesium 0.4.0 → 0.5.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/CHANGELOG.md +53 -0
- package/README.md +2 -5
- package/package.json +1 -1
- package/src/cli/commands/serve.ts +3 -0
- package/src/index.ts +4 -1
- package/src/prompt/field-reference.ts +94 -0
- package/src/prompt/system-fragment.md +56 -65
- package/src/render/blocks/catalog.ts +39 -0
- package/src/render/blocks/escape.ts +27 -0
- package/src/render/blocks/index.ts +6 -0
- package/src/render/blocks/markdown.ts +217 -0
- package/src/render/blocks/render.ts +96 -0
- package/src/render/blocks/renderers/callout.ts +38 -0
- package/src/render/blocks/renderers/code.ts +44 -0
- package/src/render/blocks/renderers/compare-table.ts +56 -0
- package/src/render/blocks/renderers/diagram.ts +48 -0
- package/src/render/blocks/renderers/divider.ts +31 -0
- package/src/render/blocks/renderers/hero.ts +66 -0
- package/src/render/blocks/renderers/kv.ts +45 -0
- package/src/render/blocks/renderers/list.ts +51 -0
- package/src/render/blocks/renderers/pill-row.ts +45 -0
- package/src/render/blocks/renderers/prose.ts +29 -0
- package/src/render/blocks/renderers/raw-html.ts +32 -0
- package/src/render/blocks/renderers/risk-table.ts +76 -0
- package/src/render/blocks/renderers/section.ts +95 -0
- package/src/render/blocks/renderers/timeline.ts +58 -0
- package/src/render/blocks/renderers/tldr.ts +30 -0
- package/src/render/blocks/types.ts +127 -0
- package/src/render/blocks/validate-block.ts +202 -0
- package/src/render/critique.ts +410 -10
- package/src/render/fallback.ts +18 -0
- package/src/render/theme.ts +154 -0
- package/src/render/validate.ts +282 -17
- package/src/render/wrap.ts +7 -7
- package/src/server/lifecycle.ts +7 -1
- package/src/storage/assets.ts +66 -0
- package/src/storage/index-cache.ts +1 -0
- package/src/storage/index-gen.ts +13 -14
- package/src/tools/ask.ts +5 -3
- package/src/tools/critique.ts +41 -6
- package/src/tools/publish.ts +39 -12
- package/src/tools/styleguide.ts +109 -9
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Owned markdown subset — no external dependencies.
|
|
2
|
+
// src/render/blocks/markdown.ts
|
|
3
|
+
//
|
|
4
|
+
// Block-level: paragraph, bullet list (- ), ordered list (1. ),
|
|
5
|
+
// blockquote (> ), horizontal rule (---), hard break (two-space EOL).
|
|
6
|
+
// Inline: **bold**, *italic*, `code`, [text](href) — external hrefs → plain text.
|
|
7
|
+
// HTML safelist: <kbd>, <span class="pill">, <span class="tag">.
|
|
8
|
+
// Everything else is HTML-escaped.
|
|
9
|
+
|
|
10
|
+
import { escapeHtml, escapeAttr } from "./escape.ts";
|
|
11
|
+
|
|
12
|
+
// ─── Safelist placeholder pass ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const SAFELIST_RE =
|
|
15
|
+
/<(kbd)>(.*?)<\/kbd>|<span\s+class="(pill|tag)">(.*?)<\/span>/gi;
|
|
16
|
+
|
|
17
|
+
function extractSafelist(input: string): { text: string; map: Map<string, string> } {
|
|
18
|
+
const map = new Map<string, string>();
|
|
19
|
+
let counter = 0;
|
|
20
|
+
const text = input.replace(SAFELIST_RE, (...args: unknown[]) => {
|
|
21
|
+
// args: [fullMatch, kbdTag, kbdContent, spanClass, spanContent, offset, string]
|
|
22
|
+
const full = args[0] as string;
|
|
23
|
+
const kbdTag = args[1] as string | undefined;
|
|
24
|
+
const kbdContent = args[2] as string | undefined;
|
|
25
|
+
const spanClass = args[3] as string | undefined;
|
|
26
|
+
const spanContent = args[4] as string | undefined;
|
|
27
|
+
|
|
28
|
+
let html: string;
|
|
29
|
+
if (kbdTag !== undefined && kbdContent !== undefined) {
|
|
30
|
+
html = `<kbd>${escapeHtml(kbdContent)}</kbd>`;
|
|
31
|
+
} else if (spanClass !== undefined && spanContent !== undefined) {
|
|
32
|
+
html = `<span class="${escapeAttr(spanClass)}">${escapeHtml(spanContent)}</span>`;
|
|
33
|
+
} else {
|
|
34
|
+
// Shouldn't happen, but fall back to escaping the whole match
|
|
35
|
+
html = escapeHtml(full);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const id = `\x00SAFE${counter++}\x00`;
|
|
39
|
+
map.set(id, html);
|
|
40
|
+
return id;
|
|
41
|
+
});
|
|
42
|
+
return { text, map };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function restoreSafelist(html: string, map: Map<string, string>): string {
|
|
46
|
+
let result = html;
|
|
47
|
+
for (const [id, replacement] of map) {
|
|
48
|
+
result = result.split(id).join(replacement);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Inline rendering ────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const RELATIVE_HREF_RE = /^([/#]|[^:/?#]*[/?#]|[^:/?#]+$)/;
|
|
56
|
+
|
|
57
|
+
function renderInline(text: string): string {
|
|
58
|
+
// Step 1: extract inline code spans before any escaping
|
|
59
|
+
const codeMap = new Map<string, string>();
|
|
60
|
+
let codeCounter = 0;
|
|
61
|
+
let working = text.replace(/`([^`]+)`/g, (_m, inner: string) => {
|
|
62
|
+
const id = `\x00CODE${codeCounter++}\x00`;
|
|
63
|
+
codeMap.set(id, `<code>${escapeHtml(inner)}</code>`);
|
|
64
|
+
return id;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Step 2: escape remaining HTML
|
|
68
|
+
working = working.replace(/&/g, "&");
|
|
69
|
+
working = working.replace(/</g, "<").replace(/>/g, ">");
|
|
70
|
+
|
|
71
|
+
// Step 3: process other inline patterns
|
|
72
|
+
// **bold**
|
|
73
|
+
working = working.replace(/\*\*(.+?)\*\*/g, (_m, inner: string) => `<strong>${inner}</strong>`);
|
|
74
|
+
// *italic* (not preceded by another *)
|
|
75
|
+
working = working.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (_m, inner: string) => `<em>${inner}</em>`);
|
|
76
|
+
// [text](href) — only relative/anchor hrefs; external → plain text
|
|
77
|
+
working = working.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, linkText: string, href: string) => {
|
|
78
|
+
if (RELATIVE_HREF_RE.test(href)) {
|
|
79
|
+
return `<a href="${escapeAttr(href)}">${linkText}</a>`;
|
|
80
|
+
}
|
|
81
|
+
return linkText;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Step 4: restore code spans and hard break placeholders
|
|
85
|
+
for (const [id, html] of codeMap) {
|
|
86
|
+
working = working.split(id).join(html);
|
|
87
|
+
}
|
|
88
|
+
working = working.split(HARD_BREAK_PLACEHOLDER).join("<br>");
|
|
89
|
+
|
|
90
|
+
return working;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Block-level rendering ───────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
type Line = string;
|
|
96
|
+
|
|
97
|
+
function isHr(line: Line): boolean {
|
|
98
|
+
return /^-{3,}\s*$/.test(line) || /^\*{3,}\s*$/.test(line) || /^_{3,}\s*$/.test(line);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isBullet(line: Line): boolean {
|
|
102
|
+
return /^[ \t]*[-*+]\s+/.test(line);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isOrdered(line: Line): boolean {
|
|
106
|
+
return /^[ \t]*\d+\.\s+/.test(line);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isBlockquote(line: Line): boolean {
|
|
110
|
+
return /^[ \t]*>\s?/.test(line);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isBlank(line: Line): boolean {
|
|
114
|
+
return line.trim() === "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getBulletContent(line: Line): string {
|
|
118
|
+
return line.replace(/^[ \t]*[-*+]\s+/, "");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getOrderedContent(line: Line): string {
|
|
122
|
+
return line.replace(/^[ \t]*\d+\.\s+/, "");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getBlockquoteContent(line: Line): string {
|
|
126
|
+
return line.replace(/^[ \t]*>\s?/, "");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Applies hard-break rule: line ending with two spaces → placeholder, restored after escaping */
|
|
130
|
+
const HARD_BREAK_PLACEHOLDER = "\x00BR\x00";
|
|
131
|
+
|
|
132
|
+
function applyHardBreak(line: string): string {
|
|
133
|
+
if (line.endsWith(" ")) {
|
|
134
|
+
return line.slice(0, -2) + HARD_BREAK_PLACEHOLDER;
|
|
135
|
+
}
|
|
136
|
+
return line;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function renderMarkdown(md: string): string {
|
|
140
|
+
const { text: safeText, map: safeMap } = extractSafelist(md);
|
|
141
|
+
const lines = safeText.split("\n");
|
|
142
|
+
const parts: string[] = [];
|
|
143
|
+
let i = 0;
|
|
144
|
+
|
|
145
|
+
while (i < lines.length) {
|
|
146
|
+
const line = lines[i] ?? "";
|
|
147
|
+
|
|
148
|
+
if (isBlank(line)) {
|
|
149
|
+
i++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isHr(line)) {
|
|
154
|
+
parts.push("<hr>");
|
|
155
|
+
i++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Bullet list
|
|
160
|
+
if (isBullet(line)) {
|
|
161
|
+
const items: string[] = [];
|
|
162
|
+
while (i < lines.length && (isBullet(lines[i] ?? "") || (!isBlank(lines[i] ?? "") && /^[ \t]{2,}/.test(lines[i] ?? "")))) {
|
|
163
|
+
const cur = lines[i] ?? "";
|
|
164
|
+
if (isBullet(cur)) {
|
|
165
|
+
items.push(renderInline(applyHardBreak(getBulletContent(cur))));
|
|
166
|
+
}
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
parts.push(`<ul>\n${items.map((it) => ` <li>${it}</li>`).join("\n")}\n</ul>`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Ordered list
|
|
174
|
+
if (isOrdered(line)) {
|
|
175
|
+
const items: string[] = [];
|
|
176
|
+
while (i < lines.length && (isOrdered(lines[i] ?? "") || (!isBlank(lines[i] ?? "") && /^[ \t]{2,}/.test(lines[i] ?? "")))) {
|
|
177
|
+
const cur = lines[i] ?? "";
|
|
178
|
+
if (isOrdered(cur)) {
|
|
179
|
+
items.push(renderInline(applyHardBreak(getOrderedContent(cur))));
|
|
180
|
+
}
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
parts.push(`<ol>\n${items.map((it) => ` <li>${it}</li>`).join("\n")}\n</ol>`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Blockquote
|
|
188
|
+
if (isBlockquote(line)) {
|
|
189
|
+
const bqLines: string[] = [];
|
|
190
|
+
while (i < lines.length && (isBlockquote(lines[i] ?? "") || (!isBlank(lines[i] ?? "") && !/^[-*]/.test(lines[i] ?? "")))) {
|
|
191
|
+
const cur = lines[i] ?? "";
|
|
192
|
+
if (isBlockquote(cur)) {
|
|
193
|
+
bqLines.push(renderInline(applyHardBreak(getBlockquoteContent(cur))));
|
|
194
|
+
} else {
|
|
195
|
+
bqLines.push(renderInline(applyHardBreak(cur)));
|
|
196
|
+
}
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
parts.push(`<blockquote>${bqLines.join("<br>")}</blockquote>`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Paragraph — collect until blank or block-level
|
|
204
|
+
const paraLines: string[] = [];
|
|
205
|
+
while (i < lines.length && !isBlank(lines[i] ?? "") && !isHr(lines[i] ?? "") && !isBullet(lines[i] ?? "") && !isOrdered(lines[i] ?? "") && !isBlockquote(lines[i] ?? "")) {
|
|
206
|
+
paraLines.push(applyHardBreak(lines[i] ?? ""));
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
if (paraLines.length > 0) {
|
|
210
|
+
const inner = paraLines.map((l) => renderInline(l)).join("\n");
|
|
211
|
+
parts.push(`<p>${inner}</p>`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const result = parts.join("\n");
|
|
216
|
+
return restoreSafelist(result, safeMap);
|
|
217
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Block tree walker and dispatcher.
|
|
2
|
+
// src/render/blocks/render.ts
|
|
3
|
+
|
|
4
|
+
import type { Block } from "./types.ts";
|
|
5
|
+
import { renderHero } from "./renderers/hero.ts";
|
|
6
|
+
import { renderTldr } from "./renderers/tldr.ts";
|
|
7
|
+
import { renderSection } from "./renderers/section.ts";
|
|
8
|
+
import { renderProse } from "./renderers/prose.ts";
|
|
9
|
+
import { renderList } from "./renderers/list.ts";
|
|
10
|
+
import { renderCallout } from "./renderers/callout.ts";
|
|
11
|
+
import { renderCode } from "./renderers/code.ts";
|
|
12
|
+
import { renderTimeline } from "./renderers/timeline.ts";
|
|
13
|
+
import { renderCompareTable } from "./renderers/compare-table.ts";
|
|
14
|
+
import { renderRiskTable } from "./renderers/risk-table.ts";
|
|
15
|
+
import { renderKv } from "./renderers/kv.ts";
|
|
16
|
+
import { renderPillRow } from "./renderers/pill-row.ts";
|
|
17
|
+
import { renderDivider } from "./renderers/divider.ts";
|
|
18
|
+
import { renderDiagram } from "./renderers/diagram.ts";
|
|
19
|
+
import { renderRawHtml } from "./renderers/raw-html.ts";
|
|
20
|
+
|
|
21
|
+
/** Shared mutable counter — all section renderers increment this via the ctx ref. */
|
|
22
|
+
export interface SectionCounter {
|
|
23
|
+
value: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Shared render context threaded through the tree walk. */
|
|
27
|
+
export interface RenderCtx {
|
|
28
|
+
/** Shared auto-incrementing section counter (1-based). Mutable ref. */
|
|
29
|
+
sectionCounter: SectionCounter;
|
|
30
|
+
/** Current nesting depth (root = 0, inside section = 1, etc.). */
|
|
31
|
+
depth: number;
|
|
32
|
+
/** Path string for error messages (e.g. "blocks[2].children[1]"). */
|
|
33
|
+
path: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeRootCtx(): RenderCtx {
|
|
37
|
+
return {
|
|
38
|
+
sectionCounter: { value: 1 },
|
|
39
|
+
depth: 0,
|
|
40
|
+
path: "blocks",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Dispatch a single block to its renderer. */
|
|
45
|
+
export function renderBlock(block: Block, ctx: RenderCtx): string {
|
|
46
|
+
switch (block.type) {
|
|
47
|
+
case "hero":
|
|
48
|
+
return renderHero(block, ctx);
|
|
49
|
+
case "tldr":
|
|
50
|
+
return renderTldr(block, ctx);
|
|
51
|
+
case "section":
|
|
52
|
+
return renderSection(block, ctx);
|
|
53
|
+
case "prose":
|
|
54
|
+
return renderProse(block, ctx);
|
|
55
|
+
case "list":
|
|
56
|
+
return renderList(block, ctx);
|
|
57
|
+
case "callout":
|
|
58
|
+
return renderCallout(block, ctx);
|
|
59
|
+
case "code":
|
|
60
|
+
return renderCode(block, ctx);
|
|
61
|
+
case "timeline":
|
|
62
|
+
return renderTimeline(block, ctx);
|
|
63
|
+
case "compare_table":
|
|
64
|
+
return renderCompareTable(block, ctx);
|
|
65
|
+
case "risk_table":
|
|
66
|
+
return renderRiskTable(block, ctx);
|
|
67
|
+
case "kv":
|
|
68
|
+
return renderKv(block, ctx);
|
|
69
|
+
case "pill_row":
|
|
70
|
+
return renderPillRow(block, ctx);
|
|
71
|
+
case "divider":
|
|
72
|
+
return renderDivider(block, ctx);
|
|
73
|
+
case "diagram":
|
|
74
|
+
return renderDiagram(block, ctx);
|
|
75
|
+
case "raw_html":
|
|
76
|
+
return renderRawHtml(block, ctx);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Render an array of blocks, returning the concatenated HTML body string. */
|
|
81
|
+
export function renderBlocks(blocks: Block[], opts?: { title?: string }): string {
|
|
82
|
+
const ctx = makeRootCtx();
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
85
|
+
const block = blocks[i];
|
|
86
|
+
if (block === undefined) continue;
|
|
87
|
+
const blockCtx: RenderCtx = {
|
|
88
|
+
...ctx,
|
|
89
|
+
path: `blocks[${i}]`,
|
|
90
|
+
};
|
|
91
|
+
parts.push(renderBlock(block, blockCtx));
|
|
92
|
+
}
|
|
93
|
+
// Unused opts.title kept for API compatibility; wrapDocument handles the title
|
|
94
|
+
void opts;
|
|
95
|
+
return parts.join("\n");
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Callout block renderer.
|
|
2
|
+
// src/render/blocks/renderers/callout.ts
|
|
3
|
+
|
|
4
|
+
import type { CalloutBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeHtml } from "../escape.ts";
|
|
8
|
+
import { renderMarkdown } from "../markdown.ts";
|
|
9
|
+
|
|
10
|
+
export function renderCallout(block: CalloutBlock, _ctx: RenderCtx): string {
|
|
11
|
+
const titleHtml =
|
|
12
|
+
block.title !== undefined && block.title !== ""
|
|
13
|
+
? `<strong>${escapeHtml(block.title)}</strong> `
|
|
14
|
+
: "";
|
|
15
|
+
const contentHtml = renderMarkdown(block.markdown);
|
|
16
|
+
return `<aside class="callout ${block.variant}">\n${titleHtml}${contentHtml}\n</aside>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const meta: BlockMeta = {
|
|
20
|
+
type: "callout",
|
|
21
|
+
description: "Highlighted aside with variant styling. Variants: note, warn, risk.",
|
|
22
|
+
schema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
type: { const: "callout" },
|
|
26
|
+
variant: { type: "string", enum: ["note", "warn", "risk"] },
|
|
27
|
+
title: { type: "string" },
|
|
28
|
+
markdown: { type: "string" },
|
|
29
|
+
},
|
|
30
|
+
required: ["type", "variant", "markdown"],
|
|
31
|
+
},
|
|
32
|
+
example: {
|
|
33
|
+
type: "callout",
|
|
34
|
+
variant: "warn",
|
|
35
|
+
title: "Breaking Change",
|
|
36
|
+
markdown: "This change removes the `html` fallback path. Migrate to `blocks` before upgrading.",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Code block renderer.
|
|
2
|
+
// src/render/blocks/renderers/code.ts
|
|
3
|
+
|
|
4
|
+
import type { CodeBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeHtml, escapeAttr } from "../escape.ts";
|
|
8
|
+
|
|
9
|
+
export function renderCode(block: CodeBlock, _ctx: RenderCtx): string {
|
|
10
|
+
const parts: string[] = [];
|
|
11
|
+
|
|
12
|
+
const captionText = block.filename ?? block.caption;
|
|
13
|
+
if (captionText !== undefined && captionText !== "") {
|
|
14
|
+
parts.push(` <figcaption>${escapeHtml(captionText)}</figcaption>`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parts.push(
|
|
18
|
+
` <pre><code class="lang-${escapeAttr(block.lang)}">${escapeHtml(block.code)}</code></pre>`,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return `<figure class="code">\n${parts.join("\n")}\n</figure>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const meta: BlockMeta = {
|
|
25
|
+
type: "code",
|
|
26
|
+
description: "Code block with syntax language label and optional filename/caption.",
|
|
27
|
+
schema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
type: { const: "code" },
|
|
31
|
+
lang: { type: "string" },
|
|
32
|
+
code: { type: "string" },
|
|
33
|
+
filename: { type: "string" },
|
|
34
|
+
caption: { type: "string" },
|
|
35
|
+
},
|
|
36
|
+
required: ["type", "lang", "code"],
|
|
37
|
+
},
|
|
38
|
+
example: {
|
|
39
|
+
type: "code",
|
|
40
|
+
lang: "typescript",
|
|
41
|
+
filename: "src/index.ts",
|
|
42
|
+
code: 'import { createPublishTool } from "./tools/publish.ts";\nexport { createPublishTool };',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// CompareTable block renderer.
|
|
2
|
+
// src/render/blocks/renderers/compare-table.ts
|
|
3
|
+
|
|
4
|
+
import type { CompareTableBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeHtml } from "../escape.ts";
|
|
8
|
+
import { renderMarkdown } from "../markdown.ts";
|
|
9
|
+
|
|
10
|
+
export function renderCompareTable(block: CompareTableBlock, _ctx: RenderCtx): string {
|
|
11
|
+
const headerCells = block.headers
|
|
12
|
+
.map((h) => ` <th>${escapeHtml(h)}</th>`)
|
|
13
|
+
.join("\n");
|
|
14
|
+
|
|
15
|
+
const bodyRows = block.rows
|
|
16
|
+
.map((row) => {
|
|
17
|
+
const cells = row
|
|
18
|
+
.map((cell) => {
|
|
19
|
+
const content = renderMarkdown(cell).replace(/^<p>|<\/p>$/g, "");
|
|
20
|
+
return ` <td>${content}</td>`;
|
|
21
|
+
})
|
|
22
|
+
.join("\n");
|
|
23
|
+
return ` <tr>\n${cells}\n </tr>`;
|
|
24
|
+
})
|
|
25
|
+
.join("\n");
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
`<table class="compare-table">\n` +
|
|
29
|
+
` <thead>\n <tr>\n${headerCells}\n </tr>\n </thead>\n` +
|
|
30
|
+
` <tbody>\n${bodyRows}\n </tbody>\n` +
|
|
31
|
+
`</table>`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const meta: BlockMeta = {
|
|
36
|
+
type: "compare_table",
|
|
37
|
+
description: "Bordered comparison grid. Headers define columns; rows must have matching cell count.",
|
|
38
|
+
schema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
type: { const: "compare_table" },
|
|
42
|
+
headers: { type: "array", items: { type: "string" } },
|
|
43
|
+
rows: { type: "array", items: { type: "array", items: { type: "string" } } },
|
|
44
|
+
},
|
|
45
|
+
required: ["type", "headers", "rows"],
|
|
46
|
+
},
|
|
47
|
+
example: {
|
|
48
|
+
type: "compare_table",
|
|
49
|
+
headers: ["Feature", "html mode", "blocks mode"],
|
|
50
|
+
rows: [
|
|
51
|
+
["Token cost", "High", "**~2× lower**"],
|
|
52
|
+
["Type safety", "None", "Full"],
|
|
53
|
+
["Escape hatch", "`raw_html`", "`raw_html`"],
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Diagram block renderer — escape-hatch for inline SVG or HTML diagrams.
|
|
2
|
+
// src/render/blocks/renderers/diagram.ts
|
|
3
|
+
|
|
4
|
+
import type { DiagramBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeHtml } from "../escape.ts";
|
|
8
|
+
import { scrub } from "../../scrub.ts";
|
|
9
|
+
|
|
10
|
+
export function renderDiagram(block: DiagramBlock, _ctx: RenderCtx): string {
|
|
11
|
+
const payload = block.svg ?? block.html ?? "";
|
|
12
|
+
const scrubResult = scrub(payload);
|
|
13
|
+
const scrubbed = scrubResult.html;
|
|
14
|
+
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
parts.push(scrubbed);
|
|
17
|
+
|
|
18
|
+
if (block.caption !== undefined && block.caption !== "") {
|
|
19
|
+
parts.push(`<figcaption>${escapeHtml(block.caption)}</figcaption>`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `<figure class="diagram">\n${parts.join("\n")}\n</figure>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const meta: BlockMeta = {
|
|
26
|
+
type: "diagram",
|
|
27
|
+
description:
|
|
28
|
+
"Escape-hatch for inline SVG or bespoke HTML diagrams. Exactly one of svg or html required. Payload is scrubbed. For SVG, prefer fill=\"currentColor\" and stroke=\"currentColor\" so the diagram inherits the theme's text color. Use explicit colors only for emphasis (accents, warnings).",
|
|
29
|
+
schema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
type: { const: "diagram" },
|
|
33
|
+
caption: { type: "string" },
|
|
34
|
+
svg: { type: "string" },
|
|
35
|
+
html: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
required: ["type"],
|
|
38
|
+
oneOf: [
|
|
39
|
+
{ required: ["svg"] },
|
|
40
|
+
{ required: ["html"] },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
example: {
|
|
44
|
+
type: "diagram",
|
|
45
|
+
caption: "System architecture overview",
|
|
46
|
+
svg: '<svg viewBox="0 0 100 50" xmlns="http://www.w3.org/2000/svg"><rect x="10" y="10" width="80" height="30" rx="4" fill="none" stroke="#888"/><text x="50" y="30" text-anchor="middle" font-size="12">cesium</text></svg>',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Divider block renderer.
|
|
2
|
+
// src/render/blocks/renderers/divider.ts
|
|
3
|
+
|
|
4
|
+
import type { DividerBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeAttr } from "../escape.ts";
|
|
8
|
+
|
|
9
|
+
export function renderDivider(block: DividerBlock, _ctx: RenderCtx): string {
|
|
10
|
+
if (block.label !== undefined && block.label !== "") {
|
|
11
|
+
return `<hr data-label="${escapeAttr(block.label)}">`;
|
|
12
|
+
}
|
|
13
|
+
return `<hr>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const meta: BlockMeta = {
|
|
17
|
+
type: "divider",
|
|
18
|
+
description: "Horizontal rule separator, with an optional text label.",
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
type: { const: "divider" },
|
|
23
|
+
label: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["type"],
|
|
26
|
+
},
|
|
27
|
+
example: {
|
|
28
|
+
type: "divider",
|
|
29
|
+
label: "End of Phase 1",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Hero block renderer.
|
|
2
|
+
// src/render/blocks/renderers/hero.ts
|
|
3
|
+
|
|
4
|
+
import type { HeroBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeHtml } from "../escape.ts";
|
|
8
|
+
|
|
9
|
+
export function renderHero(block: HeroBlock, _ctx: RenderCtx): string {
|
|
10
|
+
const parts: string[] = [];
|
|
11
|
+
|
|
12
|
+
if (block.eyebrow !== undefined && block.eyebrow !== "") {
|
|
13
|
+
parts.push(` <div class="eyebrow">${escapeHtml(block.eyebrow)}</div>`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
parts.push(` <h1 class="h-display">${escapeHtml(block.title)}</h1>`);
|
|
17
|
+
|
|
18
|
+
if (block.subtitle !== undefined && block.subtitle !== "") {
|
|
19
|
+
parts.push(` <p class="lede">${escapeHtml(block.subtitle)}</p>`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (block.meta !== undefined && block.meta.length > 0) {
|
|
23
|
+
const rows = block.meta
|
|
24
|
+
.map((row) => ` <dt>${escapeHtml(row.k)}</dt><dd>${escapeHtml(row.v)}</dd>`)
|
|
25
|
+
.join("\n");
|
|
26
|
+
parts.push(` <dl class="kv">\n${rows}\n </dl>`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return `<header>\n${parts.join("\n")}\n</header>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const meta: BlockMeta = {
|
|
33
|
+
type: "hero",
|
|
34
|
+
description: "Page title header with optional eyebrow, subtitle, and key-value metadata pairs.",
|
|
35
|
+
schema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
type: { const: "hero" },
|
|
39
|
+
eyebrow: { type: "string" },
|
|
40
|
+
title: { type: "string" },
|
|
41
|
+
subtitle: { type: "string" },
|
|
42
|
+
meta: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
k: { type: "string" },
|
|
48
|
+
v: { type: "string" },
|
|
49
|
+
},
|
|
50
|
+
required: ["k", "v"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ["type", "title"],
|
|
55
|
+
},
|
|
56
|
+
example: {
|
|
57
|
+
type: "hero",
|
|
58
|
+
eyebrow: "Phase 2",
|
|
59
|
+
title: "Block Mode Design",
|
|
60
|
+
subtitle: "Structured input for cesium_publish",
|
|
61
|
+
meta: [
|
|
62
|
+
{ k: "Status", v: "Draft" },
|
|
63
|
+
{ k: "Author", v: "AI" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// KV block renderer.
|
|
2
|
+
// src/render/blocks/renderers/kv.ts
|
|
3
|
+
|
|
4
|
+
import type { KvBlock } from "../types.ts";
|
|
5
|
+
import type { BlockMeta } from "../types.ts";
|
|
6
|
+
import type { RenderCtx } from "../render.ts";
|
|
7
|
+
import { escapeHtml } from "../escape.ts";
|
|
8
|
+
|
|
9
|
+
export function renderKv(block: KvBlock, _ctx: RenderCtx): string {
|
|
10
|
+
const rows = block.rows
|
|
11
|
+
.map((row) => ` <dt>${escapeHtml(row.k)}</dt><dd>${escapeHtml(row.v)}</dd>`)
|
|
12
|
+
.join("\n");
|
|
13
|
+
return `<dl class="kv">\n${rows}\n</dl>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const meta: BlockMeta = {
|
|
17
|
+
type: "kv",
|
|
18
|
+
description: "Key-value metadata list. Renders as a definition list.",
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
type: { const: "kv" },
|
|
23
|
+
rows: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
k: { type: "string" },
|
|
29
|
+
v: { type: "string" },
|
|
30
|
+
},
|
|
31
|
+
required: ["k", "v"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ["type", "rows"],
|
|
36
|
+
},
|
|
37
|
+
example: {
|
|
38
|
+
type: "kv",
|
|
39
|
+
rows: [
|
|
40
|
+
{ k: "Author", v: "AI Agent" },
|
|
41
|
+
{ k: "Status", v: "Draft" },
|
|
42
|
+
{ k: "Version", v: "2.0.0" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
};
|