@entropicwarrior/sdoc 0.1.18 → 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
type: skill
|
|
6
6
|
|
|
7
|
-
sdoc-version: 0.
|
|
7
|
+
sdoc-version: 0.2
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
# About @about
|
|
@@ -178,7 +178,7 @@ Content of Section B.
|
|
|
178
178
|
|
|
179
179
|
**Important:** \`{[.]}\` with the closing brace on the same line creates an empty list — the brace closes the block immediately. Always put the closing \`}\` on a separate line after the items.
|
|
180
180
|
|
|
181
|
-
Implicit lists
|
|
181
|
+
Implicit lists work for both bullet (\`-\`) and numbered (\`1.\`, \`2.\`, etc.) items. Each item must be a single line. For multi-line item titles or item body content, use the explicit \`{[.]}\` or \`{[#]\` block form.
|
|
182
182
|
|
|
183
183
|
**Task lists** — checkbox syntax inside explicit list blocks:
|
|
184
184
|
|
|
@@ -244,6 +244,27 @@ Content of Section B.
|
|
|
244
244
|
```
|
|
245
245
|
|
|
246
246
|
\`borderless\` removes borders and row striping. \`headerless\` treats all rows as data (no header). Flags combine in any order.
|
|
247
|
+
|
|
248
|
+
Width and alignment flags control table sizing and position:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
{[table 60% center]
|
|
252
|
+
Endpoint | Status
|
|
253
|
+
/v2/weather | Active
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
{[table auto]
|
|
257
|
+
Key | Value
|
|
258
|
+
Version | 2.0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
{[table 400px right borderless]
|
|
262
|
+
v2.0 | Current
|
|
263
|
+
v1.0 | Deprecated
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Width: \`auto\` (shrink to content), \`NN%\` (percentage), or \`NNpx\` (pixels). Default is 100%. Alignment: \`left\` (default), \`center\`, or \`right\`. All flags compose freely in any order.
|
|
247
268
|
}
|
|
248
269
|
|
|
249
270
|
# Inline Formatting @inline-formatting
|
|
@@ -279,7 +300,7 @@ Content of Section B.
|
|
|
279
300
|
 
|
|
280
301
|
```
|
|
281
302
|
|
|
282
|
-
Autolinks: \`\<https://example.com\>\`
|
|
303
|
+
Autolinks: \`\<https://example.com\>\` — angle brackets are optional; bare URLs starting with \`http://\`, \`https://\`, or \`mailto:\` are also auto-linked.
|
|
283
304
|
|
|
284
305
|
Math: Use \`\$...\$\` for inline math and \`\$\$...\$\$\` for display math. Use \`\\\`\\\`\\\`math\` code fences for multi-line equations. A plain \`\$\` followed by a digit (e.g. \`\$100\`) does not trigger math mode.
|
|
285
306
|
}
|
|
@@ -380,7 +401,7 @@ Content of Section B.
|
|
|
380
401
|
@meta {
|
|
381
402
|
type: doc
|
|
382
403
|
|
|
383
|
-
sdoc-version: 0.
|
|
404
|
+
sdoc-version: 0.2
|
|
384
405
|
|
|
385
406
|
company: Irreversible Inc.
|
|
386
407
|
|
|
@@ -420,7 +441,7 @@ Content of Section B.
|
|
|
420
441
|
|
|
421
442
|
\`tags: tag1, tag2\` — comma-separated tags.
|
|
422
443
|
|
|
423
|
-
\`sdoc-version: 0.
|
|
444
|
+
\`sdoc-version: 0.2\` — SDOC format version. A parser warning is
|
|
424
445
|
emitted when this key is missing from \`@meta\`.
|
|
425
446
|
}
|
|
426
447
|
|
|
@@ -441,7 +462,7 @@ Content of Section B.
|
|
|
441
462
|
{
|
|
442
463
|
Backslash escapes special characters: \`\\\\\` \`\\{\` \`\\}\` \`\\@\`
|
|
443
464
|
\`\\[\` \`\\]\` \`\\(\` \`\\)\` \`\\*\` \`\\~\` \`\\#\` \`\\!\` \`\\\<\`
|
|
444
|
-
\`\\\>\` \`\\\$\` \`\\+\` \`\\=\` \`\\-\` \`\\^\`
|
|
465
|
+
\`\\\>\` \`\\\$\` \`\\+\` \`\\=\` \`\\-\` \`\\^\` \`\\?\`
|
|
445
466
|
|
|
446
467
|
A line starting with \`\\#\` renders as a literal \`#\` (not a heading). Use \`\\\$\` to prevent a dollar sign from starting math mode.
|
|
447
468
|
}
|
|
@@ -454,6 +475,89 @@ Content of Section B.
|
|
|
454
475
|
- **Commas:** commas between list items or scopes are allowed but ignored — use them if you find them readable.
|
|
455
476
|
}
|
|
456
477
|
}
|
|
478
|
+
|
|
479
|
+
# Scope Types @scope-types
|
|
480
|
+
{
|
|
481
|
+
A \`:type\` annotation on a heading gives the scope semantic meaning. Place it after the optional \`@id\`:
|
|
482
|
+
|
|
483
|
+
```
|
|
484
|
+
# User Authentication @auth :requirement
|
|
485
|
+
{
|
|
486
|
+
The system shall authenticate users via OAuth 2.0.
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# OAuth Flow :specification @oauth-flow
|
|
490
|
+
{
|
|
491
|
+
Implements @auth using the authorization code flow.
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# Deprecation Notice :warning
|
|
495
|
+
{
|
|
496
|
+
This API will be removed in v3.0.
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
{[.]
|
|
501
|
+
- Syntax: \`# Title @id :type\` or \`# Title :type @id\` or \`# Title :type\` — both orderings work
|
|
502
|
+
- The colon requires whitespace before it: \`# Note: Important\` is NOT a type — the colon is part of the title
|
|
503
|
+
- Any string is valid. Well-known types: \`schema\`, \`example\`, \`requirement\`, \`specification\`, \`definition\`, \`note\`, \`warning\`, \`test\`, \`task\`, \`api\`, \`config\`, \`deprecated\`, \`comment\`
|
|
504
|
+
- Works with K&R style: \`# Title :warning {\`
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
The special type \`:comment\` makes a scope that is in the AST but not rendered — useful for editorial notes and AI agent instructions:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
# TODO :comment
|
|
511
|
+
{
|
|
512
|
+
Rewrite this section after the API stabilises.
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# Data Blocks @data-blocks
|
|
518
|
+
{
|
|
519
|
+
Add \`:data\` to a JSON code fence to have the parser parse the content into structured data on the AST node:
|
|
520
|
+
|
|
521
|
+
`````
|
|
522
|
+
```json :data
|
|
523
|
+
{
|
|
524
|
+
"name": "SDOC",
|
|
525
|
+
"version": "0.2",
|
|
526
|
+
"features": ["scopes", "lists", "tables"]
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
`````
|
|
530
|
+
|
|
531
|
+
{[.]
|
|
532
|
+
- Syntax: \` \`\`\`json :data \` — the \`:data\` flag follows the language tag
|
|
533
|
+
- Invalid JSON produces a parse error
|
|
534
|
+
- JSON-only in v0.2
|
|
535
|
+
- Without \`:data\`, JSON code blocks remain raw text (existing behaviour)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# Comments @comments
|
|
540
|
+
{
|
|
541
|
+
**Line comments** — \`//\` at the start of a line (after optional indentation) skips the line entirely. Not in the AST, not rendered:
|
|
542
|
+
|
|
543
|
+
```
|
|
544
|
+
# Config @config
|
|
545
|
+
{
|
|
546
|
+
// TODO: add validation
|
|
547
|
+
The config file uses JSON format.
|
|
548
|
+
|
|
549
|
+
// hidden from output
|
|
550
|
+
See @setup for details.
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
{[.]
|
|
555
|
+
- Mid-line \`//\` has no effect — URLs like \`https://example.com\` are safe
|
|
556
|
+
- Inside code blocks: \`//\` has no special meaning
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
**Comment scopes** — use the \`:comment\` scope type for structured non-rendered content (see @scope-types above).
|
|
560
|
+
}
|
|
457
561
|
}
|
|
458
562
|
|
|
459
563
|
# Common Mistakes @common-mistakes
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
# SDOC Specification v0.
|
|
1
|
+
# SDOC Specification v0.2 @sdoc-spec
|
|
2
2
|
{
|
|
3
3
|
# Meta @meta
|
|
4
4
|
{
|
|
5
5
|
type: doc
|
|
6
6
|
|
|
7
|
-
sdoc-version: 0.
|
|
7
|
+
sdoc-version: 0.2
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
# About @about
|
|
11
11
|
{
|
|
12
|
-
The formal SDOC v0.
|
|
13
|
-
lists, tables, code blocks, inline formatting, references,
|
|
14
|
-
the meta scope
|
|
15
|
-
|
|
16
|
-
user-facing
|
|
12
|
+
The formal SDOC v0.2 specification. Defines syntax for scopes,
|
|
13
|
+
lists, tables, code blocks, inline formatting, references,
|
|
14
|
+
the meta scope, scope types, data blocks, and comments.
|
|
15
|
+
Includes the formal EBNF grammar. Read for edge cases and
|
|
16
|
+
parser behaviour questions. For a friendlier user-facing
|
|
17
|
+
reference, see \`docs/reference/syntax.sdoc\`.
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
# Overview @overview
|
|
@@ -51,12 +52,17 @@
|
|
|
51
52
|
{
|
|
52
53
|
```
|
|
53
54
|
# Title text @id
|
|
55
|
+
# Title text @id :type
|
|
56
|
+
# Title text :type @id
|
|
57
|
+
# Title text :type
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
{[.]
|
|
57
61
|
- The line must start with `#` (after optional indentation)
|
|
58
62
|
- Multiple `#` characters are allowed but do not affect depth. Depth comes only from scope nesting
|
|
59
|
-
- The optional `@id` must appear at the end of the line, separated by whitespace
|
|
63
|
+
- The optional `@id` must appear at the end of the line (or before `:type`), separated by whitespace
|
|
64
|
+
- The optional `:type` assigns a scope type (see @scope-types). Both `@id :type` and `:type @id` orderings are valid
|
|
65
|
+
- `:type` requires whitespace before the colon, so `# Note: Important` is NOT a scope type — the colon is part of the title
|
|
60
66
|
- If no `@id` is present, the scope has no ID
|
|
61
67
|
- If you need a literal `@` in the title, escape it (`\@`)
|
|
62
68
|
}
|
|
@@ -122,7 +128,7 @@ Content of Section B.
|
|
|
122
128
|
- The opener must be the last token on the line (trailing whitespace is allowed)
|
|
123
129
|
- Applies to `{`, `{[.]`, `{[#]`, `{[table]`, and `{[table <flags>]`
|
|
124
130
|
- Also works on list-item shorthand lines (e.g., `- Item {`)
|
|
125
|
-
- Table options (e.g., `{[table borderless]`) work in K&R style: `# Data {[table borderless]`
|
|
131
|
+
- Table options (e.g., `{[table borderless]`, `{[table 60% center]`) work in K&R style: `# Data {[table borderless]`
|
|
126
132
|
- Escaped braces (`\{`) are not treated as openers
|
|
127
133
|
- The closing `}` must still appear on its own line
|
|
128
134
|
- Inline blocks (`{ content }`) are not affected; a line ending with `}` is not treated as K&R
|
|
@@ -344,9 +350,33 @@ Content of Section B.
|
|
|
344
350
|
{[.]
|
|
345
351
|
- `borderless` removes all table borders and row striping
|
|
346
352
|
- `headerless` treats the first row as data (no header row)
|
|
347
|
-
-
|
|
353
|
+
- `auto` sets width to shrink-to-content
|
|
354
|
+
- A percentage value (e.g. `60%`, `33.3%`) sets an explicit percentage width
|
|
355
|
+
- A pixel value (e.g. `400px`) sets an explicit pixel width
|
|
356
|
+
- `center` centers the table (auto margins)
|
|
357
|
+
- `right` right-aligns the table (margin-left: auto)
|
|
358
|
+
- `left` left-aligns the table (default, no extra styles)
|
|
359
|
+
- Default width is 100%; default alignment is left
|
|
360
|
+
- All flags can be combined in any order
|
|
348
361
|
- Works with both Allman and K&R brace styles
|
|
349
362
|
}
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
{[table 60% center]
|
|
366
|
+
Endpoint | Status
|
|
367
|
+
/v2/weather | Active
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
{[table auto right borderless]
|
|
371
|
+
Key | Value
|
|
372
|
+
Version | 2.0
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
{[table 400px]
|
|
376
|
+
Name | Age
|
|
377
|
+
Alice | 30
|
|
378
|
+
}
|
|
379
|
+
```
|
|
350
380
|
}
|
|
351
381
|
}
|
|
352
382
|
|
|
@@ -387,6 +417,8 @@ Content of Section B.
|
|
|
387
417
|
```
|
|
388
418
|
|
|
389
419
|
Only `http`, `https`, and `mailto` schemes are recognised.
|
|
420
|
+
|
|
421
|
+
Bare URLs starting with `http://`, `https://`, or `mailto:` are also auto-linked without angle brackets.
|
|
390
422
|
}
|
|
391
423
|
|
|
392
424
|
# Images @images
|
|
@@ -466,7 +498,7 @@ Content of Section B.
|
|
|
466
498
|
|
|
467
499
|
# Escaping @escaping
|
|
468
500
|
{
|
|
469
|
-
In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` `\^` and `` \` ``.
|
|
501
|
+
In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` `\^` `\?` and `` \` ``.
|
|
470
502
|
|
|
471
503
|
Escapes are processed before reference detection.
|
|
472
504
|
|
|
@@ -516,6 +548,115 @@ Content of Section B.
|
|
|
516
548
|
}
|
|
517
549
|
}
|
|
518
550
|
}
|
|
551
|
+
|
|
552
|
+
# Scope Types @scope-types
|
|
553
|
+
{
|
|
554
|
+
A scope type annotation provides semantic meaning to a scope. The type is specified with `:typename` on the heading line, after optional `@id`:
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
# User Authentication @auth :requirement
|
|
558
|
+
{
|
|
559
|
+
The system shall authenticate users via OAuth 2.0.
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
# OAuth Flow :specification @oauth-flow
|
|
563
|
+
{
|
|
564
|
+
Implements @auth using the authorization code flow.
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Important :warning
|
|
568
|
+
{
|
|
569
|
+
This API is deprecated and will be removed in v3.0.
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
{[.]
|
|
574
|
+
- Syntax: `# Title @id :type` or `# Title :type @id` or `# Title :type` — both orderings of `@id` and `:type` are supported
|
|
575
|
+
- The colon in `:type` requires whitespace before it, so `# Note: Important` is NOT a scope type — the colon is part of the title text
|
|
576
|
+
- Any string is valid as a type name
|
|
577
|
+
- Well-known types: `schema`, `example`, `requirement`, `specification`, `definition`, `note`, `warning`, `test`, `task`, `api`, `config`, `deprecated`, `comment`
|
|
578
|
+
- The type is stored on the AST node and available to renderers and tooling
|
|
579
|
+
- Scope types enable AI agents to filter and navigate by semantic meaning (e.g., "show all requirements", "find tests for this spec")
|
|
580
|
+
- Works with K&R brace style: `# Title :warning {`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Comment Scopes @comment-scopes
|
|
584
|
+
{
|
|
585
|
+
The `:comment` scope type creates a scope that is present in the AST but not rendered in the document output. This is useful for editorial annotations, AI agent instructions, and internal notes:
|
|
586
|
+
|
|
587
|
+
```
|
|
588
|
+
# TODO :comment
|
|
589
|
+
{
|
|
590
|
+
Rewrite this section after the API stabilises.
|
|
591
|
+
@alice please review the error handling.
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
# Agent Instructions :comment
|
|
595
|
+
{
|
|
596
|
+
When summarising this document, focus on the
|
|
597
|
+
requirements and skip the implementation details.
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
{[.]
|
|
602
|
+
- Comment scopes use the standard scope type system — no special syntax beyond `:comment`
|
|
603
|
+
- The scope is parsed into the AST (agents and tooling can extract it)
|
|
604
|
+
- The scope is not rendered in HTML, PDF, or other visual outputs
|
|
605
|
+
- Comment scopes can contain any valid SDOC content (paragraphs, lists, code blocks, nested scopes)
|
|
606
|
+
- Useful for structured annotations that are too complex for line comments
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# Data Blocks @data-blocks
|
|
612
|
+
{
|
|
613
|
+
A code fence with the `:data` flag causes the parser to parse the content as structured data and attach it to the AST node:
|
|
614
|
+
|
|
615
|
+
`````
|
|
616
|
+
```json :data
|
|
617
|
+
{
|
|
618
|
+
"name": "SDOC",
|
|
619
|
+
"version": "0.2",
|
|
620
|
+
"features": ["scopes", "lists", "tables"]
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
`````
|
|
624
|
+
|
|
625
|
+
{[.]
|
|
626
|
+
- Syntax: ` ```json :data ` — the `:data` flag follows the language tag on the opening fence line
|
|
627
|
+
- The parser parses the JSON content and stores the result on the AST node
|
|
628
|
+
- Invalid JSON produces a parse error
|
|
629
|
+
- JSON is the only supported data format in v0.2
|
|
630
|
+
- Without the `:data` flag, JSON code blocks are treated as raw text (existing behaviour)
|
|
631
|
+
- Data blocks render visually the same as regular code blocks, but the parsed data is available to tooling and agents
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
# Line Comments @line-comments
|
|
636
|
+
{
|
|
637
|
+
A line starting with `//` (after optional indentation) is a line comment. The line is skipped entirely — it does not appear in the AST and is not rendered:
|
|
638
|
+
|
|
639
|
+
```
|
|
640
|
+
# Configuration @config
|
|
641
|
+
{
|
|
642
|
+
// TODO: add validation rules
|
|
643
|
+
The config file uses JSON format.
|
|
644
|
+
|
|
645
|
+
// This paragraph is hidden from output
|
|
646
|
+
// but visible in the source file.
|
|
647
|
+
|
|
648
|
+
See @setup for installation steps.
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
{[.]
|
|
653
|
+
- The `//` must be at the start of the line (after optional whitespace)
|
|
654
|
+
- Comment lines are discarded during parsing — they are not present in the AST
|
|
655
|
+
- Inside code blocks: `//` has no special meaning (code block content is raw)
|
|
656
|
+
- Mid-line `//` has no special meaning — URLs like `https://example.com` are safe
|
|
657
|
+
- Use line comments for quick annotations; use comment scopes (@comment-scopes) for structured non-rendered content
|
|
658
|
+
}
|
|
659
|
+
}
|
|
519
660
|
}
|
|
520
661
|
|
|
521
662
|
# Styles, Header, and Footer @styling
|
|
@@ -585,7 +726,7 @@ Content of Section B.
|
|
|
585
726
|
date: 2026-02-09
|
|
586
727
|
version: 1.0
|
|
587
728
|
status: Draft
|
|
588
|
-
sdoc-version: 0.
|
|
729
|
+
sdoc-version: 0.2
|
|
589
730
|
}
|
|
590
731
|
```
|
|
591
732
|
|
|
@@ -593,7 +734,7 @@ Content of Section B.
|
|
|
593
734
|
- Key matching is case-insensitive
|
|
594
735
|
- The pattern requires at least one space after the colon (`key: value`, not `key:value`)
|
|
595
736
|
- Well-known keys: `style`, `styleappend`/`style-append`, `header`, `footer`, `sdoc-version`
|
|
596
|
-
- `sdoc-version` identifies the SDOC format version the document targets (current: `0.
|
|
737
|
+
- `sdoc-version` identifies the SDOC format version the document targets (current: `0.2`). A parser warning is emitted when this key is missing.
|
|
597
738
|
- All other keys are stored as custom properties (e.g., `author`, `date`, `version`, `status`, `tags`)
|
|
598
739
|
- Sub-scope syntax takes precedence: if both `# Style { path }` and `style: path` exist, the sub-scope value wins
|
|
599
740
|
- Key:value and sub-scope syntax can be mixed freely in the same meta scope
|
|
@@ -713,6 +854,7 @@ Content of Section A.
|
|
|
713
854
|
- `>` blockquote line
|
|
714
855
|
- `---` / `***` / `___` horizontal rule
|
|
715
856
|
- `` ``` `` code fence
|
|
857
|
+
- `//` line comment (discarded, not in AST)
|
|
716
858
|
}
|
|
717
859
|
|
|
718
860
|
Blank lines are allowed anywhere and are ignored.
|
|
@@ -731,25 +873,34 @@ Content of Section A.
|
|
|
731
873
|
scope = heading ws? block
|
|
732
874
|
| heading ws? braceless_body
|
|
733
875
|
| heading_with_opener block_body "}" ;
|
|
734
|
-
heading = "#" { "#" } ws title
|
|
735
|
-
|
|
876
|
+
heading = "#" { "#" } ws title
|
|
877
|
+
((ws id)? (ws scope_type)? | (ws scope_type)? (ws id)?) ;
|
|
878
|
+
heading_with_opener = "#" { "#" } ws title
|
|
879
|
+
((ws id)? (ws scope_type)? | (ws scope_type)? (ws id)?) ws block_opener ;
|
|
736
880
|
id = "@" ident ;
|
|
881
|
+
scope_type = ":" ident ; (* id and scope_type may appear in either order *)
|
|
737
882
|
block_opener = "{" | "{[.]" | "{[#]" | table_open ;
|
|
738
883
|
block = "{" ws? block_body "}" ;
|
|
739
|
-
braceless_body = { paragraph | code_block |
|
|
740
|
-
|
|
|
741
|
-
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
884
|
+
braceless_body = { paragraph | code_block | data_block | blockquote
|
|
885
|
+
| implicit_list | horizontal_rule | headingless_scope
|
|
886
|
+
| list_scope | table_scope | bare_directive
|
|
887
|
+
| line_comment | blank } ;
|
|
888
|
+
|
|
889
|
+
bare_directive = "@" ("meta" | "about") (ws block | braceless_body) ;
|
|
890
|
+
block_body = { blank | line_comment | paragraph | scope
|
|
891
|
+
| headingless_scope | list_scope | table_scope
|
|
745
892
|
| implicit_list | blockquote | horizontal_rule
|
|
746
|
-
| code_block | comma_sep } ;
|
|
893
|
+
| code_block | data_block | bare_directive | comma_sep } ;
|
|
747
894
|
headingless_scope = "{" ws? block_body "}" ;
|
|
748
895
|
list_scope = list_open ws? list_body "}" ;
|
|
749
896
|
list_open = "{[.]" | "{[#]" ;
|
|
750
897
|
table_scope = table_open ws? table_body "}" ;
|
|
751
898
|
table_open = "{[table" { ws table_flag } "]" ;
|
|
752
|
-
table_flag = "borderless" | "headerless"
|
|
899
|
+
table_flag = "borderless" | "headerless"
|
|
900
|
+
| "auto" | percentage | pixels
|
|
901
|
+
| "left" | "center" | "right" ;
|
|
902
|
+
percentage = digit { digit } [ "." digit { digit } ] "%" ;
|
|
903
|
+
pixels = digit { digit } "px" ;
|
|
753
904
|
table_body = table_row { table_row } ;
|
|
754
905
|
table_row = cell { "|" cell } ;
|
|
755
906
|
list_body = { blank | comma_sep | scope | list_item_shorthand
|
|
@@ -767,15 +918,19 @@ Content of Section A.
|
|
|
767
918
|
paragraph = text_line { ws? text_line } ;
|
|
768
919
|
text_line = line_not_starting_with_command ;
|
|
769
920
|
|
|
770
|
-
blockquote = quote_line { quote_line
|
|
921
|
+
blockquote = quote_line { quote_line } ;
|
|
771
922
|
quote_line = ">" text_line ;
|
|
772
923
|
|
|
773
924
|
horizontal_rule = "---" | "***" | "___" ;
|
|
774
925
|
|
|
775
926
|
code_block = fence_open raw_text fence_close ;
|
|
927
|
+
data_block = data_fence_open raw_text fence_close ;
|
|
776
928
|
fence_open = "```" [lang] [ws "src:" path] [ws "lines:" range] newline ;
|
|
929
|
+
data_fence_open = "```" lang ws ":data" newline ;
|
|
777
930
|
fence_close = "```" newline ;
|
|
778
931
|
|
|
932
|
+
line_comment = "//" { any_char } newline ; (* discarded, not in AST *)
|
|
933
|
+
|
|
779
934
|
comma_sep = "," ;
|
|
780
935
|
blank = newline ;
|
|
781
936
|
|
|
@@ -783,7 +938,10 @@ Content of Section A.
|
|
|
783
938
|
```
|
|
784
939
|
|
|
785
940
|
{[.]
|
|
786
|
-
- `title` is the remainder of the heading line, excluding the optional trailing `@id`
|
|
941
|
+
- `title` is the remainder of the heading line, excluding the optional trailing `@id` and `:type`
|
|
942
|
+
- `scope_type` and `id` may appear in either order after the title
|
|
943
|
+
- `line_comment` lines are discarded during parsing and do not appear in the AST
|
|
944
|
+
- `data_block` content is parsed as JSON; invalid JSON produces a parse error
|
|
787
945
|
- If a line starts with a command token, it is not a paragraph line
|
|
788
946
|
- The grammar is line-oriented; practical parsers should operate on lines
|
|
789
947
|
}
|
|
@@ -792,10 +950,10 @@ Content of Section A.
|
|
|
792
950
|
# Open Questions @open-questions
|
|
793
951
|
{
|
|
794
952
|
{[.]
|
|
795
|
-
- Comment syntax (if any)
|
|
796
953
|
- Duplicate ID resolution (error vs warning vs nearest-scope)
|
|
797
|
-
- Additional list types (
|
|
954
|
+
- Additional list types (alpha, roman)
|
|
798
955
|
- Additional inline formatting (underline)
|
|
956
|
+
- Additional data block formats beyond JSON (YAML, TOML)
|
|
799
957
|
}
|
|
800
958
|
}
|
|
801
959
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@entropicwarrior/sdoc",
|
|
3
3
|
"displayName": "SDOC",
|
|
4
4
|
"description": "A plain-text documentation format with explicit brace scoping — deterministic parsing, AI-agent efficiency, and 10-50x token savings vs Markdown.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"publisher": "entropicwarrior",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
package/src/notion-renderer.js
CHANGED
|
@@ -246,6 +246,7 @@ function renderNotionBlocks(nodes) {
|
|
|
246
246
|
// Unwrap document scope wrapper (single root scope)
|
|
247
247
|
if (nodes.length === 1 && nodes[0].type === "scope" && nodes[0].children) {
|
|
248
248
|
const doc = nodes[0];
|
|
249
|
+
if (doc.scopeType === "comment") return [];
|
|
249
250
|
if (doc.hasHeading && doc.title) {
|
|
250
251
|
// Document title scope: render as top-level toggle heading
|
|
251
252
|
const childBlocks = renderChildren(doc.children, 2, 1);
|
|
@@ -290,6 +291,8 @@ function renderNode(node, depth, nestLevel) {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
function renderScope(scope, depth, nestLevel) {
|
|
294
|
+
if (scope.scopeType === "comment") return [];
|
|
295
|
+
|
|
293
296
|
const level = Math.min(3, Math.max(1, depth));
|
|
294
297
|
|
|
295
298
|
if (scope.hasHeading === false) {
|
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 = [];
|