[GH-ISSUE #22] Request: Mandatory headings #16

Closed
opened 2026-03-03 01:23:03 +03:00 by kerem · 12 comments
Owner

Originally created by @originalfoo on GitHub (Jun 18, 2016).
Original GitHub issue: https://github.com/DavidAnson/markdownlint/issues/22

Would it be possible to get a rule which enforces mandatory headings in a file?

For example, in documentation, there may be a style guide that requires the docs to have a specific set of headers.

The rule would take an array of strings, each string being a header that must exist in the file. The rule is violated if any of the headers in the array are not found in the file.

If possible, an optional boolean flag (false by default) would make the rule ensure that the headers are in the same order as those specified in the array.

Originally created by @originalfoo on GitHub (Jun 18, 2016). Original GitHub issue: https://github.com/DavidAnson/markdownlint/issues/22 Would it be possible to get a rule which enforces mandatory headings in a file? For example, in documentation, there may be a style guide that requires the docs to have a specific set of headers. The rule would take an array of strings, each string being a header that must exist in the file. The rule is violated if any of the headers in the array are not found in the file. If possible, an optional boolean flag (false by default) would make the rule ensure that the headers are in the same order as those specified in the array.
kerem 2026-03-03 01:23:03 +03:00
Author
Owner

@originalfoo commented on GitHub (Jun 18, 2016):

Example rule config:

{ headings: [ '# Introduction', '## About', '## Dependencies' ...], ordered: true }

The # at start of strings denotes heading level - if present, the rule also validates that the heading, if found, is using the specified heading level.

<!-- gh-comment-id:226946337 --> @originalfoo commented on GitHub (Jun 18, 2016): Example rule config: ``` js { headings: [ '# Introduction', '## About', '## Dependencies' ...], ordered: true } ``` The `#` at start of strings denotes heading level - if present, the rule also validates that the heading, if found, is using the specified heading level.
Author
Owner

@DavidAnson commented on GitHub (Jun 18, 2016):

Seems reasonable, I like the design you've proposed. I'll look into this soon when I work through the backlog of markdownlint issues. Thanks!

<!-- gh-comment-id:226969600 --> @DavidAnson commented on GitHub (Jun 18, 2016): Seems reasonable, I like the design you've proposed. I'll look into this soon when I work through the backlog of `markdownlint` issues. Thanks!
Author
Owner

@originalfoo commented on GitHub (Jun 18, 2016):

Here's a rough code draft if it's any use:

  {
    "name": "MD042",
    "desc": "Mandatory headers are missing or wrong level/sequence",
    "tags": [ "headers" ],
    "aliases": [ "mandatory-headings" ],
    "func": function MD042(params, errors) {
      /**
       * Example options:
       *
       * { headings: [ '# Overview', '## About', '## Notes' ], ordered: true }
       *
       * Notes:
       *  * No `#` for heading means "any level"
       *  * Rule ignores any headings not listed in array
       */
      if ( !params.options
        || !params.options.headings
        || !params.options.headings instanceof Array ) {
        return;
      }
      var headings = params.options.headings,
          ordered  = params.options.ordered || true;
      // Split headings array in to texts and levels arrays:
      var texts  = []; // heaidng text
      var levels = []; // heading level (0 if not specified)
      headings.forEach(function(heading, idx) {
        var hed = heading.split('#');
        texts[idx]  = hed.pop().trim(); // heading text
        levels[idx] = hed.length; // heading level
      });
      // Verify headings in target document:
      // If heading is found, it gets removed from texts/levels
      // array regardless of whether it has error
      forEachHeading(params, function forHeading(heading, content) {
        // heading = "heading_open" token
        var level = parseInt(heading.tag.slice(1), 10);
        // content = heading text
        var text  = content.trim();
        if (ordered) {
          if (texts[0] === text) { // found in right place
            if (levels[0] && levels[0] !== level)
              errors.push(heading.lineNumber);
            // remove form lists
            texts.shift();
            levels.shift();
          } else { // maybe it's out of sequence?
            var wrongPos = texts.indexOf(text);
            if (wrongPos > -1) { // found, but out of sequence
              errors.push(heading.lineNumber);
              // remove from lists
              texts.splice(wrongPos,1);
              levels.splice(wrongPos,1);
            }
          } // else ignore non-listed headings
        } else { // unordered
          var pos = texts.indexOf(text);
          if (pos > -1) { // found, order not important
            if (levels[pos] && levels[pos] !== level)
              errors.push(heading.lineNumber);
            // remove from lists
            texts.splice(pos,1);
            levels.splice(pos,1);
          } // else ignore non-listed headings
        }
      });
      // any missed headings?
      if (texts.length) errors.push(0);
    }
  },
<!-- gh-comment-id:226971264 --> @originalfoo commented on GitHub (Jun 18, 2016): Here's a rough code draft if it's any use: ``` js { "name": "MD042", "desc": "Mandatory headers are missing or wrong level/sequence", "tags": [ "headers" ], "aliases": [ "mandatory-headings" ], "func": function MD042(params, errors) { /** * Example options: * * { headings: [ '# Overview', '## About', '## Notes' ], ordered: true } * * Notes: * * No `#` for heading means "any level" * * Rule ignores any headings not listed in array */ if ( !params.options || !params.options.headings || !params.options.headings instanceof Array ) { return; } var headings = params.options.headings, ordered = params.options.ordered || true; // Split headings array in to texts and levels arrays: var texts = []; // heaidng text var levels = []; // heading level (0 if not specified) headings.forEach(function(heading, idx) { var hed = heading.split('#'); texts[idx] = hed.pop().trim(); // heading text levels[idx] = hed.length; // heading level }); // Verify headings in target document: // If heading is found, it gets removed from texts/levels // array regardless of whether it has error forEachHeading(params, function forHeading(heading, content) { // heading = "heading_open" token var level = parseInt(heading.tag.slice(1), 10); // content = heading text var text = content.trim(); if (ordered) { if (texts[0] === text) { // found in right place if (levels[0] && levels[0] !== level) errors.push(heading.lineNumber); // remove form lists texts.shift(); levels.shift(); } else { // maybe it's out of sequence? var wrongPos = texts.indexOf(text); if (wrongPos > -1) { // found, but out of sequence errors.push(heading.lineNumber); // remove from lists texts.splice(wrongPos,1); levels.splice(wrongPos,1); } } // else ignore non-listed headings } else { // unordered var pos = texts.indexOf(text); if (pos > -1) { // found, order not important if (levels[pos] && levels[pos] !== level) errors.push(heading.lineNumber); // remove from lists texts.splice(pos,1); levels.splice(pos,1); } // else ignore non-listed headings } }); // any missed headings? if (texts.length) errors.push(0); } }, ```
Author
Owner

@DavidAnson commented on GitHub (Jun 21, 2016):

Nice, thanks! I will think about the algorithm independently as well. Also, what about another option only=true to fail the check if some other header is present?

<!-- gh-comment-id:227338050 --> @DavidAnson commented on GitHub (Jun 21, 2016): Nice, thanks! I will think about the algorithm independently as well. Also, what about another option `only=true` to fail the check if some _other_ header is present?
Author
Owner

@originalfoo commented on GitHub (Jun 21, 2016):

Yes, good idea!

<!-- gh-comment-id:227425434 --> @originalfoo commented on GitHub (Jun 21, 2016): Yes, good idea!
Author
Owner

@DavidAnson commented on GitHub (Jun 21, 2016):

Thinking about this in the context of something like Wikipedia, I'm not sure what we have so far is expressive enough. So I am playing with the idea of casting this as a regular expression scenario. Imagine if the list of headers was presented as a string similar to how you propose and then the rule runs a regular expression against it.

Example: # Intro\n# Body\n# Summary\n

This makes it possible to enforce simple rules like "exactly these three headers in this order", more complicated ones like "this set of 10 headers, 5 of which are optional", more generic ones like "only level one and two headings", or even stuff like "API documentation where each method is a level two heading followed by any number of level three headings for each parameter".

The concern I have with this is that violations will be harder to identify because there won't be meaningful line numbers. However, in order to cover the kinds of scenarios I see this being used for (GitHub Readme, OSS documentation), I'm thinking such a level of expressivity is necessary - and would be difficult to implement manually.

<!-- gh-comment-id:227489851 --> @DavidAnson commented on GitHub (Jun 21, 2016): Thinking about this in the context of something like Wikipedia, I'm not sure what we have so far is expressive enough. So I am playing with the idea of casting this as a regular expression scenario. Imagine if the list of headers was presented as a string similar to how you propose and then the rule runs a regular expression against it. Example: # Intro\n# Body\n# Summary\n This makes it possible to enforce simple rules like "exactly these three headers in this order", more complicated ones like "this set of 10 headers, 5 of which are optional", more generic ones like "only level one and two headings", or even stuff like "API documentation where each method is a level two heading followed by any number of level three headings for each parameter". The concern I have with this is that violations will be harder to identify because there won't be meaningful line numbers. However, in order to cover the kinds of scenarios I see this being used for (GitHub Readme, OSS documentation), I'm thinking such a level of expressivity is necessary - and would be difficult to implement manually.
Author
Owner

@originalfoo commented on GitHub (Jun 21, 2016):

I agree the regex approach would be a big improvement in terms of flexibility, so long as your docs provide decent examples for scenarios you cited - regex are often difficult to comprehend for people involved in maintaining documentation and style guides, etc.

Maybe a helper function could be provided that would take a nested JS object of specific format and make a regex from it?

Regarding the inability to report violation line numbers, I think that is an acceptable tradeoff in the specific case of headings - because headings are simple to spot in documents. Alternatively, is there some way to pick apart the regex and at least state which headings are missing or incorrect (even if we can't determine line number)?

At the very least, we would need the ability to define a custom error message to point people at style guide in case of violations.

<!-- gh-comment-id:227498071 --> @originalfoo commented on GitHub (Jun 21, 2016): I agree the regex approach would be a big improvement in terms of flexibility, so long as your docs provide decent examples for scenarios you cited - regex are often difficult to comprehend for people involved in maintaining documentation and style guides, etc. > Maybe a helper function could be provided that would take a nested JS object of specific format and [make a regex](https://github.com/slevithan/xregexp) from it? Regarding the inability to report violation line numbers, I think that is an acceptable tradeoff in the specific case of headings - because headings are simple to spot in documents. Alternatively, is there some way to pick apart the regex and at least state which headings are missing or incorrect (even if we can't determine line number)? At the very least, we would need the ability to define a custom error message to point people at style guide in case of violations.
Author
Owner

@DavidAnson commented on GitHub (Jul 3, 2016):

I went back and forth on this implementation a bunch. :| What ended up seeming like a reasonable first attempt is what you see in the associated commit. I think that it addresses the common scenarios clearly and in a way a beginner could use (using your syntax, with some changes). I will add more detailed custom error information as part of #23. I may still add RegExp support. If you feel this is not sufficient to start with, please let me know!

<!-- gh-comment-id:230136846 --> @DavidAnson commented on GitHub (Jul 3, 2016): I went back and forth on this implementation a bunch. :| What ended up seeming like a reasonable first attempt is what you see in the associated commit. I think that it addresses the common scenarios clearly and in a way a beginner could use (using your syntax, with some changes). I will add more detailed custom error information as part of #23. I may still add `RegExp` support. If you feel this is not sufficient to start with, please let me know!
Author
Owner

@sagiegurari commented on GitHub (Jul 4, 2016):

this is a really good feature.
when will this be published to npm?

<!-- gh-comment-id:230214119 --> @sagiegurari commented on GitHub (Jul 4, 2016): this is a really good feature. when will this be published to npm?
Author
Owner

@DavidAnson commented on GitHub (Jul 5, 2016):

I published version 0.2.0 a few minutes ago. :)

<!-- gh-comment-id:230622441 --> @DavidAnson commented on GitHub (Jul 5, 2016): I published version `0.2.0` a few minutes ago. :)
Author
Owner

@sagiegurari commented on GitHub (Jul 6, 2016):

thanks

<!-- gh-comment-id:230690686 --> @sagiegurari commented on GitHub (Jul 6, 2016): thanks
Author
Owner

@originalfoo commented on GitHub (Aug 22, 2016):

Been away due to RL issues, but great to see this feature implemented - many thanks!

<!-- gh-comment-id:241300800 --> @originalfoo commented on GitHub (Aug 22, 2016): Been away due to RL issues, but great to see this feature implemented - many thanks!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/markdownlint#16
No description provided.