first commit

This commit is contained in:
2026-08-06 12:00:39 +10:00
commit 91221f5a8e
3259 changed files with 569069 additions and 0 deletions
@@ -0,0 +1,49 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let missingLanguage: string;
let disallowedLanguage: string;
}
let schema: {
type: "object";
properties: {
required: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
required: any[];
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: FencedCodeLanguageOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: FencedCodeLanguageMessageIds;
}>): {
code(node: import("mdast").Code): void;
};
}
export default _default;
export type FencedCodeLanguageMessageIds = "missingLanguage" | "disallowedLanguage";
export type FencedCodeLanguageOptions = [{
required?: string[];
}];
export type FencedCodeLanguageRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: FencedCodeLanguageOptions;
MessageIds: FencedCodeLanguageMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,106 @@
/**
* @fileoverview Rule to enforce languages for fenced code.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"missingLanguage" | "disallowedLanguage"} FencedCodeLanguageMessageIds
* @typedef {[{ required?: string[] }]} FencedCodeLanguageOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: FencedCodeLanguageOptions, MessageIds: FencedCodeLanguageMessageIds }>} FencedCodeLanguageRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const fencedCodeCharacters = new Set(["`", "~"]);
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {FencedCodeLanguageRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Require languages for fenced code blocks",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/fenced-code-language.md",
},
messages: {
missingLanguage: "Missing code block language.",
disallowedLanguage: 'Code block language "{{lang}}" is not allowed.',
},
schema: [
{
type: "object",
properties: {
required: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
required: [],
},
],
},
create(context) {
const required = new Set(context.options[0].required);
const { sourceCode } = context;
return {
code(node) {
if (!node.lang) {
// only check fenced code blocks
if (!fencedCodeCharacters.has(sourceCode.text[node.position.start.offset])) {
return;
}
/** @type {number} */
let openingCodeFenceEndOffset;
for (openingCodeFenceEndOffset = node.position.start.offset; fencedCodeCharacters.has(sourceCode.text[openingCodeFenceEndOffset]); openingCodeFenceEndOffset++) {
// Find the end offset of the opening code fence.
}
context.report({
loc: {
start: node.position.start,
end: {
line: node.position.start.line,
column: node.position.start.column +
openingCodeFenceEndOffset -
node.position.start.offset,
},
},
messageId: "missingLanguage",
});
return;
}
if (required.size && !required.has(node.lang)) {
const langIndex = sourceCode
.getText(node)
.indexOf(node.lang);
context.report({
loc: {
start: node.position.start,
end: {
line: node.position.start.line,
column: node.position.start.column +
langIndex +
node.lang.length,
},
},
messageId: "disallowedLanguage",
data: {
lang: node.lang,
},
});
}
},
};
},
});
@@ -0,0 +1,35 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let missingMetadata: string;
let disallowedMetadata: string;
}
let schema: {
enum: string[];
}[];
let defaultOptions: ["always"];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: FencedCodeMetaOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: FencedCodeMetaMessageIds;
}>): {
code(node: import("mdast").Code): void;
};
}
export default _default;
export type FencedCodeMetaMessageIds = "missingMetadata" | "disallowedMetadata";
export type FencedCodeMetaOptions = ["always" | "never"];
export type FencedCodeMetaRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: FencedCodeMetaOptions;
MessageIds: FencedCodeMetaMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,82 @@
/**
* @fileoverview Rule to require or disallow metadata for fenced code blocks.
* @author TKDev7
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"missingMetadata" | "disallowedMetadata"} FencedCodeMetaMessageIds
* @typedef {["always" | "never"]} FencedCodeMetaOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: FencedCodeMetaOptions, MessageIds: FencedCodeMetaMessageIds }>} FencedCodeMetaRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {FencedCodeMetaRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: false,
description: "Require or disallow metadata for fenced code blocks",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/fenced-code-meta.md",
},
messages: {
missingMetadata: "Missing code block metadata.",
disallowedMetadata: "Code block metadata is not allowed.",
},
schema: [
{
enum: ["always", "never"],
},
],
defaultOptions: ["always"],
},
create(context) {
const [mode] = context.options;
const { sourceCode } = context;
return {
code(node) {
const lineText = sourceCode.lines[node.position.start.line - 1];
const fenceLineText = lineText.slice(node.position.start.column - 1);
if (mode === "always") {
if (node.lang && !node.meta) {
const langIndex = fenceLineText.indexOf(node.lang);
context.report({
loc: {
start: node.position.start,
end: {
line: node.position.start.line,
column: node.position.start.column +
langIndex +
node.lang.length,
},
},
messageId: "missingMetadata",
});
}
return;
}
if (node.meta) {
const metaIndex = fenceLineText.lastIndexOf(node.meta);
context.report({
loc: {
start: {
line: node.position.start.line,
column: node.position.start.column + metaIndex,
},
end: {
line: node.position.start.line,
column: node.position.start.column +
metaIndex +
node.meta.trimEnd().length,
},
},
messageId: "disallowedMetadata",
});
}
},
};
},
});
@@ -0,0 +1,47 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let skippedHeading: string;
}
let schema: {
type: "object";
properties: {
frontmatterTitle: {
type: "string";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
frontmatterTitle: string;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: HeadingIncrementOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "skippedHeading";
}>): {
yaml(node: import("mdast").Yaml): void;
toml(node: import("../types.js").Toml): void;
json(node: import("../types.js").Json): void;
heading(node: import("mdast").Heading): void;
};
}
export default _default;
export type HeadingIncrementMessageIds = "skippedHeading";
export type HeadingIncrementOptions = [{
frontmatterTitle?: string;
}];
export type HeadingIncrementRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: HeadingIncrementOptions;
MessageIds: HeadingIncrementMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,84 @@
/**
* @fileoverview Rule to enforce heading levels increment by one.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { frontmatterHasTitle } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"skippedHeading"} HeadingIncrementMessageIds
* @typedef {[{ frontmatterTitle?: string }]} HeadingIncrementOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: HeadingIncrementOptions, MessageIds: HeadingIncrementMessageIds }>} HeadingIncrementRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {HeadingIncrementRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Enforce heading levels increment by one",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/heading-increment.md",
},
messages: {
skippedHeading: "Heading level skipped from {{fromLevel}} to {{toLevel}}.",
},
schema: [
{
type: "object",
properties: {
frontmatterTitle: {
type: "string",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
frontmatterTitle: "^(?!\\s*['\"]title[:=]['\"])\\s*\\{?\\s*['\"]?title['\"]?\\s*[:=]",
},
],
},
create(context) {
const [{ frontmatterTitle }] = context.options;
const titlePattern = frontmatterTitle === "" ? null : new RegExp(frontmatterTitle, "iu");
let lastHeadingDepth = 0;
return {
yaml(node) {
if (frontmatterHasTitle(node.value, titlePattern)) {
lastHeadingDepth = 1;
}
},
toml(node) {
if (frontmatterHasTitle(node.value, titlePattern)) {
lastHeadingDepth = 1;
}
},
json(node) {
if (frontmatterHasTitle(node.value, titlePattern)) {
lastHeadingDepth = 1;
}
},
heading(node) {
if (lastHeadingDepth > 0 && node.depth > lastHeadingDepth + 1) {
context.report({
loc: node.position,
messageId: "skippedHeading",
data: {
fromLevel: lastHeadingDepth,
toLevel: node.depth,
},
});
}
lastHeadingDepth = node.depth;
},
};
},
});
+37
View File
@@ -0,0 +1,37 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let description: string;
let url: string;
}
let fixable: "code";
namespace messages {
let bareUrl: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "bareUrl";
}>): {
":matches(heading, paragraph, tableCell) html"(node: Html): void;
":matches(heading, paragraph, tableCell) link"(node: Link): void;
"heading:exit"(): void;
"paragraph:exit"(): void;
"tableCell:exit"(): void;
"root:exit"(): void;
};
}
export default _default;
export type NoBareUrlsMessageIds = "bareUrl";
export type NoBareUrlsOptions = [];
export type NoBareUrlsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoBareUrlsOptions;
MessageIds: NoBareUrlsMessageIds;
}>;
import type { Html } from "mdast";
import type { Link } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
+147
View File
@@ -0,0 +1,147 @@
/**
* @fileoverview Rule to prevent bare URLs in Markdown.
* @author xbinaryx
*/
/*
* Here's a note on how the approach (algorithm) works:
*
* - When entering an `Html` node that is a child of a `Heading`, `Paragraph` or `TableCell`,
* we check whether it is an opening or closing tag.
* If we encounter an opening tag, we store the tag name and set `lastTagName`.
* (`lastTagName` serves as a state to represent whether we're between opening and closing HTML tags.)
* If we encounter a closing tag, we reset the stored tag name and `tempLinkNodes`.
*
* - When entering a `Link` node that is a child of a `Heading`, `Paragraph` or `TableCell`,
* we check whether it is between opening and closing HTML tags.
* If it's between opening and closing HTML tags, we add it to `tempLinkNodes`.
* If it's not between opening and closing HTML tags, we add it to `linkNodes`.
*
* - When exiting a `Heading`, `Paragraph` or `TableCell`, we add all `tempLinkNodes` to `linkNodes`.
* If there are any remaining `tempLinkNodes`, it means they are not between opening and closing HTML tags. (ex. `<br> ... <br>`)
* If there are no remaining `tempLinkNodes`, it means they are between opening and closing HTML tags.
*
* - When exiting a `root` node, we report all `Link` nodes for bare URLs.
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Link, Html } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"bareUrl"} NoBareUrlsMessageIds
* @typedef {[]} NoBareUrlsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoBareUrlsOptions, MessageIds: NoBareUrlsMessageIds }>} NoBareUrlsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const htmlTagNamePattern = /^<(?<tagName>[^!>][^/\s>]*)/u;
/**
* Parses an HTML tag to extract its name and closing status
* @param {string} tagText The HTML tag text to parse
* @returns {{ name: string, isClosing: boolean } | null} Object containing tag name and closing status, or null if not a valid tag
*/
function parseHtmlTag(tagText) {
const match = tagText.match(htmlTagNamePattern);
if (match) {
const tagName = match.groups.tagName.toLowerCase();
const isClosing = tagName.startsWith("/");
return {
name: isClosing ? tagName.slice(1) : tagName,
isClosing,
};
}
return null;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoBareUrlsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
description: "Disallow bare URLs",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-bare-urls.md",
},
fixable: "code",
messages: {
bareUrl: "Unexpected bare URL. Use autolink (<URL>) or link ([text](URL)) instead.",
},
},
create(context) {
const { sourceCode } = context;
/**
* This array is used to store all `Link` nodes for the final report.
* @type {Array<Link>}
*/
const linkNodes = [];
/**
* This array is used to store `Link` nodes that are estimated to be between opening and closing HTML tags.
* @type {Array<Link>}
*/
const tempLinkNodes = [];
/** @type {string | null} */
let lastTagName = null;
/**
* Resets `tempLinkNodes` and `lastTagName`
* @returns {void}
*/
function reset() {
tempLinkNodes.length = 0;
lastTagName = null;
}
return {
":matches(heading, paragraph, tableCell) html"(
/** @type {Html} */ node) {
const tagInfo = parseHtmlTag(node.value);
if (!tagInfo) {
return;
}
if (!tagInfo.isClosing && lastTagName === null) {
lastTagName = tagInfo.name;
}
if (tagInfo.isClosing && lastTagName === tagInfo.name) {
reset();
}
},
":matches(heading, paragraph, tableCell) link"(
/** @type {Link} */ node) {
if (lastTagName !== null) {
tempLinkNodes.push(node);
}
else {
linkNodes.push(node);
}
},
"heading:exit"() {
linkNodes.push(...tempLinkNodes);
reset();
},
"paragraph:exit"() {
linkNodes.push(...tempLinkNodes);
reset();
},
"tableCell:exit"() {
linkNodes.push(...tempLinkNodes);
reset();
},
"root:exit"() {
for (const linkNode of linkNodes) {
const text = sourceCode.getText(linkNode);
const { url } = linkNode;
if (url === text ||
url === `http://${text}` ||
url === `mailto:${text}`) {
context.report({
node: linkNode,
messageId: "bareUrl",
fix(fixer) {
return fixer.replaceText(linkNode, `<${text}>`);
},
});
}
}
},
};
},
});
@@ -0,0 +1,66 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let duplicateDefinition: string;
let duplicateFootnoteDefinition: string;
}
let schema: {
type: "object";
properties: {
allowDefinitions: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
allowFootnoteDefinitions: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
checkFootnoteDefinitions: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
allowDefinitions: string[];
allowFootnoteDefinitions: any[];
checkFootnoteDefinitions: true;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoDuplicateDefinitionsOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: NoDuplicateDefinitionsMessageIds;
}>): {
definition(node: Definition): void;
footnoteDefinition(node: FootnoteDefinition): void;
};
}
export default _default;
export type NoDuplicateDefinitionsMessageIds = "duplicateDefinition" | "duplicateFootnoteDefinition";
export type NoDuplicateDefinitionsOptions = [{
allowDefinitions?: string[];
allowFootnoteDefinitions?: string[];
checkFootnoteDefinitions?: boolean;
}];
export type NoDuplicateDefinitionsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoDuplicateDefinitionsOptions;
MessageIds: NoDuplicateDefinitionsMessageIds;
}>;
import type { Definition } from "mdast";
import type { FootnoteDefinition } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,121 @@
/**
* @fileoverview Rule to prevent duplicate definitions in Markdown.
* @author 루밀LuMir(lumirlumir)
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Definition, FootnoteDefinition } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"duplicateDefinition" | "duplicateFootnoteDefinition"} NoDuplicateDefinitionsMessageIds
* @typedef {[{ allowDefinitions?: string[], allowFootnoteDefinitions?: string[], checkFootnoteDefinitions?: boolean }]} NoDuplicateDefinitionsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateDefinitionsOptions, MessageIds: NoDuplicateDefinitionsMessageIds }>} NoDuplicateDefinitionsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoDuplicateDefinitionsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow duplicate definitions",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-definitions.md",
},
messages: {
duplicateDefinition: "Unexpected duplicate definition `{{ identifier }}` (label: `{{ label }}`) found. First defined at line {{ firstLine }} (label: `{{ firstLabel }}`).",
duplicateFootnoteDefinition: "Unexpected duplicate footnote definition `{{ identifier }}` (label: `{{ label }}`) found. First defined at line {{ firstLine }} (label: `{{ firstLabel }}`).",
},
schema: [
{
type: "object",
properties: {
allowDefinitions: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
allowFootnoteDefinitions: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
checkFootnoteDefinitions: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
allowDefinitions: ["//"],
allowFootnoteDefinitions: [],
checkFootnoteDefinitions: true,
},
],
},
create(context) {
const allowDefinitions = new Set(context.options[0].allowDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
const allowFootnoteDefinitions = new Set(context.options[0].allowFootnoteDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
const [{ checkFootnoteDefinitions }] = context.options;
/** @type {Map<string, Definition>} */
const definitions = new Map();
/** @type {Map<string, FootnoteDefinition>} */
const footnoteDefinitions = new Map();
return {
definition(node) {
if (allowDefinitions.has(node.identifier)) {
return;
}
if (definitions.has(node.identifier)) {
const firstDefinitionNode = definitions.get(node.identifier);
context.report({
node,
messageId: "duplicateDefinition",
data: {
identifier: node.identifier,
label: node.label.trim(),
firstLine: firstDefinitionNode.position.start.line,
firstLabel: firstDefinitionNode.label.trim(),
},
});
}
else {
definitions.set(node.identifier, node);
}
},
footnoteDefinition(node) {
if (!checkFootnoteDefinitions ||
allowFootnoteDefinitions.has(node.identifier)) {
return;
}
if (footnoteDefinitions.has(node.identifier)) {
const firstFootnoteDefinitionNode = footnoteDefinitions.get(node.identifier);
context.report({
node,
messageId: "duplicateFootnoteDefinition",
data: {
identifier: node.identifier,
label: node.label,
firstLine: firstFootnoteDefinitionNode.position.start.line,
firstLabel: firstFootnoteDefinitionNode.label,
},
});
}
else {
footnoteDefinitions.set(node.identifier, node);
}
},
};
},
});
@@ -0,0 +1,45 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let description: string;
let url: string;
}
namespace messages {
let duplicateHeading: string;
}
let schema: {
type: "object";
properties: {
checkSiblingsOnly: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
checkSiblingsOnly: false;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoDuplicateHeadingsOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "duplicateHeading";
}>): {
heading(node: import("mdast").Heading): void;
"heading *"({ type, value }: any): void;
"heading:exit"(node: import("mdast").Heading): void;
};
}
export default _default;
export type NoDuplicateHeadingsMessageIds = "duplicateHeading";
export type NoDuplicateHeadingsOptions = [{
checkSiblingsOnly?: boolean;
}];
export type NoDuplicateHeadingsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoDuplicateHeadingsOptions;
MessageIds: NoDuplicateHeadingsMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,101 @@
/**
* @fileoverview Rule to prevent duplicate headings in Markdown.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"duplicateHeading"} NoDuplicateHeadingsMessageIds
* @typedef {[{ checkSiblingsOnly?: boolean }]} NoDuplicateHeadingsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateHeadingsOptions, MessageIds: NoDuplicateHeadingsMessageIds }>} NoDuplicateHeadingsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoDuplicateHeadingsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
description: "Disallow duplicate headings in the same document",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-headings.md",
},
messages: {
duplicateHeading: 'Duplicate heading "{{text}}" found.',
},
schema: [
{
type: "object",
properties: {
checkSiblingsOnly: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [{ checkSiblingsOnly: false }],
},
create(context) {
const [{ checkSiblingsOnly }] = context.options;
/** @type {Map<number, Set<string>>} */
const headingsByLevel = checkSiblingsOnly
? new Map([
[1, new Set()],
[2, new Set()],
[3, new Set()],
[4, new Set()],
[5, new Set()],
[6, new Set()],
])
: new Map([[1, new Set()]]);
let lastLevel = 1;
let currentLevelHeadings = headingsByLevel.get(lastLevel);
/** @type {string} */
let headingChildrenSequence;
/** @type {string} */
let headingText;
return {
heading(node) {
headingChildrenSequence = "";
headingText = "";
if (checkSiblingsOnly) {
const currentLevel = node.depth;
if (currentLevel < lastLevel) {
for (let level = lastLevel; level > currentLevel; level--) {
headingsByLevel.get(level).clear();
}
}
lastLevel = currentLevel;
currentLevelHeadings = headingsByLevel.get(currentLevel);
}
},
"heading *"({ type, value }) {
if (value) {
headingChildrenSequence += `[${type},${value}]`; // We use a custom sequence representation to keep track of heading children.
if (type !== "html") {
headingText += value;
}
}
else {
headingChildrenSequence += `[${type}]`;
}
},
"heading:exit"(node) {
if (currentLevelHeadings.has(headingChildrenSequence)) {
context.report({
loc: node.position,
messageId: "duplicateHeading",
data: {
text: headingText,
},
});
}
else {
currentLevelHeadings.add(headingChildrenSequence);
}
},
};
},
});
@@ -0,0 +1,64 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let emptyDefinition: string;
let emptyFootnoteDefinition: string;
}
let schema: {
type: "object";
properties: {
allowDefinitions: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
allowFootnoteDefinitions: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
checkFootnoteDefinitions: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
allowDefinitions: string[];
allowFootnoteDefinitions: any[];
checkFootnoteDefinitions: true;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoEmptyDefinitionsOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: NoEmptyDefinitionsMessageIds;
}>): {
definition(node: import("mdast").Definition): void;
footnoteDefinition(node: import("mdast").FootnoteDefinition): void;
};
}
export default _default;
export type NoEmptyDefinitionsMessageIds = "emptyDefinition" | "emptyFootnoteDefinition";
export type NoEmptyDefinitionsOptions = [{
allowDefinitions?: string[];
allowFootnoteDefinitions?: string[];
checkFootnoteDefinitions?: boolean;
}];
export type NoEmptyDefinitionsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoEmptyDefinitionsOptions;
MessageIds: NoEmptyDefinitionsMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,115 @@
/**
* @fileoverview Rule to prevent empty definitions in Markdown.
* @author Pixel998
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
import { htmlCommentPattern } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"emptyDefinition" | "emptyFootnoteDefinition"} NoEmptyDefinitionsMessageIds
* @typedef {[{ allowDefinitions?: string[], allowFootnoteDefinitions?: string[], checkFootnoteDefinitions?: boolean }]} NoEmptyDefinitionsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyDefinitionsOptions, MessageIds: NoEmptyDefinitionsMessageIds }>} NoEmptyDefinitionsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Checks if a string contains only HTML comments.
* @param {string} value The input string to check.
* @returns {boolean} True if the string contains only HTML comments, false otherwise.
*/
function isOnlyComments(value) {
const withoutComments = value.replace(htmlCommentPattern, "");
return withoutComments.trim().length === 0;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoEmptyDefinitionsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow empty definitions",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-definitions.md",
},
messages: {
emptyDefinition: "Unexpected empty definition `{{ identifier }}` (label: `{{ label }}`) found.",
emptyFootnoteDefinition: "Unexpected empty footnote definition `{{ identifier }}` (label: `{{ label }}`) found.",
},
schema: [
{
type: "object",
properties: {
allowDefinitions: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
allowFootnoteDefinitions: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
checkFootnoteDefinitions: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
allowDefinitions: ["//"],
allowFootnoteDefinitions: [],
checkFootnoteDefinitions: true,
},
],
},
create(context) {
const allowDefinitions = new Set(context.options[0].allowDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
const allowFootnoteDefinitions = new Set(context.options[0].allowFootnoteDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
const [{ checkFootnoteDefinitions }] = context.options;
return {
definition(node) {
if ((!node.url || node.url === "#") &&
!allowDefinitions.has(node.identifier)) {
context.report({
loc: node.position,
messageId: "emptyDefinition",
data: {
identifier: node.identifier,
label: node.label.trim(),
},
});
}
},
footnoteDefinition(node) {
if (checkFootnoteDefinitions &&
!allowFootnoteDefinitions.has(node.identifier) &&
(node.children.length === 0 ||
node.children.every(child => child.type === "html" &&
isOnlyComments(child.value)))) {
context.report({
loc: node.position,
messageId: "emptyFootnoteDefinition",
data: {
identifier: node.identifier,
label: node.label,
},
});
}
},
};
},
});
@@ -0,0 +1,30 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let emptyImage: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "emptyImage";
}>): {
image(node: import("mdast").Image): void;
};
}
export default _default;
export type NoEmptyImagesMessageIds = "emptyImage";
export type NoEmptyImagesOptions = [];
export type NoEmptyImagesRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoEmptyImagesOptions;
MessageIds: NoEmptyImagesMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,41 @@
/**
* @fileoverview Rule to prevent empty images in Markdown.
* @author 루밀LuMir(lumirlumir)
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"emptyImage"} NoEmptyImagesMessageIds
* @typedef {[]} NoEmptyImagesOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyImagesOptions, MessageIds: NoEmptyImagesMessageIds }>} NoEmptyImagesRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoEmptyImagesRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow empty images",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-images.md",
},
messages: {
emptyImage: "Unexpected empty image found.",
},
},
create(context) {
return {
image(node) {
if (!node.url || node.url === "#") {
context.report({
loc: node.position,
messageId: "emptyImage",
});
}
},
};
},
});
@@ -0,0 +1,30 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let emptyLink: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "emptyLink";
}>): {
link(node: import("mdast").Link): void;
};
}
export default _default;
export type NoEmptyLinksMessageIds = "emptyLink";
export type NoEmptyLinksOptions = [];
export type NoEmptyLinksRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoEmptyLinksOptions;
MessageIds: NoEmptyLinksMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
+41
View File
@@ -0,0 +1,41 @@
/**
* @fileoverview Rule to prevent empty links in Markdown.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"emptyLink"} NoEmptyLinksMessageIds
* @typedef {[]} NoEmptyLinksOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyLinksOptions, MessageIds: NoEmptyLinksMessageIds }>} NoEmptyLinksRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoEmptyLinksRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow empty links",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-links.md",
},
messages: {
emptyLink: "Unexpected empty link found.",
},
},
create(context) {
return {
link(node) {
if (!node.url || node.url === "#") {
context.report({
loc: node.position,
messageId: "emptyLink",
});
}
},
};
},
});
+52
View File
@@ -0,0 +1,52 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let description: string;
let url: string;
}
namespace messages {
let disallowedElement: string;
}
let schema: {
type: "object";
properties: {
allowed: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
allowedIgnoreCase: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
allowed: any[];
allowedIgnoreCase: false;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoHtmlOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "disallowedElement";
}>): {
html(node: import("mdast").Html): void;
};
}
export default _default;
export type NoHtmlMessageIds = "disallowedElement";
export type NoHtmlOptions = [{
allowed?: string[];
allowedIgnoreCase?: boolean;
}];
export type NoHtmlRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoHtmlOptions;
MessageIds: NoHtmlMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
+103
View File
@@ -0,0 +1,103 @@
/**
* @fileoverview Rule to disallow HTML inside of content.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { lineEndingPattern, stripHtmlComments } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"disallowedElement"} NoHtmlMessageIds
* @typedef {[{ allowed?: string[], allowedIgnoreCase?: boolean }]} NoHtmlOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoHtmlOptions, MessageIds: NoHtmlMessageIds }>} NoHtmlRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const htmlTagPattern = /<(?<tagName>[a-z0-9]+(?:-[a-z0-9]+)*)(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?\/?>/giu;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoHtmlRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
description: "Disallow HTML tags",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-html.md",
},
messages: {
disallowedElement: 'HTML element "{{name}}" is not allowed.',
},
schema: [
{
type: "object",
properties: {
allowed: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
allowedIgnoreCase: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
allowed: [],
allowedIgnoreCase: false,
},
],
},
create(context) {
const { sourceCode } = context;
const [{ allowed, allowedIgnoreCase }] = context.options;
/**
* Normalize a tag name based on the `allowedIgnoreCase` option.
* @param {string} tagName The tag name to normalize.
* @returns {string} The normalized tag name.
*/
function normalizeTagName(tagName) {
return allowedIgnoreCase ? tagName.toLowerCase() : tagName;
}
const allowedElements = new Set(allowed.map(normalizeTagName));
return {
html(node) {
const text = stripHtmlComments(sourceCode.getText(node));
/** @type {RegExpExecArray | null} */
let match;
while ((match = htmlTagPattern.exec(text)) !== null) {
const fullMatch = match[0];
const { tagName } = match.groups;
const firstNewlineIndex = fullMatch.search(lineEndingPattern);
const startOffset = // Adjust `htmlTagPattern` match index to the full source code.
match.index + node.position.start.offset;
const endOffset = startOffset +
(firstNewlineIndex === -1
? fullMatch.length
: firstNewlineIndex);
if (!allowedElements.has(normalizeTagName(tagName))) {
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset),
},
messageId: "disallowedElement",
data: {
name: tagName,
},
});
}
}
},
};
},
});
@@ -0,0 +1,32 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let invalidLabelRef: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "invalidLabelRef";
}>): {
text(node: Text): void;
};
}
export default _default;
export type NoInvalidLabelRefsMessageIds = "invalidLabelRef";
export type NoInvalidLabelRefsOptions = [];
export type NoInvalidLabelRefsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoInvalidLabelRefsOptions;
MessageIds: NoInvalidLabelRefsMessageIds;
}>;
import type { MarkdownSourceCode } from "../language/markdown-source-code.js";
import type { Text } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,117 @@
/**
* @fileoverview Rule to prevent non-complaint link references.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { illegalShorthandTailPattern } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Position } from "unist";
* @import { Text } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @import { MarkdownSourceCode } from "../language/markdown-source-code.js";
* @typedef {"invalidLabelRef"} NoInvalidLabelRefsMessageIds
* @typedef {[]} NoInvalidLabelRefsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoInvalidLabelRefsOptions, MessageIds: NoInvalidLabelRefsMessageIds }>} NoInvalidLabelRefsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/** matches i.e., `[foo][bar]` */
const labelPattern = /\]\[([^\]]+)\]/u;
/**
* Finds missing references in a node.
* @param {Text} node The node to check.
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
* @returns {Array<{label:string,position:Position}>} The missing references.
*/
function findInvalidLabelReferences(node, sourceCode) {
const nodeText = sourceCode.getText(node);
const docText = sourceCode.text;
const invalid = [];
let startIndex = 0;
/*
* This loop works by searching the string inside the node for the next
* label reference. If it finds one, it checks to see if there is any
* white space between the [ and ]. If there is, it reports an error.
* It then moves the start index to the end of the label reference and
* continues searching the text until the end of the text is found.
*/
while (startIndex < nodeText.length) {
const value = nodeText.slice(startIndex);
const match = value.match(labelPattern);
if (!match) {
break;
}
if (!illegalShorthandTailPattern.test(match[0])) {
startIndex += match.index + match[0].length;
continue;
}
/*
* Adjust `labelPattern` match index to the full source code.
*/
const startOffset = startIndex + match.index + node.position.start.offset;
const endOffset = startOffset + match[0].length;
/*
* Search the entire document text to find the preceding open bracket.
*/
const lastOpenBracketIndex = docText.lastIndexOf("[", startOffset);
if (lastOpenBracketIndex === -1) {
startIndex += match.index + match[0].length;
continue;
}
/*
* Note: `label` can contain leading and trailing newlines, so we need to
* take that into account when calculating the line and column offsets.
*/
const label = docText
.slice(lastOpenBracketIndex, endOffset)
.match(/!?\[([^\]]+)\]/u)[1];
invalid.push({
label: label.trim(),
position: {
start: sourceCode.getLocFromIndex(startOffset + 1),
end: sourceCode.getLocFromIndex(endOffset),
},
});
startIndex += match.index + match[0].length;
}
return invalid;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoInvalidLabelRefsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow invalid label references",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-invalid-label-refs.md",
},
messages: {
invalidLabelRef: "Label reference '{{label}}' is invalid due to white space between [ and ].",
},
},
create(context) {
const { sourceCode } = context;
return {
text(node) {
const invalidReferences = findInvalidLabelReferences(node, sourceCode);
for (const invalidReference of invalidReferences) {
context.report({
loc: invalidReference.position,
messageId: "invalidLabelRef",
data: {
label: invalidReference.label,
},
});
}
},
};
},
});
@@ -0,0 +1,46 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
let fixable: "whitespace";
namespace messages {
let missingSpace: string;
}
let schema: {
type: "object";
properties: {
checkClosedHeadings: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
checkClosedHeadings: false;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoMissingAtxHeadingSpaceOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "missingSpace";
}>): {
heading(node: import("mdast").Heading): void;
paragraph(node: import("mdast").Paragraph): void;
};
}
export default _default;
export type NoMissingAtxHeadingSpaceMessageIds = "missingSpace";
export type NoMissingAtxHeadingSpaceOptions = [{
checkClosedHeadings?: boolean;
}];
export type NoMissingAtxHeadingSpaceRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoMissingAtxHeadingSpaceOptions;
MessageIds: NoMissingAtxHeadingSpaceMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,101 @@
/**
* @fileoverview Rule to ensure there is a space after hash on ATX style headings in Markdown.
* @author Sweta Tanwar (@SwetaTanwar)
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"missingSpace"} NoMissingAtxHeadingSpaceMessageIds
* @typedef {[{ checkClosedHeadings?: boolean }]} NoMissingAtxHeadingSpaceOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingAtxHeadingSpaceOptions, MessageIds: NoMissingAtxHeadingSpaceMessageIds }>} NoMissingAtxHeadingSpaceRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const leadingAtxHeadingHashPattern = /(?:^|(?<=[\r\n]))(?<hashes>#{1,6})(?:[^# \t]|$)/gu;
const trailingAtxHeadingHashPattern = /(?<![ \t])(?<spaces>[ \t]*)(?<=(?<!\\)(?:\\{2})*)(?<hashes>#+)[ \t]*$/u;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoMissingAtxHeadingSpaceRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow headings without a space after the hash characters",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-atx-heading-space.md",
},
fixable: "whitespace",
messages: {
missingSpace: "Missing space {{position}} hash(es) on ATX style heading.",
},
schema: [
{
type: "object",
properties: {
checkClosedHeadings: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [{ checkClosedHeadings: false }],
},
create(context) {
const { sourceCode } = context;
const [{ checkClosedHeadings }] = context.options;
return {
heading(node) {
if (!checkClosedHeadings) {
return;
}
const text = sourceCode.getText(node);
const match = trailingAtxHeadingHashPattern.exec(text);
if (match === null) {
return;
}
const { spaces, hashes } = match.groups;
if (spaces.length > 0) {
return;
}
const startOffset = node.position.start.offset + match.index;
const endOffset = startOffset + hashes.length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset - 1),
end: sourceCode.getLocFromIndex(endOffset),
},
messageId: "missingSpace",
data: { position: "before" },
fix(fixer) {
return fixer.insertTextBeforeRange([startOffset, startOffset + 1], " ");
},
});
},
paragraph(node) {
const text = sourceCode.getText(node);
/** @type {RegExpExecArray | null} */
let match;
while ((match = leadingAtxHeadingHashPattern.exec(text)) !== null) {
const { hashes } = match.groups;
const startOffset = node.position.start.offset + match.index;
const endOffset = startOffset + hashes.length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset + 1),
},
messageId: "missingSpace",
data: { position: "after" },
fix(fixer) {
return fixer.insertTextAfterRange([endOffset - 1, endOffset], " ");
},
});
}
},
};
},
});
@@ -0,0 +1,52 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
let schema: {
type: "object";
properties: {
allowLabels: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
allowLabels: any[];
}];
namespace messages {
let notFound: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: MarkdownSourceCode;
RuleOptions: NoMissingLabelRefsOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "notFound";
}>): {
"root:exit"(): void;
text(node: Text): void;
definition(node: import("mdast").Definition): void;
};
}
export default _default;
export type NoMissingLabelRefsMessageIds = "notFound";
export type NoMissingLabelRefsOptions = [{
allowLabels?: string[];
}];
export type NoMissingLabelRefsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoMissingLabelRefsOptions;
MessageIds: NoMissingLabelRefsMessageIds;
}>;
import type { MarkdownSourceCode } from "../language/markdown-source-code.js";
import type { Text } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,147 @@
/**
* @fileoverview Rule to prevent missing label references in Markdown.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { illegalShorthandTailPattern } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Position } from "unist";
* @import { Text } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @import { MarkdownSourceCode } from "../language/markdown-source-code.js";
* @typedef {"notFound"} NoMissingLabelRefsMessageIds
* @typedef {[{ allowLabels?: string[] }]} NoMissingLabelRefsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingLabelRefsOptions, MessageIds: NoMissingLabelRefsMessageIds }>} NoMissingLabelRefsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Finds missing references in a node.
* @param {Text} node The node to check.
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
* @returns {Array<{label:string,position:Position}>} The missing references.
*/
function findMissingReferences(node, sourceCode) {
/** @type {Array<{label:string,position:Position}>} */
const missing = [];
const nodeText = sourceCode.getText(node);
/**
* Matches substrings like `"[foo]"`, `"[]"`, `"[foo][bar]"`, `"[foo][]"`, `"[][bar]"`, or `"[][]"`.
* `left` is the content between the first brackets. It can be empty.
* `right` is the content between the second brackets. It can be empty, and it can be undefined.
*/
const labelPattern = /(?<=(?<!\\)(?:\\{2})*)\[(?<left>(?:\\.|[^[\]\\])*)\](?:\[(?<right>(?:\\.|[^\]\\])*)\])?/dgu;
/** @type {RegExpExecArray | null} */
let match;
/*
* This loop searches the text inside the node for sequences that
* look like label references and reports an error for each one found.
*/
while ((match = labelPattern.exec(nodeText))) {
// skip illegal shorthand tail -- handled by no-invalid-label-refs
if (illegalShorthandTailPattern.test(match[0])) {
continue;
}
const { left, right } = match.groups;
// `[][]` or `[]`
if (!left && !right) {
continue;
}
let label, labelIndices;
if (right) {
label = right;
labelIndices = match.indices.groups.right;
}
else {
label = left;
labelIndices = match.indices.groups.left;
}
const startOffset = labelIndices[0] + node.position.start.offset;
const endOffset = labelIndices[1] + node.position.start.offset;
missing.push({
label: label.trim(),
position: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset),
},
});
}
return missing;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoMissingLabelRefsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow missing label references",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-label-refs.md",
},
schema: [
{
type: "object",
properties: {
allowLabels: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
allowLabels: [],
},
],
messages: {
notFound: "Label reference '{{label}}' not found.",
},
},
create(context) {
const { sourceCode } = context;
const allowLabels = new Set(context.options[0].allowLabels);
/** @type {Array<{label:string,position:Position}>} */
let allMissingReferences = [];
return {
"root:exit"() {
for (const missingReference of allMissingReferences) {
context.report({
loc: missingReference.position,
messageId: "notFound",
data: {
label: missingReference.label,
},
});
}
},
text(node) {
const missingReferences = findMissingReferences(node, sourceCode);
for (const missingReference of missingReferences) {
if (!allowLabels.has(missingReference.label)) {
allMissingReferences.push(missingReference);
}
}
},
definition(node) {
/*
* Sometimes a poorly-formatted link will end up a text node instead of a link node
* even though the label definition exists. Here, we remove any missing references
* that have a matching label definition.
*/
allMissingReferences = allMissingReferences.filter(missingReference => missingReference.label !== node.identifier);
},
};
},
});
@@ -0,0 +1,56 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
let schema: {
type: "object";
properties: {
ignoreCase: {
type: "boolean";
};
allowPattern: {
type: "string";
};
};
additionalProperties: false;
}[];
namespace messages {
let invalidFragment: string;
}
let defaultOptions: [{
ignoreCase: true;
allowPattern: string;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoMissingLinkFragmentsOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "invalidFragment";
}>): {
heading(): void;
"heading *:not(html)"({ value }: any): void;
"heading:exit"(): void;
html(node: import("mdast").Html): void;
"definition, link"(node: Definition | Link): void;
"root:exit"(): void;
};
}
export default _default;
export type NoMissingLinkFragmentsMessageIds = "invalidFragment";
export type NoMissingLinkFragmentsOptions = [{
ignoreCase?: boolean;
allowPattern?: string;
}];
export type NoMissingLinkFragmentsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoMissingLinkFragmentsOptions;
MessageIds: NoMissingLinkFragmentsMessageIds;
}>;
import type { Definition } from "mdast";
import type { Link } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,141 @@
/**
* @fileoverview Rule to ensure link fragments (URLs that start with #) reference valid headings
* @author Sweta Tanwar (@SwetaTanwar)
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import GithubSlugger from "github-slugger";
import { stripHtmlComments } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Definition, Link } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"invalidFragment"} NoMissingLinkFragmentsMessageIds
* @typedef {[{ ignoreCase?: boolean; allowPattern?: string }]} NoMissingLinkFragmentsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingLinkFragmentsOptions, MessageIds: NoMissingLinkFragmentsMessageIds }>} NoMissingLinkFragmentsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const githubLineReferencePattern = /^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$/u;
const customHeadingIdPattern = /\{#(?<id>[^}\s]+)\}\s*$/u;
const htmlHeadingPattern = /<h(?<depth>[1-6])[^>]*>(?<children>[\s\S]*?)<\/h\k<depth>\s*>/giu;
const htmlIdNamePattern = /(?<!<)<[^>]+\s(?:id|name)\s*=\s*["']?(?<id>[^"'\s>]+)["']?/giu;
const htmlTagPattern = /<\/?[a-z0-9]+(?:-[a-z0-9]+)*(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?(?:\/\s*)?>/giu;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoMissingLinkFragmentsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow link fragments that do not reference valid headings",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-link-fragments.md",
},
schema: [
{
type: "object",
properties: {
ignoreCase: {
type: "boolean",
},
allowPattern: {
type: "string",
},
},
additionalProperties: false,
},
],
messages: {
invalidFragment: "Link fragment '#{{fragment}}' does not reference a heading or anchor in this document.",
},
defaultOptions: [
{
ignoreCase: true,
allowPattern: "",
},
],
},
create(context) {
const [{ allowPattern, ignoreCase }] = context.options;
const allowPatternOrNull = allowPattern
? new RegExp(allowPattern, "u")
: null;
const fragmentIds = new Set(["top"]);
const slugger = new GithubSlugger();
/** @type {Array<Definition | Link>} */
const relevantNodes = [];
/** @type {string} */
let headingText;
return {
heading() {
headingText = "";
},
"heading *:not(html)"({ value }) {
headingText += value ?? "";
},
"heading:exit"() {
const customIdMatch = headingText.match(customHeadingIdPattern);
const id = customIdMatch
? customIdMatch.groups.id
: headingText;
fragmentIds.add(slugger.slug(id));
},
html(node) {
// 1. Remove all comments
const htmlTextWithoutComments = stripHtmlComments(node.value);
// 2. Then look for IDs in the remaining text
for (const match of htmlTextWithoutComments.matchAll(htmlIdNamePattern)) {
const { id } = match.groups;
fragmentIds.add(slugger.slug(id));
}
// 3. Finally, look for headings in the HTML
for (const match of htmlTextWithoutComments.matchAll(htmlHeadingPattern)) {
const { children } = match.groups;
// Remove any HTML tags within the heading content to get plain text
const id = children.replace(htmlTagPattern, "");
fragmentIds.add(slugger.slug(id));
}
},
"definition, link"(/** @type {Definition | Link} */ node) {
const { url } = node;
// If `url` is empty, `"#"`, or does not start with `"#"`, skip it.
if (url === "" || url === "#" || !url.startsWith("#")) {
return;
}
relevantNodes.push(node);
},
"root:exit"() {
for (const node of relevantNodes) {
const fragment = node.url.slice(1);
let decodedFragment = fragment;
// Decode URI component to handle encoded characters such as `%20`.
try {
decodedFragment = decodeURIComponent(fragment);
}
catch {
// If decoding fails due to an invalid URI sequence, use the original fragment.
}
if (allowPatternOrNull?.test(decodedFragment) ||
githubLineReferencePattern.test(decodedFragment)) {
continue;
}
const normalizedFragment = ignoreCase
? decodedFragment.toLowerCase()
: decodedFragment;
if (!fragmentIds.has(normalizedFragment)) {
context.report({
loc: node.position,
messageId: "invalidFragment",
data: { fragment },
});
}
}
},
};
},
});
@@ -0,0 +1,49 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let multipleH1: string;
}
let schema: {
type: "object";
properties: {
frontmatterTitle: {
type: "string";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
frontmatterTitle: string;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoMultipleH1Options;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "multipleH1";
}>): {
"yaml, toml, json"(node: Yaml | Toml | Json): void;
html(node: import("mdast").Html): void;
heading(node: import("mdast").Heading): void;
};
}
export default _default;
export type NoMultipleH1MessageIds = "multipleH1";
export type NoMultipleH1Options = [{
frontmatterTitle?: string;
}];
export type NoMultipleH1RuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoMultipleH1Options;
MessageIds: NoMultipleH1MessageIds;
}>;
import type { Yaml } from "mdast";
import type { Toml } from "../types.js";
import type { Json } from "../types.js";
import type { MarkdownRuleDefinition } from "../types.js";
+98
View File
@@ -0,0 +1,98 @@
/**
* @fileoverview Rule to enforce at most one H1 heading in Markdown.
* @author Pixel998
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { frontmatterHasTitle, stripHtmlComments } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Yaml } from "mdast";
* @import { MarkdownRuleDefinition, Toml, Json } from "../types.js";
* @typedef {"multipleH1"} NoMultipleH1MessageIds
* @typedef {[{ frontmatterTitle?: string }]} NoMultipleH1Options
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMultipleH1Options, MessageIds: NoMultipleH1MessageIds }>} NoMultipleH1RuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const h1TagPattern = /<h1[^>]*>[\s\S]*?<\/h1\s*>/giu;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoMultipleH1RuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow multiple H1 headings in the same document",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-multiple-h1.md",
},
messages: {
multipleH1: "Unexpected additional H1 heading found.",
},
schema: [
{
type: "object",
properties: {
frontmatterTitle: {
type: "string",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
frontmatterTitle: "^(?!\\s*['\"]title[:=]['\"])\\s*\\{?\\s*['\"]?title['\"]?\\s*[:=]",
},
],
},
create(context) {
const { sourceCode } = context;
const [{ frontmatterTitle }] = context.options;
const titlePattern = frontmatterTitle === "" ? null : new RegExp(frontmatterTitle, "iu");
let h1Count = 0;
return {
"yaml, toml, json"(/** @type {Yaml | Toml | Json} */ node) {
if (frontmatterHasTitle(node.value, titlePattern)) {
h1Count++;
}
},
html(node) {
const text = stripHtmlComments(node.value);
/** @type {RegExpExecArray | null} */
let match;
while ((match = h1TagPattern.exec(text)) !== null) {
h1Count++;
if (h1Count > 1) {
const startOffset = // Adjust `h1TagPattern` match index to the full source code.
match.index + node.position.start.offset;
const endOffset = startOffset + match[0].length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset),
},
messageId: "multipleH1",
});
}
}
},
heading(node) {
if (node.depth === 1) {
h1Count++;
if (h1Count > 1) {
context.report({
loc: node.position,
messageId: "multipleH1",
});
}
}
},
};
},
});
@@ -0,0 +1,35 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
let fixable: "code";
namespace messages {
let referenceLikeUrl: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "referenceLikeUrl";
}>): {
definition(node: import("mdast").Definition): void;
"image, link"(node: Image | Link): void;
"root:exit"(): void;
};
}
export default _default;
export type NoReferenceLikeUrlsMessageIds = "referenceLikeUrl";
export type NoReferenceLikeUrlsOptions = [];
export type NoReferenceLikeUrlsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoReferenceLikeUrlsOptions;
MessageIds: NoReferenceLikeUrlsMessageIds;
}>;
import type { Image } from "mdast";
import type { Link } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,84 @@
/**
* @fileoverview Rule to enforce reference-style links when URL matches a defined identifier.
* @author TKDev7
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Image, Link } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"referenceLikeUrl"} NoReferenceLikeUrlsMessageIds
* @typedef {[]} NoReferenceLikeUrlsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoReferenceLikeUrlsOptions, MessageIds: NoReferenceLikeUrlsMessageIds }>} NoReferenceLikeUrlsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/** Pattern to match both inline links: `[text](url)` and images: `![alt](url)`, with optional title */
const linkOrImagePattern = /\[(?<label>(?:\\.|[^()\\]|\([\s\S]*\))*?)\]\((?<destination>[ \t]*\r?\n?(?<![ \t])[ \t]*(?:<[^>]*>|[^ \t()]+))(?:[ \t]*\r?\n?(?<![ \t])[ \t]*(?:"[^"]*"|'[^']*'|\([^)]*\)))?[ \t]*\r?\n?(?<![ \t])[ \t]*\)$/u;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoReferenceLikeUrlsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow URLs that match defined reference identifiers",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-reference-like-urls.md",
},
fixable: "code",
messages: {
referenceLikeUrl: "Unexpected resource {{type}} ('{{prefix}}[text](url)') with URL that matches a definition identifier. Use '[text][id]' syntax instead.",
},
},
create(context) {
const { sourceCode } = context;
/** @type {Set<string>} */
const definitionIdentifiers = new Set();
/** @type {Array<Image | Link>} */
const relevantNodes = [];
return {
definition(node) {
definitionIdentifiers.add(node.identifier);
},
"image, link"(/** @type {Image | Link} */ node) {
relevantNodes.push(node);
},
"root:exit"() {
for (const node of relevantNodes) {
const text = sourceCode.getText(node);
const match = linkOrImagePattern.exec(text);
if (match !== null) {
const { label, destination } = match.groups;
const { type, title } = node;
const prefix = type === "image" ? "!" : "";
const url = normalizeIdentifier(destination).toLowerCase();
if (definitionIdentifiers.has(url)) {
context.report({
loc: node.position,
messageId: "referenceLikeUrl",
data: {
type,
prefix,
},
fix(fixer) {
// The AST treats both missing and empty titles as null, so it's safe to auto-fix in both cases.
if (title) {
return null;
}
return fixer.replaceText(node, `${prefix}[${label}][${destination}]`);
},
});
}
}
}
},
};
},
});
@@ -0,0 +1,42 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
let fixable: "code";
namespace messages {
let reversedSyntax: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "reversedSyntax";
}>): {
"heading, paragraph, tableCell"(node: Heading | Paragraph | TableCell): void;
":matches(heading, paragraph, tableCell) :matches(html, image, imageReference, inlineCode, linkReference, inlineMath)"(node: Html | Image | ImageReference | InlineCode | LinkReference | InlineMath): void;
":matches(heading, paragraph, tableCell):exit"(): void;
};
}
export default _default;
export type NoReversedMediaSyntaxMessageIds = "reversedSyntax";
export type NoReversedMediaSyntaxOptions = [];
export type NoReversedMediaSyntaxRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoReversedMediaSyntaxOptions;
MessageIds: NoReversedMediaSyntaxMessageIds;
}>;
import type { Heading } from "mdast";
import type { Paragraph } from "mdast";
import type { TableCell } from "mdast";
import type { Html } from "mdast";
import type { Image } from "mdast";
import type { ImageReference } from "mdast";
import type { InlineCode } from "mdast";
import type { LinkReference } from "mdast";
import type { InlineMath } from "mdast-util-math";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,85 @@
/**
* @fileoverview Rule to prevent reversed link and image syntax in Markdown.
* @author xbinaryx
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Heading, Paragraph, TableCell, Html, Image, ImageReference, InlineCode, LinkReference } from "mdast";
* @import { InlineMath } from "mdast-util-math";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"reversedSyntax"} NoReversedMediaSyntaxMessageIds
* @typedef {[]} NoReversedMediaSyntaxOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoReversedMediaSyntaxOptions, MessageIds: NoReversedMediaSyntaxMessageIds }>} NoReversedMediaSyntaxRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/** Matches reversed link/image syntax like `(text)[url]`, ignoring escaped characters like `\(text\)[url]`. */
const reversedPattern = /(?<=(?<!\\)(?:\\{2})*)\((?<label>(?:\\.|[^()\\]|\([\s\S]*\))*)\)\[(?<url>(?:\\.|[^\]\\\r\n])*)\](?!\()/gu;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoReversedMediaSyntaxRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow reversed link and image syntax",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-reversed-media-syntax.md",
},
fixable: "code",
messages: {
reversedSyntax: "Unexpected reversed syntax found. Use [label](URL) syntax instead.",
},
},
create(context) {
const { sourceCode } = context;
/** @type {string[]} */
let buffer;
/** @type {number} */
let nodeStartOffset;
return {
"heading, paragraph, tableCell"(
/** @type {Heading | Paragraph | TableCell} */ node) {
// Use UTF-16 code units so the buffer stays aligned with source offsets.
buffer = sourceCode.getText(node).split("");
// Store the start offset of the node for later calculations.
nodeStartOffset = node.position.start.offset;
},
":matches(heading, paragraph, tableCell) :matches(html, image, imageReference, inlineCode, linkReference, inlineMath)"(
/** @type {Html | Image | ImageReference | InlineCode | LinkReference | InlineMath} */ node) {
const [startOffset, endOffset] = sourceCode.getRange(node);
// Mask the content of `html`, `image`, `imageReference`, `inlineCode`, `linkReference`, and `inlineMath` nodes with whitespaces.
for (let i = startOffset; i < endOffset; i++) {
buffer[i - nodeStartOffset] = " ";
}
},
":matches(heading, paragraph, tableCell):exit"() {
const maskedText = buffer.join("");
/** @type {RegExpExecArray | null} */
let match;
while ((match = reversedPattern.exec(maskedText)) !== null) {
const { label, url } = match.groups;
const startOffset = match.index + nodeStartOffset; // Adjust `reversedPattern` match index to the full source code.
const endOffset = startOffset + match[0].length;
const labelStartOffset = startOffset + 1; // Skip "("
const labelEndOffset = labelStartOffset + label.length;
const urlStartOffset = labelEndOffset + 2; // Skip ")["
const urlEndOffset = urlStartOffset + url.length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset),
},
messageId: "reversedSyntax",
fix(fixer) {
return fixer.replaceTextRange([startOffset, endOffset], `[${sourceCode.text.slice(labelStartOffset, labelEndOffset)}](${sourceCode.text.slice(urlStartOffset, urlEndOffset)})`);
},
});
}
},
};
},
});
@@ -0,0 +1,51 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
let fixable: "whitespace";
namespace messages {
let spaceInEmphasis: string;
}
let schema: {
type: "object";
properties: {
checkStrikethrough: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
checkStrikethrough: false;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoSpaceInEmphasisOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "spaceInEmphasis";
}>): {
"heading, paragraph, tableCell"(node: Heading | Paragraph | TableCell): void;
":matches(heading, paragraph, tableCell) > text"(node: Text): void;
":matches(heading, paragraph, tableCell):exit"(node: Heading | Paragraph | TableCell): void;
};
}
export default _default;
export type NoSpaceInEmphasisMessageIds = "spaceInEmphasis";
export type NoSpaceInEmphasisOptions = [{
checkStrikethrough?: boolean;
}];
export type NoSpaceInEmphasisRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoSpaceInEmphasisOptions;
MessageIds: NoSpaceInEmphasisMessageIds;
}>;
import type { Heading } from "mdast";
import type { Paragraph } from "mdast";
import type { TableCell } from "mdast";
import type { Text } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,134 @@
/**
* @fileoverview Rule to prevent spaces around emphasis markers in Markdown.
* @author Pixel998
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { SourceRange } from "@eslint/core";
* @import { Heading, Paragraph, TableCell, Text } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"spaceInEmphasis"} NoSpaceInEmphasisMessageIds
* @typedef {[{ checkStrikethrough?: boolean }]} NoSpaceInEmphasisOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoSpaceInEmphasisOptions, MessageIds: NoSpaceInEmphasisMessageIds }>} NoSpaceInEmphasisRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const whitespacePattern = /[ \t]/u;
/**
* Creates a marker pattern based on whether strikethrough should be included.
* @param {boolean} checkStrikethrough Whether to include strikethrough markers.
* @returns {RegExp} The marker pattern.
*/
function createMarkerPattern(checkStrikethrough) {
return checkStrikethrough
? /(?<=(?<!\\)(?:\\{2})*)(?:\*\*\*|\*\*|\*|___|__|_|~~|~)/gu
: /(?<=(?<!\\)(?:\\{2})*)(?:\*\*\*|\*\*|\*|___|__|_)/gu;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoSpaceInEmphasisRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow spaces around emphasis markers",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-space-in-emphasis.md",
},
fixable: "whitespace",
messages: {
spaceInEmphasis: "Unexpected space around emphasis marker.",
},
schema: [
{
type: "object",
properties: {
checkStrikethrough: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
checkStrikethrough: false,
},
],
},
create(context) {
const { sourceCode } = context;
const [{ checkStrikethrough }] = context.options;
const markerPattern = createMarkerPattern(checkStrikethrough);
/** @type {string[]} */
let buffer;
/**
* Reports a surrounding-space violation if present.
* @param {number} checkIndex Character index to test for whitespace.
* @param {number} highlightStartIndex Start index for highlighting.
* @param {number} highlightEndIndex End index for highlighting.
* @returns {void}
*/
function reportWhitespace(checkIndex, highlightStartIndex, highlightEndIndex) {
if (whitespacePattern.test(sourceCode.text[checkIndex])) {
context.report({
loc: {
start: sourceCode.getLocFromIndex(highlightStartIndex),
end: sourceCode.getLocFromIndex(highlightEndIndex),
},
messageId: "spaceInEmphasis",
fix(fixer) {
return fixer.removeRange([checkIndex, checkIndex + 1]);
},
});
}
}
return {
"heading, paragraph, tableCell"(
/** @type {Heading | Paragraph | TableCell} */ node) {
const [startOffset, endOffset] = sourceCode.getRange(node);
// Initialize `buffer` with a whitespace-masked character array.
buffer = new Array(endOffset - startOffset).fill(" ");
},
":matches(heading, paragraph, tableCell) > text"(
/** @type {Text} */ node) {
const [startOffset, endOffset] = sourceCode.getRange(node);
const parentNodeStartOffset = // Parent node can be `Heading`, `Paragraph`, or `TableCell`.
sourceCode.getParent(node).position.start.offset;
// Add the content of a `Text` node into the current buffer at the correct offsets.
for (let i = startOffset; i < endOffset; i++) {
buffer[i - parentNodeStartOffset] = sourceCode.text[i];
}
},
":matches(heading, paragraph, tableCell):exit"(
/** @type {Heading | Paragraph | TableCell} */ node) {
const maskedText = buffer.join("");
/** @type {Map<string, SourceRange[]>} */
const markerGroups = new Map();
/** @type {RegExpExecArray | null} */
let match;
while ((match = markerPattern.exec(maskedText)) !== null) {
const marker = match[0];
const startOffset = // Adjust `markerPattern` match index to the full source code.
match.index + node.position.start.offset;
const endOffset = startOffset + marker.length;
if (!markerGroups.has(marker)) {
markerGroups.set(marker, []);
}
markerGroups.get(marker).push([startOffset, endOffset]);
}
for (const group of markerGroups.values()) {
for (let i = 0; i < group.length - 1; i += 2) {
const startMarker = group[i];
reportWhitespace(startMarker[1], startMarker[0], startMarker[1] + 2);
const endMarker = group[i + 1];
reportWhitespace(endMarker[0] - 1, endMarker[0] - 2, endMarker[1]);
}
}
},
};
},
});
@@ -0,0 +1,70 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let unusedDefinition: string;
let unusedFootnoteDefinition: string;
}
let schema: {
type: "object";
properties: {
allowDefinitions: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
allowFootnoteDefinitions: {
type: "array";
items: {
type: "string";
};
uniqueItems: true;
};
checkFootnoteDefinitions: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
allowDefinitions: string[];
allowFootnoteDefinitions: any[];
checkFootnoteDefinitions: true;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: NoUnusedDefinitionsOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: NoUnusedDefinitionsMessageIds;
}>): {
imageReference(node: import("mdast").ImageReference): void;
linkReference(node: import("mdast").LinkReference): void;
footnoteReference(node: import("mdast").FootnoteReference): void;
definition(node: Definition): void;
footnoteDefinition(node: FootnoteDefinition): void;
"root:exit"(): void;
};
}
export default _default;
export type NoUnusedDefinitionsMessageIds = "unusedDefinition" | "unusedFootnoteDefinition";
export type NoUnusedDefinitionsOptions = [{
allowDefinitions?: string[];
allowFootnoteDefinitions?: string[];
checkFootnoteDefinitions?: boolean;
}];
export type NoUnusedDefinitionsRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: NoUnusedDefinitionsOptions;
MessageIds: NoUnusedDefinitionsMessageIds;
}>;
import type { Definition } from "mdast";
import type { FootnoteDefinition } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,130 @@
/**
* @fileoverview Rule to prevent unused definitions in Markdown.
* @author 루밀LuMir(lumirlumir)
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Definition, FootnoteDefinition } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"unusedDefinition" | "unusedFootnoteDefinition"} NoUnusedDefinitionsMessageIds
* @typedef {[{ allowDefinitions?: string[], allowFootnoteDefinitions?: string[], checkFootnoteDefinitions?: boolean }]} NoUnusedDefinitionsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoUnusedDefinitionsOptions, MessageIds: NoUnusedDefinitionsMessageIds }>} NoUnusedDefinitionsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoUnusedDefinitionsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow unused definitions",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-unused-definitions.md",
},
messages: {
unusedDefinition: "Unexpected unused definition `{{ identifier }}` (label: `{{ label }}`) found.",
unusedFootnoteDefinition: "Unexpected unused footnote definition `{{ identifier }}` (label: `{{ label }}`) found.",
},
schema: [
{
type: "object",
properties: {
allowDefinitions: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
allowFootnoteDefinitions: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
checkFootnoteDefinitions: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
allowDefinitions: ["//"],
allowFootnoteDefinitions: [],
checkFootnoteDefinitions: true,
},
],
},
create(context) {
const allowDefinitions = new Set(context.options[0].allowDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
const allowFootnoteDefinitions = new Set(context.options[0].allowFootnoteDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
const [{ checkFootnoteDefinitions }] = context.options;
/** @type {Set<string>} Set to track used identifiers */
const usedIdentifiers = new Set();
/** @type {Set<string>} Set to track used footnote identifiers */
const usedFootnoteIdentifiers = new Set();
/** @type {Set<Definition>} */
const definitions = new Set();
/** @type {Set<FootnoteDefinition>} */
const footnoteDefinitions = new Set();
return {
imageReference(node) {
usedIdentifiers.add(node.identifier);
},
linkReference(node) {
usedIdentifiers.add(node.identifier);
},
footnoteReference(node) {
usedFootnoteIdentifiers.add(node.identifier);
},
definition(node) {
if (allowDefinitions.has(node.identifier)) {
return;
}
definitions.add(node);
},
footnoteDefinition(node) {
if (!checkFootnoteDefinitions ||
allowFootnoteDefinitions.has(node.identifier)) {
return;
}
footnoteDefinitions.add(node);
},
"root:exit"() {
for (const definition of definitions) {
if (!usedIdentifiers.has(definition.identifier)) {
context.report({
node: definition,
messageId: "unusedDefinition",
data: {
identifier: definition.identifier,
label: definition.label.trim(),
},
});
}
}
for (const footnoteDefinition of footnoteDefinitions) {
if (!usedFootnoteIdentifiers.has(footnoteDefinition.identifier)) {
context.report({
node: footnoteDefinition,
messageId: "unusedFootnoteDefinition",
data: {
identifier: footnoteDefinition.identifier,
label: footnoteDefinition.label,
},
});
}
}
},
};
},
});
@@ -0,0 +1,33 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let altTextRequired: string;
}
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: [];
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: "altTextRequired";
}>): {
"image, imageReference"(node: Image | ImageReference): void;
html(node: import("mdast").Html): void;
};
}
export default _default;
export type RequireAltTextMessageIds = "altTextRequired";
export type RequireAltTextOptions = [];
export type RequireAltTextRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: RequireAltTextOptions;
MessageIds: RequireAltTextMessageIds;
}>;
import type { Image } from "mdast";
import type { ImageReference } from "mdast";
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,90 @@
/**
* @fileoverview Rule to require alternative text for images in Markdown.
* @author Pixel998
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { stripHtmlComments } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Image, ImageReference } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"altTextRequired"} RequireAltTextMessageIds
* @typedef {[]} RequireAltTextOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: RequireAltTextOptions, MessageIds: RequireAltTextMessageIds }>} RequireAltTextRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const imgTagPattern = /<img(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?\/?>/giu;
/**
* Creates a regex to match HTML attributes
* @param {string} name The attribute name to match
* @returns {RegExp} Regular expression for matching the attribute
*/
function getHtmlAttributeRe(name) {
return new RegExp(`\\s${name}(?:\\s*=\\s*['"]([^'"]*)['"])?`, "iu");
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {RequireAltTextRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Require alternative text for images",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/require-alt-text.md",
},
messages: {
altTextRequired: "Alternative text for image is required.",
},
},
create(context) {
const { sourceCode } = context;
return {
"image, imageReference"(
/** @type {Image | ImageReference} */ node) {
if (node.alt.trim().length === 0) {
context.report({
loc: node.position,
messageId: "altTextRequired",
});
}
},
html(node) {
const text = stripHtmlComments(sourceCode.getText(node));
/** @type {RegExpExecArray | null} */
let match;
while ((match = imgTagPattern.exec(text)) !== null) {
const imgTag = match[0];
const ariaHiddenMatch = imgTag.match(getHtmlAttributeRe("aria-hidden"));
if (ariaHiddenMatch &&
ariaHiddenMatch[1] &&
ariaHiddenMatch[1].toLowerCase() === "true") {
continue;
}
const altMatch = imgTag.match(getHtmlAttributeRe("alt"));
if (!altMatch ||
(altMatch[1] &&
altMatch[1].trim().length === 0 &&
altMatch[1].length > 0)) {
const startOffset = // Adjust `imgTagPattern` match indices to the full source code.
match.index + node.position.start.offset;
const endOffset = startOffset + imgTag.length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset),
},
messageId: "altTextRequired",
});
}
}
},
};
},
});
@@ -0,0 +1,45 @@
declare namespace _default {
namespace meta {
let type: "problem";
namespace docs {
let recommended: boolean;
let description: string;
let url: string;
}
namespace messages {
let extraCells: string;
let missingCells: string;
}
let schema: {
type: "object";
properties: {
checkMissingCells: {
type: "boolean";
};
};
additionalProperties: false;
}[];
let defaultOptions: [{
checkMissingCells: false;
}];
}
function create(context: import("@eslint/core").RuleContext<{
LangOptions: import("../types.js").MarkdownLanguageOptions;
Code: import("../index.js").MarkdownSourceCode;
RuleOptions: TableColumnCountOptions;
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
MessageIds: TableColumnCountMessageIds;
}>): {
table(node: import("mdast").Table): void;
};
}
export default _default;
export type TableColumnCountMessageIds = "extraCells" | "missingCells";
export type TableColumnCountOptions = [{
checkMissingCells?: boolean;
}];
export type TableColumnCountRuleDefinition = MarkdownRuleDefinition<{
RuleOptions: TableColumnCountOptions;
MessageIds: TableColumnCountMessageIds;
}>;
import type { MarkdownRuleDefinition } from "../types.js";
@@ -0,0 +1,91 @@
/**
* @fileoverview Rule to disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row
* @author Sweta Tanwar (@SwetaTanwar)
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"extraCells" | "missingCells"} TableColumnCountMessageIds
* @typedef {[{ checkMissingCells?: boolean }]} TableColumnCountOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: TableColumnCountOptions, MessageIds: TableColumnCountMessageIds }>} TableColumnCountRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {TableColumnCountRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/table-column-count.md",
},
messages: {
extraCells: "Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), extra data starting here will be ignored.",
missingCells: "Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), row might be missing data.",
},
schema: [
{
type: "object",
properties: {
checkMissingCells: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [{ checkMissingCells: false }],
},
create(context) {
const [{ checkMissingCells }] = context.options;
return {
table(node) {
if (node.children.length < 1) {
return;
}
const headerRow = node.children[0];
const expectedCellsLength = headerRow.children.length;
for (let i = 1; i < node.children.length; i++) {
const currentRow = node.children[i];
const actualCellsLength = currentRow.children.length;
const lastActualCellNode = currentRow.children[actualCellsLength - 1];
if (actualCellsLength > expectedCellsLength) {
const firstExtraCellNode = currentRow.children[expectedCellsLength];
context.report({
loc: {
start: firstExtraCellNode.position.start,
end: lastActualCellNode.position.end,
},
messageId: "extraCells",
data: {
actualCells: actualCellsLength,
expectedCells: expectedCellsLength,
},
});
}
else if (checkMissingCells &&
actualCellsLength < expectedCellsLength) {
context.report({
loc: {
start: {
column: lastActualCellNode.position.end.column -
1,
line: lastActualCellNode.position.end.line,
},
end: currentRow.position.end,
},
messageId: "missingCells",
data: {
actualCells: actualCellsLength,
expectedCells: expectedCellsLength,
},
});
}
}
},
};
},
});