/** * @fileoverview Rule to enforce languages for fenced code. * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- /** * @import { MarkdownRuleDefinition } from "../types.js"; * @typedef {"missingLanguage" | "disallowedLanguage"} FencedCodeLanguageMessageIds * @typedef {[{ required?: string[] }]} FencedCodeLanguageOptions * @typedef {MarkdownRuleDefinition<{ RuleOptions: FencedCodeLanguageOptions, MessageIds: FencedCodeLanguageMessageIds }>} FencedCodeLanguageRuleDefinition */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- const fencedCodeCharacters = new Set(["`", "~"]); //----------------------------------------------------------------------------- // Rule Definition //----------------------------------------------------------------------------- export default /** @satisfies {FencedCodeLanguageRuleDefinition} */ ({ meta: { type: "problem", docs: { recommended: true, description: "Require languages for fenced code blocks", url: "https://github.com/eslint/markdown/blob/main/docs/rules/fenced-code-language.md", }, messages: { missingLanguage: "Missing code block language.", disallowedLanguage: 'Code block language "{{lang}}" is not allowed.', }, schema: [ { type: "object", properties: { required: { type: "array", items: { type: "string", }, uniqueItems: true, }, }, additionalProperties: false, }, ], defaultOptions: [ { required: [], }, ], }, create(context) { const required = new Set(context.options[0].required); const { sourceCode } = context; return { code(node) { if (!node.lang) { // only check fenced code blocks if (!fencedCodeCharacters.has(sourceCode.text[node.position.start.offset])) { return; } /** @type {number} */ let openingCodeFenceEndOffset; for (openingCodeFenceEndOffset = node.position.start.offset; fencedCodeCharacters.has(sourceCode.text[openingCodeFenceEndOffset]); openingCodeFenceEndOffset++) { // Find the end offset of the opening code fence. } context.report({ loc: { start: node.position.start, end: { line: node.position.start.line, column: node.position.start.column + openingCodeFenceEndOffset - node.position.start.offset, }, }, messageId: "missingLanguage", }); return; } if (required.size && !required.has(node.lang)) { const langIndex = sourceCode .getText(node) .indexOf(node.lang); context.report({ loc: { start: node.position.start, end: { line: node.position.start.line, column: node.position.start.column + langIndex + node.lang.length, }, }, messageId: "disallowedLanguage", data: { lang: node.lang, }, }); } }, }; }, });