sdocs 0.0.51 → 0.0.52
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 +16 -0
- package/dist/grammar/sdoc.tmLanguage.json +88 -0
- package/dist/language/index.d.ts +1 -0
- 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/server/page-markdown.d.ts +7 -0
- package/dist/server/page-markdown.js +21 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.52] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Svelte islands in `[PAGE]` bodies.** A line opening a component/HTML tag
|
|
15
|
+
or a Svelte block (`{#snippet`, `{@render`) after a blank line starts an
|
|
16
|
+
island that passes to Svelte verbatim, until its tags and blocks balance —
|
|
17
|
+
blank lines inside are fine. Markdown can no longer split a snippet across
|
|
18
|
+
paragraphs or cut an HTML section in half. Snippets declared anywhere on
|
|
19
|
+
the page are renderable from anywhere (they land at the top level of the
|
|
20
|
+
compiled fragment), so one `{#snippet}` can serve many sections.
|
|
21
|
+
- **Svelte coloring inside page bodies.** `{#…}…{/…}` blocks highlight as
|
|
22
|
+
Svelte throughout — keywords, typed params, and the markup inside them —
|
|
23
|
+
and `{@…}` tags color as keywords with TypeScript interiors, instead of
|
|
24
|
+
HTML-guess colors. Fenced code and inline code stay inert.
|
|
25
|
+
|
|
10
26
|
## [0.0.51] - 2026-07-05
|
|
11
27
|
|
|
12
28
|
### Changed
|
|
@@ -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 { segmentPageBody, type PageSegment, } from './page-islands.js';
|
|
3
4
|
export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, type ArgValue, 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
|
+
}
|
|
@@ -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
|
}
|