Skip to content

Linting

Zensical Studio's linter catches problems the formatter cannot: skipped heading levels, missing image alt text, and empty link destinations. The formatter handles Markdown style instead, such as list markers, table alignment, and spacing.

Problems appear as underlines in the file and entries in the Problems panelProblems panelDiagnostics panelProblems tool window. Results update as you edit. The linter understands your project. It checks content pulled in through snippets and @use directives, then reports problems in the source file that produced it.

What the linter checks

The linter checks document structure, accessibility, and Python Markdown extensions. Examples include empty link destinations, duplicate attribute IDs, and untitled content tabs. It does not repeat the formatter's checks.

Nineteen rules ship in total. Eleven run by default. The other eight are opt-in policy checks, such as whether raw HTML is allowed. The linter ignores code, math, HTML comments, and Jinja regions. Inline-text rules also check headings, table cells, container titles, definitions, and footnotes.

Configuration

Create a zensical-studio.toml file in the project root to activate the linter. Use it to enable opt-in rules, change severities, or disable default rules. Zensical Studio picks up changes immediately. No restart is needed. This configuration matches the built-in defaults:

version = 1

[lint.rules]
abbreviation.duplicate-definition = "warning"
attribute.duplicate-name = "warning"
attribute.rewritten-name = "warning"
fence.highlight-range = "warning"
footnote.empty-definition = "warning"
heading.level-skip = "warning"
html.invalid-markdown-mode = "warning"
image.missing-alt = "warning"
link.empty-destination = "warning"
tab.duplicate-title = "warning"
tab.empty-title = "error"

abbreviation.unused-definition = "off"
code.indented = "off"
container.empty-body = "off"
document.first-heading-h1 = "off"
fence.missing-language = "off"
heading.multiple-h1 = "off"
html.disallowed = "off"
section.empty = "off"

Severity levels

Severity controls the editor highlight and the icon in the Problems panelProblems panelDiagnostics panelProblems tool window:

Visual Studio Code shows one of the following icons for different severity levels:

Severity Icon Meaning
error The document cannot be processed reliably.
warning The document likely contains an authoring problem.
information Informational feedback.
hint Low-priority guidance.
off The rule does not run.

Migration

If you use Markdownlint, the combination of the auto-formatter and the linter gives you equivalent coverage. Zensical Studio adds checks for Python Markdown syntax, including content tabs, attribute lists, and abbreviations. It also resolves snippets, @use directives, and conditional content before checking. Problems point to their source file, not only the file being read.

There is nothing extra to install or run. Results appear in the same Problems panelProblems panelDiagnostics panelProblems tool window as broken links and orphaned footnotes.

Compatibility with Markdownlint

Some of the linter rules match Markdownlint rules, such as MD001 for heading/level-skip. The table links to the corresponding documentation.

Markdownlint Zensical Studio linter
MD001 Heading levels
MD025 Multiple H1 headings
MD033 Disallowed HTML
MD040 Missing fence language
MD041 First line must be H1
MD042 Empty link destination
MD045 Missing image alt text
MD046 Indented code

Suppressing a problem

Some problems are intentional, such as an unfinished link or decorative image. Use one of these comment forms to suppress them locally.

lint: skip

Suppress every rule

<!-- lint: skip --> applies to the next block of content when it appears on its own line:

<!-- lint: skip -->
[This intentionally has no destination]()

Suppress specific rules

Name one or more rules to suppress them and leave every other rule active:

<!-- lint: skip link.empty-destination image.missing-alt -->
[Intentional]() ![](intentional.png)

lint: off and lint: on

Suppress every rule

<!-- lint: off --> suppresses a region. <!-- lint: on --> ends it:

<!-- lint: off -->

[This intentionally has no destination]()
![](this-image-is-intentionally-decorative.svg)

<!-- lint: on -->

Suppress specific rules

As with skip, naming rules limit the suppression to those rules:

<!-- lint: off link.empty-destination image.missing-alt -->

[This intentionally has no destination]()
![](this-image-is-intentionally-decorative.svg)

<!-- lint: on link.empty-destination image.missing-alt -->

Unlike skip, which suppresses only the next block, off suppresses every following block until on appears or the containing block or document ends. An off region includes nested content. on is optional. An on inside a nested block re-enables the rule only in that block.

For example, off inside an admonition suppresses both links, then ends when the admonition ends. Replacing off with skip would suppress only the first link:

!!! note

    <!-- lint: off -->

    [This intentionally has no destination]()

    [This one is suppressed too]()

[This problem is reported]()

Default rules

heading/level-skip

Reports a heading that increases by more than one level. A skipped level breaks the document hierarchy that navigation and assistive technology both rely on.

Don't:

# Installation

### Linux

Do:

# Installation

## Linux

link/empty-destination

Reports an inline link whose destination is empty.

Don't:

Read the [installation guide]().

Do:

Read the [installation guide](installation.md).

image/missing-alt

Reports an image with empty or whitespace-only alternative text. This covers inline and reference images; a raw HTML image is not checked.

Don't:

![](deployment.svg)

Do:

![Deployment architecture](deployment.svg)

footnote/empty-definition

Reports a footnote definition with no footnote text. A snippet or @use directive counts as footnote text, since it can still supply the content.

Don't:

The deployment needs one exception.[^exception]

[^exception]:

Do:

The deployment needs one exception.[^exception]

[^exception]: The staging environment uses a temporary hostname.

tab/empty-title

Reports a content tab with an empty title, an error by default rather than a warning, since a tab with no title cannot render reliably.

Don't:

=== ""

    Content without a usable label.

Do:

=== "macOS"

    Installation instructions for macOS.

tab/duplicate-title

Reports two tabs in the same set with the same visible title. Inline formatting is resolved before titles are compared, so **macOS** and macOS still count as duplicates. Separate tab sets, and tabs nested inside different containers, are checked independently.

Don't:

=== "macOS"

    Intel instructions.

=== "**macOS**"

    Apple silicon instructions.

Do:

=== "macOS on Intel"

    Intel instructions.

=== "macOS on Apple silicon"

    Apple silicon instructions.

attribute/duplicate-name

Reports an ID or a key-value attribute assigned more than once in one attribute list. Python Markdown silently keeps only the last value, so an earlier one is lost without warning. A repeated class is not affected, since classes accumulate rather than overwrite each other.

Don't:

Release status
{ #draft #published data-state=draft data-state=published }

Do:

Release status
{ #published data-state=published .card .current }

attribute/rewritten-name

Reports an attribute name that Python Markdown silently rewrites into a different name. This is the attr_list extension's own behavior: it replaces every character outside letters, digits, underscores, colons, periods, and hyphens with an underscore, rather than rejecting the name. Nothing looks wrong in the source, but anything that targets the name you typed, such as, CSS, JavaScript, another attribute list, silently stops matching.

Don't:

Release status
{ data@state=published }

Do:

Release status
{ data-state=published }

html/invalid-markdown-mode

Reports a markdown attribute on an HTML element with an unsupported value, an element the attribute has no effect on, or a mode the surrounding HTML context makes ineffective.

Don't:

<div markdown="yes">
**This is expected to be Markdown.**
</div>

Do:

<div markdown="block">

**This is parsed as Markdown.**

</div>

fence/highlight-range

Reports an hl_lines value that names a line the fenced code block does not have, whether the value names a single line or a range.

Don't:

``` python hl_lines="3"
print("one")
print("two")
```

Do:

``` python hl_lines="2"
print("one")
print("two")
```

abbreviation/duplicate-definition

Reports an abbreviation term defined more than once. Terms are compared exactly, including case; Python Markdown otherwise silently uses whichever definition comes last.

Don't:

*[HTML]: Hyper Text Markup Language
*[HTML]: Hypertext Markup Language

Do:

*[HTML]: Hypertext Markup Language

Opt-in rules

These rules encode a project policy or a preferred convention, not a problem the linter checks for in every project. After you create zensical-studio.toml to activate the linter, enable the rules you want in that file.

heading/multiple-h1

Reports every top-level heading after the first one in a logical document. An ATX heading (#) and a Setext heading (underlined with =) in a nested container both count toward the same document-wide total.

Don't:

# Installation

# Configuration

Do:

# Product guide

## Installation

## Configuration

document/first-heading-h1

Reports a document whose first heading is not a top-level heading. Introductory text or metadata may still come before it, and a document with no headings at all is not reported.

Don't:

Introductory text.

## Installation

Do:

Introductory text.

# Installation

section/empty

Reports a heading with no substantive content before the next heading at the same or a higher level, or before the end of the document. A subsection underneath it counts as content; a hidden link, footnote, or abbreviation definition does not.

Don't:

## Installation

## Configuration

Configure the project in `zensical.toml`.

Do:

## Installation

Install the package before configuring the project.

## Configuration

Configure the project in `zensical.toml`.

html/disallowed

Reports raw HTML, inline or block-level, for a project that disallows it. Off by default because raw HTML is valid Python Markdown and some layouts depend on it.

Don't:

Use <strong>extreme caution</strong> in production.

Do:

Use **extreme caution** in production.

fence/missing-language

Reports a fenced code block with no language. An attribute list that names a language class, such as .python, still satisfies the rule.

Don't:

```
print("Hello")
```

Do:

``` python
print("Hello")
```

code/indented

Reports an indented code block and recommends fenced syntax instead. Indentation used structurally, inside a list item or a block quote, is not reported on its own.

Don't:

    print("Hello")

Do:

``` python
print("Hello")
```

container/empty-body

Reports an admonition, a details block, a content tab, a Blocks node, or a definition with no content. A nested container with content of its own counts as content for the container around it.

Don't:

!!! warning

Do:

!!! warning

    Back up the database before continuing.

abbreviation/unused-definition

Reports an abbreviation definition whose exact term never appears in the page's prose. An occurrence inside code or another opaque region does not count.

Don't:

This page explains style sheets.

*[CSS]: Cascading Style Sheets

Do:

This page explains CSS.

*[CSS]: Cascading Style Sheets

Where to go next