@entropicwarrior/sdoc 0.2.12 → 0.2.14
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 +76 -8
- package/lexica/specification.sdoc +97 -1
- package/package.json +1 -1
- package/src/notion-renderer.js +49 -1
- package/src/sdoc.js +253 -24
- package/tools/build-doc.js +10 -4
|
@@ -275,6 +275,66 @@ Content of Section B.
|
|
|
275
275
|
}
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
+
# Column Directives @column-directives
|
|
279
|
+
{
|
|
280
|
+
An optional directive row after the header controls per-column alignment and number formatting. It is consumed by the parser and not rendered as data.
|
|
281
|
+
|
|
282
|
+
# Column Alignment
|
|
283
|
+
{
|
|
284
|
+
Use \`<\` (left), \`>\` (right), or \`=\` (center):
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
{[table]
|
|
288
|
+
Programme | Amount | Status
|
|
289
|
+
< | > | =
|
|
290
|
+
Alpha | $1,000,000 | Active
|
|
291
|
+
Beta | $2,500,000 | Pending
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
If absent, all columns default to left. Empty cells in the directive row keep the default. Alignment applies to both header and data cells.
|
|
296
|
+
|
|
297
|
+
For headerless tables, the directive row is the first row — it is consumed but no header is rendered:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
{[table headerless]
|
|
301
|
+
< | >
|
|
302
|
+
Alice | 100
|
|
303
|
+
Bob | 200
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Column Formatting
|
|
309
|
+
{
|
|
310
|
+
A format token in the directive row formats numeric cells and formula results in that column. Text cells (like bold labels) pass through unchanged. The format is display-only — formulas always use raw numeric values.
|
|
311
|
+
|
|
312
|
+
{[.]
|
|
313
|
+
- \`$\` — currency, no decimals: \`$1,000,000\`
|
|
314
|
+
- \`$.N\` — currency with N decimals: \`$.2\` → \`$1,234.50\`
|
|
315
|
+
- \`,\` — thousands separator: \`1,000,000\`
|
|
316
|
+
- \`,.N\` — thousands with N decimals: \`,.2\` → \`1,234,567.89\`
|
|
317
|
+
- \`.N\` — fixed N decimals: \`.2\` → \`3.14\`
|
|
318
|
+
- \`%\` — percentage (value × 100): \`0.452\` → \`45.2%\`
|
|
319
|
+
- \`%.N\` — percentage with N decimals: \`%.1\` → \`45.7%\`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
Alignment and format combine in a single directive cell:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
{[table]
|
|
326
|
+
Item | Amount
|
|
327
|
+
< | > $
|
|
328
|
+
Widget | 1000000
|
|
329
|
+
Gadget | 2500000
|
|
330
|
+
**Total** | =SUM(B1:B2)
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The \`Amount\` column is right-aligned and formatted as currency. The formula result and data cells all display as \`$1,000,000\` style. The \`**Total**\` label renders as bold text.
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
278
338
|
# Table Formulas @table-formulas
|
|
279
339
|
{
|
|
280
340
|
Cells starting with \`=\` are evaluated as formulas. Hover a computed cell in preview to see the original formula.
|
|
@@ -282,9 +342,10 @@ Content of Section B.
|
|
|
282
342
|
```
|
|
283
343
|
{[table]
|
|
284
344
|
Investor | Shares | Ownership
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Founder
|
|
345
|
+
< | > , | > %
|
|
346
|
+
Seed Fund | 500000 | 0.25
|
|
347
|
+
Founder A | 1000000 | 0.50
|
|
348
|
+
Founder B | 500000 | 0.25
|
|
288
349
|
**Total** | =SUM(B1:B3) | =SUM(C1:C3)
|
|
289
350
|
}
|
|
290
351
|
```
|
|
@@ -301,6 +362,8 @@ Content of Section B.
|
|
|
301
362
|
}
|
|
302
363
|
|
|
303
364
|
Errors display in red italic: \`#DIV/0!\` (division by zero), \`#VALUE!\` (bad reference), \`#REF!\` (invalid syntax), \`#NAME!\` (unknown function), \`#CIRCULAR!\` (circular dependency).
|
|
365
|
+
|
|
366
|
+
**Tip:** Use numeric values with a column format directive (e.g. \`> $\`) instead of pre-formatted strings like \`$1,000,000\`. Formatted strings are text — formulas cannot reference them.
|
|
304
367
|
}
|
|
305
368
|
}
|
|
306
369
|
|
|
@@ -491,7 +554,7 @@ Content of Section B.
|
|
|
491
554
|
{
|
|
492
555
|
The reserved \`@meta\` scope configures per-file settings and is not rendered in the document body. Use the bare \`@meta\` form (preferred) or the heading form:
|
|
493
556
|
|
|
494
|
-
```
|
|
557
|
+
```sdoc
|
|
495
558
|
@meta {
|
|
496
559
|
type: doc
|
|
497
560
|
|
|
@@ -541,15 +604,20 @@ Content of Section B.
|
|
|
541
604
|
|
|
542
605
|
# About Scope @about-scope
|
|
543
606
|
{
|
|
544
|
-
The reserved \`@about\` scope provides a discovery summary for the document. It is used by \`list_knowledge\` to describe the file
|
|
607
|
+
The reserved \`@about\` scope provides a discovery summary for the document. It is used by \`list_knowledge\` to describe the file and is also rendered in the live preview with a distinct meta-section style so readers can tell it apart from regular body content. Use the bare \`@about\` form (preferred) or the heading form:
|
|
545
608
|
|
|
546
|
-
```
|
|
609
|
+
```sdoc
|
|
547
610
|
@about {
|
|
548
|
-
How to write correct SDOC files.
|
|
611
|
+
How to write correct SDOC files.
|
|
612
|
+
Covers document structure, inline formatting, block types, and common mistakes.
|
|
549
613
|
}
|
|
550
614
|
```
|
|
551
615
|
|
|
552
|
-
The heading form \`# About @about { }\` also works but the bare form is preferred.
|
|
616
|
+
The heading form \`# About @about { }\` also works but the bare form is preferred. With the bare form, the renderer synthesizes an \`About\` heading so the meta-section is always labelled. The heading form lets you override that label — \`# Document Discovery @about { }\` renders with `Document Discovery` as the title.
|
|
617
|
+
|
|
618
|
+
**Hidden by default in HTML / PDF exports.** Exported files are usually sent to a recipient who has already decided to read the document, so the \`@about\` discovery summary is redundant in that context. To keep it in an export, pass the opt-in flag — \`--include-about\` (see [CLI and Tools Reference](./cli.sdoc)).
|
|
619
|
+
|
|
620
|
+
An empty or whitespace-only \`@about\` scope is never rendered, regardless of the include flag.
|
|
553
621
|
}
|
|
554
622
|
|
|
555
623
|
# Escaping @escaping
|
|
@@ -380,6 +380,69 @@ Content of Section B.
|
|
|
380
380
|
```
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
# Column Directives @column-directives
|
|
384
|
+
{
|
|
385
|
+
An optional directive row immediately after the header row (or as the first row in a headerless table) controls per-column alignment and number formatting. The directive row is consumed by the parser and never rendered as data.
|
|
386
|
+
|
|
387
|
+
# Column Alignment @column-alignment
|
|
388
|
+
{
|
|
389
|
+
Alignment characters in the directive row set the text-align for all cells in that column, including the header:
|
|
390
|
+
|
|
391
|
+
{[.]
|
|
392
|
+
- `<` — left (default)
|
|
393
|
+
- `>` — right (most useful for numbers and currency)
|
|
394
|
+
- `=` — center
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
{[table]
|
|
399
|
+
Programme | Amount | Status
|
|
400
|
+
< | > | =
|
|
401
|
+
Alpha | $1,000,000 | Active
|
|
402
|
+
Beta | $2,500,000 | Pending
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
If the directive row is absent, all columns default to left alignment. Empty cells in the directive row inherit the default (left).
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Column Formatting @column-formatting
|
|
410
|
+
{
|
|
411
|
+
A format token in the directive row causes numeric cells and formula results in that column to be formatted for display. Text cells are unaffected. The format is display-only — formulas always operate on raw numeric values.
|
|
412
|
+
|
|
413
|
+
{[.]
|
|
414
|
+
- `$` — currency, no decimals: `1000000` → `$1,000,000`
|
|
415
|
+
- `$.N` — currency with N decimals: `$.2` → `$1,234.50`
|
|
416
|
+
- `,` — thousands separator, no decimals: `1000000` → `1,000,000`
|
|
417
|
+
- `,.N` — thousands with N decimals: `,.2` → `1,234,567.89`
|
|
418
|
+
- `.N` — fixed N decimals: `.2` → `3.14`
|
|
419
|
+
- `%` — percentage (value × 100, auto decimals): `0.452` → `45.2%`
|
|
420
|
+
- `%.N` — percentage with N decimals: `%.1` → `45.7%`
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
A directive cell may combine alignment and format, separated by whitespace:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
{[table]
|
|
427
|
+
Item | Amount
|
|
428
|
+
< | > $
|
|
429
|
+
Widget | 1000000
|
|
430
|
+
Gadget | 2500000
|
|
431
|
+
**Total** | =SUM(B1:B2)
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
All numeric values and formula results in the `Amount` column display as currency (`$3,500,000`). The `**Total**` label renders normally as bold text.
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
# Directive Row Detection @directive-detection
|
|
439
|
+
{
|
|
440
|
+
A row is identified as a directive row when every non-empty cell contains only an alignment character (`<`, `>`, `=`), a format token, or both. It must be the first row after the header (or the first row in a headerless table), and at least one cell must contain a directive.
|
|
441
|
+
|
|
442
|
+
For headerless tables, the directive row functions as a virtual header — it is consumed for its directives but no `<thead>` is rendered.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
383
446
|
# Table Formulas @table-formulas
|
|
384
447
|
{
|
|
385
448
|
Table cells whose text begins with `=` (but not `==`) are evaluated as formulas. The result replaces the cell text in rendered output; the original formula is preserved as a tooltip.
|
|
@@ -919,6 +982,29 @@ Content of Section B.
|
|
|
919
982
|
}
|
|
920
983
|
}
|
|
921
984
|
}
|
|
985
|
+
|
|
986
|
+
# About Scope @about-scope
|
|
987
|
+
{
|
|
988
|
+
A reserved \`@about\` scope holds a short discovery summary for the document — what it covers, who it is for, and when to reach for it. The bare form is preferred:
|
|
989
|
+
|
|
990
|
+
```sdoc
|
|
991
|
+
@about {
|
|
992
|
+
How to write correct SDOC files. Covers document structure,
|
|
993
|
+
inline formatting, block types, and common mistakes.
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
The heading form `# Title @about { }` is also accepted; the title given there overrides the default `About` label when the section is rendered.
|
|
998
|
+
|
|
999
|
+
{[.]
|
|
1000
|
+
- The `@about` id is reserved and must not be used for normal references
|
|
1001
|
+
- The scope is consumed by discovery tooling (e.g. `list_knowledge` and related agent-facing helpers) to describe a document without loading the full body
|
|
1002
|
+
- In the VS Code live preview, `@about` renders as a meta-section — distinct background, dashed border, subdued typography — so readers can tell it apart from regular body content
|
|
1003
|
+
- **HTML and PDF export hide `@about` by default**, on the principle that an exported file is normally sent to a recipient who has already been asked to read it, so the "should I read this?" framing is redundant. Renderers expose an `includeAbout` opt-in (`--include-about` for the CLI; `{ includeAbout: true }` for the VS Code export commands)
|
|
1004
|
+
- When rendered, a bare `@about { ... }` (no heading line) is given a synthesized `About` heading so the meta-section is always labelled. The heading form preserves any custom title the author wrote
|
|
1005
|
+
- An empty or whitespace-only `@about` scope is never rendered, regardless of the `includeAbout` setting — the meta-section box is suppressed entirely
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
922
1008
|
}
|
|
923
1009
|
|
|
924
1010
|
# Interactive Preview @interactive-preview
|
|
@@ -1083,7 +1169,17 @@ Content of Section A.
|
|
|
1083
1169
|
| "left" | "center" | "right" ;
|
|
1084
1170
|
percentage = digit { digit } [ "." digit { digit } ] "%" ;
|
|
1085
1171
|
pixels = digit { digit } "px" ;
|
|
1086
|
-
table_body = table_row { table_row }
|
|
1172
|
+
table_body = table_row [ directive_row ] { table_row }
|
|
1173
|
+
| directive_row { table_row } ;
|
|
1174
|
+
(* normal tables: header row then optional directive row;
|
|
1175
|
+
headerless tables: optional directive row first *)
|
|
1176
|
+
directive_row = directive_cell { "|" directive_cell } ;
|
|
1177
|
+
directive_cell = [ align_char ] [ format_spec ] ;
|
|
1178
|
+
align_char = "<" | ">" | "=" ;
|
|
1179
|
+
format_spec = "$" [ "." digits ]
|
|
1180
|
+
| "," [ "." digits ]
|
|
1181
|
+
| "." digits
|
|
1182
|
+
| "%" [ "." digits ] ;
|
|
1087
1183
|
table_row = cell { "|" cell } ;
|
|
1088
1184
|
citations_scope = citations_open ws? citations_body "}" ;
|
|
1089
1185
|
citations_open = "{[citations]" ;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@entropicwarrior/sdoc",
|
|
3
3
|
"displayName": "SDOC - Docs for Human/Agent Teams",
|
|
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.14",
|
|
6
6
|
"publisher": "entropicwarrior-msenfin",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
package/src/notion-renderer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// const { nodes, meta } = extractMeta(parsed.nodes);
|
|
8
8
|
// const blocks = renderNotionBlocks(nodes);
|
|
9
9
|
|
|
10
|
-
const { parseInline } = require("./sdoc");
|
|
10
|
+
const { parseInline, isAboutEmpty } = require("./sdoc");
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Constants
|
|
@@ -293,6 +293,11 @@ function renderNode(node, depth, nestLevel) {
|
|
|
293
293
|
function renderScope(scope, depth, nestLevel) {
|
|
294
294
|
if (scope.scopeType === "comment") return [];
|
|
295
295
|
|
|
296
|
+
if (scope.id && scope.id.toLowerCase() === "about") {
|
|
297
|
+
if (isAboutEmpty(scope)) return [];
|
|
298
|
+
return renderAboutCallout(scope, depth, nestLevel);
|
|
299
|
+
}
|
|
300
|
+
|
|
296
301
|
const level = Math.min(3, Math.max(1, depth));
|
|
297
302
|
|
|
298
303
|
if (scope.hasHeading === false) {
|
|
@@ -310,6 +315,49 @@ function renderScope(scope, depth, nestLevel) {
|
|
|
310
315
|
return [headingBlock(level, scope.title, null), ...childBlocks];
|
|
311
316
|
}
|
|
312
317
|
|
|
318
|
+
// Render @about as a Notion callout so readers can tell it apart from body
|
|
319
|
+
// content. Paragraph children fold into the callout's rich_text (separated by
|
|
320
|
+
// newlines); anything else becomes a child block.
|
|
321
|
+
function renderAboutCallout(scope, depth, nestLevel) {
|
|
322
|
+
// Callout children sit one level deeper in the Notion block tree than the
|
|
323
|
+
// callout itself. Clamp to MAX_NEST so we never produce blocks past Notion's
|
|
324
|
+
// nesting limit.
|
|
325
|
+
const childDepth = depth + 1;
|
|
326
|
+
const childNestLevel = Math.min(nestLevel + 1, MAX_NEST);
|
|
327
|
+
const richTexts = [];
|
|
328
|
+
const childBlocks = [];
|
|
329
|
+
|
|
330
|
+
for (const child of scope.children || []) {
|
|
331
|
+
if (child.type === "paragraph") {
|
|
332
|
+
if (richTexts.length > 0) {
|
|
333
|
+
richTexts.push(richText("\n", null, defaultAnnotations()));
|
|
334
|
+
}
|
|
335
|
+
const { richText: rt, imageBlocks } = extractImagesFromInline(child.text);
|
|
336
|
+
richTexts.push(...rt);
|
|
337
|
+
childBlocks.push(...imageBlocks);
|
|
338
|
+
} else {
|
|
339
|
+
childBlocks.push(...renderNode(child, childDepth, childNestLevel));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (richTexts.length === 0) {
|
|
344
|
+
richTexts.push(richText("", null, defaultAnnotations()));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const callout = {
|
|
348
|
+
type: "callout",
|
|
349
|
+
callout: {
|
|
350
|
+
rich_text: enforceContentLimit(richTexts, RICH_TEXT_LIMIT),
|
|
351
|
+
icon: { type: "emoji", emoji: "ℹ️" },
|
|
352
|
+
color: "gray_background"
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
if (childBlocks.length > 0) {
|
|
356
|
+
callout.callout.children = childBlocks;
|
|
357
|
+
}
|
|
358
|
+
return [callout];
|
|
359
|
+
}
|
|
360
|
+
|
|
313
361
|
function renderParagraph(node) {
|
|
314
362
|
const { richText: rt, imageBlocks } = extractImagesFromInline(node.text);
|
|
315
363
|
const blocks = [];
|
package/src/sdoc.js
CHANGED
|
@@ -63,6 +63,105 @@ function parseTableOptions(text) {
|
|
|
63
63
|
return options;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// ── Column directive row (alignment + format) ──────────────────────────
|
|
67
|
+
|
|
68
|
+
function isDirectiveRow(cells) {
|
|
69
|
+
if (cells.length === 0) return false;
|
|
70
|
+
const pattern = /^([<>=])?\s*(\$(?:\.\d+)?|,(?:\.\d+)?|\.\d+|%(?:\.\d+)?)?$/;
|
|
71
|
+
let hasDirective = false;
|
|
72
|
+
for (const cell of cells) {
|
|
73
|
+
const trimmed = cell.trim();
|
|
74
|
+
if (trimmed === "") continue;
|
|
75
|
+
if (!pattern.test(trimmed)) return false;
|
|
76
|
+
hasDirective = true;
|
|
77
|
+
}
|
|
78
|
+
return hasDirective;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseFormatSpec(spec) {
|
|
82
|
+
if (!spec) return null;
|
|
83
|
+
if (spec.startsWith("$")) {
|
|
84
|
+
const decimals = spec.includes(".") ? parseInt(spec.slice(spec.indexOf(".") + 1), 10) : 0;
|
|
85
|
+
return { prefix: "$", thousands: true, decimals, percent: false };
|
|
86
|
+
}
|
|
87
|
+
if (spec.startsWith("%")) {
|
|
88
|
+
const decimals = spec.includes(".") ? parseInt(spec.slice(spec.indexOf(".") + 1), 10) : -1;
|
|
89
|
+
return { prefix: "", thousands: false, decimals, percent: true };
|
|
90
|
+
}
|
|
91
|
+
if (spec.startsWith(",")) {
|
|
92
|
+
const decimals = spec.includes(".") ? parseInt(spec.slice(spec.indexOf(".") + 1), 10) : 0;
|
|
93
|
+
return { prefix: "", thousands: true, decimals, percent: false };
|
|
94
|
+
}
|
|
95
|
+
if (spec.startsWith(".")) {
|
|
96
|
+
const decimals = parseInt(spec.slice(1), 10);
|
|
97
|
+
return { prefix: "", thousands: false, decimals, percent: false };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseDirectiveRow(cells) {
|
|
103
|
+
const align = [];
|
|
104
|
+
const format = [];
|
|
105
|
+
let hasAlign = false;
|
|
106
|
+
let hasFormat = false;
|
|
107
|
+
const fmtPattern = /(\$(?:\.\d+)?|,(?:\.\d+)?|\.\d+|%(?:\.\d+)?)$/;
|
|
108
|
+
|
|
109
|
+
for (const cell of cells) {
|
|
110
|
+
const trimmed = cell.trim();
|
|
111
|
+
const alignMatch = trimmed.match(/^([<>=])/);
|
|
112
|
+
const fmtMatch = trimmed.match(fmtPattern);
|
|
113
|
+
|
|
114
|
+
let a = null;
|
|
115
|
+
if (alignMatch) {
|
|
116
|
+
a = alignMatch[1] === "<" ? "left" : alignMatch[1] === ">" ? "right" : "center";
|
|
117
|
+
hasAlign = true;
|
|
118
|
+
}
|
|
119
|
+
align.push(a);
|
|
120
|
+
|
|
121
|
+
let f = null;
|
|
122
|
+
if (fmtMatch) {
|
|
123
|
+
f = parseFormatSpec(fmtMatch[1]);
|
|
124
|
+
hasFormat = true;
|
|
125
|
+
}
|
|
126
|
+
format.push(f);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
align: hasAlign ? align : null,
|
|
131
|
+
format: hasFormat ? format : null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatNumber(value, spec) {
|
|
136
|
+
if (spec.percent) {
|
|
137
|
+
const pct = value * 100;
|
|
138
|
+
if (spec.decimals < 0) {
|
|
139
|
+
return (Number.isInteger(pct) ? pct.toString() : pct.toFixed(2).replace(/\.?0+$/, "")) + "%";
|
|
140
|
+
}
|
|
141
|
+
return pct.toFixed(spec.decimals) + "%";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const negative = value < 0;
|
|
145
|
+
const absVal = Math.abs(value);
|
|
146
|
+
let result;
|
|
147
|
+
|
|
148
|
+
if (spec.decimals > 0) {
|
|
149
|
+
result = absVal.toFixed(spec.decimals);
|
|
150
|
+
} else {
|
|
151
|
+
result = Math.round(absVal).toString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (spec.thousands) {
|
|
155
|
+
const parts = result.split(".");
|
|
156
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
157
|
+
result = parts.join(".");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (negative ? "-" : "") + spec.prefix + result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── End column directive row ────────────────────────────────────────────
|
|
164
|
+
|
|
66
165
|
class LineCursor {
|
|
67
166
|
constructor(lines) {
|
|
68
167
|
this.lines = lines;
|
|
@@ -144,7 +243,8 @@ function detectImplicitRoot(cursor) {
|
|
|
144
243
|
if (nextTrimmed === COMMAND_SCOPE_OPEN ||
|
|
145
244
|
nextTrimmed === COMMAND_LIST_BULLET ||
|
|
146
245
|
nextTrimmed === COMMAND_LIST_NUMBER ||
|
|
147
|
-
isTableCommand(nextTrimmed)
|
|
246
|
+
isTableCommand(nextTrimmed) ||
|
|
247
|
+
isCitationsCommand(nextTrimmed)) {
|
|
148
248
|
isImplicit = false;
|
|
149
249
|
} else if (tryParseInlineBlock(nextTrimmed) !== null) {
|
|
150
250
|
isImplicit = false;
|
|
@@ -835,18 +935,31 @@ function parseTableBody(cursor, tableStartLine, options) {
|
|
|
835
935
|
cursor.next();
|
|
836
936
|
}
|
|
837
937
|
|
|
938
|
+
// Detect column directive row (alignment / formatting)
|
|
939
|
+
// For headerless tables: check rows[0]; for normal tables: check rows[1] (after header)
|
|
940
|
+
const directiveIndex = options.headerless ? 0 : 1;
|
|
941
|
+
let columnAlign = null;
|
|
942
|
+
let columnFormat = null;
|
|
943
|
+
if (rows.length > directiveIndex && isDirectiveRow(rows[directiveIndex])) {
|
|
944
|
+
const directives = parseDirectiveRow(rows[directiveIndex]);
|
|
945
|
+
columnAlign = directives.align;
|
|
946
|
+
columnFormat = directives.format;
|
|
947
|
+
rows.splice(directiveIndex, 1);
|
|
948
|
+
}
|
|
949
|
+
|
|
838
950
|
const hasOptions = options.borderless || options.headerless || options.width || options.align;
|
|
839
951
|
|
|
952
|
+
let tableNode;
|
|
840
953
|
if (options.headerless) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
954
|
+
tableNode = { type: "table", headers: [], rows, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
955
|
+
} else {
|
|
956
|
+
const headers = rows.length > 0 ? rows[0] : [];
|
|
957
|
+
const body = rows.slice(1);
|
|
958
|
+
tableNode = { type: "table", headers, rows: body, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
844
959
|
}
|
|
845
|
-
|
|
846
|
-
const headers = rows.length > 0 ? rows[0] : [];
|
|
847
|
-
const body = rows.slice(1);
|
|
848
|
-
const tableNode = { type: "table", headers, rows: body, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
849
960
|
if (hasOptions) tableNode.options = options;
|
|
961
|
+
if (columnAlign) tableNode.columnAlign = columnAlign;
|
|
962
|
+
if (columnFormat) tableNode.columnFormat = columnFormat;
|
|
850
963
|
return tableNode;
|
|
851
964
|
}
|
|
852
965
|
|
|
@@ -1705,8 +1818,17 @@ function renderInlineNodes(nodes) {
|
|
|
1705
1818
|
function renderScope(scope, depth, isTitleScope = false) {
|
|
1706
1819
|
// :comment scopes are not rendered
|
|
1707
1820
|
if (scope.scopeType === "comment") return "";
|
|
1708
|
-
|
|
1709
|
-
|
|
1821
|
+
|
|
1822
|
+
const isAbout = scope.id && scope.id.toLowerCase() === "about";
|
|
1823
|
+
// Skip empty/whitespace-only @about — no point rendering an empty meta box.
|
|
1824
|
+
if (isAbout && isAboutEmpty(scope)) return "";
|
|
1825
|
+
|
|
1826
|
+
// Bare `@about { ... }` (no heading) and heading-form with empty title both
|
|
1827
|
+
// get a default heading of "About" so the meta-section is always labelled.
|
|
1828
|
+
// A custom title (e.g. `# Document Discovery @about`) is preserved.
|
|
1829
|
+
if (isAbout && (!scope.hasHeading || !scope.title || !scope.title.trim())) {
|
|
1830
|
+
scope = { ...scope, hasHeading: true, title: "About" };
|
|
1831
|
+
}
|
|
1710
1832
|
|
|
1711
1833
|
const level = Math.min(6, Math.max(1, depth));
|
|
1712
1834
|
const children = scope.children.map((child) => renderNode(child, depth + 1)).join("\n");
|
|
@@ -1714,9 +1836,10 @@ function renderScope(scope, depth, isTitleScope = false) {
|
|
|
1714
1836
|
const dl = dataLineAttrs(scope);
|
|
1715
1837
|
const typeAttr = scope.scopeType ? ` data-scope-type="${escapeAttr(scope.scopeType)}"` : "";
|
|
1716
1838
|
const typeClass = scope.scopeType ? ` sdoc-scope-type-${scope.scopeType}` : "";
|
|
1839
|
+
const metaClass = isAbout ? " sdoc-meta-section" : "";
|
|
1717
1840
|
|
|
1718
1841
|
if (scope.hasHeading === false) {
|
|
1719
|
-
return `<section class="sdoc-scope sdoc-scope-noheading${rootClass}${typeClass}"${typeAttr}${dl}>${children}</section>`;
|
|
1842
|
+
return `<section class="sdoc-scope sdoc-scope-noheading${rootClass}${typeClass}${metaClass}"${typeAttr}${dl}>${children}</section>`;
|
|
1720
1843
|
}
|
|
1721
1844
|
|
|
1722
1845
|
const idAttr = scope.id ? ` id="${escapeAttr(scope.id)}"` : "";
|
|
@@ -1724,7 +1847,7 @@ function renderScope(scope, depth, isTitleScope = false) {
|
|
|
1724
1847
|
const toggle = hasChildren ? `<span class="sdoc-toggle"></span>` : "";
|
|
1725
1848
|
const heading = `<h${level}${idAttr} class="sdoc-heading sdoc-depth-${level}"${dl}>${toggle}${renderInline(scope.title)}</h${level}>`;
|
|
1726
1849
|
const childrenHtml = children ? `\n<div class="sdoc-scope-children">${children}</div>` : "";
|
|
1727
|
-
return `<section class="sdoc-scope${rootClass}${typeClass}"${typeAttr}>${heading}${childrenHtml}</section>`;
|
|
1850
|
+
return `<section class="sdoc-scope${rootClass}${typeClass}${metaClass}"${typeAttr}>${heading}${childrenHtml}</section>`;
|
|
1728
1851
|
}
|
|
1729
1852
|
|
|
1730
1853
|
function renderCitations(node) {
|
|
@@ -2122,15 +2245,23 @@ function formatFormulaResult(cell) {
|
|
|
2122
2245
|
function renderTable(table) {
|
|
2123
2246
|
const dl = dataLineAttrs(table);
|
|
2124
2247
|
const opts = table.options || {};
|
|
2248
|
+
const colAlign = table.columnAlign || [];
|
|
2249
|
+
const colFormat = table.columnFormat || [];
|
|
2125
2250
|
const classes = ["sdoc-table"];
|
|
2126
2251
|
if (opts.borderless) classes.push("sdoc-table-borderless");
|
|
2127
2252
|
if (opts.headerless) classes.push("sdoc-table-headerless");
|
|
2128
2253
|
const classAttr = classes.join(" ");
|
|
2129
2254
|
|
|
2255
|
+
function cellStyle(colIndex) {
|
|
2256
|
+
const align = colAlign[colIndex];
|
|
2257
|
+
if (!align || align === "left") return "";
|
|
2258
|
+
return ` style="text-align:${align}"`;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2130
2261
|
let thead = "";
|
|
2131
2262
|
if (table.headers.length > 0) {
|
|
2132
2263
|
const headerCells = table.headers
|
|
2133
|
-
.map((cell) => `<th class="sdoc-table-th">${renderInline(cell)}</th>`)
|
|
2264
|
+
.map((cell, c) => `<th class="sdoc-table-th"${cellStyle(c)}>${renderInline(cell)}</th>`)
|
|
2134
2265
|
.join("");
|
|
2135
2266
|
thead = `<thead class="sdoc-table-head"><tr>${headerCells}</tr></thead>`;
|
|
2136
2267
|
}
|
|
@@ -2142,16 +2273,33 @@ function renderTable(table) {
|
|
|
2142
2273
|
.map((row, r) => {
|
|
2143
2274
|
const cells = row
|
|
2144
2275
|
.map((cell, c) => {
|
|
2276
|
+
const style = cellStyle(c);
|
|
2277
|
+
const fmt = colFormat[c] || null;
|
|
2278
|
+
|
|
2145
2279
|
if (isFormulaCell(cell)) {
|
|
2146
2280
|
const result = grid[r][c];
|
|
2147
|
-
|
|
2281
|
+
let display;
|
|
2282
|
+
if (!result.error && fmt) {
|
|
2283
|
+
display = escapeHtml(formatNumber(result.value, fmt));
|
|
2284
|
+
} else {
|
|
2285
|
+
display = escapeHtml(formatFormulaResult(result));
|
|
2286
|
+
}
|
|
2148
2287
|
const formula = escapeAttr(cell.trim());
|
|
2149
2288
|
if (result.error) {
|
|
2150
|
-
return `<td class="sdoc-table-td sdoc-formula-error" title="${formula}">${display}</td>`;
|
|
2289
|
+
return `<td class="sdoc-table-td sdoc-formula-error"${style} title="${formula}">${display}</td>`;
|
|
2290
|
+
}
|
|
2291
|
+
return `<td class="sdoc-table-td sdoc-formula-cell"${style} title="${formula}">${display}</td>`;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Apply column format to numeric data cells
|
|
2295
|
+
if (fmt) {
|
|
2296
|
+
const parsed = parseCellValue(cell);
|
|
2297
|
+
if (!isNaN(parsed.value)) {
|
|
2298
|
+
return `<td class="sdoc-table-td"${style}>${escapeHtml(formatNumber(parsed.value, fmt))}</td>`;
|
|
2151
2299
|
}
|
|
2152
|
-
return `<td class="sdoc-table-td sdoc-formula-cell" title="${formula}">${display}</td>`;
|
|
2153
2300
|
}
|
|
2154
|
-
|
|
2301
|
+
|
|
2302
|
+
return `<td class="sdoc-table-td"${style}>${renderInline(cell)}</td>`;
|
|
2155
2303
|
})
|
|
2156
2304
|
.join("");
|
|
2157
2305
|
return `<tr>${cells}</tr>`;
|
|
@@ -2803,6 +2951,43 @@ const DEFAULT_STYLE = `
|
|
|
2803
2951
|
display: none;
|
|
2804
2952
|
}
|
|
2805
2953
|
|
|
2954
|
+
/* Meta sections (@about) — rendered with a distinct, subdued style
|
|
2955
|
+
so readers can tell at a glance this is document metadata, not body content. */
|
|
2956
|
+
.sdoc-scope.sdoc-meta-section {
|
|
2957
|
+
position: relative;
|
|
2958
|
+
margin: 1.2rem 3rem 1.6rem 3rem;
|
|
2959
|
+
padding: 0.7rem 1rem 0.7rem 1rem;
|
|
2960
|
+
background: rgba(127, 120, 112, 0.06);
|
|
2961
|
+
border: 1px dashed var(--sdoc-border);
|
|
2962
|
+
border-left: 3px solid var(--sdoc-muted);
|
|
2963
|
+
border-radius: 6px;
|
|
2964
|
+
color: var(--sdoc-muted);
|
|
2965
|
+
font-size: 0.95em;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
.sdoc-meta-section .sdoc-heading {
|
|
2969
|
+
margin-top: 0.2rem;
|
|
2970
|
+
color: var(--sdoc-muted);
|
|
2971
|
+
font-weight: 600;
|
|
2972
|
+
border-bottom: none;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
.sdoc-meta-section .sdoc-paragraph {
|
|
2976
|
+
margin: 0.3rem 0;
|
|
2977
|
+
font-style: italic;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
.sdoc-meta-section .sdoc-scope-children > .sdoc-scope {
|
|
2981
|
+
padding-left: 0;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
/* Place the collapse toggle in the gutter to the LEFT of the meta
|
|
2985
|
+
box (in the space created by margin-left), not on the colored
|
|
2986
|
+
left border. Default is left: -1.4em which lands on the border. */
|
|
2987
|
+
.sdoc-meta-section > .sdoc-heading > .sdoc-toggle {
|
|
2988
|
+
left: -2.6em;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2806
2991
|
`;
|
|
2807
2992
|
|
|
2808
2993
|
const PRINT_STYLE = `
|
|
@@ -2940,15 +3125,21 @@ function renderBodyNodes(nodes) {
|
|
|
2940
3125
|
.join("\n");
|
|
2941
3126
|
}
|
|
2942
3127
|
|
|
2943
|
-
function renderHtmlBody(text) {
|
|
3128
|
+
function renderHtmlBody(text, options = {}) {
|
|
2944
3129
|
const parsed = parseSdoc(text);
|
|
2945
3130
|
const metaResult = extractMeta(parsed.nodes);
|
|
2946
3131
|
const savedOptions = _renderOptions;
|
|
2947
3132
|
_renderOptions = {};
|
|
2948
|
-
|
|
3133
|
+
// includeAbout defaults to false: HTML output is treated as "export-shape"
|
|
3134
|
+
// by default, hiding the discovery summary. The preview path in
|
|
3135
|
+
// extension.js opts in with `includeAbout: true` to keep the meta-section
|
|
3136
|
+
// visible during authoring.
|
|
3137
|
+
const includeAbout = options.includeAbout === true;
|
|
3138
|
+
const renderNodes = includeAbout ? metaResult.nodes : stripAboutScopes(metaResult.nodes);
|
|
3139
|
+
const citationData = buildCitationNumbering(renderNodes);
|
|
2949
3140
|
_citationNumbering = citationData.numbering;
|
|
2950
3141
|
_citationDefinitions = citationData.definitions;
|
|
2951
|
-
const result = renderBodyNodes(
|
|
3142
|
+
const result = renderBodyNodes(renderNodes);
|
|
2952
3143
|
_citationNumbering = new Map();
|
|
2953
3144
|
_citationDefinitions = new Map();
|
|
2954
3145
|
_renderOptions = savedOptions;
|
|
@@ -2957,10 +3148,16 @@ function renderHtmlBody(text) {
|
|
|
2957
3148
|
|
|
2958
3149
|
function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
2959
3150
|
_renderOptions = options.renderOptions ?? {};
|
|
2960
|
-
|
|
3151
|
+
// includeAbout defaults to false: HTML/PDF output is hidden-by-default
|
|
3152
|
+
// because exported files are normally sent to a specific recipient who has
|
|
3153
|
+
// already been asked to read the doc, so the "should I read this?" framing
|
|
3154
|
+
// in @about adds noise. The live preview opts in with `includeAbout: true`.
|
|
3155
|
+
const includeAbout = options.includeAbout === true;
|
|
3156
|
+
const renderNodes = includeAbout ? parsed.nodes : stripAboutScopes(parsed.nodes);
|
|
3157
|
+
const citationData = buildCitationNumbering(renderNodes);
|
|
2961
3158
|
_citationNumbering = citationData.numbering;
|
|
2962
3159
|
_citationDefinitions = citationData.definitions;
|
|
2963
|
-
const body = renderBodyNodes(
|
|
3160
|
+
const body = renderBodyNodes(renderNodes);
|
|
2964
3161
|
_citationNumbering = new Map();
|
|
2965
3162
|
_citationDefinitions = new Map();
|
|
2966
3163
|
_renderOptions = {};
|
|
@@ -2988,13 +3185,13 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2988
3185
|
const mermaidInit = mermaidTheme === "auto"
|
|
2989
3186
|
? `var isDark=window.matchMedia("(prefers-color-scheme:dark)").matches;mermaid.initialize({startOnLoad:true,theme:isDark?"dark":"neutral",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});`
|
|
2990
3187
|
: `mermaid.initialize({startOnLoad:true,theme:"${mermaidTheme}",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});`;
|
|
2991
|
-
const mermaidScript = hasMermaidBlocks(
|
|
3188
|
+
const mermaidScript = hasMermaidBlocks(renderNodes)
|
|
2992
3189
|
? `\n<script src="${MERMAID_CDN}"></script>\n<script>${mermaidInit}</script>`
|
|
2993
3190
|
: "";
|
|
2994
3191
|
const katexCssTag = body.includes('class="katex"')
|
|
2995
3192
|
? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
|
|
2996
3193
|
: "";
|
|
2997
|
-
const hasHljs = hasHighlightableCodeBlocks(
|
|
3194
|
+
const hasHljs = hasHighlightableCodeBlocks(renderNodes);
|
|
2998
3195
|
// Highlight.js CSS is inlined (not a <link>) so it is extracted by parseDocHtml in the web viewer
|
|
2999
3196
|
// and applied inside shadow DOM. The @media query handles dark mode in browsers.
|
|
3000
3197
|
const hljsCssInline = hasHljs
|
|
@@ -3306,6 +3503,36 @@ function extractAbout(nodes) {
|
|
|
3306
3503
|
return null;
|
|
3307
3504
|
}
|
|
3308
3505
|
|
|
3506
|
+
// True when an @about scope has no meaningful content. Renderers use this to
|
|
3507
|
+
// skip emitting an empty meta-section / callout. Whitespace-only paragraphs
|
|
3508
|
+
// count as empty.
|
|
3509
|
+
function isAboutEmpty(scope) {
|
|
3510
|
+
if (!scope || !scope.children || scope.children.length === 0) return true;
|
|
3511
|
+
return scope.children.every(
|
|
3512
|
+
(child) => child.type === "paragraph" && (!child.text || child.text.trim() === "")
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// Recursively remove @about scopes from an AST. Used by the HTML/PDF export
|
|
3517
|
+
// paths, which hide @about by default: when a doc is exported and sent to a
|
|
3518
|
+
// specific recipient, the "should I read this?" framing is moot — the sender
|
|
3519
|
+
// already decided the answer is yes. Pass-through for nodes without children.
|
|
3520
|
+
function stripAboutScopes(nodes) {
|
|
3521
|
+
if (!Array.isArray(nodes)) return nodes;
|
|
3522
|
+
const result = [];
|
|
3523
|
+
for (const node of nodes) {
|
|
3524
|
+
if (node && node.type === "scope" && node.id && node.id.toLowerCase() === "about") {
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
if (node && node.type === "scope" && Array.isArray(node.children)) {
|
|
3528
|
+
result.push({ ...node, children: stripAboutScopes(node.children) });
|
|
3529
|
+
} else {
|
|
3530
|
+
result.push(node);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
return result;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3309
3536
|
function collectAllIds(nodes) {
|
|
3310
3537
|
const ids = new Set();
|
|
3311
3538
|
function walk(nodeList) {
|
|
@@ -3531,6 +3758,8 @@ module.exports = {
|
|
|
3531
3758
|
listSections,
|
|
3532
3759
|
extractSection,
|
|
3533
3760
|
extractAbout,
|
|
3761
|
+
isAboutEmpty,
|
|
3762
|
+
stripAboutScopes,
|
|
3534
3763
|
extractDataBlocks,
|
|
3535
3764
|
KNOWN_SCOPE_TYPES,
|
|
3536
3765
|
// Validation
|
package/tools/build-doc.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// SDOC Document — CLI tool for HTML and PDF export
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// node tools/build-doc.js input.sdoc [-o output] [--html]
|
|
5
|
+
// node tools/build-doc.js input.sdoc [-o output] [--html] [--include-about]
|
|
6
6
|
//
|
|
7
7
|
// Default output is PDF (requires Chrome/Chromium).
|
|
8
8
|
// Use --html for HTML-only output (no Chrome needed).
|
|
9
9
|
// If -o is omitted, writes to input.pdf (or input.html with --html).
|
|
10
|
+
// The @about scope is hidden by default; pass --include-about to keep it.
|
|
10
11
|
|
|
11
12
|
const fs = require("fs");
|
|
12
13
|
const path = require("path");
|
|
@@ -16,7 +17,7 @@ const { parseSdoc, extractMeta, resolveIncludes, renderHtmlDocumentFromParsed }
|
|
|
16
17
|
const CONFIG_FILENAME = "sdoc.config.json";
|
|
17
18
|
|
|
18
19
|
function usage() {
|
|
19
|
-
console.error("Usage: build-doc <input.sdoc> [-o output] [--html]");
|
|
20
|
+
console.error("Usage: build-doc <input.sdoc> [-o output] [--html] [--include-about]");
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -98,7 +99,8 @@ function resolveMetaStyles(meta, documentPath) {
|
|
|
98
99
|
return result;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
async function buildHtml(filePath) {
|
|
102
|
+
async function buildHtml(filePath, options = {}) {
|
|
103
|
+
const includeAbout = options.includeAbout === true;
|
|
102
104
|
const resolvedPath = path.resolve(filePath);
|
|
103
105
|
const text = fs.readFileSync(resolvedPath, "utf8");
|
|
104
106
|
const parsed = parseSdoc(text);
|
|
@@ -141,6 +143,7 @@ async function buildHtml(filePath) {
|
|
|
141
143
|
config,
|
|
142
144
|
cssOverride: cssOverride || undefined,
|
|
143
145
|
cssAppend: cssAppendParts.join("\n") || undefined,
|
|
146
|
+
includeAbout,
|
|
144
147
|
}
|
|
145
148
|
);
|
|
146
149
|
}
|
|
@@ -150,12 +153,15 @@ async function main() {
|
|
|
150
153
|
let inputPath = null;
|
|
151
154
|
let outputPath = null;
|
|
152
155
|
let htmlMode = false;
|
|
156
|
+
let includeAbout = false;
|
|
153
157
|
|
|
154
158
|
for (let i = 0; i < args.length; i++) {
|
|
155
159
|
if (args[i] === "-o" && i + 1 < args.length) {
|
|
156
160
|
outputPath = args[++i];
|
|
157
161
|
} else if (args[i] === "--html") {
|
|
158
162
|
htmlMode = true;
|
|
163
|
+
} else if (args[i] === "--include-about") {
|
|
164
|
+
includeAbout = true;
|
|
159
165
|
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
160
166
|
usage();
|
|
161
167
|
} else if (!inputPath) {
|
|
@@ -174,7 +180,7 @@ async function main() {
|
|
|
174
180
|
process.exit(1);
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
const html = await buildHtml(resolvedInput);
|
|
183
|
+
const html = await buildHtml(resolvedInput, { includeAbout });
|
|
178
184
|
|
|
179
185
|
if (htmlMode) {
|
|
180
186
|
if (!outputPath) {
|