UNPKG

remark-lint-final-definition

Version:

remark-lint rule to warn when definitions are not placed at the end of the file

209 lines (195 loc) 5.32 kB
/** * remark-lint rule to warn when definitions are used *in* the * document instead of at the end. * * ## What is this? * * This package checks where definitions are placed. * * ## When should I use this? * * You can use this package to check that definitions are consistently at the * end of the document. * * ## API * * ### `unified().use(remarkLintFinalDefinition)` * * Warn when definitions are used *in* the document instead of at the end. * * ###### Parameters * * There are no options. * * ###### Returns * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * * ## Recommendation * * There are different strategies for placing definitions. * The simplest is perhaps to place them all at the bottem of documents. * If you prefer that, turn on this rule. * * [api-remark-lint-final-definition]: #unifieduseremarklintfinaldefinition * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer * * @module final-definition * @author Titus Wormer * @copyright Titus Wormer * @license MIT * * @example * {"name": "ok.md"} * * Mercury. * * [venus]: https://example.com * * @example * {"name": "ok.md"} * * [mercury]: https://example.com/mercury/ * [venus]: https://example.com/venus/ * * @example * {"name": "ok-html-comments.md"} * * Mercury. * * [venus]: https://example.com/venus/ * * <!-- HTML comments in markdown are ignored. --> * * [earth]: https://example.com/earth/ * * @example * {"name": "ok-mdx-comments.mdx", "mdx": true} * * Mercury. * * [venus]: https://example.com/venus/ * * {/* Comments in expressions in MDX are ignored. *␀/} * * [earth]: https://example.com/earth/ * * @example * {"label": "input", "name": "not-ok.md"} * * Mercury. * * [venus]: https://example.com/venus/ * * Earth. * @example * {"label": "output", "name": "not-ok.md"} * * 3:1-3:36: Unexpected definition before last content, expected definitions after line `5` * * @example * {"gfm": true, "label": "input", "name": "gfm-nok.md"} * * Mercury. * * [^venus]: * **Venus** is the second planet from * the Sun. * * Earth. * @example * {"gfm": true, "label": "output", "name": "gfm-nok.md"} * * 3:1-5:13: Unexpected footnote definition before last content, expected definitions after line `7` * * @example * {"gfm": true, "name": "gfm-ok.md"} * * Mercury. * * Earth. * * [^venus]: * **Venus** is the second planet from * the Sun. */ /** * @import {Nodes, Root} from 'mdast' * @import {} from 'mdast-util-mdx' */ import {ok as assert} from 'devlop' import {phrasing} from 'mdast-util-phrasing' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' import {SKIP, visitParents} from 'unist-util-visit-parents' import {VFileMessage} from 'vfile-message' const remarkLintFinalDefinition = lintRule( { origin: 'remark-lint:final-definition', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-definition#readme' }, /** * @param {Root} tree * Tree. * @returns {undefined} * Nothing. */ function (tree, file) { /** @type {Array<Array<Nodes>>} */ const definitionStacks = [] /** @type {Array<Nodes> | undefined} */ let contentAncestors visitParents(tree, function (node, parents) { // Do not walk into phrasing. if (phrasing(node)) { return SKIP } if (node.type === 'definition' || node.type === 'footnoteDefinition') { definitionStacks.push([...parents, node]) // Do not enter footnote definitions. return SKIP } if ( node.type === 'root' || // Ignore HTML comments. (node.type === 'html' && /^[\t ]*<!--/.test(node.value)) || // Ignore MDX comments. (node.type === 'mdxFlowExpression' && /^\s*\/\*/.test(node.value)) ) { return } contentAncestors = [...parents, node] }) const content = contentAncestors ? contentAncestors.at(-1) : undefined const contentEnd = pointEnd(content) if (contentEnd) { assert(content) // Always defined. assert(contentAncestors) // Always defined. for (const definitionAncestors of definitionStacks) { const definition = definitionAncestors.at(-1) assert(definition) // Always defined. const definitionStart = pointStart(definition) if (definitionStart && definitionStart.line < contentEnd.line) { file.message( 'Unexpected ' + (definition.type === 'footnoteDefinition' ? 'footnote ' : '') + 'definition before last content, expected definitions after line `' + contentEnd.line + '`', { ancestors: definitionAncestors, cause: new VFileMessage('Last content defined here', { ancestors: contentAncestors, place: content.position, ruleId: 'final-definition', source: 'remark-lint' }), place: definition.position } ) } } } } ) export default remarkLintFinalDefinition