UNPKG

@eslint/markdown

Version:

The official ESLint language plugin for Markdown

196 lines (195 loc) 6.81 kB
/** * @fileoverview The MarkdownLanguage class. * @author Nicholas C. Zakas */ /* eslint class-methods-use-this: 0 -- Required to complete interface. */ //------------------------------------------------------------------------------ // Imports //------------------------------------------------------------------------------ import { MarkdownSourceCode } from "./markdown-source-code.js"; import { fromMarkdown } from "mdast-util-from-markdown"; import { frontmatterFromMarkdown } from "mdast-util-frontmatter"; import { gfmFromMarkdown } from "mdast-util-gfm"; import { frontmatter } from "micromark-extension-frontmatter"; import { gfm } from "micromark-extension-gfm"; //----------------------------------------------------------------------------- // Types //----------------------------------------------------------------------------- /** * @import { Language, File, ParseResult, OkParseResult } from "@eslint/core"; * @import { Root } from "mdast"; * @import { Options } from "mdast-util-from-markdown"; * @import { MarkdownLanguageOptions, MarkdownLanguageContext } from "../types.js"; * @typedef {Options['extensions']} Extensions * @typedef {Options['mdastExtensions']} MdastExtensions * @typedef {"commonmark"|"gfm"} ParserMode */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** * Parser configuration for JSON frontmatter. * Example of supported frontmatter format: * ```markdown * --- * { * "title": "My Document", * "date": "2025-06-09" * } * --- * ``` */ const jsonFrontmatterConfig = { type: "json", marker: "-", }; /** * Create parser options based on `mode` and `languageOptions`. * @param {ParserMode} mode The markdown parser mode. * @param {MarkdownLanguageOptions} languageOptions Language options. * @returns {{extensions: Extensions, mdastExtensions: MdastExtensions}} Parser options for micromark and mdast */ function createParserOptions(mode, languageOptions) { /** @type {Extensions} */ const extensions = []; /** @type {MdastExtensions} */ const mdastExtensions = []; // 1. `mode`: Add GFM extensions if mode is "gfm" if (mode === "gfm") { extensions.push(gfm()); mdastExtensions.push(gfmFromMarkdown()); } // 2. `languageOptions.frontmatter`: Handle frontmatter options const frontmatterOption = languageOptions?.frontmatter; // Skip frontmatter entirely if false if (frontmatterOption !== false) { if (frontmatterOption === "yaml") { extensions.push(frontmatter(["yaml"])); mdastExtensions.push(frontmatterFromMarkdown(["yaml"])); } else if (frontmatterOption === "toml") { extensions.push(frontmatter(["toml"])); mdastExtensions.push(frontmatterFromMarkdown(["toml"])); } else if (frontmatterOption === "json") { extensions.push(frontmatter(jsonFrontmatterConfig)); mdastExtensions.push(frontmatterFromMarkdown(jsonFrontmatterConfig)); } } return { extensions, mdastExtensions, }; } //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Markdown Language Object * @implements {Language} */ export class MarkdownLanguage { /** * The type of file to read. * @type {"text"} */ fileType = "text"; /** * The line number at which the parser starts counting. * @type {0|1} */ lineStart = 1; /** * The column number at which the parser starts counting. * @type {0|1} */ columnStart = 1; /** * The name of the key that holds the type of the node. * @type {string} */ nodeTypeKey = "type"; /** * Default language options. User-defined options are merged with this object. * @type {MarkdownLanguageOptions} */ defaultLanguageOptions = { frontmatter: false, }; /** * The Markdown parser mode. * @type {ParserMode} */ #mode = "commonmark"; /** * Creates a new instance. * @param {Object} options The options to use for this instance. * @param {ParserMode} [options.mode] The Markdown parser mode to use. */ constructor({ mode } = {}) { if (mode) { this.#mode = mode; } } /** * Validates the language options. * @param {MarkdownLanguageOptions} languageOptions The language options to validate. * @returns {void} * @throws {Error} When the language options are invalid. */ validateLanguageOptions(languageOptions) { const frontmatterOption = languageOptions?.frontmatter; const validFrontmatterOptions = new Set([ false, "yaml", "toml", "json", ]); if (frontmatterOption !== undefined && !validFrontmatterOptions.has(frontmatterOption)) { throw new Error(`Invalid language option value \`${frontmatterOption}\` for frontmatter.`); } } /** * Parses the given file into an AST. * @param {File} file The virtual file to parse. * @param {MarkdownLanguageContext} context The options to use for parsing. * @returns {ParseResult<Root>} The result of parsing. */ parse(file, context) { // Note: BOM already removed const text = /** @type {string} */ (file.body); /* * Check for parsing errors first. If there's a parsing error, nothing * else can happen. However, a parsing error does not throw an error * from this method - it's just considered a fatal error message, a * problem that ESLint identified just like any other. */ try { const options = createParserOptions(this.#mode, context?.languageOptions); const root = fromMarkdown(text, options); return { ok: true, ast: root, }; } catch (ex) { return { ok: false, errors: [ex], }; } } /** * Creates a new `MarkdownSourceCode` object from the given information. * @param {File} file The virtual file to create a `MarkdownSourceCode` object from. * @param {OkParseResult<Root>} parseResult The result returned from `parse()`. * @returns {MarkdownSourceCode} The new `MarkdownSourceCode` object. */ createSourceCode(file, parseResult) { return new MarkdownSourceCode({ text: /** @type {string} */ (file.body), ast: parseResult.ast, }); } }