UNPKG

@alauda/doom

Version:

Doctor Doom making docs.

109 lines (108 loc) 3.68 kB
import { lintRule } from 'unified-lint-rule'; import { visitParents } from 'unist-util-visit-parents'; const getHtmlId = (value) => { const match = /\sid\s*=\s*(["'])(.*?)\1/iu.exec(value); if (!match) { return; } return match[2]; }; const getHtmlIdBearingElement = (node) => { const context = node.type === 'html' ? node.value : node.children.every((child) => child.type === 'html') ? node.children.map((child) => child.value).join('') : undefined; if (!context) { return; } const id = getHtmlId(context); if (!id) { return; } return { context, id, node, }; }; const getMdxIdAttribute = (node) => { const idAttribute = node.attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'id' && typeof attribute.value === 'string'); return idAttribute; }; const getMdxId = (node) => { const idAttribute = getMdxIdAttribute(node); return typeof idAttribute?.value === 'string' ? idAttribute.value : undefined; }; const stringifyMdxElement = (node) => { const attributes = node.attributes .map((attribute) => { if (attribute.type === 'mdxJsxExpressionAttribute') { return `{...${attribute.value}}`; } if (attribute.value === null || attribute.value === undefined) { return attribute.name; } if (typeof attribute.value === 'string') { return `${attribute.name}="${attribute.value}"`; } return `${attribute.name}={${attribute.value.value}}`; }) .join(' '); const name = node.name ?? ''; return `<${name}${attributes ? ` ${attributes}` : ''} />`; }; const getIdBearingElement = (node) => { if (node.type === 'html' || node.type === 'paragraph') { return getHtmlIdBearingElement(node); } if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') { const id = getMdxId(node); if (!id) { return; } return { context: stringifyMdxElement(node), id, node, }; } }; const getHeadingAnchorExample = (filepath, id) => filepath.endsWith('.mdx') ? `# Title \\{#${id}}` : `# Title {#${id}}`; const createMessage = (filepath, element) => `Unexpected id-bearing element \`${element.context}\` associated with a heading, rewrite it to \`${getHeadingAnchorExample(filepath, element.id)}\` instead.`; export const headingAnchorFormat = lintRule('doom-lint:heading-anchor-format', (root, vfile) => { visitParents(root, ['paragraph', 'html', 'mdxJsxFlowElement', 'mdxJsxTextElement'], (node, parents) => { const element = getIdBearingElement(node); if (!element) { return; } const report = () => { vfile.message(createMessage(vfile.path, element), { ancestors: [...parents, node], place: element.node.position, }); }; const parent = parents.at(-1); if (parent.type === 'heading') { report(); return; } const index = parent.children.indexOf(node); if (index < 0) { return; } for (const siblingIndex of [index - 1, index + 1]) { if (siblingIndex < 0 || siblingIndex >= parent.children.length) { continue; } const sibling = parent.children[siblingIndex]; if (sibling.type !== 'heading') { continue; } report(); return; } }); });