Add front matter support (`CMARK_OPT_FRONT_MATTER`)
This PR adds opt-in front matter parsing to cmark. When
`CMARK_OPT_FRONT_MATTER` is set (or `--front-matter` is passed to the
executable), a `---` delimited block at the very start of the document is
captured as a `CMARK_NODE_FRONT_MATTER` node and excluded from HTML output.
The feature is entirely opt-in; existing behaviour is unchanged when the flag
is not set.
## Motivation
Many Markdown-based tools (static site generators, documentation systems,
notebook formats) attach structured metadata to documents using a front matter
block. Without native support, every such tool must pre-process the input
before handing it to cmark, losing source-position information and making it
impossible to round-trip the document.
## Design
### Node type
`CMARK_NODE_FRONT_MATTER` is a first-class block node type, added to the enum
after `CMARK_NODE_THEMATIC_BREAK`. It stores data identically to
`CMARK_NODE_CODE_BLOCK`:
- `cmark_node_get_literal()` — the raw content between the delimiters.
- `cmark_node_get_fence_info()` — an optional format hint from the opening
delimiter line (e.g. `--- yaml`, `--- toml`).
The implementation is format-agnostic. How the content is interpreted (YAML,
TOML, JSON, …) is left entirely to the caller.
### Parser integration
The feature is implemented as a small state machine in `src/front_matter.c`.
Two fields are added to `cmark_parser`:
- `front_matter_scanning` — true from the opening `---` until the closing
`---` is found or the document ends.
- `front_matter_buf` / `front_matter_info` — `cmark_strbuf` accumulators for
the content and info string respectively.
`cmark_front_matter_process_line()` is called from `S_process_line()` in
`blocks.c` immediately after `parser->line_number` is incremented, so line 1
is the trigger. The state lives on the parser struct, so the feature works
correctly regardless of how many times `cmark_parser_feed()` is called.
### Delimiter rules
- **Opening**: `---` on the very first line, optionally followed by an info
string (e.g. `--- yaml`). A fourth consecutive dash (`----`) is **not**
treated as a front matter opener — it remains a thematic break.
- **Closing**: exactly `---` with optional trailing whitespace. Note that
`...` (the YAML document-end marker) is intentionally **not** supported as a
closing delimiter; this implementation is format-agnostic and `...` has no
meaning outside of YAML.
- **No closing delimiter**: if the document ends without a closing `---`, the
entire document body (after the opening delimiter) is treated as front
matter.
### Renderers
All renderers handle `CMARK_NODE_FRONT_MATTER`:
| Renderer | Behaviour |
|-------------|-----------|
| HTML | Silent (front matter is metadata, not content) |
| Plaintext | Silent |
| CommonMark | Round-trips with delimiters and info string |
| LaTeX | Silent |
| Man | Silent |
| XML | Emitted as a `front_matter` element with `xml:space="preserve"` and an optional `info` attribute |
## Files changed
- `src/front_matter.c` — new: state machine implementation
- `src/front_matter.h` — new: public declaration
- `src/cmark.h` — `CMARK_NODE_FRONT_MATTER` enum entry, `CMARK_OPT_FRONT_MATTER` flag
- `src/parser.h` — `front_matter_scanning`, `front_matter_buf`, `front_matter_info` fields
- `src/blocks.c` — strbuf lifecycle, `S_process_line` hook, `cmark_parser_finish` hook
- `src/node.c` — `S_free_nodes`, `get_type_string`, `get_literal`, `set_literal`, `get_fence_info`, `set_fence_info`
- `src/main.c` — `--front-matter` flag
- `src/html.c`, `src/commonmark.c`, `src/latex.c`, `src/man.c`, `src/xml.c` — renderer cases
- `src/CMakeLists.txt` — build
- `test/front_matter.txt` — new: spec-format test fixture (10 examples)
- `test/CMakeLists.txt` — test wiring
- `api_test/main.c` — `test_front_matter()`: 12 assertions covering node type,
literal content, info string, source position, no-flag behaviour, no closing
delimiter, and multi-feed correctness
- `changelog.txt`
## Compatibility
`CMARK_OPT_FRONT_MATTER` uses bit 11 (`1 << 11`). Note that the cmark-gfm
fork uses this bit for `CMARK_OPT_GITHUB_PRE_LANG`; these are separate
codebases with independent option namespaces.
`CMARK_NODE_LAST_BLOCK` is updated from `CMARK_NODE_THEMATIC_BREAK` to
`CMARK_NODE_FRONT_MATTER`. Code that iterates over block node types using
this sentinel will automatically include the new type.
## Testing
```
cmake -S . -B build -DBUILD_SHARED_LIBS=ON
cmake --build build
ctest --test-dir build
# 10/10 tests pass, including api_test and front_matter_executable
```
合并状态:未合并 1 条评论