nattoppet 2.1.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GUIDE.md ADDED
@@ -0,0 +1,732 @@
1
+ # Nattoppet Usage Guide
2
+
3
+ Nattoppet is a macro-based markup language that compiles to self-contained, minified HTML. You write `.ymd` files using a mix of raw HTML, plain text, and macros. The compiler inlines assets, renders math, and bundles everything into a single file.
4
+
5
+ This guide covers writing `.ymd` files from the perspective of an end user.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Quick Start](#quick-start)
12
+ 2. [The Core Idea: Macros](#the-core-idea-macros)
13
+ 3. [Paragraphs and Indentation](#paragraphs-and-indentation)
14
+ 4. [Mixins](#mixins)
15
+ 5. [Common Macros](#common-macros)
16
+ 6. [Built-in Themes](#built-in-themes)
17
+ - [vue](#vue--articles-and-blog-posts)
18
+ - [form](#form--interactive-calculators)
19
+ - [katex](#katex--math-rendering)
20
+ - [koa](#koa--long-documents-with-navigation)
21
+ - [ppt](#ppt--slide-decks)
22
+ - [tml](#tml--timeline--horizontal-articles)
23
+ 7. [Preprocessor Support](#preprocessor-support)
24
+ 8. [Scoping and Name Resolution](#scoping-and-name-resolution)
25
+ 9. [Traps and Workarounds](#traps-and-workarounds)
26
+ 10. [Troubleshooting](#troubleshooting)
27
+ 11. [Building and Deploying](#building-and-deploying)
28
+ 12. [How It Works Under the Hood](#how-it-works-under-the-hood)
29
+ 13. [Summary Cheat Sheet](#summary-cheat-sheet)
30
+
31
+ ---
32
+
33
+ ## Quick Start
34
+
35
+ ### Prerequisites
36
+
37
+ - [Bun](https://bun.sh) (≥1.0)
38
+ - Clone this repository and run `bun install` in the project root.
39
+
40
+ ### Your first document
41
+
42
+ A minimal `.ymd` file looks like this:
43
+
44
+ ```ymd
45
+ [mixin] vue
46
+
47
+ [title]: Hello World
48
+
49
+ [h2] First Section
50
+
51
+ This is a paragraph. It is automatically wrapped in `<p>` tags because it is indented by two spaces.
52
+
53
+ This line is not indented, so it passes through as raw HTML.
54
+ ```
55
+
56
+ Compile it:
57
+
58
+ ```bash
59
+ bun run nattoppet.ts input.ymd > output.html
60
+ ```
61
+
62
+ Preview with live reload:
63
+
64
+ ```bash
65
+ bun run nattoppet-dev.ts input.ymd
66
+ ```
67
+
68
+ The `--dev` flag disables minification and is useful for debugging. Both compilation modes write to stdout.
69
+
70
+ ---
71
+
72
+ ## The Core Idea: Macros
73
+
74
+ Everything dynamic in nattoppet is a **macro**. A macro is invoked by writing its name inside square brackets:
75
+
76
+ ```ymd
77
+ [my_macro]
78
+ ```
79
+
80
+ There are two kinds of macros.
81
+
82
+ ### Reference macros: `[name]:`
83
+
84
+ A reference macro is pure text substitution. When you write `[name]`, the compiler replaces it with the macro's body and interprets the result recursively.
85
+
86
+ ```ymd
87
+ [site_name]: My Blog
88
+
89
+ Welcome to [site_name]!
90
+ ```
91
+
92
+ Reference macros are defined with a colon (`:`) after the closing bracket. The body extends until the next blank line or the next macro definition, whichever comes first. The body is **trimmed** (leading and trailing whitespace are removed).
93
+
94
+ ```ymd
95
+ [site_name]: My Blog
96
+ [author]: Me
97
+
98
+ Welcome to [site_name] by [author]!
99
+ ```
100
+
101
+ **Trap**: If you forget a blank line between a macro body and the next macro definition, the second definition terminates the first one immediately:
102
+
103
+ ```ymd
104
+ [#] BAD: [author] is NOT inside [site_name]'s body
105
+ [site_name]: My Blog
106
+ [author]: Me
107
+ ```
108
+
109
+ ### Function macros: `[name]=`
110
+
111
+ A function macro is a snippet of JavaScript that runs at compile time. It is defined with an equals sign (`=`). The last evaluated expression in the block becomes the return value and is spliced into the output.
112
+
113
+ ```ymd
114
+ [upper]=
115
+ const text = this.capture_until('\n')
116
+ text.toUpperCase()
117
+
118
+ [upper]hello world
119
+ ```
120
+
121
+ Function macros are executed inside a sandboxed context. They have access to the standard library (`read`, `render_markdown`, `render_katex`, etc.) and several special runtime values.
122
+
123
+ ### Special values available inside function macros
124
+
125
+ | Name | Description |
126
+ |------|-------------|
127
+ | `this.remaining` | The unparsed text that appears **after** the macro invocation. |
128
+ | `this.interpret(str)` | Recursively compile a substring. Definitions visible at the **call site** are available (dynamic scoping). |
129
+ | `this.capture_until(delimiter)` | Consume `this.remaining` up to the first occurrence of `delimiter`, returning the text before it. The delimiter itself is also consumed from `remaining`. |
130
+ | `this.skip(n)` | Skip `n` characters from `this.remaining`. |
131
+ | `this.std_call(has_content?)` | Parse the standard option/argument DSL that many built-in macros use. See below. |
132
+
133
+ ### The `std_call` convention
134
+
135
+ Most built-in macros use `std_call()` to parse a small DSL after the macro name:
136
+
137
+ ```ymd
138
+ [macro].option1.option2(arg1)(arg2)>>block content<<
139
+ ```
140
+
141
+ `std_call()` returns an object with:
142
+
143
+ - `opts`: array of dot-prefixed options (e.g. `['option1', 'option2']`).
144
+ - `args`: array of parenthesized or braced arguments (e.g. `['arg1', 'arg2']`).
145
+ - `block`: the content between `>>` and a matching `<<` (or, if `has_content` is true, the rest of the line after a leading space).
146
+
147
+ Example usage inside a macro definition:
148
+
149
+ ```ymd
150
+ [my_macro]=
151
+ const {opts, args, block} = std_call(true)
152
+ // opts -> ['center']
153
+ // args -> ['100px']
154
+ // block -> 'hello'
155
+
156
+ [my_macro].center(100px) hello
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Paragraphs and Indentation
162
+
163
+ Nattoppet automatically wraps indented prose in `<p>` tags.
164
+
165
+ - A line that begins with **two spaces** starts a paragraph.
166
+ - Paragraphs are separated by **blank lines** (`\n\n`).
167
+ - Macros inside indented text are expanded normally.
168
+ - Non-indented text passes through unchanged (useful for raw HTML).
169
+
170
+ ```ymd
171
+ This becomes a paragraph.
172
+
173
+ This becomes a second paragraph.
174
+
175
+ <div>This is raw HTML, not a paragraph.</div>
176
+ ```
177
+
178
+ **Nesting behavior**: If every line in a block is indented, each line can open its own nested `<p>`. This is usually not what you want; keep paragraphs as contiguous indented blocks separated by blank lines.
179
+
180
+ **Trap**: Mixed indented and non-indented blocks can produce surprising nesting. When in doubt, separate prose from HTML with blank lines.
181
+
182
+ ---
183
+
184
+ ## Mixins
185
+
186
+ A mixin inlines another file into the current token stream.
187
+
188
+ ```ymd
189
+ [mixin] path/to/file.ymd
190
+ ```
191
+
192
+ **Resolution rules**:
193
+
194
+ - If the path has an extension, it is used as-is.
195
+ - If the path has no extension and a file with that exact name exists, it is used.
196
+ - Otherwise, `.ymd` is appended (e.g. `[mixin] common` resolves to `common.ymd`).
197
+ - **All mixin paths are resolved relative to the nattoppet installation directory** (where `compiler.ts` lives). Paths starting with `./` are **not** treated specially; they also resolve relative to the compiler root.
198
+
199
+ ### Non-`.ymd` mixins
200
+
201
+ If you mixin a `.css`, `.js`, or other text file, it is read as raw text and emitted verbatim.
202
+
203
+ **Trap**: `[mixin]` reads files with UTF-8 encoding. Do **not** use it for binary files (like `.wasm` or images); they will be corrupted. Use `[require]` or `[img]` instead, which read files as buffers.
204
+
205
+ ### `[#slot]` — Content Injection
206
+
207
+ A mixin can contain a `[#slot]` marker. When the mixin is expanded, the caller's tokens that appear **after** the `[mixin]` directive replace the slot.
208
+
209
+ ```ymd
210
+ [#] -- layout.ymd --
211
+ <!doctype html>
212
+ <title>[title]</title>
213
+ [#slot]
214
+
215
+ [#] -- page.ymd --
216
+ [mixin] layout.ymd
217
+
218
+ [title]: My Page
219
+ <h1>Hello</h1>
220
+ ```
221
+
222
+ **Trap**: A mixin may contain **at most one** `[#slot]`. Multiple slots raise a compilation error.
223
+
224
+ ### `common.ymd`
225
+
226
+ `common.ymd` ships with nattoppet in the project root. Since mixin paths resolve relative to the compiler root, `[mixin] common` finds it automatically. All built-in themes include it, which is why macros like `[require]`, `[img]`, and `[code]` are available in themed documents.
227
+
228
+ **Trap**: If you write a bare `.ymd` without a theme and want `[require]`, you must add `[mixin] common.ymd` yourself. `[require]` is not a compiler built-in; it is a function macro defined in `common.ymd`.
229
+
230
+ ---
231
+
232
+ ## Common Macros
233
+
234
+ These macros are available whenever you include `common.ymd` (all built-in themes do this automatically).
235
+
236
+ ### `[require]` — Inline assets intelligently
237
+
238
+ ```ymd
239
+ [require](style.less)
240
+ [require](app.coffee)
241
+ [require](app.js)
242
+ [require](data.json)
243
+ [require](module.wasm)
244
+ [require].my-id(lib/katex.css)
245
+ ```
246
+
247
+ `[require]` detects the file extension and wraps the content appropriately:
248
+
249
+ | Extension | Output |
250
+ |-----------|--------|
251
+ | `.less` | Compiles to CSS inside `<style>` |
252
+ | `.css` | Inlines inside `<style>` |
253
+ | `.coffee` | Compiles to JS inside `<script>` |
254
+ | `.js` | Inlines inside `<script>` |
255
+ | `.md` | Renders as Markdown HTML |
256
+ | `.wasm` | Inlines as compressed base64 with auto-decompress and `WebAssembly.instantiateStreaming` |
257
+ | `.json` | Inlines as compressed base64 with auto-decompress and `JSON.parse` |
258
+ | other | Raw text |
259
+
260
+ Options:
261
+ - `.name` sets `id="name"` on the generated `<style>` or `<script>` tag. The entire option string becomes the id.
262
+
263
+ **WASM/JSON specifics**:
264
+ - The file is compressed with raw deflate and encoded as base64. A small inline script decompresses it via the browser's `DecompressionStream("deflate-raw")`, then instantiates or parses it.
265
+ - The resulting exports/object is exposed on `window.<filename_without_extension>`.
266
+ - `window.wasm_ready` and `window.json_ready` promises coordinate multiple files so you can await them before accessing the data.
267
+
268
+ ### `[img]` — Inline images
269
+
270
+ ```ymd
271
+ [img].center(photo.png)(Alt text)
272
+ ```
273
+
274
+ - Arguments are separate parenthesized groups. Do **not** use commas.
275
+ - First argument is the file path.
276
+ - Second argument is alt text.
277
+ - Options become CSS classes.
278
+ - The image is inlined as a base64 data URI.
279
+ - `.center` wraps the image in a centered `<div>`.
280
+
281
+ ### Headers
282
+
283
+ ```ymd
284
+ [h2] Section Title
285
+ [h3] Subsection Title
286
+ [h4] Sub-subsection Title
287
+ ```
288
+
289
+ These capture the rest of the line, interpret it, and wrap it in the corresponding HTML tag.
290
+
291
+ ### `[quote]` — Blockquote
292
+
293
+ ```ymd
294
+ [quote]>>
295
+ A famous saying.
296
+ <<
297
+ ```
298
+
299
+ Options become CSS classes on the `<blockquote>`.
300
+
301
+ ### `[#]` — Comments
302
+
303
+ ```ymd
304
+ [#] This line is ignored.
305
+ ```
306
+
307
+ `[#]` consumes the rest of the line (or a `>>...<<` block) and produces nothing. It is a regular function macro, not special compiler syntax. It works by evaluating `std_call(true)` for its side effect (consuming input) and returning `''` via the comma operator.
308
+
309
+ ### `[-]` — List items
310
+
311
+ ```ymd
312
+ [-]>>
313
+ First item
314
+ <<
315
+ ```
316
+
317
+ Wraps content in `<li>`. Options become classes.
318
+
319
+ ### `[file]` — Download links
320
+
321
+ ```ymd
322
+ [file](report.pdf)(Download the report)
323
+ ```
324
+
325
+ Creates a download link with the file inlined as a base64 data URI. Arguments are separate parenthesized groups.
326
+
327
+ ### `[code]` — Code blocks
328
+
329
+ ```ymd
330
+ [code].js(console.log('hello'))
331
+
332
+ [code].js>>
333
+ function greet() {
334
+ return 'hi'
335
+ }
336
+ <<
337
+ ```
338
+
339
+ - The **language is a dot-option** (e.g. `.js`), not a parenthesized argument.
340
+ - With a single-line argument, wraps in `<code class="language-<option>">`.
341
+ - Without an argument but with a block, wraps in `<pre><code>`.
342
+
343
+ **Trap**: Writing `[code](js) ...` puts `js` into the code content and loses the actual code. Always use the dot-option form.
344
+
345
+ ### `[link]` — Links
346
+
347
+ ```ymd
348
+ [link](https://example.com)(Click here)
349
+ [link](https://example.com)
350
+ The link text is captured from the next line if omitted.
351
+ ```
352
+
353
+ Arguments are separate parenthesized groups. Do not use commas.
354
+
355
+ ### `[sup]` — Superscript
356
+
357
+ ```ymd
358
+ [sup](2) -- produces <sup>2</sup>
359
+ ```
360
+
361
+ ### `[eval]` / `[eval*]` — Run JavaScript at compile time
362
+
363
+ ```ymd
364
+ [eval*]>> this.counter = 0 <<
365
+
366
+ [eval]>> this.counter++ <<
367
+ ```
368
+
369
+ - `[eval]` runs the block and returns the result (inserted into output).
370
+ - `[eval*]` runs the block and discards the result (useful for side effects like initializing state).
371
+ - These macros call `std_call(true)`, so they require either a `>>...<<` block or a space followed by the rest of the line.
372
+
373
+ **Note**: Both execute in the same sandboxed context as other macros. Variables you set (e.g. `this.index = 0`) persist across macro calls but are scoped to a single compile.
374
+
375
+ ### `[cn]` — CJK text wrapping helper
376
+
377
+ ```ymd
378
+ [cn]>>
379
+
380
+
381
+
382
+
383
+ <<
384
+ ```
385
+
386
+ The `[cn]` macro removes **single newlines** between CJK characters and between CJK and adjacent non-whitespace ASCII characters. This lets you write CJK prose with hard line breaks in the source without introducing unwanted spaces in the HTML output. The block content is recursively interpreted first, so macros inside `[cn]` are expanded normally.
387
+
388
+ **Trap**: Double newlines are preserved as paragraph breaks. Newlines between CJK and whitespace-leading ASCII are also preserved.
389
+
390
+ ---
391
+
392
+ ## Built-in Themes
393
+
394
+ Themes are mixins that provide a complete page scaffold (doctype, meta tags, styles, scripts). You typically put `[mixin] theme_name` at the top of your file.
395
+
396
+ ### `vue` — Articles and blog posts
397
+
398
+ ```ymd
399
+ [mixin] vue
400
+
401
+ [title]: My Article
402
+
403
+ [h2] Introduction
404
+ Lorem ipsum...
405
+
406
+ [h3] Subsection
407
+ More text...
408
+ ```
409
+
410
+ Features:
411
+ - Generates `<!doctype html>`, viewport meta, and a title suffix (`| ylxdzsw's blog`).
412
+ - `[h2]` and `[h3]` are automatically wrapped in anchor tags with ids (`sec0`, `sec1`, ...), enabling deep-linking.
413
+ - Includes `vue.css` and `common.ymd`.
414
+
415
+ ### `form` — Interactive calculators
416
+
417
+ ```ymd
418
+ [mixin] form
419
+
420
+ [title]: Prime Spiral
421
+
422
+ [number].n N = 100
423
+ [checkbox].show_diag Show diagonals
424
+
425
+ [require](main.js)
426
+ ```
427
+
428
+ Features:
429
+ - Generates labeled input fields: `[text]`, `[number]`, `[checkbox]`.
430
+ - Each field takes an optional `.name` option. The inline content block becomes the label text.
431
+ - A **Run** button is automatically appended. When clicked, it gathers inputs into an object and calls `window.run(args)`.
432
+ - The result is displayed in a `<pre>` block.
433
+ - Input values are synced to the URL hash, making state shareable.
434
+ - Includes `form.css`, `form.js`, and `common.ymd`.
435
+
436
+ **Note**: `N = 100` in the example above is label text, not a default input value. The form theme does not set `value` attributes automatically.
437
+
438
+ ### `katex` — Math rendering
439
+
440
+ ```ymd
441
+ [mixin] katex
442
+
443
+ [$]E = mc^2$
444
+
445
+ [$$]>>
446
+ \int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
447
+ <<
448
+ ```
449
+
450
+ Features:
451
+ - `[$] ... $` — inline math. Terminates at the first `$`.
452
+ - `[$$]>> ... <<` — display math. Must use the block delimiter for multi-line equations.
453
+ - Includes vendored `katex.css` with all fonts inlined as base64. The output is fully offline-capable.
454
+ - Unicode math symbols (e.g. `α`, `β`, `∫`) and CJK inside `\text{}` work directly in the source.
455
+
456
+ **Trap**: `[$$]` without the `>>...<<` block form only captures until the end of the line. For any multi-line equation, always use the block form.
457
+
458
+ **Trap**: `[$]` terminates at the first bare `$`. You cannot use literal `$` characters (e.g. dollar amounts) inside inline math because there is no escaping mechanism.
459
+
460
+ ### `koa` — Long documents with navigation
461
+
462
+ ```ymd
463
+ [mixin] koa
464
+
465
+ [title]: Documentation
466
+
467
+ [section] Getting Started
468
+ Content here...
469
+
470
+ [section] Advanced Topics
471
+ More content...
472
+ ```
473
+
474
+ Features:
475
+ - `[section]` creates a new section and auto-populates a hamburger-nav sidebar with a table of contents.
476
+ - Each section gets an id (`sec0`, `sec1`, ...).
477
+ - Includes `koa.css` and `common.ymd`.
478
+
479
+ **Note**: `[list]` is an internal macro that generates the navigation menu HTML. You do not invoke it directly.
480
+
481
+ ### `ppt` — Slide decks
482
+
483
+ ```ymd
484
+ [mixin] ppt
485
+
486
+ [title]: My Talk
487
+
488
+ [section] Slide One
489
+ - Bullet A
490
+ - Bullet B
491
+
492
+ [section].dark Slide Two
493
+ Dark-themed slide.
494
+ ```
495
+
496
+ Features:
497
+ - `[section]` creates a slide.
498
+ - Options after `[section]` become CSS classes on the slide `<section>`. Only the **first** leading dot is replaced with a space, so `[section].dark.centered` produces `class="scen dark.centered"` rather than two separate classes. Use a single class name or write the classes as one option.
499
+ - Elements inside slides can have animation classes: `.click`, `.delay`, `.sim`, plus timing classes `.short`, `.long`, `.fast`, `.slow`.
500
+ - `[foot]` creates a `<footer>` element.
501
+ - `[span]` creates a vertical spacer `<div>` (takes a length argument).
502
+ - Includes `ppt.css`, `ppt.js`, and `common.ymd`.
503
+
504
+ ### `tml` — Timeline / horizontal articles
505
+
506
+ ```ymd
507
+ [mixin] tml
508
+
509
+ [title]: Timeline
510
+
511
+ [article] 2024-01
512
+ Event description...
513
+
514
+ [article] 2024-06
515
+ Another event...
516
+ ```
517
+
518
+ Features:
519
+ - `[article]` wraps each entry in an `<article>` tag.
520
+ - The title becomes an `<h2>` with an auto-generated anchor id.
521
+ - Includes `tml.css`, `tml.js` (enables horizontal wheel scrolling), and `common.ymd`.
522
+
523
+ ---
524
+
525
+ ## Preprocessor Support
526
+
527
+ `[require]` in `common.ymd` automatically handles several preprocessors:
528
+
529
+ - **Markdown** (`.md`) — parsed with `marked` and emitted as HTML.
530
+ - **CoffeeScript** (`.coffee`) — compiled to JavaScript with `bare: true`.
531
+ - **Less** (`.less`) — compiled to CSS synchronously.
532
+
533
+ No additional configuration is needed. Just require the file and the compiler handles the rest.
534
+
535
+ ---
536
+
537
+ ## Scoping and Name Resolution
538
+
539
+ Understanding how macros find each other is essential for non-trivial documents.
540
+
541
+ ### Forward references
542
+
543
+ Reference macros (`[name]:`) can be used **before** they are defined:
544
+
545
+ ```ymd
546
+ Before [title] after
547
+
548
+ [title]: My Title
549
+ ```
550
+
551
+ ### Self-reference is forbidden
552
+
553
+ A reference macro cannot call itself. Doing so produces `definition <name> not found` because the compiler excludes the macro from its own scope to prevent infinite recursion.
554
+
555
+ ### Dynamic scoping for function macros
556
+
557
+ When a function macro calls `this.interpret(...)`, the substring is evaluated with access to **all macros defined at the call site**, even if those macros were not visible where the function macro itself was defined.
558
+
559
+ ```ymd
560
+ Value: [caller]
561
+
562
+ [caller]=
563
+ this.interpret('[target]')
564
+
565
+ [target]: DYNAMIC
566
+ ```
567
+
568
+ This dynamic scoping applies **only** to strings passed to `this.interpret()`. Function macro bodies are JavaScript, not nattoppet markup, so bare `[name]` references inside the JS code itself are not possible.
569
+
570
+ ### Shadowing
571
+
572
+ If the same name is defined multiple times, the **earliest** definition wins.
573
+
574
+ ```ymd
575
+ [wrapper] after
576
+
577
+ [wrapper]: [shadow]
578
+
579
+ [shadow]: FIRST
580
+
581
+ [shadow]: SECOND
582
+ ```
583
+
584
+ This outputs `FIRST after`; `SECOND` is never used. This is a consequence of forward-reference support: the compiler resolves each macro name when it first encounters it, so the first definition takes precedence. Defining the same name twice is usually a mistake; the second definition is silently ignored.
585
+
586
+ ### Preserving `remaining` across nested calls
587
+
588
+ `this.interpret` saves and restores `this.remaining`. This makes it safe to call `interpret` inside a function macro without losing your place in the input stream.
589
+
590
+ ---
591
+
592
+ ## Traps and Workarounds
593
+
594
+ Common pitfalls are listed below. For a quick-reference fix table, see [Troubleshooting](#troubleshooting).
595
+
596
+ ### Square brackets in raw content
597
+
598
+ Because `[name]` is the macro invocation syntax, raw square brackets in JavaScript, Markdown, or HTML can be mistakenly interpreted as macro calls if the content inside happens to match a defined macro name.
599
+
600
+ **Workaround**: In practice this is rare because:
601
+ - Inside KaTeX math, the raw string is passed directly to the renderer without macro expansion.
602
+ - Inside `<script>` or `<style>` tags emitted by `[require]`, the content is raw.
603
+ - If you need literal square brackets in prose, define a passthrough macro or write them inside raw HTML tags.
604
+
605
+ ### Array subscripts in inline math
606
+
607
+ Writing `a[i]` inside a paragraph can trigger macro lookup for `i`. If `i` is not defined, compilation fails with `definition i not found`.
608
+
609
+ **Workaround**: Use KaTeX math (`[$]a[i][$]`) for mathematical notation containing brackets, or escape the context by wrapping the expression in raw HTML.
610
+
611
+ ### Paragraph nesting with all-indented lines
612
+
613
+ If every line in a region is indented by two spaces, each line opens a new nested `<p>`. Keep paragraphs as contiguous blocks, using blank lines to separate them.
614
+
615
+ ### Mixin path ambiguity
616
+
617
+ If you have a file named `foo` (no extension) and also `foo.ymd`, `[mixin] foo` picks the bare file. To force the `.ymd` version, write `[mixin] foo.ymd` explicitly.
618
+
619
+ ### `capture_until` does not include the delimiter
620
+
621
+ The delimiter passed to `capture_until` is consumed from `remaining` but is **not** included in the returned string. If you need to preserve it, append it manually.
622
+
623
+ ### `[$$]` without space or block
624
+
625
+ `[$$]` uses `std_call(true)`. If there is **no space** after `[$$]` and no `>>...<<` block (e.g. `[$$]expr`), `std_call` falls through to the default case with no block, and `render_katex(undefined, true)` is called, which will likely throw an error. Always provide either a space and inline content or the `>>...<<` block form.
626
+
627
+ ---
628
+
629
+ ## Troubleshooting
630
+
631
+ | Symptom | Likely Cause | Fix |
632
+ |---------|--------------|-----|
633
+ | `definition X not found` | You invoked `[X]` but never defined `[X]:` or `[X]=`. | Define the macro, or check for typos. |
634
+ | `definition X not found` inside a self-referential ref | Reference macros cannot reference themselves. | Use a function macro if you need recursion, or restructure. |
635
+ | Multiple `[#slot]` error | A mixin contains more than one slot. | Remove extra slots, or split into multiple mixins. |
636
+ | `mixin not found: X` | The mixin path could not be resolved. | Check the path; remember all mixin paths resolve relative to the compiler root, not the input file. |
637
+ | Math does not render, or renders inline when it should be display | Missing `>>...<<` block delimiters around multi-line `[$$]`. | Use `[$$]>>...<<`. |
638
+ | CJK text has unwanted spaces | Hard line breaks between CJK characters are preserved as spaces by HTML. | Wrap the text in `[cn]>>...<<`. |
639
+ | Paragraphs wrap raw HTML | Indented HTML is treated as prose. | Remove the leading two spaces from HTML lines. |
640
+ | KaTeX fonts not loading | You are not using `[mixin] katex` and instead only included the JS. | Include the `katex` mixin (or manually inline `katex.css`) because the vendored CSS contains base64 fonts. |
641
+ | `less failed to compile synchronously` | The Less compiler returned an error. | Check your `.less` file for syntax errors. |
642
+ | `[code](js)` loses code content | Language must be a dot-option, not a parenthesized argument. | Use `[code].js(...)`. |
643
+ | `[img]` or `[link]` broken | Arguments must be separate parenthesized groups. | Use `[img].center(path)(alt)`, not `[img].center(path, alt)`. |
644
+
645
+ ---
646
+
647
+ ## Building and Deploying
648
+
649
+ ### Native executable bundling
650
+
651
+ You can turn a `.ymd` file into a desktop application:
652
+
653
+ ```bash
654
+ bun run nattoppet-native.ts init # create native/ Rust scaffold
655
+ bun run nattoppet-native.ts bundle file.ymd # compile HTML into native/
656
+ bun run nattoppet-native.ts build file.ymd # compile HTML and run cargo build --release
657
+ ```
658
+
659
+ The result is a single native binary (Linux/macOS/Windows) that embeds the HTML and renders it with a web-view.
660
+
661
+ ### Project scaffolding
662
+
663
+ ```bash
664
+ bun run nattoppet-init.ts form
665
+ ```
666
+
667
+ This generates a starter `main.ymd`, `main.js`, and optionally Rust/WASM boilerplate plus a GitHub Pages deployment workflow.
668
+
669
+ ---
670
+
671
+ ## How It Works Under the Hood
672
+
673
+ The nattoppet compiler processes a `.ymd` file in two passes:
674
+
675
+ 1. **Tokenization**: The source is split into a sequence of tokens: raw text chunks (`code`), macro definitions (`ref`/`fn`), mixin directives (`mixin`), and slot markers (`slot`). During this phase, `[mixin] path` files are read and inlined recursively.
676
+ 2. **Interpretation**: The token stream is walked left-to-right. Raw text is interpreted (paragraphs and nested macros are expanded); raw asset tokens are emitted as-is.
677
+
678
+ Function macros are executed via `vm.runInContext` in a sandboxed environment. The completion value of the JavaScript block (the value of the last expression statement) is converted to a string and spliced into the output.
679
+
680
+ You do not need to understand these internals to use nattoppet, but knowing the two-phase model helps explain why mixins are fully resolved before any macros run.
681
+
682
+ ---
683
+
684
+ ## Summary Cheat Sheet
685
+
686
+ ```ymd
687
+ [#] Structure
688
+ [mixin] theme_name [#] include a theme
689
+ [mixin] common [#] include common macros
690
+ [#slot] [#] injection point inside a mixin
691
+
692
+ [#] Macro definitions
693
+ [ref]: text [#] reference macro
694
+ [fn]= js_code [#] function macro
695
+
696
+ [#] Common macros
697
+ [require](file.ext) [#] inline asset (auto-detect type)
698
+ [require].id(path) [#] with element id
699
+ [img].class(path)(alt) [#] inline image
700
+ [h2] Title [#] header
701
+ [quote]>>...<< [#] blockquote
702
+ [code].lang(code) [#] inline code
703
+ [code].lang>>...<< [#] code block
704
+ [link](url)(text) [#] hyperlink
705
+ [file](path)(text) [#] download link
706
+ [sup](text) [#] superscript
707
+ [#] comment [#] comment / ignore
708
+ [-]>>...<< [#] list item
709
+ [eval]>> expr << [#] compile-time JS (returns value)
710
+ [eval*]>> expr << [#] compile-time JS (side effect only)
711
+ [cn]>>...<< [#] CJK newline removal
712
+
713
+ [#] Math
714
+ [$] ... $ [#] inline math
715
+ [$$]>>...<< [#] display math
716
+
717
+ [#] Form theme
718
+ [text].name Label [#] text input
719
+ [number].name Label [#] number input
720
+ [checkbox].name Label [#] checkbox
721
+
722
+ [#] Koa theme
723
+ [section] Title [#] new section + TOC entry
724
+
725
+ [#] PPT theme
726
+ [section].class Title [#] new slide
727
+ [foot] text [#] footer
728
+ [span](length) [#] vertical spacer
729
+
730
+ [#] TML theme
731
+ [article] Title [#] new timeline article
732
+ ```