feat: use go tool

This commit is contained in:
2026-07-03 19:24:01 -04:00
parent 39548b4332
commit 4d0cab1fe0
1542 changed files with 252399 additions and 14 deletions
+80
View File
@@ -0,0 +1,80 @@
// @ts-check
import { getReferenceLinkImageData as helpersGetReferenceLinkImageData } from "../helpers/helpers.cjs";
import { filterByTypes } from "../helpers/micromark-helpers.cjs";
/** @typedef {import("markdownlint").RuleParams} RuleParams */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
/** @typedef {import("../helpers/helpers.cjs").GetReferenceLinkImageDataResult} GetReferenceLinkImageDataResult */
/** @type {Map<string, object>} */
const map = new Map();
/** @type {RuleParams | undefined} */
let params = undefined;
/**
* Initializes (resets) the cache.
*
* @param {RuleParams} [p] Rule parameters object.
* @returns {void}
*/
export function initialize(p) {
map.clear();
params = p;
}
/**
* Gets the cached Micromark token array (for testing).
*
* @returns {MicromarkToken[]} Micromark tokens.
*/
export function micromarkTokens() {
return params?.parsers.micromark.tokens || [];
}
/**
* Gets a cached object value - computes it and caches it.
*
* @param {string} name Cache object name.
* @param {() => Object} getValue Getter for object value.
* @returns {Object} Object value.
*/
function getCached(name, getValue) {
if (map.has(name)) {
// @ts-ignore
return map.get(name);
}
const value = getValue();
map.set(name, value);
return value;
}
/**
* Filters a list of Micromark tokens by type and caches the result.
*
* @param {MicromarkTokenType[]} types Types to allow.
* @param {boolean} [htmlFlow] Whether to include htmlFlow content.
* @returns {MicromarkToken[]} Filtered tokens.
*/
export function filterByTypesCached(types, htmlFlow) {
// @ts-ignore
return getCached(
// eslint-disable-next-line prefer-rest-params
JSON.stringify(arguments),
() => filterByTypes(micromarkTokens(), types, htmlFlow)
);
}
/**
* Gets a reference link and image data object.
*
* @returns {GetReferenceLinkImageDataResult} Reference link and image data object.
*/
export function getReferenceLinkImageData() {
// @ts-ignore
return getCached(
getReferenceLinkImageData.name,
() => helpersGetReferenceLinkImageData(micromarkTokens())
);
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
import type { ConfigurationStrict } from "./configuration-strict.d.ts";
export interface Configuration extends ConfigurationStrict {
/**
* Index signature for arbitrary custom rules.
*/
[k: string]: unknown;
}
+14
View File
@@ -0,0 +1,14 @@
// @ts-check
/** @type {string[]} */
export const deprecatedRuleNames = [];
export const fixableRuleNames = [
"MD004", "MD005", "MD007", "MD009", "MD010", "MD011",
"MD012", "MD014", "MD018", "MD019", "MD020", "MD021",
"MD022", "MD023", "MD026", "MD027", "MD029", "MD030",
"MD031", "MD032", "MD034", "MD037", "MD038", "MD039",
"MD044", "MD047", "MD049", "MD050", "MD051", "MD053",
"MD054", "MD058"
];
export const homepage = "https://github.com/DavidAnson/markdownlint";
export const version = "0.40.0";
+18
View File
@@ -0,0 +1,18 @@
// @ts-check
"use strict";
/* eslint-disable jsdoc/reject-any-type */
/**
* Calls require for markdownit.cjs. Used to synchronously defer loading because module.createRequire is buggy under webpack (https://github.com/webpack/webpack/issues/16724).
*
* @returns {any} Exported module content.
*/
function requireMarkdownItCjs() {
return require("./markdownit.cjs");
}
module.exports = {
requireMarkdownItCjs
};
+1
View File
@@ -0,0 +1 @@
export { lintAsync as lint, readConfigAsync as readConfig } from "./markdownlint.mjs";
+3
View File
@@ -0,0 +1,3 @@
// @ts-check
export { lintAsync as lint, readConfigAsync as readConfig } from "./markdownlint.mjs";
+1
View File
@@ -0,0 +1 @@
export { extendConfigPromise as extendConfig, lintPromise as lint, readConfigPromise as readConfig } from "./markdownlint.mjs";
+3
View File
@@ -0,0 +1,3 @@
// @ts-check
export { extendConfigPromise as extendConfig, lintPromise as lint, readConfigPromise as readConfig } from "./markdownlint.mjs";
+1
View File
@@ -0,0 +1 @@
export { lintSync as lint, readConfigSync as readConfig } from "./markdownlint.mjs";
+3
View File
@@ -0,0 +1,3 @@
// @ts-check
export { lintSync as lint, readConfigSync as readConfig } from "./markdownlint.mjs";
+29
View File
@@ -0,0 +1,29 @@
export { resolveModule } from "./resolve-module.cjs";
export type Configuration = import("./markdownlint.mjs").Configuration;
export type ConfigurationParser = import("./markdownlint.mjs").ConfigurationParser;
export type ConfigurationStrict = import("./markdownlint.mjs").ConfigurationStrict;
export type FixInfo = import("./markdownlint.mjs").FixInfo;
export type FixInfoNormalized = import("./markdownlint.mjs").FixInfoNormalized;
export type LintCallback = import("./markdownlint.mjs").LintCallback;
export type LintContentCallback = import("./markdownlint.mjs").LintContentCallback;
export type LintError = import("./markdownlint.mjs").LintError;
export type LintResults = import("./markdownlint.mjs").LintResults;
export type MarkdownItFactory = import("./markdownlint.mjs").MarkdownItFactory;
export type MarkdownItToken = import("./markdownlint.mjs").MarkdownItToken;
export type MarkdownParsers = import("./markdownlint.mjs").MarkdownParsers;
export type MicromarkToken = import("./markdownlint.mjs").MicromarkToken;
export type MicromarkTokenType = import("./markdownlint.mjs").MicromarkTokenType;
export type Options = import("./markdownlint.mjs").Options;
export type ParserMarkdownIt = import("./markdownlint.mjs").ParserMarkdownIt;
export type ParserMicromark = import("./markdownlint.mjs").ParserMicromark;
export type Plugin = import("./markdownlint.mjs").Plugin;
export type ReadConfigCallback = import("./markdownlint.mjs").ReadConfigCallback;
export type ResolveConfigExtendsCallback = import("./markdownlint.mjs").ResolveConfigExtendsCallback;
export type Rule = import("./markdownlint.mjs").Rule;
export type RuleConfiguration = import("./markdownlint.mjs").RuleConfiguration;
export type RuleFunction = import("./markdownlint.mjs").RuleFunction;
export type RuleOnError = import("./markdownlint.mjs").RuleOnError;
export type RuleOnErrorFixInfo = import("./markdownlint.mjs").RuleOnErrorFixInfo;
export type RuleOnErrorInfo = import("./markdownlint.mjs").RuleOnErrorInfo;
export type RuleParams = import("./markdownlint.mjs").RuleParams;
export { applyFix, applyFixes, getVersion } from "./markdownlint.mjs";
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
export { applyFix, applyFixes, getVersion } from "./markdownlint.mjs";
export { resolveModule } from "./resolve-module.cjs";
/** @typedef {import("./markdownlint.mjs").Configuration} Configuration */
/** @typedef {import("./markdownlint.mjs").ConfigurationParser} ConfigurationParser */
/** @typedef {import("./markdownlint.mjs").ConfigurationStrict} ConfigurationStrict */
/** @typedef {import("./markdownlint.mjs").FixInfo} FixInfo */
/** @typedef {import("./markdownlint.mjs").FixInfoNormalized} FixInfoNormalized */
/** @typedef {import("./markdownlint.mjs").LintCallback} LintCallback */
/** @typedef {import("./markdownlint.mjs").LintContentCallback} LintContentCallback */
/** @typedef {import("./markdownlint.mjs").LintError} LintError */
/** @typedef {import("./markdownlint.mjs").LintResults} LintResults */
/** @typedef {import("./markdownlint.mjs").MarkdownItFactory} MarkdownItFactory */
/** @typedef {import("./markdownlint.mjs").MarkdownItToken} MarkdownItToken */
/** @typedef {import("./markdownlint.mjs").MarkdownParsers} MarkdownParsers */
/** @typedef {import("./markdownlint.mjs").MicromarkToken} MicromarkToken */
/** @typedef {import("./markdownlint.mjs").MicromarkTokenType} MicromarkTokenType */
/** @typedef {import("./markdownlint.mjs").Options} Options */
/** @typedef {import("./markdownlint.mjs").ParserMarkdownIt} ParserMarkdownIt */
/** @typedef {import("./markdownlint.mjs").ParserMicromark} ParserMicromark */
/** @typedef {import("./markdownlint.mjs").Plugin} Plugin */
/** @typedef {import("./markdownlint.mjs").ReadConfigCallback} ReadConfigCallback */
/** @typedef {import("./markdownlint.mjs").ResolveConfigExtendsCallback} ResolveConfigExtendsCallback */
/** @typedef {import("./markdownlint.mjs").Rule} Rule */
/** @typedef {import("./markdownlint.mjs").RuleConfiguration} RuleConfiguration */
/** @typedef {import("./markdownlint.mjs").RuleFunction} RuleFunction */
/** @typedef {import("./markdownlint.mjs").RuleOnError} RuleOnError */
/** @typedef {import("./markdownlint.mjs").RuleOnErrorFixInfo} RuleOnErrorFixInfo */
/** @typedef {import("./markdownlint.mjs").RuleOnErrorInfo} RuleOnErrorInfo */
/** @typedef {import("./markdownlint.mjs").RuleParams} RuleParams */
+170
View File
@@ -0,0 +1,170 @@
// @ts-check
"use strict";
const { newLineRe } = require("../helpers");
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529
/** @typedef {import("markdownlint").MarkdownIt} MarkdownIt */
/** @typedef {import("markdownlint").MarkdownItToken} MarkdownItToken */
/** @typedef {import("markdownlint").Plugin} Plugin */
/**
* @callback InlineCodeSpanCallback
* @param {string} code Code content.
* @param {number} lineIndex Line index (0-based).
* @param {number} columnIndex Column index (0-based).
* @param {number} ticks Count of backticks.
* @returns {void}
*/
/**
* Calls the provided function for each inline code span's content.
*
* @param {string} input Markdown content.
* @param {InlineCodeSpanCallback} handler Callback function taking (code,
* lineIndex, columnIndex, ticks).
* @returns {void}
*/
function forEachInlineCodeSpan(input, handler) {
const backtickRe = /`+/g;
let match = null;
const backticksLengthAndIndex = [];
while ((match = backtickRe.exec(input)) !== null) {
backticksLengthAndIndex.push([ match[0].length, match.index ]);
}
const newLinesIndex = [];
while ((match = newLineRe.exec(input)) !== null) {
newLinesIndex.push(match.index);
}
let lineIndex = 0;
let lineStartIndex = 0;
let k = 0;
for (let i = 0; i < backticksLengthAndIndex.length - 1; i++) {
const [ startLength, startIndex ] = backticksLengthAndIndex[i];
if ((startIndex === 0) || (input[startIndex - 1] !== "\\")) {
for (let j = i + 1; j < backticksLengthAndIndex.length; j++) {
const [ endLength, endIndex ] = backticksLengthAndIndex[j];
if (startLength === endLength) {
for (; k < newLinesIndex.length; k++) {
const newLineIndex = newLinesIndex[k];
if (startIndex < newLineIndex) {
break;
}
lineIndex++;
lineStartIndex = newLineIndex + 1;
}
const columnIndex = startIndex - lineStartIndex + startLength;
handler(
input.slice(startIndex + startLength, endIndex),
lineIndex,
columnIndex,
startLength
);
i = j;
break;
}
}
}
}
}
/**
* Freeze all freeze-able members of a token and its children.
*
* @param {MarkdownItToken} token A markdown-it token.
* @returns {void}
*/
function freezeToken(token) {
if (token.attrs) {
for (const attr of token.attrs) {
Object.freeze(attr);
}
Object.freeze(token.attrs);
}
if (token.children) {
for (const child of token.children) {
freezeToken(child);
}
Object.freeze(token.children);
}
if (token.map) {
Object.freeze(token.map);
}
Object.freeze(token);
}
/**
* Annotate tokens with line/lineNumber and freeze them.
*
* @param {import("markdown-it").Token[]} tokens Array of markdown-it tokens.
* @param {string[]} lines Lines of Markdown content.
* @returns {void}
*/
function annotateAndFreezeTokens(tokens, lines) {
let trMap = null;
/** @type {MarkdownItToken[]} */
// @ts-ignore
const markdownItTokens = tokens;
for (const token of markdownItTokens) {
// Provide missing maps for table content
if (token.type === "tr_open") {
trMap = token.map;
} else if (token.type === "tr_close") {
trMap = null;
}
if (!token.map && trMap) {
token.map = [ ...trMap ];
}
// Update token metadata
if (token.map) {
token.line = lines[token.map[0]];
token.lineNumber = token.map[0] + 1;
// Trim bottom of token to exclude whitespace lines
while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
token.map[1]--;
}
}
// Annotate children with lineNumber
if (token.children) {
/** @type {number[]} */
const codeSpanExtraLines = [];
if (token.children.some((child) => child.type === "code_inline")) {
forEachInlineCodeSpan(token.content, (code) => {
codeSpanExtraLines.push(code.split(newLineRe).length - 1);
});
}
let lineNumber = token.lineNumber;
for (const child of token.children) {
child.lineNumber = lineNumber;
child.line = lines[lineNumber - 1];
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
lineNumber++;
} else if (child.type === "code_inline") {
lineNumber += codeSpanExtraLines.shift() || 0;
}
}
}
freezeToken(token);
}
Object.freeze(tokens);
}
/**
* Gets an array of markdown-it tokens for the input.
*
* @param {MarkdownIt} markdownIt Instance of the markdown-it parser.
* @param {string} content Markdown content.
* @param {string[]} lines Lines of Markdown content.
* @returns {MarkdownItToken[]} Array of markdown-it tokens.
*/
function getMarkdownItTokens(markdownIt, content, lines) {
const tokens = markdownIt.parse(content, {});
annotateAndFreezeTokens(tokens, lines);
return tokens;
};
module.exports = {
forEachInlineCodeSpan,
getMarkdownItTokens
};
+600
View File
@@ -0,0 +1,600 @@
/**
* Lint specified Markdown files.
*
* @param {Options | null} options Configuration options.
* @param {LintCallback} callback Callback (err, result) function.
* @returns {void}
*/
export function lintAsync(options: Options | null, callback: LintCallback): void;
/**
* Lint specified Markdown files.
*
* @param {Options | null} options Configuration options.
* @returns {Promise<LintResults>} Results object.
*/
export function lintPromise(options: Options | null): Promise<LintResults>;
/**
* Lint specified Markdown files.
*
* @param {Options | null} options Configuration options.
* @returns {LintResults} Results object.
*/
export function lintSync(options: Options | null): LintResults;
/**
* Extend specified configuration object.
*
* @param {Configuration} config Configuration object.
* @param {string} file Configuration file name.
* @param {ConfigurationParser[] | undefined} parsers Parsing function(s).
* @param {FsLike} fs File system implementation.
* @returns {Promise<Configuration>} Configuration object.
*/
export function extendConfigPromise(config: Configuration, file: string, parsers: ConfigurationParser[] | undefined, fs: FsLike): Promise<Configuration>;
/**
* Read specified configuration file.
*
* @param {string} file Configuration file name.
* @param {ConfigurationParser[] | ReadConfigCallback} [parsers] Parsing function(s).
* @param {FsLike | ReadConfigCallback} [fs] File system implementation.
* @param {ReadConfigCallback} [callback] Callback (err, result) function.
* @returns {void}
*/
export function readConfigAsync(file: string, parsers?: ConfigurationParser[] | ReadConfigCallback, fs?: FsLike | ReadConfigCallback, callback?: ReadConfigCallback): void;
/**
* Read specified configuration file.
*
* @param {string} file Configuration file name.
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
* @param {FsLike} [fs] File system implementation.
* @returns {Promise<Configuration>} Configuration object.
*/
export function readConfigPromise(file: string, parsers?: ConfigurationParser[], fs?: FsLike): Promise<Configuration>;
/**
* Read specified configuration file.
*
* @param {string} file Configuration file name.
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
* @param {FsLike} [fs] File system implementation.
* @returns {Configuration} Configuration object.
*/
export function readConfigSync(file: string, parsers?: ConfigurationParser[], fs?: FsLike): Configuration;
/**
* Applies the specified fix to a Markdown content line.
*
* @param {string} line Line of Markdown content.
* @param {FixInfo} fixInfo FixInfo instance.
* @param {string} [lineEnding] Line ending to use.
* @returns {string | null} Fixed content or null if deleted.
*/
export function applyFix(line: string, fixInfo: FixInfo, lineEnding?: string): string | null;
/**
* Applies as many of the specified fixes as possible to Markdown content.
*
* @param {string} input Lines of Markdown content.
* @param {LintError[]} errors LintError instances.
* @returns {string} Fixed content.
*/
export function applyFixes(input: string, errors: LintError[]): string;
/**
* Gets the (semantic) version of the library.
*
* @returns {string} SemVer string.
*/
export function getVersion(): string;
/**
* Result object for removeFrontMatter.
*/
export type RemoveFrontMatterResult = {
/**
* Markdown content.
*/
content: string;
/**
* Front matter lines.
*/
frontMatterLines: string[];
};
/**
* Result object for getEffectiveConfig.
*/
export type GetEffectiveConfigResult = {
/**
* Effective configuration.
*/
effectiveConfig: Configuration;
/**
* Rules enabled.
*/
rulesEnabled: Map<string, boolean>;
/**
* Rules severity.
*/
rulesSeverity: Map<string, "error" | "warning">;
};
/**
* Result object for getEnabledRulesPerLineNumber.
*/
export type EnabledRulesPerLineNumberResult = {
/**
* Effective configuration.
*/
effectiveConfig: Configuration;
/**
* Enabled rules per line number.
*/
enabledRulesPerLineNumber: Map<string, boolean>[];
/**
* Enabled rule list.
*/
enabledRuleList: Rule[];
/**
* Rules severity.
*/
rulesSeverity: Map<string, "error" | "warning">;
};
/**
* Node fs instance (or compatible object).
*/
export type FsLike = {
/**
* access method.
*/
access: (path: string, callback: (err: Error) => void) => void;
/**
* accessSync method.
*/
accessSync: (path: string) => void;
/**
* readFile method.
*/
readFile: (path: string, encoding: string, callback: (err: Error, data: string) => void) => void;
/**
* readFileSync method.
*/
readFileSync: (path: string, encoding: string) => string;
};
/**
* Function to implement rule logic.
*/
export type RuleFunction = (params: RuleParams, onError: RuleOnError) => void;
/**
* Rule parameters.
*/
export type RuleParams = {
/**
* File/string name.
*/
name: string;
/**
* Markdown parser data.
*/
parsers: MarkdownParsers;
/**
* File/string lines.
*/
lines: readonly string[];
/**
* Front matter lines.
*/
frontMatterLines: readonly string[];
/**
* Rule configuration.
*/
config: RuleConfiguration;
/**
* Version of the markdownlint library.
*/
version: string;
};
/**
* Markdown parser data.
*/
export type MarkdownParsers = {
/**
* Markdown parser data from markdown-it (only present when Rule.parser is "markdownit").
*/
markdownit: ParserMarkdownIt;
/**
* Markdown parser data from micromark (only present when Rule.parser is "micromark").
*/
micromark: ParserMicromark;
};
/**
* Markdown parser data from markdown-it.
*/
export type ParserMarkdownIt = {
/**
* Token objects from markdown-it.
*/
tokens: MarkdownItToken[];
};
/**
* Markdown parser data from micromark.
*/
export type ParserMicromark = {
/**
* Token objects from micromark.
*/
tokens: MicromarkToken[];
};
/**
* markdown-it token.
*/
export type MarkdownItToken = {
/**
* HTML attributes.
*/
attrs: string[][];
/**
* Block-level token.
*/
block: boolean;
/**
* Child nodes.
*/
children: MarkdownItToken[];
/**
* Tag contents.
*/
content: string;
/**
* Ignore element.
*/
hidden: boolean;
/**
* Fence info.
*/
info: string;
/**
* Nesting level.
*/
level: number;
/**
* Beginning/ending line numbers.
*/
map: number[];
/**
* Markup text.
*/
markup: string;
/**
* Arbitrary data.
*/
meta: any;
/**
* Level change.
*/
nesting: number;
/**
* HTML tag name.
*/
tag: string;
/**
* Token type.
*/
type: string;
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Line content.
*/
line: string;
};
export type MicromarkTokenType = import("micromark-util-types").TokenType;
/**
* micromark token.
*/
export type MicromarkToken = {
/**
* Token type.
*/
type: MicromarkTokenType;
/**
* Start line (1-based).
*/
startLine: number;
/**
* Start column (1-based).
*/
startColumn: number;
/**
* End line (1-based).
*/
endLine: number;
/**
* End column (1-based).
*/
endColumn: number;
/**
* Token text.
*/
text: string;
/**
* Child tokens.
*/
children: MicromarkToken[];
/**
* Parent token.
*/
parent: MicromarkToken | null;
};
/**
* Error-reporting callback.
*/
export type RuleOnError = (onErrorInfo: RuleOnErrorInfo) => void;
/**
* Fix information for RuleOnError callback.
*/
export type RuleOnErrorInfo = {
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Detail about the error.
*/
detail?: string;
/**
* Context for the error.
*/
context?: string;
/**
* Link to more information.
*/
information?: URL;
/**
* Column number (1-based) and length.
*/
range?: number[];
/**
* Fix information.
*/
fixInfo?: RuleOnErrorFixInfo;
};
/**
* Fix information for RuleOnErrorInfo.
*/
export type RuleOnErrorFixInfo = {
/**
* Line number (1-based).
*/
lineNumber?: number;
/**
* Column of the fix (1-based).
*/
editColumn?: number;
/**
* Count of characters to delete.
*/
deleteCount?: number;
/**
* Text to insert (after deleting).
*/
insertText?: string;
};
/**
* Rule definition.
*/
export type Rule = {
/**
* Rule name(s).
*/
names: string[];
/**
* Rule description.
*/
description: string;
/**
* Link to more information.
*/
information?: URL;
/**
* Rule tag(s).
*/
tags: string[];
/**
* Parser used.
*/
parser: "markdownit" | "micromark" | "none";
/**
* True if asynchronous.
*/
asynchronous?: boolean;
/**
* Rule implementation.
*/
function: RuleFunction;
};
/**
* Method used by the markdown-it parser to parse input.
*/
export type MarkdownItParse = (src: string, env: any) => any[];
/**
* Instance of the markdown-it parser.
*/
export type MarkdownIt = {
/**
* Method to parse input.
*/
parse: MarkdownItParse;
};
/**
* Gets an instance of the markdown-it parser. Any plugins should already have been loaded.
*/
export type MarkdownItFactory = () => MarkdownIt | Promise<MarkdownIt>;
/**
* Configuration options.
*/
export type Options = {
/**
* Configuration object.
*/
config?: Configuration;
/**
* Configuration parsers.
*/
configParsers?: ConfigurationParser[];
/**
* Custom rules.
*/
customRules?: Rule[] | Rule;
/**
* Files to lint.
*/
files?: string[] | string;
/**
* Front matter pattern.
*/
frontMatter?: RegExp | null;
/**
* File system implementation.
*/
fs?: FsLike;
/**
* True to catch exceptions.
*/
handleRuleFailures?: boolean;
/**
* Function to create a markdown-it parser.
*/
markdownItFactory?: MarkdownItFactory;
/**
* True to ignore HTML directives.
*/
noInlineConfig?: boolean;
/**
* Strings to lint.
*/
strings?: {
[x: string]: string;
};
};
/**
* A markdown-it plugin.
*/
export type Plugin = any[];
/**
* Lint results.
*/
export type LintResults = {
[x: string]: LintError[];
};
/**
* Lint error.
*/
export type LintError = {
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Rule name(s).
*/
ruleNames: string[];
/**
* Rule description.
*/
ruleDescription: string;
/**
* Link to more information.
*/
ruleInformation: string | null;
/**
* Detail about the error.
*/
errorDetail: string | null;
/**
* Context for the error.
*/
errorContext: string | null;
/**
* Column number (1-based) and length.
*/
errorRange: number[] | null;
/**
* Fix information.
*/
fixInfo: FixInfo | null;
/**
* Severity of the error.
*/
severity: "error" | "warning";
};
/**
* Fix information.
*/
export type FixInfo = {
/**
* Line number (1-based).
*/
lineNumber?: number;
/**
* Column of the fix (1-based).
*/
editColumn?: number;
/**
* Count of characters to delete.
*/
deleteCount?: number;
/**
* Text to insert (after deleting).
*/
insertText?: string;
};
/**
* FixInfo with all optional properties present.
*/
export type FixInfoNormalized = {
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Column of the fix (1-based).
*/
editColumn: number;
/**
* Count of characters to delete.
*/
deleteCount: number;
/**
* Text to insert (after deleting).
*/
insertText: string;
};
/**
* Called with the result of linting a string or document.
*/
export type LintContentCallback = (error: Error | null, result?: LintError[]) => void;
/**
* Called with the result of the lint function.
*/
export type LintCallback = (error: Error | null, results?: LintResults) => void;
/**
* Configuration object for linting rules. For the JSON schema, see
* {@link ../schema/markdownlint-config-schema.json}.
*/
export type Configuration = import("./configuration.d.ts").Configuration;
/**
* Configuration object for linting rules strictly. For the JSON schema, see
* {@link ../schema/markdownlint-config-schema-strict.json}.
*/
export type ConfigurationStrict = import("./configuration-strict.d.ts").ConfigurationStrict;
/**
* Rule configuration.
*/
export type RuleConfiguration = boolean | any;
/**
* Parses a configuration string and returns a configuration object.
*/
export type ConfigurationParser = (text: string) => Configuration;
/**
* Called with the result of the readConfig function.
*/
export type ReadConfigCallback = (err: Error | null, config?: Configuration) => void;
/**
* Called with the result of the resolveConfigExtends function.
*/
export type ResolveConfigExtendsCallback = (err: Error | null, path?: string) => void;
+1660
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
import { addErrorDetailIf, frontMatterHasTitle } from "../helpers/helpers.cjs";
import { getHeadingLevel } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD001", "heading-increment" ],
"description": "Heading levels should only increment by one level at a time",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD001(params, onError) {
const hasTitle = frontMatterHasTitle(
params.frontMatterLines,
params.config.front_matter_title
);
let prevLevel = hasTitle ? 1 : Number.MAX_SAFE_INTEGER;
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const level = getHeadingLevel(heading);
if (level > prevLevel) {
addErrorDetailIf(
onError,
heading.startLine,
`h${prevLevel + 1}`,
`h${level}`
);
}
prevLevel = level;
}
}
};
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getHeadingLevel, getHeadingStyle } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD003", "heading-style" ],
"description": "Heading style",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD003(params, onError) {
let style = String(params.config.style || "consistent");
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const styleForToken = getHeadingStyle(heading);
if (style === "consistent") {
style = styleForToken;
}
if (styleForToken !== style) {
const h12 = getHeadingLevel(heading) <= 2;
const setextWithAtx =
(style === "setext_with_atx") &&
((h12 && (styleForToken === "setext")) ||
(!h12 && (styleForToken === "atx")));
const setextWithAtxClosed =
(style === "setext_with_atx_closed") &&
((h12 && (styleForToken === "setext")) ||
(!h12 && (styleForToken === "atx_closed")));
if (!setextWithAtx && !setextWithAtxClosed) {
let expected = style;
if (style === "setext_with_atx") {
expected = h12 ? "setext" : "atx";
} else if (style === "setext_with_atx_closed") {
expected = h12 ? "setext" : "atx_closed";
}
addErrorDetailIf(
onError,
heading.startLine,
expected,
styleForToken
);
}
}
}
}
};
+72
View File
@@ -0,0 +1,72 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getDescendantsByType, getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const markerToStyle = (/** @type {string} */ marker) => (marker === "-") ? "dash" : ((marker === "+") ? "plus" : "asterisk");
const styleToMarker = (/** @type {string} */ style) => (style === "dash") ? "-" : ((style === "plus") ? "+" : "*");
const differentItemStyle = (/** @type {string} */ style) => (style === "dash") ? "plus" : ((style === "plus") ? "asterisk" : "dash");
const validStyles = new Set([
"asterisk",
"consistent",
"dash",
"plus",
"sublist"
]);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD004", "ul-style" ],
"description": "Unordered list style",
"tags": [ "bullet", "ul" ],
"parser": "micromark",
"function": function MD004(params, onError) {
const style = String(params.config.style || "consistent");
let expectedStyle = validStyles.has(style) ? style : "dash";
/** @type {("asterisk"|"dash"|"plus")[]} */
const nestingStyles = [];
for (const listUnordered of filterByTypesCached([ "listUnordered" ])) {
let nesting = 0;
if (style === "sublist") {
/** @type {import("markdownlint").MicromarkToken | null} */
let parent = listUnordered;
// @ts-ignore
while ((parent = getParentOfType(parent, [ "listOrdered", "listUnordered" ]))) {
nesting++;
}
}
const listItemMarkers = getDescendantsByType(listUnordered, [ "listItemPrefix", "listItemMarker" ]);
for (const listItemMarker of listItemMarkers) {
const itemStyle = markerToStyle(listItemMarker.text);
if (style === "sublist") {
if (!nestingStyles[nesting]) {
nestingStyles[nesting] =
(itemStyle === nestingStyles[nesting - 1]) ?
differentItemStyle(itemStyle) :
itemStyle;
}
expectedStyle = nestingStyles[nesting];
} else if (expectedStyle === "consistent") {
expectedStyle = itemStyle;
}
const column = listItemMarker.startColumn;
const length = listItemMarker.endColumn - listItemMarker.startColumn;
addErrorDetailIf(
onError,
listItemMarker.startLine,
expectedStyle,
itemStyle,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": styleToMarker(expectedStyle)
}
);
}
}
}
};
+69
View File
@@ -0,0 +1,69 @@
// @ts-check
import { addError, addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD005", "list-indent" ],
"description": "Inconsistent indentation for list items at the same level",
"tags": [ "bullet", "ul", "indentation" ],
"parser": "micromark",
"function": function MD005(params, onError) {
for (const list of filterByTypesCached([ "listOrdered", "listUnordered" ])) {
const expectedIndent = list.startColumn - 1;
let expectedEnd = 0;
let endMatching = false;
const listItemPrefixes =
list.children.filter((token) => (token.type === "listItemPrefix"));
for (const listItemPrefix of listItemPrefixes) {
const lineNumber = listItemPrefix.startLine;
const actualIndent = listItemPrefix.startColumn - 1;
const range = [ 1, listItemPrefix.endColumn - 1 ];
if (list.type === "listUnordered") {
addErrorDetailIf(
onError,
lineNumber,
expectedIndent,
actualIndent,
undefined,
undefined,
range
// No fixInfo; MD007 handles this scenario better
);
} else {
const markerLength = listItemPrefix.text.trim().length;
const actualEnd = listItemPrefix.startColumn + markerLength - 1;
expectedEnd = expectedEnd || actualEnd;
if ((expectedIndent !== actualIndent) || endMatching) {
if (expectedEnd === actualEnd) {
endMatching = true;
} else {
const detail = endMatching ?
`Expected: (${expectedEnd}); Actual: (${actualEnd})` :
`Expected: ${expectedIndent}; Actual: ${actualIndent}`;
const expected = endMatching ?
expectedEnd - markerLength :
expectedIndent;
const actual = endMatching ?
actualEnd - markerLength :
actualIndent;
addError(
onError,
lineNumber,
detail,
undefined,
range,
{
"editColumn": Math.min(actual, expected) + 1,
"deleteCount": Math.max(actual - expected, 0),
"insertText": "".padEnd(Math.max(expected - actual, 0))
}
);
}
}
}
}
}
}
};
+84
View File
@@ -0,0 +1,84 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("micromark-util-types").TokenType[]} */
const unorderedListTypes =
[ "blockQuotePrefix", "listItemPrefix", "listUnordered" ];
/** @type {import("micromark-util-types").TokenType[]} */
const unorderedParentTypes =
[ "blockQuote", "listOrdered", "listUnordered" ];
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD007", "ul-indent" ],
"description": "Unordered list indentation",
"tags": [ "bullet", "ul", "indentation" ],
"parser": "micromark",
"function": function MD007(params, onError) {
const indent = Number(params.config.indent || 2);
const startIndented = !!params.config.start_indented;
const startIndent = Number(params.config.start_indent || indent);
const unorderedListNesting = new Map();
let lastBlockQuotePrefix = null;
const tokens = filterByTypesCached(unorderedListTypes);
for (const token of tokens) {
const { endColumn, parent, startColumn, startLine, type } = token;
if (type === "blockQuotePrefix") {
lastBlockQuotePrefix = token;
} else if (type === "listUnordered") {
let nesting = 0;
/** @type {import("markdownlint").MicromarkToken | null} */
let current = token;
while (
// @ts-ignore
(current = getParentOfType(current, unorderedParentTypes))
) {
if (current.type === "listUnordered") {
nesting++;
// eslint-disable-next-line no-continue
continue;
} else if (current.type === "listOrdered") {
nesting = -1;
}
break;
}
if (nesting >= 0) {
unorderedListNesting.set(token, nesting);
}
} else {
// listItemPrefix
const nesting = unorderedListNesting.get(parent);
if (nesting !== undefined) {
// listItemPrefix for listUnordered
const baseIndent = (getParentOfType(token, [ "gfmFootnoteDefinition" ])) ? 4 : 0;
const expectedIndent =
baseIndent + (startIndented ? startIndent : 0) + (nesting * indent);
const blockQuoteAdjustment =
(lastBlockQuotePrefix?.endLine === startLine) ?
(lastBlockQuotePrefix.endColumn - 1) :
0;
const actualIndent = startColumn - 1 - blockQuoteAdjustment;
const range = [ 1, endColumn - 1 ];
const fixInfo = {
"editColumn": startColumn - actualIndent,
"deleteCount": Math.max(actualIndent - expectedIndent, 0),
"insertText": "".padEnd(Math.max(expectedIndent - actualIndent, 0))
};
addErrorDetailIf(
onError,
startLine,
expectedIndent,
actualIndent,
undefined,
undefined,
range,
fixInfo
);
}
}
}
}
};
+96
View File
@@ -0,0 +1,96 @@
// @ts-check
import { addError } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces",
"tags": [ "whitespace" ],
"parser": "micromark",
"function": function MD009(params, onError) {
let brSpaces = params.config.br_spaces;
brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces);
const codeBlocks = params.config.code_blocks;
const includeCode = (codeBlocks === undefined) ? false : !!codeBlocks;
const listItemEmptyLines = !!params.config.list_item_empty_lines;
const strict = !!params.config.strict;
const codeBlockLineNumbers = new Set();
if (!includeCode) {
for (const codeBlock of filterByTypesCached([ "codeFenced" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine + 1, codeBlock.endLine - 1);
}
for (const codeBlock of filterByTypesCached([ "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
}
const listItemLineNumbers = new Set();
if (listItemEmptyLines) {
for (const listBlock of filterByTypesCached([ "listOrdered", "listUnordered" ])) {
addRangeToSet(listItemLineNumbers, listBlock.startLine, listBlock.endLine);
let trailingIndent = true;
for (let i = listBlock.children.length - 1; i >= 0; i--) {
const child = listBlock.children[i];
switch (child.type) {
case "content":
trailingIndent = false;
break;
case "listItemIndent":
if (trailingIndent) {
listItemLineNumbers.delete(child.startLine);
}
break;
case "listItemPrefix":
trailingIndent = true;
break;
default:
break;
}
}
}
}
const paragraphLineNumbers = new Set();
const codeInlineLineNumbers = new Set();
if (strict) {
for (const paragraph of filterByTypesCached([ "paragraph" ])) {
addRangeToSet(paragraphLineNumbers, paragraph.startLine, paragraph.endLine - 1);
}
for (const codeText of filterByTypesCached([ "codeText" ])) {
addRangeToSet(codeInlineLineNumbers, codeText.startLine, codeText.endLine - 1);
}
}
const expected = (brSpaces < 2) ? 0 : brSpaces;
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1;
const trailingSpaces = line.length - line.trimEnd().length;
if (
trailingSpaces &&
!codeBlockLineNumbers.has(lineNumber) &&
!listItemLineNumbers.has(lineNumber) &&
(
(expected !== trailingSpaces) ||
(strict &&
(!paragraphLineNumbers.has(lineNumber) ||
codeInlineLineNumbers.has(lineNumber)))
)
) {
const column = line.length - trailingSpaces + 1;
addError(
onError,
lineNumber,
"Expected: " + (expected === 0 ? "" : "0 or ") +
expected + "; Actual: " + trailingSpaces,
undefined,
[ column, trailingSpaces ],
{
"editColumn": column,
"deleteCount": trailingSpaces
}
);
}
}
}
};
+78
View File
@@ -0,0 +1,78 @@
// @ts-check
import { addError, hasOverlap } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const tabRe = /\t+/g;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD010", "no-hard-tabs" ],
"description": "Hard tabs",
"tags": [ "whitespace", "hard_tab" ],
"parser": "micromark",
"function": function MD010(params, onError) {
const codeBlocks = params.config.code_blocks;
const includeCode = (codeBlocks === undefined) ? true : !!codeBlocks;
const ignoreCodeLanguages = new Set(
(params.config.ignore_code_languages || [])
.map((/** @type {void} */ language) => String(language).toLowerCase())
);
const spacesPerTab = params.config.spaces_per_tab;
const spaceMultiplier = (spacesPerTab === undefined) ?
1 :
Math.max(0, Number(spacesPerTab));
/** @type {import("markdownlint").MicromarkTokenType[]} */
const exclusionTypes = [];
if (includeCode) {
if (ignoreCodeLanguages.size > 0) {
exclusionTypes.push("codeFenced");
}
} else {
exclusionTypes.push("codeFenced", "codeIndented", "codeText");
}
const codeTokens = filterByTypesCached(exclusionTypes).filter((token) => {
if ((token.type === "codeFenced") && (ignoreCodeLanguages.size > 0)) {
const fenceInfos = getDescendantsByType(token, [ "codeFencedFence", "codeFencedFenceInfo" ]);
return fenceInfos.every((fenceInfo) => ignoreCodeLanguages.has(fenceInfo.text.toLowerCase()));
}
return true;
});
const codeRanges = codeTokens.map((token) => {
const { type, startLine, startColumn, endLine, endColumn } = token;
const codeFenced = (type === "codeFenced");
return {
"startLine": startLine + (codeFenced ? 1 : 0),
"startColumn": codeFenced ? 0 : startColumn,
"endLine": endLine - (codeFenced ? 1 : 0),
"endColumn": codeFenced ? Number.MAX_SAFE_INTEGER : endColumn
};
});
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
let match = null;
while ((match = tabRe.exec(line)) !== null) {
const lineNumber = lineIndex + 1;
const column = match.index + 1;
const length = match[0].length;
/** @type {import("../helpers/helpers.cjs").FileRange} */
const range = { "startLine": lineNumber, "startColumn": column, "endLine": lineNumber, "endColumn": column + length - 1 };
if (!codeRanges.some((codeRange) => hasOverlap(codeRange, range))) {
addError(
onError,
lineNumber,
"Column: " + column,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": "".padEnd(length * spaceMultiplier)
}
);
}
}
}
}
};
+56
View File
@@ -0,0 +1,56 @@
// @ts-check
import { addError, hasOverlap } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("micromark-extension-math")} */
const reversedLinkRe = /(^|[^\\])\(([^()]+)\)\[([^\]^][^\]]*)\](?!\()/g;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD011", "no-reversed-links" ],
"description": "Reversed link syntax",
"tags": [ "links" ],
"parser": "micromark",
"function": function MD011(params, onError) {
const ignoreBlockLineNumbers = new Set();
for (const ignoreBlock of filterByTypesCached([ "codeFenced", "codeIndented", "mathFlow" ])) {
addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine);
}
const ignoreTexts = filterByTypesCached([ "codeText", "mathText" ]);
for (const [ lineIndex, line ] of params.lines.entries()) {
const lineNumber = lineIndex + 1;
if (!ignoreBlockLineNumbers.has(lineNumber)) {
let match = null;
while ((match = reversedLinkRe.exec(line)) !== null) {
const [ reversedLink, preChar, linkText, linkDestination ] = match;
if (
!linkText.endsWith("\\") &&
!linkDestination.endsWith("\\")
) {
const column = match.index + preChar.length + 1;
const length = match[0].length - preChar.length;
/** @type {import("../helpers/helpers.cjs").FileRange} */
const range = { "startLine": lineNumber, "startColumn": column, "endLine": lineNumber, "endColumn": column + length - 1 };
if (!ignoreTexts.some((ignoreText) => hasOverlap(ignoreText, range))) {
addError(
onError,
lineNumber,
reversedLink.slice(preChar.length),
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": `[${linkText}](${linkDestination})`
}
);
}
}
}
}
}
}
};
+40
View File
@@ -0,0 +1,40 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD012", "no-multiple-blanks" ],
"description": "Multiple consecutive blank lines",
"tags": [ "whitespace", "blank_lines" ],
"parser": "micromark",
"function": function MD012(params, onError) {
const maximum = Number(params.config.maximum || 1);
const { lines } = params;
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
let count = 0;
for (const [ lineIndex, line ] of lines.entries()) {
const inCode = codeBlockLineNumbers.has(lineIndex + 1);
count = (inCode || (line.trim().length > 0)) ? 0 : count + 1;
if (maximum < count) {
addErrorDetailIf(
onError,
lineIndex + 1,
maximum,
count,
undefined,
undefined,
undefined,
{
"deleteCount": -1
}
);
}
}
}
};
+91
View File
@@ -0,0 +1,91 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached, getReferenceLinkImageData } from "./cache.mjs";
import { addRangeToSet, getDescendantsByType } from "../helpers/micromark-helpers.cjs";
// Regular expression for a line that is not wrappable
const notWrappableRe = /^(?:[#>\s]*\s)?\S*$/;
/** @typedef {import("micromark-extension-gfm-autolink-literal")} */
/** @typedef {import("micromark-extension-gfm-table")} */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD013", "line-length" ],
"description": "Line length",
"tags": [ "line_length" ],
"parser": "micromark",
"function": function MD013(params, onError) {
const lineLength = Number(params.config.line_length || 80);
const headingLineLength = Number(params.config.heading_line_length || lineLength);
const codeLineLength = Number(params.config.code_block_line_length || lineLength);
const strict = !!params.config.strict;
const stern = !!params.config.stern;
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
const tables = params.config.tables;
const includeTables = (tables === undefined) ? true : !!tables;
const headings = params.config.headings;
const includeHeadings = (headings === undefined) ? true : !!headings;
const headingLineNumbers = new Set();
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
addRangeToSet(headingLineNumbers, heading.startLine, heading.endLine);
}
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
const tableLineNumbers = new Set();
for (const table of filterByTypesCached([ "table" ])) {
addRangeToSet(tableLineNumbers, table.startLine, table.endLine);
}
const linkLineNumbers = new Set();
for (const link of filterByTypesCached([ "autolink", "image", "link", "literalAutolink" ])) {
addRangeToSet(linkLineNumbers, link.startLine, link.endLine);
}
const paragraphDataLineNumbers = new Set();
for (const paragraph of filterByTypesCached([ "paragraph" ])) {
for (const data of getDescendantsByType(paragraph, [ "data" ])) {
addRangeToSet(paragraphDataLineNumbers, data.startLine, data.endLine);
}
}
const linkOnlyLineNumbers = new Set();
for (const lineNumber of linkLineNumbers) {
if (!paragraphDataLineNumbers.has(lineNumber)) {
linkOnlyLineNumbers.add(lineNumber);
}
}
const definitionLineIndices = new Set(getReferenceLinkImageData().definitionLineIndices);
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1;
const isHeading = headingLineNumbers.has(lineNumber);
const inCode = codeBlockLineNumbers.has(lineNumber);
const inTable = tableLineNumbers.has(lineNumber);
const maxLength = inCode ? codeLineLength : (isHeading ? headingLineLength : lineLength);
// If not strict/stern, the last run of non-whitespace is allowed to go
// beyond the limit as long as it begins within the limit
const text = (strict || stern) ? line : line.replace(/\S*$/u, "#");
if ((maxLength > 0) &&
(includeCodeBlocks || !inCode) &&
(includeTables || !inTable) &&
(includeHeadings || !isHeading) &&
!definitionLineIndices.has(lineIndex) &&
(strict ||
(!(stern && notWrappableRe.test(line)) &&
!linkOnlyLineNumbers.has(lineNumber))) &&
(text.length > maxLength)) {
addErrorDetailIf(
onError,
lineNumber,
maxLength,
line.length,
undefined,
undefined,
[ maxLength + 1, line.length - maxLength ]
);
}
}
}
};
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const dollarCommandRe = /^(\s*)(\$\s+)/;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD014", "commands-show-output" ],
"description": "Dollar signs used before commands without showing output",
"tags": [ "code" ],
"parser": "micromark",
"function": function MD014(params, onError) {
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
const codeFlowValues = codeBlock.children.filter((child) => child.type === "codeFlowValue");
const dollarMatches = codeFlowValues
.map((codeFlowValue) => ({
"result": codeFlowValue.text.match(dollarCommandRe),
"startColumn": codeFlowValue.startColumn,
"startLine": codeFlowValue.startLine,
"text": codeFlowValue.text
}))
.filter((dollarMatch) => dollarMatch.result);
if (dollarMatches.length === codeFlowValues.length) {
for (const dollarMatch of dollarMatches) {
// @ts-ignore
const column = dollarMatch.startColumn + dollarMatch.result[1].length;
// @ts-ignore
const length = dollarMatch.result[2].length;
addErrorContext(
onError,
dollarMatch.startLine,
dollarMatch.text,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
}
}
};
+43
View File
@@ -0,0 +1,43 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD018", "no-missing-space-atx" ],
"description": "No space after hash on atx style heading",
"tags": [ "headings", "atx", "spaces" ],
"parser": "micromark",
"function": function MD018(params, onError) {
const { lines } = params;
const ignoreBlockLineNumbers = new Set();
for (const ignoreBlock of filterByTypesCached([ "codeFenced", "codeIndented", "htmlFlow" ])) {
addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine);
}
for (const [ lineIndex, line ] of lines.entries()) {
if (
!ignoreBlockLineNumbers.has(lineIndex + 1) &&
/^#+[^# \t]/.test(line) &&
!/#\s*$/.test(line) &&
!line.startsWith("#️⃣")
) {
// @ts-ignore
const hashCount = /^#+/.exec(line)[0].length;
addErrorContext(
onError,
lineIndex + 1,
line.trim(),
undefined,
undefined,
[ 1, hashCount + 1 ],
{
"editColumn": hashCount + 1,
"insertText": " "
}
);
}
}
}
};
+77
View File
@@ -0,0 +1,77 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getHeadingStyle } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/**
* Validate heading sequence and whitespace length at start or end.
*
* @param {import("markdownlint").RuleOnError} onError Error-reporting callback.
* @param {import("markdownlint").MicromarkToken} heading ATX heading token.
* @param {number} delta Direction to scan.
* @returns {void}
*/
function validateHeadingSpaces(onError, heading, delta) {
const { children, startLine, text } = heading;
let index = (delta > 0) ? 0 : (children.length - 1);
while (
children[index] &&
(children[index].type !== "atxHeadingSequence")
) {
index += delta;
}
const headingSequence = children[index];
const whitespace = children[index + delta];
if (
(headingSequence?.type === "atxHeadingSequence") &&
(whitespace?.type === "whitespace") &&
(whitespace.text.length > 1)
) {
const column = whitespace.startColumn + 1;
const length = whitespace.endColumn - column;
addErrorContext(
onError,
startLine,
text.trim(),
delta > 0,
delta < 0,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
/** @type {import("markdownlint").Rule[]} */
export default [
{
"names": [ "MD019", "no-multiple-space-atx" ],
"description": "Multiple spaces after hash on atx style heading",
"tags": [ "headings", "atx", "spaces" ],
"parser": "micromark",
"function": function MD019(params, onError) {
const atxHeadings = filterByTypesCached([ "atxHeading" ])
.filter((heading) => getHeadingStyle(heading) === "atx");
for (const atxHeading of atxHeadings) {
validateHeadingSpaces(onError, atxHeading, 1);
}
}
},
{
"names": [ "MD021", "no-multiple-space-closed-atx" ],
"description": "Multiple spaces inside hashes on closed atx style heading",
"tags": [ "headings", "atx_closed", "spaces" ],
"parser": "micromark",
"function": function MD021(params, onError) {
const atxClosedHeadings = filterByTypesCached([ "atxHeading" ])
.filter((heading) => getHeadingStyle(heading) === "atx_closed");
for (const atxClosedHeading of atxClosedHeadings) {
validateHeadingSpaces(onError, atxClosedHeading, 1);
validateHeadingSpaces(onError, atxClosedHeading, -1);
}
}
}
];
+68
View File
@@ -0,0 +1,68 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD020", "no-missing-space-closed-atx" ],
"description": "No space inside hashes on closed atx style heading",
"tags": [ "headings", "atx_closed", "spaces" ],
"parser": "micromark",
"function": function MD020(params, onError) {
const { lines } = params;
const ignoreBlockLineNumbers = new Set();
for (const ignoreBlock of filterByTypesCached([ "codeFenced", "codeIndented", "htmlFlow" ])) {
addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine);
}
for (const [ lineIndex, line ] of lines.entries()) {
if (!ignoreBlockLineNumbers.has(lineIndex + 1)) {
const match =
/^(#+)([ \t]*)([^# \t\\]|[^# \t][^#]*?[^# \t\\])([ \t]*)((?:\\#)?)(#+)(\s*)$/.exec(line);
if (match) {
const [
,
leftHash,
{ "length": leftSpaceLength },
content,
{ "length": rightSpaceLength },
rightEscape,
rightHash,
{ "length": trailSpaceLength }
] = match;
const leftHashLength = leftHash.length;
const rightHashLength = rightHash.length;
const left = !leftSpaceLength;
const right = !rightSpaceLength || !!rightEscape;
const rightEscapeReplacement = rightEscape ? `${rightEscape} ` : "";
if (left || right) {
const range = left ?
[
1,
leftHashLength + 1
] :
[
line.length - trailSpaceLength - rightHashLength,
rightHashLength + 1
];
addErrorContext(
onError,
lineIndex + 1,
line.trim(),
left,
right,
range,
{
"editColumn": 1,
"deleteCount": line.length,
"insertText":
`${leftHash} ${content} ${rightEscapeReplacement}${rightHash}`
}
);
}
}
}
}
}
};
+100
View File
@@ -0,0 +1,100 @@
// @ts-check
import { addErrorDetailIf, isBlankLine } from "../helpers/helpers.cjs";
import { getBlockQuotePrefixText, getHeadingLevel } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
const defaultLines = 1;
// eslint-disable-next-line jsdoc/reject-any-type
const getLinesFunction = (/** @type {any} */ linesParam) => {
if (Array.isArray(linesParam)) {
const linesArray = new Array(6).fill(defaultLines);
for (const [ index, value ] of [ ...linesParam.entries() ].slice(0, 6)) {
linesArray[index] = value;
}
return (/** @type {MicromarkToken} */ heading) => linesArray[getHeadingLevel(heading) - 1];
}
// Coerce linesParam to a number
const lines = (linesParam === undefined) ? defaultLines : Number(linesParam);
return () => lines;
};
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD022", "blanks-around-headings" ],
"description": "Headings should be surrounded by blank lines",
"tags": [ "headings", "blank_lines" ],
"parser": "micromark",
"function": function MD022(params, onError) {
const getLinesAbove = getLinesFunction(params.config.lines_above);
const getLinesBelow = getLinesFunction(params.config.lines_below);
const { lines } = params;
const blockQuotePrefixes = filterByTypesCached([ "blockQuotePrefix", "linePrefix" ]);
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const { startLine, endLine } = heading;
const line = lines[startLine - 1].trim();
// Check lines above
const linesAbove = getLinesAbove(heading);
if (linesAbove >= 0) {
let actualAbove = 0;
for (
let i = 0;
(i < linesAbove) && isBlankLine(lines[startLine - 2 - i]);
i++
) {
actualAbove++;
}
addErrorDetailIf(
onError,
startLine,
linesAbove,
actualAbove,
"Above",
line,
undefined,
{
"insertText": getBlockQuotePrefixText(
blockQuotePrefixes,
startLine - 1,
linesAbove - actualAbove
)
}
);
}
// Check lines below
const linesBelow = getLinesBelow(heading);
if (linesBelow >= 0) {
let actualBelow = 0;
for (
let i = 0;
(i < linesBelow) && isBlankLine(lines[endLine + i]);
i++
) {
actualBelow++;
}
addErrorDetailIf(
onError,
startLine,
linesBelow,
actualBelow,
"Below",
line,
undefined,
{
"lineNumber": endLine + 1,
"insertText": getBlockQuotePrefixText(
blockQuotePrefixes,
endLine + 1,
linesBelow - actualBelow
)
}
);
}
}
}
};
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD023", "heading-start-left" ],
"description": "Headings must start at the beginning of the line",
"tags": [ "headings", "spaces" ],
"parser": "micromark",
"function": function MD023(params, onError) {
const headings = filterByTypesCached([ "atxHeading", "linePrefix", "setextHeading" ]);
for (let i = 0; i < headings.length - 1; i++) {
if (
(headings[i].type === "linePrefix") &&
(headings[i + 1].type !== "linePrefix") &&
(headings[i].startLine === headings[i + 1].startLine)
) {
const { endColumn, startColumn, startLine } = headings[i];
const length = endColumn - startColumn;
addErrorContext(
onError,
startLine,
params.lines[startLine - 1],
true,
false,
[ startColumn, length ],
{
"editColumn": startColumn,
"deleteCount": length
}
);
}
}
}
};
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD024", "no-duplicate-heading" ],
"description": "Multiple headings with the same content",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD024(params, onError) {
const siblingsOnly = !!params.config.siblings_only || false;
const knownContents = [ null, [] ];
let lastLevel = 1;
let knownContent = knownContents[lastLevel];
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const headingText = getHeadingText(heading);
if (siblingsOnly) {
const newLevel = getHeadingLevel(heading);
while (lastLevel < newLevel) {
lastLevel++;
knownContents[lastLevel] = [];
}
while (lastLevel > newLevel) {
knownContents[lastLevel] = [];
lastLevel--;
}
knownContent = knownContents[newLevel];
}
// @ts-ignore
if (knownContent.includes(headingText)) {
addErrorContext(
onError,
heading.startLine,
headingText.trim()
);
} else {
// @ts-ignore
knownContent.push(headingText);
}
}
}
};
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
import { getHeadingLevel, getHeadingText, isDocfxTab, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD025", "single-title", "single-h1" ],
"description": "Multiple top-level headings in the same document",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD025(params, onError) {
const level = Number(params.config.level || 1);
const { tokens } = params.parsers.micromark;
const matchingHeadings = filterByTypesCached([ "atxHeading", "setextHeading" ])
.filter((heading) => (level === getHeadingLevel(heading)) && !isDocfxTab(heading));
if (matchingHeadings.length > 0) {
const foundFrontMatterTitle =
frontMatterHasTitle(
params.frontMatterLines,
params.config.front_matter_title
);
// Front matter title counts as a top-level heading if present
let hasTopLevelHeading = foundFrontMatterTitle;
if (!hasTopLevelHeading) {
// Check if the first matching heading is a top-level heading
const previousTokens = tokens.slice(0, tokens.indexOf(matchingHeadings[0]));
hasTopLevelHeading = previousTokens.every(
(token) => nonContentTokens.has(token.type) || isHtmlFlowComment(token)
);
}
if (hasTopLevelHeading) {
// All other matching headings are violations
for (const heading of matchingHeadings.slice(foundFrontMatterTitle ? 0 : 1)) {
addErrorContext(
onError,
heading.startLine,
getHeadingText(heading)
);
}
}
}
}
};
+46
View File
@@ -0,0 +1,46 @@
// @ts-check
import { addError, allPunctuationNoQuestion, endOfLineGemojiCodeRe,
endOfLineHtmlEntityRe, escapeForRegExp } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD026", "no-trailing-punctuation" ],
"description": "Trailing punctuation in heading",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD026(params, onError) {
let punctuation = params.config.punctuation;
punctuation = String(
(punctuation === undefined) ? allPunctuationNoQuestion : punctuation
);
const trailingPunctuationRe =
new RegExp("\\s*[" + escapeForRegExp(punctuation) + "]+$");
const headings = filterByTypesCached([ "atxHeadingText", "setextHeadingText" ]);
for (const heading of headings) {
const { endColumn, endLine, text } = heading;
const match = trailingPunctuationRe.exec(text);
if (
match &&
!endOfLineHtmlEntityRe.test(text) &&
!endOfLineGemojiCodeRe.test(text)
) {
const fullMatch = match[0];
const length = fullMatch.length;
const column = endColumn - length;
addError(
onError,
endLine,
`Punctuation: '${fullMatch}'`,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
}
};
+50
View File
@@ -0,0 +1,50 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("../helpers/micromark-helpers.cjs").TokenType[]} */
const listTypes = [ "listOrdered", "listUnordered" ];
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD027", "no-multiple-space-blockquote" ],
"description": "Multiple spaces after blockquote symbol",
"tags": [ "blockquote", "whitespace", "indentation" ],
"parser": "micromark",
"function": function MD027(params, onError) {
const listItems = params.config.list_items;
const includeListItems = (listItems === undefined) ? true : !!listItems;
const { tokens } = params.parsers.micromark;
for (const token of filterByTypesCached([ "linePrefix" ])) {
const parent = token.parent;
const codeIndented = parent?.type === "codeIndented";
const siblings = parent?.children || tokens;
if (
!codeIndented &&
(siblings[siblings.indexOf(token) - 1]?.type === "blockQuotePrefix") &&
(includeListItems || (
!listTypes.includes(siblings[siblings.indexOf(token) + 1]?.type) &&
!getParentOfType(token, listTypes)
))
) {
const { startColumn, startLine, text } = token;
const { length } = text;
const line = params.lines[startLine - 1];
addErrorContext(
onError,
startLine,
line,
undefined,
undefined,
[ startColumn, length ],
{
"editColumn": startColumn,
"deleteCount": length
}
);
}
}
}
};
+39
View File
@@ -0,0 +1,39 @@
// @ts-check
import { addError } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const ignoreTypes = new Set([ "lineEnding", "listItemIndent", "linePrefix" ]);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD028", "no-blanks-blockquote" ],
"description": "Blank line inside blockquote",
"tags": [ "blockquote", "whitespace" ],
"parser": "micromark",
"function": function MD028(params, onError) {
for (const token of filterByTypesCached([ "blockQuote" ])) {
const errorLineNumbers = [];
const siblings = token.parent?.children || params.parsers.micromark.tokens;
for (let i = siblings.indexOf(token) + 1; i < siblings.length; i++) {
const sibling = siblings[i];
const { startLine, type } = sibling;
if (type === "lineEndingBlank") {
// Possible blank between blockquotes
errorLineNumbers.push(startLine);
} else if (ignoreTypes.has(type)) {
// Ignore invisible formatting
} else if (type === "blockQuote") {
// Blockquote followed by blockquote
for (const lineNumber of errorLineNumbers) {
addError(onError, lineNumber);
}
break;
} else {
// Blockquote not followed by blockquote
break;
}
}
}
}
};
+84
View File
@@ -0,0 +1,84 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const listStyleExamples = {
"one": "1/1/1",
"ordered": "1/2/3",
"zero": "0/0/0"
};
/**
* Gets the column and text of an ordered list item prefix token.
*
* @param {import("markdownlint").MicromarkToken} listItemPrefix List item prefix token.
* @returns {{column: number, value: number}} List item value column and text.
*/
function getOrderedListItemValue(listItemPrefix) {
const listItemValue = getDescendantsByType(listItemPrefix, [ "listItemValue" ])[0];
return {
"column": listItemValue.startColumn,
"value": Number(listItemValue.text)
};
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD029", "ol-prefix" ],
"description": "Ordered list item prefix",
"tags": [ "ol" ],
"parser": "micromark",
"function": function MD029(params, onError) {
const style = String(params.config.style);
for (const listOrdered of filterByTypesCached([ "listOrdered" ])) {
const listItemPrefixes = getDescendantsByType(listOrdered, [ "listItemPrefix" ]);
let expected = 1;
let incrementing = false;
// Check for incrementing number pattern 1/2/3 or 0/1/2
if (listItemPrefixes.length >= 2) {
const first = getOrderedListItemValue(listItemPrefixes[0]);
const second = getOrderedListItemValue(listItemPrefixes[1]);
if ((second.value !== 1) || (first.value === 0)) {
incrementing = true;
if (first.value === 0) {
expected = 0;
}
}
}
// Determine effective style
const listStyle = ((style === "one") || (style === "ordered") || (style === "zero")) ?
style :
(incrementing ? "ordered" : "one");
if (listStyle === "zero") {
expected = 0;
} else if (listStyle === "one") {
expected = 1;
}
// Validate each list item marker
for (const listItemPrefix of listItemPrefixes) {
const orderedListItemValue = getOrderedListItemValue(listItemPrefix);
const actual = orderedListItemValue.value;
const fixInfo = {
"editColumn": orderedListItemValue.column,
"deleteCount": orderedListItemValue.value.toString().length,
"insertText": expected.toString()
};
addErrorDetailIf(
onError,
listItemPrefix.startLine,
expected,
actual,
"Style: " + listStyleExamples[listStyle],
undefined,
[ listItemPrefix.startColumn, listItemPrefix.endColumn - listItemPrefix.startColumn ],
fixInfo
);
if (listStyle === "ordered") {
expected++;
}
}
}
}
};
+57
View File
@@ -0,0 +1,57 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD030", "list-marker-space" ],
"description": "Spaces after list markers",
"tags": [ "ol", "ul", "whitespace" ],
"parser": "micromark",
"function": function MD030(params, onError) {
const ulSingle = Number(params.config.ul_single || 1);
const olSingle = Number(params.config.ol_single || 1);
const ulMulti = Number(params.config.ul_multi || 1);
const olMulti = Number(params.config.ol_multi || 1);
for (const list of filterByTypesCached([ "listOrdered", "listUnordered" ])) {
const ordered = (list.type === "listOrdered");
const listItemPrefixes =
list.children.filter((token) => (token.type === "listItemPrefix"));
const allSingleLine =
(list.endLine - list.startLine + 1) === listItemPrefixes.length;
const expectedSpaces = ordered ?
(allSingleLine ? olSingle : olMulti) :
(allSingleLine ? ulSingle : ulMulti);
for (const listItemPrefix of listItemPrefixes) {
const range = [
listItemPrefix.startColumn,
listItemPrefix.endColumn - listItemPrefix.startColumn
];
const listItemPrefixWhitespaces = listItemPrefix.children.filter(
(token) => (token.type === "listItemPrefixWhitespace")
);
for (const listItemPrefixWhitespace of listItemPrefixWhitespaces) {
const { endColumn, startColumn, startLine } =
listItemPrefixWhitespace;
const actualSpaces = endColumn - startColumn;
const fixInfo = {
"editColumn": startColumn,
"deleteCount": actualSpaces,
"insertText": "".padEnd(expectedSpaces)
};
addErrorDetailIf(
onError,
startLine,
expectedSpaces,
actualSpaces,
undefined,
undefined,
range,
fixInfo
);
}
}
}
}
};
+61
View File
@@ -0,0 +1,61 @@
// @ts-check
import { addErrorContext, isBlankLine } from "../helpers/helpers.cjs";
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const codeFencePrefixRe = /^(.*?)[`~]/;
/** @typedef {readonly string[]} ReadonlyStringArray */
/**
* Adds an error for the top or bottom of a code fence.
*
* @param {import("markdownlint").RuleOnError} onError Error-reporting callback.
* @param {ReadonlyStringArray} lines Lines of Markdown content.
* @param {number} lineNumber Line number.
* @param {boolean} top True iff top fence.
* @returns {void}
*/
function addError(onError, lines, lineNumber, top) {
const line = lines[lineNumber - 1];
const [ , prefix ] = line.match(codeFencePrefixRe) || [];
const fixInfo = (prefix === undefined) ?
undefined :
{
"lineNumber": lineNumber + (top ? 0 : 1),
"insertText": `${prefix.replace(/[^>]/g, " ").trim()}\n`
};
addErrorContext(
onError,
lineNumber,
line.trim(),
undefined,
undefined,
undefined,
fixInfo
);
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD031", "blanks-around-fences" ],
"description": "Fenced code blocks should be surrounded by blank lines",
"tags": [ "code", "blank_lines" ],
"parser": "micromark",
"function": function MD031(params, onError) {
const listItems = params.config.list_items;
const includeListItems = (listItems === undefined) ? true : !!listItems;
const { lines } = params;
for (const codeBlock of filterByTypesCached([ "codeFenced" ])) {
if (includeListItems || !(getParentOfType(codeBlock, [ "listOrdered", "listUnordered" ]))) {
if (!isBlankLine(lines[codeBlock.startLine - 2])) {
addError(onError, lines, codeBlock.startLine, true);
}
if (!isBlankLine(lines[codeBlock.endLine]) && !isBlankLine(lines[codeBlock.endLine - 1])) {
addError(onError, lines, codeBlock.endLine, false);
}
}
}
}
};
+78
View File
@@ -0,0 +1,78 @@
// @ts-check
import { addErrorContext, isBlankLine } from "../helpers/helpers.cjs";
import { filterByPredicate, getBlockQuotePrefixText, nonContentTokens } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
const isList = (/** @type {MicromarkToken} */ token) => (
(token.type === "listOrdered") || (token.type === "listUnordered")
);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD032", "blanks-around-lists" ],
"description": "Lists should be surrounded by blank lines",
"tags": [ "bullet", "ul", "ol", "blank_lines" ],
"parser": "micromark",
"function": function MD032(params, onError) {
const { lines, parsers } = params;
const blockQuotePrefixes = filterByTypesCached([ "blockQuotePrefix", "linePrefix" ]);
// For every top-level list...
const topLevelLists = filterByPredicate(
parsers.micromark.tokens,
isList,
(token) => (
(isList(token) || (token.type === "htmlFlow")) ? [] : token.children
)
);
for (const list of topLevelLists) {
// Look for a blank line above the list
const firstLineNumber = list.startLine;
if (!isBlankLine(lines[firstLineNumber - 2])) {
addErrorContext(
onError,
firstLineNumber,
lines[firstLineNumber - 1].trim(),
undefined,
undefined,
undefined,
{
"insertText": getBlockQuotePrefixText(blockQuotePrefixes, firstLineNumber)
}
);
}
// Find the "visual" end of the list
const flattenedChildren = filterByPredicate(
list.children,
(token) => !nonContentTokens.has(token.type),
(token) => nonContentTokens.has(token.type) ? [] : token.children
);
let endLine = list.endLine;
if (flattenedChildren.length > 0) {
endLine = flattenedChildren[flattenedChildren.length - 1].endLine;
}
// Look for a blank line below the list
const lastLineNumber = endLine;
if (!isBlankLine(lines[lastLineNumber])) {
addErrorContext(
onError,
lastLineNumber,
lines[lastLineNumber - 1].trim(),
undefined,
undefined,
undefined,
{
"lineNumber": lastLineNumber + 1,
"insertText": getBlockQuotePrefixText(blockQuotePrefixes, lastLineNumber)
}
);
}
}
}
};
+46
View File
@@ -0,0 +1,46 @@
// @ts-check
import { addError, nextLinesRe } from "../helpers/helpers.cjs";
import { getHtmlTagInfo, getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("micromark-extension-gfm-table")} */
// eslint-disable-next-line jsdoc/reject-any-type
const toLowerCaseStringArray = (/** @type {any} */ arr) => Array.isArray(arr) ? arr.map((elm) => String(elm).toLowerCase()) : [];
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD033", "no-inline-html" ],
"description": "Inline HTML",
"tags": [ "html" ],
"parser": "micromark",
"function": function MD033(params, onError) {
const allowedElements = toLowerCaseStringArray(params.config.allowed_elements);
// If not defined, use allowedElements for backward compatibility
const tableAllowedElements = toLowerCaseStringArray(params.config.table_allowed_elements || params.config.allowed_elements);
for (const token of filterByTypesCached([ "htmlText" ], true)) {
const htmlTagInfo = getHtmlTagInfo(token);
if (htmlTagInfo && !htmlTagInfo.close) {
const elementName = htmlTagInfo?.name.toLowerCase();
const inTable = !!getParentOfType(token, [ "table" ]);
if (
(inTable || !allowedElements.includes(elementName)) &&
(!inTable || !tableAllowedElements.includes(elementName))
) {
const range = [
token.startColumn,
token.text.replace(nextLinesRe, "").length
];
addError(
onError,
token.startLine,
"Element: " + htmlTagInfo.name,
undefined,
range
);
}
}
}
}
};
+92
View File
@@ -0,0 +1,92 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { filterByPredicate, getHtmlTagInfo, inHtmlFlow } from "../helpers/micromark-helpers.cjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("micromark-extension-gfm-autolink-literal")} */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD034", "no-bare-urls" ],
"description": "Bare URL used",
"tags": [ "links", "url" ],
"parser": "micromark",
"function": function MD034(params, onError) {
const literalAutolinks = (/** @type {MicromarkToken[]} */ tokens) => (
filterByPredicate(
tokens,
(token) => {
if ((token.type === "literalAutolink") && !inHtmlFlow(token)) {
// Detect and ignore https://github.com/micromark/micromark/issues/164
const siblings = token.parent?.children;
const index = siblings?.indexOf(token);
// @ts-ignore
const prev = siblings?.at(index - 1);
// @ts-ignore
const next = siblings?.at(index + 1);
return !(
prev &&
next &&
(prev.type === "data") &&
(next.type === "data") &&
prev.text.endsWith("<") &&
next.text.startsWith(">")
);
}
return false;
},
(token) => {
// Ignore content of inline HTML tags
const { children } = token;
const result = [];
for (let i = 0; i < children.length; i++) {
const current = children[i];
const openTagInfo = getHtmlTagInfo(current);
if (openTagInfo && !openTagInfo.close) {
let count = 1;
for (let j = i + 1; j < children.length; j++) {
const candidate = children[j];
const closeTagInfo = getHtmlTagInfo(candidate);
if (closeTagInfo && (openTagInfo.name === closeTagInfo.name)) {
if (closeTagInfo.close) {
count--;
if (count === 0) {
i = j;
break;
}
} else {
count++;
}
}
}
} else {
result.push(current);
}
}
return result;
}
)
);
for (const token of literalAutolinks(params.parsers.micromark.tokens)) {
const range = [
token.startColumn,
token.endColumn - token.startColumn
];
const fixInfo = {
"editColumn": range[0],
"deleteCount": range[1],
"insertText": `<${token.text}>`
};
addErrorContext(
onError,
token.startLine,
token.text,
undefined,
undefined,
range,
fixInfo
);
}
}
};
+23
View File
@@ -0,0 +1,23 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD035", "hr-style" ],
"description": "Horizontal rule style",
"tags": [ "hr" ],
"parser": "micromark",
"function": function MD035(params, onError) {
let style = String(params.config.style || "consistent").trim();
const thematicBreaks = filterByTypesCached([ "thematicBreak" ]);
for (const token of thematicBreaks) {
const { startLine, text } = token;
if (style === "consistent") {
style = text;
}
addErrorDetailIf(onError, startLine, style, text);
}
}
};
+54
View File
@@ -0,0 +1,54 @@
// @ts-check
import { addErrorContext, allPunctuation } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
/** @type {MicromarkTokenType[][]} */
const emphasisTypes = [
[ "emphasis", "emphasisText" ],
[ "strong", "strongText" ]
];
const isParagraphChildMeaningful = (/** @type {MicromarkToken} */ token) => !(
(token.type === "htmlText") ||
((token.type === "data") && (token.text.trim().length === 0))
);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD036", "no-emphasis-as-heading" ],
"description": "Emphasis used instead of a heading",
"tags": [ "headings", "emphasis" ],
"parser": "micromark",
"function": function MD036(params, onError) {
let punctuation = params.config.punctuation;
punctuation = String((punctuation === undefined) ? allPunctuation : punctuation);
const punctuationRe = new RegExp("[" + punctuation + "]$");
const paragraphTokens =
filterByTypesCached([ "paragraph" ], true)
.filter((token) =>
(token.parent?.type === "content") &&
(
!token.parent?.parent ||
((token.parent?.parent?.type === "htmlFlow") && !token.parent?.parent?.parent)
) &&
(token.children.filter(isParagraphChildMeaningful).length === 1)
);
for (const emphasisType of emphasisTypes) {
const textTokens = getDescendantsByType(paragraphTokens, emphasisType);
for (const textToken of textTokens) {
if (
(textToken.children.length === 1) &&
(textToken.children[0].type === "data") &&
!punctuationRe.test(textToken.text)
) {
addErrorContext(onError, textToken.startLine, textToken.text);
}
}
}
}
};
+94
View File
@@ -0,0 +1,94 @@
// @ts-check
import { addError } from "../helpers/helpers.cjs";
import { filterByPredicate, inHtmlFlow } from "../helpers/micromark-helpers.cjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD037", "no-space-in-emphasis" ],
"description": "Spaces inside emphasis markers",
"tags": [ "whitespace", "emphasis" ],
"parser": "micromark",
"function": function MD037(params, onError) {
// Initialize variables
const { lines, parsers } = params;
const emphasisTokensByMarker = new Map();
for (const marker of [ "_", "__", "___", "*", "**", "***" ]) {
emphasisTokensByMarker.set(marker, []);
}
const tokens = filterByPredicate(
parsers.micromark.tokens,
(token) => token.children.some((child) => child.type === "data")
);
for (const token of tokens) {
// Build lists of bare tokens for each emphasis marker type
for (const emphasisTokens of emphasisTokensByMarker.values()) {
emphasisTokens.length = 0;
}
for (const child of token.children) {
const { text, type } = child;
if ((type === "data") && (text.length <= 3)) {
const emphasisTokens = emphasisTokensByMarker.get(text);
if (emphasisTokens && !inHtmlFlow(child)) {
emphasisTokens.push(child);
}
}
}
// Process bare tokens for each emphasis marker type
for (const entry of emphasisTokensByMarker.entries()) {
const [ marker, emphasisTokens ] = entry;
for (let i = 0; i + 1 < emphasisTokens.length; i += 2) {
// Process start token of start/end pair
const startToken = emphasisTokens[i];
const startLine = lines[startToken.startLine - 1];
const startSlice = startLine.slice(startToken.endColumn - 1);
const startMatch = startSlice.match(/^\s+\S/);
if (startMatch) {
const [ startSpaceCharacter ] = startMatch;
const startContext = `${marker}${startSpaceCharacter}`;
const column = startToken.endColumn;
const count = startSpaceCharacter.length - 1;
addError(
onError,
startToken.startLine,
undefined,
startContext,
[ column, count ],
{
"editColumn": column,
"deleteCount": count
}
);
}
// Process end token of start/end pair
const endToken = emphasisTokens[i + 1];
const endLine = lines[endToken.startLine - 1];
const endSlice = endLine.slice(0, endToken.startColumn - 1);
const endMatch = endSlice.match(/\S\s+$/);
if (endMatch) {
const [ endSpaceCharacter ] = endMatch;
const endContext = `${endSpaceCharacter}${marker}`;
const column = endToken.startColumn - (endSpaceCharacter.length - 1);
const count = endSpaceCharacter.length - 1;
addError(
onError,
endToken.startLine,
undefined,
endContext,
[ column, count ],
{
"editColumn": column,
"deleteCount": count
}
);
}
}
}
}
}
};
+73
View File
@@ -0,0 +1,73 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD038", "no-space-in-code" ],
"description": "Spaces inside code span elements",
"tags": [ "whitespace", "code" ],
"parser": "micromark",
"function": function MD038(params, onError) {
const codeTexts = filterByTypesCached([ "codeText" ]);
for (const codeText of codeTexts) {
const datas = getDescendantsByType(codeText, [ "codeTextData" ]);
if (datas.length > 0) {
const paddings = getDescendantsByType(codeText, [ "codeTextPadding" ]);
// Check for extra space at start of code
const startPadding = paddings[0];
const startData = datas[0];
const startMatch = /^(\s+)(\S)/.exec(startData.text) || [ null, "", "" ];
const startBacktick = (startMatch[2] === "`");
const startCount = startMatch[1].length - ((startBacktick && !startPadding) ? 1 : 0);
const startSpaces = startCount > 0;
// Check for extra space at end of code
const endPadding = paddings[paddings.length - 1];
const endData = datas[datas.length - 1];
const endMatch = /(\S)(\s+)$/.exec(endData.text) || [ null, "", "" ];
const endBacktick = (endMatch[1] === "`");
const endCount = endMatch[2].length - ((endBacktick && !endPadding) ? 1 : 0);
const endSpaces = endCount > 0;
// Check if safe to remove 1-space padding
const removePadding = startSpaces && endSpaces && startPadding && endPadding && !startBacktick && !endBacktick;
const context = codeText.text;
// If extra space at start, report violation
if (startSpaces) {
const startColumn = (removePadding ? startPadding : startData).startColumn;
const length = startCount + (removePadding ? startPadding.text.length : 0);
addErrorContext(
onError,
startData.startLine,
context,
true,
false,
[ startColumn, length ],
{
"editColumn": startColumn,
"deleteCount": length
}
);
}
// If extra space at end, report violation
if (endSpaces) {
const endColumn = (removePadding ? endPadding : endData).endColumn;
const length = endCount + (removePadding ? endPadding.text.length : 0);
addErrorContext(
onError,
endData.endLine,
context,
false,
true,
[ endColumn - length, length ],
{
"editColumn": endColumn - length,
"deleteCount": length
}
);
}
}
}
}
};
+59
View File
@@ -0,0 +1,59 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/**
* Adds an error for a label space issue.
*
* @param {import("markdownlint").RuleOnError} onError Error-reporting callback.
* @param {import("markdownlint").MicromarkToken} label Label token.
* @param {import("markdownlint").MicromarkToken} labelText LabelText token.
* @param {boolean} isStart True iff error is at the start of the link.
*/
function addLabelSpaceError(onError, label, labelText, isStart) {
const match = labelText.text.match(isStart ? /^[^\S\r\n]+/ : /[^\S\r\n]+$/);
const range = match ?
[
(isStart ? (labelText.startColumn) : (labelText.endColumn - match[0].length)),
match[0].length
] :
undefined;
addErrorContext(
onError,
isStart ? (labelText.startLine + (match ? 0 : 1)) : (labelText.endLine - (match ? 0 : 1)),
label.text.replace(/\s+/g, " "),
isStart,
!isStart,
range,
range ?
{
"editColumn": range[0],
"deleteCount": range[1]
} :
undefined
);
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD039", "no-space-in-links" ],
"description": "Spaces inside link text",
"tags": [ "whitespace", "links" ],
"parser": "micromark",
"function": function MD039(params, onError) {
const labels = filterByTypesCached([ "label" ])
.filter((label) => label.parent?.type === "link");
for (const label of labels) {
const labelTexts = label.children.filter((child) => child.type === "labelText");
for (const labelText of labelTexts) {
if (labelText.text.trimStart().length !== labelText.text.length) {
addLabelSpaceError(onError, label, labelText, true);
}
if (labelText.text.trimEnd().length !== labelText.text.length) {
addLabelSpaceError(onError, label, labelText, false);
}
}
}
}
};
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
import { addError, addErrorContext } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD040", "fenced-code-language" ],
"description": "Fenced code blocks should have a language specified",
"tags": [ "code", "language" ],
"parser": "micromark",
"function": function MD040(params, onError) {
let allowed = params.config.allowed_languages;
allowed = Array.isArray(allowed) ? allowed : [];
const languageOnly = !!params.config.language_only;
const fencedCodes = filterByTypesCached([ "codeFenced" ]);
for (const fencedCode of fencedCodes) {
const openingFence = getDescendantsByType(fencedCode, [ "codeFencedFence" ])[0];
const { startLine, text } = openingFence;
const info = getDescendantsByType(openingFence, [ "codeFencedFenceInfo" ])[0]?.text;
if (!info) {
addErrorContext(onError, startLine, text);
} else if ((allowed.length > 0) && !allowed.includes(info)) {
addError(onError, startLine, `"${info}" is not allowed`);
}
if (languageOnly && getDescendantsByType(openingFence, [ "codeFencedFenceMeta" ]).length > 0) {
addError(onError, startLine, `Info string contains more than language: "${text}"`);
}
}
}
};
+71
View File
@@ -0,0 +1,71 @@
// @ts-check
import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
import { filterByTypes, getHeadingLevel, getHtmlTagInfo, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
const headingTagNameRe = /^h[1-6]$/;
/**
* Gets the HTML tag name of an htmlFlow token.
*
* @param {import("markdownlint").MicromarkToken} token Micromark Token.
* @returns {string | null} Tag name.
*/
function getHtmlFlowTagName(token) {
const { children, type } = token;
if (type === "htmlFlow") {
const htmlTexts = filterByTypes(children, [ "htmlText" ], true);
const tagInfo = (htmlTexts.length > 0) && getHtmlTagInfo(htmlTexts[0]);
if (tagInfo) {
return tagInfo.name.toLowerCase();
}
}
return null;
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD041", "first-line-heading", "first-line-h1" ],
"description": "First line in a file should be a top-level heading",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD041(params, onError) {
const allowPreamble = !!params.config.allow_preamble;
const level = Number(params.config.level || 1);
const { tokens } = params.parsers.micromark;
if (
!frontMatterHasTitle(
params.frontMatterLines,
params.config.front_matter_title
)
) {
let errorLineNumber = 0;
for (const token of tokens) {
const { startLine, type } = token;
if (!nonContentTokens.has(type) && !isHtmlFlowComment(token)) {
let tagName = null;
if ((type === "atxHeading") || (type === "setextHeading")) {
// First heading needs to have the expected level
if (getHeadingLevel(token) !== level) {
errorLineNumber = startLine;
}
break;
} else if ((tagName = getHtmlFlowTagName(token)) && headingTagNameRe.test(tagName)) {
// First HTML element needs to have an <h?> with the expected level
if (tagName !== `h${level}`) {
errorLineNumber = startLine;
}
break;
} else if (!allowPreamble) {
// First non-content needs to be a heading with the expected level
errorLineNumber = startLine;
break;
}
}
}
if (errorLineNumber > 0) {
addErrorContext(onError, errorLineNumber, params.lines[errorLineNumber - 1]);
}
}
}
};
+58
View File
@@ -0,0 +1,58 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD042", "no-empty-links" ],
"description": "No empty links",
"tags": [ "links" ],
"parser": "micromark",
"function": function MD042(params, onError) {
const { definitions } = getReferenceLinkImageData();
const isReferenceDefinitionHash = (/** @type {MicromarkToken} */ token) => {
const definition = definitions.get(token.text.trim());
return Boolean(definition && (definition[1] === "#"));
};
const links = filterByTypesCached([ "link" ]);
for (const link of links) {
const labelText = getDescendantsByType(link, [ "label", "labelText" ]);
const reference = getDescendantsByType(link, [ "reference" ]);
const resource = getDescendantsByType(link, [ "resource" ]);
const referenceString = getDescendantsByType(reference, [ "referenceString" ]);
const resourceDestinationString = getDescendantsByType(resource, [ "resourceDestination", [ "resourceDestinationLiteral", "resourceDestinationRaw" ], "resourceDestinationString" ]);
const hasLabelText = labelText.length > 0;
const hasReference = reference.length > 0;
const hasResource = resource.length > 0;
const hasReferenceString = referenceString.length > 0;
const hasResourceDestinationString = resourceDestinationString.length > 0;
let error = false;
if (
hasLabelText &&
((!hasReference && !hasResource) || (hasReference && !hasReferenceString))
) {
error = isReferenceDefinitionHash(labelText[0]);
} else if (hasReferenceString && !hasResourceDestinationString) {
error = isReferenceDefinitionHash(referenceString[0]);
} else if (!hasReferenceString && hasResourceDestinationString) {
error = (resourceDestinationString[0].text.trim() === "#");
} else if (!hasReferenceString && !hasResourceDestinationString) {
error = true;
}
if (error) {
addErrorContext(
onError,
link.startLine,
link.text,
undefined,
undefined,
[ link.startColumn, link.endColumn - link.startColumn ]
);
}
}
}
};
+72
View File
@@ -0,0 +1,72 @@
// @ts-check
import { addErrorContext, addErrorDetailIf } from "../helpers/helpers.cjs";
import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD043", "required-headings" ],
"description": "Required heading structure",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD043(params, onError) {
const requiredHeadings = params.config.headings;
if (!Array.isArray(requiredHeadings)) {
// Nothing to check; avoid doing any work
return;
}
const matchCase = params.config.match_case || false;
let i = 0;
let matchAny = false;
let hasError = false;
let anyHeadings = false;
const getExpected = () => String(requiredHeadings[i++] || "[None]");
const handleCase = (/** @type {string} */ str) => (matchCase ? str : str.toLowerCase());
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
if (!hasError) {
const headingText = getHeadingText(heading);
const headingLevel = getHeadingLevel(heading);
anyHeadings = true;
const actual = `${"".padEnd(headingLevel, "#")} ${headingText}`;
const expected = getExpected();
if (expected === "*") {
const nextExpected = getExpected();
if (handleCase(nextExpected) !== handleCase(actual)) {
matchAny = true;
i--;
}
} else if (expected === "+") {
matchAny = true;
} else if (expected === "?") {
// Allow current, match next
} else if (handleCase(expected) === handleCase(actual)) {
matchAny = false;
} else if (matchAny) {
i--;
} else {
addErrorDetailIf(
onError,
heading.startLine,
expected,
actual
);
hasError = true;
}
}
}
const extraHeadings = requiredHeadings.length - i;
if (
!hasError &&
((extraHeadings > 1) ||
((extraHeadings === 1) && (requiredHeadings[i] !== "*"))) &&
(anyHeadings || !requiredHeadings.every((heading) => heading === "*"))
) {
addErrorContext(
onError,
params.lines.length,
requiredHeadings[i]
);
}
}
};
+110
View File
@@ -0,0 +1,110 @@
// @ts-check
import { addErrorDetailIf, escapeForRegExp, hasOverlap } from "../helpers/helpers.cjs";
import { filterByPredicate, filterByTypes } from "../helpers/micromark-helpers.cjs";
import { parse } from "./micromark-parse.mjs";
const ignoredChildTypes = new Set(
[ "codeFencedFence", "definition", "reference", "resource" ]
);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD044", "proper-names" ],
"description": "Proper names should have the correct capitalization",
"tags": [ "spelling" ],
"parser": "micromark",
"function": function MD044(params, onError) {
let names = params.config.names;
names = Array.isArray(names) ? names : [];
names.sort((/** @type {string} */ a, /** @type {string} */ b) => (b.length - a.length) || a.localeCompare(b));
if (names.length === 0) {
// Nothing to check; avoid doing any work
return;
}
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks =
(codeBlocks === undefined) ? true : !!codeBlocks;
const htmlElements = params.config.html_elements;
const includeHtmlElements =
(htmlElements === undefined) ? true : !!htmlElements;
const scannedTypes = new Set([ "data" ]);
if (includeCodeBlocks) {
scannedTypes.add("codeFlowValue");
scannedTypes.add("codeTextData");
}
if (includeHtmlElements) {
scannedTypes.add("htmlFlowData");
scannedTypes.add("htmlTextData");
}
const contentTokens =
filterByPredicate(
params.parsers.micromark.tokens,
(token) => scannedTypes.has(token.type),
(token) => (
token.children.filter((t) => !ignoredChildTypes.has(t.type))
)
);
/** @type {import("../helpers/helpers.cjs").FileRange[]} */
const exclusions = [];
const scannedTokens = new Set();
for (const name of names) {
const escapedName = escapeForRegExp(name);
const startNamePattern = /^\W/.test(name) ? "" : "\\b_*";
const endNamePattern = /\W$/.test(name) ? "" : "_*\\b";
const namePattern = `(${startNamePattern})(${escapedName})${endNamePattern}`;
const nameRe = new RegExp(namePattern, "gi");
for (const token of contentTokens) {
let match = null;
while ((match = nameRe.exec(token.text)) !== null) {
const [ , leftMatch, nameMatch ] = match;
const column = token.startColumn + match.index + leftMatch.length;
const length = nameMatch.length;
const lineNumber = token.startLine;
/** @type {import("../helpers/helpers.cjs").FileRange} */
const nameRange = {
"startLine": lineNumber,
"startColumn": column,
"endLine": lineNumber,
"endColumn": column + length - 1
};
if (
!names.includes(nameMatch) &&
!exclusions.some((exclusion) => hasOverlap(exclusion, nameRange))
) {
/** @type {import("../helpers/helpers.cjs").FileRange[]} */
let autolinkRanges = [];
if (!scannedTokens.has(token)) {
autolinkRanges = filterByTypes(parse(token.text), [ "literalAutolink" ])
.map((tok) => ({
"startLine": lineNumber,
"startColumn": token.startColumn + tok.startColumn - 1,
"endLine": lineNumber,
"endColumn": token.endColumn + tok.endColumn - 1
}));
exclusions.push(...autolinkRanges);
scannedTokens.add(token);
}
if (!autolinkRanges.some((autolinkRange) => hasOverlap(autolinkRange, nameRange))) {
addErrorDetailIf(
onError,
token.startLine,
name,
nameMatch,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": name
}
);
}
}
exclusions.push(nameRange);
}
}
}
}
};
+61
View File
@@ -0,0 +1,61 @@
// @ts-check
import { addError, getHtmlAttributeRe, nextLinesRe } from "../helpers/helpers.cjs";
import { getHtmlTagInfo, getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const altRe = getHtmlAttributeRe("alt");
const ariaHiddenRe = getHtmlAttributeRe("aria-hidden");
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD045", "no-alt-text" ],
"description": "Images should have alternate text (alt text)",
"tags": [ "accessibility", "images" ],
"parser": "micromark",
"function": function MD045(params, onError) {
// Process Markdown images
const images = filterByTypesCached([ "image" ]);
for (const image of images) {
const labelTexts = getDescendantsByType(image, [ "label", "labelText" ]);
if (labelTexts.some((labelText) => labelText.text.length === 0)) {
const range = (image.startLine === image.endLine) ?
[ image.startColumn, image.endColumn - image.startColumn ] :
undefined;
addError(
onError,
image.startLine,
undefined,
undefined,
range
);
}
}
// Process HTML images
const htmlTexts = filterByTypesCached([ "htmlText" ], true);
for (const htmlText of htmlTexts) {
const { startColumn, startLine, text } = htmlText;
const htmlTagInfo = getHtmlTagInfo(htmlText);
if (
htmlTagInfo &&
!htmlTagInfo.close &&
(htmlTagInfo.name.toLowerCase() === "img") &&
!altRe.test(text) &&
(ariaHiddenRe.exec(text)?.[1].toLowerCase() !== "true")
) {
const range = [
startColumn,
text.replace(nextLinesRe, "").length
];
addError(
onError,
startLine,
undefined,
undefined,
range
);
}
}
}
};
+31
View File
@@ -0,0 +1,31 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
const tokenTypeToStyle = (/** @type {MicromarkTokenType} */ tokenType) => (tokenType === "codeFenced") ? "fenced" : "indented";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD046", "code-block-style" ],
"description": "Code block style",
"tags": [ "code" ],
"parser": "micromark",
"function": function MD046(params, onError) {
let expectedStyle = String(params.config.style || "consistent");
for (const token of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
const { startLine, type } = token;
if (expectedStyle === "consistent") {
expectedStyle = tokenTypeToStyle(type);
}
addErrorDetailIf(
onError,
startLine,
expectedStyle,
tokenTypeToStyle(type)
);
}
}
};
+28
View File
@@ -0,0 +1,28 @@
// @ts-check
import { addError, isBlankLine } from "../helpers/helpers.cjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD047", "single-trailing-newline" ],
"description": "Files should end with a single newline character",
"tags": [ "blank_lines" ],
"parser": "none",
"function": function MD047(params, onError) {
const lastLineNumber = params.lines.length;
const lastLine = params.lines[lastLineNumber - 1];
if (!isBlankLine(lastLine)) {
addError(
onError,
lastLineNumber,
undefined,
undefined,
[ lastLine.length, 1 ],
{
"insertText": "\n",
"editColumn": lastLine.length + 1
}
);
}
}
};
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/**
* Return the string representation of a fence markup character.
*
* @param {string} markup Fence string.
* @returns {"tilde" | "backtick"} String representation.
*/
function fencedCodeBlockStyleFor(markup) {
switch (markup[0]) {
case "~":
return "tilde";
default:
return "backtick";
}
};
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD048", "code-fence-style" ],
"description": "Code fence style",
"tags": [ "code" ],
"parser": "micromark",
"function": function MD048(params, onError) {
const style = String(params.config.style || "consistent");
let expectedStyle = style;
const codeFenceds = filterByTypesCached([ "codeFenced" ]);
for (const codeFenced of codeFenceds) {
const codeFencedFenceSequence =
getDescendantsByType(codeFenced, [ "codeFencedFence", "codeFencedFenceSequence" ])[0];
const { startLine, text } = codeFencedFenceSequence;
if (expectedStyle === "consistent") {
expectedStyle = fencedCodeBlockStyleFor(text);
}
addErrorDetailIf(
onError,
startLine,
expectedStyle,
fencedCodeBlockStyleFor(text)
);
}
}
};
+115
View File
@@ -0,0 +1,115 @@
// @ts-check
import { addError } from "../helpers/helpers.cjs";
import { filterByPredicate, getDescendantsByType } from "../helpers/micromark-helpers.cjs";
const intrawordRe = /^\w$/;
/**
* Return the string representation of a emphasis or strong markup character.
*
* @param {string} markup Emphasis or strong string.
* @returns {"asterisk" | "underscore"} String representation.
*/
function emphasisOrStrongStyleFor(markup) {
switch (markup[0]) {
case "*":
return "asterisk";
default:
return "underscore";
}
};
/**
* @param {import("markdownlint").RuleParams} params Rule parameters.
* @param {import("markdownlint").RuleOnError} onError Error-reporting callback.
* @param {import("micromark-util-types").TokenType} type Token type.
* @param {import("micromark-util-types").TokenType} typeSequence Token sequence type.
* @param {"*" | "**"} asterisk Asterisk kind.
* @param {"_" | "__"} underline Underline kind.
* @param {"asterisk" | "consistent" | "underscore"} style Style string.
*/
const impl =
(params, onError, type, typeSequence, asterisk, underline, style = "consistent") => {
const { lines, parsers } = params;
const emphasisTokens = filterByPredicate(
parsers.micromark.tokens,
(token) => token.type === type,
(token) => ((token.type === "htmlFlow") ? [] : token.children)
);
for (const token of emphasisTokens) {
const sequences = getDescendantsByType(token, [ typeSequence ]);
const startSequence = sequences[0];
const endSequence = sequences[sequences.length - 1];
if (startSequence && endSequence) {
const markupStyle = emphasisOrStrongStyleFor(startSequence.text);
if (style === "consistent") {
style = markupStyle;
}
if (style !== markupStyle) {
const underscoreIntraword = (style === "underscore") && (
intrawordRe.test(
lines[startSequence.startLine - 1][startSequence.startColumn - 2]
) ||
intrawordRe.test(
lines[endSequence.endLine - 1][endSequence.endColumn - 1]
)
);
if (!underscoreIntraword) {
for (const sequence of [ startSequence, endSequence ]) {
addError(
onError,
sequence.startLine,
`Expected: ${style}; Actual: ${markupStyle}`,
undefined,
[ sequence.startColumn, sequence.text.length ],
{
"editColumn": sequence.startColumn,
"deleteCount": sequence.text.length,
"insertText": (style === "asterisk") ? asterisk : underline
}
);
}
}
}
}
}
};
/** @type {import("markdownlint").Rule[]} */
export default [
{
"names": [ "MD049", "emphasis-style" ],
"description": "Emphasis style",
"tags": [ "emphasis" ],
"parser": "micromark",
"function": function MD049(params, onError) {
return impl(
params,
onError,
"emphasis",
"emphasisSequence",
"*",
"_",
params.config.style || undefined
);
}
},
{
"names": [ "MD050", "strong-style" ],
"description": "Strong style",
"tags": [ "emphasis" ],
"parser": "micromark",
"function": function MD050(params, onError) {
return impl(
params,
onError,
"strong",
"strongSequence",
"**",
"__",
params.config.style || undefined
);
}
}
];
+172
View File
@@ -0,0 +1,172 @@
// @ts-check
import { addError, getHtmlAttributeRe } from "../helpers/helpers.cjs";
import { filterByPredicate, filterByTypes, getHtmlTagInfo, isDocfxTab } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
// Regular expression for identifying HTML anchor names
const idRe = getHtmlAttributeRe("id");
const nameRe = getHtmlAttributeRe("name");
const anchorRe = /\{(#[a-z\d]+(?:[-_][a-z\d]+)*)\}/gu;
const lineFragmentRe = /^#(?:L\d+(?:C\d+)?-L\d+(?:C\d+)?|L\d+)$/;
// Sets for filtering heading tokens during conversion
const childrenExclude = new Set([ "image", "reference", "resource" ]);
const tokensInclude = new Set(
[ "characterEscapeValue", "codeTextData", "data", "mathTextData" ]
);
/**
* Converts a Markdown heading into an HTML fragment according to the rules
* used by GitHub.
*
* @param {import("markdownlint").MicromarkToken} headingText Heading text token.
* @returns {string} Fragment string for heading.
*/
function convertHeadingToHTMLFragment(headingText) {
const inlineText =
filterByPredicate(
headingText.children,
(token) => tokensInclude.has(token.type),
(token) => (childrenExclude.has(token.type) ? [] : token.children)
)
.map((token) => token.text)
.join("");
return "#" + encodeURIComponent(
inlineText
.toLowerCase()
// RegExp source with Ruby's \p{Word} expanded into its General Categories
// https://github.com/gjtorikian/html-pipeline/blob/main/lib/html/pipeline/toc_filter.rb
// https://ruby-doc.org/core-3.0.2/Regexp.html
.replace(
/[^\p{Letter}\p{Mark}\p{Number}\p{Connector_Punctuation}\- ]/gu,
""
)
.replace(/ /gu, "-")
);
}
/**
* Unescapes the text of a String-type micromark Token.
*
* @param {import("markdownlint").MicromarkToken} token String-type micromark Token.
* @returns {string} Unescaped token text.
*/
function unescapeStringTokenText(token) {
return filterByTypes(token.children, [ "characterEscapeValue", "data" ])
.map((child) => child.text)
.join("");
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD051", "link-fragments" ],
"description": "Link fragments should be valid",
"tags": [ "links" ],
"parser": "micromark",
"function": function MD051(params, onError) {
const ignoreCase = params.config.ignore_case || false;
const ignoredPattern = params.config.ignored_pattern || "";
const ignoredPatternRe = new RegExp(ignoredPattern || "^$");
/** @type {Map<string, number>} */
const fragments = new Map([ [ "#top", 0 ] ]);
// Process headings
const headingTexts = filterByTypesCached([ "atxHeadingText", "setextHeadingText" ]);
for (const headingText of headingTexts) {
const fragment = convertHeadingToHTMLFragment(headingText);
if (fragment !== "#") {
const count = fragments.get(fragment) || 0;
if (count) {
fragments.set(`${fragment}-${count}`, 0);
}
fragments.set(fragment, count + 1);
let match = null;
while ((match = anchorRe.exec(headingText.text)) !== null) {
const [ , anchor ] = match;
if (!fragments.has(anchor)) {
fragments.set(anchor, 1);
}
}
}
}
// Process HTML anchors
for (const token of filterByTypesCached([ "htmlText" ], true)) {
const htmlTagInfo = getHtmlTagInfo(token);
if (htmlTagInfo && !htmlTagInfo.close) {
const anchorMatch = idRe.exec(token.text) ||
(htmlTagInfo.name.toLowerCase() === "a" && nameRe.exec(token.text));
if (anchorMatch && anchorMatch.length > 0) {
fragments.set(`#${anchorMatch[1]}`, 0);
}
}
}
// Process link and definition fragments
/** @type {import("markdownlint").MicromarkTokenType[][]} */
const parentChilds = [
[ "link", "resourceDestinationString" ],
[ "definition", "definitionDestinationString" ]
];
for (const [ parentType, definitionType ] of parentChilds) {
const links = filterByTypesCached([ parentType ])
.filter(
(link) => !((link.parent?.type === "atxHeadingText") && isDocfxTab(link.parent.parent))
);
for (const link of links) {
const definitions = filterByTypes(link.children, [ definitionType ]);
for (const definition of definitions) {
const { endColumn, startColumn } = definition;
const text = unescapeStringTokenText(definition);
const textSliceOne = text.slice(1);
const encodedText = `#${encodeURIComponent(textSliceOne)}`;
if (
(text.length > 1) &&
text.startsWith("#") &&
!fragments.has(encodedText) &&
!lineFragmentRe.test(encodedText) &&
!ignoredPatternRe.test(textSliceOne)
) {
let context = undefined;
let range = undefined;
let fixInfo = undefined;
if (link.startLine === link.endLine) {
context = link.text;
range = [ link.startColumn, link.endColumn - link.startColumn ];
fixInfo = {
"editColumn": startColumn,
"deleteCount": endColumn - startColumn
};
}
const textLower = text.toLowerCase();
const mixedCaseKey = [ ...fragments.keys() ]
.find((key) => textLower === key.toLowerCase());
if (mixedCaseKey) {
// @ts-ignore
(fixInfo || {}).insertText = mixedCaseKey;
if (!ignoreCase && (mixedCaseKey !== text)) {
addError(
onError,
link.startLine,
`Expected: ${mixedCaseKey}; Actual: ${text}`,
context,
range,
fixInfo
);
}
} else {
addError(
onError,
link.startLine,
undefined,
context,
range
);
}
}
}
}
}
}
};
+40
View File
@@ -0,0 +1,40 @@
// @ts-check
import { addError } from "../helpers/helpers.cjs";
import { getReferenceLinkImageData } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD052", "reference-links-images" ],
"description":
"Reference links and images should use a label that is defined",
"tags": [ "images", "links" ],
"parser": "none",
"function": function MD052(params, onError) {
const { config, lines } = params;
const shortcutSyntax = config.shortcut_syntax || false;
const ignoredLabels = new Set(config.ignored_labels || [ "x" ]);
const { definitions, references, shortcuts } = getReferenceLinkImageData();
const entries = shortcutSyntax ?
[ ...references.entries(), ...shortcuts.entries() ] :
references.entries();
// Look for links/images that use an undefined link reference
for (const reference of entries) {
const [ label, datas ] = reference;
if (!definitions.has(label) && !ignoredLabels.has(label)) {
for (const data of datas) {
const [ lineIndex, index, length ] = data;
// Context will be incomplete if reporting for a multi-line link
const context = lines[lineIndex].slice(index, index + length);
addError(
onError,
lineIndex + 1,
`Missing link or image reference definition: "${label}"`,
context,
[ index + 1, context.length ]
);
}
}
}
}
};
+60
View File
@@ -0,0 +1,60 @@
// @ts-check
import { addError, ellipsify } from "../helpers/helpers.cjs";
import { getReferenceLinkImageData } from "./cache.mjs";
const linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])\]:/;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD053", "link-image-reference-definitions" ],
"description": "Link and image reference definitions should be needed",
"tags": [ "images", "links" ],
"parser": "none",
"function": function MD053(params, onError) {
const ignored = new Set(params.config.ignored_definitions || [ "//" ]);
const lines = params.lines;
const { references, shortcuts, definitions, duplicateDefinitions } =
getReferenceLinkImageData();
const singleLineDefinition = (/** @type {string} */ line) => (
line.replace(linkReferenceDefinitionRe, "").trim().length > 0
);
const deleteFixInfo = {
"deleteCount": -1
};
// Look for unused link references (unreferenced by any link/image)
for (const definition of definitions.entries()) {
const [ label, [ lineIndex ] ] = definition;
if (
!ignored.has(label) &&
!references.has(label) &&
!shortcuts.has(label)
) {
const line = lines[lineIndex];
addError(
onError,
lineIndex + 1,
`Unused link or image reference definition: "${label}"`,
ellipsify(line),
[ 1, line.length ],
singleLineDefinition(line) ? deleteFixInfo : undefined
);
}
}
// Look for duplicate link references (defined more than once)
for (const duplicateDefinition of duplicateDefinitions) {
const [ label, lineIndex ] = duplicateDefinition;
if (!ignored.has(label)) {
const line = lines[lineIndex];
addError(
onError,
lineIndex + 1,
`Duplicate link or image reference definition: "${label}"`,
ellipsify(line),
[ 1, line.length ],
singleLineDefinition(line) ? deleteFixInfo : undefined
);
}
}
}
};
+122
View File
@@ -0,0 +1,122 @@
// @ts-check
import { addErrorContext, nextLinesRe } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
const backslashEscapeRe = /\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g;
const removeBackslashEscapes = (/** @type {string} **/ text) => text.replace(backslashEscapeRe, "$1");
const autolinkDisallowedRe = /[ <>]/;
const autolinkAble = (/** @type {string} */ destination) => {
try {
// eslint-disable-next-line no-new
new URL(destination);
} catch {
// Not an absolute URL
return false;
}
return !autolinkDisallowedRe.test(destination);
};
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD054", "link-image-style" ],
"description": "Link and image style",
"tags": [ "images", "links" ],
"parser": "micromark",
"function": (params, onError) => {
const config = params.config;
const autolink = (config.autolink === undefined) || !!config.autolink;
const inline = (config.inline === undefined) || !!config.inline;
const full = (config.full === undefined) || !!config.full;
const collapsed = (config.collapsed === undefined) || !!config.collapsed;
const shortcut = (config.shortcut === undefined) || !!config.shortcut;
const urlInline = (config.url_inline === undefined) || !!config.url_inline;
if (autolink && inline && full && collapsed && shortcut && urlInline) {
// Everything allowed, nothing to check
return;
}
const { definitions } = getReferenceLinkImageData();
const links = filterByTypesCached([ "autolink", "image", "link" ]);
for (const link of links) {
let label = null;
let destination = null;
const {
endColumn, endLine, startColumn, startLine, text, type
} = link;
const image = (type === "image");
let isError = false;
if (type === "autolink") {
// link kind is an autolink
destination = getDescendantsByType(link, [ [ "autolinkEmail", "autolinkProtocol" ] ])[0]?.text;
label = destination;
isError = !autolink && Boolean(destination);
} else {
// link type is "image" or "link"
label = getDescendantsByType(link, [ "label", "labelText" ])[0].text;
destination =
getDescendantsByType(link, [ "resource", "resourceDestination", [ "resourceDestinationLiteral", "resourceDestinationRaw" ], "resourceDestinationString" ])[0]?.text;
if (destination) {
// link kind is an inline link
const title = getDescendantsByType(link, [ "resource", "resourceTitle", "resourceTitleString" ])[0]?.text;
isError = !inline || (
!urlInline &&
autolink &&
!image &&
!title &&
(label === destination) &&
autolinkAble(destination)
);
} else {
// link kind is a full/collapsed/shortcut reference link
const isShortcut = getDescendantsByType(link, [ "reference" ]).length === 0;
const referenceString = getDescendantsByType(link, [ "reference", "referenceString" ])[0]?.text;
const isCollapsed = (referenceString === undefined);
const definition = definitions.get(referenceString || label);
destination = (definition && definition[1]) || "";
isError = Boolean(
destination &&
(isShortcut ? !shortcut : (isCollapsed ? !collapsed : !full))
);
}
}
if (isError) {
let range = undefined;
let fixInfo = undefined;
if (startLine === endLine) {
range = [ startColumn, endColumn - startColumn ];
let insertText = null;
const canInline = (inline && label);
const canAutolink = (autolink && !image && autolinkAble(destination));
if (canInline && (urlInline || !canAutolink)) {
// Most useful form
const prefix = (image ? "!" : "");
// @ts-ignore
const escapedLabel = label.replace(/[[\]]/g, "\\$&");
const escapedDestination = destination.replace(/[()]/g, "\\$&");
insertText = `${prefix}[${escapedLabel}](${escapedDestination})`;
} else if (canAutolink) {
// Simplest form
insertText = `<${removeBackslashEscapes(destination)}>`;
}
if (insertText) {
fixInfo = {
"editColumn": range[0],
insertText,
"deleteCount": range[1]
};
}
}
addErrorContext(
onError,
startLine,
text.replace(nextLinesRe, ""),
undefined,
undefined,
range,
fixInfo
);
}
}
}
};
+72
View File
@@ -0,0 +1,72 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("micromark-extension-gfm-table")} */
const whitespaceTypes = new Set([ "linePrefix", "whitespace" ]);
const ignoreWhitespace = (/** @type {MicromarkToken[]} */ tokens) => tokens.filter(
(token) => !whitespaceTypes.has(token.type)
);
const firstOrNothing = (/** @type {MicromarkToken[]} */ items) => items[0];
const lastOrNothing = (/** @type {MicromarkToken[]} */ items) => items[items.length - 1];
const makeRange = (/** @type {number} */ start, /** @type {number} */ end) => [ start, end - start + 1 ];
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD055", "table-pipe-style" ],
"description": "Table pipe style",
"tags": [ "table" ],
"parser": "micromark",
"function": function MD055(params, onError) {
const style = String(params.config.style || "consistent");
let expectedStyle = style;
let expectedLeadingPipe =
((expectedStyle !== "no_leading_or_trailing") && (expectedStyle !== "trailing_only"));
let expectedTrailingPipe =
((expectedStyle !== "no_leading_or_trailing") && (expectedStyle !== "leading_only"));
const rows = filterByTypesCached([ "tableDelimiterRow", "tableRow" ]);
for (const row of rows) {
// The following uses of first/lastOrNothing lack fallback handling
// because it seems not to be possible (i.e., 0% coverage)
const firstCell = firstOrNothing(row.children);
const leadingToken = firstOrNothing(ignoreWhitespace(firstCell.children));
const actualLeadingPipe = (leadingToken.type === "tableCellDivider");
const lastCell = lastOrNothing(row.children);
const trailingToken = lastOrNothing(ignoreWhitespace(lastCell.children));
const actualTrailingPipe = (trailingToken.type === "tableCellDivider");
const actualStyle = actualLeadingPipe ?
(actualTrailingPipe ? "leading_and_trailing" : "leading_only") :
(actualTrailingPipe ? "trailing_only" : "no_leading_or_trailing");
if (expectedStyle === "consistent") {
expectedStyle = actualStyle;
expectedLeadingPipe = actualLeadingPipe;
expectedTrailingPipe = actualTrailingPipe;
}
if (actualLeadingPipe !== expectedLeadingPipe) {
addErrorDetailIf(
onError,
firstCell.startLine,
expectedStyle,
actualStyle,
`${expectedLeadingPipe ? "Missing" : "Unexpected"} leading pipe`,
undefined,
makeRange(row.startColumn, firstCell.startColumn)
);
}
if (actualTrailingPipe !== expectedTrailingPipe) {
addErrorDetailIf(
onError,
lastCell.endLine,
expectedStyle,
actualStyle,
`${expectedTrailingPipe ? "Missing" : "Unexpected"} trailing pipe`,
undefined,
makeRange(lastCell.endColumn - 1, row.endColumn - 1)
);
}
}
}
};
+50
View File
@@ -0,0 +1,50 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const makeRange = (/** @type {number} */ start, /** @type {number} */ end) => [ start, end - start + 1 ];
/** @typedef {import("micromark-extension-gfm-table")} */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD056", "table-column-count" ],
"description": "Table column count",
"tags": [ "table" ],
"parser": "micromark",
"function": function MD056(params, onError) {
const rows = filterByTypesCached([ "tableDelimiterRow", "tableRow" ]);
let expectedCount = 0;
let currentTable = null;
for (const row of rows) {
const table = getParentOfType(row, [ "table" ]);
if (currentTable !== table) {
expectedCount = 0;
currentTable = table;
}
const cells = row.children.filter((child) => [ "tableData", "tableDelimiter", "tableHeader" ].includes(child.type));
const actualCount = cells.length;
expectedCount ||= actualCount;
let detail = undefined;
let range = undefined;
if (actualCount < expectedCount) {
detail = "Too few cells, row will be missing data";
range = [ row.endColumn - 1, 1 ];
} else if (expectedCount < actualCount) {
detail = "Too many cells, extra data will be missing";
range = makeRange(cells[expectedCount].startColumn, row.endColumn - 1);
}
addErrorDetailIf(
onError,
row.endLine,
expectedCount,
actualCount,
detail,
undefined,
range
);
}
}
};
+57
View File
@@ -0,0 +1,57 @@
// @ts-check
import { addErrorContext, isBlankLine } from "../helpers/helpers.cjs";
import { getBlockQuotePrefixText } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("micromark-extension-gfm-table")} */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD058", "blanks-around-tables" ],
"description": "Tables should be surrounded by blank lines",
"tags": [ "table" ],
"parser": "micromark",
"function": function MD058(params, onError) {
const { lines } = params;
const blockQuotePrefixes = filterByTypesCached([ "blockQuotePrefix", "linePrefix" ]);
// For every table...
const tables = filterByTypesCached([ "table" ]);
for (const table of tables) {
// Look for a blank line above the table
const firstLineNumber = table.startLine;
if (!isBlankLine(lines[firstLineNumber - 2])) {
addErrorContext(
onError,
firstLineNumber,
lines[firstLineNumber - 1].trim(),
undefined,
undefined,
undefined,
{
"insertText": getBlockQuotePrefixText(blockQuotePrefixes, firstLineNumber)
}
);
}
// Look for a blank line below the table
const lastLineNumber = table.endLine;
if (!isBlankLine(lines[lastLineNumber])) {
addErrorContext(
onError,
lastLineNumber,
lines[lastLineNumber - 1].trim(),
undefined,
undefined,
undefined,
{
"lineNumber": lastLineNumber + 1,
"insertText": getBlockQuotePrefixText(blockQuotePrefixes, lastLineNumber)
}
);
}
}
}
};
+71
View File
@@ -0,0 +1,71 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
/** @type {Set<MicromarkTokenType>} */
const allowedChildrenTypes = new Set([
"codeText",
"htmlText"
]);
const defaultProhibitedTexts = [
"click here",
"here",
"link",
"more"
];
/**
* Normalizes a string by removing extra whitespaces and punctuation.
*
* @param {string} str String to normalize.
* @returns {string} Normalized string.
*/
function normalize(str) {
return str
.replace(/[\W_]+/g, " ")
.replace(/\s+/g, " ")
.toLowerCase()
.trim();
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD059", "descriptive-link-text" ],
"description": "Link text should be descriptive",
"tags": [ "accessibility", "links" ],
"parser": "micromark",
"function": function MD059(params, onError) {
const prohibitedTexts = new Set(
(params.config.prohibited_texts || defaultProhibitedTexts).map(normalize)
);
if (prohibitedTexts.size > 0) {
const links = filterByTypesCached([ "link" ]);
for (const link of links) {
const labelTexts = getDescendantsByType(link, [ "label", "labelText" ]);
for (const labelText of labelTexts) {
const { children, endColumn, endLine, parent, startColumn, startLine, text } = labelText;
if (
!children.some((child) => allowedChildrenTypes.has(child.type)) &&
prohibitedTexts.has(normalize(text))
) {
const range = (startLine === endLine) ?
[ startColumn, endColumn - startColumn ] :
undefined;
addErrorContext(
onError,
startLine,
// @ts-ignore
parent.text,
undefined,
undefined,
range
);
}
}
}
}
}
};
+170
View File
@@ -0,0 +1,170 @@
// @ts-check
import { filterByTypes } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
import stringWidth from "string-width";
/** @typedef {import("micromark-extension-gfm-table")} */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */
/**
* Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
*
* @param {RuleOnErrorInfo[]} errors List of errors.
* @param {number} lineNumber Line number.
* @param {number} column Column number.
* @param {string} detail Detail message.
*/
function addError(errors, lineNumber, column, detail) {
errors.push({
lineNumber,
detail,
"range": [ column, 1 ]
});
}
/**
* @typedef Column
* @property {number} actual Actual column (1-based).
* @property {number} effective Effective column (1-based).
*/
/**
* Gets a list of table cell divider columns.
*
* @param {readonly string[]} lines File/string lines.
* @param {MicromarkToken} row Micromark row token.
* @returns {Column[]} Divider columns.
*/
function getTableDividerColumns(lines, row) {
return filterByTypes(
row.children,
[ "tableCellDivider" ]
).map(
(divider) => ({
"actual": divider.startColumn,
"effective": stringWidth(lines[row.startLine - 1].slice(0, divider.startColumn - 1))
})
);
}
/**
* Checks the specified table rows for consistency with the "aligned" style.
*
* @param {readonly string[]} lines File/string lines.
* @param {MicromarkToken[]} rows Micromark row tokens.
* @param {string} detail Detail message.
* @returns {RuleOnErrorInfo[]} List of errors.
*/
function checkStyleAligned(lines, rows, detail) {
/** @type {RuleOnErrorInfo[]} */
const errorInfos = [];
const headerRow = rows[0];
const headerDividerColumns = getTableDividerColumns(lines, headerRow);
for (const row of rows.slice(1)) {
const remainingHeaderDividerColumns = new Set(headerDividerColumns.map((column) => column.effective));
const rowDividerColumns = getTableDividerColumns(lines, row);
for (const dividerColumn of rowDividerColumns) {
if ((remainingHeaderDividerColumns.size > 0) && !remainingHeaderDividerColumns.delete(dividerColumn.effective)) {
addError(errorInfos, row.startLine, dividerColumn.actual, detail);
}
}
}
return errorInfos;
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD060", "table-column-style" ],
"description": "Table column style",
"tags": [ "table" ],
"parser": "micromark",
"function": function MD060(params, onError) {
const style = String(params.config.style || "any");
const styleAlignedAllowed = (style === "any") || (style === "aligned");
const styleCompactAllowed = (style === "any") || (style === "compact");
const styleTightAllowed = (style === "any") || (style === "tight");
const alignedDelimiter = !!params.config.aligned_delimiter;
const lines = params.lines;
// Scan all tables/rows
const tables = filterByTypesCached([ "table" ]);
for (const table of tables) {
const rows = filterByTypes(table.children, [ "tableDelimiterRow", "tableRow" ]);
// Determine errors for style "aligned"
/** @type {RuleOnErrorInfo[]} */
const errorsIfAligned = [];
if (styleAlignedAllowed) {
errorsIfAligned.push(...checkStyleAligned(lines, rows, "Table pipe does not align with header for style \"aligned\""));
}
// Determine errors for styles "compact" and "tight"
/** @type {RuleOnErrorInfo[]} */
const errorsIfCompact = [];
/** @type {RuleOnErrorInfo[]} */
const errorsIfTight = [];
if (
(styleCompactAllowed || styleTightAllowed) &&
!(styleAlignedAllowed && (errorsIfAligned.length === 0))
) {
if (alignedDelimiter) {
const errorInfos = checkStyleAligned(lines, rows.slice(0, 2), "Table pipe does not align with header for option \"aligned_delimiter\"");
errorsIfCompact.push(...errorInfos);
errorsIfTight.push(...errorInfos);
}
for (const row of rows) {
const tokensOfInterest = filterByTypes(row.children, [ "tableCellDivider", "tableContent", "whitespace" ]);
for (let i = 0; i < tokensOfInterest.length; i++) {
const { startColumn, startLine, type } = tokensOfInterest[i];
if (type === "tableCellDivider") {
const previous = tokensOfInterest[i - 1];
if (previous) {
if (previous.type === "whitespace") {
if (previous.text.length !== 1) {
addError(errorsIfCompact, startLine, startColumn, "Table pipe has extra space to the left for style \"compact\"");
}
addError(errorsIfTight, startLine, startColumn, "Table pipe has space to the left for style \"tight\"");
} else {
addError(errorsIfCompact, startLine, startColumn, "Table pipe is missing space to the left for style \"compact\"");
}
}
const next = tokensOfInterest[i + 1];
if (next) {
if (next.type === "whitespace") {
if (next.endColumn !== row.endColumn) {
if (next.text.length !== 1) {
addError(errorsIfCompact, startLine, startColumn, "Table pipe has extra space to the right for style \"compact\"");
}
addError(errorsIfTight, startLine, startColumn, "Table pipe has space to the right for style \"tight\"");
}
} else {
addError(errorsIfCompact, startLine, startColumn, "Table pipe is missing space to the right for style \"compact\"");
}
}
}
}
}
}
// Report errors for whatever (allowed) style has the fewest
let errorInfos = errorsIfAligned;
if (
styleCompactAllowed &&
((errorsIfCompact.length < errorInfos.length) || !styleAlignedAllowed)
) {
errorInfos = errorsIfCompact;
}
if (
styleTightAllowed &&
((errorsIfTight.length < errorInfos.length) || (!styleAlignedAllowed && !styleCompactAllowed))
) {
errorInfos = errorsIfTight;
}
for (const errorInfo of errorInfos) {
onError(errorInfo);
}
}
}
};
+322
View File
@@ -0,0 +1,322 @@
// @ts-check
import { directive } from "micromark-extension-directive";
import { gfmAutolinkLiteral } from "micromark-extension-gfm-autolink-literal";
import { gfmFootnote } from "micromark-extension-gfm-footnote";
import { gfmTable } from "micromark-extension-gfm-table";
import { math } from "micromark-extension-math";
import { parse as micromarkParse, postprocess as micromarkPostprocess, preprocess as micromarkPreprocess } from "micromark";
// micromark-core-commonmark must exactly match what's used by micromark for the shim below to work correctly
// Unfortunately, omitting this dependency from package.json breaks strict dependency resolution (e.g., pnpm)
import { labelEnd } from "micromark-core-commonmark";
import { isHtmlFlowComment } from "../helpers/micromark-helpers.cjs";
import { flatTokensSymbol, htmlFlowSymbol, newLineRe } from "../helpers/shared.cjs";
/** @typedef {import("micromark-util-types").Event} Event */
/** @typedef {import("micromark-util-types").ParseOptions} MicromarkParseOptions */
/** @typedef {import("micromark-util-types").State} State */
/** @typedef {import("micromark-util-types").Token} Token */
/** @typedef {import("micromark-util-types").Tokenizer} Tokenizer */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("./micromark-types.d.mts")} */
/**
* Gets the Markdown text for a Micromark token.
*
* @param {string} markdown Markdown content.
* @param {Token} token Micromark token.
* @returns {string} Token text.
*/
function getText(markdown, token) {
return markdown.slice(token.start.offset, token.end.offset);
}
/**
* Parse options.
*
* @typedef {Object} ParseOptions
* @property {boolean} [freezeTokens] Whether to freeze output Tokens.
*/
/**
* Parses a Markdown document and returns Micromark events.
*
* @param {string} markdown Markdown document.
* @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
* @returns {Event[]} Micromark events.
*/
export function getEvents(
markdown,
micromarkParseOptions = {}
) {
// Customize extensions list to add useful extensions
const extensions = [
directive(),
gfmAutolinkLiteral(),
gfmFootnote(),
gfmTable(),
math(),
...(micromarkParseOptions.extensions || [])
];
// // Shim labelEnd to identify undefined link labels
/** @type {Event[][]} */
const artificialEventLists = [];
const tokenizeOriginal = labelEnd.tokenize;
/** @type {Tokenizer} */
function tokenizeShim(effects, okOriginal, nokOriginal) {
// eslint-disable-next-line consistent-this, unicorn/no-this-assignment, no-invalid-this
const tokenizeContext = this;
const events = tokenizeContext.events;
/** @type {State} */
const nokShim = (code) => {
// Find start of label (image or link)
let indexStart = events.length;
while (--indexStart >= 0) {
const event = events[indexStart];
const [ kind, token ] = event;
if (kind === "enter") {
const { type } = token;
if ((type === "labelImage") || (type === "labelLink")) {
// Found it
break;
}
}
}
// If found...
if (indexStart >= 0) {
// Create artificial enter/exit events and replicate all data/lineEnding events within
const eventStart = events[indexStart];
const [ , eventStartToken ] = eventStart;
const eventEnd = events[events.length - 1];
const [ , eventEndToken ] = eventEnd;
/** @type {Token} */
const undefinedReferenceType = {
"type": "undefinedReferenceShortcut",
"start": eventStartToken.start,
"end": eventEndToken.end
};
/** @type {Token} */
const undefinedReference = {
"type": "undefinedReference",
"start": eventStartToken.start,
"end": eventEndToken.end
};
const eventsToReplicate = events
.slice(indexStart)
.filter((event) => {
const [ , eventToken ] = event;
const { type } = eventToken;
return (type === "data") || (type === "lineEnding");
});
// Determine the type of the undefined reference
const previousUndefinedEvent = (artificialEventLists.length > 0) && artificialEventLists[artificialEventLists.length - 1][0];
const previousUndefinedToken = previousUndefinedEvent && previousUndefinedEvent[1];
if (
previousUndefinedToken &&
(previousUndefinedToken.end.line === undefinedReferenceType.start.line) &&
(previousUndefinedToken.end.column === undefinedReferenceType.start.column)
) {
// Previous undefined reference event is immediately before this one
if (eventsToReplicate.length === 0) {
// The pair represent a collapsed reference (ex: [...][])
previousUndefinedToken.type = "undefinedReferenceCollapsed";
previousUndefinedToken.end = eventEndToken.end;
} else {
// The pair represent a full reference (ex: [...][...])
undefinedReferenceType.type = "undefinedReferenceFull";
undefinedReferenceType.start = previousUndefinedToken.start;
artificialEventLists.pop();
}
}
// Create artificial event list and replicate content
const text = eventsToReplicate
.filter((event) => event[0] === "enter")
.map((event) => getText(markdown, event[1]))
.join("")
.trim();
if ((text.length > 0) && !text.includes("]")) {
/** @type {Event[]} */
const artificialEvents = [
[ "enter", undefinedReferenceType, tokenizeContext ],
[ "enter", undefinedReference, tokenizeContext ]
];
for (const event of eventsToReplicate) {
const [ kind, token ] = event;
// Copy token because the current object will get modified by the parser
artificialEvents.push([ kind, { ...token }, tokenizeContext ]);
}
artificialEvents.push(
[ "exit", undefinedReference, tokenizeContext ],
[ "exit", undefinedReferenceType, tokenizeContext ]
);
artificialEventLists.push(artificialEvents);
}
}
// Continue with original behavior
return nokOriginal(code);
};
// Shim nok handler of labelEnd's tokenize
return tokenizeOriginal.call(tokenizeContext, effects, okOriginal, nokShim);
}
try {
// Shim labelEnd behavior to detect undefined references
labelEnd.tokenize = tokenizeShim;
// Use micromark to parse document into Events
const encoding = undefined;
const eol = true;
const parseContext = micromarkParse({ ...micromarkParseOptions, extensions });
const chunks = micromarkPreprocess()(markdown, encoding, eol);
const events = micromarkPostprocess(parseContext.document().write(chunks));
// Append artificial events and return all events
// eslint-disable-next-line unicorn/prefer-spread
return events.concat(...artificialEventLists);
} finally {
// Restore shimmed labelEnd behavior
labelEnd.tokenize = tokenizeOriginal;
}
}
/**
* Parses a Markdown document and returns micromark tokens (internal).
*
* @param {string} markdown Markdown document.
* @param {ParseOptions} [parseOptions] Options.
* @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
* @param {number} [lineDelta] Offset for start/end line.
* @param {MicromarkToken} [ancestor] Parent of top-most tokens.
* @returns {MicromarkToken[]} Micromark tokens.
*/
function parseInternal(
markdown,
parseOptions = {},
micromarkParseOptions = {},
lineDelta = 0,
ancestor = undefined
) {
// Get options
const freezeTokens = Boolean(parseOptions.freezeTokens);
// Use micromark to parse document into Events
const events = getEvents(markdown, micromarkParseOptions);
// Create Token objects
/** @type {MicromarkToken[]} */
const document = [];
/** @type {MicromarkToken[]} */
let flatTokens = [];
/** @type {MicromarkToken} */
const root = {
"type": "data",
"startLine": -1,
"startColumn": -1,
"endLine": -1,
"endColumn": -1,
"text": "ROOT",
"children": document,
"parent": null
};
const history = [ root ];
let current = root;
/** @type {MicromarkParseOptions | null} */
let reparseOptions = null;
let lines = null;
let skipHtmlFlowChildren = false;
for (const event of events) {
const [ kind, token ] = event;
const { type, start, end } = token;
const { "column": startColumn, "line": startLine } = start;
const { "column": endColumn, "line": endLine } = end;
const text = getText(markdown, token);
if ((kind === "enter") && !skipHtmlFlowChildren) {
const previous = current;
history.push(previous);
current = {
type,
"startLine": startLine + lineDelta,
startColumn,
"endLine": endLine + lineDelta,
endColumn,
text,
"children": [],
"parent": ((previous === root) ? (ancestor || null) : previous)
};
if (ancestor) {
Object.defineProperty(current, htmlFlowSymbol, { "value": true });
}
previous.children.push(current);
flatTokens.push(current);
if ((current.type === "htmlFlow") && !isHtmlFlowComment(current)) {
skipHtmlFlowChildren = true;
if (!reparseOptions || !lines) {
reparseOptions = {
...micromarkParseOptions,
"extensions": [
{
"disable": {
"null": [ "codeIndented", "htmlFlow" ]
}
}
]
};
lines = markdown.split(newLineRe);
}
const reparseMarkdown = lines
.slice(current.startLine - 1, current.endLine)
.join("\n");
const tokens = parseInternal(
reparseMarkdown,
parseOptions,
reparseOptions,
current.startLine - 1,
current
);
current.children = tokens;
// Avoid stack overflow of Array.push(...spread)
// @ts-ignore
// eslint-disable-next-line unicorn/prefer-spread
flatTokens = flatTokens.concat(tokens[flatTokensSymbol]);
}
} else if (kind === "exit") {
if (type === "htmlFlow") {
skipHtmlFlowChildren = false;
}
if (!skipHtmlFlowChildren) {
if (freezeTokens) {
Object.freeze(current.children);
Object.freeze(current);
}
// @ts-ignore
current = history.pop();
}
}
}
// Return document
Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens });
if (freezeTokens) {
Object.freeze(document);
}
return document;
}
/**
* Parses a Markdown document and returns micromark tokens.
*
* @param {string} markdown Markdown document.
* @param {ParseOptions} [parseOptions] Options.
* @returns {MicromarkToken[]} Micromark tokens.
*/
export function parse(markdown, parseOptions) {
return parseInternal(markdown, parseOptions);
}
+11
View File
@@ -0,0 +1,11 @@
export {};
// Augment TokenTypeMap with markdownlint-specific types.
declare module "micromark-util-types" {
export interface TokenTypeMap {
undefinedReference: "undefinedReference"
undefinedReferenceCollapsed: "undefinedReferenceCollapsed"
undefinedReferenceFull: "undefinedReferenceFull"
undefinedReferenceShortcut: "undefinedReferenceShortcut"
}
}
+22
View File
@@ -0,0 +1,22 @@
// @ts-check
const getError = () => new Error("Node APIs are not available in browser context.");
const throwForSync = () => {
throw getError();
};
export const fs = {
// @ts-ignore
"access": (path, callback) => callback(getError()),
"accessSync": throwForSync,
// @ts-ignore
"readFile": (path, options, callback) => callback(getError()),
"readFileSync": throwForSync
};
export const os = {};
export const path = {
"dirname": throwForSync,
"resolve": throwForSync
};
+11
View File
@@ -0,0 +1,11 @@
// @ts-check
import { access, accessSync, readFile, readFileSync } from "node:fs";
export const fs = { access, accessSync, readFile, readFileSync };
import { EOL, homedir } from "node:os";
export const os = { EOL, homedir };
// eslint-disable-next-line unicorn/import-style
import { dirname, resolve } from "node:path";
export const path = { dirname, resolve };
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
/**
* Result of a call to parseConfiguration.
*
* @typedef {Object} ParseConfigurationResult
* @property {import("markdownlint").Configuration | null} config Configuration object if successful.
* @property {string | null} message Error message if an error occurred.
*/
/**
* Parse the content of a configuration file.
*
* @param {string} name Name of the configuration file.
* @param {string} content Configuration content.
* @param {import("markdownlint").ConfigurationParser[]} [parsers] Parsing function(s).
* @returns {ParseConfigurationResult} Parse configuration result.
*/
export default function parseConfiguration(name, content, parsers) {
let config = null;
let message = null;
const errors = [];
let index = 0;
// Try each parser
const failed = (parsers || [ JSON.parse ]).every((parser) => {
try {
const result = parser(content);
config = (result && (typeof result === "object") && !Array.isArray(result)) ? result : {};
// Succeeded
return false;
// eslint-disable-next-line jsdoc/reject-any-type
} catch(/** @type {any} */ error) {
errors.push(`Parser ${index++}: ${error?.message}`);
}
// Failed, try the next parser
return true;
});
// Message if unable to parse
if (failed) {
errors.unshift(`Unable to parse '${name}'`);
message = errors.join("; ");
}
return {
config,
message
};
}
+52
View File
@@ -0,0 +1,52 @@
// @ts-check
"use strict";
// @ts-ignore
// eslint-disable-next-line camelcase, no-inline-comments, no-undef
const nativeRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__;
// Captures the native require implementation (even under webpack).
/**
* @typedef RequireResolveOptions
* @property {string[]} [paths] Additional paths to resolve from.
*/
/**
* @callback RequireResolve
* @param {string} id Module name or path.
* @param {RequireResolveOptions} options Options to apply.
* @returns {string} Resolved module path.
*/
/**
* Resolves modules according to Node's resolution rules.
*
* @param {RequireResolve} resolve Node-like require.resolve implementation.
* @param {string} id Module name or path.
* @param {string[]} [paths] Additional paths to resolve from.
* @returns {string} Resolved module path.
*/
const resolveModuleCustomResolve = (resolve, id, paths = []) => {
// resolve.paths is sometimes not present under webpack or VS Code
// @ts-ignore
const resolvePaths = resolve.paths?.("") || [];
const allPaths = [ ...paths, ...resolvePaths ];
return resolve(id, { "paths": allPaths });
};
/**
* Resolves modules according to Node's resolution rules.
*
* @param {string} id Module name or path.
* @param {string[]} [paths] Additional paths to resolve from.
* @returns {string} Resolved module path.
*/
const resolveModule = (id, paths) => (
resolveModuleCustomResolve(nativeRequire.resolve, id, paths)
);
module.exports = {
resolveModule,
resolveModuleCustomResolve
};
+34
View File
@@ -0,0 +1,34 @@
export type RequireResolveOptions = {
/**
* Additional paths to resolve from.
*/
paths?: string[];
};
export type RequireResolve = (id: string, options: RequireResolveOptions) => string;
/**
* Resolves modules according to Node's resolution rules.
*
* @param {string} id Module name or path.
* @param {string[]} [paths] Additional paths to resolve from.
* @returns {string} Resolved module path.
*/
export function resolveModule(id: string, paths?: string[]): string;
/**
* @typedef RequireResolveOptions
* @property {string[]} [paths] Additional paths to resolve from.
*/
/**
* @callback RequireResolve
* @param {string} id Module name or path.
* @param {RequireResolveOptions} options Options to apply.
* @returns {string} Resolved module path.
*/
/**
* Resolves modules according to Node's resolution rules.
*
* @param {RequireResolve} resolve Node-like require.resolve implementation.
* @param {string} id Module name or path.
* @param {string[]} [paths] Additional paths to resolve from.
* @returns {string} Resolved module path.
*/
export function resolveModuleCustomResolve(resolve: RequireResolve, id: string, paths?: string[]): string;
+122
View File
@@ -0,0 +1,122 @@
// @ts-check
import { homepage, version } from "./constants.mjs";
import md001 from "./md001.mjs";
import md003 from "./md003.mjs";
import md004 from "./md004.mjs";
import md005 from "./md005.mjs";
import md007 from "./md007.mjs";
import md009 from "./md009.mjs";
import md010 from "./md010.mjs";
import md011 from "./md011.mjs";
import md012 from "./md012.mjs";
import md013 from "./md013.mjs";
import md014 from "./md014.mjs";
import md018 from "./md018.mjs";
import md019md021 from "./md019-md021.mjs";
const [ md019, md021 ] = md019md021;
import md020 from "./md020.mjs";
import md022 from "./md022.mjs";
import md023 from "./md023.mjs";
import md024 from "./md024.mjs";
import md025 from "./md025.mjs";
import md026 from "./md026.mjs";
import md027 from "./md027.mjs";
import md028 from "./md028.mjs";
import md029 from "./md029.mjs";
import md030 from "./md030.mjs";
import md031 from "./md031.mjs";
import md032 from "./md032.mjs";
import md033 from "./md033.mjs";
import md034 from "./md034.mjs";
import md035 from "./md035.mjs";
import md036 from "./md036.mjs";
import md037 from "./md037.mjs";
import md038 from "./md038.mjs";
import md039 from "./md039.mjs";
import md040 from "./md040.mjs";
import md041 from "./md041.mjs";
import md042 from "./md042.mjs";
import md043 from "./md043.mjs";
import md044 from "./md044.mjs";
import md045 from "./md045.mjs";
import md046 from "./md046.mjs";
import md047 from "./md047.mjs";
import md048 from "./md048.mjs";
import md049md050 from "./md049-md050.mjs";
const [ md049, md050 ] = md049md050;
import md051 from "./md051.mjs";
import md052 from "./md052.mjs";
import md053 from "./md053.mjs";
import md054 from "./md054.mjs";
import md055 from "./md055.mjs";
import md056 from "./md056.mjs";
import md058 from "./md058.mjs";
import md059 from "./md059.mjs";
import md060 from "./md060.mjs";
const rules = [
md001,
// md002: Deprecated and removed
md003,
md004,
md005,
// md006: Deprecated and removed
md007,
md009,
md010,
md011,
md012,
md013,
md014,
md018,
md019,
md020,
md021,
md022,
md023,
md024,
md025,
md026,
md027,
md028,
md029,
md030,
md031,
md032,
md033,
md034,
md035,
md036,
md037,
md038,
md039,
md040,
md041,
md042,
md043,
md044,
md045,
md046,
md047,
md048,
md049,
md050,
md051,
md052,
md053,
md054,
md055,
md056,
// md057: See https://github.com/markdownlint/markdownlint
md058,
md059,
md060
];
for (const rule of rules) {
const name = rule.names[0].toLowerCase();
// eslint-disable-next-line dot-notation
rule["information"] = new URL(`${homepage}/blob/v${version}/doc/${name}.md`);
}
export default rules;
+13
View File
@@ -0,0 +1,13 @@
export {};
declare module "markdownlint" {
export * from "./exports.mjs";
}
declare module "markdownlint/async" {
export * from "./exports-async.mjs";
}
declare module "markdownlint/promise" {
export * from "./exports-promise.mjs";
}
declare module "markdownlint/sync" {
export * from "./exports-sync.mjs";
}