eslint-plugin-markdown-preferences 0.16.0 → 0.18.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/lib/index.js CHANGED
@@ -73,23 +73,30 @@ function getListItemMarker(sourceCode, node) {
73
73
  if (matchDot) return {
74
74
  kind: ".",
75
75
  raw: matchDot[0],
76
- sequence: Number(matchDot[1])
76
+ sequence: {
77
+ value: Number(matchDot[1]),
78
+ raw: matchDot[1]
79
+ }
77
80
  };
78
81
  const matchParen = /^(\d+)\)/.exec(text);
79
82
  return {
80
83
  kind: ")",
81
84
  raw: matchParen[0],
82
- sequence: Number(matchParen[1])
85
+ sequence: {
86
+ value: Number(matchParen[1]),
87
+ raw: matchParen[1]
88
+ }
83
89
  };
84
90
  }
85
91
  /**
86
92
  * Get the marker for a thematic break.
87
93
  */
88
94
  function getThematicBreakMarker(sourceCode, node) {
89
- const text = sourceCode.getText(node);
95
+ const text = sourceCode.getText(node).trimEnd();
90
96
  return {
91
- kind: text.startsWith("-") ? "-" : "*",
92
- hasSpaces: /\s/u.test(text)
97
+ kind: text.startsWith("-") ? "-" : text.startsWith("*") ? "*" : "_",
98
+ hasSpaces: /\s/u.test(text),
99
+ text
93
100
  };
94
101
  }
95
102
  /**
@@ -126,55 +133,138 @@ function getSourceLocationFromRange(sourceCode, node, range) {
126
133
  //#endregion
127
134
  //#region src/utils/atx-heading.ts
128
135
  /**
129
- * Parse the closing sequence of an ATX heading.
136
+ * Parse the ATX heading.
130
137
  */
131
- function parseATXHeadingClosingSequence(sourceCode, node) {
138
+ function parseATXHeading(sourceCode, node) {
132
139
  if (getHeadingKind(sourceCode, node) !== "atx") return null;
133
140
  const loc = sourceCode.getLoc(node);
134
141
  const range = sourceCode.getRange(node);
135
- const parsed = parseATXHeadingClosingSequenceFromText(sourceCode.text.slice(...range));
136
- if (parsed == null) return { closingSequence: null };
137
- const rawAfter = {
138
- text: parsed.rawAfter,
139
- range: [range[1] - parsed.rawAfter.length, range[1]],
142
+ const text = sourceCode.text.slice(...range);
143
+ const parsedOpening = parseATXHeadingOpeningSequenceFromText(text);
144
+ if (parsedOpening === null) return null;
145
+ const openingSequence = {
146
+ text: parsedOpening.openingSequence,
147
+ range: [range[0], range[0] + parsedOpening.openingSequence.length],
148
+ loc: {
149
+ start: loc.start,
150
+ end: {
151
+ line: loc.start.line,
152
+ column: loc.start.column + parsedOpening.openingSequence.length
153
+ }
154
+ }
155
+ };
156
+ const spaceAfterOpening = {
157
+ text: parsedOpening.rawAfter,
158
+ range: [openingSequence.range[1], openingSequence.range[1] + parsedOpening.rawAfter.length],
159
+ loc: {
160
+ start: openingSequence.loc.end,
161
+ end: {
162
+ line: openingSequence.loc.end.line,
163
+ column: openingSequence.loc.end.column + parsedOpening.rawAfter.length
164
+ }
165
+ }
166
+ };
167
+ const parsedClosing = parseATXHeadingClosingSequenceFromText(text);
168
+ if (parsedClosing == null) {
169
+ const textAfterOpening = sourceCode.text.slice(spaceAfterOpening.range[1], range[1]);
170
+ const contentText$1 = textAfterOpening.trimEnd();
171
+ return {
172
+ openingSequence: {
173
+ ...openingSequence,
174
+ raws: { spaceAfter: spaceAfterOpening }
175
+ },
176
+ content: {
177
+ text: contentText$1,
178
+ range: [spaceAfterOpening.range[1], spaceAfterOpening.range[1] + contentText$1.length],
179
+ loc: {
180
+ start: spaceAfterOpening.loc.end,
181
+ end: {
182
+ line: loc.end.line,
183
+ column: loc.end.column - (textAfterOpening.length - contentText$1.length)
184
+ }
185
+ }
186
+ },
187
+ closingSequence: null
188
+ };
189
+ }
190
+ const spaceAfterClosing = {
191
+ text: parsedClosing.rawAfter,
192
+ range: [range[1] - parsedClosing.rawAfter.length, range[1]],
140
193
  loc: {
141
194
  start: {
142
195
  line: loc.end.line,
143
- column: loc.end.column - parsed.rawAfter.length
196
+ column: loc.end.column - parsedClosing.rawAfter.length
144
197
  },
145
- end: {
146
- line: loc.end.line,
147
- column: loc.end.column
148
- }
198
+ end: loc.end
149
199
  }
150
200
  };
151
201
  const closingSequence = {
152
- text: parsed.closingSequence,
153
- range: [rawAfter.range[0] - parsed.closingSequence.length, rawAfter.range[0]],
202
+ text: parsedClosing.closingSequence,
203
+ range: [spaceAfterClosing.range[0] - parsedClosing.closingSequence.length, spaceAfterClosing.range[0]],
154
204
  loc: {
155
205
  start: {
156
- line: rawAfter.loc.start.line,
157
- column: rawAfter.loc.start.column - parsed.closingSequence.length
206
+ line: spaceAfterClosing.loc.start.line,
207
+ column: spaceAfterClosing.loc.start.column - parsedClosing.closingSequence.length
158
208
  },
159
- end: rawAfter.loc.start
209
+ end: spaceAfterClosing.loc.start
160
210
  }
161
211
  };
162
- const rawBefore = {
163
- text: parsed.rawBefore,
164
- range: [closingSequence.range[0] - parsed.rawBefore.length, closingSequence.range[0]],
212
+ const spaceBeforeClosing = {
213
+ text: parsedClosing.rawBefore,
214
+ range: [closingSequence.range[0] - parsedClosing.rawBefore.length, closingSequence.range[0]],
165
215
  loc: {
166
216
  start: {
167
217
  line: closingSequence.loc.start.line,
168
- column: closingSequence.loc.start.column - parsed.rawBefore.length
218
+ column: closingSequence.loc.start.column - parsedClosing.rawBefore.length
169
219
  },
170
220
  end: closingSequence.loc.start
171
221
  }
172
222
  };
223
+ const contentText = sourceCode.text.slice(spaceAfterOpening.range[1], spaceBeforeClosing.range[0]);
224
+ return {
225
+ openingSequence: {
226
+ ...openingSequence,
227
+ raws: { spaceAfter: spaceAfterOpening }
228
+ },
229
+ content: {
230
+ text: contentText,
231
+ range: [spaceAfterOpening.range[1], spaceBeforeClosing.range[0]],
232
+ loc: {
233
+ start: spaceAfterOpening.loc.end,
234
+ end: spaceBeforeClosing.loc.start
235
+ }
236
+ },
237
+ closingSequence: {
238
+ ...closingSequence,
239
+ raws: {
240
+ spaceBefore: spaceBeforeClosing,
241
+ spaceAfter: spaceAfterClosing
242
+ }
243
+ }
244
+ };
245
+ }
246
+ /**
247
+ * Parse the opening sequence from a text string.
248
+ */
249
+ function parseATXHeadingOpeningSequenceFromText(text) {
250
+ if (!text.startsWith("#")) return null;
251
+ let openingSequenceAfterOffset = 1;
252
+ while (openingSequenceAfterOffset < text.length && text[openingSequenceAfterOffset] === "#") openingSequenceAfterOffset++;
253
+ const afterOffset = skipWhitespace(openingSequenceAfterOffset);
254
+ if (afterOffset === openingSequenceAfterOffset || afterOffset >= text.length) return null;
173
255
  return {
174
- rawBefore,
175
- closingSequence,
176
- rawAfter
256
+ openingSequence: text.slice(0, openingSequenceAfterOffset),
257
+ rawAfter: text.slice(openingSequenceAfterOffset, afterOffset)
177
258
  };
259
+ /**
260
+ * Skip whitespace characters at the start of the text.
261
+ */
262
+ function skipWhitespace(index) {
263
+ let result = index;
264
+ let c;
265
+ while (result < text.length && (c = text[result]) && (c === " " || c === " ")) result++;
266
+ return result;
267
+ }
178
268
  }
179
269
  /**
180
270
  * Parse the closing sequence from a text string.
@@ -255,9 +345,23 @@ function getParsedLines(sourceCode) {
255
345
  }
256
346
 
257
347
  //#endregion
258
- //#region src/rules/atx-headings-closing-sequence-length.ts
348
+ //#region src/utils/get-text-width.ts
259
349
  let segmenter;
260
- var atx_headings_closing_sequence_length_default = createRule("atx-headings-closing-sequence-length", {
350
+ /**
351
+ * Get the width of a text string.
352
+ */
353
+ function getTextWidth(text) {
354
+ if (!text.includes(" ")) return stringWidth(text);
355
+ if (!segmenter) segmenter = new Intl.Segmenter("en");
356
+ let width = 0;
357
+ for (const { segment: c } of segmenter.segment(text)) if (c === " ") width += 4 - width % 4;
358
+ else width += stringWidth(c);
359
+ return width;
360
+ }
361
+
362
+ //#endregion
363
+ //#region src/rules/atx-heading-closing-sequence-length.ts
364
+ var atx_heading_closing_sequence_length_default = createRule("atx-heading-closing-sequence-length", {
261
365
  meta: {
262
366
  type: "layout",
263
367
  docs: {
@@ -292,14 +396,11 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
292
396
  /**
293
397
  * Verify the closing sequence length of an ATX heading.
294
398
  */
295
- function verifyATXHeadingClosingSequenceLength(node, getExpected) {
296
- const parsed = parseATXHeadingClosingSequence(sourceCode, node);
297
- if (!parsed || parsed.closingSequence == null) return;
399
+ function verifyATXHeadingClosingSequenceLength(parsed, reportNode, expectedLength) {
298
400
  const actualLength = parsed.closingSequence.text.length;
299
- const expectedLength = getExpected(node, parsed);
300
401
  if (expectedLength === actualLength) return;
301
402
  context.report({
302
- node,
403
+ node: reportNode,
303
404
  loc: parsed.closingSequence.loc,
304
405
  messageId: "wrongClosingLength",
305
406
  data: {
@@ -311,40 +412,41 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
311
412
  }
312
413
  });
313
414
  }
314
- return option.mode === "match-opening" ? (() => {
315
- const getExpected = (node) => node.depth;
316
- return { heading(node) {
317
- verifyATXHeadingClosingSequenceLength(node, getExpected);
318
- } };
319
- })() : option.mode === "length" ? (() => {
415
+ if (option.mode === "match-opening") return { heading(node) {
416
+ const parsed = parseATXHeading(sourceCode, node);
417
+ if (!parsed || parsed.closingSequence == null) return;
418
+ verifyATXHeadingClosingSequenceLength(parsed, node, node.depth);
419
+ } };
420
+ if (option.mode === "length") {
320
421
  const expected = option.length || 2;
321
- const getExpected = () => expected;
322
422
  return { heading(node) {
323
- verifyATXHeadingClosingSequenceLength(node, getExpected);
423
+ const parsed = parseATXHeading(sourceCode, node);
424
+ if (!parsed || parsed.closingSequence == null) return;
425
+ verifyATXHeadingClosingSequenceLength(parsed, node, expected);
324
426
  } };
325
- })() : option.mode === "fixed-line-length" ? (() => {
427
+ }
428
+ if (option.mode === "fixed-line-length") {
326
429
  const totalLength = option.length || 80;
327
- const getExpected = (_node, parsed) => {
328
- return totalLength - getContentLength(parsed);
329
- };
330
430
  return { heading(node) {
331
- verifyATXHeadingClosingSequenceLength(node, getExpected);
431
+ const parsed = parseATXHeading(sourceCode, node);
432
+ if (!parsed || parsed.closingSequence == null) return;
433
+ verifyATXHeadingClosingSequenceLength(parsed, node, totalLength - getContentLength(parsed));
332
434
  } };
333
- })() : option.mode === "consistent" ? (() => {
334
- let getExpected = null;
435
+ }
436
+ if (option.mode === "consistent") {
437
+ let expectedLength = null;
335
438
  return { heading(node) {
336
- if (getExpected == null) {
337
- const parsed = parseATXHeadingClosingSequence(sourceCode, node);
338
- if (!parsed || parsed.closingSequence == null) return;
339
- const expected = parsed.closingSequence.text.length;
340
- getExpected = () => expected;
341
- } else verifyATXHeadingClosingSequenceLength(node, getExpected);
439
+ const parsed = parseATXHeading(sourceCode, node);
440
+ if (!parsed || parsed.closingSequence == null) return;
441
+ if (expectedLength == null) expectedLength = parsed.closingSequence.text.length;
442
+ else verifyATXHeadingClosingSequenceLength(parsed, node, expectedLength);
342
443
  } };
343
- })() : option.mode === "consistent-line-length" ? (() => {
444
+ }
445
+ if (option.mode === "consistent-line-length") {
344
446
  const headings = [];
345
447
  return {
346
448
  heading(node) {
347
- const parsed = parseATXHeadingClosingSequence(sourceCode, node);
449
+ const parsed = parseATXHeading(sourceCode, node);
348
450
  if (!parsed || !parsed.closingSequence) return;
349
451
  headings.push({
350
452
  node,
@@ -370,13 +472,11 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
370
472
  const lineLength = getLineLength(heading.parsed);
371
473
  if (mostLongContentHeading.contentLength < lineLength && lineLength < minLineLength) minLineLength = Math.min(minLineLength, lineLength);
372
474
  }
373
- const getExpected = (_node, parsed) => {
374
- return minLineLength - getContentLength(parsed);
375
- };
376
- for (const { node } of headings) verifyATXHeadingClosingSequenceLength(node, getExpected);
475
+ for (const heading of headings) verifyATXHeadingClosingSequenceLength(heading.parsed, heading.node, minLineLength - getContentLength(heading.parsed));
377
476
  }
378
477
  };
379
- })() : {};
478
+ }
479
+ return {};
380
480
  /**
381
481
  * Get the content length of the heading.
382
482
  */
@@ -397,21 +497,10 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
397
497
  }
398
498
  }
399
499
  });
400
- /**
401
- * Get the width of a text string.
402
- */
403
- function getTextWidth(text) {
404
- if (!text.includes(" ")) return stringWidth(text);
405
- if (!segmenter) segmenter = new Intl.Segmenter("en");
406
- let width = 0;
407
- for (const { segment: c } of segmenter.segment(text)) if (c === " ") width += 4 - width % 4;
408
- else width += stringWidth(c);
409
- return width;
410
- }
411
500
 
412
501
  //#endregion
413
- //#region src/rules/atx-headings-closing-sequence.ts
414
- var atx_headings_closing_sequence_default = createRule("atx-headings-closing-sequence", {
502
+ //#region src/rules/atx-heading-closing-sequence.ts
503
+ var atx_heading_closing_sequence_default = createRule("atx-heading-closing-sequence", {
415
504
  meta: {
416
505
  type: "layout",
417
506
  docs: {
@@ -439,7 +528,7 @@ var atx_headings_closing_sequence_default = createRule("atx-headings-closing-seq
439
528
  if (opt.closingSequence === "always" || opt.closingSequence === "never") closingSequence = opt.closingSequence;
440
529
  }
441
530
  return { heading(node) {
442
- const parsed = parseATXHeadingClosingSequence(sourceCode, node);
531
+ const parsed = parseATXHeading(sourceCode, node);
443
532
  if (!parsed) return;
444
533
  if (closingSequence === "always") {
445
534
  if (parsed.closingSequence) return;
@@ -456,12 +545,12 @@ var atx_headings_closing_sequence_default = createRule("atx-headings-closing-seq
456
545
  context.report({
457
546
  node,
458
547
  loc: {
459
- start: parsed.rawBefore.loc.start,
548
+ start: parsed.closingSequence.raws.spaceBefore.loc.start,
460
549
  end: parsed.closingSequence.loc.end
461
550
  },
462
551
  messageId: "forbidClosing",
463
552
  *fix(fixer) {
464
- const removeRange = [parsed.rawBefore.range[0], parsed.closingSequence.range[1]];
553
+ const removeRange = [parsed.closingSequence.raws.spaceBefore.range[0], parsed.closingSequence.range[1]];
465
554
  const newHeadingText = sourceCode.text.slice(sourceCode.getRange(node)[0], removeRange[0]);
466
555
  const newHeadingParsed = parseATXHeadingClosingSequenceFromText(newHeadingText);
467
556
  if (newHeadingParsed) {
@@ -593,6 +682,176 @@ var blockquote_marker_alignment_default = createRule("blockquote-marker-alignmen
593
682
  }
594
683
  });
595
684
 
685
+ //#endregion
686
+ //#region src/rules/bullet-list-marker-style.ts
687
+ const MARKERS$1 = [
688
+ "-",
689
+ "*",
690
+ "+"
691
+ ];
692
+ /**
693
+ * Get the other marker.
694
+ */
695
+ function getOtherMarker(unavailableMarker) {
696
+ return MARKERS$1.find((mark) => unavailableMarker !== mark);
697
+ }
698
+ /**
699
+ * Parse rule options.
700
+ */
701
+ function parseOptions$1(options) {
702
+ const primary = options.primary || "-";
703
+ const secondary = options.secondary || getOtherMarker(primary);
704
+ if (primary === secondary) throw new Error(`\`primary\` and \`secondary\` cannot be the same (primary: "${primary}", secondary: "${secondary}").`);
705
+ const overrides = (options.overrides ?? []).map((override, index) => {
706
+ const primaryForOverride = override.primary || "-";
707
+ const secondaryForOverride = override.secondary || getOtherMarker(primaryForOverride);
708
+ if (primaryForOverride === secondaryForOverride) throw new Error(`overrides[${index}]: \`primary\` and \`secondary\` cannot be the same (primary: "${primaryForOverride}", secondary: "${secondaryForOverride}").`);
709
+ return {
710
+ level: override.level,
711
+ parentMarker: override.parentMarker ?? "any",
712
+ primary: primaryForOverride,
713
+ secondary: secondaryForOverride
714
+ };
715
+ }).reverse();
716
+ return { get(level, parentMarker) {
717
+ for (const o of overrides) if ((o.level == null || o.level === level) && (o.parentMarker === "any" || o.parentMarker === parentMarker)) return {
718
+ primary: o.primary,
719
+ secondary: o.secondary
720
+ };
721
+ return {
722
+ primary,
723
+ secondary
724
+ };
725
+ } };
726
+ }
727
+ var bullet_list_marker_style_default = createRule("bullet-list-marker-style", {
728
+ meta: {
729
+ type: "layout",
730
+ docs: {
731
+ description: "enforce consistent bullet list (unordered list) marker style",
732
+ categories: [],
733
+ listCategory: "Stylistic"
734
+ },
735
+ fixable: "code",
736
+ hasSuggestions: false,
737
+ schema: [{
738
+ type: "object",
739
+ properties: {
740
+ primary: { enum: MARKERS$1 },
741
+ secondary: { enum: [...MARKERS$1, "any"] },
742
+ overrides: {
743
+ type: "array",
744
+ items: {
745
+ type: "object",
746
+ properties: {
747
+ level: {
748
+ type: "integer",
749
+ minimum: 1
750
+ },
751
+ parentMarker: { enum: [
752
+ ...MARKERS$1,
753
+ "any",
754
+ "ordered"
755
+ ] },
756
+ primary: { enum: MARKERS$1 },
757
+ secondary: { enum: [...MARKERS$1, "any"] }
758
+ },
759
+ additionalProperties: false
760
+ }
761
+ }
762
+ },
763
+ additionalProperties: false
764
+ }],
765
+ messages: { unexpected: "Bullet list marker should be '{{marker}}'." }
766
+ },
767
+ create(context) {
768
+ const sourceCode = context.sourceCode;
769
+ const options = parseOptions$1(context.options[0] || {});
770
+ let containerStack = {
771
+ node: sourceCode.ast,
772
+ level: 1,
773
+ upper: null
774
+ };
775
+ /**
776
+ * Check bullet list marker style
777
+ */
778
+ function checkBulletList(node) {
779
+ let parentMarker;
780
+ if (containerStack.node.type === "listItem") {
781
+ const parentMarkerKind = getListItemMarker(sourceCode, containerStack.node).kind;
782
+ parentMarker = parentMarkerKind === "." || parentMarkerKind === ")" ? "ordered" : parentMarkerKind;
783
+ } else parentMarker = "top";
784
+ const { primary, secondary } = options.get(containerStack.level, parentMarker);
785
+ const nodeIndex = containerStack.node.children.indexOf(node);
786
+ if (nodeIndex === -1) return;
787
+ const prevNode = nodeIndex > 0 ? containerStack.node.children[nodeIndex - 1] : null;
788
+ const prevBulletList = prevNode && (prevNode.type === "list" ? prevNode : null);
789
+ const prevBulletListMarker = prevBulletList && getListItemMarker(sourceCode, prevBulletList);
790
+ const expectedMarker = prevBulletListMarker?.kind !== primary ? primary : secondary;
791
+ if (expectedMarker === "any") return;
792
+ const marker = getListItemMarker(sourceCode, node);
793
+ if (marker.kind === expectedMarker) return;
794
+ const loc = sourceCode.getLoc(node);
795
+ context.report({
796
+ node,
797
+ loc: {
798
+ start: loc.start,
799
+ end: {
800
+ line: loc.start.line,
801
+ column: loc.start.column + marker.raw.length
802
+ }
803
+ },
804
+ messageId: "unexpected",
805
+ data: { marker: expectedMarker },
806
+ *fix(fixer) {
807
+ if (prevNode?.type === "list" && prevBulletListMarker && (prevBulletListMarker.kind === "-" || prevBulletListMarker.kind === "*" || prevBulletListMarker.kind === "+")) yield fixMarker(prevNode.children[0], prevBulletListMarker.kind);
808
+ yield* fixMarkers(node, expectedMarker);
809
+ let prevMarker = expectedMarker;
810
+ for (let index = nodeIndex + 1; index < containerStack.node.children.length; index++) {
811
+ const nextNode = containerStack.node.children[index];
812
+ if (nextNode.type !== "list") break;
813
+ const nextMarker = getListItemMarker(sourceCode, nextNode);
814
+ if (nextMarker.kind !== prevMarker) break;
815
+ let expectedNextMarker = prevMarker === primary ? secondary : primary;
816
+ if (expectedNextMarker === "any") expectedNextMarker = getOtherMarker(prevMarker);
817
+ yield* fixMarkers(nextNode, expectedNextMarker);
818
+ prevMarker = expectedNextMarker;
819
+ }
820
+ /**
821
+ * Fix bullet list markers
822
+ */
823
+ function* fixMarkers(list, replacementMarker) {
824
+ for (const item of list.children) yield fixMarker(item, replacementMarker);
825
+ }
826
+ /**
827
+ * Fix bullet list item marker
828
+ */
829
+ function fixMarker(item, replacementMarker) {
830
+ const range = sourceCode.getRange(item);
831
+ return fixer.replaceTextRange([range[0], range[0] + 1], replacementMarker);
832
+ }
833
+ }
834
+ });
835
+ }
836
+ return {
837
+ list(node) {
838
+ if (node.ordered) return;
839
+ checkBulletList(node);
840
+ },
841
+ "root, blockquote, listItem, footnoteDefinition"(node) {
842
+ containerStack = {
843
+ node,
844
+ level: node.type === "listItem" ? containerStack.level + 1 : 1,
845
+ upper: containerStack
846
+ };
847
+ },
848
+ "root, blockquote, listItem, footnoteDefinition:exit"() {
849
+ containerStack = containerStack.upper;
850
+ }
851
+ };
852
+ }
853
+ });
854
+
596
855
  //#endregion
597
856
  //#region src/rules/canonical-code-block-language.ts
598
857
  const DEFAULT_LANGUAGES = {
@@ -3594,84 +3853,358 @@ var heading_casing_default = createRule("heading-casing", {
3594
3853
  });
3595
3854
 
3596
3855
  //#endregion
3597
- //#region src/rules/list-marker-alignment.ts
3598
- const ALIGN_TO_POSITION_NAME = {
3599
- left: "start",
3600
- right: "end"
3601
- };
3602
- var list_marker_alignment_default = createRule("list-marker-alignment", {
3856
+ //#region src/utils/setext-heading.ts
3857
+ /**
3858
+ * Parse the setext heading.
3859
+ */
3860
+ function parseSetextHeading(sourceCode, node) {
3861
+ if (getHeadingKind(sourceCode, node) !== "setext") return null;
3862
+ const lines = getParsedLines(sourceCode);
3863
+ const contentLines = [];
3864
+ const nodeLoc = sourceCode.getLoc(node);
3865
+ for (let lineNumber = nodeLoc.start.line; lineNumber < nodeLoc.end.line; lineNumber++) {
3866
+ const content = parseContent(lines.get(lineNumber));
3867
+ contentLines.push(content);
3868
+ }
3869
+ const underline = parseUnderline(lines.get(nodeLoc.end.line));
3870
+ if (!underline) return null;
3871
+ return {
3872
+ contentLines,
3873
+ underline
3874
+ };
3875
+ }
3876
+ /**
3877
+ * Parse the content line of a setext heading.
3878
+ */
3879
+ function parseContent(line) {
3880
+ let prefix = "";
3881
+ let spaceBefore = "";
3882
+ let suffix = "";
3883
+ for (let index = 0; index < line.text.length; index++) {
3884
+ const c = line.text[index];
3885
+ if (!c.trim()) {
3886
+ spaceBefore += c;
3887
+ continue;
3888
+ }
3889
+ if (c === ">" && spaceBefore.length < 4) {
3890
+ prefix += spaceBefore + c;
3891
+ spaceBefore = "";
3892
+ continue;
3893
+ }
3894
+ suffix = line.text.slice(index);
3895
+ break;
3896
+ }
3897
+ const content = suffix.trimEnd();
3898
+ const spaceAfter = suffix.slice(content.length);
3899
+ return {
3900
+ text: content,
3901
+ range: [line.range[0] + prefix.length + spaceBefore.length, line.range[1] - line.linebreak.length - spaceAfter.length],
3902
+ loc: {
3903
+ start: {
3904
+ line: line.line,
3905
+ column: prefix.length + spaceBefore.length + 1
3906
+ },
3907
+ end: {
3908
+ line: line.line,
3909
+ column: prefix.length + spaceBefore.length + content.length + 1
3910
+ }
3911
+ },
3912
+ raws: {
3913
+ prefix,
3914
+ spaceBefore,
3915
+ spaceAfter
3916
+ }
3917
+ };
3918
+ }
3919
+ /**
3920
+ * Parse the underline of a setext heading.
3921
+ */
3922
+ function parseUnderline(line) {
3923
+ let marker = null;
3924
+ let underlineText = "";
3925
+ let prefix = "";
3926
+ let spaceBefore = "";
3927
+ let spaceAfter = "";
3928
+ for (let index = line.text.length - 1; index >= 0; index--) {
3929
+ const c = line.text[index];
3930
+ if (!marker) {
3931
+ if (c === "=" || c === "-") {
3932
+ underlineText = c + underlineText;
3933
+ marker = c;
3934
+ } else if (!c.trim()) spaceAfter = c + spaceAfter;
3935
+ else return null;
3936
+ continue;
3937
+ }
3938
+ if (c === marker) {
3939
+ underlineText = c + spaceBefore + underlineText;
3940
+ spaceBefore = "";
3941
+ } else if (!c.trim()) spaceBefore = c + spaceBefore;
3942
+ else {
3943
+ prefix = line.text.slice(0, index + 1);
3944
+ break;
3945
+ }
3946
+ }
3947
+ if (!marker) return null;
3948
+ const underlineLoc = {
3949
+ start: {
3950
+ line: line.line,
3951
+ column: prefix.length + spaceBefore.length + 1
3952
+ },
3953
+ end: {
3954
+ line: line.line,
3955
+ column: prefix.length + spaceBefore.length + underlineText.length + 1
3956
+ }
3957
+ };
3958
+ return {
3959
+ text: underlineText,
3960
+ range: [line.range[0] + prefix.length + spaceBefore.length, line.range[1] - line.linebreak.length - spaceAfter.length],
3961
+ loc: underlineLoc,
3962
+ marker,
3963
+ raws: {
3964
+ prefix,
3965
+ spaceBefore,
3966
+ spaceAfter
3967
+ }
3968
+ };
3969
+ }
3970
+
3971
+ //#endregion
3972
+ //#region src/rules/level1-heading-style.ts
3973
+ var level1_heading_style_default = createRule("level1-heading-style", {
3603
3974
  meta: {
3604
3975
  type: "layout",
3605
3976
  docs: {
3606
- description: "enforce consistent alignment of list markers",
3607
- categories: ["recommended"],
3977
+ description: "enforce consistent style for level 1 headings",
3978
+ categories: [],
3608
3979
  listCategory: "Stylistic"
3609
3980
  },
3610
- fixable: "whitespace",
3981
+ fixable: "code",
3611
3982
  hasSuggestions: false,
3612
3983
  schema: [{
3613
3984
  type: "object",
3614
- properties: { align: { enum: ["left", "right"] } },
3985
+ properties: {
3986
+ style: { enum: ["atx", "setext"] },
3987
+ allowMultilineSetext: { type: "boolean" }
3988
+ },
3615
3989
  additionalProperties: false
3616
3990
  }],
3617
- messages: { incorrectAlignment: "List marker alignment is inconsistent. Expected {{expected}} characters of indentation, but got {{actual}}." }
3991
+ messages: {
3992
+ expectedAtx: "Expected ATX style heading (# Heading).",
3993
+ expectedSetext: "Expected Setext style heading (Heading\\n======).",
3994
+ multilineSetextNotAllowed: "Multiline Setext headings are not allowed."
3995
+ }
3618
3996
  },
3619
3997
  create(context) {
3620
3998
  const sourceCode = context.sourceCode;
3621
- const alignPositionName = ALIGN_TO_POSITION_NAME[context.options[0]?.align ?? "left"];
3622
- /**
3623
- * Get the marker location of a list item
3624
- */
3625
- function getMarkerLocation(node) {
3626
- const start = sourceCode.getLoc(node).start;
3627
- const startColumnIndex = start.column - 1;
3628
- const marker = getListItemMarker(sourceCode, node);
3629
- return {
3630
- line: start.line,
3631
- start: startColumnIndex,
3632
- end: startColumnIndex + marker.raw.length
3633
- };
3634
- }
3635
- /**
3636
- * Check if list items have consistent alignment
3637
- */
3638
- function checkListAlignment(listNode) {
3639
- const items = listNode.children;
3640
- if (items.length <= 1) return;
3641
- const referenceMarkerLocation = getMarkerLocation(items[0]);
3642
- for (const item of items.slice(1)) {
3643
- const markerLocation = getMarkerLocation(item);
3644
- const diff = markerLocation[alignPositionName] - referenceMarkerLocation[alignPositionName];
3645
- if (diff === 0) continue;
3999
+ const opt = context.options[0] || {};
4000
+ const style = opt.style ?? "atx";
4001
+ const allowMultilineSetext = opt.allowMultilineSetext;
4002
+ return { heading(node) {
4003
+ if (node.depth !== 1) return;
4004
+ const headingKind = getHeadingKind(sourceCode, node);
4005
+ if (style === "atx") {
4006
+ if (headingKind !== "setext") return;
4007
+ const parsed = parseSetextHeading(sourceCode, node);
4008
+ if (!parsed) return;
4009
+ const isMultiline = parsed.contentLines.length > 1;
4010
+ if (isMultiline) {
4011
+ if (allowMultilineSetext) return;
4012
+ context.report({
4013
+ node,
4014
+ messageId: "multilineSetextNotAllowed"
4015
+ });
4016
+ return;
4017
+ }
3646
4018
  context.report({
3647
- node: item,
3648
- loc: {
3649
- start: {
3650
- line: markerLocation.line,
3651
- column: markerLocation.start + 1
3652
- },
3653
- end: {
3654
- line: markerLocation.line,
3655
- column: markerLocation.end + 1
3656
- }
3657
- },
3658
- messageId: "incorrectAlignment",
3659
- data: {
3660
- expected: String(markerLocation.start - diff),
3661
- actual: String(markerLocation.start)
3662
- },
3663
- fix(fixer) {
4019
+ node,
4020
+ messageId: "expectedAtx",
4021
+ *fix(fixer) {
4022
+ const heading = parsed.contentLines[0];
4023
+ yield fixer.insertTextBeforeRange(heading.range, "# ");
3664
4024
  const lines = getParsedLines(sourceCode);
3665
- const line = lines.get(markerLocation.line);
3666
- if (diff < 0) {
3667
- const addSpaces = " ".repeat(-diff);
3668
- return fixer.insertTextBeforeRange([line.range[0] + markerLocation.start, line.range[0] + markerLocation.start], addSpaces);
3669
- }
3670
- const itemBefore = line.text.slice(0, markerLocation.start - diff);
3671
- if (itemBefore.includes(" ")) return null;
3672
- const referenceMarkerBefore = lines.get(referenceMarkerLocation.line).text.slice(0, referenceMarkerLocation.start);
3673
- if (referenceMarkerBefore === itemBefore) {
3674
- const removeEndIndex = line.range[0] + markerLocation.start;
4025
+ yield fixer.removeRange(lines.get(parsed.underline.loc.start.line).range);
4026
+ }
4027
+ });
4028
+ } else if (style === "setext") {
4029
+ if (headingKind !== "atx" || node.children.length === 0) return;
4030
+ context.report({
4031
+ node,
4032
+ messageId: "expectedSetext",
4033
+ *fix(fixer) {
4034
+ const parsed = parseATXHeading(sourceCode, node);
4035
+ if (!parsed) return;
4036
+ yield fixer.removeRange([parsed.openingSequence.range[0], parsed.openingSequence.raws.spaceAfter.range[1]]);
4037
+ if (parsed.closingSequence) yield fixer.removeRange([parsed.closingSequence.raws.spaceBefore.range[0], parsed.closingSequence.raws.spaceAfter.range[1]]);
4038
+ const lines = getParsedLines(sourceCode);
4039
+ const underline = "=".repeat(Math.max(getTextWidth(parsed.content.text), 3));
4040
+ const prefix = lines.get(parsed.openingSequence.loc.start.line).text.slice(0, parsed.openingSequence.loc.start.column - 1);
4041
+ const appendingText = `\n${prefix}${underline}`;
4042
+ yield fixer.insertTextAfter(node, appendingText);
4043
+ }
4044
+ });
4045
+ }
4046
+ } };
4047
+ }
4048
+ });
4049
+
4050
+ //#endregion
4051
+ //#region src/rules/level2-heading-style.ts
4052
+ var level2_heading_style_default = createRule("level2-heading-style", {
4053
+ meta: {
4054
+ type: "layout",
4055
+ docs: {
4056
+ description: "enforce consistent style for level 2 headings",
4057
+ categories: [],
4058
+ listCategory: "Stylistic"
4059
+ },
4060
+ fixable: "code",
4061
+ hasSuggestions: false,
4062
+ schema: [{
4063
+ type: "object",
4064
+ properties: {
4065
+ style: { enum: ["atx", "setext"] },
4066
+ allowMultilineSetext: { type: "boolean" }
4067
+ },
4068
+ additionalProperties: false
4069
+ }],
4070
+ messages: {
4071
+ expectedAtx: "Expected ATX style heading (## Heading).",
4072
+ expectedSetext: "Expected Setext style heading (Heading\\n------).",
4073
+ multilineSetextNotAllowed: "Multiline Setext headings are not allowed."
4074
+ }
4075
+ },
4076
+ create(context) {
4077
+ const sourceCode = context.sourceCode;
4078
+ const opt = context.options[0] || {};
4079
+ const style = opt.style ?? "atx";
4080
+ const allowMultilineSetext = opt.allowMultilineSetext;
4081
+ return { heading(node) {
4082
+ if (node.depth !== 2) return;
4083
+ const headingKind = getHeadingKind(sourceCode, node);
4084
+ if (style === "atx") {
4085
+ if (headingKind !== "setext") return;
4086
+ const parsed = parseSetextHeading(sourceCode, node);
4087
+ if (!parsed) return;
4088
+ const isMultiline = parsed.contentLines.length > 1;
4089
+ if (isMultiline) {
4090
+ if (allowMultilineSetext) return;
4091
+ context.report({
4092
+ node,
4093
+ messageId: "multilineSetextNotAllowed"
4094
+ });
4095
+ return;
4096
+ }
4097
+ context.report({
4098
+ node,
4099
+ messageId: "expectedAtx",
4100
+ *fix(fixer) {
4101
+ const heading = parsed.contentLines[0];
4102
+ yield fixer.insertTextBeforeRange(heading.range, "## ");
4103
+ const lines = getParsedLines(sourceCode);
4104
+ yield fixer.removeRange(lines.get(parsed.underline.loc.start.line).range);
4105
+ }
4106
+ });
4107
+ } else if (style === "setext") {
4108
+ if (headingKind !== "atx" || node.children.length === 0) return;
4109
+ context.report({
4110
+ node,
4111
+ messageId: "expectedSetext",
4112
+ *fix(fixer) {
4113
+ const parsed = parseATXHeading(sourceCode, node);
4114
+ if (!parsed) return;
4115
+ yield fixer.removeRange([parsed.openingSequence.range[0], parsed.openingSequence.raws.spaceAfter.range[1]]);
4116
+ if (parsed.closingSequence) yield fixer.removeRange([parsed.closingSequence.raws.spaceBefore.range[0], parsed.closingSequence.raws.spaceAfter.range[1]]);
4117
+ const lines = getParsedLines(sourceCode);
4118
+ const underline = "-".repeat(Math.max(getTextWidth(parsed.content.text), 3));
4119
+ const prefix = lines.get(parsed.openingSequence.loc.start.line).text.slice(0, parsed.openingSequence.loc.start.column - 1);
4120
+ const appendingText = `\n${prefix}${underline}`;
4121
+ yield fixer.insertTextAfter(node, appendingText);
4122
+ }
4123
+ });
4124
+ }
4125
+ } };
4126
+ }
4127
+ });
4128
+
4129
+ //#endregion
4130
+ //#region src/rules/list-marker-alignment.ts
4131
+ const ALIGN_TO_POSITION_NAME = {
4132
+ left: "start",
4133
+ right: "end"
4134
+ };
4135
+ var list_marker_alignment_default = createRule("list-marker-alignment", {
4136
+ meta: {
4137
+ type: "layout",
4138
+ docs: {
4139
+ description: "enforce consistent alignment of list markers",
4140
+ categories: ["recommended"],
4141
+ listCategory: "Stylistic"
4142
+ },
4143
+ fixable: "whitespace",
4144
+ hasSuggestions: false,
4145
+ schema: [{
4146
+ type: "object",
4147
+ properties: { align: { enum: ["left", "right"] } },
4148
+ additionalProperties: false
4149
+ }],
4150
+ messages: { incorrectAlignment: "List marker alignment is inconsistent. Expected {{expected}} characters of indentation, but got {{actual}}." }
4151
+ },
4152
+ create(context) {
4153
+ const sourceCode = context.sourceCode;
4154
+ const alignPositionName = ALIGN_TO_POSITION_NAME[context.options[0]?.align ?? "left"];
4155
+ /**
4156
+ * Get the marker location of a list item
4157
+ */
4158
+ function getMarkerLocation(node) {
4159
+ const start = sourceCode.getLoc(node).start;
4160
+ const startColumnIndex = start.column - 1;
4161
+ const marker = getListItemMarker(sourceCode, node);
4162
+ return {
4163
+ line: start.line,
4164
+ start: startColumnIndex,
4165
+ end: startColumnIndex + marker.raw.length
4166
+ };
4167
+ }
4168
+ /**
4169
+ * Check if list items have consistent alignment
4170
+ */
4171
+ function checkListAlignment(listNode) {
4172
+ const items = listNode.children;
4173
+ if (items.length <= 1) return;
4174
+ const referenceMarkerLocation = getMarkerLocation(items[0]);
4175
+ for (const item of items.slice(1)) {
4176
+ const markerLocation = getMarkerLocation(item);
4177
+ const diff = markerLocation[alignPositionName] - referenceMarkerLocation[alignPositionName];
4178
+ if (diff === 0) continue;
4179
+ context.report({
4180
+ node: item,
4181
+ loc: {
4182
+ start: {
4183
+ line: markerLocation.line,
4184
+ column: markerLocation.start + 1
4185
+ },
4186
+ end: {
4187
+ line: markerLocation.line,
4188
+ column: markerLocation.end + 1
4189
+ }
4190
+ },
4191
+ messageId: "incorrectAlignment",
4192
+ data: {
4193
+ expected: String(markerLocation.start - diff),
4194
+ actual: String(markerLocation.start)
4195
+ },
4196
+ fix(fixer) {
4197
+ const lines = getParsedLines(sourceCode);
4198
+ const line = lines.get(markerLocation.line);
4199
+ if (diff < 0) {
4200
+ const addSpaces = " ".repeat(-diff);
4201
+ return fixer.insertTextBeforeRange([line.range[0] + markerLocation.start, line.range[0] + markerLocation.start], addSpaces);
4202
+ }
4203
+ const itemBefore = line.text.slice(0, markerLocation.start - diff);
4204
+ if (itemBefore.includes(" ")) return null;
4205
+ const referenceMarkerBefore = lines.get(referenceMarkerLocation.line).text.slice(0, referenceMarkerLocation.start);
4206
+ if (referenceMarkerBefore === itemBefore) {
4207
+ const removeEndIndex = line.range[0] + markerLocation.start;
3675
4208
  const removeStartIndex = removeEndIndex - diff;
3676
4209
  return fixer.removeRange([removeStartIndex, removeEndIndex]);
3677
4210
  }
@@ -4146,7 +4679,7 @@ var ordered_list_marker_sequence_default = createRule("ordered-list-marker-seque
4146
4679
  messageId: "inconsistentStart",
4147
4680
  data: {
4148
4681
  expected: new Intl.ListFormat("en-US", { type: "disjunction" }).format(expected.map((n) => `'${n}'`)),
4149
- actual: String(marker.sequence)
4682
+ actual: marker.sequence.raw
4150
4683
  },
4151
4684
  fix: scope.last == null ? (fixer) => {
4152
4685
  const expectedMarker = `1${marker.kind}`;
@@ -4167,7 +4700,7 @@ var ordered_list_marker_sequence_default = createRule("ordered-list-marker-seque
4167
4700
  const marker = getListItemMarker(sourceCode, item);
4168
4701
  if (marker.kind !== "." && marker.kind !== ")") continue;
4169
4702
  const expectedSequence = node.start + i;
4170
- if (marker.sequence !== expectedSequence) {
4703
+ if (marker.sequence.value !== expectedSequence) {
4171
4704
  const expectedMarker = `${expectedSequence}${marker.kind}`;
4172
4705
  context.report({
4173
4706
  node: item,
@@ -4302,6 +4835,174 @@ var ordered_list_marker_start_default = createRule("ordered-list-marker-start",
4302
4835
  }
4303
4836
  });
4304
4837
 
4838
+ //#endregion
4839
+ //#region src/rules/ordered-list-marker-style.ts
4840
+ const MARKER_KINDS = [".", ")"];
4841
+ const MARKERS = MARKER_KINDS.map((kind) => `n${kind}`);
4842
+ /**
4843
+ * Get the other marker kind.
4844
+ */
4845
+ function getOtherMarkerKind(unavailableMarker) {
4846
+ return MARKER_KINDS.find((mark) => unavailableMarker !== mark);
4847
+ }
4848
+ function markerToKind(marker) {
4849
+ if (marker === "n.") return ".";
4850
+ if (marker === "n)") return ")";
4851
+ return marker;
4852
+ }
4853
+ /**
4854
+ * Parse rule options.
4855
+ */
4856
+ function parseOptions(options) {
4857
+ const prefer = markerToKind(options.prefer) || ".";
4858
+ const overrides = (options.overrides ?? []).map((override) => {
4859
+ const preferForOverride = markerToKind(override.prefer) || ".";
4860
+ return {
4861
+ level: override.level,
4862
+ parentMarker: markerToKind(override.parentMarker) ?? "any",
4863
+ prefer: preferForOverride
4864
+ };
4865
+ }).reverse();
4866
+ return { get(level, parentMarker) {
4867
+ for (const o of overrides) if ((o.level == null || o.level === level) && (o.parentMarker === "any" || o.parentMarker === parentMarker)) return { prefer: o.prefer };
4868
+ return { prefer };
4869
+ } };
4870
+ }
4871
+ /**
4872
+ * Check if a item marker is an ordered list marker.
4873
+ */
4874
+ function isOrderedListItemMarker(itemMarker) {
4875
+ return itemMarker.kind === "." || itemMarker.kind === ")";
4876
+ }
4877
+ var ordered_list_marker_style_default = createRule("ordered-list-marker-style", {
4878
+ meta: {
4879
+ type: "layout",
4880
+ docs: {
4881
+ description: "enforce consistent ordered list marker style",
4882
+ categories: [],
4883
+ listCategory: "Stylistic"
4884
+ },
4885
+ fixable: "code",
4886
+ hasSuggestions: false,
4887
+ schema: [{
4888
+ type: "object",
4889
+ properties: {
4890
+ prefer: { enum: MARKERS },
4891
+ overrides: {
4892
+ type: "array",
4893
+ items: {
4894
+ type: "object",
4895
+ properties: {
4896
+ level: {
4897
+ type: "integer",
4898
+ minimum: 1
4899
+ },
4900
+ parentMarker: { enum: [
4901
+ ...MARKERS,
4902
+ "any",
4903
+ "bullet"
4904
+ ] },
4905
+ prefer: { enum: MARKERS }
4906
+ },
4907
+ additionalProperties: false
4908
+ }
4909
+ }
4910
+ },
4911
+ additionalProperties: false
4912
+ }],
4913
+ messages: { unexpected: "Ordered list marker should be '{{sequence}}{{markerKind}}'." }
4914
+ },
4915
+ create(context) {
4916
+ const sourceCode = context.sourceCode;
4917
+ const options = parseOptions(context.options[0] || {});
4918
+ let containerStack = {
4919
+ node: sourceCode.ast,
4920
+ level: 1,
4921
+ upper: null
4922
+ };
4923
+ /**
4924
+ * Check ordered list marker style
4925
+ */
4926
+ function checkOrderedList(node) {
4927
+ let parentMarker;
4928
+ if (containerStack.node.type === "listItem") {
4929
+ const parentMarkerKind = getListItemMarker(sourceCode, containerStack.node).kind;
4930
+ parentMarker = parentMarkerKind === "-" || parentMarkerKind === "*" || parentMarkerKind === "+" ? "bullet" : parentMarkerKind;
4931
+ } else parentMarker = "top";
4932
+ const { prefer } = options.get(containerStack.level, parentMarker);
4933
+ const nodeIndex = containerStack.node.children.indexOf(node);
4934
+ if (nodeIndex === -1) return;
4935
+ const prevNode = nodeIndex > 0 ? containerStack.node.children[nodeIndex - 1] : null;
4936
+ const prevBulletList = prevNode && (prevNode.type === "list" ? prevNode : null);
4937
+ const prevBulletListMarker = prevBulletList && getListItemMarker(sourceCode, prevBulletList);
4938
+ const expectedMarker = prevBulletListMarker?.kind !== prefer ? prefer : getOtherMarkerKind(prefer);
4939
+ const marker = getListItemMarker(sourceCode, node);
4940
+ if (marker.kind === expectedMarker || !isOrderedListItemMarker(marker)) return;
4941
+ const loc = sourceCode.getLoc(node);
4942
+ context.report({
4943
+ node,
4944
+ loc: {
4945
+ start: loc.start,
4946
+ end: {
4947
+ line: loc.start.line,
4948
+ column: loc.start.column + marker.raw.length
4949
+ }
4950
+ },
4951
+ messageId: "unexpected",
4952
+ data: {
4953
+ sequence: marker.sequence.raw,
4954
+ markerKind: expectedMarker
4955
+ },
4956
+ *fix(fixer) {
4957
+ if (prevNode?.type === "list" && prevBulletListMarker && isOrderedListItemMarker(prevBulletListMarker)) yield* fixMarker(prevNode.children[0], prevBulletListMarker.kind);
4958
+ yield* fixMarkers(node, expectedMarker);
4959
+ let prevMarker = expectedMarker;
4960
+ for (let index = nodeIndex + 1; index < containerStack.node.children.length; index++) {
4961
+ const nextNode = containerStack.node.children[index];
4962
+ if (nextNode.type !== "list") break;
4963
+ const nextMarker = getListItemMarker(sourceCode, nextNode);
4964
+ if (nextMarker.kind !== prevMarker) break;
4965
+ const expectedNextMarker = prevMarker === prefer ? getOtherMarkerKind(prefer) : prefer;
4966
+ yield* fixMarkers(nextNode, expectedNextMarker);
4967
+ prevMarker = expectedNextMarker;
4968
+ }
4969
+ /**
4970
+ * Fix ordered list markers
4971
+ */
4972
+ function* fixMarkers(list, replacementMarker) {
4973
+ for (const item of list.children) yield* fixMarker(item, replacementMarker);
4974
+ }
4975
+ /**
4976
+ * Fix ordered list item marker
4977
+ */
4978
+ function* fixMarker(item, replacementMarker) {
4979
+ const range = sourceCode.getRange(item);
4980
+ const itemMarker = getListItemMarker(sourceCode, item);
4981
+ if (!isOrderedListItemMarker(itemMarker)) return;
4982
+ yield fixer.replaceTextRange([range[0] + itemMarker.raw.length - 1, range[0] + itemMarker.raw.length], replacementMarker);
4983
+ }
4984
+ }
4985
+ });
4986
+ }
4987
+ return {
4988
+ list(node) {
4989
+ if (!node.ordered) return;
4990
+ checkOrderedList(node);
4991
+ },
4992
+ "root, blockquote, listItem, footnoteDefinition"(node) {
4993
+ containerStack = {
4994
+ node,
4995
+ level: node.type === "listItem" ? containerStack.level + 1 : 1,
4996
+ upper: containerStack
4997
+ };
4998
+ },
4999
+ "root, blockquote, listItem, footnoteDefinition:exit"() {
5000
+ containerStack = containerStack.upper;
5001
+ }
5002
+ };
5003
+ }
5004
+ });
5005
+
4305
5006
  //#endregion
4306
5007
  //#region src/rules/padding-line-between-blocks.ts
4307
5008
  /**
@@ -5119,6 +5820,244 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
5119
5820
  }
5120
5821
  });
5121
5822
 
5823
+ //#endregion
5824
+ //#region src/rules/setext-heading-underline-length.ts
5825
+ var setext_heading_underline_length_default = createRule("setext-heading-underline-length", {
5826
+ meta: {
5827
+ type: "layout",
5828
+ docs: {
5829
+ description: "enforce setext heading underline length",
5830
+ categories: [],
5831
+ listCategory: "Stylistic"
5832
+ },
5833
+ fixable: "whitespace",
5834
+ schema: [{
5835
+ type: "object",
5836
+ properties: {
5837
+ mode: {
5838
+ type: "string",
5839
+ enum: [
5840
+ "exact",
5841
+ "minimum",
5842
+ "consistent",
5843
+ "consistent-line-length"
5844
+ ]
5845
+ },
5846
+ align: {
5847
+ type: "string",
5848
+ enum: [
5849
+ "any",
5850
+ "exact",
5851
+ "minimum",
5852
+ "length"
5853
+ ]
5854
+ },
5855
+ length: {
5856
+ type: "integer",
5857
+ minimum: 1
5858
+ }
5859
+ },
5860
+ additionalProperties: false
5861
+ }],
5862
+ messages: {
5863
+ exactLength: "Setext heading underline should be exactly the same length as the heading text.",
5864
+ minimumLength: "Setext heading underline should be at least as long as the heading text.",
5865
+ consistentAny: "Setext heading underline should be consistent with other underlines in the document.",
5866
+ consistentExact: "Setext heading underline should be exactly the same length as the longest heading text in the document.",
5867
+ consistentMinimum: "Setext heading underline should be at least as long as the longest heading text in the document.",
5868
+ consistentLength: "Setext heading underline should be {{expectedLength}} characters long for consistency.",
5869
+ consistentLineLengthAny: "Setext heading underline should be consistent in line length with other underlines in the document.",
5870
+ consistentLineLengthExact: "Setext heading underline should be exactly the same line length as the longest heading line in the document.",
5871
+ consistentLineLengthMinimum: "Setext heading underline should be at least as long as the longest heading line in the document.",
5872
+ consistentLineLengthLength: "Setext heading underline should be {{expectedLength}} characters long for line length consistency."
5873
+ }
5874
+ },
5875
+ create(context) {
5876
+ const sourceCode = context.sourceCode;
5877
+ const options = context.options[0] || {};
5878
+ const mode = options.mode || "exact";
5879
+ const parsedSetextHeadings = /* @__PURE__ */ new Map();
5880
+ /**
5881
+ * Get the parsed setext heading for a specific heading.
5882
+ */
5883
+ function getParsedSetextHeading(heading) {
5884
+ const cached = parsedSetextHeadings.get(heading);
5885
+ if (cached) return cached;
5886
+ const underline = parseSetextHeading(sourceCode, heading);
5887
+ if (!underline) return null;
5888
+ parsedSetextHeadings.set(heading, underline);
5889
+ return underline;
5890
+ }
5891
+ /**
5892
+ * Helper function to report errors for both regular and blockquote headings
5893
+ */
5894
+ function reportError(node, messageId, expectedLength) {
5895
+ const parsed = getParsedSetextHeading(node);
5896
+ if (!parsed) return;
5897
+ context.report({
5898
+ node,
5899
+ messageId,
5900
+ loc: parsed.underline.loc,
5901
+ data: { expectedLength: String(expectedLength) },
5902
+ fix(fixer) {
5903
+ const newUnderline = parsed.underline.marker.repeat(expectedLength);
5904
+ return fixer.replaceTextRange(parsed.underline.range, newUnderline);
5905
+ }
5906
+ });
5907
+ }
5908
+ if (mode === "exact" || mode === "minimum") return { heading(node) {
5909
+ const parsed = getParsedSetextHeading(node);
5910
+ if (!parsed) return;
5911
+ const expectedLength = getMaxHeaderTextWidth(parsed);
5912
+ if (expectedLength < 1) return;
5913
+ if (mode === "exact") {
5914
+ if (parsed.underline.text.length !== expectedLength) reportError(node, "exactLength", expectedLength);
5915
+ } else if (mode === "minimum") {
5916
+ if (parsed.underline.text.length < expectedLength) reportError(node, "minimumLength", expectedLength);
5917
+ }
5918
+ } };
5919
+ if (mode === "consistent") {
5920
+ const align = options.align || "exact";
5921
+ const fixedLength = options.length || 0;
5922
+ const setextHeadings = [];
5923
+ return {
5924
+ heading(node) {
5925
+ if (getHeadingKind(sourceCode, node) !== "setext") return;
5926
+ setextHeadings.push(node);
5927
+ },
5928
+ "root:exit"() {
5929
+ if (setextHeadings.length === 0) return;
5930
+ let expectedLength = 0;
5931
+ if (align === "any") {
5932
+ if (setextHeadings.length < 2) return;
5933
+ for (const node of setextHeadings) {
5934
+ const parsed = getParsedSetextHeading(node);
5935
+ if (!parsed) continue;
5936
+ expectedLength = parsed.underline.text.length;
5937
+ break;
5938
+ }
5939
+ } else if (align === "exact") for (const node of setextHeadings) {
5940
+ const parsed = getParsedSetextHeading(node);
5941
+ if (!parsed) continue;
5942
+ expectedLength = Math.max(expectedLength, getMaxHeaderTextWidth(parsed));
5943
+ }
5944
+ else if (align === "minimum") {
5945
+ let maxTextWidth = 0;
5946
+ for (const node of setextHeadings) {
5947
+ const parsed = getParsedSetextHeading(node);
5948
+ if (!parsed) continue;
5949
+ maxTextWidth = Math.max(maxTextWidth, getMaxHeaderTextWidth(parsed));
5950
+ expectedLength = Math.max(expectedLength, parsed.underline.text.length);
5951
+ }
5952
+ if (expectedLength < maxTextWidth) expectedLength = maxTextWidth;
5953
+ else for (const node of setextHeadings) {
5954
+ const parsed = getParsedSetextHeading(node);
5955
+ if (!parsed) continue;
5956
+ if (maxTextWidth <= parsed.underline.text.length) expectedLength = Math.min(expectedLength, parsed.underline.text.length);
5957
+ }
5958
+ } else if (align === "length") expectedLength = fixedLength;
5959
+ else return;
5960
+ if (!expectedLength || expectedLength < 1) return;
5961
+ for (const node of setextHeadings) {
5962
+ const parsed = getParsedSetextHeading(node);
5963
+ if (!parsed) continue;
5964
+ if (parsed.underline.text.length === expectedLength) continue;
5965
+ if (align === "any") reportError(node, "consistentAny", expectedLength);
5966
+ else if (align === "exact") reportError(node, "consistentExact", expectedLength);
5967
+ else if (align === "minimum") reportError(node, "consistentMinimum", expectedLength);
5968
+ else if (align === "length") reportError(node, "consistentLength", expectedLength);
5969
+ }
5970
+ }
5971
+ };
5972
+ }
5973
+ if (mode === "consistent-line-length") {
5974
+ const align = options.align || "exact";
5975
+ const fixedLength = options.length || 0;
5976
+ const setextHeadings = [];
5977
+ return {
5978
+ heading(node) {
5979
+ if (getHeadingKind(sourceCode, node) !== "setext") return;
5980
+ setextHeadings.push(node);
5981
+ },
5982
+ "root:exit"() {
5983
+ if (setextHeadings.length === 0) return;
5984
+ let minimumRequiredLineLength = 1;
5985
+ for (const node of setextHeadings) {
5986
+ const parsed = getParsedSetextHeading(node);
5987
+ if (!parsed) continue;
5988
+ minimumRequiredLineLength = Math.max(minimumRequiredLineLength, parsed.underline.raws.prefix.length + parsed.underline.raws.spaceBefore.length + 1);
5989
+ }
5990
+ let expectedLineLength = minimumRequiredLineLength;
5991
+ if (align === "any") {
5992
+ if (setextHeadings.length < 2) return;
5993
+ for (const node of setextHeadings) {
5994
+ const parsed = getParsedSetextHeading(node);
5995
+ if (!parsed) continue;
5996
+ expectedLineLength = Math.max(parsed.underline.loc.end.column - 1, minimumRequiredLineLength);
5997
+ break;
5998
+ }
5999
+ } else if (align === "exact") for (const node of setextHeadings) {
6000
+ const parsed = getParsedSetextHeading(node);
6001
+ if (!parsed) continue;
6002
+ expectedLineLength = Math.max(expectedLineLength, getMaxHeaderLineWidth(parsed));
6003
+ }
6004
+ else if (align === "minimum") {
6005
+ let maxLineWidth = 0;
6006
+ for (const node of setextHeadings) {
6007
+ const parsed = getParsedSetextHeading(node);
6008
+ if (!parsed) continue;
6009
+ maxLineWidth = Math.max(maxLineWidth, getMaxHeaderLineWidth(parsed));
6010
+ expectedLineLength = Math.max(expectedLineLength, parsed.underline.loc.end.column - 1);
6011
+ }
6012
+ if (expectedLineLength < maxLineWidth) expectedLineLength = maxLineWidth;
6013
+ else for (const node of setextHeadings) {
6014
+ const parsed = getParsedSetextHeading(node);
6015
+ if (!parsed) continue;
6016
+ if (maxLineWidth <= parsed.underline.loc.end.column - 1) expectedLineLength = Math.min(expectedLineLength, parsed.underline.loc.end.column - 1);
6017
+ }
6018
+ } else if (align === "length") expectedLineLength = Math.max(fixedLength, minimumRequiredLineLength);
6019
+ else return;
6020
+ if (!expectedLineLength || expectedLineLength < 1) return;
6021
+ for (const node of setextHeadings) {
6022
+ const parsed = getParsedSetextHeading(node);
6023
+ if (!parsed) continue;
6024
+ const expectedLength = expectedLineLength - parsed.underline.raws.prefix.length - parsed.underline.raws.spaceBefore.length;
6025
+ if (parsed.underline.text.length === expectedLength) continue;
6026
+ if (align === "any") reportError(node, "consistentLineLengthAny", expectedLength);
6027
+ else if (align === "exact") reportError(node, "consistentLineLengthExact", expectedLength);
6028
+ else if (align === "minimum") reportError(node, "consistentLineLengthMinimum", expectedLength);
6029
+ else if (align === "length") reportError(node, "consistentLineLengthLength", expectedLength);
6030
+ }
6031
+ }
6032
+ };
6033
+ }
6034
+ return {};
6035
+ /**
6036
+ * Get the maximum width of header lines.
6037
+ */
6038
+ function getMaxHeaderTextWidth(parsed) {
6039
+ let maxWidth = 0;
6040
+ for (const contentLine of parsed.contentLines) {
6041
+ const lineWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore + contentLine.text);
6042
+ const prefixWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore);
6043
+ maxWidth = Math.max(maxWidth, lineWidth - prefixWidth);
6044
+ }
6045
+ return maxWidth;
6046
+ }
6047
+ /**
6048
+ * Get the maximum width of header lines.
6049
+ */
6050
+ function getMaxHeaderLineWidth(parsed) {
6051
+ let maxLineWidth = 0;
6052
+ for (const contentLine of parsed.contentLines) {
6053
+ const lineWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore + contentLine.text);
6054
+ maxLineWidth = Math.max(maxLineWidth, lineWidth);
6055
+ }
6056
+ return maxLineWidth;
6057
+ }
6058
+ }
6059
+ });
6060
+
5122
6061
  //#endregion
5123
6062
  //#region src/rules/sort-definitions.ts
5124
6063
  var sort_definitions_default = createRule("sort-definitions", {
@@ -5536,17 +6475,231 @@ var table_header_casing_default = createRule("table-header-casing", {
5536
6475
  }
5537
6476
  });
5538
6477
 
6478
+ //#endregion
6479
+ //#region src/rules/thematic-break-character-style.ts
6480
+ var thematic_break_character_style_default = createRule("thematic-break-character-style", {
6481
+ meta: {
6482
+ type: "layout",
6483
+ docs: {
6484
+ description: "enforce consistent character style for thematic breaks (horizontal rules) in Markdown.",
6485
+ categories: [],
6486
+ listCategory: "Stylistic"
6487
+ },
6488
+ fixable: "code",
6489
+ hasSuggestions: false,
6490
+ schema: [{
6491
+ type: "object",
6492
+ properties: { style: {
6493
+ type: "string",
6494
+ enum: [
6495
+ "-",
6496
+ "*",
6497
+ "_"
6498
+ ]
6499
+ } },
6500
+ additionalProperties: false
6501
+ }],
6502
+ messages: { unexpected: "Thematic break should use '{{expected}}' but found '{{actual}}'." }
6503
+ },
6504
+ create(context) {
6505
+ const option = context.options[0];
6506
+ const style = option?.style || "-";
6507
+ return { thematicBreak(node) {
6508
+ const marker = getThematicBreakMarker(context.sourceCode, node);
6509
+ if (marker.kind !== style) context.report({
6510
+ node,
6511
+ messageId: "unexpected",
6512
+ data: {
6513
+ expected: style,
6514
+ actual: marker.kind
6515
+ },
6516
+ fix(fixer) {
6517
+ const range = context.sourceCode.getRange(node);
6518
+ const text = context.sourceCode.getText(node);
6519
+ const rep = text.replaceAll(marker.kind, style);
6520
+ return fixer.replaceTextRange(range, rep);
6521
+ }
6522
+ });
6523
+ } };
6524
+ }
6525
+ });
6526
+
6527
+ //#endregion
6528
+ //#region src/utils/thematic-break.ts
6529
+ /**
6530
+ * Check if the pattern is valid within the thematic break string.
6531
+ */
6532
+ function isValidThematicBreakPattern(pattern, text) {
6533
+ for (let i = 0; i < text.length; i += pattern.length) {
6534
+ const subSequence = text.slice(i, i + pattern.length);
6535
+ if (subSequence === pattern) continue;
6536
+ if (subSequence.length < pattern.length && pattern.startsWith(subSequence)) continue;
6537
+ return false;
6538
+ }
6539
+ return true;
6540
+ }
6541
+ /**
6542
+ * Create a thematic break string from a pattern and length.
6543
+ */
6544
+ function createThematicBreakFromPattern(pattern, length) {
6545
+ const mark = pattern[0];
6546
+ let candidate = pattern.repeat(Math.floor(length / pattern.length));
6547
+ if (candidate.length < length) candidate += pattern.slice(0, length - candidate.length);
6548
+ candidate = candidate.trim();
6549
+ if (candidate.length !== length) return null;
6550
+ let markCount = 0;
6551
+ for (const c of candidate) {
6552
+ if (c !== mark) continue;
6553
+ markCount++;
6554
+ if (markCount >= 3) return candidate;
6555
+ }
6556
+ return null;
6557
+ }
6558
+
6559
+ //#endregion
6560
+ //#region src/rules/thematic-break-length.ts
6561
+ var thematic_break_length_default = createRule("thematic-break-length", {
6562
+ meta: {
6563
+ type: "layout",
6564
+ docs: {
6565
+ description: "enforce consistent length for thematic breaks (horizontal rules) in Markdown.",
6566
+ categories: [],
6567
+ listCategory: "Stylistic"
6568
+ },
6569
+ fixable: "code",
6570
+ hasSuggestions: false,
6571
+ schema: [{
6572
+ type: "object",
6573
+ properties: { length: {
6574
+ type: "integer",
6575
+ minimum: 3
6576
+ } },
6577
+ additionalProperties: false
6578
+ }],
6579
+ messages: { unexpected: "Thematic break should be {{expected}} characters, but found {{actual}}." }
6580
+ },
6581
+ create(context) {
6582
+ const option = context.options[0] || {};
6583
+ const expectedLength = option.length ?? 3;
6584
+ const sourceCode = context.sourceCode;
6585
+ return { thematicBreak(node) {
6586
+ const marker = getThematicBreakMarker(sourceCode, node);
6587
+ if (marker.text.length === expectedLength) return;
6588
+ context.report({
6589
+ node,
6590
+ messageId: "unexpected",
6591
+ data: {
6592
+ expected: String(expectedLength),
6593
+ actual: String(marker.text.length)
6594
+ },
6595
+ fix(fixer) {
6596
+ const sequence = replacementSequence(marker);
6597
+ if (!sequence) return null;
6598
+ return fixer.replaceText(node, sequence);
6599
+ }
6600
+ });
6601
+ } };
6602
+ /**
6603
+ * Replace the sequence in the thematic break marker with the expected length.
6604
+ */
6605
+ function replacementSequence(marker) {
6606
+ if (marker.hasSpaces) {
6607
+ const pattern = inferSequencePattern(marker.text);
6608
+ if (pattern) return createThematicBreakFromPattern(pattern, expectedLength);
6609
+ return null;
6610
+ }
6611
+ return marker.kind.repeat(expectedLength);
6612
+ }
6613
+ /**
6614
+ * Infer sequence pattern from the original string.
6615
+ */
6616
+ function inferSequencePattern(original) {
6617
+ for (let length = 2; length < original.length; length++) {
6618
+ const pattern = original.slice(0, length);
6619
+ if (isValidThematicBreakPattern(pattern, original)) return pattern;
6620
+ }
6621
+ return null;
6622
+ }
6623
+ }
6624
+ });
6625
+
6626
+ //#endregion
6627
+ //#region src/rules/thematic-break-sequence-pattern.ts
6628
+ var thematic_break_sequence_pattern_default = createRule("thematic-break-sequence-pattern", {
6629
+ meta: {
6630
+ type: "layout",
6631
+ docs: {
6632
+ description: "enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown.",
6633
+ categories: [],
6634
+ listCategory: "Stylistic"
6635
+ },
6636
+ fixable: "code",
6637
+ hasSuggestions: false,
6638
+ schema: [{
6639
+ type: "object",
6640
+ properties: { pattern: { anyOf: [
6641
+ {
6642
+ type: "string",
6643
+ minLength: 1,
6644
+ pattern: "^\\-[ \\-]*$"
6645
+ },
6646
+ {
6647
+ type: "string",
6648
+ minLength: 1,
6649
+ pattern: "^\\*[ *]*$"
6650
+ },
6651
+ {
6652
+ type: "string",
6653
+ minLength: 1,
6654
+ pattern: "^_[ _]*$"
6655
+ }
6656
+ ] } },
6657
+ required: ["pattern"],
6658
+ additionalProperties: false
6659
+ }],
6660
+ messages: { inconsistentPattern: "Thematic break does not match the preferred repeating pattern '{{pattern}}'." }
6661
+ },
6662
+ create(context) {
6663
+ const option = context.options[0] || {};
6664
+ const pattern = option.pattern ?? "-";
6665
+ const sourceCode = context.sourceCode;
6666
+ const patterns = {
6667
+ "-": pattern.replaceAll(/[*_]/gu, "-"),
6668
+ "*": pattern.replaceAll(/[-_]/gu, "*"),
6669
+ _: pattern.replaceAll(/[*-]/gu, "_")
6670
+ };
6671
+ return { thematicBreak(node) {
6672
+ const marker = getThematicBreakMarker(sourceCode, node);
6673
+ const patternForKind = patterns[marker.kind];
6674
+ if (isValidThematicBreakPattern(patternForKind, marker.text)) return;
6675
+ context.report({
6676
+ node,
6677
+ messageId: "inconsistentPattern",
6678
+ data: { pattern },
6679
+ fix(fixer) {
6680
+ const replacement = createThematicBreakFromPattern(patternForKind, marker.text.length);
6681
+ if (!replacement) return null;
6682
+ return fixer.replaceText(node, replacement);
6683
+ }
6684
+ });
6685
+ } };
6686
+ }
6687
+ });
6688
+
5539
6689
  //#endregion
5540
6690
  //#region src/utils/rules.ts
5541
6691
  const rules$1 = [
5542
- atx_headings_closing_sequence_length_default,
5543
- atx_headings_closing_sequence_default,
6692
+ atx_heading_closing_sequence_length_default,
6693
+ atx_heading_closing_sequence_default,
5544
6694
  blockquote_marker_alignment_default,
6695
+ bullet_list_marker_style_default,
5545
6696
  canonical_code_block_language_default,
5546
6697
  definitions_last_default,
5547
6698
  emoji_notation_default,
5548
6699
  hard_linebreak_style_default,
5549
6700
  heading_casing_default,
6701
+ level1_heading_style_default,
6702
+ level2_heading_style_default,
5550
6703
  list_marker_alignment_default,
5551
6704
  no_laziness_blockquotes_default,
5552
6705
  no_multiple_empty_lines_default,
@@ -5554,14 +6707,19 @@ const rules$1 = [
5554
6707
  no_trailing_spaces_default,
5555
6708
  ordered_list_marker_sequence_default,
5556
6709
  ordered_list_marker_start_default,
6710
+ ordered_list_marker_style_default,
5557
6711
  padding_line_between_blocks_default,
5558
6712
  prefer_autolinks_default,
5559
6713
  prefer_fenced_code_blocks_default,
5560
6714
  prefer_inline_code_words_default,
5561
6715
  prefer_link_reference_definitions_default,
5562
6716
  prefer_linked_words_default,
6717
+ setext_heading_underline_length_default,
5563
6718
  sort_definitions_default,
5564
- table_header_casing_default
6719
+ table_header_casing_default,
6720
+ thematic_break_character_style_default,
6721
+ thematic_break_length_default,
6722
+ thematic_break_sequence_pattern_default
5565
6723
  ];
5566
6724
 
5567
6725
  //#endregion
@@ -5604,7 +6762,7 @@ __export(meta_exports, {
5604
6762
  version: () => version
5605
6763
  });
5606
6764
  const name = "eslint-plugin-markdown-preferences";
5607
- const version = "0.16.0";
6765
+ const version = "0.18.0";
5608
6766
 
5609
6767
  //#endregion
5610
6768
  //#region src/index.ts