Skip to content

Rule - MD051#

Property Value
Aliases md051, link-fragments
Autofix Available No
Enabled By Default Yes

Summary#

Local link fragments should be valid.

Reasoning#

Correctness#

Broken internal links degrade the reader's experience: a reader who clicks a link expecting to land on a specific section, only to find the fragment does not resolve to any target, is left with a confusing, unhelpful result. Unlike external targets, local link fragments (such as #target) are defined entirely within the same document, so there is no reason not to validate them.

Spec Basis#

There is no official GitHub Flavored Markdown specification for what constitutes a valid link target. This rule collects the target text from one of the following Markdown elements:

  • ATX heading: any text on the same line, excluding any trailing # characters that are part of an ATX heading close.
  • Setext heading: any text that is part of the paragraph before the Setext heading characters.
  • HTML Blocks and Raw HTML: the name attribute for an a start tag (i.e. <a name="bookmark">) or any start tag's id attribute (i.e. <a id="bookmark">).

For the ATX and Setext heading cases, the collected text is transformed into a fragment using the historic GitHub heading algorithm. The transformation steps, and how they are affected by configuration, are documented in the Configuration section.

Examples#

Failure Scenarios#

This rule triggers when there is an inline link with a local fragment specified, but the document does not contain any targets with that name.

# Heading Name

[Local Link](#local-fragment)

Explanation: This example fails because the inline link points to the local fragment #local-fragment, but the document contains no heading (or other target) that generates that fragment. The only heading, # Heading Name, produces the fragment #heading-name (lowercased, with spaces converted to dashes). Because the referenced fragment does not resolve to any real target, the unresolved fragment is invalid.

Unlike the previous example, which used an inline link, this scenario places the invalid fragment inside a link reference definition. The rule triggers on the definition itself rather than on the shorter reference that uses it.

[Local Link][Local Link]

[Local Link]: #local-fragment

Explanation: This example fails for the same underlying reason as the previous one, but the invalid fragment is defined in a link reference definition rather than written inline. The definition [Local Link]: #local-fragment points to a fragment that no target in the document generates. Because the rule evaluates the destination of the link regardless of how it is written, it triggers on the link reference definition itself — not on the shorter [Local Link] reference that uses it — since that definition is where the unresolvable fragment actually lives.

Unlike the previous examples, which used ATX headings, this scenario uses a Setext heading. The heading text still generates a fragment, but the referenced fragment does not match it.

Heading Name
============

[Local Link](#heading-name-xyz)

Explanation: This example fails because the Setext heading Heading Name generates the fragment #heading-name (text lowercased, spaces converted to dashes), but the link points to #heading-name-xyz, which no heading or target produces. The fragment is therefore unresolved and invalid, even though the heading is a Setext heading rather than an ATX heading.

Unlike the previous examples, which relied on headings, this scenario uses an HTML id attribute as the only target in the document. The referenced fragment does not match the id.

<a id="real-bookmark"></a>

[Local Link](#wrong-bookmark)

Explanation: This example fails because the only target in the document is the HTML element with id="real-bookmark", which provides the fragment #real-bookmark. The link points to #wrong-bookmark, which matches neither the HTML id/name target nor any heading, so the fragment does not resolve to any target in the document.

Unlike the previous example, which used an HTML id attribute as the only target in the document, this scenario uses an HTML name attribute instead. The rule treats name and id as equivalent targets, but the referenced fragment still does not match the name value.

<a name="real-bookmark"></a>

[Local Link](#wrong-bookmark)

Explanation: This example fails because the only target in the document is the HTML element with name="real-bookmark", which provides the fragment #real-bookmark. The link points to #wrong-bookmark, which does not match the name value and does not correspond to any heading, so the fragment does not resolve to any target in the document. The failure is structurally identical to the id case in the previous scenario; it confirms that the rule validates both name and id HTML targets.

Unlike the previous examples, which each had a single target in the document, this scenario contains two identical headings. The rule appends an incrementing integer to duplicate headings, but the link references a third occurrence (#heading-name-3) that does not exist.

# Heading Name

some text

# Heading Name

[Local Link](#heading-name-3)

Explanation: This example fails because the document contains only two occurrences of the heading Heading Name. The rule assigns the fragments #heading-name (first occurrence) and #heading-name-2 (second occurrence), but no fragment #heading-name-3 is generated. The link [Local Link](#heading-name-3) therefore references a fragment that no target in the document produces, and the rule flags it as an unresolved local fragment. This is the inverse of the "duplicate heading" correct scenario: that one targets an existing numbered fragment, while this one targets a number beyond the count of duplicates.

Unlike the previous examples, which each relied on a fragment that did not match the target at all, this scenario isolates the case-sensitivity behavior controlled by ignore-case. The fragment differs from the heading-generated fragment only in case, and the configuration ignore-case is set to False (the non-default value), so the rule performs a case-sensitive comparison and flags the mismatch.

# Heading Name

[Local Link](#HEADING-NAME)

Explanation: The heading # Heading Name generates the lowercase fragment #heading-name. The link uses #HEADING-NAME, which differs only in case. With ignore-case set to False (non-default), the rule compares the fragment case-sensitively: #HEADING-NAME does not equal #heading-name, so the fragment does not resolve to any target in the document and the rule triggers. This is the inverse of the "case-insensitive match" correct scenario: that one uses the same fragment with ignore-case set to True (the default) and passes, while this one uses the same fragment with ignore-case set to False and fails.

Correct Scenarios#

This rule does not trigger when there is a link with a local fragment specified, with the document containing a target with that name.

# Heading Name

[Local Link](#heading-name)

Explanation: This example passes because the local fragment #heading-name matches the fragment generated by the heading # Heading Name. The heading text is converted to lowercase, punctuation is removed, and the space is converted to a dash, producing exactly #heading-name. Since that target exists in the document, the link resolves to a real section and a reader clicking it is taken to the expected place. Because the fragment is valid, the rule does not trigger.

Unlike the previous correct scenario, where a fragment must match a generated heading, this scenario uses the special target #top, which the rule recognizes without requiring a corresponding heading or element in the document.

[Top Of Page](#top)

Explanation: This example passes because #top is a universally recognized special target that does not require a corresponding heading or element in the document. The rule exempts this well-known fragment from validation, so the link resolves to the top of the page without needing a matching target.

Unlike the previous correct scenarios, this example's link fragment (#figure-1a) would normally be invalid because no heading generates that fragment. It is allowed here only because the configuration ignore-pattern-regex is set to a non-default value (^figure-); under the default empty-string setting this would be a failure.

The above [figure](#figure-1a).

Explanation: With the default ignore-pattern-regex value of (empty string), this fragment would be flagged as unresolved. In this example the configuration ignore-pattern-regex is set to ^figure-, so the rule consults this pattern before evaluating the fragment and, because figure-1a matches the ^figure- prefix, the rule skips validation. This is the intended mechanism for tolerating generator-added anchors (e.g., image captions labelled figure-1, figure-2, …) that do not correspond to any author-defined heading.

Unlike the previous examples, this scenario involves a repeated heading, so the heading the link intends is the second occurrence. Because the rule appends an incrementing integer to repeated headings, the second one resolves to #heading-name-2, not the bare #heading-name.

# Heading Name

some text

# Heading Name

[Local Link](#heading-name-2)

Explanation: This example passes because the document contains two occurrences of the heading Heading Name. The rule appends an incrementing integer to duplicate headings to ensure unique fragments: the first occurrence generates #heading-name and the second generates #heading-name-2. The link [Local Link](#heading-name-2) correctly targets the second occurrence's fragment, which is a valid, resolvable target in the document.

Unlike the previous examples, this scenario isolates the case-sensitivity behavior controlled by ignore-case. With the value of ignore-case set to True (its default), the following example does not trigger the rule.

# Heading Name

[Local Link](#HEADING-NAME)

Explanation: The heading # Heading Name generates the lowercase fragment #heading-name. The link uses #HEADING-NAME, which differs only in case. With ignore-case set to True (the default) the rule treats these as matching and does not trigger this rule.

Unlike the previous correct scenarios, which used ATX headings, this scenario uses a Setext heading whose text still generates a matching fragment.

Heading Name
============

[Local Link](#heading-name)

Explanation: This example passes because the Setext heading Heading Name generates the fragment #heading-name using the same transformation as ATX headings (lowercase, spaces to dashes, punctuation removed). The link [Local Link](#heading-name) therefore resolves to a real target, and the rule does not trigger.

Unlike the previous correct scenarios, which relied on headings, this scenario uses an HTML id attribute as the matching target.

<a id="my-bookmark"></a>

[Local Link](#my-bookmark)

Explanation: This example passes because the HTML element's id attribute provides the fragment #my-bookmark, and the link [Local Link](#my-bookmark) points directly at it. The rule treats HTML id (and name) targets as valid link-fragment targets, so the fragment resolves and the rule does not trigger.

Unlike the previous correct scenarios, which used inline links, this scenario uses a link reference definition whose destination matches an existing heading fragment.

[Local Link][ref]

[ref]: #heading-name

# Heading Name

Explanation: This example passes because the link reference definition [ref]: #heading-name resolves to the fragment #heading-name, which is generated by the ATX heading # Heading Name later in the document. The rule evaluates the destination of the link regardless of how it is written, so the resolvable fragment satisfies the rule and no failure is reported.

Fix Description#

This rule cannot be auto-fixed because the only valid correction is to change the fragment to point at a target the author actually intended. PyMarkdown cannot reliably infer that intent from an invalid fragment alone (for example, #local-fragment could be a typo for #heading-name, #local-fragments, or a new heading the author plans to add). Because any auto-correction could silently redirect the reader to the wrong section, the rule reports the failure and leaves the correction to the author.

Configuration#

Prefixes
plugins.md051.
plugins.link-fragments.
Value Name Type Default Description
enabled boolean True Whether the Rule Plugin is enabled.
ignore-case boolean True Whether the Rule Plugin ignores case when matching local link fragments.
ignore-pattern-regex string (empty string) If not empty, regular expression for link fragment text to ignore.

Fragment Computation#

The HTML Block and Raw HTML targets are used directly, specified by the tag's id or name attribute value. For the ATX heading and Setext heading target information, the text contained within the heading must be processed to determine the appropriate fragment. While section links are not part of the CommonMark specification, this rule enforces the historic GitHub heading algorithm to generate those fragments by:

  • Converting the text to lowercase (if ignore-case is set to True)
  • Removing any punctuation characters
  • Converting any spaces to dashes
  • Appending an incrementing integer (as needed for uniqueness)
  • URI-encoding the result

Examples of this are:

  • Simple text
    • Example: # Heading Name --> #heading-name
  • Simple text with punctuation characters
    • Example: # Heading & Name ! --> #heading--name-
  • Unicode text (Ã = \xC3 in unicode, when encoded in utf-8, becomes \xC3\xA3)
    • Example: # Heading à name --> #heading-%C3%A3-name
  • Backslash escapes
    • Example: # Heading \* Name --> #heading--name
  • Numeric character references
    • Example: # Heading &#x0041; Name --> #heading-a-name
  • Entity character references
    • Example: # Heading &copy; Name --> #heading--name
  • Emphasis
    • Example: # Heading *foo* Name --> #heading-foo-name
  • Links
    • Example: # Heading [Google](www.google.com) Name --> #heading-google-name
  • Code spans
    • Example: # Heading `foo` Name --> #heading-foo-name
  • URI Autolinks
    • Example: # Heading <http://foo.bar.baz> Name --> #heading-httpfoobarbaz-name
  • Email Autolinks
    • Example: # Heading <foo@bar.example.com> Name --> #heading-foobarexamplecom-name
  • Raw HTML
    • Example: # Heading <del>d</del> Name --> #heading-d-name

Origination of Rule#

This rule is largely inspired by the MarkdownLint rule MD051.

Differences From MarkdownLint Rule#

The Line and Line/Column link format is supported by the MarkdownLint rule but not by this rule. At its core, this format allows for link fragments formatted as #l89 to specify that the 89th line of the current document should be highlighted and #L19C5-L21C11 to specify that from line 19, column 5 through line 21, column 11 of the current document should be highlighted.

During testing of these formats, two observations emerged. First, pointing to a Markdown document on GitHub is non-trivial: to view the Markdown itself, you need to add ?plain=1 to the end of the URL like this just to point at the Markdown document itself. Then, to point to the correct line, say line 40, you need to add ?plain=1#L40 to the end of the URL, like this. This creates ambiguity about whether you are even adding a link fragment to the document itself, or appending some text to the value for the plain option of the URL.

Second, GitHub's processing of these link fragments is inconsistent: After reviewing this page, over ten combinations were tested to determine how to validate the link fragments. All were accepted by GitHub — including malformed ones such as #xL40-L41 that were expected to be rejected. As a result, it was not possible to determine which fragments were actually valid.

Due to these observations and the ambiguity they introduced, the Line and Line/Column formats are not supported.