sdocs 0.0.57 → 0.0.59
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 +48 -0
- package/dist/commands/build.js +2 -0
- package/dist/commands/dev.js +2 -0
- package/dist/explorer/views/PageView.svelte +106 -36
- package/dist/language/config-schema.js +13 -1
- package/dist/language/page-islands.d.ts +3 -1
- package/dist/language/page-islands.js +15 -1
- package/dist/language/parser.js +2 -0
- package/dist/server/config.js +5 -1
- package/dist/server/highlighter.d.ts +2 -1
- package/dist/server/highlighter.js +8 -3
- package/dist/server/page-markdown.d.ts +3 -0
- package/dist/server/page-markdown.js +60 -3
- package/dist/types.d.ts +14 -0
- package/dist/ui/styles/sdocs.css +9 -0
- package/dist/vite.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.59] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Code blocks render on a dark stage.** Highlighted code — page fences and
|
|
15
|
+
the code panels — always uses the dark syntax theme, so blocks read as
|
|
16
|
+
code at a glance in both app themes (they previously rendered white-on-white
|
|
17
|
+
in light mode).
|
|
18
|
+
- **A page's body `#` title matches the entity title size** (24px/700) — it
|
|
19
|
+
is the page title, after all.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Every fence gets a themed block.** `bash`, `json`, `markdown`, `yaml`,
|
|
24
|
+
and `diff` fences highlight properly, and unknown languages fall back to a
|
|
25
|
+
plaintext shiki block instead of an unstyled bare `<pre>`.
|
|
26
|
+
|
|
27
|
+
## [0.0.58] - 2026-07-05
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Richer page markdown.** GitHub-style alerts (`> [!NOTE]`, `[!TIP]`,
|
|
32
|
+
`[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`) render as tinted callouts;
|
|
33
|
+
task lists get proper checkboxes; table columns honor `:---:` alignment;
|
|
34
|
+
external links open in a new tab (internal ones stay in the app); images
|
|
35
|
+
render lazily and Svelte-safe.
|
|
36
|
+
- **`static` config option.** A folder of static assets served at the site
|
|
37
|
+
root in dev and copied into `dist/` by `sdocs build` — ``
|
|
38
|
+
in a page just works. Powers the standalone CLI flows; embedded apps keep
|
|
39
|
+
using the host's public directory.
|
|
40
|
+
- **Page column alignment.** `content.page.contentX` (config) and
|
|
41
|
+
`contentX` on `[PAGE]` align the content column `left`/`center`/`right`.
|
|
42
|
+
- **A body `#` heading takes over as the page title** — the header no longer
|
|
43
|
+
duplicates it; the entity `title` keeps naming the sidebar entry.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **A page's `maxWidth` now bounds the content column together with its
|
|
48
|
+
table of contents**; when the toc is hidden the prose takes its space.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Code fences can show component tags.** A `<Component />` line inside a
|
|
53
|
+
markdown fence, preceded by a blank line, was treated as a live Svelte
|
|
54
|
+
island and rendered (or crashed the page) instead of staying highlighted
|
|
55
|
+
code. Fences now shield island detection, matching the scanner and the
|
|
56
|
+
editor projection.
|
|
57
|
+
|
|
10
58
|
## [0.0.57] - 2026-07-05
|
|
11
59
|
|
|
12
60
|
### Added
|
package/dist/commands/build.js
CHANGED
package/dist/commands/dev.js
CHANGED
|
@@ -37,6 +37,8 @@ export async function devCommand() {
|
|
|
37
37
|
const server = await createServer({
|
|
38
38
|
configFile: false,
|
|
39
39
|
root: sdocsDir,
|
|
40
|
+
// The project's static assets (config `static`), served at the site root
|
|
41
|
+
publicDir: config.static ?? false,
|
|
40
42
|
resolve: {
|
|
41
43
|
dedupe: svelteDedupe(cwd),
|
|
42
44
|
},
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
function scrollToHeading(id: string) {
|
|
41
41
|
container?.querySelector(`#${CSS.escape(id)}`)?.scrollIntoView({ behavior: 'smooth' });
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
// contentX aligns the whole column (content + toc) inside the view.
|
|
45
|
+
const columnMargin = $derived(
|
|
46
|
+
doc.contentX === 'center' ? '0 auto' : doc.contentX === 'right' ? '0 0 0 auto' : undefined,
|
|
47
|
+
);
|
|
43
48
|
</script>
|
|
44
49
|
|
|
45
50
|
{#snippet exampleFrame(index: number)}
|
|
@@ -65,52 +70,61 @@
|
|
|
65
70
|
{/snippet}
|
|
66
71
|
|
|
67
72
|
<div class="sdocs-page-view" style:padding={doc.padding}>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
<!-- maxWidth constrains the whole column — content plus toc; contentX
|
|
74
|
+
places it. Without a toc the content takes the toc's space. -->
|
|
75
|
+
<div class="sdocs-page-inner" style:max-width={doc.maxWidth} style:margin={columnMargin}>
|
|
76
|
+
<div class="sdocs-page-main">
|
|
77
|
+
<!-- Header: a body `#` heading takes over as the page title -->
|
|
78
|
+
{#if !doc.bodyTitle}
|
|
79
|
+
<div class="sdocs-view-header">
|
|
80
|
+
<h1 class="sdocs-view-title">{displayTitle(meta.title)}</h1>
|
|
81
|
+
{#if meta.description}
|
|
82
|
+
<p class="sdocs-view-description">{meta.description}</p>
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
74
85
|
{/if}
|
|
75
|
-
</div>
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
<!-- Content -->
|
|
88
|
+
<div class="sdocs-page-content" bind:this={container}>
|
|
89
|
+
{#if PageComponent}
|
|
90
|
+
<PageComponent __sdocsExample={exampleFrame} />
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
82
93
|
</div>
|
|
83
|
-
</div>
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
<!-- Table of Contents -->
|
|
96
|
+
{#if toc.length > 0 && doc.showToc !== false}
|
|
97
|
+
<aside class="sdocs-toc">
|
|
98
|
+
<h3 class="sdocs-toc-title">On this page</h3>
|
|
99
|
+
<nav>
|
|
100
|
+
<ul class="sdocs-toc-list">
|
|
101
|
+
{#each toc as heading (heading.id)}
|
|
102
|
+
<li class="sdocs-toc-item" style:padding-left="{(heading.level - 2) * 12}px">
|
|
103
|
+
<button
|
|
104
|
+
class="sdocs-toc-link"
|
|
105
|
+
onclick={() => scrollToHeading(heading.id)}
|
|
106
|
+
>
|
|
107
|
+
{heading.text}
|
|
108
|
+
</button>
|
|
109
|
+
</li>
|
|
110
|
+
{/each}
|
|
111
|
+
</ul>
|
|
112
|
+
</nav>
|
|
113
|
+
</aside>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
105
116
|
</div>
|
|
106
117
|
|
|
107
118
|
<style>
|
|
108
119
|
.sdocs-page-view {
|
|
109
|
-
display: flex;
|
|
110
|
-
gap: 24px;
|
|
111
120
|
/* padding comes from the doc entry (config/entity cascade) */
|
|
112
121
|
font-family: var(--sans);
|
|
113
122
|
}
|
|
123
|
+
.sdocs-page-inner {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: 24px;
|
|
126
|
+
/* max-width and margin (contentX) come from the doc entry */
|
|
127
|
+
}
|
|
114
128
|
.sdocs-page-main {
|
|
115
129
|
flex: 1;
|
|
116
130
|
min-width: 0;
|
|
@@ -148,7 +162,11 @@
|
|
|
148
162
|
margin: 1.6em 0 0.5em;
|
|
149
163
|
scroll-margin-top: 16px;
|
|
150
164
|
}
|
|
151
|
-
|
|
165
|
+
/* The body h1 serves as the page title — match .sdocs-view-title */
|
|
166
|
+
.sdocs-page-content :global(h1) {
|
|
167
|
+
font-size: 24px;
|
|
168
|
+
font-weight: 700;
|
|
169
|
+
}
|
|
152
170
|
.sdocs-page-content :global(h2) { font-size: 18px; }
|
|
153
171
|
.sdocs-page-content :global(h3) { font-size: 15px; }
|
|
154
172
|
.sdocs-page-content :global(h4),
|
|
@@ -207,6 +225,9 @@
|
|
|
207
225
|
.sdocs-page-content :global(table) {
|
|
208
226
|
border-collapse: collapse;
|
|
209
227
|
margin: 0.9em 0;
|
|
228
|
+
display: block;
|
|
229
|
+
max-width: 100%;
|
|
230
|
+
overflow-x: auto;
|
|
210
231
|
}
|
|
211
232
|
.sdocs-page-content :global(th),
|
|
212
233
|
.sdocs-page-content :global(td) {
|
|
@@ -214,12 +235,61 @@
|
|
|
214
235
|
padding: 6px 12px;
|
|
215
236
|
text-align: left;
|
|
216
237
|
}
|
|
238
|
+
.sdocs-page-content :global(td[align='center']),
|
|
239
|
+
.sdocs-page-content :global(th[align='center']) {
|
|
240
|
+
text-align: center;
|
|
241
|
+
}
|
|
242
|
+
.sdocs-page-content :global(td[align='right']),
|
|
243
|
+
.sdocs-page-content :global(th[align='right']) {
|
|
244
|
+
text-align: right;
|
|
245
|
+
}
|
|
217
246
|
.sdocs-page-content :global(th) {
|
|
218
247
|
background: var(--color-base-50);
|
|
219
248
|
font-weight: 600;
|
|
220
249
|
}
|
|
221
250
|
.sdocs-page-content :global(img) {
|
|
222
251
|
max-width: 100%;
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Task lists: GitHub-style checkboxes, no bullet */
|
|
256
|
+
.sdocs-page-content :global(li:has(> input[type='checkbox']:first-child)) {
|
|
257
|
+
list-style: none;
|
|
258
|
+
margin-left: -1.3em;
|
|
259
|
+
}
|
|
260
|
+
.sdocs-page-content :global(li > input[type='checkbox']) {
|
|
261
|
+
margin-right: 6px;
|
|
262
|
+
vertical-align: -2px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* GitHub-style alerts: > [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION] */
|
|
266
|
+
.sdocs-page-content :global(.sdocs-alert) {
|
|
267
|
+
margin: 0.9em 0;
|
|
268
|
+
padding: 8px 16px;
|
|
269
|
+
border-left: 3px solid var(--sdocs-alert-color);
|
|
270
|
+
border-radius: 0 6px 6px 0;
|
|
271
|
+
background: color-mix(in srgb, var(--sdocs-alert-color) 6%, transparent);
|
|
272
|
+
}
|
|
273
|
+
.sdocs-page-content :global(.sdocs-alert-label) {
|
|
274
|
+
font-size: 13px;
|
|
275
|
+
font-weight: 650;
|
|
276
|
+
color: var(--sdocs-alert-color);
|
|
277
|
+
margin: 0.25em 0 0;
|
|
278
|
+
}
|
|
279
|
+
.sdocs-page-content :global(.sdocs-alert-note) {
|
|
280
|
+
--sdocs-alert-color: var(--color-blue-500);
|
|
281
|
+
}
|
|
282
|
+
.sdocs-page-content :global(.sdocs-alert-tip) {
|
|
283
|
+
--sdocs-alert-color: var(--color-green-500);
|
|
284
|
+
}
|
|
285
|
+
.sdocs-page-content :global(.sdocs-alert-important) {
|
|
286
|
+
--sdocs-alert-color: var(--color-purple-500);
|
|
287
|
+
}
|
|
288
|
+
.sdocs-page-content :global(.sdocs-alert-warning) {
|
|
289
|
+
--sdocs-alert-color: var(--color-amber-500);
|
|
290
|
+
}
|
|
291
|
+
.sdocs-page-content :global(.sdocs-alert-caution) {
|
|
292
|
+
--sdocs-alert-color: var(--color-red-500);
|
|
223
293
|
}
|
|
224
294
|
|
|
225
295
|
/* ── Example stages in the page flow ── */
|
|
@@ -65,6 +65,11 @@ export const configSchema = {
|
|
|
65
65
|
doc: 'CSS loaded inside preview iframes — a single stylesheet path, or a map of named stylesheets to switch between.',
|
|
66
66
|
insert: ": '$0'",
|
|
67
67
|
},
|
|
68
|
+
static: {
|
|
69
|
+
detail: 'string',
|
|
70
|
+
doc: 'Folder of static assets served at the site root — images for pages, files for previews. Standalone CLI flows; embedded apps use the host\'s public directory.',
|
|
71
|
+
insert: ": './${0:static}'",
|
|
72
|
+
},
|
|
68
73
|
logo: {
|
|
69
74
|
detail: 'string',
|
|
70
75
|
doc: 'Sidebar logo text. Default: `sdocs`.',
|
|
@@ -101,7 +106,7 @@ export const configSchema = {
|
|
|
101
106
|
object: {
|
|
102
107
|
page: {
|
|
103
108
|
detail: 'object',
|
|
104
|
-
doc: '`[PAGE]` content. Defaults: `maxWidth` `1200px`, `padding` `32px`, `toc` `true`.',
|
|
109
|
+
doc: '`[PAGE]` content. Defaults: `maxWidth` `1200px`, `padding` `32px`, `toc` `true`, `contentX` `left`.',
|
|
105
110
|
insert: ': {\n\t$0\n}',
|
|
106
111
|
object: {
|
|
107
112
|
maxWidth,
|
|
@@ -112,6 +117,13 @@ export const configSchema = {
|
|
|
112
117
|
insert: ': ${0:true}',
|
|
113
118
|
values: ['true', 'false'],
|
|
114
119
|
},
|
|
120
|
+
contentX: {
|
|
121
|
+
detail: "'left' | 'center' | 'right'",
|
|
122
|
+
doc: 'Horizontal alignment of the page content column (with its toc). Default: `left`.',
|
|
123
|
+
insert: ": '${0:center}'",
|
|
124
|
+
values: ['left', 'center', 'right'],
|
|
125
|
+
quoted: true,
|
|
126
|
+
},
|
|
115
127
|
},
|
|
116
128
|
},
|
|
117
129
|
docs: {
|
|
@@ -19,5 +19,7 @@ export interface PageSegment {
|
|
|
19
19
|
/** Verbatim lines of the segment */
|
|
20
20
|
lines: string[];
|
|
21
21
|
}
|
|
22
|
-
/** Split a page body into prose and island segments.
|
|
22
|
+
/** Split a page body into prose and island segments. Lines inside markdown
|
|
23
|
+
* code fences are always prose — a component tag shown in a fence must stay
|
|
24
|
+
* displayed code, never a live island. */
|
|
23
25
|
export declare function segmentPageBody(body: string): PageSegment[];
|
|
@@ -80,7 +80,9 @@ function lineDepths(line) {
|
|
|
80
80
|
}
|
|
81
81
|
return { svelte, html };
|
|
82
82
|
}
|
|
83
|
-
/** Split a page body into prose and island segments.
|
|
83
|
+
/** Split a page body into prose and island segments. Lines inside markdown
|
|
84
|
+
* code fences are always prose — a component tag shown in a fence must stay
|
|
85
|
+
* displayed code, never a live island. */
|
|
84
86
|
export function segmentPageBody(body) {
|
|
85
87
|
const lines = body.split('\n');
|
|
86
88
|
const segments = [];
|
|
@@ -92,9 +94,21 @@ export function segmentPageBody(body) {
|
|
|
92
94
|
}
|
|
93
95
|
current.lines.push(line);
|
|
94
96
|
};
|
|
97
|
+
let inFence = false;
|
|
95
98
|
let i = 0;
|
|
96
99
|
while (i < lines.length) {
|
|
97
100
|
const line = lines[i];
|
|
101
|
+
if (/^\s*(`{3,}|~{3,})/.test(line)) {
|
|
102
|
+
inFence = !inFence;
|
|
103
|
+
push('prose', line);
|
|
104
|
+
i++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (inFence) {
|
|
108
|
+
push('prose', line);
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
98
112
|
const prevBlank = i === 0 || lines[i - 1].trim() === '';
|
|
99
113
|
if (prevBlank && line.trim() !== '' && startsIsland(line)) {
|
|
100
114
|
// Consume the island: until Svelte blocks and HTML tags balance out.
|
package/dist/language/parser.js
CHANGED
|
@@ -75,6 +75,8 @@ const ENTITY_ATTR_RULES = {
|
|
|
75
75
|
PAGE: {
|
|
76
76
|
title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
|
|
77
77
|
...SIZING_ATTR_RULES,
|
|
78
|
+
// On PAGE, contentX aligns the content column (with its toc), not a stage.
|
|
79
|
+
contentX: { required: false, kind: 'string', hint: 'contentX="center"' },
|
|
78
80
|
toc: { required: false, kind: 'string', hint: 'toc="false"' },
|
|
79
81
|
},
|
|
80
82
|
LAYOUT: {
|
package/dist/server/config.js
CHANGED
|
@@ -7,6 +7,7 @@ const DEFAULTS = {
|
|
|
7
7
|
port: 3000,
|
|
8
8
|
open: false,
|
|
9
9
|
css: null,
|
|
10
|
+
static: null,
|
|
10
11
|
logo: 'sdocs',
|
|
11
12
|
icon: 'sdocs',
|
|
12
13
|
sidebar: {
|
|
@@ -14,7 +15,7 @@ const DEFAULTS = {
|
|
|
14
15
|
open: [],
|
|
15
16
|
},
|
|
16
17
|
content: {
|
|
17
|
-
page: { maxWidth: '1200px', padding: '32px', toc: true },
|
|
18
|
+
page: { maxWidth: '1200px', padding: '32px', toc: true, contentX: 'left' },
|
|
18
19
|
docs: {
|
|
19
20
|
maxWidth: '1200px',
|
|
20
21
|
padding: '16px',
|
|
@@ -69,6 +70,8 @@ export function resolveAndFinalize(userConfig, root) {
|
|
|
69
70
|
const resolved = resolveConfig(userConfig);
|
|
70
71
|
resolved.include = resolveIncludePatterns(resolved.include, root);
|
|
71
72
|
resolved.css = resolveCssPaths(resolved.css, root);
|
|
73
|
+
if (resolved.static)
|
|
74
|
+
resolved.static = resolve(root, resolved.static);
|
|
72
75
|
return resolved;
|
|
73
76
|
}
|
|
74
77
|
/** Import a config file (.js/.mjs; .ts only on Node with native type stripping) */
|
|
@@ -89,6 +92,7 @@ export function resolveConfig(userConfig) {
|
|
|
89
92
|
port: userConfig.port ?? DEFAULTS.port,
|
|
90
93
|
open: userConfig.open ?? DEFAULTS.open,
|
|
91
94
|
css: userConfig.css ?? DEFAULTS.css,
|
|
95
|
+
static: userConfig.static ?? DEFAULTS.static,
|
|
92
96
|
logo: userConfig.logo ?? DEFAULTS.logo,
|
|
93
97
|
icon: userConfig.icon ?? DEFAULTS.icon,
|
|
94
98
|
sidebar: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Highlight source code and return HTML
|
|
1
|
+
/** Highlight source code and return HTML. Unknown languages render as
|
|
2
|
+
* plaintext, so a fence is always a themed shiki block, never a bare pre. */
|
|
2
3
|
export declare function highlight(code: string, lang?: string): Promise<string>;
|
|
3
4
|
/** Dispose the highlighter (for cleanup) */
|
|
4
5
|
export declare function disposeHighlighter(): Promise<void>;
|
|
@@ -5,16 +5,21 @@ async function getHighlighter() {
|
|
|
5
5
|
if (!highlighterPromise) {
|
|
6
6
|
highlighterPromise = createHighlighter({
|
|
7
7
|
themes: ['github-light', 'github-dark'],
|
|
8
|
-
langs: [
|
|
8
|
+
langs: [
|
|
9
|
+
'svelte', 'typescript', 'javascript', 'css', 'html',
|
|
10
|
+
'bash', 'json', 'markdown', 'yaml', 'diff',
|
|
11
|
+
],
|
|
9
12
|
});
|
|
10
13
|
}
|
|
11
14
|
return highlighterPromise;
|
|
12
15
|
}
|
|
13
|
-
/** Highlight source code and return HTML
|
|
16
|
+
/** Highlight source code and return HTML. Unknown languages render as
|
|
17
|
+
* plaintext, so a fence is always a themed shiki block, never a bare pre. */
|
|
14
18
|
export async function highlight(code, lang = 'svelte') {
|
|
15
19
|
const highlighter = await getHighlighter();
|
|
20
|
+
const resolved = highlighter.getLoadedLanguages().includes(lang) ? lang : 'text';
|
|
16
21
|
return highlighter.codeToHtml(code, {
|
|
17
|
-
lang,
|
|
22
|
+
lang: resolved,
|
|
18
23
|
themes: {
|
|
19
24
|
light: 'github-light',
|
|
20
25
|
dark: 'github-dark',
|
|
@@ -11,6 +11,9 @@ import type { TocHeading } from '../types.js';
|
|
|
11
11
|
export interface RenderedPage {
|
|
12
12
|
html: string;
|
|
13
13
|
toc: TocHeading[];
|
|
14
|
+
/** Text of the body's first `#` heading, when present — it takes over as
|
|
15
|
+
* the page's displayed title. */
|
|
16
|
+
bodyTitle?: string;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
19
|
* Render a [PAGE] body: Svelte islands (see segmentPageBody) pass through
|
|
@@ -97,26 +97,56 @@ function plainText(tokens) {
|
|
|
97
97
|
export async function renderPageMarkdown(source) {
|
|
98
98
|
const toc = [];
|
|
99
99
|
const usedIds = new Set();
|
|
100
|
+
const state = { toc, usedIds, bodyTitle: undefined };
|
|
100
101
|
const parts = [];
|
|
101
102
|
for (const segment of segmentPageBody(source)) {
|
|
102
103
|
if (segment.kind === 'island') {
|
|
103
104
|
parts.push(segment.lines.join('\n'));
|
|
104
105
|
}
|
|
105
106
|
else {
|
|
106
|
-
parts.push(await renderProse(segment.lines.join('\n'),
|
|
107
|
+
parts.push(await renderProse(segment.lines.join('\n'), state));
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
|
-
return { html: parts.join('\n'), toc };
|
|
110
|
+
return { html: parts.join('\n'), toc, bodyTitle: state.bodyTitle };
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
/** GitHub-style alert kinds recognized on a blockquote's first line. */
|
|
113
|
+
const ALERT_KINDS = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'];
|
|
114
|
+
const ALERT_RE = new RegExp(`^\\[!(${ALERT_KINDS.join('|')})\\][ \\t]*(?:\\n|$)`);
|
|
115
|
+
async function renderProse(source, state) {
|
|
116
|
+
const { toc, usedIds } = state;
|
|
112
117
|
const headingIds = new WeakMap();
|
|
113
118
|
const fenceHtml = new WeakMap();
|
|
119
|
+
const alertKinds = new WeakMap();
|
|
114
120
|
const marked = new Marked({ gfm: true });
|
|
115
121
|
const tokens = marked.lexer(source);
|
|
122
|
+
/** Detect a GitHub-style alert marker on a blockquote's first line and
|
|
123
|
+
* strip it from the tokens; the renderer wraps the rest as a callout. */
|
|
124
|
+
const detectAlert = (token) => {
|
|
125
|
+
if (!('tokens' in token) || !token.tokens)
|
|
126
|
+
return;
|
|
127
|
+
const para = token.tokens[0];
|
|
128
|
+
if (para?.type !== 'paragraph' || !para.tokens)
|
|
129
|
+
return;
|
|
130
|
+
const first = para.tokens[0];
|
|
131
|
+
if (first?.type !== 'text')
|
|
132
|
+
return;
|
|
133
|
+
const m = ALERT_RE.exec(first.text ?? '');
|
|
134
|
+
if (!m)
|
|
135
|
+
return;
|
|
136
|
+
alertKinds.set(token, m[1]);
|
|
137
|
+
first.text = first.text.slice(m[0].length);
|
|
138
|
+
if (first.text === '')
|
|
139
|
+
para.tokens.shift();
|
|
140
|
+
if (para.tokens.length === 0)
|
|
141
|
+
token.tokens.shift();
|
|
142
|
+
};
|
|
116
143
|
const walk = (list) => {
|
|
117
144
|
for (const token of list) {
|
|
118
145
|
if (token.type === 'heading') {
|
|
119
146
|
const text = plainText(token.tokens ?? []);
|
|
147
|
+
if (token.depth === 1 && state.bodyTitle === undefined) {
|
|
148
|
+
state.bodyTitle = text;
|
|
149
|
+
}
|
|
120
150
|
const baseId = slugify(text);
|
|
121
151
|
let id = baseId;
|
|
122
152
|
for (let n = 2; usedIds.has(id); n++)
|
|
@@ -127,6 +157,8 @@ async function renderProse(source, toc, usedIds) {
|
|
|
127
157
|
toc.push({ text, level: token.depth, id });
|
|
128
158
|
}
|
|
129
159
|
}
|
|
160
|
+
if (token.type === 'blockquote')
|
|
161
|
+
detectAlert(token);
|
|
130
162
|
if ('tokens' in token && token.tokens)
|
|
131
163
|
walk(token.tokens);
|
|
132
164
|
if ('items' in token && token.items)
|
|
@@ -155,6 +187,31 @@ async function renderProse(source, toc, usedIds) {
|
|
|
155
187
|
? `<h${token.depth} id="${id}">${html}</h${token.depth}>\n`
|
|
156
188
|
: `<h${token.depth}>${html}</h${token.depth}>\n`;
|
|
157
189
|
},
|
|
190
|
+
// GitHub-style alerts: `> [!NOTE]` blockquotes become callouts.
|
|
191
|
+
blockquote(token) {
|
|
192
|
+
const body = this.parser.parse(token.tokens ?? []);
|
|
193
|
+
const kind = alertKinds.get(token);
|
|
194
|
+
if (!kind)
|
|
195
|
+
return `<blockquote>\n${body}</blockquote>\n`;
|
|
196
|
+
const label = kind.charAt(0) + kind.slice(1).toLowerCase();
|
|
197
|
+
return `<div class="sdocs-alert sdocs-alert-${kind.toLowerCase()}"><p class="sdocs-alert-label">${label}</p>\n${body}</div>\n`;
|
|
198
|
+
},
|
|
199
|
+
// External links open away from the docs app; hrefs stay inert.
|
|
200
|
+
link(token) {
|
|
201
|
+
const inner = this.parser.parseInline(token.tokens ?? []);
|
|
202
|
+
const href = escapeBraces(escapeHtml(token.href ?? ''));
|
|
203
|
+
const title = token.title ? ` title="${escapeBraces(escapeHtml(token.title))}"` : '';
|
|
204
|
+
const external = /^https?:\/\//i.test(token.href ?? '');
|
|
205
|
+
const attrs = external ? ' target="_blank" rel="noopener noreferrer"' : '';
|
|
206
|
+
return `<a href="${href}"${title}${attrs}>${inner}</a>`;
|
|
207
|
+
},
|
|
208
|
+
// Self-closing and brace-escaped so images are always Svelte-safe.
|
|
209
|
+
image(token) {
|
|
210
|
+
const src = escapeBraces(escapeHtml(token.href ?? ''));
|
|
211
|
+
const alt = escapeBraces(escapeHtml(token.text ?? ''));
|
|
212
|
+
const title = token.title ? ` title="${escapeBraces(escapeHtml(token.title))}"` : '';
|
|
213
|
+
return `<img src="${src}" alt="${alt}"${title} loading="lazy" />`;
|
|
214
|
+
},
|
|
158
215
|
code(token) {
|
|
159
216
|
return (fenceHtml.get(token) ??
|
|
160
217
|
`<pre><code>${escapeBraces(escapeHtml(token.text))}</code></pre>\n`);
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export interface SdocsConfig {
|
|
|
8
8
|
open?: boolean;
|
|
9
9
|
/** CSS loaded in preview iframes. Single path or named stylesheets. */
|
|
10
10
|
css?: string | Record<string, string>;
|
|
11
|
+
/** Folder of static assets served at the site root — images for pages,
|
|
12
|
+
* files for previews. Standalone CLI flows (`sdocs dev`/`build`); when
|
|
13
|
+
* embedding the Vite plugin, use the host app's own public directory. */
|
|
14
|
+
static?: string;
|
|
11
15
|
/** Sidebar logo text. Default: 'sdocs' */
|
|
12
16
|
logo?: string;
|
|
13
17
|
/** Sidebar logo icon: 'sdocs' for the built-in mascot, an image URL, or false to hide. Default: 'sdocs' */
|
|
@@ -25,6 +29,9 @@ export interface SdocsConfig {
|
|
|
25
29
|
page?: ContentSizing & {
|
|
26
30
|
/** Show the table of contents. Default: true */
|
|
27
31
|
toc?: boolean;
|
|
32
|
+
/** Horizontal alignment of the content column (with its toc) inside
|
|
33
|
+
* the view: 'left'|'center'|'right'. Default: 'left' */
|
|
34
|
+
contentX?: string;
|
|
28
35
|
};
|
|
29
36
|
/** [DOCS] pages: maxWidth is the content column (default '1200px');
|
|
30
37
|
* padding/direction/gap are the default preview/example stage layout
|
|
@@ -66,6 +73,7 @@ export interface ResolvedSdocsConfig {
|
|
|
66
73
|
port: number;
|
|
67
74
|
open: boolean;
|
|
68
75
|
css: string | Record<string, string> | null;
|
|
76
|
+
static: string | null;
|
|
69
77
|
logo: string;
|
|
70
78
|
icon: string | false;
|
|
71
79
|
sidebar: {
|
|
@@ -75,6 +83,7 @@ export interface ResolvedSdocsConfig {
|
|
|
75
83
|
content: {
|
|
76
84
|
page: Required<ContentSizing> & {
|
|
77
85
|
toc: boolean;
|
|
86
|
+
contentX: string;
|
|
78
87
|
};
|
|
79
88
|
docs: Required<ContentSizing> & {
|
|
80
89
|
direction: string;
|
|
@@ -186,8 +195,13 @@ export interface DocEntry {
|
|
|
186
195
|
maxWidth?: string;
|
|
187
196
|
/** Resolved page padding (pages only) */
|
|
188
197
|
padding?: string;
|
|
198
|
+
/** Resolved horizontal alignment of the content column (pages only) */
|
|
199
|
+
contentX?: string;
|
|
189
200
|
/** Resolved table-of-contents visibility (pages only) */
|
|
190
201
|
showToc?: boolean;
|
|
202
|
+
/** The body's `#` heading, shown as the page title instead of the entity
|
|
203
|
+
* title (pages only) */
|
|
204
|
+
bodyTitle?: string;
|
|
191
205
|
/** Key into the virtual module's pageModules map (pages only) */
|
|
192
206
|
contentKey?: string;
|
|
193
207
|
}
|
package/dist/ui/styles/sdocs.css
CHANGED
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
text-decoration: none;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Highlighted code blocks always render shiki's dark theme — a dark stage
|
|
32
|
+
* for code in both app themes (the inline light-theme colors lose to the
|
|
33
|
+
* dual-theme --shiki-dark variables). */
|
|
34
|
+
.sdocs-app pre.shiki,
|
|
35
|
+
.sdocs-app pre.shiki span {
|
|
36
|
+
color: var(--shiki-dark) !important;
|
|
37
|
+
background-color: var(--shiki-dark-bg) !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
/* Type chips and value literals, color-coded by kind (shared by doc tables) */
|
|
32
41
|
.sdocs-app code[class*='sdocs-type-'] {
|
|
33
42
|
font-family: var(--mono);
|
package/dist/vite.js
CHANGED
|
@@ -386,6 +386,8 @@ export function sdocsPlugin(userConfig) {
|
|
|
386
386
|
entry.content = snippets[0];
|
|
387
387
|
entry.toc = rendered.toc;
|
|
388
388
|
entry.padding = entity.sizing.padding ?? config.content.page.padding;
|
|
389
|
+
entry.contentX = entity.sizing.contentX ?? config.content.page.contentX;
|
|
390
|
+
entry.bodyTitle = rendered.bodyTitle;
|
|
389
391
|
entry.contentKey = encodeEntityId(filePath, entity.slug);
|
|
390
392
|
entry.examples = snippets.filter((s) => s.role === 'example');
|
|
391
393
|
entity.examples.forEach((example, i) => {
|
|
@@ -436,7 +438,9 @@ export function sdocsPlugin(userConfig) {
|
|
|
436
438
|
toc: e.toc,
|
|
437
439
|
maxWidth: e.maxWidth,
|
|
438
440
|
padding: e.padding,
|
|
441
|
+
contentX: e.contentX,
|
|
439
442
|
showToc: e.showToc,
|
|
443
|
+
bodyTitle: e.bodyTitle,
|
|
440
444
|
contentKey: e.contentKey,
|
|
441
445
|
}));
|
|
442
446
|
// Extract named CSS stylesheet names (empty array if single string or null)
|