peisar 1.0.0

A practical Markdown parser written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# Peisar Markdown Language Specification

Version 1.0.0 — describes exactly what `peisar` parses and renders with
**default options** (`gfm: true`, `kramdown: true`). Every example in this
document was verified against the parser itself.

Peisar implements a practical CommonMark-style core plus GitHub Flavored
Markdown (GFM) extensions and Kramdown block attributes. Where peisar differs
from strict CommonMark, the difference is called out explicitly.

```js
const { Peisar } = require("peisar");
const doc = new Peisar("# Hello **world**", { fragment: true });
doc.html;      // <h1>Hello <strong>world</strong></h1>\n
doc.ast;       // JavaScript AST object
doc.frontmatter; // YAML object or null
```

---

## 1. Document structure

A document is a sequence of **block** nodes. Blocks contain **inline** content
(or nested blocks). Blank lines separate blocks; they carry no meaning
otherwise. Both `\n` and `\r\n` line endings are accepted.

### 1.1 YAML front matter

An optional YAML block at the very top of the document:

```markdown
---
title: Guide
tags:
  - docs
---

# The body
```

Rules:

- The opening `---` must be the first non-whitespace line and must be followed
  immediately by a newline (`---\n` or `---\r\n`). A trailing space after
  `---` disables front matter.
- The block closes at the next line that starts with `---`.
- Everything after the closing marker is the Markdown body (leading blank
  lines are stripped).
- YAML is deserialized with full serde_yaml semantics: nested maps, lists,
  scalars, quotes.
- An **unterminated** block is not front matter; the text is parsed as normal
  Markdown.
- **Invalid YAML does not throw** — the front matter is discarded and the whole
  input is parsed as Markdown body.
- `document.frontmatter` returns the deserialized YAML object, or `null`.

---

## 2. Blocks

### 2.1 ATX headings

```markdown
# H1
## H2
###### H6
```

- 1–6 `#` characters, optionally indented, followed by a space or end of line.
- Seven or more `#`, or a missing space (`#Hello`), is a paragraph.
- Inline markup inside headings is parsed (`# a *b*` → `<h1>a <em>b</em></h1>`).
- **Difference from CommonMark:** trailing closing hashes are *not* stripped
  (`## Title ##` keeps the ` ##` as text).
- **Setext headings are not supported.** `Title\n===` is a paragraph;
  `Title\n---` is a paragraph followed by a thematic break.

### 2.2 Paragraphs

Consecutive non-blank lines form one paragraph. A paragraph ends at a blank
line or the start of another block construct (heading, fence, thematic break,
list marker, blockquote, HTML block, table, link reference definition, or a
Kramdown attribute line). Lines within a paragraph are joined by soft breaks.

### 2.3 Thematic breaks

```markdown
---
***
___
- - -
```

Three or more of the *same* character (`-`, `*`, or `_`), optionally spaced and
indented, on a line by itself. Renders `<hr>`.

### 2.4 Fenced code blocks

```markdown
```rust
fn main() {}
```
```

- Fence: 3 or more `` ` `` or `~` characters. The closing fence must use the
  same character and be at least as long as the opener (it may be indented).
- The info string after the opening fence is kept **whole** as the language
  hint: `` ```rust extra `` sets `class="language-rust extra"`.
- Content is captured verbatim (no dedent, no escape processing) until the
  closing fence or end of document. `&`, `<`, `>` are HTML-escaped on render.
- With a language: `<pre><code class="language-rust">…</code></pre>`.
- Without: `<pre><code>…</code></pre>`.
- A fence inside a *longer* fence is content (open with `````` to include ``` ).

### 2.5 Indented code blocks

Four spaces or one tab of indentation:

```markdown
    indented code
    second line
```

Consume consecutive lines indented by 4 spaces / 1 tab. A single blank line
inside is kept if the next line is still indented; otherwise the block ends.
No language hint. Renders like a fenced block without a language.

### 2.6 Block quotes

```markdown
> Quoted text.
> More.

> Lazy continuation
still part of the quote
```

- A line starting with `>` (optionally indented) opens a quote; one optional
  space after `>` is stripped from each line.
- Lines without `>` continue the quote (lazy continuation) until a blank line.
- Quotes nest (`> > …`) and their content is re-parsed as a full sub-document
  (headings, lists, tables all work inside).
- Renders `<blockquote>…</blockquote>`.

### 2.7 Lists

```markdown
- unordered
* also unordered
+ also unordered

1. ordered
2) ordered with paren
```

- Markers: `-`, `*`, `+` followed by a space; or 1–9 digits followed by `.`
  or `)` and a space (a bare `.`/`)` at end of line also counts).
- A tab after the marker is **not** supported (`-\tb` is a paragraph).
- Continuation lines indented to the item's content column join the item;
  non-indented "lazy" lines also join unless they start a new block construct.
- A blank line between items makes a **loose list**: item content is wrapped in
  `<p>`. Without blank lines the list is **tight** and a single-paragraph item
  renders inline (no `<p>`), matching CommonMark convention.
- Ordered lists always render `<ol>` starting at 1; the first number is not
  preserved (no `start` attribute).
- Items may contain any nested blocks (paragraphs, code, nested lists, quotes).

### 2.8 Task lists (GFM)

```markdown
- [ ] unchecked
- [x] checked
- [X] also checked
1. [x] ordered task
```

A `[ ]`, `[x]`, or `[X]` marker directly after a list marker turns the item
into a task. The marker is stripped from the text. Renders:

```html
<li class="task-list-item" data-checked="true">
  <input type="checkbox" class="task-list-item-checkbox" checked disabled> done
</li>
```

(`data-checked="true"` and `checked` appear only for checked items.)

### 2.9 HTML blocks

A line starting with `<` followed by a letter, `/`, or `!` begins a raw HTML
block. All lines until the next blank line are captured **verbatim** and
emitted unescaped. `<!DOCTYPE html>` is passed through the same way.

### 2.10 HTML comments

A line starting with `<!--` opens a comment block, captured until the line
containing `-->` (or the next blank line if never closed). The AST stores the
comment text without the `<!--`/`-->` markers; rendering re-emits
`<!-- value -->`.

### 2.11 Tables (GFM)

```markdown
| Name | Value |
| :--- | ----: |
| a    | 1     |
```

- A table starts where a line containing `|` is followed by a delimiter row
  whose cells contain only `-` and `:`, with at least one `-` per cell.
- Leading/trailing pipes are optional; cells are split on `|` and trimmed.
- Alignment per column: `:--` left, `--:` right, `:--:` center, `--` default.
- Header/delimiter/row cell counts may differ; extra cells are kept, missing
  ones absent.
- Body rows continue until a blank line or a line without `|`.
- Inline markup is parsed inside cells.
- **Difference from GFM:** escaped pipes (`\|`) are *not* supported — `\|` ends
  the cell and keeps the backslash.
- Renders `<table><thead>…<tbody>…`; alignment emits
  `style="text-align: center"` / `"right"` (left/default emit nothing).

### 2.12 Link reference definitions

```markdown
[google]: https://google.com "Google"
[wiki]: <https://w.org> (Wiki)
```

- Form: `[label]: url` with optional title in `"…"`, `'…'`, or `(…)`.
- The URL may be wrapped in `<…>` (kept verbatim, not unwrapped) or bare
  (up to first whitespace).
- Definitions are collected in a document-wide pre-pass, so they may appear
  **after** their uses and inside any container.
- They render nothing; they appear in the AST both as block nodes and in the
  root's `linkReferences` array.
- Labels are normalized: trimmed, internal whitespace collapsed, lowercased.

### 2.13 Kramdown block attributes

```markdown
# Heading
{: #title .big data-role="note"}

Paragraph with attributes.
{: .lead}

```code```
{: .cb}
```

- An attribute line is `{…}` on its own line, placed **after** the block it
  annotates. Blank lines between the block and the `{…}` line are allowed
  (and consumed).
- Syntax inside the braces (a `:` after `{` is optional, i.e. `{:` and `{`
  both work):
  - `#id` → HTML `id`
  - `.class` → `class` (repeatable, joined with spaces)
  - `key="value"` / `key='value'` → arbitrary attributes
  - `bare` → boolean attribute rendered with an empty value (`bare=""`)
  - `{`/`}` nesting and `}` inside quoted values are handled.
- Supported on: headings, paragraphs, code blocks, block quotes, lists, HTML
  blocks, tables, and parser-hook blocks. **Not** thematic breaks (an attr
  line after `---` becomes a paragraph carrying the attributes).
- Inside a block quote, an attribute line **ends the quote** and applies to
  it.
- Rendering order: `id`, then `class`, then key/value pairs; `& " < >` in
  values are HTML-escaped.

---

## 3. Inlines

### 3.1 Emphasis

```markdown
*italic*   **bold**
_italic_   __bold__
```

- `*` and `_` are interchangeable. A run of 2+ is tried as **bold** first;
  if no closer is found, a single-marker **italic** is tried.
- Closing: any later run of the same character with length ≥ the opener.
- **Intraword emphasis works:** `a*b*c` → `a<em>b</em>c`, and `_snake_case_`
  becomes `<em>snake</em>case_` (unlike CommonMark).
- Nesting: `**a *b* c**` → bold containing italic.
- `***both***` parses as bold with a stray `*` on each side.
- An unclosed run may still match shorter: `**unclosed` yields
  `<em></em>unclosed`.
- Renders `<em>` / `<strong>`.

### 3.2 Inline code

```markdown
`code`        ``code with ` inside``
```

- An opening backtick run matches the next run of the *same length*; the
  content between is kept verbatim (spaces preserved, nothing escaped in the
  AST; `& < > "` `'` are escaped on render).
- No language, no backslash processing inside.

### 3.3 Strikethrough (GFM)

```markdown
~~deleted~~
```

Renders `<del>deleted</del>`. Content is parsed as inline markup.

### 3.4 Links

```markdown
[text](https://example.com)
[text](https://example.com "Title")
[text](url 'single-quoted title')
[nested [brackets]](url)
```

- Square brackets nest by depth; parentheses nest by depth.
- An optional title follows the URL separated by whitespace, in `"` or `'`.
  Parens after the URL are treated as part of the URL, not a title.
- The link text is parsed as inline markup (`[**b**](u)` keeps the bold).
- `<…>`-wrapped destinations are kept **verbatim** (angle brackets are not
  stripped and end up in the `href`).
- Renders `<a href="…" title="…">…</a>` with `& " < >` escaped in `href`.

### 3.5 Images

```markdown
![alt text](image.png "Title")
```

Alt text is a raw string (not parsed as markup). Renders
`<img src="…" alt="…" title="…">`.

### 3.6 Reference links

```markdown
[text][label]     full
[label][]         collapsed
[label]           shortcut

[label]: https://example.com "Title"
```

- All three forms resolve against the document-wide reference map
  (definitions may appear anywhere, including after the use).
- Labels match case-insensitively with collapsed whitespace.
- An unresolved reference stays literal text: `[missing][nope]`.
- The AST node is `LinkReference` with the resolved `url`/`title`; rendering is
  identical to an inline link.

### 3.7 Autolinks (GFM)

```markdown
https://example.com
http://example.com/path
www.example.com
```

- Bare `http://`, `https://`, and `www.` URLs become links — **lowercase
  schemes only** (`WWW.` does not link).
- `www.` links get `https://` prepended in the `href`; the visible text shows
  the full `https://www.…` URL.
- Matching requires only that the position starts with `h`/`w` — there is no
  word-boundary check (`xhttps://a.com` links).
- The URL ends at whitespace, `<`, `>`, or trailing punctuation
  (`. , ; : ! ? )`) at the end of the run.
- Angle-bracket autolinks (`<https://…>`) are **not** supported and stay
  literal.

### 3.8 Line breaks

| Source | AST | HTML |
|---|---|---|
| `a  \nb` (2+ trailing spaces) | `HardBreak` | `a<br>\nb` |
| `a\\\nb` (backslash) | `HardBreak` | `a<br>\nb` |
| `a\nb` (plain newline) | `SoftBreak` | `a\nb` |

Trailing spaces / a trailing backslash with no following newline are kept as
literal text.

### 3.9 Inline HTML

`<tag …>`, `</tag>`, and `<!-- … -->` are captured as raw `HtmlInline` nodes
and emitted verbatim. Inside link text, inline HTML is preserved.

### 3.10 Text and escaping — important differences

- **No backslash escapes.** `\*` does not prevent emphasis; the backslash is
  kept as literal text and the `*` still parses. `\`` and `\n`-style escapes
  do not exist. The only backslash behavior is the hard break (3.8).
- **No character-entity decoding.** `&amp;` stays the six characters `&amp;`
  in the AST (rendered as `&amp;amp;`).
- Plain text is HTML-escaped on render: `&` `&amp;`, `<` `&lt;`, `>`
  `&gt;`, `"` `&quot;`, `'` `&#39;`.

---

## 4. Options

All options are optional; defaults shown:

| Option | Default | Effect |
|---|---|---|
| `gfm` | `true` | Tables, strikethrough, task lists, autolinks |
| `kramdown` | `true` | `{…}` attribute lines |
| `fragment` | `false` | `true`: body only; `false`: full HTML document |
| `fileName` | `null` | Attached to the AST root |
| `charset` | `true` | `<meta charset>` in full-document mode |
| `viewport` | `true` | `<meta name="viewport">` in full-document mode |
| `title` | `null` | `<title>` |
| `bodyClass` | `null` | `<body class="…">` |
| `style` | `null` | Inline CSS in a `<style>` tag |

With `gfm: false`: table syntax becomes paragraph text, `~~x~~` stays literal,
`[ ]` markers stay text, bare URLs are not linked. With `kramdown: false`,
`{…}` lines are ordinary paragraph text.

Full-document output shape:

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>…</title>
<style>…</style>
</head>
<body class="…">
…blocks…
</body>
</html>
```

---

## 5. AST shape

- Root: `{ nodeType: "root", fileName?, pos, children: Block[], linkReferences[] }`.
- Every node carries `pos: { start, end }` — a half-open span with zero-based
  `line`, `column` (characters), and `offset` (bytes).
- **Spans of nodes inside containers** (block quotes, list items) are relative
  to that container's sub-document, not the original file.
- In the JavaScript `ast` property, `type` values are PascalCase (`Heading`,
 `Paragraph`, `Link`); `astJson` mirrors the Rust serialization with
  snake_case types (`heading`, `paragraph`, `link`).

Node kinds — blocks: `Heading` (level 1–6), `Paragraph`, `CodeBlock`
(lang/code), `BlockQuote`, `List` (ordered/items), `ThematicBreak`, `HtmlBlock`,
`Table` (header/rows/alignments), `LinkReferenceDefinition` (label/url/title),
`Comment`; inlines: `Text`, `Emphasis` (level `"Italic"` / `"Bold"`), `Code`,
`HtmlInline`, `Strikethrough`, `HardBreak`, `SoftBreak`, `Image`
(alt/url/title), `Link` (text/url/title/autolink), `LinkReference`
(text/label/url/title).

Task state: `"Unchecked"` / `"Checked"` (type `PeisarTaskState`). Table
alignment: `"Default"` / `"Left"` / `"Center"` / `"Right"` (type
`PeisarTableCellAlignment`, arrays as `PeisarTableCellAlignments`). Emphasis
level type: `PeisarEmphasisLevel`. All four type unions are exported from the
package root.

---

## 6. Extensibility (not default syntax)

Custom syntax is added per-document, not by the language itself:

- `useParser({ parseBlock, parseInline })` — hooks that run **before** the
  built-in matchers; block hooks receive `{ line, lineIndex, lines }` and
  return `{ block, consumed }`; inline hooks receive `{ rest, index }` and
  return `{ inline, consumed }`. Registering a hook re-parses the source.
- `useVisitor({ visitBlock, visitInline })` — post-parse transforms with
  control objects (`insertBefore`, `insertAfter`, `replaceWith`, `remove`,
  `recurse`).

These do not change the default language described above.