Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
fixtures/expected/*.ansi binary
fixtures/*.md text eol=lf
fixtures/expected/**/*.ansi binary
fixtures/**/*.md text eol=lf
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **Markdown frontmatter support.** YAML (`---`) and TOML (`+++`) metadata
blocks at the top of a document are now parsed and never leak into body
content. `--cat` renders a dim one-line summary
(`[metadata · key=value, …]`); TUI shows the same line and can expand it
to an inline key/value box with the new `m` key. A blank row follows the
summary for visual separation. Opt out entirely via `[metadata] show = false`
in `~/.termdown/config.toml`. See `docs/adr/0001-metadata-block-handling.md`.

## [0.5.1] - 2026-05-26

### Added
Expand Down
53 changes: 53 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# termdown — Context & Glossary

Single-context project. This file is the canonical glossary for domain terms
that appear in code, ADRs, and conversations about termdown. Keep entries short.
If you're tempted to add a term that future maintainers can derive from the
code itself (struct names, file layout, etc.), don't — only put **shared
vocabulary** here.

## Glossary

### Frontmatter
A block of metadata written at the **very beginning** of a Markdown file, fenced
by either `---` (YAML syntax) or `+++` (TOML syntax). Used by static site
generators (Jekyll, Hugo, Zola), note apps (Obsidian, Logseq), and agent skill
files (Anthropic, Cursor) to attach structured fields (title, author, tags,
name, description, …) to a document. Not part of CommonMark or GFM. Termdown
supports both YAML and TOML fences.

Synonym: **metadata block**. The two terms are interchangeable in this
project — `frontmatter` is the user-facing word, `MetadataBlock` is the
pulldown-cmark event name.

### Metadata one-line summary
The single dim line termdown renders in place of a parsed frontmatter block.
Format: `[metadata · key=value, key=value, …]` — wrapped in square brackets,
truncated to terminal width with the closing `]` preserved after the ellipsis.
Identical in both `--cat` and TUI **folded** state. Followed by one blank row
for visual separation from the body.

### Folded / Expanded (TUI metadata)
The two display states for a metadata block in TUI mode:
- **Folded** (default): one dim line — the [[metadata one-line summary]].
- **Expanded**: an inline box listing each key/value on its own row, pushing
body content down. Triggered by the `m` key. Second `m` collapses back.

Cat mode has no "expanded" state — only the one-line summary or nothing.

### `[metadata] show`
The single config knob (in `~/.termdown/config.toml`) controlling whether
frontmatter is visible at all. `show = true` (default) renders the [[metadata
one-line summary]] / expanded box; `show = false` hides the metadata block in
**both** cat and TUI. The pulldown-cmark metadata extensions are always
enabled internally regardless — `show` only gates rendering, never parsing.
See [[adr-0001-metadata-block-handling]].

### Heuristic parser
The line-based key/value extractor used to turn a raw frontmatter block into
the one-line summary. Does **not** depend on a real YAML/TOML parser; splits
each non-blank line on the first `:` (YAML) or `=` (TOML) and trims. If zero
valid key/value pairs are extracted, falls back to a raw single-line join of
the block. Rationale: keeps the dependency surface small for a use case
(quick visual summary) where parse fidelity doesn't matter.
See [[adr-0001-metadata-block-handling]].
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- 本地跑 `cargo install cargo-msrv && cargo msrv find`,二分出最低能编译的版本
- 同步更新 `Cargo.toml` 的 `rust-version` 和 `README.md` 里的 "Requires Rust X.Y+"
- 在 `.github/workflows/ci.yml` 加一个 `msrv` job(`cargo check --all-targets` on pinned toolchain),防止以后 PR 悄悄抬高 MSRV
- [ ] 测试 markdown metadata 支持
- [x] 测试 markdown metadata 支持
- [ ] 检测文件变化
- [x] 文件到顶、末尾时,播放声音提示,增加喇叭icon
- [ ] t 开启目录时,支持左右等方向键在目录和内容之间切换,并可以有一些界面上的 focus 提示
Expand Down
4 changes: 2 additions & 2 deletions docs/MARKDOWN_FEATURE_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Audit of `src/markdown.rs` against pulldown-cmark 0.13 and common Markdown exten
| Horizontal rule | ✓ | |
| HTML blocks | ✓ | Rendered verbatim as a dim preformatted block; HTML comments dropped |
| Inline HTML | ⚠ | Format tags (`b`/`strong`, `i`/`em`, `u`, `s`/`del`/`strike`, `code`/`kbd`) map to ANSI; `<br/>` / `<hr/>` handled; comments dropped; unknown tags stripped but their content is preserved. Attributes (e.g. `style="color:red"`, `href`) are not interpreted. |
| YAML / TOML frontmatter | ✓ | Parsed via pulldown-cmark's metadata-block extensions. Rendered as a dim one-line summary (`[metadata · key=value, …]`) in `--cat`; foldable inline box in TUI (toggle with `m`). Heuristic key/value extraction. See `docs/adr/0001-metadata-block-handling.md`. Opt out via `[metadata] show = false` in `~/.termdown/config.toml`. |

## Enabled GFM extensions

Expand All @@ -37,7 +38,6 @@ Audit of `src/markdown.rs` against pulldown-cmark 0.13 and common Markdown exten

- **Math** `$...$` / `$$...$$` — `ENABLE_MATH` not set
- **Definition list** — not enabled
- **YAML / TOML frontmatter** — not enabled; currently leaks into body
- **Smart punctuation** — not enabled
- **Wikilinks / Subscript / Superscript** — not enabled

Expand All @@ -50,6 +50,6 @@ Audit of `src/markdown.rs` against pulldown-cmark 0.13 and common Markdown exten

## Suggested priorities

1. **High value (hit in everyday Markdown)**: skip frontmatter, footnotes, alert/admonition styling, GFM autolinks, at least a graceful fallback for HTML
1. **High value (hit in everyday Markdown)**: footnotes, alert/admonition styling, GFM autolinks, at least a graceful fallback for HTML
2. **Differentiating (plays to termdown's Kitty-graphics strength)**: Mermaid rendering, real image rendering, code-block syntax highlighting
3. **Nice to have**: math (KaTeX → image), smart punctuation, definition list
134 changes: 134 additions & 0 deletions docs/adr/0001-metadata-block-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# ADR 0001 — Metadata block (frontmatter) handling

- **Date**: 2026-05-28
- **Status**: Accepted
- **Owners**: shawn

## Context

Markdown files in the wild — agent skill files (Anthropic/Cursor), static-site
posts (Jekyll/Hugo/Zola), and notes (Obsidian/Logseq) — commonly start with a
**frontmatter** block fenced by `---` (YAML) or `+++` (TOML). Termdown today
does not enable pulldown-cmark's metadata-block extensions, so the opening
fence is parsed as a horizontal rule and the YAML body collapses into a setext
H2 heading — which then gets rasterized into a large PNG via the Kitty
graphics pipeline. The result is loud, ugly, and useless.

Documented prior state: `docs/MARKDOWN_FEATURE_COVERAGE.md:40` notes
"YAML / TOML frontmatter — not enabled; currently leaks into body";
`fixtures/supported-syntax.md:13-15` calls it out as a known roadmap item.

This ADR records how termdown will handle metadata blocks going forward.

## Decision

Termdown will treat frontmatter as a **first-class lightweight element**, not
as body content and not as completely invisible noise:

1. **Parsing**: Enable `ENABLE_YAML_STYLE_METADATA_BLOCKS` **and**
`ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS` on the pulldown-cmark parser.
Capture the raw block text via the `MetadataBlock` event pair.
2. **One-line summary**: A heuristic parser splits the raw block into
`key=value, …` pairs, joined into a single dim line wrapped as
`[metadata · key=value, …]`, truncated to terminal width — the closing
`]` is preserved after the truncation ellipsis. No real YAML/TOML parser
is introduced.
3. **Fallback**: If the heuristic extracts zero valid key/value pairs, fall
back to a raw single-line join of the block content (so something useful is
still shown for malformed or exotic input).
4. **Cat mode**: Render exactly the one-line summary at the document's top.
5. **TUI mode**: Render the same one-line summary by default (**folded**). The
`m` key toggles to an **expanded** inline box listing each key/value on its
own row. The box is part of the scrolling content (not pinned), not part
of search, and not part of the Table of Contents. Default state is folded.
6. **Config gate**: A single boolean knob `[metadata] show` (default `true`)
in `~/.termdown/config.toml`. When `false`, frontmatter is **completely
hidden** in both cat and TUI; `m` becomes a no-op. The pulldown extensions
remain enabled — `show` gates rendering only, never parsing, so frontmatter
never leaks back into body regardless of config.

## Alternatives considered

### Visibility (what to show)

- **Hide completely, glow-style**. glow (the most-used CLI Markdown renderer)
silently strips frontmatter — no chip, no line, gone. Simplest possible
implementation. *Rejected*: termdown is positioned as a richer
terminal-reader experience; silently dropping authored metadata sacrifices
the agent-skill / Hugo-blog reading use case where the title/author/date
is genuinely informative.
- **Render as a `yaml` code block**. Keeps the original text fully visible.
*Rejected*: defeats the goal — the whole reason this is annoying today is
that the raw text dominates the top of the document.
- **One-line summary only, no expanded state**. Always-folded, no `m` key.
*Rejected*: users opening agent-skill files often want to see all fields at
once; the expanded state addresses that without burning screen real estate
by default.

### Parsing strategy

- **Full YAML parser** (e.g. `yaml-rust2`, `saphyr` — `serde_yaml` is
unmaintained). *Rejected*: adds a non-trivial dependency (~100–200 KB to
the binary) for a feature whose output is a 1-line summary. Termdown is a
terminal reader, not a YAML validator. The 5% of real-world files with
multi-line strings or deep nesting are not the target audience.
- **Treat each block as opaque text, no parsing**. *Rejected*: rejected as a
primary mode (loses the `key=value` structure that makes the summary
readable) but **accepted as the fallback** when the heuristic fails.

### Config surface

- **No config knob**. *Rejected*: at least one knob is justified for users who
consistently don't want any frontmatter UI noise (the glow-style preference).
- **Multiple knobs** (`default_state`, `cat_format`, …). *Rejected*: YAGNI.
The `m` key already handles per-document fold/expand preference; a single
master switch covers the only opt-out we can name a real user for.

### Key binding

- `m` is unused in the current TUI map (`src/tui/input.rs`), short, and a
natural mnemonic. Alternatives considered: `z`-style vim folds (multi-key
chord, overkill), `F` for fold (less obvious mnemonic).

## Consequences

- **New behavior visible in every fixture that has frontmatter.**
`fixtures/supported-syntax.md` already carries one; its golden snapshot
`fixtures/expected/supported-syntax.ansi` will change in the implementation
PR — reviewers should expect a large diff there and verify the new top
line reads `· metadata · …`.
- **New keybinding** in TUI: `m`. Must be added to the `?` help screen.
- **New config field** in `config.toml`. Backward compatible — missing field
defaults to `true`. Schema change in `src/config.rs`.
- **No new dependencies**. Both pulldown-cmark options already ship in the
pinned 0.13 release.
- **Edge cases handled implicitly by pulldown-cmark**: stray `---` mid-doc
is still a horizontal rule; frontmatter requires column 1, line 1; missing
closing fence consumes to EOF (rare; documented behavior, not
specially handled).
- **What's explicitly out of scope**: parsing frontmatter fields for
*functional* use (e.g. piping `title` to the TUI title bar, using `tags`
for filtering). Tracked separately if and when demand emerges.

## Test plan

- Snapshot diff on existing `fixtures/supported-syntax.md` (mutates golden).
- New focused fixtures under `fixtures/specialized/`:
- `metadata-yaml.md` — standard YAML block, exercises heuristic happy path.
- `metadata-toml.md` — standard TOML block, proves both syntaxes work.
- `metadata-malformed.md` — multi-line / nested values that trip the
heuristic, exercises fallback.
- `metadata-none.md` — body containing mid-document `---` thematic
breaks but no frontmatter; regression guard against false positives.
- Unit tests for the heuristic in `src/frontmatter.rs` (or wherever it lands):
empty block, single field, multiple fields, value containing `:` / `=`,
zero-valid-pairs → fallback.
- TUI fold/expand toggle: manual verification, not automated. Pure render
state, no branching logic worth automating.

## Open follow-ups

- Migration of config dir from `~/.termdown/` to XDG `~/.config/termdown/`
is unrelated and explicitly out of scope here.
- "Use `title` field in TUI title bar" — sketched as a future enhancement,
not committed.
8 changes: 8 additions & 0 deletions fixtures/expected/specialized/metadata-malformed.ansi
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[metadata · description=>, key1=value1, key2=value2, title=Malformed Sample]

Body. The frontmatter above mixes a folded scalar and a nested mapping —
shapes the line-based heuristic cannot fully understand. It skips the folded
continuation lines (which have no separator), but it cannot tell that  key1  /
 key2  are nested under  nested , so it lifts them to top-level keys. The point
of this fixture is that the heuristic degrades gracefully and the document does
not break — not that the summary is semantically perfect.
11 changes: 11 additions & 0 deletions fixtures/expected/specialized/metadata-none.ansi
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<IMG>

This file has no YAML or TOML metadata block. The  ---  line further down
should be rendered as a normal horizontal rule, not as the opening
fence of a frontmatter block.

Some body text before the rule.

────────────────────────────────────────────────────────────

Some body text after the rule.
4 changes: 4 additions & 0 deletions fixtures/expected/specialized/metadata-toml.ansi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[metadata · title=TOML Frontmatter Example, author=shawn, description=Standard…]

Body content follows. The TOML frontmatter above should be parsed the same
way as YAML and rendered as a one-line summary.
4 changes: 4 additions & 0 deletions fixtures/expected/specialized/metadata-yaml.ansi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[metadata · title=YAML Frontmatter Example, author=shawn, description=Standard…]

Body content follows. The YAML frontmatter above should appear as a single
dim summary line, not as a horizontal rule or stray paragraph.
10 changes: 4 additions & 6 deletions fixtures/expected/supported-syntax.ansi
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
────────────────────────────────────────────────────────────

<IMG>
[metadata · title=Supported Syntax Showcase, author=termdown, tags=[markdown, …]

<IMG>

Expand All @@ -10,9 +8,9 @@ or has explicitly committed to supporting (see  TODO.md [
change a
renderer feature, you should expect to update the snapshot of this file.

The YAML frontmatter above is part of the "markdown metadata" roadmap feature.
It
currently leaks into the rendered output and should one day be hidden.
The YAML frontmatter above is parsed and rendered as a single dim summary line
in  --cat , or as a collapsible inline box in TUI (toggle with  m ). It does
not leak into body content.

<IMG>

Expand Down
17 changes: 17 additions & 0 deletions fixtures/specialized/metadata-malformed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
description: >
this is a long
paragraph value spread
across multiple lines
nested:
key1: value1
key2: value2
title: Malformed Sample
---

Body. The frontmatter above mixes a folded scalar and a nested mapping —
shapes the line-based heuristic cannot fully understand. It skips the folded
continuation lines (which have no separator), but it cannot tell that `key1` /
`key2` are nested under `nested`, so it lifts them to top-level keys. The point
of this fixture is that the heuristic degrades gracefully and the document does
not break — not that the summary is semantically perfect.
11 changes: 11 additions & 0 deletions fixtures/specialized/metadata-none.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Plain Document, No Frontmatter

This file has no YAML or TOML metadata block. The `---` line further down
should be rendered as a normal horizontal rule, **not** as the opening
fence of a frontmatter block.

Some body text before the rule.

---

Some body text after the rule.
9 changes: 9 additions & 0 deletions fixtures/specialized/metadata-toml.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
+++
title = "TOML Frontmatter Example"
author = "shawn"
description = "Standard TOML metadata block, fenced with +++."
draft = false
+++

Body content follows. The TOML frontmatter above should be parsed the same
way as YAML and rendered as a one-line summary.
9 changes: 9 additions & 0 deletions fixtures/specialized/metadata-yaml.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: YAML Frontmatter Example
author: shawn
description: Standard YAML metadata block — heuristic happy path.
tags: [markdown, frontmatter, test]
---

Body content follows. The YAML frontmatter above should appear as a single
dim summary line, not as a horizontal rule or stray paragraph.
5 changes: 3 additions & 2 deletions fixtures/supported-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ This fixture is the **single source of truth** for syntax termdown currently sup
or has explicitly committed to supporting (see `TODO.md`). When you add or change a
renderer feature, you should expect to update the snapshot of *this* file.

The YAML frontmatter above is part of the "markdown metadata" roadmap feature. It
currently leaks into the rendered output and should one day be hidden.
The YAML frontmatter above is parsed and rendered as a single dim summary line
in `--cat`, or as a collapsible inline box in TUI (toggle with `m`). It does
not leak into body content.

## 1. Headings

Expand Down
Loading
Loading