212 lines
7.4 KiB
JavaScript
212 lines
7.4 KiB
JavaScript
/**
|
|
* @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 { mathFromMarkdown } from "mdast-util-math";
|
|
import { frontmatter } from "micromark-extension-frontmatter";
|
|
import { gfm } from "micromark-extension-gfm";
|
|
import { math } from "micromark-extension-math";
|
|
//-----------------------------------------------------------------------------
|
|
// 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));
|
|
}
|
|
}
|
|
// 3. `languageOptions.math`: Handle math option
|
|
const mathOption = languageOptions?.math;
|
|
// Skip math entirely if false
|
|
if (mathOption === true) {
|
|
extensions.push(math());
|
|
mdastExtensions.push(mathFromMarkdown());
|
|
}
|
|
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,
|
|
math: 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) {
|
|
// `frontmatter` option validation
|
|
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. Expected one of \`false\`, \`"yaml"\`, \`"toml"\`, or \`"json"\`.`);
|
|
}
|
|
// `math` option validation
|
|
const mathOption = languageOptions?.math;
|
|
if (mathOption !== undefined && typeof mathOption !== "boolean") {
|
|
throw new Error(`Invalid language option value \`${mathOption}\` for math. Expected a boolean.`);
|
|
}
|
|
}
|
|
/**
|
|
* 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,
|
|
});
|
|
}
|
|
}
|