Prose inside files that aren’t prose
Views
A markup file is a document, and Vale parses it into the blocks its rules see. A data file, a source file, or a plain-text file with a convention has prose in it too—in the descriptions of an API specification, the docstrings of a module, the body of a commit message—but nothing marks where. A View says where: a list of queries that pull named pieces out of the file, so Vale lints those and passes over the rest.
Views referenceThree engines, one idea
A View is a YAML file in your styles directory. It names an engine and a list of scopes, each a query the engine evaluates. What a query finds becomes a block with the scope's name, placed at the line and column it came from.
Data
daselJSON, YAML, and TOML, by extension.
A selector over the parsed document. Every string it lands on is one value.
- OpenAPI
- Jupyter
- Helm values
- Actions workflows
Text
TextFSMAny plain-text file with a convention.
A template of named values and a state machine of regular expressions.
- Commit messages
- Transcripts
- Changelogs
- Logs
Code
tree-sitterThe nineteen languages Vale has a grammar for.
A query against the syntax tree, replacing the built-in comment query.
- Docstrings
- String literals
- Comments
The View key names a View under a section of your config, so the files
it applies to are whichever that section matches. A name that isn't there is an error when the
configuration loads, and a query that fails is an error that points at the section.
An API spec is documentation
Every description in an OpenAPI document is published prose—it becomes your API reference. Two selectors reach all of them, and a description written in Markdown is parsed as Markdown, so the link target inside it is skipped as it would be on a page.
Tinted lines are linted. Version numbers, operation IDs, and path keys are structure, and a rule never sees them. A selector may land on one string or many; every string it selects is a value, and anything that isn't a string is dropped.
A notebook is a document
A Jupyter notebook keeps each Markdown cell's source as a list of lines. A scope's join puts a list back together, so the cell is one value, placed where its first line is.
A commit message has three parts
The subject, the body, and the trailers each deserve rules of their own. A TextFSM template declares the values to capture and a state machine that fills them, line by line, in the same regular-expression dialect every rule uses.
Reading begins in Start: the first line is the subject, and reading
moves to Body. There, each line is tried against the rules in order.
A line starting with # is Git's own commentary and is read past; a line
shaped like Word: text is a trailer; anything else is body. Point a commit-msg hook at the file and the message is checked before the commit lands.
A rule can aim at one part
A scope's name is how a rule reaches what the query found. It's appended to the scope of every block the value produces, so a rule that names it runs there and nowhere else.
scope: subject runs on the first line of a commit message and nowhere
else. A rule with the usual scope: text runs on everything the View
extracted, and a query without a name is linted but reachable only by the scopes its format
gives it.
A scope's type is the format the extracted text is parsed as: md, rst, html, org, or adoc. Without one, the text is
read as plain lines, which is right for a subject and wrong for a body written in
Markdown.
One side of a conversation
A transcript alternates between a user and a model, and only one side is yours to lint. A turn runs until the next label, so each state needs to know when the turn is over before it knows whose turn comes next.
The first rule in each state is the trick: it matches any label, captures nothing, emits the turn in hand as a record, and continues, so the rules below it capture the new turn into a fresh record. The user's lines are captured so they have somewhere to go, but no scope names them, so the misspelling on the last line goes unreported. The column is the column of the capture, so an alert on the first line of a turn points past the label.
Your own query, in source
Vale reads source code by its comments. A tree-sitter View replaces that with queries of your own, run against the file's syntax tree, so you decide what counts as prose in a language.
Each capture is one value. Its scope is text.comment, then the
query's name, so the docstrings answer to scope: text.comment.docstring and a plain scope: comment reaches both queries. Delimiters and per-line
decoration are stripped before linting, and #offset! trims a capture by
rows and columns from each end, which is how the docstring loses its quotes. The same shape lints
string literals, a UI's user-facing text, or anything else a grammar can name.