DavidAnson / markdownlint

A Node.js style checker and lint tool for Markdown/CommonMark files.
MIT License
4.73k stars 718 forks source link

Support Headings that start with a number in a compatible way with MD051 #705

Closed RaimondB closed 1 year ago

RaimondB commented 1 year ago

I am using Markdownlint in VS Code to check for internal links in a markdown document that gets translated by VuePress into anchors for headings.

This is mostly compatible, but it turns out that there are some cases where the algorithm differs.

Vuepress uses the following algorithm:

const rControl = /[\u0000-\u001f]/g
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’–—<>,.?/]+/g
const rCombining = /[\u0300-\u036F]/g

export = function slugify (str: string): string {
  // Split accented characters into components
  return str.normalize('NFKD')
    // Remove accents
    .replace(rCombining, '')
    // Remove control characters
    .replace(rControl, '')
    // Replace special characters
    .replace(rSpecial, '-')
    // Remove continuous separators
    .replace(/\-{2,}/g, '-')
    // Remove prefixing and trailing separators
    .replace(/^\-+|\-+$/g, '')
    // ensure it doesn't start with a number (#121)
    .replace(/^(\d)/, '_$1')
    // lowercase
    .toLowerCase()
}

It allows for configuring your own version of slugify, which I used to plugin the algorithm that markdownlint uses. However, it turns out that links to anchors on other pages that start with a digit do not work. (e.g. /otherpage.html#1-some-header) This is probably why they prefix numbers with an underscore.

To make things compatible, I want to be able to configure the way that markdownlint checks for anchor links. Can this be made configurable?

DavidAnson commented 1 year ago

Very related to #570 which asks for a different algorithm.

RaimondB commented 1 year ago

I'd be happy to help with implementation btw.

DavidAnson commented 1 year ago

It's more of a design question to begin with. Rule configuration is done via JSON or YAML and there is precedent for passing a regular expression. However, something like what you show below feels too complicated to express like that. I don't think it's practical to allow a custom function in JSON, so the next option would be to bake-in the various different algorithms people want. I think there are three or four so far (including the existing one) and I am not in love with that being unbounded. I also do not see a good way to automatically detect the user's preference (such as the "consistent" option some other rules have).

RaimondB commented 1 year ago

I've looked into the VS Code API a bit. A way to prevent lots of different versions of the same thing in the linter could be that you have to install a second extension (that everyone can build themselves) and that you basically configure in the markdownlint extension which other extension to use as a "plugin" to markdownlint.

It does complicate things in the sense that you would now need two extensions if you want to override the way markdownlint normally behaves, but it solves the problem of baking in lots of different versions of MD051 into markdownlint.

Also, everyone can make their own very special version and maybe target them to a specific environment. So for instance, I could make a "vuepress lint" extension for markdownlint, where I can implement maybe even specific rules I would like to add for this context.

RaimondB commented 1 year ago

The documentation shows this example:

When depending on the API of another extension add an extensionDependencies-entry to package.json, and use the getExtension-function and the exports-property, like below:

let mathExt = extensions.getExtension('genius.math');
let importedApi = mathExt.exports;

console.log(importedApi.mul(42, 1));

So the question here is if this would work also if you don't add the extensionDependency upfront but assume it is available and installed in a scope where you can reach it...

RaimondB commented 1 year ago

Btw I noticed that there is already a way to add your own custom rules but it is not clear to me yet how to combine this with the VS Code extension

DavidAnson commented 1 year ago

Yes, custom rules are already supported by the VS Code extension and provide a straightforward way to add an additional rule that implements whatever policy you'd like. My comments above were about implementing this in the markdownlint library/rules itself. If you are content writing a custom rule for this, it should be fairly straightforward to copy and modify the existing MD051 implementation and then reference that from your project.

RaimondB commented 1 year ago

I implemented the custom rule and was able to use it within vscode. However it turns out that markdownlinkcheck, which we are using in CI/CD uses again a different algorithm. So it seems that vuepress itself is actually causing the incompatibility the most by prefixing an anchor that starts with a number with an underscore.

So for now we are moving away from this problem and simply are applying the convention that you should not start headings with a number. :-/