@cfbender/cesium 0.5.1 → 0.6.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 +97 -3
- package/README.md +8 -8
- package/package.json +19 -17
- package/src/cli/commands/ls.ts +62 -65
- package/src/cli/commands/open.ts +47 -62
- package/src/cli/commands/prune.ts +59 -71
- package/src/cli/commands/restart.ts +100 -12
- package/src/cli/commands/serve.ts +119 -116
- package/src/cli/commands/stop.ts +51 -84
- package/src/cli/commands/theme.ts +54 -92
- package/src/cli/index.ts +17 -70
- package/src/index.ts +4 -1
- package/src/prompt/field-reference.ts +2 -2
- package/src/prompt/system-fragment.md +46 -16
- package/src/render/blocks/catalog.ts +2 -0
- package/src/render/blocks/diff/myers.ts +221 -0
- package/src/render/blocks/diff/parse-unified.ts +101 -0
- package/src/render/blocks/highlight.ts +8 -11
- package/src/render/blocks/markdown.ts +28 -7
- package/src/render/blocks/render.ts +3 -0
- package/src/render/blocks/renderers/code.ts +1 -3
- package/src/render/blocks/renderers/compare-table.ts +3 -4
- package/src/render/blocks/renderers/diagram.ts +2 -5
- package/src/render/blocks/renderers/diff.ts +378 -0
- package/src/render/blocks/renderers/prose.ts +1 -2
- package/src/render/blocks/renderers/timeline.ts +2 -1
- package/src/render/blocks/themes/claret-dark.ts +1 -6
- package/src/render/blocks/themes/claret-light.ts +1 -6
- package/src/render/blocks/types.ts +13 -1
- package/src/render/blocks/validate-block.ts +19 -9
- package/src/render/theme.ts +149 -0
- package/src/render/validate.ts +53 -9
- package/src/server/api.ts +112 -124
- package/src/server/favicon.ts +8 -16
- package/src/server/http.ts +101 -106
- package/src/server/lifecycle.ts +12 -6
- package/src/storage/assets.ts +8 -10
- package/src/storage/index-gen.ts +2 -3
- package/src/storage/theme-write.ts +17 -3
- package/src/tools/publish.ts +1 -3
- package/src/tools/styleguide.ts +3 -7
- package/src/tools/wait.ts +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Myers O(ND) line-level diff algorithm.
|
|
2
|
+
// src/render/blocks/diff/myers.ts
|
|
3
|
+
//
|
|
4
|
+
// Re-implemented from scratch — no external dependencies.
|
|
5
|
+
// Reference: "An O(ND) Difference Algorithm and Its Variations" — Eugene W. Myers (1986).
|
|
6
|
+
//
|
|
7
|
+
// Implementation follows the standard "trace + backtrack" pattern from:
|
|
8
|
+
// https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
|
|
9
|
+
|
|
10
|
+
import type { DiffLine } from "./parse-unified.ts";
|
|
11
|
+
|
|
12
|
+
export type { DiffLine };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split a string into lines, trimming a single trailing empty element when the
|
|
16
|
+
* input ends with "\n" (so clean files don't produce a phantom blank final line).
|
|
17
|
+
*/
|
|
18
|
+
function splitLines(text: string): string[] {
|
|
19
|
+
if (text === "") return [];
|
|
20
|
+
const lines = text.split("\n");
|
|
21
|
+
// Trim trailing phantom empty line produced by a trailing newline
|
|
22
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
23
|
+
lines.pop();
|
|
24
|
+
}
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Myers forward pass ───────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run the Myers forward pass and return the trace (one v[] snapshot per d).
|
|
32
|
+
* v[k + offset] = furthest row reached on diagonal k at this step.
|
|
33
|
+
*/
|
|
34
|
+
function myersTrace(a: string[], b: string[]): number[][] {
|
|
35
|
+
const N = a.length;
|
|
36
|
+
const M = b.length;
|
|
37
|
+
const MAX = N + M;
|
|
38
|
+
const offset = MAX;
|
|
39
|
+
|
|
40
|
+
const v: number[] = Array.from({ length: 2 * MAX + 2 }, () => 0);
|
|
41
|
+
v[offset + 1] = 0;
|
|
42
|
+
|
|
43
|
+
const trace: number[][] = [];
|
|
44
|
+
|
|
45
|
+
for (let d = 0; d <= MAX; d++) {
|
|
46
|
+
for (let k = -d; k <= d; k += 2) {
|
|
47
|
+
const ki = k + offset;
|
|
48
|
+
const vKm1 = v[ki - 1] ?? 0;
|
|
49
|
+
const vKp1 = v[ki + 1] ?? 0;
|
|
50
|
+
|
|
51
|
+
let x: number;
|
|
52
|
+
if (k === -d || (k !== d && vKm1 < vKp1)) {
|
|
53
|
+
x = vKp1; // down: insert
|
|
54
|
+
} else {
|
|
55
|
+
x = vKm1 + 1; // right: delete
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let y = x - k;
|
|
59
|
+
|
|
60
|
+
// Follow the diagonal (matches)
|
|
61
|
+
while (x < N && y < M && a[x] === b[y]) {
|
|
62
|
+
x++;
|
|
63
|
+
y++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
v[ki] = x;
|
|
67
|
+
|
|
68
|
+
if (x >= N && y >= M) {
|
|
69
|
+
trace.push(v.slice());
|
|
70
|
+
return trace;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
trace.push(v.slice());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return trace;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Myers backtrack ──────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
type Edit =
|
|
83
|
+
| { op: "keep"; aIdx: number; bIdx: number }
|
|
84
|
+
| { op: "delete"; aIdx: number }
|
|
85
|
+
| { op: "insert"; bIdx: number };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Backtrack through the trace to produce the edit script.
|
|
89
|
+
* Reconstructs the path from (N, M) back to (0, 0).
|
|
90
|
+
*/
|
|
91
|
+
function backtrack(a: string[], b: string[], trace: number[][]): Edit[] {
|
|
92
|
+
const MAX = a.length + b.length;
|
|
93
|
+
const offset = MAX;
|
|
94
|
+
const edits: Edit[] = [];
|
|
95
|
+
|
|
96
|
+
let x = a.length;
|
|
97
|
+
let y = b.length;
|
|
98
|
+
|
|
99
|
+
// Walk backwards through d steps
|
|
100
|
+
for (let d = trace.length - 1; d > 0; d--) {
|
|
101
|
+
const vPrev = trace[d - 1] ?? [];
|
|
102
|
+
const k = x - y;
|
|
103
|
+
const ki = k + offset;
|
|
104
|
+
|
|
105
|
+
const vPrevKm1 = vPrev[ki - 1] ?? 0;
|
|
106
|
+
const vPrevKp1 = vPrev[ki + 1] ?? 0;
|
|
107
|
+
|
|
108
|
+
// Determine which diagonal we came from
|
|
109
|
+
let prevK: number;
|
|
110
|
+
if (k === -d || (k !== d && vPrevKm1 < vPrevKp1)) {
|
|
111
|
+
prevK = k + 1; // came via down (insert)
|
|
112
|
+
} else {
|
|
113
|
+
prevK = k - 1; // came via right (delete)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const prevX = vPrev[prevK + offset] ?? 0;
|
|
117
|
+
const prevY = prevX - prevK;
|
|
118
|
+
|
|
119
|
+
// Retrace the snake (diagonal matches) from (prevX, prevY) to (x, y)
|
|
120
|
+
// but skip the single edit step itself.
|
|
121
|
+
// After the edit step, we were at:
|
|
122
|
+
// insert: (prevX, prevY + 1)
|
|
123
|
+
// delete: (prevX + 1, prevY)
|
|
124
|
+
if (prevK === k + 1) {
|
|
125
|
+
// insert: moved down, x stays same, y += 1
|
|
126
|
+
const snakeX = prevX;
|
|
127
|
+
// snake goes from (snakeX, prevY+1) to (x, y)
|
|
128
|
+
for (let sx = x - 1; sx >= snakeX; sx--) {
|
|
129
|
+
const sy = sx - k;
|
|
130
|
+
edits.push({ op: "keep", aIdx: sx, bIdx: sy });
|
|
131
|
+
}
|
|
132
|
+
edits.push({ op: "insert", bIdx: prevY });
|
|
133
|
+
} else {
|
|
134
|
+
// delete: moved right, x += 1, y stays same
|
|
135
|
+
const snakeX = prevX + 1;
|
|
136
|
+
// snake goes from (snakeX, prevY) to (x, y)
|
|
137
|
+
for (let sx = x - 1; sx >= snakeX; sx--) {
|
|
138
|
+
const sy = sx - k;
|
|
139
|
+
edits.push({ op: "keep", aIdx: sx, bIdx: sy });
|
|
140
|
+
}
|
|
141
|
+
edits.push({ op: "delete", aIdx: prevX });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
x = prevX;
|
|
145
|
+
y = prevY;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remaining snake at d=0: (0,0) to (x,y)
|
|
149
|
+
for (let sx = x - 1; sx >= 0; sx--) {
|
|
150
|
+
const sy = sx; // k=0 at the start
|
|
151
|
+
edits.push({ op: "keep", aIdx: sx, bIdx: sy });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
edits.reverse();
|
|
155
|
+
return edits;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function myersDiff(a: string[], b: string[]): Edit[] {
|
|
159
|
+
if (a.length === 0 && b.length === 0) return [];
|
|
160
|
+
if (a.length === 0) return b.map((_, i) => ({ op: "insert" as const, bIdx: i }));
|
|
161
|
+
if (b.length === 0) return a.map((_, i) => ({ op: "delete" as const, aIdx: i }));
|
|
162
|
+
|
|
163
|
+
const trace = myersTrace(a, b);
|
|
164
|
+
return backtrack(a, b, trace);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compute a line-level diff between two text strings.
|
|
171
|
+
* Returns a flat DiffLine[] with 1-indexed line numbers per side.
|
|
172
|
+
* No hunk-sep entries — single contiguous diff.
|
|
173
|
+
*/
|
|
174
|
+
export function diffLines(before: string, after: string): DiffLine[] {
|
|
175
|
+
const aLines = splitLines(before);
|
|
176
|
+
const bLines = splitLines(after);
|
|
177
|
+
|
|
178
|
+
const edits = myersDiff(aLines, bLines);
|
|
179
|
+
|
|
180
|
+
let beforeLine = 1;
|
|
181
|
+
let afterLine = 1;
|
|
182
|
+
|
|
183
|
+
return edits.map((edit): DiffLine => {
|
|
184
|
+
switch (edit.op) {
|
|
185
|
+
case "keep": {
|
|
186
|
+
const text = aLines[edit.aIdx] ?? "";
|
|
187
|
+
const entry: DiffLine = {
|
|
188
|
+
kind: "context",
|
|
189
|
+
text,
|
|
190
|
+
beforeLineNum: beforeLine,
|
|
191
|
+
afterLineNum: afterLine,
|
|
192
|
+
};
|
|
193
|
+
beforeLine++;
|
|
194
|
+
afterLine++;
|
|
195
|
+
return entry;
|
|
196
|
+
}
|
|
197
|
+
case "delete": {
|
|
198
|
+
const text = aLines[edit.aIdx] ?? "";
|
|
199
|
+
const entry: DiffLine = {
|
|
200
|
+
kind: "remove",
|
|
201
|
+
text,
|
|
202
|
+
beforeLineNum: beforeLine,
|
|
203
|
+
afterLineNum: null,
|
|
204
|
+
};
|
|
205
|
+
beforeLine++;
|
|
206
|
+
return entry;
|
|
207
|
+
}
|
|
208
|
+
case "insert": {
|
|
209
|
+
const text = bLines[edit.bIdx] ?? "";
|
|
210
|
+
const entry: DiffLine = {
|
|
211
|
+
kind: "add",
|
|
212
|
+
text,
|
|
213
|
+
beforeLineNum: null,
|
|
214
|
+
afterLineNum: afterLine,
|
|
215
|
+
};
|
|
216
|
+
afterLine++;
|
|
217
|
+
return entry;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Unified diff parser — converts a unified patch string to a DiffEntry[].
|
|
2
|
+
// src/render/blocks/diff/parse-unified.ts
|
|
3
|
+
|
|
4
|
+
export type DiffLine = {
|
|
5
|
+
kind: "context" | "add" | "remove";
|
|
6
|
+
text: string; // raw text, no leading +/- prefix
|
|
7
|
+
beforeLineNum: number | null; // 1-indexed line in "before" file, null for adds
|
|
8
|
+
afterLineNum: number | null; // 1-indexed line in "after" file, null for removes
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DiffEntry = DiffLine | { kind: "hunk-sep"; oldStart: number; newStart: number };
|
|
12
|
+
|
|
13
|
+
// Matches: @@ -<oldStart>[,<oldLines>] +<newStart>[,<newLines>] @@
|
|
14
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a unified diff patch string into a DiffEntry[].
|
|
18
|
+
* Returns null if no hunk header is found (caller falls back to plaintext).
|
|
19
|
+
*/
|
|
20
|
+
export function parseUnifiedDiff(patch: string): DiffEntry[] | null {
|
|
21
|
+
const lines = patch.split("\n");
|
|
22
|
+
const result: DiffEntry[] = [];
|
|
23
|
+
let foundHunk = false;
|
|
24
|
+
let firstHunk = true;
|
|
25
|
+
|
|
26
|
+
// Counters for current hunk position
|
|
27
|
+
let beforeLine = 0;
|
|
28
|
+
let afterLine = 0;
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
// Skip file header lines (--- / +++ at the very top)
|
|
32
|
+
if (line.startsWith("--- ") || line.startsWith("+++ ")) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for hunk header
|
|
37
|
+
const hunkMatch = HUNK_HEADER.exec(line);
|
|
38
|
+
if (hunkMatch !== null) {
|
|
39
|
+
const oldStart = parseInt(hunkMatch[1] ?? "1", 10);
|
|
40
|
+
const newStart = parseInt(hunkMatch[3] ?? "1", 10);
|
|
41
|
+
|
|
42
|
+
if (!firstHunk) {
|
|
43
|
+
// Emit hunk separator between hunks
|
|
44
|
+
result.push({ kind: "hunk-sep", oldStart, newStart });
|
|
45
|
+
}
|
|
46
|
+
firstHunk = false;
|
|
47
|
+
foundHunk = true;
|
|
48
|
+
|
|
49
|
+
beforeLine = oldStart;
|
|
50
|
+
afterLine = newStart;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!foundHunk) {
|
|
55
|
+
// Haven't seen a hunk header yet — skip pre-header lines
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Skip "" markers
|
|
60
|
+
if (line.startsWith("\\")) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const prefix = line[0];
|
|
65
|
+
const text = line.slice(1);
|
|
66
|
+
|
|
67
|
+
if (prefix === " ") {
|
|
68
|
+
// Context line — appears on both sides
|
|
69
|
+
result.push({
|
|
70
|
+
kind: "context",
|
|
71
|
+
text,
|
|
72
|
+
beforeLineNum: beforeLine,
|
|
73
|
+
afterLineNum: afterLine,
|
|
74
|
+
});
|
|
75
|
+
beforeLine++;
|
|
76
|
+
afterLine++;
|
|
77
|
+
} else if (prefix === "-") {
|
|
78
|
+
// Removed line — only in "before"
|
|
79
|
+
result.push({
|
|
80
|
+
kind: "remove",
|
|
81
|
+
text,
|
|
82
|
+
beforeLineNum: beforeLine,
|
|
83
|
+
afterLineNum: null,
|
|
84
|
+
});
|
|
85
|
+
beforeLine++;
|
|
86
|
+
} else if (prefix === "+") {
|
|
87
|
+
// Added line — only in "after"
|
|
88
|
+
result.push({
|
|
89
|
+
kind: "add",
|
|
90
|
+
text,
|
|
91
|
+
beforeLineNum: null,
|
|
92
|
+
afterLineNum: afterLine,
|
|
93
|
+
});
|
|
94
|
+
afterLine++;
|
|
95
|
+
}
|
|
96
|
+
// Any other prefix: skip (e.g. empty lines after hunk body)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!foundHunk) return null;
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
@@ -13,11 +13,7 @@ import { THEME_PRESETS, isThemePresetName } from "../../render/theme.ts";
|
|
|
13
13
|
|
|
14
14
|
// ─── Highlight theme type ─────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
export type HighlightTheme =
|
|
17
|
-
| "claret-dark"
|
|
18
|
-
| "claret-light"
|
|
19
|
-
| "vitesse-dark"
|
|
20
|
-
| "vitesse-light";
|
|
16
|
+
export type HighlightTheme = "claret-dark" | "claret-light" | "vitesse-dark" | "vitesse-light";
|
|
21
17
|
|
|
22
18
|
// ─── Supported languages ─────────────────────────────────────────────────────
|
|
23
19
|
|
|
@@ -88,12 +84,13 @@ export function resolveHighlightTheme(cesiumThemeName: string | undefined): High
|
|
|
88
84
|
*/
|
|
89
85
|
function isHexDark(hex: string): boolean {
|
|
90
86
|
const clean = hex.replace("#", "");
|
|
91
|
-
const full =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
const full =
|
|
88
|
+
clean.length === 3
|
|
89
|
+
? clean
|
|
90
|
+
.split("")
|
|
91
|
+
.map((c) => c + c)
|
|
92
|
+
.join("")
|
|
93
|
+
: clean;
|
|
97
94
|
const r = parseInt(full.slice(0, 2), 16);
|
|
98
95
|
const g = parseInt(full.slice(2, 4), 16);
|
|
99
96
|
const b = parseInt(full.slice(4, 6), 16);
|
|
@@ -11,8 +11,7 @@ import { escapeHtml, escapeAttr } from "./escape.ts";
|
|
|
11
11
|
|
|
12
12
|
// ─── Safelist placeholder pass ───────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
-
const SAFELIST_RE =
|
|
15
|
-
/<(kbd)>(.*?)<\/kbd>|<span\s+class="(pill|tag)">(.*?)<\/span>/gi;
|
|
14
|
+
const SAFELIST_RE = /<(kbd)>(.*?)<\/kbd>|<span\s+class="(pill|tag)">(.*?)<\/span>/gi;
|
|
16
15
|
|
|
17
16
|
function extractSafelist(input: string): { text: string; map: Map<string, string> } {
|
|
18
17
|
const map = new Map<string, string>();
|
|
@@ -72,7 +71,10 @@ function renderInline(text: string): string {
|
|
|
72
71
|
// **bold**
|
|
73
72
|
working = working.replace(/\*\*(.+?)\*\*/g, (_m, inner: string) => `<strong>${inner}</strong>`);
|
|
74
73
|
// *italic* (not preceded by another *)
|
|
75
|
-
working = working.replace(
|
|
74
|
+
working = working.replace(
|
|
75
|
+
/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,
|
|
76
|
+
(_m, inner: string) => `<em>${inner}</em>`,
|
|
77
|
+
);
|
|
76
78
|
// [text](href) — only relative/anchor hrefs; external → plain text
|
|
77
79
|
working = working.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, linkText: string, href: string) => {
|
|
78
80
|
if (RELATIVE_HREF_RE.test(href)) {
|
|
@@ -159,7 +161,11 @@ export function renderMarkdown(md: string): string {
|
|
|
159
161
|
// Bullet list
|
|
160
162
|
if (isBullet(line)) {
|
|
161
163
|
const items: string[] = [];
|
|
162
|
-
while (
|
|
164
|
+
while (
|
|
165
|
+
i < lines.length &&
|
|
166
|
+
(isBullet(lines[i] ?? "") ||
|
|
167
|
+
(!isBlank(lines[i] ?? "") && /^[ \t]{2,}/.test(lines[i] ?? "")))
|
|
168
|
+
) {
|
|
163
169
|
const cur = lines[i] ?? "";
|
|
164
170
|
if (isBullet(cur)) {
|
|
165
171
|
items.push(renderInline(applyHardBreak(getBulletContent(cur))));
|
|
@@ -173,7 +179,11 @@ export function renderMarkdown(md: string): string {
|
|
|
173
179
|
// Ordered list
|
|
174
180
|
if (isOrdered(line)) {
|
|
175
181
|
const items: string[] = [];
|
|
176
|
-
while (
|
|
182
|
+
while (
|
|
183
|
+
i < lines.length &&
|
|
184
|
+
(isOrdered(lines[i] ?? "") ||
|
|
185
|
+
(!isBlank(lines[i] ?? "") && /^[ \t]{2,}/.test(lines[i] ?? "")))
|
|
186
|
+
) {
|
|
177
187
|
const cur = lines[i] ?? "";
|
|
178
188
|
if (isOrdered(cur)) {
|
|
179
189
|
items.push(renderInline(applyHardBreak(getOrderedContent(cur))));
|
|
@@ -187,7 +197,11 @@ export function renderMarkdown(md: string): string {
|
|
|
187
197
|
// Blockquote
|
|
188
198
|
if (isBlockquote(line)) {
|
|
189
199
|
const bqLines: string[] = [];
|
|
190
|
-
while (
|
|
200
|
+
while (
|
|
201
|
+
i < lines.length &&
|
|
202
|
+
(isBlockquote(lines[i] ?? "") ||
|
|
203
|
+
(!isBlank(lines[i] ?? "") && !/^[-*]/.test(lines[i] ?? "")))
|
|
204
|
+
) {
|
|
191
205
|
const cur = lines[i] ?? "";
|
|
192
206
|
if (isBlockquote(cur)) {
|
|
193
207
|
bqLines.push(renderInline(applyHardBreak(getBlockquoteContent(cur))));
|
|
@@ -202,7 +216,14 @@ export function renderMarkdown(md: string): string {
|
|
|
202
216
|
|
|
203
217
|
// Paragraph — collect until blank or block-level
|
|
204
218
|
const paraLines: string[] = [];
|
|
205
|
-
while (
|
|
219
|
+
while (
|
|
220
|
+
i < lines.length &&
|
|
221
|
+
!isBlank(lines[i] ?? "") &&
|
|
222
|
+
!isHr(lines[i] ?? "") &&
|
|
223
|
+
!isBullet(lines[i] ?? "") &&
|
|
224
|
+
!isOrdered(lines[i] ?? "") &&
|
|
225
|
+
!isBlockquote(lines[i] ?? "")
|
|
226
|
+
) {
|
|
206
227
|
paraLines.push(applyHardBreak(lines[i] ?? ""));
|
|
207
228
|
i++;
|
|
208
229
|
}
|
|
@@ -18,6 +18,7 @@ import { renderPillRow } from "./renderers/pill-row.ts";
|
|
|
18
18
|
import { renderDivider } from "./renderers/divider.ts";
|
|
19
19
|
import { renderDiagram } from "./renderers/diagram.ts";
|
|
20
20
|
import { renderRawHtml } from "./renderers/raw-html.ts";
|
|
21
|
+
import { renderDiff } from "./renderers/diff.ts";
|
|
21
22
|
|
|
22
23
|
/** Shared mutable counter — all section renderers increment this via the ctx ref. */
|
|
23
24
|
export interface SectionCounter {
|
|
@@ -78,6 +79,8 @@ export async function renderBlock(block: Block, ctx: RenderCtx): Promise<string>
|
|
|
78
79
|
return renderDiagram(block, ctx);
|
|
79
80
|
case "raw_html":
|
|
80
81
|
return renderRawHtml(block, ctx);
|
|
82
|
+
case "diff":
|
|
83
|
+
return renderDiff(block, ctx);
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -16,9 +16,7 @@ export async function renderCode(block: CodeBlock, ctx: RenderCtx): Promise<stri
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const highlighted = await highlightCode(block.code, block.lang, ctx.highlightTheme);
|
|
19
|
-
parts.push(
|
|
20
|
-
` <pre><code class="lang-${escapeAttr(block.lang)}">${highlighted}</code></pre>`,
|
|
21
|
-
);
|
|
19
|
+
parts.push(` <pre><code class="lang-${escapeAttr(block.lang)}">${highlighted}</code></pre>`);
|
|
22
20
|
|
|
23
21
|
return `<figure class="code">\n${parts.join("\n")}\n</figure>`;
|
|
24
22
|
}
|
|
@@ -8,9 +8,7 @@ import { escapeHtml } from "../escape.ts";
|
|
|
8
8
|
import { renderMarkdown } from "../markdown.ts";
|
|
9
9
|
|
|
10
10
|
export function renderCompareTable(block: CompareTableBlock, _ctx: RenderCtx): string {
|
|
11
|
-
const headerCells = block.headers
|
|
12
|
-
.map((h) => ` <th>${escapeHtml(h)}</th>`)
|
|
13
|
-
.join("\n");
|
|
11
|
+
const headerCells = block.headers.map((h) => ` <th>${escapeHtml(h)}</th>`).join("\n");
|
|
14
12
|
|
|
15
13
|
const bodyRows = block.rows
|
|
16
14
|
.map((row) => {
|
|
@@ -34,7 +32,8 @@ export function renderCompareTable(block: CompareTableBlock, _ctx: RenderCtx): s
|
|
|
34
32
|
|
|
35
33
|
export const meta: BlockMeta = {
|
|
36
34
|
type: "compare_table",
|
|
37
|
-
description:
|
|
35
|
+
description:
|
|
36
|
+
"Bordered comparison grid. Headers define columns; rows must have matching cell count.",
|
|
38
37
|
schema: {
|
|
39
38
|
type: "object",
|
|
40
39
|
properties: {
|
|
@@ -25,7 +25,7 @@ export function renderDiagram(block: DiagramBlock, _ctx: RenderCtx): string {
|
|
|
25
25
|
export const meta: BlockMeta = {
|
|
26
26
|
type: "diagram",
|
|
27
27
|
description:
|
|
28
|
-
|
|
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
29
|
schema: {
|
|
30
30
|
type: "object",
|
|
31
31
|
properties: {
|
|
@@ -35,10 +35,7 @@ export const meta: BlockMeta = {
|
|
|
35
35
|
html: { type: "string" },
|
|
36
36
|
},
|
|
37
37
|
required: ["type"],
|
|
38
|
-
oneOf: [
|
|
39
|
-
{ required: ["svg"] },
|
|
40
|
-
{ required: ["html"] },
|
|
41
|
-
],
|
|
38
|
+
oneOf: [{ required: ["svg"] }, { required: ["html"] }],
|
|
42
39
|
},
|
|
43
40
|
example: {
|
|
44
41
|
type: "diagram",
|