@entropicwarrior/sdoc 0.1.17 → 0.2.0
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/docs/reference/sdoc-authoring.sdoc +110 -6
- package/lexica/specification.sdoc +185 -27
- package/package.json +1 -1
- package/src/notion-renderer.js +3 -0
- package/src/sdoc.js +213 -129
- package/src/slide-renderer.js +21 -6
- package/docs/guide/intro.sdoc +0 -112
- package/docs/guide/notion-sync.sdoc +0 -177
- package/docs/guide/setup.sdoc +0 -112
- package/docs/guide/why-sdoc.sdoc +0 -206
- package/docs/index.sdoc +0 -39
- package/docs/reference/api.sdoc +0 -208
- package/docs/reference/cli.sdoc +0 -188
- package/docs/reference/syntax.sdoc +0 -502
- package/docs/tutorials/first-steps.sdoc +0 -117
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;
|
|
1045
1024
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
+
}
|
|
1040
|
+
|
|
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) {
|
|
@@ -1429,13 +1428,18 @@ function renderInlineNodes(nodes) {
|
|
|
1429
1428
|
}
|
|
1430
1429
|
|
|
1431
1430
|
function renderScope(scope, depth, isTitleScope = false) {
|
|
1431
|
+
// :comment scopes are not rendered
|
|
1432
|
+
if (scope.scopeType === "comment") return "";
|
|
1433
|
+
|
|
1432
1434
|
const level = Math.min(6, Math.max(1, depth));
|
|
1433
1435
|
const children = scope.children.map((child) => renderNode(child, depth + 1)).join("\n");
|
|
1434
1436
|
const rootClass = isTitleScope ? " sdoc-root" : "";
|
|
1435
1437
|
const dl = dataLineAttrs(scope);
|
|
1438
|
+
const typeAttr = scope.scopeType ? ` data-scope-type="${escapeAttr(scope.scopeType)}"` : "";
|
|
1439
|
+
const typeClass = scope.scopeType ? ` sdoc-scope-type-${scope.scopeType}` : "";
|
|
1436
1440
|
|
|
1437
1441
|
if (scope.hasHeading === false) {
|
|
1438
|
-
return `<section class="sdoc-scope sdoc-scope-noheading${rootClass}"${dl}>${children}</section>`;
|
|
1442
|
+
return `<section class="sdoc-scope sdoc-scope-noheading${rootClass}${typeClass}"${typeAttr}${dl}>${children}</section>`;
|
|
1439
1443
|
}
|
|
1440
1444
|
|
|
1441
1445
|
const idAttr = scope.id ? ` id="${escapeAttr(scope.id)}"` : "";
|
|
@@ -1443,7 +1447,7 @@ function renderScope(scope, depth, isTitleScope = false) {
|
|
|
1443
1447
|
const toggle = hasChildren ? `<span class="sdoc-toggle"></span>` : "";
|
|
1444
1448
|
const heading = `<h${level}${idAttr} class="sdoc-heading sdoc-depth-${level}"${dl}>${toggle}${renderInline(scope.title)}</h${level}>`;
|
|
1445
1449
|
const childrenHtml = children ? `\n<div class="sdoc-scope-children">${children}</div>` : "";
|
|
1446
|
-
return `<section class="sdoc-scope${rootClass}">${heading}${childrenHtml}</section>`;
|
|
1450
|
+
return `<section class="sdoc-scope${rootClass}${typeClass}"${typeAttr}>${heading}${childrenHtml}</section>`;
|
|
1447
1451
|
}
|
|
1448
1452
|
|
|
1449
1453
|
function renderList(list, depth) {
|
|
@@ -1536,7 +1540,16 @@ function renderTable(table) {
|
|
|
1536
1540
|
.join("\n");
|
|
1537
1541
|
const tbody = bodyRows ? `<tbody class="sdoc-table-body">${bodyRows}</tbody>` : "";
|
|
1538
1542
|
|
|
1539
|
-
|
|
1543
|
+
const styleParts = [];
|
|
1544
|
+
if (opts.width) {
|
|
1545
|
+
styleParts.push(`width:${opts.width}`);
|
|
1546
|
+
if (opts.width !== "auto") styleParts.push("table-layout:fixed");
|
|
1547
|
+
}
|
|
1548
|
+
if (opts.align === "center") styleParts.push("margin-left:auto", "margin-right:auto");
|
|
1549
|
+
else if (opts.align === "right") styleParts.push("margin-left:auto", "margin-right:0");
|
|
1550
|
+
const styleAttr = styleParts.length ? ` style="${styleParts.join(";")}"` : "";
|
|
1551
|
+
|
|
1552
|
+
return `<table class="${classAttr}"${dl}${styleAttr}>${thead}${thead ? "\n" : ""}${tbody}</table>`;
|
|
1540
1553
|
}
|
|
1541
1554
|
|
|
1542
1555
|
function renderNode(node, depth) {
|
|
@@ -1568,7 +1581,8 @@ function renderNode(node, depth) {
|
|
|
1568
1581
|
return `<div class="sdoc-math sdoc-math-block"${dl}>${renderKatex(node.text, true)}</div>`;
|
|
1569
1582
|
}
|
|
1570
1583
|
const langClass = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
|
|
1571
|
-
|
|
1584
|
+
const dataLabel = node.dataFlag ? `<span class="sdoc-data-label">data</span>` : "";
|
|
1585
|
+
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
1586
|
}
|
|
1573
1587
|
default:
|
|
1574
1588
|
return "";
|
|
@@ -2009,6 +2023,22 @@ const DEFAULT_STYLE = `
|
|
|
2009
2023
|
padding-left: 1.2rem;
|
|
2010
2024
|
}
|
|
2011
2025
|
|
|
2026
|
+
.sdoc-data-label {
|
|
2027
|
+
position: absolute;
|
|
2028
|
+
top: 6px;
|
|
2029
|
+
left: 8px;
|
|
2030
|
+
z-index: 1;
|
|
2031
|
+
font-size: 0.65rem;
|
|
2032
|
+
font-weight: 600;
|
|
2033
|
+
text-transform: uppercase;
|
|
2034
|
+
letter-spacing: 0.05em;
|
|
2035
|
+
padding: 1px 5px;
|
|
2036
|
+
border-radius: 3px;
|
|
2037
|
+
background: rgba(59, 130, 195, 0.15);
|
|
2038
|
+
color: #3b82c3;
|
|
2039
|
+
pointer-events: none;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2012
2042
|
`;
|
|
2013
2043
|
|
|
2014
2044
|
const PRINT_STYLE = `
|
|
@@ -2314,17 +2344,35 @@ function listSections(nodes) {
|
|
|
2314
2344
|
id: node.id || null,
|
|
2315
2345
|
derivedId: slugify(node.title),
|
|
2316
2346
|
title: node.title,
|
|
2347
|
+
scopeType: node.scopeType || null,
|
|
2317
2348
|
preview: firstParagraphPreview(node.children || [], 100)
|
|
2318
2349
|
}));
|
|
2319
2350
|
}
|
|
2320
2351
|
|
|
2352
|
+
function collectDataBlocks(children) {
|
|
2353
|
+
const data = [];
|
|
2354
|
+
for (const child of children) {
|
|
2355
|
+
if (child.type === "code" && child.dataFlag && child.data !== undefined) {
|
|
2356
|
+
data.push(child.data);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return data;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2321
2362
|
function extractSection(nodes, sectionId) {
|
|
2322
2363
|
const scopes = getContentScopes(nodes);
|
|
2323
2364
|
|
|
2365
|
+
function buildResult(node) {
|
|
2366
|
+
const data = collectDataBlocks(node.children || []);
|
|
2367
|
+
const result = { title: node.title, content: collectPlainText(node.children || []) };
|
|
2368
|
+
if (data.length) result.data = data;
|
|
2369
|
+
return result;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2324
2372
|
// First pass: match explicit @id (case-sensitive)
|
|
2325
2373
|
for (const node of scopes) {
|
|
2326
2374
|
if (node.id && node.id === sectionId) {
|
|
2327
|
-
return
|
|
2375
|
+
return buildResult(node);
|
|
2328
2376
|
}
|
|
2329
2377
|
}
|
|
2330
2378
|
|
|
@@ -2332,13 +2380,41 @@ function extractSection(nodes, sectionId) {
|
|
|
2332
2380
|
const lowerTarget = sectionId.toLowerCase();
|
|
2333
2381
|
for (const node of scopes) {
|
|
2334
2382
|
if (slugify(node.title).toLowerCase() === lowerTarget) {
|
|
2335
|
-
return
|
|
2383
|
+
return buildResult(node);
|
|
2336
2384
|
}
|
|
2337
2385
|
}
|
|
2338
2386
|
|
|
2339
2387
|
return null;
|
|
2340
2388
|
}
|
|
2341
2389
|
|
|
2390
|
+
function extractDataBlocks(nodes) {
|
|
2391
|
+
const results = [];
|
|
2392
|
+
function walk(nodeList, scopeId, scopeType, scopeTitle) {
|
|
2393
|
+
for (const node of nodeList) {
|
|
2394
|
+
if (node.type === "code" && node.dataFlag && node.data !== undefined) {
|
|
2395
|
+
results.push({
|
|
2396
|
+
scopeId: scopeId || null,
|
|
2397
|
+
scopeType: scopeType || null,
|
|
2398
|
+
scopeTitle: scopeTitle || null,
|
|
2399
|
+
data: node.data
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
if (node.type === "scope") {
|
|
2403
|
+
walk(node.children || [], node.id, node.scopeType, node.title);
|
|
2404
|
+
}
|
|
2405
|
+
if (node.type === "list" && node.items) {
|
|
2406
|
+
for (const item of node.items) {
|
|
2407
|
+
if (item.type === "scope") {
|
|
2408
|
+
walk(item.children || [], item.id, item.scopeType, item.title);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
walk(nodes, null, null, null);
|
|
2415
|
+
return results;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2342
2418
|
function extractAbout(nodes) {
|
|
2343
2419
|
const doc = getDocumentScope(nodes);
|
|
2344
2420
|
const children = doc ? doc.children : nodes;
|
|
@@ -2364,6 +2440,12 @@ async function resolveIncludes(nodes, resolverFn) {
|
|
|
2364
2440
|
text = allLines.slice(node.lines.start - 1, node.lines.end).join("\n");
|
|
2365
2441
|
}
|
|
2366
2442
|
node.text = text;
|
|
2443
|
+
// Re-parse JSON for :data blocks after include resolution
|
|
2444
|
+
if (node.dataFlag && node.lang === "json") {
|
|
2445
|
+
try {
|
|
2446
|
+
node.data = JSON.parse(node.text);
|
|
2447
|
+
} catch { /* leave node.data undefined — caller can check */ }
|
|
2448
|
+
}
|
|
2367
2449
|
} catch (err) {
|
|
2368
2450
|
node.text = `// Error: Could not read ${node.src} — ${err.message}`;
|
|
2369
2451
|
}
|
|
@@ -2392,6 +2474,8 @@ module.exports = {
|
|
|
2392
2474
|
listSections,
|
|
2393
2475
|
extractSection,
|
|
2394
2476
|
extractAbout,
|
|
2477
|
+
extractDataBlocks,
|
|
2478
|
+
KNOWN_SCOPE_TYPES,
|
|
2395
2479
|
// Low-level helpers for custom renderers (e.g. slide-renderer)
|
|
2396
2480
|
parseInline,
|
|
2397
2481
|
renderKatex,
|
package/src/slide-renderer.js
CHANGED
|
@@ -130,15 +130,26 @@ function renderTable(table) {
|
|
|
130
130
|
.join("\n");
|
|
131
131
|
const tbody = bodyRows ? `<tbody>\n${bodyRows}\n</tbody>` : "";
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
const styleParts = [];
|
|
134
|
+
if (opts.width) {
|
|
135
|
+
styleParts.push(`width:${opts.width}`);
|
|
136
|
+
if (opts.width !== "auto") styleParts.push("table-layout:fixed");
|
|
137
|
+
}
|
|
138
|
+
if (opts.align === "center") styleParts.push("margin-left:auto", "margin-right:auto");
|
|
139
|
+
else if (opts.align === "right") styleParts.push("margin-left:auto", "margin-right:0");
|
|
140
|
+
const styleAttr = styleParts.length ? ` style="${styleParts.join(";")}"` : "";
|
|
141
|
+
|
|
142
|
+
return `<table${classAttr}${styleAttr}>${thead}${thead ? "\n" : ""}${tbody}</table>`;
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
function renderNestedScope(scope) {
|
|
146
|
+
if (scope.scopeType === "comment") return "";
|
|
137
147
|
const heading = scope.hasHeading !== false && scope.title
|
|
138
148
|
? `<h3>${renderInline(scope.title)}</h3>`
|
|
139
149
|
: "";
|
|
150
|
+
const typeAttr = scope.scopeType ? ` data-scope-type="${escapeAttr(scope.scopeType)}"` : "";
|
|
140
151
|
const children = scope.children.map((child) => renderNode(child)).join("\n");
|
|
141
|
-
return `<section>${heading}\n${children}</section>`;
|
|
152
|
+
return `<section${typeAttr}>${heading}\n${children}</section>`;
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
function renderChildren(nodes) {
|
|
@@ -179,6 +190,10 @@ function extractNotes(children) {
|
|
|
179
190
|
const notes = [];
|
|
180
191
|
const rest = [];
|
|
181
192
|
for (const child of children) {
|
|
193
|
+
if (child.type === "scope" && child.scopeType === "comment") {
|
|
194
|
+
// :comment scopes are excluded from both notes and content
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
182
197
|
if (child.type === "scope" && child.id && child.id.toLowerCase() === "notes") {
|
|
183
198
|
notes.push(child);
|
|
184
199
|
} else {
|
|
@@ -208,8 +223,8 @@ function renderSlide(scope, slideIndex, overlayHtml) {
|
|
|
208
223
|
let bodyHtml;
|
|
209
224
|
if (config.layout === "two-column") {
|
|
210
225
|
// In two-column layout, child scopes become columns
|
|
211
|
-
const columns = contentNodes.filter((n) => n.type === "scope");
|
|
212
|
-
const nonColumns = contentNodes.filter((n) => n.type !== "scope");
|
|
226
|
+
const columns = contentNodes.filter((n) => n.type === "scope" && n.scopeType !== "comment");
|
|
227
|
+
const nonColumns = contentNodes.filter((n) => n.type !== "scope" || n.scopeType === "comment");
|
|
213
228
|
const preamble = nonColumns.length ? renderChildren(nonColumns) : "";
|
|
214
229
|
const columnsHtml = columns
|
|
215
230
|
.map((col) => {
|
|
@@ -256,8 +271,8 @@ function renderSlides(nodes, options = {}) {
|
|
|
256
271
|
slideScopes = nodes;
|
|
257
272
|
}
|
|
258
273
|
|
|
259
|
-
// Filter to scope nodes only (skip stray paragraphs
|
|
260
|
-
const slides = slideScopes.filter((n) => n.type === "scope");
|
|
274
|
+
// Filter to scope nodes only (skip stray paragraphs and :comment scopes)
|
|
275
|
+
const slides = slideScopes.filter((n) => n.type === "scope" && n.scopeType !== "comment");
|
|
261
276
|
|
|
262
277
|
// Build per-slide footer: < CONFIDENTIAL ---gap--- Company >
|
|
263
278
|
const footerParts = [];
|