@entropicwarrior/sdoc 0.1.3 → 0.1.5
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/knowledge.js +2 -0
- package/lexica/sdoc-authoring.sdoc +445 -0
- package/lexica/slide-authoring.sdoc +411 -0
- package/lexica/specification.sdoc +779 -0
- package/package.json +5 -2
- package/src/slide-pdf.js +99 -0
- package/src/slide-renderer.js +344 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
# SDOC Specification v0.1 @sdoc-spec
|
|
2
|
+
{
|
|
3
|
+
# Meta @meta
|
|
4
|
+
{
|
|
5
|
+
type: doc
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
# About @about
|
|
9
|
+
{
|
|
10
|
+
The formal SDOC v0.1 specification. Defines syntax for scopes,
|
|
11
|
+
lists, tables, code blocks, inline formatting, references, and
|
|
12
|
+
the meta scope. Includes the formal EBNF grammar. Read for
|
|
13
|
+
edge cases and parser behaviour questions. For a friendlier
|
|
14
|
+
user-facing reference, see \`docs/reference/syntax.sdoc\`.
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Overview @overview
|
|
18
|
+
{
|
|
19
|
+
SDOC ("Simple/Smart Documentation") is a plain text documentation format with explicit scoping.
|
|
20
|
+
|
|
21
|
+
# Goals @goals
|
|
22
|
+
{
|
|
23
|
+
{[.]
|
|
24
|
+
- Explicit scoping with braces; whitespace and indentation are cosmetic
|
|
25
|
+
- Minimal syntax, easy to parse and render
|
|
26
|
+
- Automatic heading styles based on nesting depth
|
|
27
|
+
- Easy lists and easy references
|
|
28
|
+
- No hidden metadata: only user-provided IDs exist
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Core Concepts @core-concepts
|
|
33
|
+
{
|
|
34
|
+
{[.]
|
|
35
|
+
- A document is a tree of scopes
|
|
36
|
+
- A scope has a heading line and either a brace-delimited block (`{ ... }`) or braceless content terminated by the next heading, `}`, or EOF
|
|
37
|
+
- Headings are explicit and always start with `#`
|
|
38
|
+
- A scope may have an optional human ID (e.g., `@overview`)
|
|
39
|
+
- References use `@id` in text
|
|
40
|
+
- Paragraphs are separated by blank lines or by a new scope at the same level
|
|
41
|
+
- Whitespace (spaces/tabs) is ignored except where noted
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Syntax @syntax
|
|
47
|
+
{
|
|
48
|
+
# Heading Line @heading-line
|
|
49
|
+
{
|
|
50
|
+
```
|
|
51
|
+
# Title text @id
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
{[.]
|
|
55
|
+
- The line must start with `#` (after optional indentation)
|
|
56
|
+
- Multiple `#` characters are allowed but do not affect depth. Depth comes only from scope nesting
|
|
57
|
+
- The optional `@id` must appear at the end of the line, separated by whitespace
|
|
58
|
+
- If no `@id` is present, the scope has no ID
|
|
59
|
+
- If you need a literal `@` in the title, escape it (`\@`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Scope Block @scope-block
|
|
64
|
+
{
|
|
65
|
+
```
|
|
66
|
+
# Title
|
|
67
|
+
{
|
|
68
|
+
...
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`{` opens a scope block and `}` closes it. Indentation is cosmetic.
|
|
73
|
+
|
|
74
|
+
# Braceless Leaf Scopes @braceless-scopes
|
|
75
|
+
{
|
|
76
|
+
A heading not followed by `{` or a block opener creates a braceless scope. The scope's content runs until the next `#` heading at the same level, a closing `}`, or end of file:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
# Section A
|
|
80
|
+
This is content of Section A.
|
|
81
|
+
It can span multiple lines.
|
|
82
|
+
|
|
83
|
+
# Section B
|
|
84
|
+
Content of Section B.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
{[.]
|
|
88
|
+
- Braceless scopes can contain paragraphs, code blocks, blockquotes, implicit lists, horizontal rules, tables, and headingless scopes
|
|
89
|
+
- Braceless scopes cannot contain child `#` headings; encountering one ends the scope (the heading becomes a sibling)
|
|
90
|
+
- A closing `}` also terminates a braceless scope (used when braceless scopes appear inside an explicit parent)
|
|
91
|
+
- Braceless and explicit scopes can be freely mixed in the same document
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# K&R Style Brace Placement @knr-braces
|
|
96
|
+
{
|
|
97
|
+
The opening brace (or list/table opener) may appear at the end of a heading or list-item line instead of on its own line:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
# Title {
|
|
101
|
+
...
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# My List {[.]
|
|
105
|
+
- Item 1
|
|
106
|
+
- Item 2
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Data {[table]
|
|
110
|
+
Name | Age
|
|
111
|
+
Alice | 30
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Title @id {
|
|
115
|
+
...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
{[.]
|
|
120
|
+
- The opener must be the last token on the line (trailing whitespace is allowed)
|
|
121
|
+
- Applies to `{`, `{[.]`, `{[#]`, `{[table]`, and `{[table <flags>]`
|
|
122
|
+
- Also works on list-item shorthand lines (e.g., `- Item {`)
|
|
123
|
+
- Table options (e.g., `{[table borderless]`) work in K&R style: `# Data {[table borderless]`
|
|
124
|
+
- Escaped braces (`\{`) are not treated as openers
|
|
125
|
+
- The closing `}` must still appear on its own line
|
|
126
|
+
- Inline blocks (`{ content }`) are not affected; a line ending with `}` is not treated as K&R
|
|
127
|
+
- Headingless scopes (bare `{` on its own line) are not affected
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Headingless Scopes @headingless-scopes
|
|
132
|
+
{
|
|
133
|
+
A bare `{` block without a preceding heading creates a headingless scope:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
{
|
|
137
|
+
Grouped content here.
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
{[.]
|
|
142
|
+
- Headingless scopes render as sections without a heading element
|
|
143
|
+
- They inherit the same nesting/indentation as headed scopes
|
|
144
|
+
- Useful for grouping paragraphs or creating visual indentation without a title
|
|
145
|
+
- Can be nested arbitrarily
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Inline Blocks @inline-blocks
|
|
150
|
+
{
|
|
151
|
+
For simple content, blocks can be written on a single line:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
# Name
|
|
155
|
+
{ John Doe }
|
|
156
|
+
|
|
157
|
+
# Count
|
|
158
|
+
{ 42 }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
{[.]
|
|
162
|
+
- Inline blocks must not contain unescaped `{` or `}` characters
|
|
163
|
+
- Multi-line blocks are always supported and sometimes clearer for longer content
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Lists @lists
|
|
169
|
+
{
|
|
170
|
+
Lists are declared by a list block type:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
# Bulleted List
|
|
174
|
+
{[.]
|
|
175
|
+
# Item 1
|
|
176
|
+
{ ... }
|
|
177
|
+
# Item 2
|
|
178
|
+
{ ... }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Numbered List
|
|
182
|
+
{[#]
|
|
183
|
+
# Item 1
|
|
184
|
+
{ ... }
|
|
185
|
+
# Item 2
|
|
186
|
+
{ ... }
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
{[.]
|
|
191
|
+
- `{[.]` creates a bulleted list
|
|
192
|
+
- `{[#]` creates a numbered list
|
|
193
|
+
- List items are scopes inside the list block
|
|
194
|
+
- Commas between list items are allowed but ignored
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# List Item Shorthand @list-shorthand
|
|
198
|
+
{
|
|
199
|
+
Inside a list block, `- Title text` is shorthand for a list item scope:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
{[.]
|
|
203
|
+
- Item 1
|
|
204
|
+
- Item 2
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
{[.]
|
|
209
|
+
- A shorthand item may optionally be followed by a block (`{ ... }` or list opener)
|
|
210
|
+
- If no block follows, the item has no body
|
|
211
|
+
- Outside list blocks, leading list-item lines form implicit lists (see @implicit-lists)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Numbered shorthand is also supported:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
{[#]
|
|
218
|
+
1. Item 1
|
|
219
|
+
2. Item 2
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
# Multi-line List Item Shorthand @multi-line-list-items
|
|
224
|
+
{
|
|
225
|
+
Inside an explicit list block (`{[.]` or `{[#]`), a shorthand item's title may span multiple lines. Lines that follow the item marker and are not themselves a command token (heading, list marker, brace, fence, blockquote, horizontal rule, or blank line) are treated as continuation lines and joined to the item title with a single space:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
{[.]
|
|
229
|
+
- This is a long list item
|
|
230
|
+
that continues on the next line
|
|
231
|
+
- Short item
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
{[.]
|
|
236
|
+
- Continuation lines are joined with a single space
|
|
237
|
+
- Continuation stops at: blank lines, list markers (`- `, `1. `), headings (`#`), block openers (`{`, `{[.]`, `{[#]`, `{[table]`), block closers (`}`), code fences, blockquotes, and horizontal rules
|
|
238
|
+
- A block (`{ ... }` or list/table opener) may still follow the completed multi-line title
|
|
239
|
+
- Multi-line continuation is only supported in explicit list blocks, not in implicit lists
|
|
240
|
+
- Indentation of continuation lines is cosmetic
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Implicit Lists @implicit-lists
|
|
246
|
+
{
|
|
247
|
+
Inside a normal scope block, a run of list-item lines is treated as a list:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
# Simple bulleted list
|
|
251
|
+
{
|
|
252
|
+
- Item 1
|
|
253
|
+
- Item 2
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Simple numbered list
|
|
257
|
+
{
|
|
258
|
+
1. First
|
|
259
|
+
2. Second
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
{[.]
|
|
264
|
+
- The list type is chosen by the first item marker (`-` for bulleted, `1.` / `1)` for numbered)
|
|
265
|
+
- Mixed markers end the implicit list
|
|
266
|
+
- Each item may optionally be followed by a block (`{ ... }` or list opener)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Anonymous List Items @anonymous-list-items
|
|
271
|
+
{
|
|
272
|
+
Inside a list block, a list item can start with a block directly:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
{[.]
|
|
276
|
+
{
|
|
277
|
+
This item has no heading line.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The block contents are rendered as the list item body.
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Task Lists @task-lists
|
|
286
|
+
{
|
|
287
|
+
Inside a list block, task items use Markdown-style checkboxes:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
{[.]
|
|
291
|
+
- [ ] Pending task
|
|
292
|
+
- [x] Completed task
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Works in both bulleted and numbered list blocks.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Tables @tables
|
|
301
|
+
{
|
|
302
|
+
Tables are declared with `{[table]`:
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
{[table]
|
|
306
|
+
Name | Age | City
|
|
307
|
+
Alice | 30 | NYC
|
|
308
|
+
Bob | 25 | LA
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
{[.]
|
|
313
|
+
- The first row is always the header (unless `headerless` is specified)
|
|
314
|
+
- Columns are separated by `|`
|
|
315
|
+
- Each row is a single line
|
|
316
|
+
- Cell contents support inline formatting
|
|
317
|
+
- Leading and trailing whitespace in cells is trimmed
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Table Options @table-options
|
|
321
|
+
{
|
|
322
|
+
Optional flags after `table` in the block opener control table rendering:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
{[table borderless]
|
|
326
|
+
Feature | Status
|
|
327
|
+
Parser | Complete
|
|
328
|
+
Renderer | Complete
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
{[table headerless]
|
|
332
|
+
Alice | 30 | New York
|
|
333
|
+
Bob | 25 | Los Angeles
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
{[table borderless headerless]
|
|
337
|
+
 | 
|
|
338
|
+
 | 
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
{[.]
|
|
343
|
+
- `borderless` removes all table borders and row striping
|
|
344
|
+
- `headerless` treats the first row as data (no header row)
|
|
345
|
+
- Flags can be combined in any order
|
|
346
|
+
- Works with both Allman and K&R brace styles
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Paragraphs @paragraphs
|
|
352
|
+
{
|
|
353
|
+
{[.]
|
|
354
|
+
- Consecutive text lines are joined into a single paragraph
|
|
355
|
+
- A blank line ends the paragraph
|
|
356
|
+
- A new scope or list at the same level also ends the paragraph
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# References @references
|
|
361
|
+
{
|
|
362
|
+
{[.]
|
|
363
|
+
- A reference is `@id` in text (unescaped)
|
|
364
|
+
- References link to the scope with that ID
|
|
365
|
+
- ID uniqueness is strongly recommended; tooling may warn on duplicates
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# External Links @external-links
|
|
370
|
+
{
|
|
371
|
+
Markdown-style links:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
[label](https://example.com)
|
|
375
|
+
```
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Autolinks @autolinks
|
|
379
|
+
{
|
|
380
|
+
Angle-bracket links:
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
<https://example.com>
|
|
384
|
+
<mailto:hello@example.com>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Only `http`, `https`, and `mailto` schemes are recognised.
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Images @images
|
|
391
|
+
{
|
|
392
|
+
Markdown-style images:
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+

|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
# Image Width and Alignment @image-width
|
|
399
|
+
{
|
|
400
|
+
An optional size and alignment suffix can follow the URL, separated by a space and introduced with `=`:
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+

|
|
404
|
+

|
|
405
|
+

|
|
406
|
+

|
|
407
|
+

|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
{[.]
|
|
411
|
+
- `=<number>%` or `=<number>px` sets the image width
|
|
412
|
+
- An optional alignment keyword follows the width: `center`, `left`, or `right`
|
|
413
|
+
- `center` uses auto margins for horizontal centering
|
|
414
|
+
- `left` floats the image left with text wrapping on the right
|
|
415
|
+
- `right` floats the image right with text wrapping on the left
|
|
416
|
+
- Without an alignment keyword, images display inline (side by side when adjacent)
|
|
417
|
+
- Two images on the same line with explicit widths sit side by side naturally
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Blockquotes @blockquotes
|
|
423
|
+
{
|
|
424
|
+
```
|
|
425
|
+
> A quoted line.
|
|
426
|
+
> Another line in the same quote.
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
{[.]
|
|
430
|
+
- Consecutive `>` lines form a single blockquote
|
|
431
|
+
- A blank `>` line breaks paragraphs within the blockquote
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Horizontal Rules @horizontal-rules
|
|
436
|
+
{
|
|
437
|
+
A line of three or more `-`, `*`, or `_` characters:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
---
|
|
441
|
+
```
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Inline Formatting @inline-formatting
|
|
445
|
+
{
|
|
446
|
+
{[.]
|
|
447
|
+
- Emphasis: `*em*`
|
|
448
|
+
- Strong: `**strong**`
|
|
449
|
+
- Strikethrough: `~~strike~~`
|
|
450
|
+
- Inline code: `` `code` ``
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# Escaping @escaping
|
|
455
|
+
{
|
|
456
|
+
In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` and `` \` ``.
|
|
457
|
+
|
|
458
|
+
Escapes are processed before reference detection.
|
|
459
|
+
|
|
460
|
+
If a line begins with `\#`, it is treated as a normal paragraph line (rendered with a literal `#`). If a line begins with `\>`, it is treated as a normal paragraph line (rendered with a literal `>`).
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# Code Blocks @code-blocks
|
|
464
|
+
{
|
|
465
|
+
Fenced blocks for code or raw text:
|
|
466
|
+
|
|
467
|
+
`````
|
|
468
|
+
```lang
|
|
469
|
+
raw text here { # @ } is not parsed
|
|
470
|
+
```
|
|
471
|
+
`````
|
|
472
|
+
|
|
473
|
+
{[.]
|
|
474
|
+
- The opening and closing fences must be on their own lines
|
|
475
|
+
- Anything inside is treated as raw text (no parsing, no escapes)
|
|
476
|
+
- Optional language tag after the opening fence
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# Include by Link @code-includes
|
|
480
|
+
{
|
|
481
|
+
A code fence can reference an external file using `src:` metadata on the opening fence line. The code block body is replaced by the file contents:
|
|
482
|
+
|
|
483
|
+
`````
|
|
484
|
+
```json src:./data.json
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```json src:./data.json lines:3-5
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
```src:https://example.com/schema.json
|
|
491
|
+
```
|
|
492
|
+
`````
|
|
493
|
+
|
|
494
|
+
{[.]
|
|
495
|
+
- `src:<path>` references a file path (relative to the document) or URL
|
|
496
|
+
- `lines:<start>-<end>` optionally limits to a line range (1-based, inclusive)
|
|
497
|
+
- The language tag is optional and goes before `src:`
|
|
498
|
+
- Any body text in the code block is replaced by the resolved file contents
|
|
499
|
+
- If the file cannot be read, an error message is shown in the code block
|
|
500
|
+
- URL sources are fetched and cached; the cache is cleared on document save
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
# Styles, Header, and Footer @styling
|
|
507
|
+
{
|
|
508
|
+
# Hierarchical Config @hierarchical-config
|
|
509
|
+
{
|
|
510
|
+
Place `sdoc.config.json` in any folder. When rendering a file, configs are merged from the workspace root down to the file's folder.
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
{
|
|
514
|
+
"style": "styles/sdoc.custom.css",
|
|
515
|
+
"styleAppend": "styles/overrides.css",
|
|
516
|
+
"header": "My Project Docs",
|
|
517
|
+
"footer": "© 2026 My Company"
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
{[.]
|
|
522
|
+
- The closest config to the file overrides parent configs
|
|
523
|
+
- `style` replaces the default stylesheet
|
|
524
|
+
- `styleAppend` is appended after the base stylesheet (string or array)
|
|
525
|
+
- `header` and `footer` are plain text with inline formatting supported
|
|
526
|
+
- Paths in `style` and `styleAppend` are resolved relative to the config file
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
A starter stylesheet template is provided at `examples/sdoc.template.css`.
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
# Per-File Overrides (Meta Scope) @meta-scope
|
|
533
|
+
{
|
|
534
|
+
A reserved meta scope overrides header/footer and styles:
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
# Meta @meta
|
|
538
|
+
{
|
|
539
|
+
# Style
|
|
540
|
+
{ styles/sdoc.custom.css }
|
|
541
|
+
# StyleAppend
|
|
542
|
+
{ styles/overrides.css }
|
|
543
|
+
# Header
|
|
544
|
+
{ My *custom* header }
|
|
545
|
+
# Footer
|
|
546
|
+
{ Page-specific footer text }
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
{[.]
|
|
551
|
+
- The `@meta` scope is not rendered in the document body
|
|
552
|
+
- The meta scope should appear at the top level of the document
|
|
553
|
+
- `Style` and `StyleAppend` are treated as file paths (relative to the SDOC file)
|
|
554
|
+
- `Header` and `Footer` render their scope contents at the top/bottom of the page
|
|
555
|
+
- Per-file meta settings override the merged `sdoc.config.json` values
|
|
556
|
+
- `@meta` is reserved and should not be used for normal references
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
# Key:Value Meta Syntax @kv-meta
|
|
560
|
+
{
|
|
561
|
+
Inside a `@meta` scope, paragraph lines matching `Key: value` are parsed as metadata. This provides a lighter-weight alternative to sub-scopes for simple values:
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
# Meta @meta
|
|
565
|
+
{
|
|
566
|
+
style: styles/custom.css
|
|
567
|
+
header: My Header
|
|
568
|
+
footer: My Footer
|
|
569
|
+
author: Jane Smith
|
|
570
|
+
date: 2026-02-09
|
|
571
|
+
version: 1.0
|
|
572
|
+
status: Draft
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
{[.]
|
|
577
|
+
- Key matching is case-insensitive
|
|
578
|
+
- The pattern requires at least one space after the colon (`key: value`, not `key:value`)
|
|
579
|
+
- Well-known keys: `style`, `styleappend`/`style-append`, `header`, `footer`
|
|
580
|
+
- All other keys are stored as custom properties (e.g., `author`, `date`, `version`, `status`, `tags`)
|
|
581
|
+
- Sub-scope syntax takes precedence: if both `# Style { path }` and `style: path` exist, the sub-scope value wins
|
|
582
|
+
- Key:value and sub-scope syntax can be mixed freely in the same meta scope
|
|
583
|
+
- Each key:value pair must be on its own paragraph line (separated by blank lines from other pairs)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Interactive Preview @interactive-preview
|
|
590
|
+
{
|
|
591
|
+
The VSCode extension provides an interactive preview with the following features. These are preview-only behaviours and do not affect the SDOC format or static HTML export.
|
|
592
|
+
|
|
593
|
+
# Collapsible Scopes @collapsible-scopes
|
|
594
|
+
{
|
|
595
|
+
Scope headings that have children display a toggle triangle, visible on hover. Clicking the triangle collapses the scope's children (hides the content below the heading). Clicking again expands them.
|
|
596
|
+
|
|
597
|
+
{[.]
|
|
598
|
+
- The triangle points right when collapsed, down when expanded
|
|
599
|
+
- Collapse state is preserved across preview refreshes using webview state
|
|
600
|
+
- Scopes are identified by their `@id` if present, otherwise by source line number
|
|
601
|
+
- Only scopes with children show the toggle
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
# Click-to-Navigate @click-to-navigate
|
|
606
|
+
{
|
|
607
|
+
Clicking any rendered element in the preview (heading, paragraph, code block, blockquote, list item, table, horizontal rule) navigates the editor cursor to the corresponding source line in the SDOC file.
|
|
608
|
+
|
|
609
|
+
{[.]
|
|
610
|
+
- Each rendered element carries a `data-line` attribute with its 1-indexed source line number
|
|
611
|
+
- Clicking inside an editable paragraph or on a collapse toggle does not trigger navigation
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
# Inline Text Editing @inline-editing
|
|
616
|
+
{
|
|
617
|
+
Paragraphs in the preview are directly editable. Clicking a paragraph gives it focus with a visible outline. Typing changes the text, and pressing Enter or clicking away writes the change back to the source file.
|
|
618
|
+
|
|
619
|
+
{[.]
|
|
620
|
+
- Only paragraph elements are editable (not headings, code blocks, etc.)
|
|
621
|
+
- Inline formatting (`*bold*`, `@ref`) in the original source is lost if the user edits text that contained it
|
|
622
|
+
- Escape also blurs the paragraph (discarding focus without triggering a save of the current edit state)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# Document Formatting @document-formatting
|
|
628
|
+
{
|
|
629
|
+
The VSCode extension provides a built-in document formatter accessible via Format Document (Shift+Option+F). The formatter reindents the document based on brace depth.
|
|
630
|
+
|
|
631
|
+
# Formatting Rules @formatting-rules
|
|
632
|
+
{
|
|
633
|
+
The formatter operates line-by-line, tracking brace depth to compute indentation:
|
|
634
|
+
|
|
635
|
+
{[.]
|
|
636
|
+
- Blank lines are preserved as empty lines with no indentation
|
|
637
|
+
- Code block content (between ``` fences) is passed through raw with no reindentation
|
|
638
|
+
- Code fence lines are indented at the current depth
|
|
639
|
+
- Closing braces `}` decrement depth before indenting
|
|
640
|
+
- Standalone openers (`{`, `{[.]`, `{[#]`, `{[table]`) indent at current depth, then increment
|
|
641
|
+
- Inline blocks (`{ content }`) indent at current depth with no depth change
|
|
642
|
+
- K&R lines (heading or list item ending with an opener) indent at current depth, then increment
|
|
643
|
+
- All other lines (headings, paragraphs, list items, blockquotes, HRs) indent at current depth
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
# Behaviour @formatting-behaviour
|
|
648
|
+
{
|
|
649
|
+
{[.]
|
|
650
|
+
- The formatter respects the user's VS Code tab size and spaces/tabs preference
|
|
651
|
+
- Formatting is idempotent: formatting an already-formatted document produces identical output
|
|
652
|
+
- Structure is never changed; only indentation is adjusted
|
|
653
|
+
- The formatter does not reflow paragraph text or reorder content
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# Implicit Root Scope @implicit-root
|
|
659
|
+
{
|
|
660
|
+
If the first non-blank line of a document is a `#` heading and the next non-blank line after it is NOT `{`, an inline block, or a list/table opener, the document is in implicit root mode:
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
# My Document
|
|
664
|
+
|
|
665
|
+
# Section A
|
|
666
|
+
Content of Section A.
|
|
667
|
+
|
|
668
|
+
# Section B
|
|
669
|
+
{
|
|
670
|
+
Content of Section B.
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
{[.]
|
|
675
|
+
- The first heading becomes the root scope title
|
|
676
|
+
- Everything after it until EOF becomes the root scope's children
|
|
677
|
+
- Works with both braceless and explicit child scopes
|
|
678
|
+
- If the first heading IS followed by `{` or a block opener, the document uses explicit root mode (existing behavior)
|
|
679
|
+
- K&R brace style on the first heading also triggers explicit mode
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
# Parsing Rules @parsing-rules
|
|
684
|
+
{
|
|
685
|
+
Command tokens are recognised only at the start of a line (after optional indentation):
|
|
686
|
+
|
|
687
|
+
{[.]
|
|
688
|
+
- `#` heading line
|
|
689
|
+
- `{` scope open
|
|
690
|
+
- `}` scope close
|
|
691
|
+
- `{[.]` / `{[#]` list open
|
|
692
|
+
- `{[table]` / `{[table <flags>]` table open
|
|
693
|
+
- `>` blockquote line
|
|
694
|
+
- `---` / `***` / `___` horizontal rule
|
|
695
|
+
- `` ``` `` code fence
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
Blank lines are allowed anywhere and are ignored.
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
# Formal Grammar @grammar
|
|
702
|
+
{
|
|
703
|
+
Informal EBNF grammar:
|
|
704
|
+
|
|
705
|
+
```
|
|
706
|
+
document = implicit_root | block_body ;
|
|
707
|
+
implicit_root = heading block_body ;
|
|
708
|
+
|
|
709
|
+
scope = heading ws? block
|
|
710
|
+
| heading ws? braceless_body
|
|
711
|
+
| heading_with_opener block_body "}" ;
|
|
712
|
+
heading = "#" { "#" } ws title (ws id)? ;
|
|
713
|
+
heading_with_opener = "#" { "#" } ws title (ws id)? ws block_opener ;
|
|
714
|
+
id = "@" ident ;
|
|
715
|
+
block_opener = "{" | "{[.]" | "{[#]" | table_open ;
|
|
716
|
+
block = "{" ws? block_body "}" ;
|
|
717
|
+
braceless_body = { paragraph | code_block | blockquote | implicit_list
|
|
718
|
+
| horizontal_rule | headingless_scope | table_scope
|
|
719
|
+
| blank } ;
|
|
720
|
+
|
|
721
|
+
block_body = { blank | paragraph | scope | headingless_scope
|
|
722
|
+
| list_scope | table_scope
|
|
723
|
+
| implicit_list | blockquote | horizontal_rule
|
|
724
|
+
| code_block | comma_sep } ;
|
|
725
|
+
headingless_scope = "{" ws? block_body "}" ;
|
|
726
|
+
list_scope = list_open ws? list_body "}" ;
|
|
727
|
+
list_open = "{[.]" | "{[#]" ;
|
|
728
|
+
table_scope = table_open ws? table_body "}" ;
|
|
729
|
+
table_open = "{[table" { ws table_flag } "]" ;
|
|
730
|
+
table_flag = "borderless" | "headerless" ;
|
|
731
|
+
table_body = table_row { table_row } ;
|
|
732
|
+
table_row = cell { "|" cell } ;
|
|
733
|
+
list_body = { blank | comma_sep | scope | list_item_shorthand
|
|
734
|
+
| anonymous_item } ;
|
|
735
|
+
implicit_list = list_item_shorthand { list_item_shorthand } ;
|
|
736
|
+
list_item_shorthand = bullet_item | numbered_item ;
|
|
737
|
+
bullet_item = "-" ws title { continuation_line } [ws? block]?
|
|
738
|
+
| "-" ws title ws block_opener block_body "}" ;
|
|
739
|
+
numbered_item = number ("." | ")") ws title { continuation_line } [ws? block]?
|
|
740
|
+
| number ("." | ")") ws title ws block_opener block_body "}" ;
|
|
741
|
+
continuation_line = text_line ; (* only in explicit list blocks *)
|
|
742
|
+
anonymous_item = "{" ws? block_body "}" ;
|
|
743
|
+
number = DIGIT { DIGIT } ;
|
|
744
|
+
|
|
745
|
+
paragraph = text_line { ws? text_line } ;
|
|
746
|
+
text_line = line_not_starting_with_command ;
|
|
747
|
+
|
|
748
|
+
blockquote = quote_line { quote_line | blank } ;
|
|
749
|
+
quote_line = ">" text_line ;
|
|
750
|
+
|
|
751
|
+
horizontal_rule = "---" | "***" | "___" ;
|
|
752
|
+
|
|
753
|
+
code_block = fence_open raw_text fence_close ;
|
|
754
|
+
fence_open = "```" [lang] [ws "src:" path] [ws "lines:" range] newline ;
|
|
755
|
+
fence_close = "```" newline ;
|
|
756
|
+
|
|
757
|
+
comma_sep = "," ;
|
|
758
|
+
blank = newline ;
|
|
759
|
+
|
|
760
|
+
ident = (ALPHA | "_") { ALPHA | DIGIT | "_" | "-" } ;
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
{[.]
|
|
764
|
+
- `title` is the remainder of the heading line, excluding the optional trailing `@id`
|
|
765
|
+
- If a line starts with a command token, it is not a paragraph line
|
|
766
|
+
- The grammar is line-oriented; practical parsers should operate on lines
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
# Open Questions @open-questions
|
|
771
|
+
{
|
|
772
|
+
{[.]
|
|
773
|
+
- Comment syntax (if any)
|
|
774
|
+
- Duplicate ID resolution (error vs warning vs nearest-scope)
|
|
775
|
+
- Additional list types (checkboxes, alpha, roman)
|
|
776
|
+
- Additional inline formatting (underline, highlight)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|