Skip to content

Commit 8538c10

Browse files
authored
fix: false positives for inline elements in no-reversed-media-syntax (#597)
* fix: false positives for inline elements in `no-reversed-media-syntax` * wip: add more test cases * wip: refactor * wip: refactor
1 parent d22de7d commit 8538c10

2 files changed

Lines changed: 55 additions & 56 deletions

File tree

src/rules/no-reversed-media-syntax.js

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
//-----------------------------------------------------------------------------
99

1010
/**
11-
* @import { SourceRange } from "@eslint/core"
1211
* @import { Heading, Paragraph, TableCell, Html, Image, ImageReference, InlineCode, LinkReference } from "mdast";
1312
* @import { MarkdownRuleDefinition } from "../types.js";
1413
* @typedef {"reversedSyntax"} NoReversedMediaSyntaxMessageIds
@@ -24,18 +23,6 @@
2423
const reversedPattern =
2524
/(?<=(?<!\\)(?:\\{2})*)\((?<label>(?:\\.|[^()\\]|\([\s\S]*\))*)\)\[(?<url>(?:\\.|[^\]\\\r\n])*)\](?!\()/gu;
2625

27-
/**
28-
* Checks if a match is within any skip range
29-
* @param {number} matchIndex The index of the match
30-
* @param {Array<SourceRange>} skipRanges The skip ranges
31-
* @returns {boolean} True if the match is within a skip range
32-
*/
33-
function isInSkipRange(matchIndex, skipRanges) {
34-
return skipRanges.some(
35-
range => range[0] <= matchIndex && matchIndex < range[1],
36-
);
37-
}
38-
3926
//-----------------------------------------------------------------------------
4027
// Rule Definition
4128
//-----------------------------------------------------------------------------
@@ -62,57 +49,64 @@ export default {
6249
create(context) {
6350
const { sourceCode } = context;
6451

65-
/** @type {Array<SourceRange>} */
66-
const skipRanges = [];
67-
68-
/**
69-
* Finds reversed link/image syntax in a node.
70-
* @param {Heading | Paragraph | TableCell} node The node to check.
71-
* @returns {void} Reports any reversed syntax found.
72-
*/
73-
function findReversedMediaSyntax(node) {
74-
const text = sourceCode.getText(node);
75-
76-
/** @type {RegExpExecArray} */
77-
let match;
52+
/** @type {string[]} */
53+
let buffer;
54+
/** @type {number} */
55+
let nodeStartOffset;
7856

79-
while ((match = reversedPattern.exec(text)) !== null) {
80-
const { label, url } = match.groups;
81-
const startOffset = match.index + node.position.start.offset; // Adjust `reversedPattern` match index to the full source code.
82-
const endOffset = startOffset + match[0].length;
83-
84-
if (isInSkipRange(startOffset, skipRanges)) {
85-
continue;
86-
}
57+
return {
58+
"heading, paragraph, tableCell"(
59+
/** @type {Heading | Paragraph | TableCell} */ node,
60+
) {
61+
// Initialize `buffer` with the full character array of the node text.
62+
buffer = Array.from(sourceCode.getText(node));
8763

88-
context.report({
89-
loc: {
90-
start: sourceCode.getLocFromIndex(startOffset),
91-
end: sourceCode.getLocFromIndex(endOffset),
92-
},
93-
messageId: "reversedSyntax",
94-
fix(fixer) {
95-
return fixer.replaceTextRange(
96-
[startOffset, endOffset],
97-
`[${label}](${url})`,
98-
);
99-
},
100-
});
101-
}
102-
}
64+
// Store the start offset of the node for later calculations.
65+
nodeStartOffset = node.position.start.offset;
66+
},
10367

104-
return {
10568
":matches(heading, paragraph, tableCell) :matches(html, image, imageReference, inlineCode, linkReference)"(
10669
/** @type {Html | Image | ImageReference | InlineCode | LinkReference} */ node,
10770
) {
108-
skipRanges.push(sourceCode.getRange(node));
71+
const [startOffset, endOffset] = sourceCode.getRange(node);
72+
73+
// Mask the content of `html`, `image`, `imageReference`, `inlineCode`, and `linkReference` nodes with whitespaces.
74+
for (let i = startOffset; i < endOffset; i++) {
75+
buffer[i - nodeStartOffset] = " ";
76+
}
10977
},
11078

111-
":matches(heading, paragraph, tableCell):exit"(
112-
/** @type {Heading | Paragraph | TableCell} */ node,
113-
) {
114-
findReversedMediaSyntax(node);
115-
skipRanges.length = 0;
79+
":matches(heading, paragraph, tableCell):exit"() {
80+
const maskedText = buffer.join("");
81+
82+
/** @type {RegExpExecArray | null} */
83+
let match;
84+
85+
while ((match = reversedPattern.exec(maskedText)) !== null) {
86+
const { label, url } = match.groups;
87+
const startOffset = match.index + nodeStartOffset; // Adjust `reversedPattern` match index to the full source code.
88+
const endOffset = startOffset + match[0].length;
89+
90+
const labelStartOffset = startOffset + 1; // Skip "("
91+
const labelEndOffset = labelStartOffset + label.length;
92+
93+
const urlStartOffset = labelEndOffset + 2; // Skip ")["
94+
const urlEndOffset = urlStartOffset + url.length;
95+
96+
context.report({
97+
loc: {
98+
start: sourceCode.getLocFromIndex(startOffset),
99+
end: sourceCode.getLocFromIndex(endOffset),
100+
},
101+
messageId: "reversedSyntax",
102+
fix(fixer) {
103+
return fixer.replaceTextRange(
104+
[startOffset, endOffset],
105+
`[${sourceCode.text.slice(labelStartOffset, labelEndOffset)}](${sourceCode.text.slice(urlStartOffset, urlEndOffset)})`,
106+
);
107+
},
108+
});
109+
}
116110
},
117111
};
118112
},

tests/rules/no-reversed-media-syntax.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ ruleTester.run("no-reversed-media-syntax", rule, {
7171
"text [foo](bar)[foo](bar)[foo](bar) text",
7272
"text (text `func()[index]`) text",
7373
'hi <span class="foo(bar)[baz]">hi</span>',
74+
"( <!-- hi)[ --> ]",
75+
'( <img data-custom = ")[" alt="alt"> ]',
76+
"( ![image)[]]()",
77+
"(`)[`]",
78+
"(`)[]`",
7479
// Heading
7580
"# [ESLint](https://eslint.org/)",
7681
"# ![A beautiful sunset](sunset.png)",

0 commit comments

Comments
 (0)