@entropicwarrior/sdoc 0.1.18 → 0.2.1
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 +126 -7
- package/lexica/specification.sdoc +197 -29
- package/package.json +3 -3
- package/src/notion-renderer.js +3 -0
- package/src/sdoc.js +376 -132
- package/src/slide-renderer.js +21 -6
- package/tools/sdoc2html +32 -0
package/src/sdoc.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
const SDOC_FORMAT_VERSION = "0.
|
|
1
|
+
const SDOC_FORMAT_VERSION = "0.2";
|
|
2
|
+
|
|
3
|
+
const KNOWN_SCOPE_TYPES = [
|
|
4
|
+
"schema", "example", "requirement", "specification", "definition",
|
|
5
|
+
"note", "warning", "test", "task", "api", "config", "deprecated", "comment"
|
|
6
|
+
];
|
|
2
7
|
|
|
3
8
|
const COMMAND_HEADING = "#";
|
|
4
9
|
const COMMAND_SCOPE_OPEN = "{";
|
|
@@ -45,6 +50,10 @@ function parseTableOptions(text) {
|
|
|
45
50
|
for (const flag of flags) {
|
|
46
51
|
if (flag === "borderless") options.borderless = true;
|
|
47
52
|
else if (flag === "headerless") options.headerless = true;
|
|
53
|
+
else if (flag === "auto") options.width = "auto";
|
|
54
|
+
else if (/^\d+(?:\.\d+)?%$/.test(flag)) options.width = flag;
|
|
55
|
+
else if (/^\d+px$/.test(flag)) options.width = flag;
|
|
56
|
+
else if (flag === "center" || flag === "left" || flag === "right") options.align = flag;
|
|
48
57
|
}
|
|
49
58
|
return options;
|
|
50
59
|
}
|
|
@@ -84,15 +93,7 @@ function parseSdoc(text) {
|
|
|
84
93
|
const parsedHeading = parseHeading(cursor.current());
|
|
85
94
|
cursor.next();
|
|
86
95
|
const children = parseBlock(cursor, "normal");
|
|
87
|
-
const rootNode =
|
|
88
|
-
type: "scope",
|
|
89
|
-
title: parsedHeading.title,
|
|
90
|
-
id: parsedHeading.id,
|
|
91
|
-
children,
|
|
92
|
-
hasHeading: true,
|
|
93
|
-
lineStart: scopeStartLine,
|
|
94
|
-
lineEnd: cursor.index
|
|
95
|
-
};
|
|
96
|
+
const rootNode = makeScopeNode(parsedHeading, children, true, scopeStartLine, cursor.index);
|
|
96
97
|
return { nodes: [rootNode], errors: cursor.errors };
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -228,6 +229,12 @@ function parseBlock(cursor, kind) {
|
|
|
228
229
|
continue;
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
// Line comments — skip, don't flush paragraph (invisible to AST)
|
|
233
|
+
if (trimmedLeft.startsWith("//")) {
|
|
234
|
+
cursor.next();
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
231
238
|
if (trimmed === COMMAND_SCOPE_CLOSE) {
|
|
232
239
|
flushParagraph();
|
|
233
240
|
cursor.next();
|
|
@@ -348,6 +355,21 @@ function extractTrailingOpener(text) {
|
|
|
348
355
|
return null;
|
|
349
356
|
}
|
|
350
357
|
|
|
358
|
+
function makeScopeNode(parsedHeading, children, hasHeading, lineStart, lineEnd, extra) {
|
|
359
|
+
const node = {
|
|
360
|
+
type: "scope",
|
|
361
|
+
title: parsedHeading.title,
|
|
362
|
+
id: parsedHeading.id,
|
|
363
|
+
children,
|
|
364
|
+
hasHeading,
|
|
365
|
+
lineStart,
|
|
366
|
+
lineEnd
|
|
367
|
+
};
|
|
368
|
+
if (parsedHeading.scopeType) node.scopeType = parsedHeading.scopeType;
|
|
369
|
+
if (extra) Object.assign(node, extra);
|
|
370
|
+
return node;
|
|
371
|
+
}
|
|
372
|
+
|
|
351
373
|
function parseScope(cursor) {
|
|
352
374
|
const scopeStartLine = cursor.index + 1;
|
|
353
375
|
const headingLine = cursor.current();
|
|
@@ -369,15 +391,7 @@ function parseScope(cursor) {
|
|
|
369
391
|
} else {
|
|
370
392
|
children = parseBlock(cursor, "normal");
|
|
371
393
|
}
|
|
372
|
-
return
|
|
373
|
-
type: "scope",
|
|
374
|
-
title: parsedHeading.title,
|
|
375
|
-
id: parsedHeading.id,
|
|
376
|
-
children,
|
|
377
|
-
hasHeading: true,
|
|
378
|
-
lineStart: scopeStartLine,
|
|
379
|
-
lineEnd: cursor.index
|
|
380
|
-
};
|
|
394
|
+
return makeScopeNode(parsedHeading, children, true, scopeStartLine, cursor.index);
|
|
381
395
|
}
|
|
382
396
|
|
|
383
397
|
const parsedHeading = parseHeading(headingLine);
|
|
@@ -385,38 +399,14 @@ function parseScope(cursor) {
|
|
|
385
399
|
|
|
386
400
|
if (blockResult.blockType === "braceless") {
|
|
387
401
|
const children = parseBracelessBlock(cursor);
|
|
388
|
-
return
|
|
389
|
-
type: "scope",
|
|
390
|
-
title: parsedHeading.title,
|
|
391
|
-
id: parsedHeading.id,
|
|
392
|
-
children,
|
|
393
|
-
hasHeading: true,
|
|
394
|
-
lineStart: scopeStartLine,
|
|
395
|
-
lineEnd: cursor.index
|
|
396
|
-
};
|
|
402
|
+
return makeScopeNode(parsedHeading, children, true, scopeStartLine, cursor.index);
|
|
397
403
|
}
|
|
398
404
|
|
|
399
405
|
if (blockResult.blockType === "list") {
|
|
400
|
-
return
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
children: [blockResult.children],
|
|
405
|
-
hasHeading: true,
|
|
406
|
-
lineStart: scopeStartLine,
|
|
407
|
-
lineEnd: cursor.index
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return {
|
|
412
|
-
type: "scope",
|
|
413
|
-
title: parsedHeading.title,
|
|
414
|
-
id: parsedHeading.id,
|
|
415
|
-
children: blockResult.children,
|
|
416
|
-
hasHeading: true,
|
|
417
|
-
lineStart: scopeStartLine,
|
|
418
|
-
lineEnd: cursor.index
|
|
419
|
-
};
|
|
406
|
+
return makeScopeNode(parsedHeading, [blockResult.children], true, scopeStartLine, cursor.index);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return makeScopeNode(parsedHeading, blockResult.children, true, scopeStartLine, cursor.index);
|
|
420
410
|
}
|
|
421
411
|
|
|
422
412
|
function tryParseInlineBlock(trimmed) {
|
|
@@ -494,6 +484,12 @@ function parseBracelessBlock(cursor) {
|
|
|
494
484
|
break;
|
|
495
485
|
}
|
|
496
486
|
|
|
487
|
+
// Line comments — skip, don't flush paragraph
|
|
488
|
+
if (trimmedLeft.startsWith("//")) {
|
|
489
|
+
cursor.next();
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
497
493
|
if (trimmed === ",") {
|
|
498
494
|
flushParagraph();
|
|
499
495
|
cursor.next();
|
|
@@ -710,6 +706,9 @@ function parseListItemLine(cursor, info, allowContinuation = false) {
|
|
|
710
706
|
const textForOpener = task ? task.text : raw;
|
|
711
707
|
const trailing = extractTrailingOpener(textForOpener);
|
|
712
708
|
|
|
709
|
+
const listExtra = { shorthand: true };
|
|
710
|
+
if (task) listExtra.task = { checked: task.checked };
|
|
711
|
+
|
|
713
712
|
if (trailing) {
|
|
714
713
|
const parsed = parseHeadingText(trailing.text);
|
|
715
714
|
cursor.next();
|
|
@@ -725,17 +724,7 @@ function parseListItemLine(cursor, info, allowContinuation = false) {
|
|
|
725
724
|
children = parseBlock(cursor, "normal");
|
|
726
725
|
}
|
|
727
726
|
|
|
728
|
-
return
|
|
729
|
-
type: "scope",
|
|
730
|
-
title: parsed.title,
|
|
731
|
-
id: parsed.id,
|
|
732
|
-
children,
|
|
733
|
-
hasHeading: true,
|
|
734
|
-
shorthand: true,
|
|
735
|
-
task: task ? { checked: task.checked } : undefined,
|
|
736
|
-
lineStart: itemStartLine,
|
|
737
|
-
lineEnd: cursor.index
|
|
738
|
-
};
|
|
727
|
+
return makeScopeNode(parsed, children, true, itemStartLine, cursor.index, listExtra);
|
|
739
728
|
}
|
|
740
729
|
|
|
741
730
|
cursor.next();
|
|
@@ -756,44 +745,14 @@ function parseListItemLine(cursor, info, allowContinuation = false) {
|
|
|
756
745
|
|
|
757
746
|
const block = parseOptionalBlock(cursor);
|
|
758
747
|
if (!block) {
|
|
759
|
-
return
|
|
760
|
-
type: "scope",
|
|
761
|
-
title: parsed.title,
|
|
762
|
-
id: parsed.id,
|
|
763
|
-
children: [],
|
|
764
|
-
hasHeading: true,
|
|
765
|
-
shorthand: true,
|
|
766
|
-
task: task ? { checked: task.checked } : undefined,
|
|
767
|
-
lineStart: itemStartLine,
|
|
768
|
-
lineEnd: cursor.index
|
|
769
|
-
};
|
|
748
|
+
return makeScopeNode(parsed, [], true, itemStartLine, cursor.index, listExtra);
|
|
770
749
|
}
|
|
771
750
|
|
|
772
751
|
if (block.blockType === "list") {
|
|
773
|
-
return
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
children: [block.children],
|
|
778
|
-
hasHeading: true,
|
|
779
|
-
shorthand: true,
|
|
780
|
-
task: task ? { checked: task.checked } : undefined,
|
|
781
|
-
lineStart: itemStartLine,
|
|
782
|
-
lineEnd: cursor.index
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
return {
|
|
787
|
-
type: "scope",
|
|
788
|
-
title: parsed.title,
|
|
789
|
-
id: parsed.id,
|
|
790
|
-
children: block.children,
|
|
791
|
-
hasHeading: true,
|
|
792
|
-
shorthand: true,
|
|
793
|
-
task: task ? { checked: task.checked } : undefined,
|
|
794
|
-
lineStart: itemStartLine,
|
|
795
|
-
lineEnd: cursor.index
|
|
796
|
-
};
|
|
752
|
+
return makeScopeNode(parsed, [block.children], true, itemStartLine, cursor.index, listExtra);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return makeScopeNode(parsed, block.children, true, itemStartLine, cursor.index, listExtra);
|
|
797
756
|
}
|
|
798
757
|
|
|
799
758
|
function parseImplicitListBlock(cursor, listType) {
|
|
@@ -843,16 +802,18 @@ function parseTableBody(cursor, tableStartLine, options) {
|
|
|
843
802
|
cursor.next();
|
|
844
803
|
}
|
|
845
804
|
|
|
805
|
+
const hasOptions = options.borderless || options.headerless || options.width || options.align;
|
|
806
|
+
|
|
846
807
|
if (options.headerless) {
|
|
847
808
|
const tableNode = { type: "table", headers: [], rows, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
848
|
-
if (
|
|
809
|
+
if (hasOptions) tableNode.options = options;
|
|
849
810
|
return tableNode;
|
|
850
811
|
}
|
|
851
812
|
|
|
852
813
|
const headers = rows.length > 0 ? rows[0] : [];
|
|
853
814
|
const body = rows.slice(1);
|
|
854
815
|
const tableNode = { type: "table", headers, rows: body, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
855
|
-
if (
|
|
816
|
+
if (hasOptions) tableNode.options = options;
|
|
856
817
|
return tableNode;
|
|
857
818
|
}
|
|
858
819
|
|
|
@@ -911,7 +872,7 @@ function parseTaskPrefix(raw) {
|
|
|
911
872
|
function parseFenceMetadata(meta) {
|
|
912
873
|
if (!meta) return {};
|
|
913
874
|
const tokens = meta.split(/\s+/).filter(Boolean);
|
|
914
|
-
let lang, src, lines;
|
|
875
|
+
let lang, src, lines, data = false;
|
|
915
876
|
for (const token of tokens) {
|
|
916
877
|
if (token.startsWith("src:")) {
|
|
917
878
|
src = token.slice(4);
|
|
@@ -921,11 +882,13 @@ function parseFenceMetadata(meta) {
|
|
|
921
882
|
if (match) {
|
|
922
883
|
lines = { start: parseInt(match[1], 10), end: parseInt(match[2], 10) };
|
|
923
884
|
}
|
|
885
|
+
} else if (token === ":data") {
|
|
886
|
+
data = true;
|
|
924
887
|
} else if (!lang) {
|
|
925
888
|
lang = token;
|
|
926
889
|
}
|
|
927
890
|
}
|
|
928
|
-
return { lang, src, lines };
|
|
891
|
+
return { lang, src, lines, data };
|
|
929
892
|
}
|
|
930
893
|
|
|
931
894
|
function parseCodeBlock(cursor) {
|
|
@@ -943,26 +906,37 @@ function parseCodeBlock(cursor) {
|
|
|
943
906
|
|
|
944
907
|
const contentLines = [];
|
|
945
908
|
|
|
909
|
+
function buildCodeNode() {
|
|
910
|
+
const node = { type: "code", lang, text: stripIndent(contentLines, fenceIndent), lineStart: codeStartLine, lineEnd: cursor.index };
|
|
911
|
+
if (fenceMeta.src) node.src = fenceMeta.src;
|
|
912
|
+
if (fenceMeta.lines) node.lines = fenceMeta.lines;
|
|
913
|
+
if (fenceMeta.data) {
|
|
914
|
+
node.dataFlag = true;
|
|
915
|
+
if (lang === "json" && !node.src) {
|
|
916
|
+
try {
|
|
917
|
+
node.data = JSON.parse(node.text);
|
|
918
|
+
} catch {
|
|
919
|
+
cursor.error("Invalid JSON in :data code block.");
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return node;
|
|
924
|
+
}
|
|
925
|
+
|
|
946
926
|
while (!cursor.eof()) {
|
|
947
927
|
const nextLine = cursor.current();
|
|
948
928
|
const nextTrimmed = nextLine.replace(/^\s+/, "");
|
|
949
929
|
const closeMatch = nextTrimmed.match(/^(`{3,})\s*$/);
|
|
950
930
|
if (closeMatch && closeMatch[1].length >= fenceLen) {
|
|
951
931
|
cursor.next();
|
|
952
|
-
|
|
953
|
-
if (fenceMeta.src) node.src = fenceMeta.src;
|
|
954
|
-
if (fenceMeta.lines) node.lines = fenceMeta.lines;
|
|
955
|
-
return node;
|
|
932
|
+
return buildCodeNode();
|
|
956
933
|
}
|
|
957
934
|
contentLines.push(nextLine);
|
|
958
935
|
cursor.next();
|
|
959
936
|
}
|
|
960
937
|
|
|
961
938
|
cursor.error("Unterminated code fence.");
|
|
962
|
-
|
|
963
|
-
if (fenceMeta.src) node.src = fenceMeta.src;
|
|
964
|
-
if (fenceMeta.lines) node.lines = fenceMeta.lines;
|
|
965
|
-
return node;
|
|
939
|
+
return buildCodeNode();
|
|
966
940
|
}
|
|
967
941
|
|
|
968
942
|
/**
|
|
@@ -1016,12 +990,15 @@ function parseBlockquote(cursor) {
|
|
|
1016
990
|
function parseHeading(line) {
|
|
1017
991
|
const trimmedLeft = line.replace(/^\s+/, "");
|
|
1018
992
|
const raw = stripHeadingToken(trimmedLeft);
|
|
1019
|
-
|
|
993
|
+
const result = parseHeadingText(raw);
|
|
994
|
+
return result;
|
|
1020
995
|
}
|
|
1021
996
|
|
|
1022
997
|
function parseHeadingText(raw) {
|
|
1023
998
|
const split = splitTrailingId(raw);
|
|
1024
|
-
|
|
999
|
+
const result = { title: split.text.trimEnd(), id: split.id ? split.id.slice(1) : undefined };
|
|
1000
|
+
if (split.scopeType) result.scopeType = split.scopeType;
|
|
1001
|
+
return result;
|
|
1025
1002
|
}
|
|
1026
1003
|
|
|
1027
1004
|
function stripHeadingToken(line) {
|
|
@@ -1033,27 +1010,49 @@ function stripHeadingToken(line) {
|
|
|
1033
1010
|
}
|
|
1034
1011
|
|
|
1035
1012
|
function splitTrailingId(raw) {
|
|
1036
|
-
let
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1013
|
+
let id = undefined;
|
|
1014
|
+
let scopeType = undefined;
|
|
1015
|
+
let remaining = raw;
|
|
1040
1016
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
i
|
|
1044
|
-
|
|
1017
|
+
// Make up to two passes to extract trailing @id and :type in any order
|
|
1018
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
1019
|
+
let i = remaining.length - 1;
|
|
1020
|
+
while (i >= 0 && /\s/.test(remaining[i])) {
|
|
1021
|
+
i -= 1;
|
|
1022
|
+
}
|
|
1023
|
+
if (i < 0) break;
|
|
1024
|
+
|
|
1025
|
+
const end = i;
|
|
1026
|
+
while (i >= 0 && isIdentChar(remaining[i])) {
|
|
1027
|
+
i -= 1;
|
|
1028
|
+
}
|
|
1029
|
+
if (i < 0 || end === i) break;
|
|
1030
|
+
|
|
1031
|
+
if (remaining[i] === "@" && !id && isIdentStart(remaining[i + 1])) {
|
|
1032
|
+
if (!isEscaped(remaining, i)) {
|
|
1033
|
+
if (i === 0 || /\s/.test(remaining[i - 1])) {
|
|
1034
|
+
id = remaining.slice(i, end + 1);
|
|
1035
|
+
remaining = remaining.slice(0, i).trimEnd();
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1045
1040
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
return { text, id };
|
|
1041
|
+
if (remaining[i] === ":" && !scopeType && isIdentStart(remaining[i + 1])) {
|
|
1042
|
+
if (i === 0 || /\s/.test(remaining[i - 1])) {
|
|
1043
|
+
scopeType = remaining.slice(i + 1, end + 1);
|
|
1044
|
+
remaining = remaining.slice(0, i).trimEnd();
|
|
1045
|
+
continue;
|
|
1052
1046
|
}
|
|
1053
1047
|
}
|
|
1048
|
+
|
|
1049
|
+
break;
|
|
1054
1050
|
}
|
|
1055
1051
|
|
|
1056
|
-
|
|
1052
|
+
const result = { text: remaining };
|
|
1053
|
+
if (id) result.id = id;
|
|
1054
|
+
if (scopeType) result.scopeType = scopeType;
|
|
1055
|
+
return result;
|
|
1057
1056
|
}
|
|
1058
1057
|
|
|
1059
1058
|
function isHeadingLine(line) {
|
|
@@ -1380,6 +1379,9 @@ function renderInlineNodes(nodes) {
|
|
|
1380
1379
|
return escapeHtml(node.value);
|
|
1381
1380
|
case "ref": {
|
|
1382
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
|
+
}
|
|
1383
1385
|
return `<a class="sdoc-ref" href="${href}">@${escapeHtml(node.id)}</a>`;
|
|
1384
1386
|
}
|
|
1385
1387
|
case "code":
|
|
@@ -1405,6 +1407,11 @@ function renderInlineNodes(nodes) {
|
|
|
1405
1407
|
case "mark_highlight":
|
|
1406
1408
|
return `<mark class="sdoc-mark sdoc-mark-highlight">${renderInlineNodes(node.children)}</mark>`;
|
|
1407
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
|
+
}
|
|
1408
1415
|
return `<a class="sdoc-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer">${renderInlineNodes(
|
|
1409
1416
|
node.children
|
|
1410
1417
|
)}</a>`;
|
|
@@ -1429,13 +1436,18 @@ function renderInlineNodes(nodes) {
|
|
|
1429
1436
|
}
|
|
1430
1437
|
|
|
1431
1438
|
function renderScope(scope, depth, isTitleScope = false) {
|
|
1439
|
+
// :comment scopes are not rendered
|
|
1440
|
+
if (scope.scopeType === "comment") return "";
|
|
1441
|
+
|
|
1432
1442
|
const level = Math.min(6, Math.max(1, depth));
|
|
1433
1443
|
const children = scope.children.map((child) => renderNode(child, depth + 1)).join("\n");
|
|
1434
1444
|
const rootClass = isTitleScope ? " sdoc-root" : "";
|
|
1435
1445
|
const dl = dataLineAttrs(scope);
|
|
1446
|
+
const typeAttr = scope.scopeType ? ` data-scope-type="${escapeAttr(scope.scopeType)}"` : "";
|
|
1447
|
+
const typeClass = scope.scopeType ? ` sdoc-scope-type-${scope.scopeType}` : "";
|
|
1436
1448
|
|
|
1437
1449
|
if (scope.hasHeading === false) {
|
|
1438
|
-
return `<section class="sdoc-scope sdoc-scope-noheading${rootClass}"${dl}>${children}</section>`;
|
|
1450
|
+
return `<section class="sdoc-scope sdoc-scope-noheading${rootClass}${typeClass}"${typeAttr}${dl}>${children}</section>`;
|
|
1439
1451
|
}
|
|
1440
1452
|
|
|
1441
1453
|
const idAttr = scope.id ? ` id="${escapeAttr(scope.id)}"` : "";
|
|
@@ -1443,7 +1455,7 @@ function renderScope(scope, depth, isTitleScope = false) {
|
|
|
1443
1455
|
const toggle = hasChildren ? `<span class="sdoc-toggle"></span>` : "";
|
|
1444
1456
|
const heading = `<h${level}${idAttr} class="sdoc-heading sdoc-depth-${level}"${dl}>${toggle}${renderInline(scope.title)}</h${level}>`;
|
|
1445
1457
|
const childrenHtml = children ? `\n<div class="sdoc-scope-children">${children}</div>` : "";
|
|
1446
|
-
return `<section class="sdoc-scope${rootClass}">${heading}${childrenHtml}</section>`;
|
|
1458
|
+
return `<section class="sdoc-scope${rootClass}${typeClass}"${typeAttr}>${heading}${childrenHtml}</section>`;
|
|
1447
1459
|
}
|
|
1448
1460
|
|
|
1449
1461
|
function renderList(list, depth) {
|
|
@@ -1536,7 +1548,16 @@ function renderTable(table) {
|
|
|
1536
1548
|
.join("\n");
|
|
1537
1549
|
const tbody = bodyRows ? `<tbody class="sdoc-table-body">${bodyRows}</tbody>` : "";
|
|
1538
1550
|
|
|
1539
|
-
|
|
1551
|
+
const styleParts = [];
|
|
1552
|
+
if (opts.width) {
|
|
1553
|
+
styleParts.push(`width:${opts.width}`);
|
|
1554
|
+
if (opts.width !== "auto") styleParts.push("table-layout:fixed");
|
|
1555
|
+
}
|
|
1556
|
+
if (opts.align === "center") styleParts.push("margin-left:auto", "margin-right:auto");
|
|
1557
|
+
else if (opts.align === "right") styleParts.push("margin-left:auto", "margin-right:0");
|
|
1558
|
+
const styleAttr = styleParts.length ? ` style="${styleParts.join(";")}"` : "";
|
|
1559
|
+
|
|
1560
|
+
return `<table class="${classAttr}"${dl}${styleAttr}>${thead}${thead ? "\n" : ""}${tbody}</table>`;
|
|
1540
1561
|
}
|
|
1541
1562
|
|
|
1542
1563
|
function renderNode(node, depth) {
|
|
@@ -1568,7 +1589,8 @@ function renderNode(node, depth) {
|
|
|
1568
1589
|
return `<div class="sdoc-math sdoc-math-block"${dl}>${renderKatex(node.text, true)}</div>`;
|
|
1569
1590
|
}
|
|
1570
1591
|
const langClass = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
|
|
1571
|
-
|
|
1592
|
+
const dataLabel = node.dataFlag ? `<span class="sdoc-data-label">data</span>` : "";
|
|
1593
|
+
return `<div class="sdoc-code-wrap"${dl}>${dataLabel}<pre class="sdoc-code"><code${langClass}>${escapeHtml(node.text)}</code></pre><button class="sdoc-copy-btn" title="Copy code">\u29C9</button></div>`;
|
|
1572
1594
|
}
|
|
1573
1595
|
default:
|
|
1574
1596
|
return "";
|
|
@@ -1967,6 +1989,19 @@ const DEFAULT_STYLE = `
|
|
|
1967
1989
|
.sdoc-mark-negative { background-color: rgba(210, 25, 25, 0.18); color: #a81414; }
|
|
1968
1990
|
.sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
|
|
1969
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
|
+
|
|
1970
2005
|
.sdoc-image {
|
|
1971
2006
|
display: inline-block;
|
|
1972
2007
|
max-width: 100%;
|
|
@@ -2009,6 +2044,22 @@ const DEFAULT_STYLE = `
|
|
|
2009
2044
|
padding-left: 1.2rem;
|
|
2010
2045
|
}
|
|
2011
2046
|
|
|
2047
|
+
.sdoc-data-label {
|
|
2048
|
+
position: absolute;
|
|
2049
|
+
top: 6px;
|
|
2050
|
+
left: 8px;
|
|
2051
|
+
z-index: 1;
|
|
2052
|
+
font-size: 0.65rem;
|
|
2053
|
+
font-weight: 600;
|
|
2054
|
+
text-transform: uppercase;
|
|
2055
|
+
letter-spacing: 0.05em;
|
|
2056
|
+
padding: 1px 5px;
|
|
2057
|
+
border-radius: 3px;
|
|
2058
|
+
background: rgba(59, 130, 195, 0.15);
|
|
2059
|
+
color: #3b82c3;
|
|
2060
|
+
pointer-events: none;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2012
2063
|
`;
|
|
2013
2064
|
|
|
2014
2065
|
const PRINT_STYLE = `
|
|
@@ -2063,6 +2114,9 @@ const PRINT_STYLE = `
|
|
|
2063
2114
|
word-wrap: break-word;
|
|
2064
2115
|
}
|
|
2065
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; }
|
|
2066
2120
|
}
|
|
2067
2121
|
`;
|
|
2068
2122
|
|
|
@@ -2082,9 +2136,8 @@ function hasMermaidBlocks(nodes) {
|
|
|
2082
2136
|
return false;
|
|
2083
2137
|
}
|
|
2084
2138
|
|
|
2085
|
-
function
|
|
2086
|
-
|
|
2087
|
-
const body = parsed.nodes
|
|
2139
|
+
function renderBodyNodes(nodes) {
|
|
2140
|
+
return nodes
|
|
2088
2141
|
.map((node, index) => {
|
|
2089
2142
|
if (node.type === "scope" && index === 0) {
|
|
2090
2143
|
return renderScope(node, 1, true);
|
|
@@ -2092,6 +2145,17 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2092
2145
|
return renderNode(node, 1);
|
|
2093
2146
|
})
|
|
2094
2147
|
.join("\n");
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
function renderHtmlBody(text) {
|
|
2151
|
+
const parsed = parseSdoc(text);
|
|
2152
|
+
const metaResult = extractMeta(parsed.nodes);
|
|
2153
|
+
return renderBodyNodes(metaResult.nodes);
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
2157
|
+
_renderOptions = options.renderOptions ?? {};
|
|
2158
|
+
const body = renderBodyNodes(parsed.nodes);
|
|
2095
2159
|
_renderOptions = {};
|
|
2096
2160
|
const errorHtml = renderErrors(parsed.errors);
|
|
2097
2161
|
|
|
@@ -2314,17 +2378,35 @@ function listSections(nodes) {
|
|
|
2314
2378
|
id: node.id || null,
|
|
2315
2379
|
derivedId: slugify(node.title),
|
|
2316
2380
|
title: node.title,
|
|
2381
|
+
scopeType: node.scopeType || null,
|
|
2317
2382
|
preview: firstParagraphPreview(node.children || [], 100)
|
|
2318
2383
|
}));
|
|
2319
2384
|
}
|
|
2320
2385
|
|
|
2386
|
+
function collectDataBlocks(children) {
|
|
2387
|
+
const data = [];
|
|
2388
|
+
for (const child of children) {
|
|
2389
|
+
if (child.type === "code" && child.dataFlag && child.data !== undefined) {
|
|
2390
|
+
data.push(child.data);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return data;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2321
2396
|
function extractSection(nodes, sectionId) {
|
|
2322
2397
|
const scopes = getContentScopes(nodes);
|
|
2323
2398
|
|
|
2399
|
+
function buildResult(node) {
|
|
2400
|
+
const data = collectDataBlocks(node.children || []);
|
|
2401
|
+
const result = { title: node.title, content: collectPlainText(node.children || []) };
|
|
2402
|
+
if (data.length) result.data = data;
|
|
2403
|
+
return result;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2324
2406
|
// First pass: match explicit @id (case-sensitive)
|
|
2325
2407
|
for (const node of scopes) {
|
|
2326
2408
|
if (node.id && node.id === sectionId) {
|
|
2327
|
-
return
|
|
2409
|
+
return buildResult(node);
|
|
2328
2410
|
}
|
|
2329
2411
|
}
|
|
2330
2412
|
|
|
@@ -2332,13 +2414,41 @@ function extractSection(nodes, sectionId) {
|
|
|
2332
2414
|
const lowerTarget = sectionId.toLowerCase();
|
|
2333
2415
|
for (const node of scopes) {
|
|
2334
2416
|
if (slugify(node.title).toLowerCase() === lowerTarget) {
|
|
2335
|
-
return
|
|
2417
|
+
return buildResult(node);
|
|
2336
2418
|
}
|
|
2337
2419
|
}
|
|
2338
2420
|
|
|
2339
2421
|
return null;
|
|
2340
2422
|
}
|
|
2341
2423
|
|
|
2424
|
+
function extractDataBlocks(nodes) {
|
|
2425
|
+
const results = [];
|
|
2426
|
+
function walk(nodeList, scopeId, scopeType, scopeTitle) {
|
|
2427
|
+
for (const node of nodeList) {
|
|
2428
|
+
if (node.type === "code" && node.dataFlag && node.data !== undefined) {
|
|
2429
|
+
results.push({
|
|
2430
|
+
scopeId: scopeId || null,
|
|
2431
|
+
scopeType: scopeType || null,
|
|
2432
|
+
scopeTitle: scopeTitle || null,
|
|
2433
|
+
data: node.data
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
if (node.type === "scope") {
|
|
2437
|
+
walk(node.children || [], node.id, node.scopeType, node.title);
|
|
2438
|
+
}
|
|
2439
|
+
if (node.type === "list" && node.items) {
|
|
2440
|
+
for (const item of node.items) {
|
|
2441
|
+
if (item.type === "scope") {
|
|
2442
|
+
walk(item.children || [], item.id, item.scopeType, item.title);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
walk(nodes, null, null, null);
|
|
2449
|
+
return results;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2342
2452
|
function extractAbout(nodes) {
|
|
2343
2453
|
const doc = getDocumentScope(nodes);
|
|
2344
2454
|
const children = doc ? doc.children : nodes;
|
|
@@ -2354,6 +2464,127 @@ function extractAbout(nodes) {
|
|
|
2354
2464
|
return null;
|
|
2355
2465
|
}
|
|
2356
2466
|
|
|
2467
|
+
function collectAllIds(nodes) {
|
|
2468
|
+
const ids = new Set();
|
|
2469
|
+
function walk(nodeList) {
|
|
2470
|
+
for (const node of nodeList) {
|
|
2471
|
+
if (node.type === "scope") {
|
|
2472
|
+
if (node.id) ids.add(node.id);
|
|
2473
|
+
if (node.title) ids.add(slugify(node.title));
|
|
2474
|
+
}
|
|
2475
|
+
if (node.children) walk(node.children);
|
|
2476
|
+
if (node.type === "list" && node.items) {
|
|
2477
|
+
walk(node.items);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
walk(nodes);
|
|
2482
|
+
return ids;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function collectInlineRefs(nodes) {
|
|
2486
|
+
const refs = [];
|
|
2487
|
+
const links = [];
|
|
2488
|
+
|
|
2489
|
+
function walkInlineNodes(inlineNodes, lineStart, lineEnd) {
|
|
2490
|
+
for (const node of inlineNodes) {
|
|
2491
|
+
if (node.type === "ref") {
|
|
2492
|
+
refs.push({ id: node.id, lineStart, lineEnd });
|
|
2493
|
+
} else if (node.type === "link") {
|
|
2494
|
+
links.push({ href: node.href, lineStart, lineEnd });
|
|
2495
|
+
}
|
|
2496
|
+
if (node.children) {
|
|
2497
|
+
walkInlineNodes(node.children, lineStart, lineEnd);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function processText(text, lineStart, lineEnd) {
|
|
2503
|
+
const inlineNodes = parseInline(text);
|
|
2504
|
+
walkInlineNodes(inlineNodes, lineStart, lineEnd);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function walk(nodeList) {
|
|
2508
|
+
for (const node of nodeList) {
|
|
2509
|
+
if (node.type === "paragraph" && node.text) {
|
|
2510
|
+
processText(node.text, node.lineStart, node.lineEnd);
|
|
2511
|
+
} else if (node.type === "blockquote" && node.paragraphs) {
|
|
2512
|
+
for (const para of node.paragraphs) {
|
|
2513
|
+
processText(para, node.lineStart, node.lineEnd);
|
|
2514
|
+
}
|
|
2515
|
+
} else if (node.type === "scope") {
|
|
2516
|
+
if (node.title) {
|
|
2517
|
+
processText(node.title, node.lineStart, node.lineStart);
|
|
2518
|
+
}
|
|
2519
|
+
if (node.children) walk(node.children);
|
|
2520
|
+
} else if (node.type === "list" && node.items) {
|
|
2521
|
+
walk(node.items);
|
|
2522
|
+
} else if (node.type === "table") {
|
|
2523
|
+
if (node.headers) {
|
|
2524
|
+
for (const cell of node.headers) {
|
|
2525
|
+
processText(cell, node.lineStart, node.lineEnd);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (node.rows) {
|
|
2529
|
+
for (const row of node.rows) {
|
|
2530
|
+
for (const cell of row) {
|
|
2531
|
+
processText(cell, node.lineStart, node.lineEnd);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
// Handle list items (no type field, but have title/children)
|
|
2537
|
+
if (!node.type) {
|
|
2538
|
+
if (node.title) processText(node.title, node.lineStart || 0, node.lineEnd || node.lineStart || 0);
|
|
2539
|
+
if (node.children) walk(node.children);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
walk(nodes);
|
|
2544
|
+
return { refs, links };
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function validateRefs(nodes, options = {}) {
|
|
2548
|
+
const ids = collectAllIds(nodes);
|
|
2549
|
+
const externalIds = options.externalIds || new Set();
|
|
2550
|
+
const { refs, links } = collectInlineRefs(nodes);
|
|
2551
|
+
const warnings = [];
|
|
2552
|
+
|
|
2553
|
+
for (const ref of refs) {
|
|
2554
|
+
if (!ids.has(ref.id) && !externalIds.has(ref.id)) {
|
|
2555
|
+
warnings.push({
|
|
2556
|
+
type: "broken-ref",
|
|
2557
|
+
id: ref.id,
|
|
2558
|
+
message: `Broken reference: @${ref.id} does not match any scope ID or title`,
|
|
2559
|
+
lineStart: ref.lineStart,
|
|
2560
|
+
lineEnd: ref.lineEnd
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
for (const link of links) {
|
|
2566
|
+
const href = link.href;
|
|
2567
|
+
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href) || href.startsWith("#") || href.startsWith("data:")) {
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
if (options.resolveFilePath) {
|
|
2571
|
+
const filePath = href.split("#")[0].split("?")[0];
|
|
2572
|
+
if (!filePath) continue;
|
|
2573
|
+
if (!options.resolveFilePath(filePath)) {
|
|
2574
|
+
warnings.push({
|
|
2575
|
+
type: "broken-link",
|
|
2576
|
+
href,
|
|
2577
|
+
message: `Broken link: file not found — ${href}`,
|
|
2578
|
+
lineStart: link.lineStart,
|
|
2579
|
+
lineEnd: link.lineEnd
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
return warnings;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2357
2588
|
async function resolveIncludes(nodes, resolverFn) {
|
|
2358
2589
|
for (const node of nodes) {
|
|
2359
2590
|
if (node.type === "code" && node.src) {
|
|
@@ -2364,6 +2595,12 @@ async function resolveIncludes(nodes, resolverFn) {
|
|
|
2364
2595
|
text = allLines.slice(node.lines.start - 1, node.lines.end).join("\n");
|
|
2365
2596
|
}
|
|
2366
2597
|
node.text = text;
|
|
2598
|
+
// Re-parse JSON for :data blocks after include resolution
|
|
2599
|
+
if (node.dataFlag && node.lang === "json") {
|
|
2600
|
+
try {
|
|
2601
|
+
node.data = JSON.parse(node.text);
|
|
2602
|
+
} catch { /* leave node.data undefined — caller can check */ }
|
|
2603
|
+
}
|
|
2367
2604
|
} catch (err) {
|
|
2368
2605
|
node.text = `// Error: Could not read ${node.src} — ${err.message}`;
|
|
2369
2606
|
}
|
|
@@ -2384,6 +2621,7 @@ module.exports = {
|
|
|
2384
2621
|
resolveIncludes,
|
|
2385
2622
|
renderFragment,
|
|
2386
2623
|
renderTextParagraphs,
|
|
2624
|
+
renderHtmlBody,
|
|
2387
2625
|
renderHtmlDocumentFromParsed,
|
|
2388
2626
|
renderHtmlDocument,
|
|
2389
2627
|
formatSdoc,
|
|
@@ -2392,6 +2630,12 @@ module.exports = {
|
|
|
2392
2630
|
listSections,
|
|
2393
2631
|
extractSection,
|
|
2394
2632
|
extractAbout,
|
|
2633
|
+
extractDataBlocks,
|
|
2634
|
+
KNOWN_SCOPE_TYPES,
|
|
2635
|
+
// Validation
|
|
2636
|
+
collectAllIds,
|
|
2637
|
+
collectInlineRefs,
|
|
2638
|
+
validateRefs,
|
|
2395
2639
|
// Low-level helpers for custom renderers (e.g. slide-renderer)
|
|
2396
2640
|
parseInline,
|
|
2397
2641
|
renderKatex,
|