feat: use go tool
This commit is contained in:
+80
@@ -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())
|
||||
);
|
||||
}
|
||||
+2409
File diff suppressed because it is too large
Load Diff
+8
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export { lintAsync as lint, readConfigAsync as readConfig } from "./markdownlint.mjs";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
export { lintAsync as lint, readConfigAsync as readConfig } from "./markdownlint.mjs";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { extendConfigPromise as extendConfig, lintPromise as lint, readConfigPromise as readConfig } from "./markdownlint.mjs";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
export { extendConfigPromise as extendConfig, lintPromise as lint, readConfigPromise as readConfig } from "./markdownlint.mjs";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { lintSync as lint, readConfigSync as readConfig } from "./markdownlint.mjs";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
export { lintSync as lint, readConfigSync as readConfig } from "./markdownlint.mjs";
|
||||
+29
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+32
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user