reference
navi export
Explicit cards and free-input parameters exported as navi .cheat files.
smill build navi <file-or-directory>... --out <new-dir> [--shell bash|zsh|fish|nu] [--root <dir>] [--strict]
writes one <sheet>.cheat and <sheet>.navi.json pair per exported sheet to a new directory. It generates text
only: SheetMill never runs navi, a shell or any exported command, and a lint test
keeps the renderer free of os/exec, syscall, plugin and network imports.
Export is one way; importing .cheat files is not supported. Behavior was
checked against navi v2.24.0
(commit 5515367dc8a2a561d2d82b8352f729b1da4120d3) by reading its source, never
by running it; see the source evidence.
One explicit command card
Keep ordinary GFM prose, tables and layout. A card is exported only when it has
{.command} (or legacy {.navi}) and an explicit {#id}, and contains exactly one fenced code block
whose language belongs to the chosen profile. Generated IDs change with titles, so
they cannot anchor parameter metadata. Unmarked cards are ignored.
---
title: SheetMill CLI
tags: [sheetmill, validation]
cheatsheet:
schema: 1
tool: smill
cards:
check-sheet:
params:
sheet:
type: path
description: Markdown sheet to validate
---
## Validation
### Check a sheet {#check-sheet .command}
Validate a sheet before publishing it.
```bash
smill check "<sheet>"
```
For the input sheets/smill.md (relative to the root) this produces:
; Generated by SheetMill smill build navi; format 1
; Source: sheets/smill.md
; Title: SheetMill CLI
; Shell: bash
; Parameters: trusted raw shell text; type hints are not enforced
; License: not stated
; card: check-sheet
% sheetmill:sheets/smill.md#check-sheet!, smill, sheetmill, validation
# Check a sheet
smill check "<sheet>"
After shared source validation, a selected card that fails navi-specific export
checks is skipped with a warning; the
rest of the build continues and the warning names the card. A .command card
without an explicit ID fails core checking with CMD002; legacy .navi retains
NAV101 warning/skip behavior. Metadata, profile and
batch-identity problems are errors: nothing is written. For automation, --strict
turns any warning into a failure (exit 1) before anything is written.
Legacy syntax
The following remains supported. With identical paths, metadata values, command
bytes and build options, it produces byte-identical .cheat output and pinned
entry identities to the shared example above. Do not change IDs while migrating.
---
title: SheetMill CLI
tags: [sheetmill, validation]
cheatsheet:
schema: 1
tool: smill
navi:
shell: bash
cards:
check-sheet:
params:
sheet:
type: path
description: Markdown sheet to validate
---
## Validation
### Check a sheet {#check-sheet .navi}
Validate a sheet before publishing it.
```bash
smill check "<sheet>"
```
Shell profiles
navi runs every snippet through the one shell in its own configuration and the
fence label does not select it. A build therefore targets one profile, states it in
each header, and does not configure navi: configure navi's shell to match. A
legacy sheet's cheatsheet.navi.shell, when present, must equal the profile (NAV006
otherwise). Build each shell into its own directory.
| Profile | Accepted fence languages | Quote check |
|---|---|---|
bash (default) | bash, sh | Pinned Bash 5.2 parser, quote diagnostics only |
zsh | zsh | Pinned experimental Zsh parser, quote diagnostics only |
fish | fish | backslash escapes inside single quotes |
nu | nu, nushell | none; nushell raw strings are not analysed |
Cards in other languages are skipped with NAV101, never reinterpreted. Commands
in the experimental Fish profile skip its heuristic quote check when they
contain a heredoc marker. Bash/Zsh parsing understands heredoc payloads.
bash is the primary profile. zsh, fish and nu apply the same structural
checks with their own fence languages and quoting rules, and are experimental:
their fixtures are synthetic and have not been verified against real shells.
Bash and Zsh quote diagnostics use only mvdan.cc/sh/v3/syntax v3.14.1,
a non-executing Go parser; its Bash grammar follows 5.2, and Zsh support is
experimental. A validation-only copy substitutes inert words for navi placeholders;
the emitted command is unchanged. Case patterns, nested substitutions and heredocs
are parsed structurally. Only the parser's unclosed-quote diagnostics are reported;
other syntax errors are ignored and can prevent later quote checks. This is not
full syntax validation. Fish retains a heuristic, and Nushell performs no quote
check. Stronger syntax checks remain planned.
Metadata schema
| Location | Value |
|---|---|
cheatsheet.cards | Shared card settings validated by core; see command cards |
cheatsheet.navi | Legacy mapping with optional shell and cards only |
cheatsheet.updated, cheatsheet.tool_version | Sheet freshness: never exported, never a navi tag, .cheat bytes and identities unchanged; the companion keeps them verbatim as ordinary metadata. Source files are validated while parsing, so invalid values fail with VAL004 (source format) |
.cards.<id> | Keyed by an explicit command-card ID; mapping with only params |
.params.<name> | type (string, int or path; required) and description (optional single-line string) |
Names match [a-z][a-z0-9_]{0,31} and are local to one card, which has its own
navi context, so one name may be declared differently in different cards. Unknown
keys, references to missing/unmarked cards or automatically generated IDs, and
unsupported legacy types are NAV006. Shared metadata uses CMD001/CMD002;
conflicting complete declarations in both namespaces use NAV006. Invalid metadata
references are errors; a legacy marked
card without metadata that lacks an explicit ID still follows the NAV101
warning-and-skip rule.
type and description are SheetMill metadata; navi does not enforce them.
Defaults, choice lists, suggestion commands, previews, multi-selection and shared
contexts are not generated yet, and $ provider lines are never emitted.
Equal complete per-card declarations coalesce; disjoint card IDs combine. Different parameter subsets for one card are a conflict, not a partial merge. Both marker classes on one card select it once. See the migration rules.
Metadata companion
Every exported sheet has a <sheet>.navi.json companion with schema version 1:
{
"schema": 1,
"source": "sheets/smill.md",
"shell": "bash",
"metadata": {"title": "SheetMill CLI", "license": "EUPL-1.2"},
"cards": [{"id": "check-sheet", "title": "Check a sheet", "context": "sheetmill:sheets/smill.md#check-sheet!"}]
}
metadata contains the complete semantic frontmatter, including provenance,
unknown nested fields and parameter type/description hints. This preserves values,
not YAML spelling, comments or the Markdown body; it is not a round-trip format.
source is the normalized path relative to the selected root. shell records the
selected profile. cards lists only successfully exported cards, in source order,
with their original titles and exact generated contexts; metadata for skipped
cards remains in metadata.
JSON uses two-space indentation, fixed schema-field order, lexically sorted
metadata mapping keys, source-ordered arrays, HTML escaping and a final newline. It contains no generated timestamps or
absolute host paths introduced by the exporter. Author-supplied metadata is kept
unchanged, so review it before sharing. Companion schema and .cheat format
versions are independent. navi consumes only .cheat; SheetMill does not load or
execute companions. Both files use the existing new-directory/no-overwrite writer.
A sheet with no exported cards gets neither file. Invalid metadata still fails
before writing, as does companion serialization failure (REN001). Preserved
metadata no longer causes --strict to fail; actual warnings still do. License
warnings remain even if an unusual license value is preserved in the companion.
Placeholders
<name> with a lowercase name as above is a placeholder, and every placeholder
needs a declaration which must be used. navi's own token syntax is wider: its
regex \\?<(\w[\w\d\-_]*)> (Unicode \w) also reads <1>, <_x>, <a-b>,
<Name>, <ünï> and the escaped form \<name>. SheetMill matches navi's syntax
first, then rejects whatever its narrower grammar does not allow: the card is
skipped (NAV103). There is no escape for a literal placeholder. Redirections
(< in > out), heredocs (<<EOF) and spaced angle brackets are ordinary shell.
SheetMill adds no quoting: quote placeholders as you want them in the command.
Command text
The command is exported byte for byte. Skipped with NAV102: a carriage return,
NUL and other control characters, invisible or bidirectional format characters,
U+2028/U+2029, U+2800 (navi's finder field separator), blank lines (navi turns them
into separators), a trailing continuation backslash, an unterminated quote, and a
first word navi (navi rewrites it to its own executable path). navi reads a line
starting in column 0 with %, @, ;, # or $, or a code fence, as syntax
even inside a fence; such a line skips the card and is never indented or
hidden. SheetMill does not detect dangerous commands; the card's author owns that.
Identity and the .cheat structure
navi keeps one entry per hash of the trimmed tags concatenated, without separator,
with the trimmed description, and silently drops later duplicates. Each card
therefore gets its own first tag, sheetmill:<path>#<id>!, where the path is
relative to --root (default: the common directory of the inputs) and both parts
percent-encode every byte outside [A-Za-z0-9_./-]. The final ! terminates the
context so tags and titles cannot concatenate into another card's text. tool
and root tags follow, in source order, for filtering. Before writing, every entry's
pinned item hash is computed across the batch; equal hashes are NAV008 (error).
--root must name an existing directory without symlinks, and every input must
be inside it. An invalid root or outside input fails with IO001 before writing.
Titles, tags and header values are collapsed to one line, so they cannot create a
navi line. A title that is empty after that (for example only U+200B) or contains
U+2800 skips the card, because navi ignores entries without a description. A tag
with a comma, a line break or U+2800 is NAV006. The generated
; card: comment records the card ID.
Free input is trusted shell text
navi substitutes placeholder values as raw text, including values from its
environment. Double quotes keep ordinary spaces together, but do not make quotes,
substitutions or shell operators in a value safe. Neither type: path nor
type: string enforces anything at runtime. A leading dash may become a program
option; use a tool-supported -- or a ./ path where appropriate. Choosing to run
an exported template in navi is a separate user action.
Diagnostics
| Code | Severity | Meaning |
|---|---|---|
NAV001 | error | Unsupported shell profile |
NAV006 | error | Invalid or unknown cheatsheet.navi metadata, dangling card ID, wrong shell, unusable tag |
NAV007 | retired | Was a sheet-wide parameter conflict; parameters are now card-local |
NAV008 | error | Two entries share a pinned navi identity hash |
NAV101 | warning | Card skipped: no explicit ID, not exactly one code block, or language outside the profile |
NAV102 | warning | Card skipped: unsafe or unbalanced command text or title |
NAV103 | warning | Card skipped: malformed placeholder |
NAV104 | warning | Card skipped: placeholders and declarations disagree |
NAV105 | warning | License not stated; the header says so |
NAV106 | warning | Sheet exported nothing, so no file is written for it. If no sheet exports, the build fails and writes nothing |
NAV107 | retired | Metadata omission warning; complete frontmatter now survives in the companion |
NAV000 | error | --strict and at least one warning; nothing is written |
Card/code diagnostics use their source spans. Sheet-level metadata and global
profile diagnostics use the one-based 1:1 document/command anchor because the
IR does not retain individual frontmatter key spans.
REN001 and IO003 apply as for the other renderers. The output directory must
not exist.
Verification and gaps
Table-driven negative tests, golden bytes, a pure Go model of navi's line grammar
and entry deduplication, a fuzz target over command text, CLI tests and the
no-execution lint. The item hash model is pinned by fixed vectors
(itemHash("") = 0x1c9d3adb639f298e, itemHash("abc") = 0x64372d1c87d01e7c) that an
independent review calculated from the Rust hash input. Not done: running navi
(never, by policy). Fish quote analysis remains heuristic; Bash/Zsh only surface selected parser
diagnostics. Re-check the
upstream source when moving to another navi version.