[GH-ISSUE #1708] Proposal: Add rules for math block separation and heading emphasis #2588

Open
opened 2026-03-07 20:09:07 +03:00 by kerem · 3 comments
Owner

Originally created by @obgnail on GitHub (Aug 9, 2025).
Original GitHub issue: https://github.com/DavidAnson/markdownlint/issues/1708

I would like to propose two new rules for markdownlint that are currently implemented in my own repository:

  1. Math blocks should be surrounded by blank lines :This rule ensures that math blocks (delimited by $$ ... $$) have blank lines before and after them, improving readability and consistency.

  2. Headings should not be fully emphasized :This rule prevents headings from being composed entirely of emphasized text (e.g., ## **bold** or ## _italic_), which can reduce clarity or hinder accessibility. Similar issue: #1608

I believe these rules could benefit the markdownlint community by promoting best practices in markdown documents. Please let me know if you would consider including these rules, or if there are any requirements or guidelines I should follow when proposing new rules.

Originally created by @obgnail on GitHub (Aug 9, 2025). Original GitHub issue: https://github.com/DavidAnson/markdownlint/issues/1708 I would like to propose two new rules for markdownlint that are currently implemented in my own [repository](https://github.com/obgnail/markdownlint-custom-rules): 1. **Math blocks should be surrounded by blank lines** :This rule ensures that math blocks (delimited by `$$ ... $$`) have blank lines before and after them, improving readability and consistency. 2. **Headings should not be fully emphasized** :This rule prevents headings from being composed entirely of emphasized text (e.g., `## **bold**` or `## _italic_`), which can reduce clarity or hinder accessibility. Similar issue: #1608 I believe these rules could benefit the markdownlint community by promoting best practices in markdown documents. Please let me know if you would consider including these rules, or if there are any requirements or guidelines I should follow when proposing new rules.
Author
Owner

@obgnail commented on GitHub (Aug 9, 2025):

Here is my simple implementation:

const { addErrorContext, isBlankLine } = require("markdownlint-rule-helpers")
const { getParentOfType, filterByTypes } = require("markdownlint-rule-helpers/micromark")

const mathBlockPrefixRe = /^(.*?)[$\[]/

// eslint-disable-next-line jsdoc/valid-types
/** @typedef {readonly string[]} ReadonlyStringArray */

/**
 * Adds an error for the top or bottom of a math fence.
 *
 * @param {import("markdownlint").RuleOnError} onError Error-reporting callback.
 * @param {ReadonlyStringArray} lines Lines of Markdown content.
 * @param {number} lineNumber Line number.
 * @param {boolean} top True if top math.
 * @returns {void}
 */
function addError(onError, lines, lineNumber, top) {
    const line = lines[lineNumber - 1]
    const [, prefix] = line.match(mathBlockPrefixRe) || []
    const fixInfo = (prefix === undefined) ?
        undefined :
        {
            "lineNumber": lineNumber + (top ? 0 : 1),
            "insertText": `${prefix.replace(/[^>]/g, " ").trim()}\n`
        }
    addErrorContext(
        onError,
        lineNumber,
        line.trim(),
        undefined,
        undefined,
        undefined,
        fixInfo
    )
}

const MD101 = {
    "names": ["MD101", "math-surrounded-by-blank-lines"],
    "description": "Math Blocks should be surrounded by blank lines",
    "tags": ["math", "blank_lines"],
    "parser": "micromark",
    "function": (params, onError) => {
        const listItems = params.config.list_items
        const includeListItems = (listItems === undefined) ? true : !!listItems
        const { lines } = params

        for (const mathBlock of filterByTypes(params.parsers.micromark.tokens, ["mathFlow"])) {
            if (includeListItems || !(getParentOfType(mathBlock, ["listOrdered", "listUnordered"]))) {
                if (!isBlankLine(lines[mathBlock.startLine - 2])) {
                    addError(onError, lines, mathBlock.startLine, true)
                }
                if (!isBlankLine(lines[mathBlock.endLine]) && !isBlankLine(lines[mathBlock.endLine - 1])) {
                    addError(onError, lines, mathBlock.endLine, false)
                }
            }
        }
    }
}

function checkFullyEmphasize(token, headContentToken, onError) {
    const isEmphasis = token.type === "emphasis"
    const isStrong = token.type === "strong"

    if (isEmphasis || isStrong) {
        const type = isEmphasis ? "emphasisText" : "strongText"
        const textToken = token.children.find(t => t.type === type)
        if (textToken?.children.length === 1) {
            checkFullyEmphasize(textToken.children[0], headContentToken, onError)
            return
        }
        token = textToken
    }

    const column = headContentToken.startColumn
    const length = headContentToken.endColumn - column
    const fixInfo = token ? { editColumn: column, deleteCount: length, insertText: token.text } : undefined
    addErrorContext(
        onError,
        headContentToken.startLine,
        headContentToken.text.trim(),
        true,
        true,
        [column, length],
        fixInfo,
    )
}

const MD102 = {
    names: ["MD102", "no-fully-emphasized-heading"],
    description: "Headings should not be fully emphasized",
    tags: ["headings", "emphasis", "strong"],
    parser: "micromark",
    "function": (params, onError) => {
        const headings = filterByTypes(params.parsers.micromark.tokens, ["atxHeading"])
        for (const heading of headings) {
            const headingTextToken = heading.children.find(t => t.type === "atxHeadingText")
            if (!headingTextToken || headingTextToken.children.length !== 1) continue

            const headContentToken = headingTextToken.children[0]
            if (headContentToken.type === "emphasis" || headContentToken.type === "strong") {
                checkFullyEmphasize(headContentToken, headContentToken, onError)
            }
        }
    }
}

module.exports = { MD101, MD102 }

lint

<!-- gh-comment-id:3170692721 --> @obgnail commented on GitHub (Aug 9, 2025): Here is my simple implementation: ```js const { addErrorContext, isBlankLine } = require("markdownlint-rule-helpers") const { getParentOfType, filterByTypes } = require("markdownlint-rule-helpers/micromark") const mathBlockPrefixRe = /^(.*?)[$\[]/ // eslint-disable-next-line jsdoc/valid-types /** @typedef {readonly string[]} ReadonlyStringArray */ /** * Adds an error for the top or bottom of a math fence. * * @param {import("markdownlint").RuleOnError} onError Error-reporting callback. * @param {ReadonlyStringArray} lines Lines of Markdown content. * @param {number} lineNumber Line number. * @param {boolean} top True if top math. * @returns {void} */ function addError(onError, lines, lineNumber, top) { const line = lines[lineNumber - 1] const [, prefix] = line.match(mathBlockPrefixRe) || [] const fixInfo = (prefix === undefined) ? undefined : { "lineNumber": lineNumber + (top ? 0 : 1), "insertText": `${prefix.replace(/[^>]/g, " ").trim()}\n` } addErrorContext( onError, lineNumber, line.trim(), undefined, undefined, undefined, fixInfo ) } const MD101 = { "names": ["MD101", "math-surrounded-by-blank-lines"], "description": "Math Blocks should be surrounded by blank lines", "tags": ["math", "blank_lines"], "parser": "micromark", "function": (params, onError) => { const listItems = params.config.list_items const includeListItems = (listItems === undefined) ? true : !!listItems const { lines } = params for (const mathBlock of filterByTypes(params.parsers.micromark.tokens, ["mathFlow"])) { if (includeListItems || !(getParentOfType(mathBlock, ["listOrdered", "listUnordered"]))) { if (!isBlankLine(lines[mathBlock.startLine - 2])) { addError(onError, lines, mathBlock.startLine, true) } if (!isBlankLine(lines[mathBlock.endLine]) && !isBlankLine(lines[mathBlock.endLine - 1])) { addError(onError, lines, mathBlock.endLine, false) } } } } } function checkFullyEmphasize(token, headContentToken, onError) { const isEmphasis = token.type === "emphasis" const isStrong = token.type === "strong" if (isEmphasis || isStrong) { const type = isEmphasis ? "emphasisText" : "strongText" const textToken = token.children.find(t => t.type === type) if (textToken?.children.length === 1) { checkFullyEmphasize(textToken.children[0], headContentToken, onError) return } token = textToken } const column = headContentToken.startColumn const length = headContentToken.endColumn - column const fixInfo = token ? { editColumn: column, deleteCount: length, insertText: token.text } : undefined addErrorContext( onError, headContentToken.startLine, headContentToken.text.trim(), true, true, [column, length], fixInfo, ) } const MD102 = { names: ["MD102", "no-fully-emphasized-heading"], description: "Headings should not be fully emphasized", tags: ["headings", "emphasis", "strong"], parser: "micromark", "function": (params, onError) => { const headings = filterByTypes(params.parsers.micromark.tokens, ["atxHeading"]) for (const heading of headings) { const headingTextToken = heading.children.find(t => t.type === "atxHeadingText") if (!headingTextToken || headingTextToken.children.length !== 1) continue const headContentToken = headingTextToken.children[0] if (headContentToken.type === "emphasis" || headContentToken.type === "strong") { checkFullyEmphasize(headContentToken, headContentToken, onError) } } } } module.exports = { MD101, MD102 } ``` ![lint](https://github.com/user-attachments/assets/e4ebf695-73e8-4832-8e88-18bba4f3c62e)
Author
Owner

@DavidAnson commented on GitHub (Aug 9, 2025):

The rule about blanks surrounding math blocks seems very natural and a quick scan of your implementation makes me think I would have relatively few feedback comments. If you want to create a pull request for that as rule MD061 (MD060 does not exist yet, but it is in progress), I think that would be reasonable.

Regarding your second rule about emphasized headings, I understand the intent and again the implementation looks reasonable. However I wonder if this is more popular in practice than we realize. If you are familiar with how this repository works, I would be very interested what happens when that rule is applied to the set of external test repositories. If/when you decide to add the first rule, it should be fairly clear how to evaluate the second in a similar manner. While the test repositories are somewhat arbitrary, they've been quite helpful in flagging patterns that are more popular than I expect.

<!-- gh-comment-id:3171902902 --> @DavidAnson commented on GitHub (Aug 9, 2025): The rule about blanks surrounding math blocks seems very natural and a quick scan of your implementation makes me think I would have relatively few feedback comments. If you want to create a pull request for that as rule MD061 (MD060 does not exist yet, but it is in progress), I think that would be reasonable. Regarding your second rule about emphasized headings, I understand the intent and again the implementation looks reasonable. However I wonder if this is more popular in practice than we realize. If you are familiar with how this repository works, I would be very interested what happens when that rule is applied to the set of external test repositories. If/when you decide to add the first rule, it should be fairly clear how to evaluate the second in a similar manner. While the test repositories are somewhat arbitrary, they've been quite helpful in flagging patterns that are more popular than I expect.
Author
Owner

@paulhm7 commented on GitHub (Feb 5, 2026):

_****_

<!-- gh-comment-id:3854717999 --> @paulhm7 commented on GitHub (Feb 5, 2026): ### > [`_****_`](url)
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#2588
No description provided.