first commit
This commit is contained in:
+49
@@ -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";
|
||||
+106
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+35
@@ -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";
|
||||
+82
@@ -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",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+47
@@ -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";
|
||||
+84
@@ -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
@@ -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
@@ -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}>`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+66
@@ -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";
|
||||
+121
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+45
@@ -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";
|
||||
+101
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+64
@@ -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";
|
||||
+115
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+30
@@ -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";
|
||||
+41
@@ -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",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+30
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+32
@@ -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";
|
||||
+117
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+46
@@ -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";
|
||||
Generated
Vendored
+101
@@ -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], " ");
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+52
@@ -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";
|
||||
+147
@@ -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);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+56
@@ -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";
|
||||
Generated
Vendored
+141
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+49
@@ -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
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+35
@@ -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";
|
||||
+84
@@ -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: ``, 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}]`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+42
@@ -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";
|
||||
+85
@@ -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)})`);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+51
@@ -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";
|
||||
+134
@@ -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]);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+70
@@ -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";
|
||||
+130
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+33
@@ -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";
|
||||
+90
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+45
@@ -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";
|
||||
+91
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user