@entropicwarrior/sdoc 0.2.0 → 0.2.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/README.md +28 -10
- package/docs/reference/sdoc-authoring.sdoc +16 -1
- package/lexica/specification.sdoc +12 -2
- package/package.json +3 -3
- package/src/sdoc.js +217 -5
- package/tools/sdoc2html +32 -0
package/README.md
CHANGED
|
@@ -87,20 +87,26 @@ All `.sdoc` files are designed for progressive disclosure — read the `@about`
|
|
|
87
87
|
{
|
|
88
88
|
Unlimited nesting. Each scope is independently addressable.
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
```python
|
|
91
|
+
def hello():
|
|
92
|
+
print("Hello from SDOC")
|
|
93
|
+
```
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
#
|
|
96
|
+
# Status :example
|
|
97
97
|
{
|
|
98
|
-
{[
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
{[table 60% center]
|
|
99
|
+
Endpoint | Status
|
|
100
|
+
/v2/api | {+Active+}
|
|
101
|
+
/v1/api | {-Deprecated-}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
# Internal Notes :comment
|
|
106
|
+
{
|
|
107
|
+
This scope is invisible in rendered output but
|
|
108
|
+
stays in the AST for tooling and agents.
|
|
109
|
+
}
|
|
104
110
|
}
|
|
105
111
|
```
|
|
106
112
|
|
|
@@ -153,7 +159,7 @@ Markdown-style images with optional width and alignment:
|
|
|
153
159
|
|
|
154
160
|
### Tables
|
|
155
161
|
|
|
156
|
-
Pipe-delimited tables with optional `borderless` and `
|
|
162
|
+
Pipe-delimited tables with optional flags for appearance (`borderless`, `headerless`), width (`auto`, `60%`, `400px`), and alignment (`left`, `center`, `right`). All flags compose freely.
|
|
157
163
|
|
|
158
164
|
### Lists
|
|
159
165
|
|
|
@@ -167,6 +173,18 @@ Tag any section with `@id` and cross-reference it anywhere with `@id` — render
|
|
|
167
173
|
|
|
168
174
|
Turn any SDOC file into an HTML slide deck with themes, layouts (center, two-column), speaker notes, and PDF export.
|
|
169
175
|
|
|
176
|
+
### Scope Types
|
|
177
|
+
|
|
178
|
+
Classify scopes with a `:type` annotation — `:schema`, `:warning`, `:deprecated`, `:example`, or any custom label. Types render as `data-scope-type` attributes and CSS classes for styling.
|
|
179
|
+
|
|
180
|
+
### Data Blocks
|
|
181
|
+
|
|
182
|
+
Tag a JSON code fence with `:data` and the parser validates and stores the parsed result on the AST node. `extractDataBlocks()` gives programmatic access. Ideal for embedding schemas, configs, and structured metadata alongside prose.
|
|
183
|
+
|
|
184
|
+
### Comment Scopes
|
|
185
|
+
|
|
186
|
+
A `:comment` scope is excluded from rendered output but stays in the AST — perfect for agent instructions, internal notes, and build metadata that readers shouldn't see.
|
|
187
|
+
|
|
170
188
|
### Custom Styling
|
|
171
189
|
|
|
172
190
|
Per-folder `sdoc.config.json` or per-file `@meta` scope for custom CSS, headers, footers, and confidentiality banners. Configs cascade from workspace root to file.
|
|
@@ -286,7 +286,7 @@ Content of Section B.
|
|
|
286
286
|
\`\{~text~\}\` | Highlight (yellow)
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
Links: \`[Link text](https://example.com)\`
|
|
289
|
+
Links: \`[Link text](https://example.com)\` or \`[Other doc](./other-file.sdoc)\`. Relative paths resolve from the document's directory.
|
|
290
290
|
|
|
291
291
|
Images: \`\`
|
|
292
292
|
|
|
@@ -671,5 +671,20 @@ Content of Section B.
|
|
|
671
671
|
}
|
|
672
672
|
```
|
|
673
673
|
}
|
|
674
|
+
|
|
675
|
+
# @References Inside Link Labels @refs-in-link-labels
|
|
676
|
+
{
|
|
677
|
+
Inline \`@references\` are parsed everywhere, including inside link labels. If you mention a scope ID in a link label, escape the \`@\` to prevent it being treated as a reference to the current document:
|
|
678
|
+
|
|
679
|
+
**Wrong:** \`[See domain-model.sdoc @my-section](./domain-model.sdoc#my-section)\`
|
|
680
|
+
|
|
681
|
+
The \`@my-section\` is parsed as a reference and flagged as broken (it does not exist in *this* file).
|
|
682
|
+
|
|
683
|
+
**Right:** \`[See domain-model.sdoc \\@my-section](./domain-model.sdoc#my-section)\`
|
|
684
|
+
|
|
685
|
+
Or simply omit the \`@\` from the label — the URL fragment already carries the target:
|
|
686
|
+
|
|
687
|
+
**Also right:** \`[See domain-model.sdoc § my-section](./domain-model.sdoc#my-section)\`
|
|
688
|
+
}
|
|
674
689
|
}
|
|
675
690
|
}
|
|
@@ -395,16 +395,26 @@ Content of Section B.
|
|
|
395
395
|
- A reference is `@id` in text (unescaped)
|
|
396
396
|
- References link to the scope with that ID
|
|
397
397
|
- ID uniqueness is strongly recommended; tooling may warn on duplicates
|
|
398
|
+
- References are parsed inside link labels — use `\@` to include a literal `@` in a link label without triggering a reference
|
|
398
399
|
}
|
|
399
400
|
}
|
|
400
401
|
|
|
401
|
-
#
|
|
402
|
+
# Links @links
|
|
402
403
|
{
|
|
403
|
-
Markdown-style links:
|
|
404
|
+
Markdown-style links with absolute URLs or relative file paths:
|
|
404
405
|
|
|
405
406
|
```
|
|
406
407
|
[label](https://example.com)
|
|
408
|
+
[other doc](./other-file.sdoc)
|
|
409
|
+
[parent doc](../guide/intro.sdoc)
|
|
407
410
|
```
|
|
411
|
+
|
|
412
|
+
{[.]
|
|
413
|
+
- Absolute URLs (any scheme) open externally
|
|
414
|
+
- Relative paths are resolved from the document's directory
|
|
415
|
+
- Fragments (`./file.sdoc#section`) and query strings are stripped for file resolution
|
|
416
|
+
- Tooling may warn on broken relative links (target file does not exist)
|
|
417
|
+
}
|
|
408
418
|
}
|
|
409
419
|
|
|
410
420
|
# Autolinks @autolinks
|
package/package.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"name": "@entropicwarrior/sdoc",
|
|
3
3
|
"displayName": "SDOC",
|
|
4
4
|
"description": "A plain-text documentation format with explicit brace scoping — deterministic parsing, AI-agent efficiency, and 10-50x token savings vs Markdown.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.2",
|
|
6
6
|
"publisher": "entropicwarrior",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/entropicwarrior/sdoc"
|
|
10
|
+
"url": "git+https://github.com/entropicwarrior/sdoc.git"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://github.com/entropicwarrior/sdoc",
|
|
13
13
|
"bugs": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"ai-agent"
|
|
23
23
|
],
|
|
24
24
|
"bin": {
|
|
25
|
-
"sdoc-sync-notion": "
|
|
25
|
+
"sdoc-sync-notion": "tools/sync-notion.js"
|
|
26
26
|
},
|
|
27
27
|
"exports": {
|
|
28
28
|
".": "./index.js",
|
package/src/sdoc.js
CHANGED
|
@@ -1379,6 +1379,9 @@ function renderInlineNodes(nodes) {
|
|
|
1379
1379
|
return escapeHtml(node.value);
|
|
1380
1380
|
case "ref": {
|
|
1381
1381
|
const href = `#${escapeAttr(node.id)}`;
|
|
1382
|
+
if (_renderOptions.brokenRefIds && _renderOptions.brokenRefIds.has(node.id)) {
|
|
1383
|
+
return `<a class="sdoc-ref sdoc-broken-ref" href="${href}"><span class="sdoc-broken-icon">\u26A0</span>@${escapeHtml(node.id)}</a>`;
|
|
1384
|
+
}
|
|
1382
1385
|
return `<a class="sdoc-ref" href="${href}">@${escapeHtml(node.id)}</a>`;
|
|
1383
1386
|
}
|
|
1384
1387
|
case "code":
|
|
@@ -1404,6 +1407,11 @@ function renderInlineNodes(nodes) {
|
|
|
1404
1407
|
case "mark_highlight":
|
|
1405
1408
|
return `<mark class="sdoc-mark sdoc-mark-highlight">${renderInlineNodes(node.children)}</mark>`;
|
|
1406
1409
|
case "link":
|
|
1410
|
+
if (_renderOptions.brokenLinkHrefs && _renderOptions.brokenLinkHrefs.has(node.href)) {
|
|
1411
|
+
return `<a class="sdoc-link sdoc-broken-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer"><span class="sdoc-broken-icon">\u26A0</span>${renderInlineNodes(
|
|
1412
|
+
node.children
|
|
1413
|
+
)}</a>`;
|
|
1414
|
+
}
|
|
1407
1415
|
return `<a class="sdoc-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer">${renderInlineNodes(
|
|
1408
1416
|
node.children
|
|
1409
1417
|
)}</a>`;
|
|
@@ -1981,6 +1989,19 @@ const DEFAULT_STYLE = `
|
|
|
1981
1989
|
.sdoc-mark-negative { background-color: rgba(210, 25, 25, 0.18); color: #a81414; }
|
|
1982
1990
|
.sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
|
|
1983
1991
|
|
|
1992
|
+
.sdoc-broken-ref, .sdoc-link.sdoc-broken-link {
|
|
1993
|
+
color: #c33;
|
|
1994
|
+
text-decoration: wavy underline #c33;
|
|
1995
|
+
text-underline-offset: 2px;
|
|
1996
|
+
background: rgba(204, 51, 51, 0.08);
|
|
1997
|
+
border-radius: 2px;
|
|
1998
|
+
padding: 0 0.15em;
|
|
1999
|
+
}
|
|
2000
|
+
.sdoc-broken-icon {
|
|
2001
|
+
font-size: 0.75em;
|
|
2002
|
+
margin-right: 0.15em;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1984
2005
|
.sdoc-image {
|
|
1985
2006
|
display: inline-block;
|
|
1986
2007
|
max-width: 100%;
|
|
@@ -2093,11 +2114,44 @@ const PRINT_STYLE = `
|
|
|
2093
2114
|
word-wrap: break-word;
|
|
2094
2115
|
}
|
|
2095
2116
|
.sdoc-mark { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
2117
|
+
.sdoc-table { break-inside: avoid; }
|
|
2118
|
+
.sdoc-code { break-inside: avoid; }
|
|
2119
|
+
.sdoc-blockquote { break-inside: avoid; }
|
|
2096
2120
|
}
|
|
2097
2121
|
`;
|
|
2098
2122
|
|
|
2099
2123
|
const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
|
|
2100
2124
|
const KATEX_CDN_CSS = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
|
|
2125
|
+
const HLJS_CDN = "https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.11.1/highlight.min.js";
|
|
2126
|
+
|
|
2127
|
+
// Custom highlight.js grammar for SDOC language – registered before highlightElement calls
|
|
2128
|
+
const HLJS_SDOC_GRAMMAR = `hljs.registerLanguage('sdoc',function(hljs){return{name:'SDOC',aliases:['sdoc'],contains:[
|
|
2129
|
+
hljs.COMMENT('^\\\\s*//','$'),
|
|
2130
|
+
{className:'section',begin:'^\\\\s*#+\\\\s+',end:'$',contains:[
|
|
2131
|
+
{className:'symbol',begin:'@[A-Za-z_][A-Za-z0-9_-]*'}
|
|
2132
|
+
]},
|
|
2133
|
+
{className:'meta',begin:'^\\\\s*@[A-Za-z_][A-Za-z0-9_-]*',end:'(?=\\\\s*\\\\{|$)'},
|
|
2134
|
+
{className:'code',begin:'\`\`\`',end:'\`\`\`',contains:[hljs.BACKSLASH_ESCAPE]},
|
|
2135
|
+
{className:'code',begin:'\`[^\`]+\`'},
|
|
2136
|
+
{className:'string',begin:'\\\\[',end:'\\\\]\\\\([^)]*\\\\)',contains:[
|
|
2137
|
+
{className:'link',begin:'\\\\(',end:'\\\\)'}
|
|
2138
|
+
]},
|
|
2139
|
+
{className:'keyword',begin:'\\\\{[!?+\\\\-=~^]',end:'[!?+\\\\-=~^]\\\\}'},
|
|
2140
|
+
{className:'keyword',begin:'\\\\{\\\\[(?:\\\\.|#|\\\\d+|table)\\\\]'},
|
|
2141
|
+
{className:'strong',begin:'\\\\*\\\\*',end:'\\\\*\\\\*'},
|
|
2142
|
+
{className:'emphasis',begin:'(?<!\\\\*)\\\\*(?!\\\\*)',end:'\\\\*(?!\\\\*)'},
|
|
2143
|
+
{className:'deletion',begin:'~~',end:'~~'},
|
|
2144
|
+
{className:'bullet',begin:'^\\\\s*[-]\\\\s'},
|
|
2145
|
+
{className:'bullet',begin:'^\\\\s*\\\\d+[.)]\\\\ '},
|
|
2146
|
+
{className:'quote',begin:'^\\\\s*>',end:'$'},
|
|
2147
|
+
{className:'symbol',begin:'(?<!\\\\\\\\)@[A-Za-z_][A-Za-z0-9_-]*'},
|
|
2148
|
+
{className:'attr',begin:'[A-Za-z_][A-Za-z0-9_-]*(?=\\\\s*:)',end:':',excludeEnd:true}
|
|
2149
|
+
]}});`;
|
|
2150
|
+
|
|
2151
|
+
// Highlight.js GitHub light theme (inline so it works inside shadow DOM in the web viewer)
|
|
2152
|
+
const HLJS_LIGHT_CSS = "pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}";
|
|
2153
|
+
// Highlight.js GitHub dark theme color overrides (used in @media and VS Code dark-mode CSS)
|
|
2154
|
+
const HLJS_DARK_COLORS_CSS = ".hljs{color:#c9d1d9;background:transparent}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}";
|
|
2101
2155
|
|
|
2102
2156
|
function hasMermaidBlocks(nodes) {
|
|
2103
2157
|
for (const node of nodes) {
|
|
@@ -2112,9 +2166,21 @@ function hasMermaidBlocks(nodes) {
|
|
|
2112
2166
|
return false;
|
|
2113
2167
|
}
|
|
2114
2168
|
|
|
2115
|
-
function
|
|
2116
|
-
|
|
2117
|
-
|
|
2169
|
+
function hasHighlightableCodeBlocks(nodes) {
|
|
2170
|
+
for (const node of nodes) {
|
|
2171
|
+
if (node.type === "code" && node.lang && node.lang !== "mermaid" && node.lang !== "math") return true;
|
|
2172
|
+
if (node.children && hasHighlightableCodeBlocks(node.children)) return true;
|
|
2173
|
+
if (node.items) {
|
|
2174
|
+
for (const item of node.items) {
|
|
2175
|
+
if (item.children && hasHighlightableCodeBlocks(item.children)) return true;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function renderBodyNodes(nodes) {
|
|
2183
|
+
return nodes
|
|
2118
2184
|
.map((node, index) => {
|
|
2119
2185
|
if (node.type === "scope" && index === 0) {
|
|
2120
2186
|
return renderScope(node, 1, true);
|
|
@@ -2122,6 +2188,17 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2122
2188
|
return renderNode(node, 1);
|
|
2123
2189
|
})
|
|
2124
2190
|
.join("\n");
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function renderHtmlBody(text) {
|
|
2194
|
+
const parsed = parseSdoc(text);
|
|
2195
|
+
const metaResult = extractMeta(parsed.nodes);
|
|
2196
|
+
return renderBodyNodes(metaResult.nodes);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
2200
|
+
_renderOptions = options.renderOptions ?? {};
|
|
2201
|
+
const body = renderBodyNodes(parsed.nodes);
|
|
2125
2202
|
_renderOptions = {};
|
|
2126
2203
|
const errorHtml = renderErrors(parsed.errors);
|
|
2127
2204
|
|
|
@@ -2152,6 +2229,15 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2152
2229
|
const katexCssTag = body.includes('class="katex"')
|
|
2153
2230
|
? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
|
|
2154
2231
|
: "";
|
|
2232
|
+
const hasHljs = hasHighlightableCodeBlocks(parsed.nodes);
|
|
2233
|
+
// Highlight.js CSS is inlined (not a <link>) so it is extracted by parseDocHtml in the web viewer
|
|
2234
|
+
// and applied inside shadow DOM. The @media query handles dark mode in browsers.
|
|
2235
|
+
const hljsCssInline = hasHljs
|
|
2236
|
+
? `\n${HLJS_LIGHT_CSS}\n.sdoc-code code.hljs{padding:0;background:transparent}\n@media (prefers-color-scheme:dark){${HLJS_DARK_COLORS_CSS}}`
|
|
2237
|
+
: "";
|
|
2238
|
+
const hljsScript = hasHljs
|
|
2239
|
+
? `\n<script src="${HLJS_CDN}"></script>\n<script>${HLJS_SDOC_GRAMMAR}\ndocument.querySelectorAll('pre.sdoc-code code[class*="language-"]').forEach(function(b){hljs.highlightElement(b);});</script>`
|
|
2240
|
+
: "";
|
|
2155
2241
|
|
|
2156
2242
|
return `<!DOCTYPE html>
|
|
2157
2243
|
<html lang="en">
|
|
@@ -2160,7 +2246,7 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2160
2246
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2161
2247
|
<title>${escapeHtml(title)}</title>
|
|
2162
2248
|
<style>
|
|
2163
|
-
${cssBase}${cssAppend}
|
|
2249
|
+
${cssBase}${cssAppend}${hljsCssInline}
|
|
2164
2250
|
</style>${katexCssTag}
|
|
2165
2251
|
</head>
|
|
2166
2252
|
<body>
|
|
@@ -2174,7 +2260,7 @@ ${cssBase}${cssAppend}
|
|
|
2174
2260
|
</main>
|
|
2175
2261
|
</div>
|
|
2176
2262
|
${footerContent ? `<footer class="sdoc-page-footer">${footerContent}</footer>` : ""}
|
|
2177
|
-
</div>${scriptTag}${mermaidScript}
|
|
2263
|
+
</div>${scriptTag}${mermaidScript}${hljsScript}
|
|
2178
2264
|
</body>
|
|
2179
2265
|
</html>`;
|
|
2180
2266
|
}
|
|
@@ -2430,6 +2516,127 @@ function extractAbout(nodes) {
|
|
|
2430
2516
|
return null;
|
|
2431
2517
|
}
|
|
2432
2518
|
|
|
2519
|
+
function collectAllIds(nodes) {
|
|
2520
|
+
const ids = new Set();
|
|
2521
|
+
function walk(nodeList) {
|
|
2522
|
+
for (const node of nodeList) {
|
|
2523
|
+
if (node.type === "scope") {
|
|
2524
|
+
if (node.id) ids.add(node.id);
|
|
2525
|
+
if (node.title) ids.add(slugify(node.title));
|
|
2526
|
+
}
|
|
2527
|
+
if (node.children) walk(node.children);
|
|
2528
|
+
if (node.type === "list" && node.items) {
|
|
2529
|
+
walk(node.items);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
walk(nodes);
|
|
2534
|
+
return ids;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
function collectInlineRefs(nodes) {
|
|
2538
|
+
const refs = [];
|
|
2539
|
+
const links = [];
|
|
2540
|
+
|
|
2541
|
+
function walkInlineNodes(inlineNodes, lineStart, lineEnd) {
|
|
2542
|
+
for (const node of inlineNodes) {
|
|
2543
|
+
if (node.type === "ref") {
|
|
2544
|
+
refs.push({ id: node.id, lineStart, lineEnd });
|
|
2545
|
+
} else if (node.type === "link") {
|
|
2546
|
+
links.push({ href: node.href, lineStart, lineEnd });
|
|
2547
|
+
}
|
|
2548
|
+
if (node.children) {
|
|
2549
|
+
walkInlineNodes(node.children, lineStart, lineEnd);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function processText(text, lineStart, lineEnd) {
|
|
2555
|
+
const inlineNodes = parseInline(text);
|
|
2556
|
+
walkInlineNodes(inlineNodes, lineStart, lineEnd);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function walk(nodeList) {
|
|
2560
|
+
for (const node of nodeList) {
|
|
2561
|
+
if (node.type === "paragraph" && node.text) {
|
|
2562
|
+
processText(node.text, node.lineStart, node.lineEnd);
|
|
2563
|
+
} else if (node.type === "blockquote" && node.paragraphs) {
|
|
2564
|
+
for (const para of node.paragraphs) {
|
|
2565
|
+
processText(para, node.lineStart, node.lineEnd);
|
|
2566
|
+
}
|
|
2567
|
+
} else if (node.type === "scope") {
|
|
2568
|
+
if (node.title) {
|
|
2569
|
+
processText(node.title, node.lineStart, node.lineStart);
|
|
2570
|
+
}
|
|
2571
|
+
if (node.children) walk(node.children);
|
|
2572
|
+
} else if (node.type === "list" && node.items) {
|
|
2573
|
+
walk(node.items);
|
|
2574
|
+
} else if (node.type === "table") {
|
|
2575
|
+
if (node.headers) {
|
|
2576
|
+
for (const cell of node.headers) {
|
|
2577
|
+
processText(cell, node.lineStart, node.lineEnd);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
if (node.rows) {
|
|
2581
|
+
for (const row of node.rows) {
|
|
2582
|
+
for (const cell of row) {
|
|
2583
|
+
processText(cell, node.lineStart, node.lineEnd);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
// Handle list items (no type field, but have title/children)
|
|
2589
|
+
if (!node.type) {
|
|
2590
|
+
if (node.title) processText(node.title, node.lineStart || 0, node.lineEnd || node.lineStart || 0);
|
|
2591
|
+
if (node.children) walk(node.children);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
walk(nodes);
|
|
2596
|
+
return { refs, links };
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function validateRefs(nodes, options = {}) {
|
|
2600
|
+
const ids = collectAllIds(nodes);
|
|
2601
|
+
const externalIds = options.externalIds || new Set();
|
|
2602
|
+
const { refs, links } = collectInlineRefs(nodes);
|
|
2603
|
+
const warnings = [];
|
|
2604
|
+
|
|
2605
|
+
for (const ref of refs) {
|
|
2606
|
+
if (!ids.has(ref.id) && !externalIds.has(ref.id)) {
|
|
2607
|
+
warnings.push({
|
|
2608
|
+
type: "broken-ref",
|
|
2609
|
+
id: ref.id,
|
|
2610
|
+
message: `Broken reference: @${ref.id} does not match any scope ID or title`,
|
|
2611
|
+
lineStart: ref.lineStart,
|
|
2612
|
+
lineEnd: ref.lineEnd
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
for (const link of links) {
|
|
2618
|
+
const href = link.href;
|
|
2619
|
+
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href) || href.startsWith("#") || href.startsWith("data:")) {
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
if (options.resolveFilePath) {
|
|
2623
|
+
const filePath = href.split("#")[0].split("?")[0];
|
|
2624
|
+
if (!filePath) continue;
|
|
2625
|
+
if (!options.resolveFilePath(filePath)) {
|
|
2626
|
+
warnings.push({
|
|
2627
|
+
type: "broken-link",
|
|
2628
|
+
href,
|
|
2629
|
+
message: `Broken link: file not found — ${href}`,
|
|
2630
|
+
lineStart: link.lineStart,
|
|
2631
|
+
lineEnd: link.lineEnd
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
return warnings;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2433
2640
|
async function resolveIncludes(nodes, resolverFn) {
|
|
2434
2641
|
for (const node of nodes) {
|
|
2435
2642
|
if (node.type === "code" && node.src) {
|
|
@@ -2466,6 +2673,7 @@ module.exports = {
|
|
|
2466
2673
|
resolveIncludes,
|
|
2467
2674
|
renderFragment,
|
|
2468
2675
|
renderTextParagraphs,
|
|
2676
|
+
renderHtmlBody,
|
|
2469
2677
|
renderHtmlDocumentFromParsed,
|
|
2470
2678
|
renderHtmlDocument,
|
|
2471
2679
|
formatSdoc,
|
|
@@ -2476,6 +2684,10 @@ module.exports = {
|
|
|
2476
2684
|
extractAbout,
|
|
2477
2685
|
extractDataBlocks,
|
|
2478
2686
|
KNOWN_SCOPE_TYPES,
|
|
2687
|
+
// Validation
|
|
2688
|
+
collectAllIds,
|
|
2689
|
+
collectInlineRefs,
|
|
2690
|
+
validateRefs,
|
|
2479
2691
|
// Low-level helpers for custom renderers (e.g. slide-renderer)
|
|
2480
2692
|
parseInline,
|
|
2481
2693
|
renderKatex,
|
package/tools/sdoc2html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SDOC → HTML fragment translator
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node tools/sdoc2html input.sdoc # read from file
|
|
6
|
+
// cat input.sdoc | node tools/sdoc2html # read from stdin
|
|
7
|
+
//
|
|
8
|
+
// Writes an HTML fragment to stdout (no <!DOCTYPE>, no <html>/<body> wrapper).
|
|
9
|
+
// Designed for integration with github/markup as a translator script.
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const { renderHtmlBody } = require("../src/sdoc");
|
|
13
|
+
|
|
14
|
+
function main() {
|
|
15
|
+
let input;
|
|
16
|
+
const filename = process.argv[2];
|
|
17
|
+
|
|
18
|
+
if (filename) {
|
|
19
|
+
try {
|
|
20
|
+
input = fs.readFileSync(filename, "utf8");
|
|
21
|
+
} catch (err) {
|
|
22
|
+
process.stderr.write(`sdoc2html: ${err.message}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
input = fs.readFileSync(0, "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.stdout.write(renderHtmlBody(input) + "\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main();
|