86 lines
4.5 KiB
JavaScript
86 lines
4.5 KiB
JavaScript
/**
|
|
* @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)})`);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|