Scopes
Learn how Vale decides which parts of a file a rule applies to.
Vale is “markup aware”: it reads a file into the pieces a reader would recognize—headings, paragraphs, list items, links—and lets a rule say which of those pieces it applies to. That choice is the rule’s scope.
What a scope is
Every file Vale reads becomes a sequence of blocks. Each block carries a scope made of dot-separated parts that say what the block is and where it came from. A list item in a Markdown file, for example, is text.list.md.
A rule’s scope is a selector over those parts. It matches a block when all of its parts appear in the block’s scope. It isn’t a prefix match or an exact match, and the order you write the parts in doesn’t matter:
Selector
Matches text.list.md?
list
Yes
list.md
Yes
md.list
Yes—order is irrelevant
text.list
Yes
list.text.md
Yes
heading
No—not one of its parts
Two things follow. Any selector can be qualified with a file extension to make it format-specific: paragraph matches paragraphs everywhere, and paragraph.rst matches them only in reStructuredText. And a selector with fewer parts is broader: text matches nearly everything, because nearly every scope contains it.
Vale classifies files into one of three types—markup, code, or text—and the type determines which parts exist. Within a type, every format shares the same parts, so a rule written for Markdown headings applies to AsciiDoc headings too.
Markup
The default behavior for markup files is to apply rules to all non-ignored text. For most rules, you don’t need a scope at all.
For rules that target a kind of element, the parser assigns these parts:
heading
Matches all h{1,...} tags. You can specify an exact level by
appending tags—for example, heading.h1 matches all h1 tags.
table.header
Matches all th tags.
table.cell
Matches all td tags.
table.caption
Matches all caption tags.
figure.caption
Matches all figcaption tags.
list
Matches all li tags.
blockquote
Matches all blockquote tags.
alt
Matches all alt attributes.
The supported formats for markup files are:
HTML Built-in
Markdown Built-in, including R Markdown
MDX Built-in
MyST Built-in
Org Built-in
QDoc Built-in
Quarto Built-in
The formats marked as Built-in are included with Vale by default. The other formats require a third-party dependency to be installed. See each format’s documentation for more information and installation instructions.
Class scopes
Markup that carries no distinct tag of its own can still be selected by the classes wrapping it. Any enclosing class is appended to the scope as .class.<name>.
An AsciiDoc block title, for example, renders as <div class="title">—indistinguishable from body text by tag alone:
A directive’s name lands here too: a MyST or Quarto :::{note} scopes its content as class.note, and a QDoc \note does the same.
Classes nest, so a block inside two classed elements is reachable as text.class.outer.class.inner—and every block inside a classed container carries its class, however many blocks that is. To ignore classed content rather than target it, see IgnoredClasses.
Inline elements
Inline elements have scopes of their own, which let a rule target the text inside a link, a code span, or an emphasized phrase:
link
Matches the text of all a tags.
code
Matches all code and tt tags (code spans).
strong
Matches all strong and b tags.
emphasis
Matches all em and i tags.
A rule that asks for one of these scopes is the only kind that runs against it:
The text inside code spans is skipped by default (IgnoredScopes defaults to tt, code, and kbd), so a rule has to ask for code explicitly. To exclude inline text from every rule, see IgnoredScopes.
To exclude it from one rule, negate the scope. A negated inline scope keeps the rule running on the block around the element, with the element's text left out (Vale v3.22.0 or later):
This rule corrects the casing in paragraphs and list items, and leaves headings and link text, which follow title case, alone. See URLs for more on links.
Metadata
A document’s machine-readable content—an anchor name, an image’s file name—is collected under the meta scope. A QDoc file’s anchors and image file names land here, as does the content of an HTML data element.
Like the inline elements, meta is a sibling of text: an ordinary prose rule passes over it, and only a rule that asks for meta reaches it. Its text isn’t segmented, either—an identifier has no sentences.
Each piece carries its kind as a class, so a rule can target all of a document’s metadata or one kind of it:
Prose units
The parts above name elements of the document. These four name units of prose that Vale builds itself:
sentence
Each sentence, from every kind of prose—paragraphs, headings, list items, and table cells.
paragraph
Each body paragraph (a segment of text separated by two newlines). Headings, list items, table cells, and blockquotes are not paragraphs.
summary
The document’s prose as one block, excluding headings, code spans, code blocks, and
table cells. This is what metric and readability measure by default.
raw
The raw, unprocessed markup source as one block. This scope is useful for regex-based rules that need to match against the original source text.
A scope is a request as well as a selection. Sentences exist only because some rule declared sentence, and paragraphs only because some rule declared paragraph; Vale builds each unit when a rule asks for it and skips the work when none does. The same is true of the selections below.
Selections
A doc(...) term selects elements of the document by CSS selector. Every markup format Vale reads is parsed into the same HTML, and the selector runs against that, so a selection means the same thing in Markdown, AsciiDoc, reStructuredText, Org, and HTML.
Before the selectors run, Vale wraps each heading and everything that follows it—up to the next heading of the same or a higher level—in a section element. Sections nest, so an h3 after an h2 sits inside the h2’s section. That is what lets a selector name a section of a document, which no format marks up on its own.
A doc(...) term means one of three things, depending on where it stands:
Alone, the element is the block. Its text is gathered as one unit and linted once, so a rule can count or measure it:
An alert on a selection lands on the element’s first line, which is the heading of a section.
Beside a scope, it narrows that scope to blocks inside the element. Any existing rule can be pointed at one part of a document:
Negated, it excludes the element. ~doc(...) reaches everything outside the elements the selector matches.
A selection that matches nothing in a file still produces its block, empty, on line one. That is how a rule reports that a section is missing: an occurrence rule with min: 1 and a token that matches any character finds zero occurrences and reports the shortfall.
Selectors follow the syntax of Selectors Level 4: the combinators >, +, and ~; :not(), :is(), and :where(); :has() with a relative selector or a group of them, such as :has(> h1, > h2) for a section opened by either level or :has(> h2 + p) for a heading with a paragraph right after it; :first-of-type, :last-of-type, and the nth family; and attribute selectors. Three things to know:
:contains("…")matches any part of the text, ignoring case, soh2:contains("Decision")also matches a heading that reads “Decision log.” Give it the whole heading.An element with no prose in it—a code block, an image—selects fine and then has nothing to lint. Selections are for elements that hold text.
:scopeand theof Sform of:nth-child()are not supported. A relative selector in:has()is already anchored to the element, so:has(:scope > h2)is written:has(> h2).
The elements a selector matches carry a data-vale-doc attribute in Vale’s internal HTML, and blocks inside them carry the selection in their scope as in.<id>. Neither is something a rule writes; the rule writes the selector.
Named scopes
A selector that several rules share can be written once and given a name. Names live in YAML files under config/scopes/ on the StylesPath, one mapping of names to scope expressions per file:
A rule uses the name wherever it would have written the expression: alone, negated, or in a chain.
Every file under the directory is read, so a package that ships its names beside its rules needs nothing added to a configuration. A name may hold a chain, as lead does above; a chained name joins the rule’s other terms, and can’t be negated. A name can’t be one Vale already uses for a scope, such as heading or paragraph, and the same name defined twice with different expressions is an error.
Combining selectors
Rules may define multiple scopes by using a YAML array. An entry matches if any of them does:
Any scope prefaced with ~ is negated:
You can chain multiple scopes together using &, which requires all of them:
The two combine: & is an AND within a single entry, and the array is an OR across entries. A doc(...) term is a term like any other, so it chains with prose units and negates the same way:
Checks and scopes
Every check runs on the blocks its scope names. Three of them have a default worth knowing:
metricandreadabilitymeasure the whole document unless the rule declares a scope.textalso means the whole document here, since the document’s prose is whatsummaryholds. Any other scope measures each block it names on its own—a paragraph, a heading, a list item, or a selection—and a selection counts the elements it holds, soheading.h3inside a section is the number of subheadings in that section.sequencereads part-of-speech tags, which are assigned a sentence at a time, so it always reads sentences. Its scope says which blocks the sentences come from:listis the sentences of list items, and adoc(...)term is the sentences inside the selection.
Code
There are two code scopes: comment.line and comment.block.
See the Code documentation for more information.
Last updated