sdocs 0.0.51 → 0.0.53
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 +33 -0
- package/dist/explorer/views/ComponentView.svelte +3 -3
- package/dist/explorer/views/PageView.svelte +2 -2
- package/dist/grammar/sdoc.tmLanguage.json +88 -0
- package/dist/language/index.d.ts +2 -1
- package/dist/language/index.js +1 -0
- package/dist/language/page-islands.d.ts +23 -0
- package/dist/language/page-islands.js +122 -0
- package/dist/language/parser.d.ts +16 -1
- package/dist/language/parser.js +32 -5
- package/dist/server/config.js +10 -0
- package/dist/server/page-markdown.d.ts +7 -0
- package/dist/server/page-markdown.js +21 -1
- package/dist/server/snippet-compiler.d.ts +6 -1
- package/dist/server/snippet-compiler.js +22 -2
- package/dist/types.d.ts +48 -2
- package/dist/vite.js +31 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.53] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Content layout is configurable — globally and in place.** The config's
|
|
15
|
+
`content` option sets per-kind defaults, and the same names work as entity
|
|
16
|
+
and block attributes (block beats entity beats config):
|
|
17
|
+
- `maxWidth` — the content column for `[DOCS]`/`[PAGE]` (default `1200px`),
|
|
18
|
+
the stage for `[LAYOUT]` (default `100%`) and for `[preview]`/`[example]`
|
|
19
|
+
(default `100%`; narrower stages center).
|
|
20
|
+
- `padding` — page content `32px`, preview/example stages `16px`
|
|
21
|
+
(settable on `[DOCS]` as the entity default), layout stages `0px`.
|
|
22
|
+
- `direction` / `gap` — preview/example stages are flex containers:
|
|
23
|
+
`flex-direction` (default `row`) and `gap` (default `16px`).
|
|
24
|
+
- `toc` — `[PAGE]` table-of-contents visibility (default `true`;
|
|
25
|
+
`toc="false"` hides it).
|
|
26
|
+
|
|
27
|
+
## [0.0.52] - 2026-07-05
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Svelte islands in `[PAGE]` bodies.** A line opening a component/HTML tag
|
|
32
|
+
or a Svelte block (`{#snippet`, `{@render`) after a blank line starts an
|
|
33
|
+
island that passes to Svelte verbatim, until its tags and blocks balance —
|
|
34
|
+
blank lines inside are fine. Markdown can no longer split a snippet across
|
|
35
|
+
paragraphs or cut an HTML section in half. Snippets declared anywhere on
|
|
36
|
+
the page are renderable from anywhere (they land at the top level of the
|
|
37
|
+
compiled fragment), so one `{#snippet}` can serve many sections.
|
|
38
|
+
- **Svelte coloring inside page bodies.** `{#…}…{/…}` blocks highlight as
|
|
39
|
+
Svelte throughout — keywords, typed params, and the markup inside them —
|
|
40
|
+
and `{@…}` tags color as keywords with TypeScript interiors, instead of
|
|
41
|
+
HTML-guess colors. Fenced code and inline code stay inert.
|
|
42
|
+
|
|
10
43
|
## [0.0.51] - 2026-07-05
|
|
11
44
|
|
|
12
45
|
### Changed
|
|
@@ -288,7 +288,7 @@
|
|
|
288
288
|
);
|
|
289
289
|
</script>
|
|
290
290
|
|
|
291
|
-
<div class="sdocs-component-view">
|
|
291
|
+
<div class="sdocs-component-view" style:max-width={doc.maxWidth}>
|
|
292
292
|
{#if focusedSnippet}
|
|
293
293
|
<!-- Example full-page view -->
|
|
294
294
|
<div class="sdocs-view-header">
|
|
@@ -482,7 +482,7 @@
|
|
|
482
482
|
<style>
|
|
483
483
|
.sdocs-component-view {
|
|
484
484
|
padding: 24px 32px;
|
|
485
|
-
max-width
|
|
485
|
+
/* max-width comes from the doc entry (config/entity cascade) */
|
|
486
486
|
font-family: var(--sans);
|
|
487
487
|
}
|
|
488
488
|
.sdocs-doc-section {
|
|
@@ -579,8 +579,8 @@
|
|
|
579
579
|
overflow: hidden;
|
|
580
580
|
}
|
|
581
581
|
.sdocs-preview-wrapper {
|
|
582
|
+
/* stage padding lives inside the iframe (config/entity/block cascade) */
|
|
582
583
|
background: var(--color-base-0);
|
|
583
|
-
padding: 16px;
|
|
584
584
|
}
|
|
585
585
|
.sdocs-divider {
|
|
586
586
|
border: none;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
25
|
<div class="sdocs-page-view">
|
|
26
|
-
<div class="sdocs-page-main">
|
|
26
|
+
<div class="sdocs-page-main" style:max-width={doc.maxWidth}>
|
|
27
27
|
<!-- Header -->
|
|
28
28
|
<div class="sdocs-view-header">
|
|
29
29
|
<h1 class="sdocs-view-title">{displayTitle(meta.title)}</h1>
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
</div>
|
|
42
42
|
|
|
43
43
|
<!-- Table of Contents -->
|
|
44
|
-
{#if toc.length > 0}
|
|
44
|
+
{#if toc.length > 0 && doc.showToc !== false}
|
|
45
45
|
<aside class="sdocs-toc">
|
|
46
46
|
<h3 class="sdocs-toc-title">On this page</h3>
|
|
47
47
|
<nav>
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
"fileTypes": [
|
|
6
6
|
"sdoc"
|
|
7
7
|
],
|
|
8
|
+
"injections": {
|
|
9
|
+
"L:meta.embedded.block.markdown - (markup.fenced_code.block.markdown, markup.inline.raw.string.markdown, meta.template.block.sdoc)": {
|
|
10
|
+
"patterns": [
|
|
11
|
+
{
|
|
12
|
+
"include": "#page-svelte-tag"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"include": "#page-svelte-expression"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
8
20
|
"patterns": [
|
|
9
21
|
{
|
|
10
22
|
"include": "#comment"
|
|
@@ -232,6 +244,9 @@
|
|
|
232
244
|
"while": "(^|\\G)(?!\\s*\\[/PAGE\\]\\s*$)(?:\\t|[ ]{1,4})?",
|
|
233
245
|
"contentName": "meta.embedded.block.markdown",
|
|
234
246
|
"patterns": [
|
|
247
|
+
{
|
|
248
|
+
"include": "#page-svelte-block"
|
|
249
|
+
},
|
|
235
250
|
{
|
|
236
251
|
"include": "text.html.markdown"
|
|
237
252
|
}
|
|
@@ -239,6 +254,79 @@
|
|
|
239
254
|
}
|
|
240
255
|
]
|
|
241
256
|
},
|
|
257
|
+
"page-svelte-block": {
|
|
258
|
+
"name": "meta.template.block.sdoc",
|
|
259
|
+
"begin": "(\\{#[a-zA-Z]+)([^}]*)(\\})",
|
|
260
|
+
"beginCaptures": {
|
|
261
|
+
"1": {
|
|
262
|
+
"name": "keyword.control.svelte.sdoc"
|
|
263
|
+
},
|
|
264
|
+
"2": {
|
|
265
|
+
"patterns": [
|
|
266
|
+
{
|
|
267
|
+
"include": "source.ts"
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
},
|
|
271
|
+
"3": {
|
|
272
|
+
"name": "keyword.control.svelte.sdoc"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"end": "(\\{/[a-zA-Z]+\\})",
|
|
276
|
+
"endCaptures": {
|
|
277
|
+
"1": {
|
|
278
|
+
"name": "keyword.control.svelte.sdoc"
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
"patterns": [
|
|
282
|
+
{
|
|
283
|
+
"include": "source.svelte"
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
"page-svelte-tag": {
|
|
288
|
+
"name": "meta.template.block.sdoc",
|
|
289
|
+
"begin": "(\\{)([@:][a-zA-Z]+)",
|
|
290
|
+
"beginCaptures": {
|
|
291
|
+
"1": {
|
|
292
|
+
"name": "punctuation.definition.block.begin.sdoc"
|
|
293
|
+
},
|
|
294
|
+
"2": {
|
|
295
|
+
"name": "keyword.control.svelte.sdoc"
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"end": "(\\})",
|
|
299
|
+
"endCaptures": {
|
|
300
|
+
"1": {
|
|
301
|
+
"name": "punctuation.definition.block.end.sdoc"
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"patterns": [
|
|
305
|
+
{
|
|
306
|
+
"include": "source.ts"
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
},
|
|
310
|
+
"page-svelte-expression": {
|
|
311
|
+
"name": "meta.template.expression.sdoc",
|
|
312
|
+
"begin": "(\\{)(?![#/@:\\s}])(?=[^{}]*\\})",
|
|
313
|
+
"beginCaptures": {
|
|
314
|
+
"1": {
|
|
315
|
+
"name": "punctuation.definition.expression.begin.sdoc"
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
"end": "(\\})",
|
|
319
|
+
"endCaptures": {
|
|
320
|
+
"1": {
|
|
321
|
+
"name": "punctuation.definition.expression.end.sdoc"
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
"patterns": [
|
|
325
|
+
{
|
|
326
|
+
"include": "source.ts"
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
},
|
|
242
330
|
"layout": {
|
|
243
331
|
"name": "meta.block.layout.sdoc",
|
|
244
332
|
"begin": "^\\s*(\\[)(LAYOUT)\\b(.*)$",
|
package/dist/language/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, type Span, type EntityKind, type SubBlockKind, type AttrValue, type Attrs, type SubBlock, type Entity, type TagBlock, type ScanError, type SdocFile, } from './scanner.js';
|
|
2
2
|
export { projectSdoc, type SdocProjection, type ProjectedLineKind, } from './projection.js';
|
|
3
|
-
export {
|
|
3
|
+
export { segmentPageBody, type PageSegment, } from './page-islands.js';
|
|
4
|
+
export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, type ArgValue, type Sizing, type PreviewBlock, type ExampleBlock, type DocsEntity, type PageEntity, type LayoutEntity, type SdocEntity, type SdocDocument, } from './parser.js';
|
package/dist/language/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, } from './scanner.js';
|
|
2
2
|
export { projectSdoc, } from './projection.js';
|
|
3
|
+
export { segmentPageBody, } from './page-islands.js';
|
|
3
4
|
export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, } from './parser.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Island segmentation for [PAGE] bodies.
|
|
3
|
+
*
|
|
4
|
+
* A page body is markdown prose interleaved with Svelte "islands" — component
|
|
5
|
+
* tags, HTML sections, and template blocks like {#snippet}…{/snippet}. Markdown
|
|
6
|
+
* tooling (the renderer, prettier) must never look inside an island: markdown's
|
|
7
|
+
* paragraph and HTML-block rules split Svelte structure apart (a snippet opened
|
|
8
|
+
* inside a <p> and closed outside fails to compile; a blank line cuts a <div>
|
|
9
|
+
* section in half; a formatter re-indents block lines it can't understand).
|
|
10
|
+
*
|
|
11
|
+
* The rule an author sees: a line that starts a Svelte block ({#…}, {@render …})
|
|
12
|
+
* or an HTML/component tag, sitting at the start of a markdown block (preceded
|
|
13
|
+
* by a blank line or the body edge), begins an island. The island runs until
|
|
14
|
+
* its Svelte blocks and HTML tags are balanced — blank lines inside are part of
|
|
15
|
+
* the island. Everything else is prose.
|
|
16
|
+
*/
|
|
17
|
+
export interface PageSegment {
|
|
18
|
+
kind: 'prose' | 'island';
|
|
19
|
+
/** Verbatim lines of the segment */
|
|
20
|
+
lines: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Split a page body into prose and island segments. */
|
|
23
|
+
export declare function segmentPageBody(body: string): PageSegment[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Island segmentation for [PAGE] bodies.
|
|
3
|
+
*
|
|
4
|
+
* A page body is markdown prose interleaved with Svelte "islands" — component
|
|
5
|
+
* tags, HTML sections, and template blocks like {#snippet}…{/snippet}. Markdown
|
|
6
|
+
* tooling (the renderer, prettier) must never look inside an island: markdown's
|
|
7
|
+
* paragraph and HTML-block rules split Svelte structure apart (a snippet opened
|
|
8
|
+
* inside a <p> and closed outside fails to compile; a blank line cuts a <div>
|
|
9
|
+
* section in half; a formatter re-indents block lines it can't understand).
|
|
10
|
+
*
|
|
11
|
+
* The rule an author sees: a line that starts a Svelte block ({#…}, {@render …})
|
|
12
|
+
* or an HTML/component tag, sitting at the start of a markdown block (preceded
|
|
13
|
+
* by a blank line or the body edge), begins an island. The island runs until
|
|
14
|
+
* its Svelte blocks and HTML tags are balanced — blank lines inside are part of
|
|
15
|
+
* the island. Everything else is prose.
|
|
16
|
+
*/
|
|
17
|
+
/** Void elements never contribute to tag depth. */
|
|
18
|
+
const VOID_TAGS = new Set([
|
|
19
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
20
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
21
|
+
]);
|
|
22
|
+
/** Does this line begin an island (at a markdown block start)? */
|
|
23
|
+
function startsIsland(line) {
|
|
24
|
+
const t = line.trimStart();
|
|
25
|
+
return /^\{[#/@]/.test(t) || /^<[A-Za-z]/.test(t) || /^<\//.test(t);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Net Svelte block depth and HTML tag depth contributed by one line.
|
|
29
|
+
* Line-based and heuristic by design: quoted attribute values are skipped,
|
|
30
|
+
* self-closing and void tags are neutral. Unbalanced input never throws —
|
|
31
|
+
* the island simply extends (and Svelte reports the real error).
|
|
32
|
+
*/
|
|
33
|
+
function lineDepths(line) {
|
|
34
|
+
let svelte = 0;
|
|
35
|
+
let html = 0;
|
|
36
|
+
for (let i = 0; i < line.length; i++) {
|
|
37
|
+
const ch = line[i];
|
|
38
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
39
|
+
for (i++; i < line.length && line[i] !== ch; i++) {
|
|
40
|
+
if (line[i] === '\\')
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '{') {
|
|
46
|
+
if (line[i + 1] === '#')
|
|
47
|
+
svelte++;
|
|
48
|
+
else if (line[i + 1] === '/')
|
|
49
|
+
svelte--;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch !== '<')
|
|
53
|
+
continue;
|
|
54
|
+
const rest = line.slice(i);
|
|
55
|
+
const close = rest.match(/^<\/([A-Za-z][A-Za-z0-9-]*)/);
|
|
56
|
+
if (close) {
|
|
57
|
+
if (!VOID_TAGS.has(close[1].toLowerCase()))
|
|
58
|
+
html--;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const open = rest.match(/^<([A-Za-z][A-Za-z0-9-]*)/);
|
|
62
|
+
if (!open)
|
|
63
|
+
continue;
|
|
64
|
+
if (VOID_TAGS.has(open[1].toLowerCase()))
|
|
65
|
+
continue;
|
|
66
|
+
// Find the tag's own '>' (skipping quoted attrs) to detect self-closing.
|
|
67
|
+
let j = i + open[0].length;
|
|
68
|
+
for (; j < line.length; j++) {
|
|
69
|
+
const c = line[j];
|
|
70
|
+
if (c === '"' || c === "'") {
|
|
71
|
+
for (j++; j < line.length && line[j] !== c; j++)
|
|
72
|
+
;
|
|
73
|
+
}
|
|
74
|
+
else if (c === '>')
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (j >= line.length || line[j - 1] !== '/')
|
|
78
|
+
html++;
|
|
79
|
+
// Self-closing (`/>`) is neutral; an unterminated tag counts as open.
|
|
80
|
+
}
|
|
81
|
+
return { svelte, html };
|
|
82
|
+
}
|
|
83
|
+
/** Split a page body into prose and island segments. */
|
|
84
|
+
export function segmentPageBody(body) {
|
|
85
|
+
const lines = body.split('\n');
|
|
86
|
+
const segments = [];
|
|
87
|
+
let current = null;
|
|
88
|
+
const push = (kind, line) => {
|
|
89
|
+
if (current?.kind !== kind) {
|
|
90
|
+
current = { kind, lines: [] };
|
|
91
|
+
segments.push(current);
|
|
92
|
+
}
|
|
93
|
+
current.lines.push(line);
|
|
94
|
+
};
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < lines.length) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
const prevBlank = i === 0 || lines[i - 1].trim() === '';
|
|
99
|
+
if (prevBlank && line.trim() !== '' && startsIsland(line)) {
|
|
100
|
+
// Consume the island: until Svelte blocks and HTML tags balance out.
|
|
101
|
+
let svelte = 0;
|
|
102
|
+
let html = 0;
|
|
103
|
+
let j = i;
|
|
104
|
+
for (; j < lines.length; j++) {
|
|
105
|
+
const d = lineDepths(lines[j]);
|
|
106
|
+
svelte += d.svelte;
|
|
107
|
+
html += d.html;
|
|
108
|
+
if (svelte <= 0 && html <= 0)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
const end = Math.min(j, lines.length - 1);
|
|
112
|
+
for (let k = i; k <= end; k++)
|
|
113
|
+
push('island', lines[k]);
|
|
114
|
+
current = null; // consecutive islands stay separate segments
|
|
115
|
+
i = end + 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
push('prose', line);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
return segments;
|
|
122
|
+
}
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type ScanError, type Span, type TagBlock } from './scanner.js';
|
|
7
7
|
export type ArgValue = string | number | boolean;
|
|
8
|
+
/** Explicit presentation overrides from stage attributes */
|
|
9
|
+
export interface Sizing {
|
|
10
|
+
maxWidth: string | null;
|
|
11
|
+
padding: string | null;
|
|
12
|
+
/** flex-direction of preview/example stages */
|
|
13
|
+
direction: string | null;
|
|
14
|
+
/** gap of preview/example stages */
|
|
15
|
+
gap: string | null;
|
|
16
|
+
/** table-of-contents visibility (PAGE) */
|
|
17
|
+
toc: boolean | null;
|
|
18
|
+
}
|
|
8
19
|
export interface PreviewBlock {
|
|
9
20
|
/** The identifier from component={X}; null when invalid/missing (already reported) */
|
|
10
21
|
componentName: string | null;
|
|
@@ -16,12 +27,14 @@ export interface PreviewBlock {
|
|
|
16
27
|
title: string | null;
|
|
17
28
|
/** Tab label: the title override or the component name */
|
|
18
29
|
label: string;
|
|
30
|
+
sizing: Sizing;
|
|
19
31
|
body: string;
|
|
20
32
|
bodySpan: Span;
|
|
21
33
|
span: Span;
|
|
22
34
|
}
|
|
23
35
|
export interface ExampleBlock {
|
|
24
36
|
title: string;
|
|
37
|
+
sizing: Sizing;
|
|
25
38
|
body: string;
|
|
26
39
|
bodySpan: Span;
|
|
27
40
|
span: Span;
|
|
@@ -31,6 +44,7 @@ export interface DocsEntity {
|
|
|
31
44
|
title: string;
|
|
32
45
|
slug: string;
|
|
33
46
|
description: string | null;
|
|
47
|
+
sizing: Sizing;
|
|
34
48
|
previews: PreviewBlock[];
|
|
35
49
|
examples: ExampleBlock[];
|
|
36
50
|
openerSpan: Span;
|
|
@@ -40,6 +54,7 @@ export interface PageEntity {
|
|
|
40
54
|
kind: 'PAGE';
|
|
41
55
|
title: string;
|
|
42
56
|
slug: string;
|
|
57
|
+
sizing: Sizing;
|
|
43
58
|
body: string;
|
|
44
59
|
bodySpan: Span;
|
|
45
60
|
openerSpan: Span;
|
|
@@ -49,7 +64,7 @@ export interface LayoutEntity {
|
|
|
49
64
|
kind: 'LAYOUT';
|
|
50
65
|
title: string;
|
|
51
66
|
slug: string;
|
|
52
|
-
|
|
67
|
+
sizing: Sizing;
|
|
53
68
|
body: string;
|
|
54
69
|
bodySpan: Span;
|
|
55
70
|
openerSpan: Span;
|
package/dist/language/parser.js
CHANGED
|
@@ -43,17 +43,39 @@ export function slugifyTitle(title) {
|
|
|
43
43
|
.replace(/^-+|-+$/g, '') || 'untitled');
|
|
44
44
|
}
|
|
45
45
|
const IDENTIFIER_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
|
46
|
+
const SIZING_ATTR_RULES = {
|
|
47
|
+
maxWidth: { required: false, kind: 'string', hint: 'maxWidth="1200px"' },
|
|
48
|
+
padding: { required: false, kind: 'string', hint: 'padding="32px"' },
|
|
49
|
+
};
|
|
50
|
+
const STAGE_LAYOUT_ATTR_RULES = {
|
|
51
|
+
direction: { required: false, kind: 'string', hint: 'direction="column"' },
|
|
52
|
+
gap: { required: false, kind: 'string', hint: 'gap="16px"' },
|
|
53
|
+
};
|
|
54
|
+
function sizingOf(attrs) {
|
|
55
|
+
const toc = stringAttr(attrs, 'toc');
|
|
56
|
+
return {
|
|
57
|
+
maxWidth: stringAttr(attrs, 'maxWidth'),
|
|
58
|
+
padding: stringAttr(attrs, 'padding'),
|
|
59
|
+
direction: stringAttr(attrs, 'direction'),
|
|
60
|
+
gap: stringAttr(attrs, 'gap'),
|
|
61
|
+
toc: toc === null ? null : toc === 'true',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
46
64
|
const ENTITY_ATTR_RULES = {
|
|
47
65
|
DOCS: {
|
|
48
66
|
title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
|
|
49
67
|
description: { required: false, kind: 'string', hint: 'description="…"' },
|
|
68
|
+
...SIZING_ATTR_RULES,
|
|
69
|
+
...STAGE_LAYOUT_ATTR_RULES,
|
|
50
70
|
},
|
|
51
71
|
PAGE: {
|
|
52
72
|
title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
|
|
73
|
+
...SIZING_ATTR_RULES,
|
|
74
|
+
toc: { required: false, kind: 'string', hint: 'toc="false"' },
|
|
53
75
|
},
|
|
54
76
|
LAYOUT: {
|
|
55
77
|
title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
|
|
56
|
-
|
|
78
|
+
...SIZING_ATTR_RULES,
|
|
57
79
|
},
|
|
58
80
|
};
|
|
59
81
|
const SUB_BLOCK_ATTR_RULES = {
|
|
@@ -61,9 +83,13 @@ const SUB_BLOCK_ATTR_RULES = {
|
|
|
61
83
|
component: { required: true, kind: 'expression', hint: 'component={Button}' },
|
|
62
84
|
args: { required: false, kind: 'expression', hint: 'args={{ label: "Hi" }}' },
|
|
63
85
|
title: { required: false, kind: 'string', hint: 'title="…"' },
|
|
86
|
+
...SIZING_ATTR_RULES,
|
|
87
|
+
...STAGE_LAYOUT_ATTR_RULES,
|
|
64
88
|
},
|
|
65
89
|
example: {
|
|
66
90
|
title: { required: true, kind: 'string', hint: 'title="…"' },
|
|
91
|
+
...SIZING_ATTR_RULES,
|
|
92
|
+
...STAGE_LAYOUT_ATTR_RULES,
|
|
67
93
|
},
|
|
68
94
|
};
|
|
69
95
|
function checkAttrs(owner, attrs, rules, ownerSpan, diagnostics) {
|
|
@@ -192,6 +218,7 @@ function parsePreview(block, diagnostics) {
|
|
|
192
218
|
argsRaw,
|
|
193
219
|
title,
|
|
194
220
|
label: title ?? componentName ?? 'Preview',
|
|
221
|
+
sizing: sizingOf(block.attrs),
|
|
195
222
|
body: normalizeBody(block.body),
|
|
196
223
|
bodySpan: block.bodySpan,
|
|
197
224
|
span: block.span,
|
|
@@ -229,6 +256,7 @@ function parseDocs(entity, diagnostics) {
|
|
|
229
256
|
exampleTitles.add(title);
|
|
230
257
|
examples.push({
|
|
231
258
|
title,
|
|
259
|
+
sizing: sizingOf(block.attrs),
|
|
232
260
|
body: normalizeBody(block.body),
|
|
233
261
|
bodySpan: block.bodySpan,
|
|
234
262
|
span: block.span,
|
|
@@ -241,6 +269,7 @@ function parseDocs(entity, diagnostics) {
|
|
|
241
269
|
title,
|
|
242
270
|
slug: slugifyTitle(title),
|
|
243
271
|
description: stringAttr(entity.attrs, 'description'),
|
|
272
|
+
sizing: sizingOf(entity.attrs),
|
|
244
273
|
previews,
|
|
245
274
|
examples,
|
|
246
275
|
openerSpan: entity.openerSpan,
|
|
@@ -263,15 +292,13 @@ export function parseSdoc(source) {
|
|
|
263
292
|
const base = {
|
|
264
293
|
title,
|
|
265
294
|
slug: slugifyTitle(title),
|
|
295
|
+
sizing: sizingOf(entity.attrs),
|
|
266
296
|
body: normalizeBody(entity.body),
|
|
267
297
|
bodySpan: entity.bodySpan,
|
|
268
298
|
openerSpan: entity.openerSpan,
|
|
269
299
|
span: entity.span,
|
|
270
300
|
};
|
|
271
|
-
typed =
|
|
272
|
-
entity.kind === 'PAGE'
|
|
273
|
-
? { kind: 'PAGE', ...base }
|
|
274
|
-
: { kind: 'LAYOUT', padding: stringAttr(entity.attrs, 'padding'), ...base };
|
|
301
|
+
typed = entity.kind === 'PAGE' ? { kind: 'PAGE', ...base } : { kind: 'LAYOUT', ...base };
|
|
275
302
|
}
|
|
276
303
|
if (slugs.has(typed.slug)) {
|
|
277
304
|
diagnostics.push({
|
package/dist/server/config.js
CHANGED
|
@@ -13,6 +13,11 @@ const DEFAULTS = {
|
|
|
13
13
|
order: {},
|
|
14
14
|
open: [],
|
|
15
15
|
},
|
|
16
|
+
content: {
|
|
17
|
+
page: { maxWidth: '1200px', padding: '32px', toc: true },
|
|
18
|
+
docs: { maxWidth: '1200px', padding: '16px', direction: 'row', gap: '16px' },
|
|
19
|
+
layout: { maxWidth: '100%', padding: '0px' },
|
|
20
|
+
},
|
|
16
21
|
};
|
|
17
22
|
/** Find the config file path in the given directory */
|
|
18
23
|
export function findConfigFile(root) {
|
|
@@ -83,5 +88,10 @@ export function resolveConfig(userConfig) {
|
|
|
83
88
|
order: userConfig.sidebar?.order ?? DEFAULTS.sidebar.order,
|
|
84
89
|
open: userConfig.sidebar?.open ?? DEFAULTS.sidebar.open,
|
|
85
90
|
},
|
|
91
|
+
content: {
|
|
92
|
+
page: { ...DEFAULTS.content.page, ...userConfig.content?.page },
|
|
93
|
+
docs: { ...DEFAULTS.content.docs, ...userConfig.content?.docs },
|
|
94
|
+
layout: { ...DEFAULTS.content.layout, ...userConfig.content?.layout },
|
|
95
|
+
},
|
|
86
96
|
};
|
|
87
97
|
}
|
|
@@ -12,4 +12,11 @@ export interface RenderedPage {
|
|
|
12
12
|
html: string;
|
|
13
13
|
toc: TocHeading[];
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Render a [PAGE] body: Svelte islands (see segmentPageBody) pass through
|
|
17
|
+
* verbatim — markdown never splits a snippet or an HTML section — and the
|
|
18
|
+
* prose between them renders as markdown. Islands land at the top level of
|
|
19
|
+
* the compiled fragment, so a snippet declared anywhere is renderable from
|
|
20
|
+
* anywhere on the page.
|
|
21
|
+
*/
|
|
15
22
|
export declare function renderPageMarkdown(source: string): Promise<RenderedPage>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Marked } from 'marked';
|
|
11
11
|
import { highlight } from './highlighter.js';
|
|
12
|
+
import { segmentPageBody } from '../language/page-islands.js';
|
|
12
13
|
function slugify(text) {
|
|
13
14
|
return (text
|
|
14
15
|
.toLowerCase()
|
|
@@ -86,9 +87,28 @@ function plainText(tokens) {
|
|
|
86
87
|
}
|
|
87
88
|
return out;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Render a [PAGE] body: Svelte islands (see segmentPageBody) pass through
|
|
92
|
+
* verbatim — markdown never splits a snippet or an HTML section — and the
|
|
93
|
+
* prose between them renders as markdown. Islands land at the top level of
|
|
94
|
+
* the compiled fragment, so a snippet declared anywhere is renderable from
|
|
95
|
+
* anywhere on the page.
|
|
96
|
+
*/
|
|
89
97
|
export async function renderPageMarkdown(source) {
|
|
90
98
|
const toc = [];
|
|
91
99
|
const usedIds = new Set();
|
|
100
|
+
const parts = [];
|
|
101
|
+
for (const segment of segmentPageBody(source)) {
|
|
102
|
+
if (segment.kind === 'island') {
|
|
103
|
+
parts.push(segment.lines.join('\n'));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
parts.push(await renderProse(segment.lines.join('\n'), toc, usedIds));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { html: parts.join('\n'), toc };
|
|
110
|
+
}
|
|
111
|
+
async function renderProse(source, toc, usedIds) {
|
|
92
112
|
const headingIds = new WeakMap();
|
|
93
113
|
const fenceHtml = new WeakMap();
|
|
94
114
|
const marked = new Marked({ gfm: true });
|
|
@@ -161,5 +181,5 @@ export async function renderPageMarkdown(source) {
|
|
|
161
181
|
}
|
|
162
182
|
}
|
|
163
183
|
});
|
|
164
|
-
return
|
|
184
|
+
return marked.parser(tokens);
|
|
165
185
|
}
|
|
@@ -23,7 +23,12 @@ export declare function resolveScriptImports(script: string, docFilePath: string
|
|
|
23
23
|
/** Generate a virtual Svelte iframe wrapper component for a snippet.
|
|
24
24
|
* Includes $state for reactive prop updates via postMessage, invokes
|
|
25
25
|
* component methods on request, and broadcasts exported state values. */
|
|
26
|
-
export declare function generateIframeComponent(scriptPrelude: string, snippetBody: string, stateNames?: string[], componentName?: string
|
|
26
|
+
export declare function generateIframeComponent(scriptPrelude: string, snippetBody: string, stateNames?: string[], componentName?: string, stage?: {
|
|
27
|
+
maxWidth: string;
|
|
28
|
+
padding: string;
|
|
29
|
+
direction?: string;
|
|
30
|
+
gap?: string;
|
|
31
|
+
}): string;
|
|
27
32
|
/** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
|
|
28
33
|
export declare function generateMountScript(iframeComponentId: string): string;
|
|
29
34
|
/** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
|
|
@@ -81,7 +81,27 @@ function injectRootRef(snippetBody, componentName) {
|
|
|
81
81
|
/** Generate a virtual Svelte iframe wrapper component for a snippet.
|
|
82
82
|
* Includes $state for reactive prop updates via postMessage, invokes
|
|
83
83
|
* component methods on request, and broadcasts exported state values. */
|
|
84
|
-
export function generateIframeComponent(scriptPrelude, snippetBody, stateNames = [], componentName) {
|
|
84
|
+
export function generateIframeComponent(scriptPrelude, snippetBody, stateNames = [], componentName, stage) {
|
|
85
|
+
// The stage layout (config -> entity -> block cascade) applies here, inside
|
|
86
|
+
// the iframe, so every consumer of the preview page gets it. Preview and
|
|
87
|
+
// example stages are flex containers (direction + gap); page and layout
|
|
88
|
+
// stages are flow-root blocks. Both contain child margins, so the height
|
|
89
|
+
// reported for iframe auto-sizing is exact.
|
|
90
|
+
const stageStyle = [
|
|
91
|
+
...(stage?.direction
|
|
92
|
+
? [
|
|
93
|
+
'display: flex',
|
|
94
|
+
`flex-direction: ${stage.direction}`,
|
|
95
|
+
'flex-wrap: wrap',
|
|
96
|
+
'align-items: flex-start',
|
|
97
|
+
`gap: ${stage.gap}`,
|
|
98
|
+
]
|
|
99
|
+
: ['display: flow-root']),
|
|
100
|
+
...(stage ? [`padding: ${stage.padding}`] : []),
|
|
101
|
+
...(stage && stage.maxWidth !== '100%'
|
|
102
|
+
? [`max-width: ${stage.maxWidth}`, 'margin-inline: auto']
|
|
103
|
+
: []),
|
|
104
|
+
].join('; ');
|
|
85
105
|
// The file <script> (imports + shared values), lifted verbatim so previews
|
|
86
106
|
// and examples see everything the entity's siblings do.
|
|
87
107
|
const importBlock = scriptPrelude.trim() ? scriptPrelude.trim() + '\n' : '';
|
|
@@ -151,7 +171,7 @@ ${stateBroadcast}
|
|
|
151
171
|
});
|
|
152
172
|
</script>
|
|
153
173
|
|
|
154
|
-
<div id="sdocs-preview" style="
|
|
174
|
+
<div id="sdocs-preview" style="${stageStyle}">
|
|
155
175
|
{#snippet SdocsPreview(args)}
|
|
156
176
|
${injectRootRef(snippetBody, componentName)}
|
|
157
177
|
{/snippet}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,38 @@ export interface SdocsConfig {
|
|
|
19
19
|
/** Folders expanded by default on load. */
|
|
20
20
|
open?: string[];
|
|
21
21
|
};
|
|
22
|
+
/** Content presentation per entity kind; entity/block attributes override these. */
|
|
23
|
+
content?: {
|
|
24
|
+
/** [PAGE] content. Defaults: maxWidth '1200px', padding '32px', toc true. */
|
|
25
|
+
page?: ContentSizing & {
|
|
26
|
+
/** Show the table of contents. Default: true */
|
|
27
|
+
toc?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** [DOCS] pages: maxWidth is the content column (default '1200px');
|
|
30
|
+
* padding/direction/gap are the default preview/example stage layout
|
|
31
|
+
* (defaults '16px', 'row', '16px'). */
|
|
32
|
+
docs?: ContentSizing & {
|
|
33
|
+
/** Stage flex-direction. Default: 'row' */
|
|
34
|
+
direction?: string;
|
|
35
|
+
/** Stage gap. Default: '16px' */
|
|
36
|
+
gap?: string;
|
|
37
|
+
};
|
|
38
|
+
/** [LAYOUT] stages. Defaults: maxWidth '100%', padding '0px'. */
|
|
39
|
+
layout?: ContentSizing;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Content sizing knobs (any CSS length; padding takes CSS shorthand) */
|
|
43
|
+
export interface ContentSizing {
|
|
44
|
+
maxWidth?: string;
|
|
45
|
+
padding?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Resolved stage layout applied inside a preview iframe */
|
|
48
|
+
export interface StageLayout {
|
|
49
|
+
maxWidth: string;
|
|
50
|
+
padding: string;
|
|
51
|
+
/** flex-direction + gap; set for preview/example stages only */
|
|
52
|
+
direction?: string;
|
|
53
|
+
gap?: string;
|
|
22
54
|
}
|
|
23
55
|
/** Resolved config with all defaults applied */
|
|
24
56
|
export interface ResolvedSdocsConfig {
|
|
@@ -32,6 +64,16 @@ export interface ResolvedSdocsConfig {
|
|
|
32
64
|
order: Record<string, string[]>;
|
|
33
65
|
open: string[];
|
|
34
66
|
};
|
|
67
|
+
content: {
|
|
68
|
+
page: Required<ContentSizing> & {
|
|
69
|
+
toc: boolean;
|
|
70
|
+
};
|
|
71
|
+
docs: Required<ContentSizing> & {
|
|
72
|
+
direction: string;
|
|
73
|
+
gap: string;
|
|
74
|
+
};
|
|
75
|
+
layout: Required<ContentSizing>;
|
|
76
|
+
};
|
|
35
77
|
}
|
|
36
78
|
/** Entity metadata from a [DOCS]/[PAGE]/[LAYOUT] opener */
|
|
37
79
|
export interface SdocMeta {
|
|
@@ -87,6 +129,8 @@ export interface ExtractedSnippet {
|
|
|
87
129
|
highlightedHtml?: string;
|
|
88
130
|
/** Preview URL for iframe (added by virtual module) */
|
|
89
131
|
previewUrl?: string;
|
|
132
|
+
/** Resolved stage layout applied inside the iframe (config → entity → block) */
|
|
133
|
+
stage?: StageLayout;
|
|
90
134
|
}
|
|
91
135
|
/** One [preview] of a DOCS entity: a live showcase of one component */
|
|
92
136
|
export interface PreviewEntry {
|
|
@@ -128,6 +172,8 @@ export interface DocEntry {
|
|
|
128
172
|
content: ExtractedSnippet | null;
|
|
129
173
|
/** Table of contents headings (pages only) */
|
|
130
174
|
toc?: TocHeading[];
|
|
131
|
-
/**
|
|
132
|
-
|
|
175
|
+
/** Resolved content-column max width (component and page kinds) */
|
|
176
|
+
maxWidth?: string;
|
|
177
|
+
/** Resolved table-of-contents visibility (pages only) */
|
|
178
|
+
showToc?: boolean;
|
|
133
179
|
}
|
package/dist/vite.js
CHANGED
|
@@ -228,7 +228,7 @@ export function sdocsPlugin(userConfig) {
|
|
|
228
228
|
// example iframes fall back to the first preview's component.
|
|
229
229
|
const preview = entry.previews.find((p) => p.snippet.slug === snippet.slug) ?? entry.previews[0];
|
|
230
230
|
const stateNames = (preview?.componentData?.state ?? []).map((s) => s.name);
|
|
231
|
-
return generateIframeComponent(scriptPrelude, snippet.body, stateNames, preview?.componentName ?? undefined);
|
|
231
|
+
return generateIframeComponent(scriptPrelude, snippet.body, stateNames, preview?.componentName ?? undefined, snippet.stage);
|
|
232
232
|
}
|
|
233
233
|
// Virtual mount script for an emitted preview page
|
|
234
234
|
if (id.startsWith('\0' + MOUNT_PREFIX)) {
|
|
@@ -299,6 +299,27 @@ export function sdocsPlugin(userConfig) {
|
|
|
299
299
|
examples: [],
|
|
300
300
|
content: null,
|
|
301
301
|
};
|
|
302
|
+
// Sizing cascade: block attribute -> entity attribute -> config default.
|
|
303
|
+
const kindKey = entity.kind === 'DOCS' ? 'docs' : entity.kind === 'PAGE' ? 'page' : 'layout';
|
|
304
|
+
const kindDefaults = config.content[kindKey];
|
|
305
|
+
const stageOf = (block) => ({
|
|
306
|
+
// Entity-level maxWidth on DOCS/PAGE is the content column, not the
|
|
307
|
+
// stage; stages inside them span their panel unless the block says so.
|
|
308
|
+
maxWidth: block?.maxWidth ??
|
|
309
|
+
(entity.kind === 'LAYOUT' ? (entity.sizing.maxWidth ?? kindDefaults.maxWidth) : '100%'),
|
|
310
|
+
padding: block?.padding ?? entity.sizing.padding ?? kindDefaults.padding,
|
|
311
|
+
// direction/gap flex the preview/example stages only
|
|
312
|
+
...(entity.kind === 'DOCS' && block
|
|
313
|
+
? {
|
|
314
|
+
direction: block.direction ?? entity.sizing.direction ?? config.content.docs.direction,
|
|
315
|
+
gap: block.gap ?? entity.sizing.gap ?? config.content.docs.gap,
|
|
316
|
+
}
|
|
317
|
+
: {}),
|
|
318
|
+
});
|
|
319
|
+
entry.maxWidth = entity.sizing.maxWidth ?? kindDefaults.maxWidth;
|
|
320
|
+
if (entity.kind === 'PAGE') {
|
|
321
|
+
entry.showToc = entity.sizing.toc ?? config.content.page.toc;
|
|
322
|
+
}
|
|
302
323
|
if (entity.kind === 'DOCS') {
|
|
303
324
|
if (entity.description)
|
|
304
325
|
entry.meta.description = entity.description;
|
|
@@ -318,6 +339,7 @@ export function sdocsPlugin(userConfig) {
|
|
|
318
339
|
console.warn(`[sdocs] ${filePath}: component {${preview.componentName}} is not imported in the file's <script>`);
|
|
319
340
|
}
|
|
320
341
|
}
|
|
342
|
+
snippet.stage = stageOf(preview.sizing);
|
|
321
343
|
snippet.highlightedHtml = await highlight(snippet.body);
|
|
322
344
|
entry.previews.push({
|
|
323
345
|
label: preview.label,
|
|
@@ -330,6 +352,10 @@ export function sdocsPlugin(userConfig) {
|
|
|
330
352
|
});
|
|
331
353
|
}
|
|
332
354
|
entry.examples = snippets.filter((s) => s.role === 'example');
|
|
355
|
+
entity.examples.forEach((example, i) => {
|
|
356
|
+
if (entry.examples[i])
|
|
357
|
+
entry.examples[i].stage = stageOf(example.sizing);
|
|
358
|
+
});
|
|
333
359
|
for (const example of entry.examples) {
|
|
334
360
|
example.highlightedHtml = await highlight(example.body);
|
|
335
361
|
}
|
|
@@ -337,12 +363,13 @@ export function sdocsPlugin(userConfig) {
|
|
|
337
363
|
else if (entity.kind === 'PAGE') {
|
|
338
364
|
const rendered = await renderPageMarkdown(entity.body);
|
|
339
365
|
snippets[0].body = rendered.html;
|
|
366
|
+
snippets[0].stage = stageOf();
|
|
340
367
|
entry.content = snippets[0];
|
|
341
368
|
entry.toc = rendered.toc;
|
|
342
369
|
}
|
|
343
370
|
else {
|
|
371
|
+
snippets[0].stage = stageOf();
|
|
344
372
|
entry.content = snippets[0];
|
|
345
|
-
entry.padding = entity.padding;
|
|
346
373
|
}
|
|
347
374
|
docEntries.set(entityKey(filePath, entity.slug), entry);
|
|
348
375
|
}
|
|
@@ -369,7 +396,8 @@ export function sdocsPlugin(userConfig) {
|
|
|
369
396
|
examples: e.examples.map((s) => withUrl(e, s)),
|
|
370
397
|
content: e.content ? withUrl(e, e.content) : null,
|
|
371
398
|
toc: e.toc,
|
|
372
|
-
|
|
399
|
+
maxWidth: e.maxWidth,
|
|
400
|
+
showToc: e.showToc,
|
|
373
401
|
}));
|
|
374
402
|
// Extract named CSS stylesheet names (empty array if single string or null)
|
|
375
403
|
const cssNames = config.css && typeof config.css === 'object'
|