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
2423const 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 } ,
0 commit comments