eslint-plugin-markdown-preferences 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -130,6 +130,8 @@ The rules with the following 💄 are included in the `standard` config.
130
130
  | [markdown-preferences/link-bracket-newline](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-bracket-newline.html) | enforce linebreaks after opening and before closing link brackets | 🔧 | 💄 |
131
131
  | [markdown-preferences/link-bracket-spacing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-bracket-spacing.html) | enforce consistent spacing inside link brackets | 🔧 | 💄 |
132
132
  | [markdown-preferences/link-destination-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-destination-style.html) | enforce a consistent style for link destinations | 🔧 | 💄 |
133
+ | [markdown-preferences/link-paren-newline](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-paren-newline.html) | enforce linebreaks after opening and before closing link parentheses | 🔧 | 💄 |
134
+ | [markdown-preferences/link-paren-spacing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-paren-spacing.html) | enforce consistent spacing inside link parentheses | 🔧 | 💄 |
133
135
  | [markdown-preferences/link-title-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-title-style.html) | enforce a consistent style for link titles | 🔧 | 💄 |
134
136
  | [markdown-preferences/list-marker-alignment](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html) | enforce consistent alignment of list markers | 🔧 | ⭐💄 |
135
137
  | [markdown-preferences/no-laziness-blockquotes](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html) | disallow laziness in blockquotes | | ⭐💄 |
package/lib/index.d.ts CHANGED
@@ -95,6 +95,16 @@ interface RuleOptions {
95
95
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-destination-style.html
96
96
  */
97
97
  'markdown-preferences/link-destination-style'?: Linter.RuleEntry<MarkdownPreferencesLinkDestinationStyle>;
98
+ /**
99
+ * enforce linebreaks after opening and before closing link parentheses
100
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-paren-newline.html
101
+ */
102
+ 'markdown-preferences/link-paren-newline'?: Linter.RuleEntry<MarkdownPreferencesLinkParenNewline>;
103
+ /**
104
+ * enforce consistent spacing inside link parentheses
105
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-paren-spacing.html
106
+ */
107
+ 'markdown-preferences/link-paren-spacing'?: Linter.RuleEntry<MarkdownPreferencesLinkParenSpacing>;
98
108
  /**
99
109
  * enforce a consistent style for link titles
100
110
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/link-title-style.html
@@ -297,6 +307,13 @@ type MarkdownPreferencesLinkDestinationStyle = [] | [{
297
307
  avoidEscape?: boolean;
298
308
  [k: string]: unknown | undefined;
299
309
  }];
310
+ type MarkdownPreferencesLinkParenNewline = [] | [{
311
+ newline?: ("always" | "never" | "consistent");
312
+ multiline?: boolean;
313
+ }];
314
+ type MarkdownPreferencesLinkParenSpacing = [] | [{
315
+ space?: ("always" | "never");
316
+ }];
300
317
  type MarkdownPreferencesLinkTitleStyle = [] | [{
301
318
  style?: ("double" | "single" | "parentheses");
302
319
  avoidEscape?: boolean;
@@ -425,7 +442,7 @@ declare namespace meta_d_exports {
425
442
  export { name, version };
426
443
  }
427
444
  declare const name: "eslint-plugin-markdown-preferences";
428
- declare const version: "0.22.0";
445
+ declare const version: "0.23.0";
429
446
  //#endregion
430
447
  //#region src/index.d.ts
431
448
  declare const configs: {
package/lib/index.js CHANGED
@@ -186,47 +186,48 @@ function parseATXHeading(sourceCode, node) {
186
186
  }
187
187
  }
188
188
  };
189
- const spaceAfterOpening = {
190
- text: parsedOpening.rawAfter,
191
- range: [openingSequence.range[1], openingSequence.range[1] + parsedOpening.rawAfter.length],
192
- loc: {
193
- start: openingSequence.loc.end,
194
- end: {
195
- line: openingSequence.loc.end.line,
196
- column: openingSequence.loc.end.column + parsedOpening.rawAfter.length
197
- }
198
- }
189
+ const contentLocStart = {
190
+ line: openingSequence.loc.end.line,
191
+ column: openingSequence.loc.end.column + parsedOpening.after.length
199
192
  };
200
193
  const parsedClosing = parseATXHeadingClosingSequenceFromText(text);
201
194
  if (parsedClosing == null) {
202
- const textAfterOpening = sourceCode.text.slice(spaceAfterOpening.range[1], range[1]);
195
+ const textAfterOpening = sourceCode.text.slice(openingSequence.range[1] + parsedOpening.after.length, range[1]);
203
196
  const contentText$1 = textAfterOpening.trimEnd();
197
+ const contentRange$1 = [openingSequence.range[1] + parsedOpening.after.length, openingSequence.range[1] + parsedOpening.after.length + contentText$1.length];
198
+ const contentLocEnd = {
199
+ line: loc.end.line,
200
+ column: loc.end.column - (textAfterOpening.length - contentText$1.length)
201
+ };
202
+ const after = contentText$1 === textAfterOpening ? null : {
203
+ text: textAfterOpening.slice(contentText$1.length),
204
+ range: [contentRange$1[1], range[1]],
205
+ loc: {
206
+ start: contentLocEnd,
207
+ end: loc.end
208
+ }
209
+ };
204
210
  return {
205
- openingSequence: {
206
- ...openingSequence,
207
- raws: { spaceAfter: spaceAfterOpening }
208
- },
211
+ openingSequence,
209
212
  content: {
210
213
  text: contentText$1,
211
- range: [spaceAfterOpening.range[1], spaceAfterOpening.range[1] + contentText$1.length],
214
+ range: contentRange$1,
212
215
  loc: {
213
- start: spaceAfterOpening.loc.end,
214
- end: {
215
- line: loc.end.line,
216
- column: loc.end.column - (textAfterOpening.length - contentText$1.length)
217
- }
216
+ start: contentLocStart,
217
+ end: contentLocEnd
218
218
  }
219
219
  },
220
- closingSequence: null
220
+ closingSequence: null,
221
+ after
221
222
  };
222
223
  }
223
224
  const spaceAfterClosing = {
224
- text: parsedClosing.rawAfter,
225
- range: [range[1] - parsedClosing.rawAfter.length, range[1]],
225
+ text: parsedClosing.after,
226
+ range: [range[1] - parsedClosing.after.length, range[1]],
226
227
  loc: {
227
228
  start: {
228
229
  line: loc.end.line,
229
- column: loc.end.column - parsedClosing.rawAfter.length
230
+ column: loc.end.column - parsedClosing.after.length
230
231
  },
231
232
  end: loc.end
232
233
  }
@@ -242,38 +243,23 @@ function parseATXHeading(sourceCode, node) {
242
243
  end: spaceAfterClosing.loc.start
243
244
  }
244
245
  };
245
- const spaceBeforeClosing = {
246
- text: parsedClosing.rawBefore,
247
- range: [closingSequence.range[0] - parsedClosing.rawBefore.length, closingSequence.range[0]],
248
- loc: {
249
- start: {
250
- line: closingSequence.loc.start.line,
251
- column: closingSequence.loc.start.column - parsedClosing.rawBefore.length
252
- },
253
- end: closingSequence.loc.start
254
- }
255
- };
256
- const contentText = sourceCode.text.slice(spaceAfterOpening.range[1], spaceBeforeClosing.range[0]);
246
+ const contentRange = [openingSequence.range[1] + parsedOpening.after.length, closingSequence.range[0] - parsedClosing.before.length];
247
+ const contentText = sourceCode.text.slice(...contentRange);
257
248
  return {
258
- openingSequence: {
259
- ...openingSequence,
260
- raws: { spaceAfter: spaceAfterOpening }
261
- },
249
+ openingSequence,
262
250
  content: {
263
251
  text: contentText,
264
- range: [spaceAfterOpening.range[1], spaceBeforeClosing.range[0]],
252
+ range: contentRange,
265
253
  loc: {
266
- start: spaceAfterOpening.loc.end,
267
- end: spaceBeforeClosing.loc.start
254
+ start: contentLocStart,
255
+ end: {
256
+ line: closingSequence.loc.start.line,
257
+ column: closingSequence.loc.start.column - parsedClosing.before.length
258
+ }
268
259
  }
269
260
  },
270
- closingSequence: {
271
- ...closingSequence,
272
- raws: {
273
- spaceBefore: spaceBeforeClosing,
274
- spaceAfter: spaceAfterClosing
275
- }
276
- }
261
+ closingSequence,
262
+ after: spaceAfterClosing.range[0] < spaceAfterClosing.range[1] ? spaceAfterClosing : null
277
263
  };
278
264
  }
279
265
  /**
@@ -287,7 +273,7 @@ function parseATXHeadingOpeningSequenceFromText(text) {
287
273
  if (afterOffset === openingSequenceAfterOffset || afterOffset >= text.length) return null;
288
274
  return {
289
275
  openingSequence: text.slice(0, openingSequenceAfterOffset),
290
- rawAfter: text.slice(openingSequenceAfterOffset, afterOffset)
276
+ after: text.slice(openingSequenceAfterOffset, afterOffset)
291
277
  };
292
278
  /**
293
279
  * Skip whitespace characters at the start of the text.
@@ -310,9 +296,9 @@ function parseATXHeadingClosingSequenceFromText(text) {
310
296
  const beforeOffset = skipEndWhitespace(closingSequenceBeforeOffset);
311
297
  if (beforeOffset === closingSequenceBeforeOffset || beforeOffset < 0) return null;
312
298
  return {
313
- rawBefore: text.slice(beforeOffset + 1, closingSequenceBeforeOffset + 1),
299
+ before: text.slice(beforeOffset + 1, closingSequenceBeforeOffset + 1),
314
300
  closingSequence: text.slice(closingSequenceBeforeOffset + 1, trimmedEndOffset),
315
- rawAfter: text.slice(trimmedEndOffset)
301
+ after: text.slice(trimmedEndOffset)
316
302
  };
317
303
  /**
318
304
  * Skip whitespace characters at the end of the text.
@@ -577,16 +563,16 @@ var atx_heading_closing_sequence_default = createRule("atx-heading-closing-seque
577
563
  context.report({
578
564
  node,
579
565
  loc: {
580
- start: parsed.closingSequence.raws.spaceBefore.loc.start,
566
+ start: parsed.content.loc.end,
581
567
  end: parsed.closingSequence.loc.end
582
568
  },
583
569
  messageId: "forbidClosing",
584
570
  *fix(fixer) {
585
- const removeRange = [parsed.closingSequence.raws.spaceBefore.range[0], parsed.closingSequence.range[1]];
571
+ const removeRange = [parsed.content.range[1], parsed.closingSequence.range[1]];
586
572
  const newHeadingText = sourceCode.text.slice(sourceCode.getRange(node)[0], removeRange[0]);
587
573
  const newHeadingParsed = parseATXHeadingClosingSequenceFromText(newHeadingText);
588
574
  if (newHeadingParsed) {
589
- const escapeIndex = removeRange[0] - newHeadingParsed.rawAfter.length - 1;
575
+ const escapeIndex = removeRange[0] - newHeadingParsed.after.length - 1;
590
576
  yield fixer.insertTextBeforeRange([escapeIndex, escapeIndex], "\\");
591
577
  }
592
578
  yield fixer.removeRange(removeRange);
@@ -4527,8 +4513,8 @@ var level1_heading_style_default = createRule("level1-heading-style", {
4527
4513
  *fix(fixer) {
4528
4514
  const parsed = parseATXHeading(sourceCode, node);
4529
4515
  if (!parsed) return;
4530
- yield fixer.removeRange([parsed.openingSequence.range[0], parsed.openingSequence.raws.spaceAfter.range[1]]);
4531
- if (parsed.closingSequence) yield fixer.removeRange([parsed.closingSequence.raws.spaceBefore.range[0], parsed.closingSequence.raws.spaceAfter.range[1]]);
4516
+ yield fixer.removeRange([parsed.openingSequence.range[0], parsed.content.range[0]]);
4517
+ if (parsed.closingSequence) yield fixer.removeRange([parsed.content.range[1], parsed.after?.range[1] ?? parsed.closingSequence.range[1]]);
4532
4518
  const lines = getParsedLines(sourceCode);
4533
4519
  const underline = "=".repeat(Math.max(getTextWidth(parsed.content.text), 3));
4534
4520
  const appendingText = `\n${lines.get(parsed.openingSequence.loc.start.line).text.slice(0, parsed.openingSequence.loc.start.column - 1)}${underline}`;
@@ -4604,8 +4590,8 @@ var level2_heading_style_default = createRule("level2-heading-style", {
4604
4590
  *fix(fixer) {
4605
4591
  const parsed = parseATXHeading(sourceCode, node);
4606
4592
  if (!parsed) return;
4607
- yield fixer.removeRange([parsed.openingSequence.range[0], parsed.openingSequence.raws.spaceAfter.range[1]]);
4608
- if (parsed.closingSequence) yield fixer.removeRange([parsed.closingSequence.raws.spaceBefore.range[0], parsed.closingSequence.raws.spaceAfter.range[1]]);
4593
+ yield fixer.removeRange([parsed.openingSequence.range[0], parsed.content.range[0]]);
4594
+ if (parsed.closingSequence) yield fixer.removeRange([parsed.content.range[1], parsed.after?.range[1] ?? parsed.closingSequence.range[1]]);
4609
4595
  const lines = getParsedLines(sourceCode);
4610
4596
  const underline = "-".repeat(Math.max(getTextWidth(parsed.content.text), 3));
4611
4597
  const appendingText = `\n${lines.get(parsed.openingSequence.loc.start.line).text.slice(0, parsed.openingSequence.loc.start.column - 1)}${underline}`;
@@ -4633,12 +4619,18 @@ function parseInlineLink(sourceCode, node) {
4633
4619
  }
4634
4620
  const parsed = parseInlineLinkDestAndTitleFromText(sourceCode.text.slice(textRange[1], nodeRange[1]));
4635
4621
  if (!parsed) return null;
4622
+ const openingParenRange = [textRange[1] + parsed.openingParen.range[0], textRange[1] + parsed.openingParen.range[1]];
4636
4623
  const destinationRange = [textRange[1] + parsed.destination.range[0], textRange[1] + parsed.destination.range[1]];
4624
+ const closingParenRange = [textRange[1] + parsed.closingParen.range[0], textRange[1] + parsed.closingParen.range[1]];
4637
4625
  return {
4638
4626
  text: {
4639
4627
  range: textRange,
4640
4628
  loc: getSourceLocationFromRange(sourceCode, node, textRange)
4641
4629
  },
4630
+ openingParen: {
4631
+ range: openingParenRange,
4632
+ loc: getSourceLocationFromRange(sourceCode, node, openingParenRange)
4633
+ },
4642
4634
  destination: {
4643
4635
  type: parsed.destination.type,
4644
4636
  text: parsed.destination.text,
@@ -4650,7 +4642,11 @@ function parseInlineLink(sourceCode, node) {
4650
4642
  text: parsed.title.text,
4651
4643
  range: [textRange[1] + parsed.title.range[0], textRange[1] + parsed.title.range[1]],
4652
4644
  loc: getSourceLocationFromRange(sourceCode, node, [textRange[1] + parsed.title.range[0], textRange[1] + parsed.title.range[1]])
4653
- } : null
4645
+ } : null,
4646
+ closingParen: {
4647
+ range: closingParenRange,
4648
+ loc: getSourceLocationFromRange(sourceCode, node, closingParenRange)
4649
+ }
4654
4650
  };
4655
4651
  }
4656
4652
  /**
@@ -4685,12 +4681,15 @@ function parseInlineLinkDestAndTitleFromText(text) {
4685
4681
  }
4686
4682
  skipSpaces();
4687
4683
  if (text[index] === ")") {
4684
+ const closingParenStartIndex$1 = index;
4688
4685
  index++;
4689
4686
  skipSpaces();
4690
4687
  if (index < text.length) return null;
4691
4688
  return {
4689
+ openingParen: { range: [0, 1] },
4692
4690
  destination,
4693
- title: null
4691
+ title: null,
4692
+ closingParen: { range: [closingParenStartIndex$1, index] }
4694
4693
  };
4695
4694
  }
4696
4695
  if (text.length <= index) return null;
@@ -4711,12 +4710,15 @@ function parseInlineLinkDestAndTitleFromText(text) {
4711
4710
  } else return null;
4712
4711
  skipSpaces();
4713
4712
  if (text[index] !== ")") return null;
4713
+ const closingParenStartIndex = index;
4714
4714
  index++;
4715
4715
  skipSpaces();
4716
4716
  if (index < text.length) return null;
4717
4717
  return {
4718
+ openingParen: { range: [0, 1] },
4718
4719
  destination,
4719
- title
4720
+ title,
4721
+ closingParen: { range: [closingParenStartIndex, index] }
4720
4722
  };
4721
4723
  /**
4722
4724
  * Skip spaces
@@ -4808,12 +4810,18 @@ function parseImage(sourceCode, node) {
4808
4810
  if (!parsed) return null;
4809
4811
  const nodeRange = sourceCode.getRange(node);
4810
4812
  const textRange = [nodeRange[0] + parsed.text.range[0], nodeRange[0] + parsed.text.range[1]];
4813
+ const openingParenRange = [nodeRange[0] + parsed.openingParen.range[0], nodeRange[0] + parsed.openingParen.range[1]];
4811
4814
  const destinationRange = [nodeRange[0] + parsed.destination.range[0], nodeRange[0] + parsed.destination.range[1]];
4815
+ const closingParenRange = [nodeRange[0] + parsed.closingParen.range[0], nodeRange[0] + parsed.closingParen.range[1]];
4812
4816
  return {
4813
4817
  text: {
4814
4818
  range: textRange,
4815
4819
  loc: getSourceLocationFromRange(sourceCode, node, textRange)
4816
4820
  },
4821
+ openingParen: {
4822
+ range: openingParenRange,
4823
+ loc: getSourceLocationFromRange(sourceCode, node, openingParenRange)
4824
+ },
4817
4825
  destination: {
4818
4826
  type: parsed.destination.type,
4819
4827
  text: parsed.destination.text,
@@ -4825,7 +4833,11 @@ function parseImage(sourceCode, node) {
4825
4833
  text: parsed.title.text,
4826
4834
  range: [nodeRange[0] + parsed.title.range[0], nodeRange[0] + parsed.title.range[1]],
4827
4835
  loc: getSourceLocationFromRange(sourceCode, node, [nodeRange[0] + parsed.title.range[0], nodeRange[0] + parsed.title.range[1]])
4828
- } : null
4836
+ } : null,
4837
+ closingParen: {
4838
+ range: closingParenRange,
4839
+ loc: getSourceLocationFromRange(sourceCode, node, closingParenRange)
4840
+ }
4829
4841
  };
4830
4842
  }
4831
4843
  /**
@@ -4836,6 +4848,7 @@ function parseImageFromText(text) {
4836
4848
  let index = text.length - 1;
4837
4849
  skipSpaces();
4838
4850
  if (text[index] !== ")") return null;
4851
+ const closingParenStartIndex = index;
4839
4852
  index--;
4840
4853
  skipSpaces();
4841
4854
  let title = null;
@@ -4880,12 +4893,16 @@ function parseImageFromText(text) {
4880
4893
  }
4881
4894
  skipSpaces();
4882
4895
  if (text[index] !== "(") return null;
4896
+ const openingParenStartIndex = index;
4883
4897
  index--;
4884
4898
  if (text[index] !== "]") return null;
4899
+ const textRange = [1, index + 1];
4885
4900
  return {
4886
- text: { range: [1, index + 1] },
4901
+ openingParen: { range: [openingParenStartIndex, openingParenStartIndex + 1] },
4902
+ text: { range: textRange },
4887
4903
  destination,
4888
- title
4904
+ title,
4905
+ closingParen: { range: [closingParenStartIndex, closingParenStartIndex + 1] }
4889
4906
  };
4890
4907
  /**
4891
4908
  * Skip spaces
@@ -5624,6 +5641,265 @@ function hasLoneLastBackslash(text) {
5624
5641
  return escapeText.length % 2 === 1;
5625
5642
  }
5626
5643
 
5644
+ //#endregion
5645
+ //#region src/rules/link-paren-newline.ts
5646
+ var link_paren_newline_default = createRule("link-paren-newline", {
5647
+ meta: {
5648
+ type: "layout",
5649
+ docs: {
5650
+ description: "enforce linebreaks after opening and before closing link parentheses",
5651
+ categories: ["standard"],
5652
+ listCategory: "Stylistic"
5653
+ },
5654
+ fixable: "whitespace",
5655
+ hasSuggestions: false,
5656
+ schema: [{
5657
+ type: "object",
5658
+ properties: {
5659
+ newline: { enum: [
5660
+ "always",
5661
+ "never",
5662
+ "consistent"
5663
+ ] },
5664
+ multiline: { type: "boolean" }
5665
+ },
5666
+ additionalProperties: false
5667
+ }],
5668
+ messages: {
5669
+ expectedNewlineAfterOpeningParen: "Expected a linebreak after this opening parenthesis.",
5670
+ unexpectedNewlineAfterOpeningParen: "Unexpected linebreak after this opening parenthesis.",
5671
+ expectedNewlineBeforeClosingParen: "Expected a linebreak before this closing parenthesis.",
5672
+ unexpectedNewlineBeforeClosingParen: "Unexpected linebreak before this closing parenthesis."
5673
+ }
5674
+ },
5675
+ create(context) {
5676
+ const sourceCode = context.sourceCode;
5677
+ const optionProvider = parseOptions$3(context.options[0]);
5678
+ /**
5679
+ * Parse the options.
5680
+ */
5681
+ function parseOptions$3(option) {
5682
+ const newline = option?.newline ?? "never";
5683
+ const multiline = option?.multiline ?? false;
5684
+ return (openingParenIndex, closingParenIndex) => {
5685
+ if (multiline) {
5686
+ if (sourceCode.text.slice(openingParenIndex + 1, closingParenIndex).trim().includes("\n")) return "always";
5687
+ }
5688
+ return newline;
5689
+ };
5690
+ }
5691
+ /**
5692
+ * Verify the newline around the parentheses.
5693
+ */
5694
+ function verifyNewlineAroundParens(node, openingParenIndex, closingParenIndex) {
5695
+ const newline = optionProvider(openingParenIndex, closingParenIndex);
5696
+ const spaceAfterOpeningParen = getSpaceAfterOpeningParen(openingParenIndex);
5697
+ verifyNewlineAfterOpeningParen(node, spaceAfterOpeningParen, openingParenIndex, newline);
5698
+ const newlineOptionBeforeClosingParen = newline === "consistent" ? getSpaceAfterOpeningParen(openingParenIndex).includes("\n") ? "always" : "never" : newline;
5699
+ verifyNewlineBeforeClosingParen(node, getSpaceBeforeClosingParen(closingParenIndex), openingParenIndex, closingParenIndex, newlineOptionBeforeClosingParen);
5700
+ }
5701
+ /**
5702
+ * Verify the newline after the opening parenthesis and before the closing parenthesis.
5703
+ */
5704
+ function verifyNewlineAfterOpeningParen(node, spaceAfterOpeningParen, openingParenIndex, newline) {
5705
+ if (newline === "always") {
5706
+ if (spaceAfterOpeningParen.includes("\n")) return;
5707
+ const loc = getSourceLocationFromRange(sourceCode, node, [openingParenIndex, openingParenIndex + 1]);
5708
+ context.report({
5709
+ node,
5710
+ loc,
5711
+ messageId: "expectedNewlineAfterOpeningParen",
5712
+ fix: (fixer) => fixer.insertTextAfterRange([openingParenIndex, openingParenIndex + 1], `\n${" ".repeat(loc.start.column)}${spaceAfterOpeningParen}`)
5713
+ });
5714
+ } else if (newline === "never") {
5715
+ if (!spaceAfterOpeningParen.includes("\n")) return;
5716
+ context.report({
5717
+ node,
5718
+ loc: getSourceLocationFromRange(sourceCode, node, [openingParenIndex + 1, openingParenIndex + 1 + spaceAfterOpeningParen.length]),
5719
+ messageId: "unexpectedNewlineAfterOpeningParen",
5720
+ fix: (fixer) => fixer.replaceTextRange([openingParenIndex + 1, openingParenIndex + 1 + spaceAfterOpeningParen.length], " ")
5721
+ });
5722
+ }
5723
+ }
5724
+ /**
5725
+ * Verify the newline before the closing parenthesis.
5726
+ */
5727
+ function verifyNewlineBeforeClosingParen(node, spaceBeforeClosingParen, openingParenIndex, closingParenIndex, newline) {
5728
+ if (openingParenIndex + 1 === closingParenIndex - spaceBeforeClosingParen.length) return;
5729
+ if (newline === "always") {
5730
+ if (spaceBeforeClosingParen.includes("\n")) return;
5731
+ context.report({
5732
+ node,
5733
+ loc: getSourceLocationFromRange(sourceCode, node, [closingParenIndex, closingParenIndex + 1]),
5734
+ messageId: "expectedNewlineBeforeClosingParen",
5735
+ fix: (fixer) => {
5736
+ const openingParenLoc = getSourceLocationFromRange(sourceCode, node, [openingParenIndex, openingParenIndex + 1]);
5737
+ return fixer.insertTextBeforeRange([closingParenIndex, closingParenIndex + 1], `\n${" ".repeat(openingParenLoc.start.column)}`);
5738
+ }
5739
+ });
5740
+ } else if (newline === "never") {
5741
+ if (!spaceBeforeClosingParen.includes("\n")) return;
5742
+ context.report({
5743
+ node,
5744
+ loc: getSourceLocationFromRange(sourceCode, node, [closingParenIndex - spaceBeforeClosingParen.length, closingParenIndex]),
5745
+ messageId: "unexpectedNewlineBeforeClosingParen",
5746
+ fix: (fixer) => fixer.replaceTextRange([closingParenIndex - spaceBeforeClosingParen.length, closingParenIndex], " ")
5747
+ });
5748
+ }
5749
+ }
5750
+ return {
5751
+ link(node) {
5752
+ if (getLinkKind(sourceCode, node) !== "inline") return;
5753
+ const parsed = parseInlineLink(sourceCode, node);
5754
+ if (!parsed) return;
5755
+ verifyNewlineAroundParens(node, parsed.openingParen.range[0], parsed.closingParen.range[0]);
5756
+ },
5757
+ image(node) {
5758
+ const parsed = parseImage(sourceCode, node);
5759
+ if (!parsed) return;
5760
+ verifyNewlineAroundParens(node, parsed.openingParen.range[0], parsed.closingParen.range[0]);
5761
+ }
5762
+ };
5763
+ /**
5764
+ * Get the spaces after the opening parenthesis.
5765
+ */
5766
+ function getSpaceAfterOpeningParen(openingParenIndex) {
5767
+ for (let i = openingParenIndex + 1; i < sourceCode.text.length; i++) {
5768
+ const char = sourceCode.text[i];
5769
+ if (isWhitespace(char)) continue;
5770
+ return sourceCode.text.slice(openingParenIndex + 1, i);
5771
+ }
5772
+ return sourceCode.text.slice(openingParenIndex + 1);
5773
+ }
5774
+ /**
5775
+ * Get the spaces before the closing parenthesis.
5776
+ */
5777
+ function getSpaceBeforeClosingParen(closingParenIndex) {
5778
+ for (let i = closingParenIndex - 1; i >= 0; i--) {
5779
+ const char = sourceCode.text[i];
5780
+ if (isWhitespace(char)) continue;
5781
+ return sourceCode.text.slice(i + 1, closingParenIndex);
5782
+ }
5783
+ return sourceCode.text.slice(0, closingParenIndex);
5784
+ }
5785
+ }
5786
+ });
5787
+
5788
+ //#endregion
5789
+ //#region src/rules/link-paren-spacing.ts
5790
+ var link_paren_spacing_default = createRule("link-paren-spacing", {
5791
+ meta: {
5792
+ type: "layout",
5793
+ docs: {
5794
+ description: "enforce consistent spacing inside link parentheses",
5795
+ categories: ["standard"],
5796
+ listCategory: "Stylistic"
5797
+ },
5798
+ fixable: "whitespace",
5799
+ hasSuggestions: false,
5800
+ schema: [{
5801
+ type: "object",
5802
+ properties: { space: { enum: ["always", "never"] } },
5803
+ additionalProperties: false
5804
+ }],
5805
+ messages: {
5806
+ unexpectedSpaceAfterOpeningParen: "Unexpected space after opening parenthesis.",
5807
+ expectedSpaceAfterOpeningParen: "Expected space after opening parenthesis.",
5808
+ unexpectedSpaceBeforeClosingParen: "Unexpected space before closing parenthesis.",
5809
+ expectedSpaceBeforeClosingParen: "Expected space before closing parenthesis."
5810
+ }
5811
+ },
5812
+ create(context) {
5813
+ const sourceCode = context.sourceCode;
5814
+ const spaceOption = context.options[0]?.space || "never";
5815
+ /**
5816
+ * Verify the space after the opening paren and before the closing paren.
5817
+ */
5818
+ function verifySpaceAfterOpeningParen(node, openingParen) {
5819
+ const space = getSpaceAfterOpeningParen(openingParen);
5820
+ if (space.includes("\n")) return;
5821
+ if (spaceOption === "always") {
5822
+ if (space.length > 0) return;
5823
+ context.report({
5824
+ node,
5825
+ loc: openingParen.loc,
5826
+ messageId: "expectedSpaceAfterOpeningParen",
5827
+ fix: (fixer) => fixer.insertTextAfterRange(openingParen.range, " ")
5828
+ });
5829
+ } else if (spaceOption === "never") {
5830
+ if (space.length === 0) return;
5831
+ context.report({
5832
+ node,
5833
+ loc: getSourceLocationFromRange(sourceCode, node, [openingParen.range[1], openingParen.range[1] + space.length]),
5834
+ messageId: "unexpectedSpaceAfterOpeningParen",
5835
+ fix: (fixer) => fixer.removeRange([openingParen.range[1], openingParen.range[1] + space.length])
5836
+ });
5837
+ }
5838
+ }
5839
+ /**
5840
+ * Verify the space before the closing paren.
5841
+ */
5842
+ function verifySpaceBeforeClosingParen(node, closingParen) {
5843
+ const space = getSpaceBeforeClosingParen(closingParen);
5844
+ if (space.includes("\n")) return;
5845
+ if (spaceOption === "always") {
5846
+ if (space.length > 0) return;
5847
+ context.report({
5848
+ node,
5849
+ loc: closingParen.loc,
5850
+ messageId: "expectedSpaceBeforeClosingParen",
5851
+ fix: (fixer) => fixer.insertTextBeforeRange(closingParen.range, " ")
5852
+ });
5853
+ } else if (spaceOption === "never") {
5854
+ if (space.length === 0) return;
5855
+ context.report({
5856
+ node,
5857
+ loc: getSourceLocationFromRange(sourceCode, node, [closingParen.range[0] - space.length, closingParen.range[0]]),
5858
+ messageId: "unexpectedSpaceBeforeClosingParen",
5859
+ fix: (fixer) => fixer.removeRange([closingParen.range[0] - space.length, closingParen.range[0]])
5860
+ });
5861
+ }
5862
+ }
5863
+ return {
5864
+ link(node) {
5865
+ if (getLinkKind(sourceCode, node) !== "inline") return;
5866
+ const parsed = parseInlineLink(sourceCode, node);
5867
+ if (!parsed) return;
5868
+ verifySpaceAfterOpeningParen(node, parsed.openingParen);
5869
+ verifySpaceBeforeClosingParen(node, parsed.closingParen);
5870
+ },
5871
+ image(node) {
5872
+ const parsed = parseImage(sourceCode, node);
5873
+ if (!parsed) return;
5874
+ verifySpaceAfterOpeningParen(node, parsed.openingParen);
5875
+ verifySpaceBeforeClosingParen(node, parsed.closingParen);
5876
+ }
5877
+ };
5878
+ /**
5879
+ * Get the spaces after the opening paren.
5880
+ */
5881
+ function getSpaceAfterOpeningParen(openingParen) {
5882
+ for (let i = openingParen.range[1]; i < sourceCode.text.length; i++) {
5883
+ const char = sourceCode.text[i];
5884
+ if (isWhitespace(char)) continue;
5885
+ return sourceCode.text.slice(openingParen.range[1], i);
5886
+ }
5887
+ return sourceCode.text.slice(openingParen.range[1]);
5888
+ }
5889
+ /**
5890
+ * Get the spaces before the closing paren.
5891
+ */
5892
+ function getSpaceBeforeClosingParen(closingParen) {
5893
+ for (let i = closingParen.range[0] - 1; i >= 0; i--) {
5894
+ const char = sourceCode.text[i];
5895
+ if (isWhitespace(char)) continue;
5896
+ return sourceCode.text.slice(i + 1, closingParen.range[0]);
5897
+ }
5898
+ return sourceCode.text.slice(0, closingParen.range[0]);
5899
+ }
5900
+ }
5901
+ });
5902
+
5627
5903
  //#endregion
5628
5904
  //#region src/rules/link-title-style.ts
5629
5905
  const STYLES = {
@@ -8543,6 +8819,8 @@ const rules$1 = [
8543
8819
  link_bracket_newline_default,
8544
8820
  link_bracket_spacing_default,
8545
8821
  link_destination_style_default,
8822
+ link_paren_newline_default,
8823
+ link_paren_spacing_default,
8546
8824
  link_title_style_default,
8547
8825
  list_marker_alignment_default,
8548
8826
  no_laziness_blockquotes_default,
@@ -8634,6 +8912,8 @@ const rules$2 = {
8634
8912
  "markdown-preferences/link-bracket-newline": "error",
8635
8913
  "markdown-preferences/link-bracket-spacing": "error",
8636
8914
  "markdown-preferences/link-destination-style": "error",
8915
+ "markdown-preferences/link-paren-newline": "error",
8916
+ "markdown-preferences/link-paren-spacing": "error",
8637
8917
  "markdown-preferences/link-title-style": "error",
8638
8918
  "markdown-preferences/list-marker-alignment": "error",
8639
8919
  "markdown-preferences/no-laziness-blockquotes": "error",
@@ -8663,7 +8943,7 @@ __export(meta_exports, {
8663
8943
  version: () => version
8664
8944
  });
8665
8945
  const name = "eslint-plugin-markdown-preferences";
8666
- const version = "0.22.0";
8946
+ const version = "0.23.0";
8667
8947
 
8668
8948
  //#endregion
8669
8949
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -94,7 +94,7 @@
94
94
  "eslint-plugin-json-schema-validator": "^5.3.1",
95
95
  "eslint-plugin-jsonc": "^2.19.1",
96
96
  "eslint-plugin-markdown": "^5.1.0",
97
- "eslint-plugin-markdown-links": "^0.4.0",
97
+ "eslint-plugin-markdown-links": "^0.5.0",
98
98
  "eslint-plugin-n": "^17.16.2",
99
99
  "eslint-plugin-node-dependencies": "^1.0.0",
100
100
  "eslint-plugin-prettier": "^5.2.3",
@@ -115,7 +115,7 @@
115
115
  "stylelint-config-recommended-vue": "^1.6.0",
116
116
  "stylelint-config-standard": "^39.0.0",
117
117
  "stylelint-config-standard-vue": "^1.0.0",
118
- "tsdown": "^0.14.0",
118
+ "tsdown": "^0.15.0",
119
119
  "tsx": "^4.19.3",
120
120
  "twoslash-eslint": "^0.3.1",
121
121
  "type-fest": "^4.37.0",