Linting YAML embedded in Markdown¶
ryl can lint YAML that lives inside Markdown documents, in addition to
standalone .yaml/.yml files. Two kinds of embedded YAML are recognised:
- Front matter — the leading block delimited by
---…---(or a closing...) at the very top of the file. - Fenced code blocks tagged
yamloryml(including the{.yaml}attribute form and~~~tilde fences).
Each region is linted as its own independent YAML document, and every diagnostic's line and column point back into the original Markdown file.
This is a ryl-only capability, so it is configured exclusively in TOML
(ryl.toml, .ryl.toml, or [tool.ryl] in pyproject.toml). The YAML
(yamllint-compatible) configuration uses the legacy yaml-files key and has no
markdown support.
Source kinds and the [files] table¶
In TOML, ryl assigns every file a source kind via the [files] table, which
maps each kind to a list of gitignore-style glob patterns:
[files]
yaml = ["*.yaml", "*.yml", ".yamllint"] # default if [files] is omitted
markdown = ["*.md", "docs/**/*.md"] # opt-in: enables markdown linting
yamldefaults to["*.yaml", "*.yml", ".yamllint"]; setting it replaces the default. Add patterns for YAML stored under less-common names or extensions — for example Citation File Format (*.cff), clang's.clang-format/.clang-tidy, or Common Workflow Language (*.cwl):yaml = ["*.yaml", "*.yml", "*.cff", ".clang-format", "*.cwl"]. Globs match exact filenames and extensionless dotfiles too. (Avoid pointing it at templated pseudo-YAML such as SaltStack*.slsor*.yaml.j2, which embed Jinja and are not valid standalone YAML.)markdownis empty by default — listing patterns is what enables markdown linting (and scopes it, so only matching files are touched).- A file that matches more than one kind is a hard error (a file has exactly one kind).
- A file passed explicitly that matches no kind is rejected with an error telling you to add a glob; a file found while scanning a directory that matches no kind is simply skipped.
The legacy
yaml-fileskey is not valid in TOML — use[files].yaml. It remains valid in the yamllint-compatible YAML config.
Markdown behaviour is tuned in a separate [markdown] table (both default true):
[markdown]
front-matter = true # lint the --- ... --- block
fenced-blocks = true # lint yaml / yml fenced blocks
Set either flag to false to lint only the other source.
Other Markdown-family formats (Quarto, RMarkdown, MDX, …)¶
The markdown kind is not tied to the .md extension. Front matter is found with
a format-agnostic line scan and fenced blocks are located with a CommonMark parser,
so any Markdown-superset format works — map its extension(s) to the markdown kind:
Or, for a one-off run without editing config, pass --markdown, which enables the
markdown kind with those default globs:
ryl --markdown docs/ # scan a tree for *.md/*.markdown/*.qmd/*.Rmd/*.mdx
ryl --markdown report.qmd # a single Quarto document
cat SKILL.md | ryl --markdown - # from stdin (e.g. an editor / pre-commit)
This lints the YAML front matter and fenced yaml/yml blocks in Quarto (.qmd),
RMarkdown (.Rmd), MDX (.mdx), and similar documents. Format-specific constructs
that are not CommonMark (e.g. MDX/JSX, Quarto/RMarkdown executable {r}/{python}
chunks) are ignored — only YAML front matter and yaml/yml fenced blocks are
extracted.
In practice Quarto and RMarkdown keep their YAML almost entirely in front matter
(their code chunks are ```{r}/```{python}, not ```yaml), so linting them
mostly exercises the front-matter path. For example, ryl --markdown report.qmd
checks the leading block of:
and reports the extra space after toc: at its real line and column inside the
.qmd file. The same applies to agent skill files: a SKILL.md is YAML front
matter (name, description) plus prose, so ryl --markdown SKILL.md (or a
markdown = ["**/SKILL.md"] glob) lints that block.
How rules apply¶
The same rule set and configuration that applies to standalone YAML applies to each embedded region. Four file-shape rules are suppressed inside embedded regions, because a region is not a standalone file:
document-startanddocument-end— the front matter delimiters are not part of the linted content, and code-block fragments rarely carry markers.new-line-at-end-of-fileandnew-lines— these are governed by the host Markdown file, not the embedded snippet.
All other rules (indentation, key-duplicates, colons, truthy,
line-length, trailing-spaces, …) run normally.
Inline directives (# ryl disable / # yamllint disable) also
work inside an embedded region; a directive applies within the region that
contains it.
Example¶
A document with both a front matter block and a fenced yaml block:
With colons enabled, ryl docs.md reports the extra space after title: on
line 2 and any spacing problems inside the fenced block on its actual line —
columns include the block's indentation.
--fix¶
--fix applies the same safe fixes to each embedded region and writes the result
back into the Markdown document, re-applying whatever prefix the parser stripped
from each line — leading spaces, a blockquote >, or a tab — and preserving the
document's line endings (CRLF stays CRLF). The four file-shape rules suppressed in
check mode are also excluded from fixing, so a fragment never gains a ---/...
marker or a trailing newline.
Write-back is conservative by construction: ryl only rewrites a region when
re-applying that prefix reproduces the region's original bytes exactly. A region it
cannot reproduce — one whose lines do not share a single prefix (ragged indentation
where content lines are indented less than the fence, or other non-uniform layouts)
— is left byte-for-byte untouched while still being reported. This guarantees
--fix can never corrupt a Markdown document: the worst case is that an unusual
region is reported but not auto-fixed.
Linting Markdown from stdin and the CLI¶
Markdown linting is normally enabled by listing [files].markdown globs, but it can
also be turned on for a single run from the command line:
--markdownenables Markdown linting using default globs (*.md,*.markdown,*.mdx,*.qmd,*.Rmd) without editing config. It is a no-op when[files].markdownis already set, and its injected globs win over theyamlglobs for an overlapping file (so the flag never aborts a run whoseyamlglobs happen to match a Markdown extension). When linting stdin,--markdownforces the input to be treated as Markdown regardless of--stdin-filename.- Reading from stdin otherwise honours the source kind:
ryl - --stdin-filename doc.mdlints the piped bytes as Markdown whendoc.mdmatches themarkdownglobs (front matter and fenced blocks are extracted exactly as for a file on disk). Without--stdin-filenameand without--markdown, stdin is linted as plain YAML. As with files,--fixis not supported when reading from stdin.
Use with pre-commit¶
ryl is published as a pre-commit hook at
ryl-pre-commit. The default ryl
hook only sees YAML files. To also lint YAML embedded in Markdown, add the
dedicated ryl-markdown hook — it runs ryl --markdown, so it needs no
[files] config:
- repo: https://github.com/owenlamont/ryl-pre-commit
rev: v0.11.0
hooks:
- id: ryl
- id: ryl-markdown
The ryl-markdown hook targets .md, .markdown, .mdx, .qmd, and .Rmd
files and requires ryl >= 0.11.0. To autofix the embedded YAML in place, add
args: [--fix]:
Prefer a single hook? You can instead widen the plain ryl hook to also pass
Markdown files and opt them in via [files]:
- repo: https://github.com/owenlamont/ryl-pre-commit
rev: v0.11.0
hooks:
- id: ryl
types_or: [yaml, markdown]
This route needs a markdown glob under [files] in your ryl config: pre-commit
decides which files to pass; [files] decides how ryl treats each. So if the
hook passes a .md that no [files] glob matches, ryl reports an error (it was
named explicitly) — add a markdown glob to [files] to lint it, or narrow the
hook's file filter.
ryl also applies its ignore patterns to explicitly passed files, not just
to files found by scanning a directory. So a file pre-commit hands to ryl that
matches ignore is skipped — the equivalent of ruff's force-exclude, always on,
with no separate flag to set.