@cfbender/cesium 0.5.0 → 0.5.2
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 +86 -1
- package/README.md +8 -8
- package/package.json +19 -18
- package/src/cli/commands/serve.ts +16 -4
- 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 +185 -0
- package/src/render/blocks/markdown.ts +28 -7
- package/src/render/blocks/render.ts +16 -5
- package/src/render/blocks/renderers/code.ts +5 -5
- 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/section.ts +4 -2
- package/src/render/blocks/renderers/timeline.ts +2 -1
- package/src/render/blocks/themes/claret-dark.ts +201 -0
- package/src/render/blocks/themes/claret-light.ts +222 -0
- package/src/render/blocks/types.ts +13 -1
- package/src/render/blocks/validate-block.ts +19 -9
- package/src/render/theme.ts +131 -0
- package/src/render/validate.ts +53 -9
- package/src/server/lifecycle.ts +188 -3
- package/src/storage/index-gen.ts +2 -3
- package/src/tools/ask.ts +2 -2
- package/src/tools/publish.ts +6 -6
- package/src/tools/styleguide.ts +25 -20
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Claret-light shiki theme — derived from claret.nvim light palette.
|
|
2
|
+
// src/render/blocks/themes/claret-light.ts
|
|
3
|
+
//
|
|
4
|
+
// Source of truth: /claret.nvim/lua/claret/palette.lua (light section)
|
|
5
|
+
// Scope-to-role mapping mirrors ClaretDark.tmTheme exactly; colors
|
|
6
|
+
// are substituted from the light palette using the same semantic roles.
|
|
7
|
+
//
|
|
8
|
+
// Light palette (palette.lua):
|
|
9
|
+
// bg #F5E6E2 editor background
|
|
10
|
+
// bg_mute #DDD0CC selection / line highlight
|
|
11
|
+
// text #2A1F1A default fg
|
|
12
|
+
// text_4 #928578 comment / gutter fg
|
|
13
|
+
// rose_1 #B80842 keyword / heading / statement
|
|
14
|
+
// rose_2 #920820 property / data keys
|
|
15
|
+
// gold_1 #946000 function / number / constant / decorator
|
|
16
|
+
// sage_1 #1B5500 string
|
|
17
|
+
// slate_1 #0E3088 type / class / tag / escape / link
|
|
18
|
+
// slate_2 #0A2575 tag attribute
|
|
19
|
+
// terra_1 #D42010 invalid / diff deleted
|
|
20
|
+
//
|
|
21
|
+
// Role mapping (matching dark tmTheme):
|
|
22
|
+
// Comment → text_4 #928578 italic
|
|
23
|
+
// Keyword → rose_1 #B80842
|
|
24
|
+
// Function → gold_1 #946000
|
|
25
|
+
// String → sage_1 #1B5500
|
|
26
|
+
// Number → gold_1 #946000
|
|
27
|
+
// Constant → gold_1 #946000
|
|
28
|
+
// Type → slate_1 #0E3088
|
|
29
|
+
// Variable → text #2A1F1A
|
|
30
|
+
// Parameter → text #2A1F1A italic
|
|
31
|
+
// Property → rose_2 #920820
|
|
32
|
+
// Keys → rose_2 #920820
|
|
33
|
+
// Operator → text_4 #928578 (text_3 equivalent; nearest warm muted in light palette)
|
|
34
|
+
// Punctuation → text_4 #928578
|
|
35
|
+
// Decorator → gold_1 #946000 italic
|
|
36
|
+
// Tag → slate_1 #0E3088
|
|
37
|
+
// Tag Attribute → slate_2 #0A2575
|
|
38
|
+
// Invalid → terra_1 #D42010
|
|
39
|
+
// Escape → slate_1 #0E3088
|
|
40
|
+
// Markup Heading→ rose_1 #B80842 bold
|
|
41
|
+
// Markup Bold → (no fg) bold
|
|
42
|
+
// Markup Italic → (no fg) italic
|
|
43
|
+
// Markup Link → slate_1 #0E3088
|
|
44
|
+
// Diff Added → sage_1 #1B5500
|
|
45
|
+
// Diff Deleted → terra_1 #D42010
|
|
46
|
+
// Diff Changed → gold_1 #946000
|
|
47
|
+
|
|
48
|
+
import type { ThemeRegistration } from "shiki";
|
|
49
|
+
|
|
50
|
+
export const claretLight: ThemeRegistration = {
|
|
51
|
+
name: "claret-light",
|
|
52
|
+
type: "light",
|
|
53
|
+
fg: "#2A1F1A",
|
|
54
|
+
bg: "#F5E6E2",
|
|
55
|
+
colors: {
|
|
56
|
+
"editor.foreground": "#2A1F1A",
|
|
57
|
+
"editor.background": "#F5E6E2",
|
|
58
|
+
"editor.selectionBackground": "#DDD0CC",
|
|
59
|
+
"editor.lineHighlightBackground": "#DDD0CC",
|
|
60
|
+
"editorLineNumber.foreground": "#928578",
|
|
61
|
+
},
|
|
62
|
+
tokenColors: [
|
|
63
|
+
// Comment — #928578 italic
|
|
64
|
+
{
|
|
65
|
+
name: "Comment",
|
|
66
|
+
scope: ["comment", "punctuation.definition.comment"],
|
|
67
|
+
settings: { foreground: "#928578", fontStyle: "italic" },
|
|
68
|
+
},
|
|
69
|
+
// Keyword — #B80842
|
|
70
|
+
{
|
|
71
|
+
name: "Keyword",
|
|
72
|
+
scope: ["keyword", "storage.type", "storage.modifier"],
|
|
73
|
+
settings: { foreground: "#B80842" },
|
|
74
|
+
},
|
|
75
|
+
// Function — #946000
|
|
76
|
+
{
|
|
77
|
+
name: "Function",
|
|
78
|
+
scope: ["entity.name.function", "support.function"],
|
|
79
|
+
settings: { foreground: "#946000" },
|
|
80
|
+
},
|
|
81
|
+
// String — #1B5500
|
|
82
|
+
{
|
|
83
|
+
name: "String",
|
|
84
|
+
scope: ["string", "punctuation.definition.string"],
|
|
85
|
+
settings: { foreground: "#1B5500" },
|
|
86
|
+
},
|
|
87
|
+
// Number — #946000
|
|
88
|
+
{
|
|
89
|
+
name: "Number",
|
|
90
|
+
scope: ["constant.numeric"],
|
|
91
|
+
settings: { foreground: "#946000" },
|
|
92
|
+
},
|
|
93
|
+
// Constant — #946000
|
|
94
|
+
{
|
|
95
|
+
name: "Constant",
|
|
96
|
+
scope: ["constant", "constant.language", "variable.language"],
|
|
97
|
+
settings: { foreground: "#946000" },
|
|
98
|
+
},
|
|
99
|
+
// Type — #0E3088
|
|
100
|
+
{
|
|
101
|
+
name: "Type",
|
|
102
|
+
scope: ["entity.name.type", "entity.name.class", "support.type", "support.class"],
|
|
103
|
+
settings: { foreground: "#0E3088" },
|
|
104
|
+
},
|
|
105
|
+
// Variable — #2A1F1A
|
|
106
|
+
{
|
|
107
|
+
name: "Variable",
|
|
108
|
+
scope: ["variable", "variable.parameter"],
|
|
109
|
+
settings: { foreground: "#2A1F1A" },
|
|
110
|
+
},
|
|
111
|
+
// Parameter — #2A1F1A italic (overrides Variable for parameters)
|
|
112
|
+
{
|
|
113
|
+
name: "Parameter",
|
|
114
|
+
scope: ["variable.parameter"],
|
|
115
|
+
settings: { foreground: "#2A1F1A", fontStyle: "italic" },
|
|
116
|
+
},
|
|
117
|
+
// Property — #920820
|
|
118
|
+
{
|
|
119
|
+
name: "Property",
|
|
120
|
+
scope: ["variable.other.property", "variable.other.member"],
|
|
121
|
+
settings: { foreground: "#920820" },
|
|
122
|
+
},
|
|
123
|
+
// JSON/YAML/TOML Keys — #920820
|
|
124
|
+
{
|
|
125
|
+
name: "JSON/YAML/TOML Keys",
|
|
126
|
+
scope: [
|
|
127
|
+
"meta.mapping.key string",
|
|
128
|
+
"support.type.property-name.json",
|
|
129
|
+
"punctuation.support.type.property-name.json",
|
|
130
|
+
"support.type.property-name.toml",
|
|
131
|
+
"punctuation.support.type.property-name.toml",
|
|
132
|
+
"entity.name.tag.yaml",
|
|
133
|
+
"support.type.property-name.yaml",
|
|
134
|
+
],
|
|
135
|
+
settings: { foreground: "#920820" },
|
|
136
|
+
},
|
|
137
|
+
// Operator — #928578
|
|
138
|
+
{
|
|
139
|
+
name: "Operator",
|
|
140
|
+
scope: ["keyword.operator"],
|
|
141
|
+
settings: { foreground: "#928578" },
|
|
142
|
+
},
|
|
143
|
+
// Punctuation — #928578
|
|
144
|
+
{
|
|
145
|
+
name: "Punctuation",
|
|
146
|
+
scope: ["punctuation"],
|
|
147
|
+
settings: { foreground: "#928578" },
|
|
148
|
+
},
|
|
149
|
+
// Decorator — #946000 italic
|
|
150
|
+
{
|
|
151
|
+
name: "Decorator",
|
|
152
|
+
scope: ["meta.decorator", "punctuation.decorator"],
|
|
153
|
+
settings: { foreground: "#946000", fontStyle: "italic" },
|
|
154
|
+
},
|
|
155
|
+
// Tag — #0E3088
|
|
156
|
+
{
|
|
157
|
+
name: "Tag",
|
|
158
|
+
scope: ["entity.name.tag"],
|
|
159
|
+
settings: { foreground: "#0E3088" },
|
|
160
|
+
},
|
|
161
|
+
// Tag Attribute — #0A2575
|
|
162
|
+
{
|
|
163
|
+
name: "Tag Attribute",
|
|
164
|
+
scope: ["entity.other.attribute-name"],
|
|
165
|
+
settings: { foreground: "#0A2575" },
|
|
166
|
+
},
|
|
167
|
+
// Invalid — #D42010
|
|
168
|
+
{
|
|
169
|
+
name: "Invalid",
|
|
170
|
+
scope: ["invalid", "invalid.illegal"],
|
|
171
|
+
settings: { foreground: "#D42010" },
|
|
172
|
+
},
|
|
173
|
+
// Escape — #0E3088
|
|
174
|
+
{
|
|
175
|
+
name: "Escape",
|
|
176
|
+
scope: ["constant.character.escape"],
|
|
177
|
+
settings: { foreground: "#0E3088" },
|
|
178
|
+
},
|
|
179
|
+
// Markup Heading — #B80842 bold
|
|
180
|
+
{
|
|
181
|
+
name: "Markup Heading",
|
|
182
|
+
scope: ["markup.heading"],
|
|
183
|
+
settings: { foreground: "#B80842", fontStyle: "bold" },
|
|
184
|
+
},
|
|
185
|
+
// Markup Bold — bold (no color override)
|
|
186
|
+
{
|
|
187
|
+
name: "Markup Bold",
|
|
188
|
+
scope: ["markup.bold"],
|
|
189
|
+
settings: { fontStyle: "bold" },
|
|
190
|
+
},
|
|
191
|
+
// Markup Italic — italic (no color override)
|
|
192
|
+
{
|
|
193
|
+
name: "Markup Italic",
|
|
194
|
+
scope: ["markup.italic"],
|
|
195
|
+
settings: { fontStyle: "italic" },
|
|
196
|
+
},
|
|
197
|
+
// Markup Link — #0E3088
|
|
198
|
+
{
|
|
199
|
+
name: "Markup Link",
|
|
200
|
+
scope: ["markup.underline.link", "string.other.link"],
|
|
201
|
+
settings: { foreground: "#0E3088" },
|
|
202
|
+
},
|
|
203
|
+
// Diff Added — #1B5500
|
|
204
|
+
{
|
|
205
|
+
name: "Diff Added",
|
|
206
|
+
scope: ["markup.inserted"],
|
|
207
|
+
settings: { foreground: "#1B5500" },
|
|
208
|
+
},
|
|
209
|
+
// Diff Deleted — #D42010
|
|
210
|
+
{
|
|
211
|
+
name: "Diff Deleted",
|
|
212
|
+
scope: ["markup.deleted"],
|
|
213
|
+
settings: { foreground: "#D42010" },
|
|
214
|
+
},
|
|
215
|
+
// Diff Changed — #946000
|
|
216
|
+
{
|
|
217
|
+
name: "Diff Changed",
|
|
218
|
+
scope: ["markup.changed"],
|
|
219
|
+
settings: { foreground: "#946000" },
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
@@ -16,7 +16,8 @@ export type Block =
|
|
|
16
16
|
| PillRowBlock
|
|
17
17
|
| DividerBlock
|
|
18
18
|
| DiagramBlock
|
|
19
|
-
| RawHtmlBlock
|
|
19
|
+
| RawHtmlBlock
|
|
20
|
+
| DiffBlock;
|
|
20
21
|
|
|
21
22
|
export type HeroBlock = {
|
|
22
23
|
type: "hero";
|
|
@@ -116,6 +117,17 @@ export type RawHtmlBlock = {
|
|
|
116
117
|
purpose?: string; // brief reason; surfaced in critique findings
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
export type DiffBlock = {
|
|
121
|
+
type: "diff";
|
|
122
|
+
// Provide exactly one of these arms:
|
|
123
|
+
patch?: string; // unified diff format
|
|
124
|
+
before?: string; // OR
|
|
125
|
+
after?: string; // (paired with before)
|
|
126
|
+
lang?: string; // for per-line shiki highlight; default "text"
|
|
127
|
+
filename?: string; // optional, shown in header strip
|
|
128
|
+
caption?: string; // optional, shown below the diff
|
|
129
|
+
};
|
|
130
|
+
|
|
119
131
|
// ─── BlockMeta ───────────────────────────────────────────────────────────────
|
|
120
132
|
|
|
121
133
|
export type BlockMeta = {
|
|
@@ -23,11 +23,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
23
23
|
curr[0] = i;
|
|
24
24
|
for (let j = 1; j <= bLen; j++) {
|
|
25
25
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
26
|
-
curr[j] = Math.min(
|
|
27
|
-
(prev[j] ?? 0) + 1,
|
|
28
|
-
(curr[j - 1] ?? 0) + 1,
|
|
29
|
-
(prev[j - 1] ?? 0) + cost,
|
|
30
|
-
);
|
|
26
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
31
27
|
}
|
|
32
28
|
[prev, curr] = [curr, prev];
|
|
33
29
|
}
|
|
@@ -91,11 +87,19 @@ function isSchemaNode(v: unknown): v is SchemaNode {
|
|
|
91
87
|
* Validate a single value against a JSON Schema fragment node.
|
|
92
88
|
* Appends any findings to `errors`. `path` is the dotted path for error messages.
|
|
93
89
|
*/
|
|
94
|
-
function validateNode(
|
|
90
|
+
function validateNode(
|
|
91
|
+
value: unknown,
|
|
92
|
+
schema: SchemaNode,
|
|
93
|
+
path: string,
|
|
94
|
+
errors: BlockFieldError[],
|
|
95
|
+
): void {
|
|
95
96
|
// const node
|
|
96
97
|
if ("const" in schema) {
|
|
97
98
|
if (value !== schema.const) {
|
|
98
|
-
errors.push({
|
|
99
|
+
errors.push({
|
|
100
|
+
path,
|
|
101
|
+
message: `expected ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`,
|
|
102
|
+
});
|
|
99
103
|
}
|
|
100
104
|
return;
|
|
101
105
|
}
|
|
@@ -143,7 +147,10 @@ function validateNode(value: unknown, schema: SchemaNode, path: string, errors:
|
|
|
143
147
|
}
|
|
144
148
|
case "object": {
|
|
145
149
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
146
|
-
errors.push({
|
|
150
|
+
errors.push({
|
|
151
|
+
path,
|
|
152
|
+
message: `expected object, got ${Array.isArray(value) ? "array" : typeof value}`,
|
|
153
|
+
});
|
|
147
154
|
return;
|
|
148
155
|
}
|
|
149
156
|
const obj = value as Record<string, unknown>;
|
|
@@ -171,7 +178,10 @@ function validateNode(value: unknown, schema: SchemaNode, path: string, errors:
|
|
|
171
178
|
// Unknown field — suggest closest known
|
|
172
179
|
const suggestion = didYouMean(key, knownKeys);
|
|
173
180
|
const suggestionMsg = suggestion !== null ? `; did you mean "${suggestion}"?` : "";
|
|
174
|
-
errors.push({
|
|
181
|
+
errors.push({
|
|
182
|
+
path: `${path}.${key}`,
|
|
183
|
+
message: `unknown field "${key}"${suggestionMsg}`,
|
|
184
|
+
});
|
|
175
185
|
} else if (isSchemaNode(propSchema)) {
|
|
176
186
|
validateNode(val, propSchema, `${path}.${key}`, errors);
|
|
177
187
|
}
|
package/src/render/theme.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface ThemePalette {
|
|
|
13
13
|
olive: string;
|
|
14
14
|
codeBg: string;
|
|
15
15
|
codeFg: string;
|
|
16
|
+
diffAdd: string; // line-tint and connector color for additions
|
|
17
|
+
diffRemove: string; // line-tint and connector color for deletions
|
|
18
|
+
diffChange: string; // connector color for replacements
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface ThemeFonts {
|
|
@@ -56,6 +59,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
56
59
|
olive: "#8FA86E",
|
|
57
60
|
codeBg: "#2B1F22",
|
|
58
61
|
codeFg: "#DDD3C7",
|
|
62
|
+
diffAdd: "#6D9E60",
|
|
63
|
+
diffRemove: "#C75B5B",
|
|
64
|
+
diffChange: "#D4A85A",
|
|
59
65
|
},
|
|
60
66
|
// claret-light: deep-rose-on-warm-cream — derived from claret.nvim light palette.
|
|
61
67
|
// (this is the old "claret" palette, now renamed)
|
|
@@ -72,6 +78,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
72
78
|
olive: "#5A6B40",
|
|
73
79
|
codeBg: "#180810",
|
|
74
80
|
codeFg: "#DDD3C7",
|
|
81
|
+
diffAdd: "#5A6B40",
|
|
82
|
+
diffRemove: "#9E3838",
|
|
83
|
+
diffChange: "#B07A2A",
|
|
75
84
|
},
|
|
76
85
|
// claret: alias for claret-dark (backward compat)
|
|
77
86
|
claret: {
|
|
@@ -87,6 +96,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
87
96
|
olive: "#8FA86E",
|
|
88
97
|
codeBg: "#2B1F22",
|
|
89
98
|
codeFg: "#DDD3C7",
|
|
99
|
+
diffAdd: "#6D9E60",
|
|
100
|
+
diffRemove: "#C75B5B",
|
|
101
|
+
diffChange: "#D4A85A",
|
|
90
102
|
},
|
|
91
103
|
// Warm: ivory/clay/oat — the html-effectiveness reference palette.
|
|
92
104
|
warm: {
|
|
@@ -102,6 +114,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
102
114
|
olive: "#788C5D",
|
|
103
115
|
codeBg: "#141413",
|
|
104
116
|
codeFg: "#E8E6DE",
|
|
117
|
+
diffAdd: "#788C5D",
|
|
118
|
+
diffRemove: "#C0392B",
|
|
119
|
+
diffChange: "#D97757",
|
|
105
120
|
},
|
|
106
121
|
// Cool: desaturated blue-grey — technical, trustworthy.
|
|
107
122
|
cool: {
|
|
@@ -117,6 +132,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
117
132
|
olive: "#4E8A6A",
|
|
118
133
|
codeBg: "#1B2333",
|
|
119
134
|
codeFg: "#D8E0ED",
|
|
135
|
+
diffAdd: "#4E8A6A",
|
|
136
|
+
diffRemove: "#B6443A",
|
|
137
|
+
diffChange: "#3A7BB8",
|
|
120
138
|
},
|
|
121
139
|
// Mono: black/white/grey — editorial, high-contrast.
|
|
122
140
|
mono: {
|
|
@@ -132,6 +150,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
132
150
|
olive: "#5A7A5A",
|
|
133
151
|
codeBg: "#111111",
|
|
134
152
|
codeFg: "#EBEBEB",
|
|
153
|
+
diffAdd: "#5A7A5A",
|
|
154
|
+
diffRemove: "#A03A2B",
|
|
155
|
+
diffChange: "#666666",
|
|
135
156
|
},
|
|
136
157
|
// Paper: sepia/cream — soft, book-like, warm and aged.
|
|
137
158
|
paper: {
|
|
@@ -147,6 +168,9 @@ export const THEME_PRESETS: Readonly<Record<ThemePresetName, ThemePalette>> = {
|
|
|
147
168
|
olive: "#607848",
|
|
148
169
|
codeBg: "#2A2218",
|
|
149
170
|
codeFg: "#E8DEC8",
|
|
171
|
+
diffAdd: "#607848",
|
|
172
|
+
diffRemove: "#A0392B",
|
|
173
|
+
diffChange: "#B05A2A",
|
|
150
174
|
},
|
|
151
175
|
};
|
|
152
176
|
|
|
@@ -198,6 +222,9 @@ export function themeToCssVars(theme: ThemeTokens): string {
|
|
|
198
222
|
--olive: ${colors.olive};
|
|
199
223
|
--code-bg: ${colors.codeBg};
|
|
200
224
|
--code-fg: ${colors.codeFg};
|
|
225
|
+
--diff-add: ${colors.diffAdd};
|
|
226
|
+
--diff-remove: ${colors.diffRemove};
|
|
227
|
+
--diff-change: ${colors.diffChange};
|
|
201
228
|
--serif: ${fonts.serif};
|
|
202
229
|
--sans: ${fonts.sans};
|
|
203
230
|
--mono: ${fonts.mono};
|
|
@@ -936,6 +963,110 @@ textarea.cs-text { font-family: var(--mono); }
|
|
|
936
963
|
font-weight: 600;
|
|
937
964
|
z-index: 10;
|
|
938
965
|
}
|
|
966
|
+
|
|
967
|
+
/* diff block */
|
|
968
|
+
.diff-block {
|
|
969
|
+
margin: var(--space-6, 1.5rem) 0;
|
|
970
|
+
border: 1.5px solid var(--rule);
|
|
971
|
+
border-radius: 12px;
|
|
972
|
+
overflow: hidden;
|
|
973
|
+
background: var(--code-bg);
|
|
974
|
+
font-family: var(--mono);
|
|
975
|
+
font-size: 13px;
|
|
976
|
+
color: var(--code-fg);
|
|
977
|
+
}
|
|
978
|
+
.diff-block.fallback pre {
|
|
979
|
+
margin: 0;
|
|
980
|
+
padding: 12px 14px;
|
|
981
|
+
white-space: pre;
|
|
982
|
+
overflow-x: auto;
|
|
983
|
+
}
|
|
984
|
+
.diff-header {
|
|
985
|
+
display: flex;
|
|
986
|
+
justify-content: space-between;
|
|
987
|
+
align-items: center;
|
|
988
|
+
padding: 8px 14px;
|
|
989
|
+
border-bottom: 1px solid color-mix(in oklab, var(--rule), transparent 40%);
|
|
990
|
+
background: color-mix(in oklab, var(--code-bg), var(--code-fg) 5%);
|
|
991
|
+
font-size: 12px;
|
|
992
|
+
color: color-mix(in oklab, var(--code-fg), transparent 30%);
|
|
993
|
+
}
|
|
994
|
+
.diff-filename { font-weight: 500; }
|
|
995
|
+
.diff-stat { font-variant-numeric: tabular-nums; display: inline-flex; gap: 8px; }
|
|
996
|
+
.diff-stat .add { color: var(--diff-add); }
|
|
997
|
+
.diff-stat .rem { color: var(--diff-remove); }
|
|
998
|
+
|
|
999
|
+
.diff-grid {
|
|
1000
|
+
display: grid;
|
|
1001
|
+
grid-template-columns: 1fr 60px 1fr;
|
|
1002
|
+
align-items: start;
|
|
1003
|
+
}
|
|
1004
|
+
.diff-side {
|
|
1005
|
+
list-style: none;
|
|
1006
|
+
margin: 0;
|
|
1007
|
+
padding: 8px 0;
|
|
1008
|
+
overflow-x: auto;
|
|
1009
|
+
min-width: 0;
|
|
1010
|
+
}
|
|
1011
|
+
.diff-line {
|
|
1012
|
+
display: grid;
|
|
1013
|
+
grid-template-columns: 3.25em 1fr;
|
|
1014
|
+
gap: 0;
|
|
1015
|
+
height: 22px;
|
|
1016
|
+
line-height: 22px;
|
|
1017
|
+
white-space: pre;
|
|
1018
|
+
}
|
|
1019
|
+
.diff-line .num {
|
|
1020
|
+
text-align: right;
|
|
1021
|
+
padding-right: 12px;
|
|
1022
|
+
color: color-mix(in oklab, var(--code-fg), transparent 60%);
|
|
1023
|
+
user-select: none;
|
|
1024
|
+
font-variant-numeric: tabular-nums;
|
|
1025
|
+
}
|
|
1026
|
+
.diff-line .content {
|
|
1027
|
+
padding-right: 14px;
|
|
1028
|
+
overflow: hidden;
|
|
1029
|
+
text-overflow: ellipsis;
|
|
1030
|
+
}
|
|
1031
|
+
.diff-line.add { background: color-mix(in oklab, transparent, var(--diff-add) 14%); }
|
|
1032
|
+
.diff-line.remove { background: color-mix(in oklab, transparent, var(--diff-remove) 14%); }
|
|
1033
|
+
.diff-line.hunk-sep {
|
|
1034
|
+
background: color-mix(in oklab, var(--code-bg), var(--code-fg) 8%);
|
|
1035
|
+
color: color-mix(in oklab, var(--code-fg), transparent 50%);
|
|
1036
|
+
font-style: italic;
|
|
1037
|
+
font-size: 11px;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.diff-connector {
|
|
1041
|
+
position: relative;
|
|
1042
|
+
align-self: stretch;
|
|
1043
|
+
padding: 8px 0;
|
|
1044
|
+
background: color-mix(in oklab, var(--code-bg), var(--code-fg) 2%);
|
|
1045
|
+
border-left: 1px solid color-mix(in oklab, var(--rule), transparent 60%);
|
|
1046
|
+
border-right: 1px solid color-mix(in oklab, var(--rule), transparent 60%);
|
|
1047
|
+
}
|
|
1048
|
+
.diff-connector svg {
|
|
1049
|
+
display: block;
|
|
1050
|
+
width: 100%;
|
|
1051
|
+
}
|
|
1052
|
+
.diff-conn { stroke-width: 1; }
|
|
1053
|
+
.diff-conn.add { fill: var(--diff-add); stroke: var(--diff-add); fill-opacity: 0.22; }
|
|
1054
|
+
.diff-conn.remove { fill: var(--diff-remove); stroke: var(--diff-remove); fill-opacity: 0.22; }
|
|
1055
|
+
.diff-conn.change { fill: var(--diff-change); stroke: var(--diff-change); fill-opacity: 0.18; }
|
|
1056
|
+
|
|
1057
|
+
.diff-block figcaption {
|
|
1058
|
+
padding: 8px 14px;
|
|
1059
|
+
border-top: 1px solid color-mix(in oklab, var(--rule), transparent 40%);
|
|
1060
|
+
font-size: 12px;
|
|
1061
|
+
font-family: var(--sans);
|
|
1062
|
+
color: color-mix(in oklab, var(--code-fg), transparent 30%);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
@media (max-width: 720px) {
|
|
1066
|
+
.diff-grid { grid-template-columns: 1fr; }
|
|
1067
|
+
.diff-connector { display: none; }
|
|
1068
|
+
.diff-side.before { border-bottom: 1px solid color-mix(in oklab, var(--rule), transparent 60%); }
|
|
1069
|
+
}
|
|
939
1070
|
`;
|
|
940
1071
|
}
|
|
941
1072
|
|
package/src/render/validate.ts
CHANGED
|
@@ -439,7 +439,9 @@ function isPublishKind(val: unknown): val is PublishKind {
|
|
|
439
439
|
// ─── Block validation ─────────────────────────────────────────────────────────
|
|
440
440
|
|
|
441
441
|
type BlockValidationError = { path: string; message: string };
|
|
442
|
-
type BlockValidationResult =
|
|
442
|
+
type BlockValidationResult =
|
|
443
|
+
| { ok: true; blocks: Block[] }
|
|
444
|
+
| { ok: false; errors: BlockValidationError[] };
|
|
443
445
|
|
|
444
446
|
function validateBlock(raw: unknown, path: string, depth: number): BlockValidationError[] {
|
|
445
447
|
const errors: BlockValidationError[] = [];
|
|
@@ -484,7 +486,10 @@ function validateBlock(raw: unknown, path: string, depth: number): BlockValidati
|
|
|
484
486
|
errors.push({ path, message: "section block requires children array" });
|
|
485
487
|
} else {
|
|
486
488
|
if (depth > 3) {
|
|
487
|
-
errors.push({
|
|
489
|
+
errors.push({
|
|
490
|
+
path,
|
|
491
|
+
message: `section nesting depth exceeds maximum of 3 (current depth: ${depth})`,
|
|
492
|
+
});
|
|
488
493
|
} else {
|
|
489
494
|
const children = b["children"] as unknown[];
|
|
490
495
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -518,7 +523,10 @@ function validateBlock(raw: unknown, path: string, depth: number): BlockValidati
|
|
|
518
523
|
}
|
|
519
524
|
case "code": {
|
|
520
525
|
if (typeof b["lang"] !== "string" || (b["lang"] as string).trim() === "") {
|
|
521
|
-
errors.push({
|
|
526
|
+
errors.push({
|
|
527
|
+
path,
|
|
528
|
+
message: 'code block requires a non-empty lang (use "text" if unknown)',
|
|
529
|
+
});
|
|
522
530
|
}
|
|
523
531
|
if (typeof b["code"] !== "string") {
|
|
524
532
|
errors.push({ path, message: "code block requires code field" });
|
|
@@ -580,7 +588,10 @@ function validateBlock(raw: unknown, path: string, depth: number): BlockValidati
|
|
|
580
588
|
const hasSvg = typeof b["svg"] === "string";
|
|
581
589
|
const hasHtml = typeof b["html"] === "string";
|
|
582
590
|
if (hasSvg && hasHtml) {
|
|
583
|
-
errors.push({
|
|
591
|
+
errors.push({
|
|
592
|
+
path,
|
|
593
|
+
message: "diagram block must have exactly one of svg or html, not both",
|
|
594
|
+
});
|
|
584
595
|
} else if (!hasSvg && !hasHtml) {
|
|
585
596
|
errors.push({ path, message: "diagram block requires exactly one of svg or html" });
|
|
586
597
|
}
|
|
@@ -592,6 +603,35 @@ function validateBlock(raw: unknown, path: string, depth: number): BlockValidati
|
|
|
592
603
|
}
|
|
593
604
|
break;
|
|
594
605
|
}
|
|
606
|
+
case "diff": {
|
|
607
|
+
const hasPatch = "patch" in b && b["patch"] !== undefined;
|
|
608
|
+
const hasBefore = "before" in b && b["before"] !== undefined;
|
|
609
|
+
const hasAfter = "after" in b && b["after"] !== undefined;
|
|
610
|
+
|
|
611
|
+
if (hasPatch && (hasBefore || hasAfter)) {
|
|
612
|
+
errors.push({
|
|
613
|
+
path,
|
|
614
|
+
message: "provide exactly one of patch or before/after, not both",
|
|
615
|
+
});
|
|
616
|
+
} else if (!hasPatch && !hasBefore && !hasAfter) {
|
|
617
|
+
errors.push({
|
|
618
|
+
path,
|
|
619
|
+
message: "diff block requires either patch or before+after",
|
|
620
|
+
});
|
|
621
|
+
} else if (hasPatch) {
|
|
622
|
+
if (typeof b["patch"] !== "string" || (b["patch"] as string).trim() === "") {
|
|
623
|
+
errors.push({ path, message: "diff block patch must be a non-empty string" });
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
// before/after arm
|
|
627
|
+
if (hasBefore && !hasAfter) {
|
|
628
|
+
errors.push({ path, message: "diff block before/after arm requires both fields" });
|
|
629
|
+
} else if (!hasBefore && hasAfter) {
|
|
630
|
+
errors.push({ path, message: "diff block before/after arm requires both fields" });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
595
635
|
}
|
|
596
636
|
|
|
597
637
|
// Deep field validation against catalog schema — catches unknown fields (with "did you mean"
|
|
@@ -611,7 +651,10 @@ function validateBlocksArray(raw: unknown): BlockValidationResult {
|
|
|
611
651
|
}
|
|
612
652
|
|
|
613
653
|
if (raw.length > 1000) {
|
|
614
|
-
return {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
errors: [{ path: "blocks", message: "blocks array exceeds maximum of 1000 blocks" }],
|
|
657
|
+
};
|
|
615
658
|
}
|
|
616
659
|
|
|
617
660
|
const allErrors: BlockValidationError[] = [];
|
|
@@ -627,7 +670,10 @@ function validateBlocksArray(raw: unknown): BlockValidationResult {
|
|
|
627
670
|
if (blockType === "hero") {
|
|
628
671
|
heroCount++;
|
|
629
672
|
if (i !== 0) {
|
|
630
|
-
allErrors.push({
|
|
673
|
+
allErrors.push({
|
|
674
|
+
path: `blocks[${i}]`,
|
|
675
|
+
message: "hero block must be the first block if present",
|
|
676
|
+
});
|
|
631
677
|
}
|
|
632
678
|
}
|
|
633
679
|
if (blockType === "tldr") {
|
|
@@ -740,9 +786,7 @@ export function validatePublishInput(input: unknown): ValidationResult<PublishIn
|
|
|
740
786
|
// blocks branch
|
|
741
787
|
const blocksResult = validateBlocksArray(raw["blocks"]);
|
|
742
788
|
if (!blocksResult.ok) {
|
|
743
|
-
const errorMessages = blocksResult.errors
|
|
744
|
-
.map((e) => `${e.path}: ${e.message}`)
|
|
745
|
-
.join("; ");
|
|
789
|
+
const errorMessages = blocksResult.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
746
790
|
return { ok: false, error: `blocks validation failed — ${errorMessages}` };
|
|
747
791
|
}
|
|
748
792
|
return {
|