eslint-plugin-markdown-preferences 0.16.0 → 0.17.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
@@ -129,7 +129,11 @@ The rules with the following star ⭐ are included in the configs.
129
129
  | [markdown-preferences/prefer-autolinks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html) | enforce the use of autolinks for URLs | 🔧 | ⭐ |
130
130
  | [markdown-preferences/prefer-fenced-code-blocks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-fenced-code-blocks.html) | enforce the use of fenced code blocks over indented code blocks | 🔧 | ⭐ |
131
131
  | [markdown-preferences/prefer-link-reference-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html) | enforce using link reference definitions instead of inline links | 🔧 | |
132
+ | [markdown-preferences/setext-heading-underline-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/setext-heading-underline-length.html) | enforce setext heading underline length | 🔧 | |
132
133
  | [markdown-preferences/sort-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html) | enforce a specific order for link definitions and footnote definitions | 🔧 | |
134
+ | [markdown-preferences/thematic-break-character-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-character-style.html) | enforce consistent character style for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
135
+ | [markdown-preferences/thematic-break-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-length.html) | enforce consistent length for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
136
+ | [markdown-preferences/thematic-break-sequence-pattern](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-sequence-pattern.html) | enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
133
137
 
134
138
  <!-- prettier-ignore-end -->
135
139
 
package/lib/index.d.ts CHANGED
@@ -115,6 +115,11 @@ interface RuleOptions {
115
115
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
116
116
  */
117
117
  'markdown-preferences/prefer-linked-words'?: Linter.RuleEntry<MarkdownPreferencesPreferLinkedWords>;
118
+ /**
119
+ * enforce setext heading underline length
120
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/setext-heading-underline-length.html
121
+ */
122
+ 'markdown-preferences/setext-heading-underline-length'?: Linter.RuleEntry<MarkdownPreferencesSetextHeadingUnderlineLength>;
118
123
  /**
119
124
  * enforce a specific order for link definitions and footnote definitions
120
125
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
@@ -125,6 +130,21 @@ interface RuleOptions {
125
130
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html
126
131
  */
127
132
  'markdown-preferences/table-header-casing'?: Linter.RuleEntry<MarkdownPreferencesTableHeaderCasing>;
133
+ /**
134
+ * enforce consistent character style for thematic breaks (horizontal rules) in Markdown.
135
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-character-style.html
136
+ */
137
+ 'markdown-preferences/thematic-break-character-style'?: Linter.RuleEntry<MarkdownPreferencesThematicBreakCharacterStyle>;
138
+ /**
139
+ * enforce consistent length for thematic breaks (horizontal rules) in Markdown.
140
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-length.html
141
+ */
142
+ 'markdown-preferences/thematic-break-length'?: Linter.RuleEntry<MarkdownPreferencesThematicBreakLength>;
143
+ /**
144
+ * enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown.
145
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-sequence-pattern.html
146
+ */
147
+ 'markdown-preferences/thematic-break-sequence-pattern'?: Linter.RuleEntry<MarkdownPreferencesThematicBreakSequencePattern>;
128
148
  }
129
149
  type MarkdownPreferencesAtxHeadingsClosingSequence = [] | [{
130
150
  closingSequence?: ("always" | "never");
@@ -205,6 +225,11 @@ type MarkdownPreferencesPreferLinkedWords = [] | [{
205
225
  }[];
206
226
  [k: string]: unknown | undefined;
207
227
  }];
228
+ type MarkdownPreferencesSetextHeadingUnderlineLength = [] | [{
229
+ mode?: ("exact" | "minimum" | "consistent" | "consistent-line-length");
230
+ align?: ("any" | "exact" | "minimum" | "length");
231
+ length?: number;
232
+ }];
208
233
  type MarkdownPreferencesSortDefinitions = [] | [{
209
234
  order?: (string | [string, ...(string)[]] | {
210
235
  match: (string | [string, ...(string)[]]);
@@ -218,6 +243,15 @@ type MarkdownPreferencesTableHeaderCasing = [] | [{
218
243
  ignorePatterns?: string[];
219
244
  minorWords?: string[];
220
245
  }];
246
+ type MarkdownPreferencesThematicBreakCharacterStyle = [] | [{
247
+ style?: ("-" | "*" | "_");
248
+ }];
249
+ type MarkdownPreferencesThematicBreakLength = [] | [{
250
+ length?: number;
251
+ }];
252
+ type MarkdownPreferencesThematicBreakSequencePattern = [] | [{
253
+ pattern: (string | string | string);
254
+ }];
221
255
  declare namespace recommended_d_exports {
222
256
  export { files, language, languageOptions, name$1 as name, plugins, rules$1 as rules };
223
257
  }
@@ -236,7 +270,7 @@ declare namespace meta_d_exports {
236
270
  export { name, version };
237
271
  }
238
272
  declare const name: "eslint-plugin-markdown-preferences";
239
- declare const version: "0.16.0";
273
+ declare const version: "0.17.0";
240
274
  //#endregion
241
275
  //#region src/index.d.ts
242
276
  declare const configs: {
package/lib/index.js CHANGED
@@ -86,10 +86,11 @@ function getListItemMarker(sourceCode, node) {
86
86
  * Get the marker for a thematic break.
87
87
  */
88
88
  function getThematicBreakMarker(sourceCode, node) {
89
- const text = sourceCode.getText(node);
89
+ const text = sourceCode.getText(node).trimEnd();
90
90
  return {
91
- kind: text.startsWith("-") ? "-" : "*",
92
- hasSpaces: /\s/u.test(text)
91
+ kind: text.startsWith("-") ? "-" : text.startsWith("*") ? "*" : "_",
92
+ hasSpaces: /\s/u.test(text),
93
+ text
93
94
  };
94
95
  }
95
96
  /**
@@ -255,8 +256,22 @@ function getParsedLines(sourceCode) {
255
256
  }
256
257
 
257
258
  //#endregion
258
- //#region src/rules/atx-headings-closing-sequence-length.ts
259
+ //#region src/utils/get-text-width.ts
259
260
  let segmenter;
261
+ /**
262
+ * Get the width of a text string.
263
+ */
264
+ function getTextWidth(text) {
265
+ if (!text.includes(" ")) return stringWidth(text);
266
+ if (!segmenter) segmenter = new Intl.Segmenter("en");
267
+ let width = 0;
268
+ for (const { segment: c } of segmenter.segment(text)) if (c === " ") width += 4 - width % 4;
269
+ else width += stringWidth(c);
270
+ return width;
271
+ }
272
+
273
+ //#endregion
274
+ //#region src/rules/atx-headings-closing-sequence-length.ts
260
275
  var atx_headings_closing_sequence_length_default = createRule("atx-headings-closing-sequence-length", {
261
276
  meta: {
262
277
  type: "layout",
@@ -397,17 +412,6 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
397
412
  }
398
413
  }
399
414
  });
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
415
 
412
416
  //#endregion
413
417
  //#region src/rules/atx-headings-closing-sequence.ts
@@ -5119,6 +5123,360 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
5119
5123
  }
5120
5124
  });
5121
5125
 
5126
+ //#endregion
5127
+ //#region src/utils/setext-heading.ts
5128
+ /**
5129
+ * Parse the setext heading.
5130
+ */
5131
+ function parseSetextHeading(sourceCode, node) {
5132
+ if (getHeadingKind(sourceCode, node) !== "setext") return null;
5133
+ const lines = getParsedLines(sourceCode);
5134
+ const contentLines = [];
5135
+ const nodeLoc = sourceCode.getLoc(node);
5136
+ for (let lineNumber = nodeLoc.start.line; lineNumber < nodeLoc.end.line; lineNumber++) {
5137
+ const content = parseContent(lines.get(lineNumber));
5138
+ contentLines.push(content);
5139
+ }
5140
+ const underline = parseUnderline(lines.get(nodeLoc.end.line));
5141
+ if (!underline) return null;
5142
+ return {
5143
+ contentLines,
5144
+ underline
5145
+ };
5146
+ }
5147
+ /**
5148
+ * Parse the content line of a setext heading.
5149
+ */
5150
+ function parseContent(line) {
5151
+ let prefix = "";
5152
+ let spaceBefore = "";
5153
+ let suffix = "";
5154
+ for (let index = 0; index < line.text.length; index++) {
5155
+ const c = line.text[index];
5156
+ if (!c.trim()) {
5157
+ spaceBefore += c;
5158
+ continue;
5159
+ }
5160
+ if (c === ">" && spaceBefore.length < 4) {
5161
+ prefix += spaceBefore + c;
5162
+ spaceBefore = "";
5163
+ continue;
5164
+ }
5165
+ suffix = line.text.slice(index);
5166
+ break;
5167
+ }
5168
+ const content = suffix.trimEnd();
5169
+ const spaceAfter = suffix.slice(content.length);
5170
+ return {
5171
+ text: content,
5172
+ range: [line.range[0] + prefix.length + spaceBefore.length, line.range[1] - line.linebreak.length - spaceAfter.length],
5173
+ loc: {
5174
+ start: {
5175
+ line: line.line,
5176
+ column: prefix.length + spaceBefore.length + 1
5177
+ },
5178
+ end: {
5179
+ line: line.line,
5180
+ column: prefix.length + spaceBefore.length + content.length + 1
5181
+ }
5182
+ },
5183
+ raws: {
5184
+ prefix,
5185
+ spaceBefore,
5186
+ spaceAfter
5187
+ }
5188
+ };
5189
+ }
5190
+ /**
5191
+ * Parse the underline of a setext heading.
5192
+ */
5193
+ function parseUnderline(line) {
5194
+ let marker = null;
5195
+ let underlineText = "";
5196
+ let prefix = "";
5197
+ let spaceBefore = "";
5198
+ let spaceAfter = "";
5199
+ for (let index = line.text.length - 1; index >= 0; index--) {
5200
+ const c = line.text[index];
5201
+ if (!marker) {
5202
+ if (c === "=" || c === "-") {
5203
+ underlineText = c + underlineText;
5204
+ marker = c;
5205
+ } else if (!c.trim()) spaceAfter = c + spaceAfter;
5206
+ else return null;
5207
+ continue;
5208
+ }
5209
+ if (c === marker) {
5210
+ underlineText = c + spaceBefore + underlineText;
5211
+ spaceBefore = "";
5212
+ } else if (!c.trim()) spaceBefore = c + spaceBefore;
5213
+ else {
5214
+ prefix = line.text.slice(0, index + 1);
5215
+ break;
5216
+ }
5217
+ }
5218
+ if (!marker) return null;
5219
+ const underlineLoc = {
5220
+ start: {
5221
+ line: line.line,
5222
+ column: prefix.length + spaceBefore.length + 1
5223
+ },
5224
+ end: {
5225
+ line: line.line,
5226
+ column: prefix.length + spaceBefore.length + underlineText.length + 1
5227
+ }
5228
+ };
5229
+ return {
5230
+ text: underlineText,
5231
+ range: [line.range[0] + prefix.length + spaceBefore.length, line.range[1] - line.linebreak.length - spaceAfter.length],
5232
+ loc: underlineLoc,
5233
+ marker,
5234
+ raws: {
5235
+ prefix,
5236
+ spaceBefore,
5237
+ spaceAfter
5238
+ }
5239
+ };
5240
+ }
5241
+
5242
+ //#endregion
5243
+ //#region src/rules/setext-heading-underline-length.ts
5244
+ var setext_heading_underline_length_default = createRule("setext-heading-underline-length", {
5245
+ meta: {
5246
+ type: "layout",
5247
+ docs: {
5248
+ description: "enforce setext heading underline length",
5249
+ categories: [],
5250
+ listCategory: "Stylistic"
5251
+ },
5252
+ fixable: "whitespace",
5253
+ schema: [{
5254
+ type: "object",
5255
+ properties: {
5256
+ mode: {
5257
+ type: "string",
5258
+ enum: [
5259
+ "exact",
5260
+ "minimum",
5261
+ "consistent",
5262
+ "consistent-line-length"
5263
+ ]
5264
+ },
5265
+ align: {
5266
+ type: "string",
5267
+ enum: [
5268
+ "any",
5269
+ "exact",
5270
+ "minimum",
5271
+ "length"
5272
+ ]
5273
+ },
5274
+ length: {
5275
+ type: "integer",
5276
+ minimum: 1
5277
+ }
5278
+ },
5279
+ additionalProperties: false
5280
+ }],
5281
+ messages: {
5282
+ exactLength: "Setext heading underline should be exactly the same length as the heading text.",
5283
+ minimumLength: "Setext heading underline should be at least as long as the heading text.",
5284
+ consistentAny: "Setext heading underline should be consistent with other underlines in the document.",
5285
+ consistentExact: "Setext heading underline should be exactly the same length as the longest heading text in the document.",
5286
+ consistentMinimum: "Setext heading underline should be at least as long as the longest heading text in the document.",
5287
+ consistentLength: "Setext heading underline should be {{expectedLength}} characters long for consistency.",
5288
+ consistentLineLengthAny: "Setext heading underline should be consistent in line length with other underlines in the document.",
5289
+ consistentLineLengthExact: "Setext heading underline should be exactly the same line length as the longest heading line in the document.",
5290
+ consistentLineLengthMinimum: "Setext heading underline should be at least as long as the longest heading line in the document.",
5291
+ consistentLineLengthLength: "Setext heading underline should be {{expectedLength}} characters long for line length consistency."
5292
+ }
5293
+ },
5294
+ create(context) {
5295
+ const sourceCode = context.sourceCode;
5296
+ const options = context.options[0] || {};
5297
+ const mode = options.mode || "exact";
5298
+ const parsedSetextHeadings = /* @__PURE__ */ new Map();
5299
+ /**
5300
+ * Get the parsed setext heading for a specific heading.
5301
+ */
5302
+ function getParsedSetextHeading(heading) {
5303
+ const cached = parsedSetextHeadings.get(heading);
5304
+ if (cached) return cached;
5305
+ const underline = parseSetextHeading(sourceCode, heading);
5306
+ if (!underline) return null;
5307
+ parsedSetextHeadings.set(heading, underline);
5308
+ return underline;
5309
+ }
5310
+ /**
5311
+ * Helper function to report errors for both regular and blockquote headings
5312
+ */
5313
+ function reportError(node, messageId, expectedLength) {
5314
+ const parsed = getParsedSetextHeading(node);
5315
+ if (!parsed) return;
5316
+ context.report({
5317
+ node,
5318
+ messageId,
5319
+ loc: parsed.underline.loc,
5320
+ data: { expectedLength: String(expectedLength) },
5321
+ fix(fixer) {
5322
+ const newUnderline = parsed.underline.marker.repeat(expectedLength);
5323
+ return fixer.replaceTextRange(parsed.underline.range, newUnderline);
5324
+ }
5325
+ });
5326
+ }
5327
+ if (mode === "exact" || mode === "minimum") return { heading(node) {
5328
+ const parsed = getParsedSetextHeading(node);
5329
+ if (!parsed) return;
5330
+ const expectedLength = getMaxHeaderTextWidth(parsed);
5331
+ if (expectedLength < 1) return;
5332
+ if (mode === "exact") {
5333
+ if (parsed.underline.text.length !== expectedLength) reportError(node, "exactLength", expectedLength);
5334
+ } else if (mode === "minimum") {
5335
+ if (parsed.underline.text.length < expectedLength) reportError(node, "minimumLength", expectedLength);
5336
+ }
5337
+ } };
5338
+ if (mode === "consistent") {
5339
+ const align = options.align || "exact";
5340
+ const fixedLength = options.length || 0;
5341
+ const setextHeadings = [];
5342
+ return {
5343
+ heading(node) {
5344
+ if (getHeadingKind(sourceCode, node) !== "setext") return;
5345
+ setextHeadings.push(node);
5346
+ },
5347
+ "root:exit"() {
5348
+ if (setextHeadings.length === 0) return;
5349
+ let expectedLength = 0;
5350
+ if (align === "any") {
5351
+ if (setextHeadings.length < 2) return;
5352
+ for (const node of setextHeadings) {
5353
+ const parsed = getParsedSetextHeading(node);
5354
+ if (!parsed) continue;
5355
+ expectedLength = parsed.underline.text.length;
5356
+ break;
5357
+ }
5358
+ } else if (align === "exact") for (const node of setextHeadings) {
5359
+ const parsed = getParsedSetextHeading(node);
5360
+ if (!parsed) continue;
5361
+ expectedLength = Math.max(expectedLength, getMaxHeaderTextWidth(parsed));
5362
+ }
5363
+ else if (align === "minimum") {
5364
+ let maxTextWidth = 0;
5365
+ for (const node of setextHeadings) {
5366
+ const parsed = getParsedSetextHeading(node);
5367
+ if (!parsed) continue;
5368
+ maxTextWidth = Math.max(maxTextWidth, getMaxHeaderTextWidth(parsed));
5369
+ expectedLength = Math.max(expectedLength, parsed.underline.text.length);
5370
+ }
5371
+ if (expectedLength < maxTextWidth) expectedLength = maxTextWidth;
5372
+ else for (const node of setextHeadings) {
5373
+ const parsed = getParsedSetextHeading(node);
5374
+ if (!parsed) continue;
5375
+ if (maxTextWidth <= parsed.underline.text.length) expectedLength = Math.min(expectedLength, parsed.underline.text.length);
5376
+ }
5377
+ } else if (align === "length") expectedLength = fixedLength;
5378
+ else return;
5379
+ if (!expectedLength || expectedLength < 1) return;
5380
+ for (const node of setextHeadings) {
5381
+ const parsed = getParsedSetextHeading(node);
5382
+ if (!parsed) continue;
5383
+ if (parsed.underline.text.length === expectedLength) continue;
5384
+ if (align === "any") reportError(node, "consistentAny", expectedLength);
5385
+ else if (align === "exact") reportError(node, "consistentExact", expectedLength);
5386
+ else if (align === "minimum") reportError(node, "consistentMinimum", expectedLength);
5387
+ else if (align === "length") reportError(node, "consistentLength", expectedLength);
5388
+ }
5389
+ }
5390
+ };
5391
+ }
5392
+ if (mode === "consistent-line-length") {
5393
+ const align = options.align || "exact";
5394
+ const fixedLength = options.length || 0;
5395
+ const setextHeadings = [];
5396
+ return {
5397
+ heading(node) {
5398
+ if (getHeadingKind(sourceCode, node) !== "setext") return;
5399
+ setextHeadings.push(node);
5400
+ },
5401
+ "root:exit"() {
5402
+ if (setextHeadings.length === 0) return;
5403
+ let minimumRequiredLineLength = 1;
5404
+ for (const node of setextHeadings) {
5405
+ const parsed = getParsedSetextHeading(node);
5406
+ if (!parsed) continue;
5407
+ minimumRequiredLineLength = Math.max(minimumRequiredLineLength, parsed.underline.raws.prefix.length + parsed.underline.raws.spaceBefore.length + 1);
5408
+ }
5409
+ let expectedLineLength = minimumRequiredLineLength;
5410
+ if (align === "any") {
5411
+ if (setextHeadings.length < 2) return;
5412
+ for (const node of setextHeadings) {
5413
+ const parsed = getParsedSetextHeading(node);
5414
+ if (!parsed) continue;
5415
+ expectedLineLength = Math.max(parsed.underline.loc.end.column - 1, minimumRequiredLineLength);
5416
+ break;
5417
+ }
5418
+ } else if (align === "exact") for (const node of setextHeadings) {
5419
+ const parsed = getParsedSetextHeading(node);
5420
+ if (!parsed) continue;
5421
+ expectedLineLength = Math.max(expectedLineLength, getMaxHeaderLineWidth(parsed));
5422
+ }
5423
+ else if (align === "minimum") {
5424
+ let maxLineWidth = 0;
5425
+ for (const node of setextHeadings) {
5426
+ const parsed = getParsedSetextHeading(node);
5427
+ if (!parsed) continue;
5428
+ maxLineWidth = Math.max(maxLineWidth, getMaxHeaderLineWidth(parsed));
5429
+ expectedLineLength = Math.max(expectedLineLength, parsed.underline.loc.end.column - 1);
5430
+ }
5431
+ if (expectedLineLength < maxLineWidth) expectedLineLength = maxLineWidth;
5432
+ else for (const node of setextHeadings) {
5433
+ const parsed = getParsedSetextHeading(node);
5434
+ if (!parsed) continue;
5435
+ if (maxLineWidth <= parsed.underline.loc.end.column - 1) expectedLineLength = Math.min(expectedLineLength, parsed.underline.loc.end.column - 1);
5436
+ }
5437
+ } else if (align === "length") expectedLineLength = Math.max(fixedLength, minimumRequiredLineLength);
5438
+ else return;
5439
+ if (!expectedLineLength || expectedLineLength < 1) return;
5440
+ for (const node of setextHeadings) {
5441
+ const parsed = getParsedSetextHeading(node);
5442
+ if (!parsed) continue;
5443
+ const expectedLength = expectedLineLength - parsed.underline.raws.prefix.length - parsed.underline.raws.spaceBefore.length;
5444
+ if (parsed.underline.text.length === expectedLength) continue;
5445
+ if (align === "any") reportError(node, "consistentLineLengthAny", expectedLength);
5446
+ else if (align === "exact") reportError(node, "consistentLineLengthExact", expectedLength);
5447
+ else if (align === "minimum") reportError(node, "consistentLineLengthMinimum", expectedLength);
5448
+ else if (align === "length") reportError(node, "consistentLineLengthLength", expectedLength);
5449
+ }
5450
+ }
5451
+ };
5452
+ }
5453
+ return {};
5454
+ /**
5455
+ * Get the maximum width of header lines.
5456
+ */
5457
+ function getMaxHeaderTextWidth(parsed) {
5458
+ let maxWidth = 0;
5459
+ for (const contentLine of parsed.contentLines) {
5460
+ const lineWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore + contentLine.text);
5461
+ const prefixWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore);
5462
+ maxWidth = Math.max(maxWidth, lineWidth - prefixWidth);
5463
+ }
5464
+ return maxWidth;
5465
+ }
5466
+ /**
5467
+ * Get the maximum width of header lines.
5468
+ */
5469
+ function getMaxHeaderLineWidth(parsed) {
5470
+ let maxLineWidth = 0;
5471
+ for (const contentLine of parsed.contentLines) {
5472
+ const lineWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore + contentLine.text);
5473
+ maxLineWidth = Math.max(maxLineWidth, lineWidth);
5474
+ }
5475
+ return maxLineWidth;
5476
+ }
5477
+ }
5478
+ });
5479
+
5122
5480
  //#endregion
5123
5481
  //#region src/rules/sort-definitions.ts
5124
5482
  var sort_definitions_default = createRule("sort-definitions", {
@@ -5536,6 +5894,217 @@ var table_header_casing_default = createRule("table-header-casing", {
5536
5894
  }
5537
5895
  });
5538
5896
 
5897
+ //#endregion
5898
+ //#region src/rules/thematic-break-character-style.ts
5899
+ var thematic_break_character_style_default = createRule("thematic-break-character-style", {
5900
+ meta: {
5901
+ type: "layout",
5902
+ docs: {
5903
+ description: "enforce consistent character style for thematic breaks (horizontal rules) in Markdown.",
5904
+ categories: [],
5905
+ listCategory: "Stylistic"
5906
+ },
5907
+ fixable: "code",
5908
+ hasSuggestions: false,
5909
+ schema: [{
5910
+ type: "object",
5911
+ properties: { style: {
5912
+ type: "string",
5913
+ enum: [
5914
+ "-",
5915
+ "*",
5916
+ "_"
5917
+ ]
5918
+ } },
5919
+ additionalProperties: false
5920
+ }],
5921
+ messages: { unexpected: "Thematic break should use '{{expected}}' but found '{{actual}}'." }
5922
+ },
5923
+ create(context) {
5924
+ const option = context.options[0];
5925
+ const style = option?.style || "-";
5926
+ return { thematicBreak(node) {
5927
+ const marker = getThematicBreakMarker(context.sourceCode, node);
5928
+ if (marker.kind !== style) context.report({
5929
+ node,
5930
+ messageId: "unexpected",
5931
+ data: {
5932
+ expected: style,
5933
+ actual: marker.kind
5934
+ },
5935
+ fix(fixer) {
5936
+ const range = context.sourceCode.getRange(node);
5937
+ const text = context.sourceCode.getText(node);
5938
+ const rep = text.replaceAll(marker.kind, style);
5939
+ return fixer.replaceTextRange(range, rep);
5940
+ }
5941
+ });
5942
+ } };
5943
+ }
5944
+ });
5945
+
5946
+ //#endregion
5947
+ //#region src/utils/thematic-break.ts
5948
+ /**
5949
+ * Check if the pattern is valid within the thematic break string.
5950
+ */
5951
+ function isValidThematicBreakPattern(pattern, text) {
5952
+ for (let i = 0; i < text.length; i += pattern.length) {
5953
+ const subSequence = text.slice(i, i + pattern.length);
5954
+ if (subSequence === pattern) continue;
5955
+ if (subSequence.length < pattern.length && pattern.startsWith(subSequence)) continue;
5956
+ return false;
5957
+ }
5958
+ return true;
5959
+ }
5960
+ /**
5961
+ * Create a thematic break string from a pattern and length.
5962
+ */
5963
+ function createThematicBreakFromPattern(pattern, length) {
5964
+ const mark = pattern[0];
5965
+ let candidate = pattern.repeat(Math.floor(length / pattern.length));
5966
+ if (candidate.length < length) candidate += pattern.slice(0, length - candidate.length);
5967
+ candidate = candidate.trim();
5968
+ if (candidate.length !== length) return null;
5969
+ let markCount = 0;
5970
+ for (const c of candidate) {
5971
+ if (c !== mark) continue;
5972
+ markCount++;
5973
+ if (markCount >= 3) return candidate;
5974
+ }
5975
+ return null;
5976
+ }
5977
+
5978
+ //#endregion
5979
+ //#region src/rules/thematic-break-length.ts
5980
+ var thematic_break_length_default = createRule("thematic-break-length", {
5981
+ meta: {
5982
+ type: "layout",
5983
+ docs: {
5984
+ description: "enforce consistent length for thematic breaks (horizontal rules) in Markdown.",
5985
+ categories: [],
5986
+ listCategory: "Stylistic"
5987
+ },
5988
+ fixable: "code",
5989
+ hasSuggestions: false,
5990
+ schema: [{
5991
+ type: "object",
5992
+ properties: { length: {
5993
+ type: "integer",
5994
+ minimum: 3
5995
+ } },
5996
+ additionalProperties: false
5997
+ }],
5998
+ messages: { unexpected: "Thematic break should be {{expected}} characters, but found {{actual}}." }
5999
+ },
6000
+ create(context) {
6001
+ const option = context.options[0] || {};
6002
+ const expectedLength = option.length ?? 3;
6003
+ const sourceCode = context.sourceCode;
6004
+ return { thematicBreak(node) {
6005
+ const marker = getThematicBreakMarker(sourceCode, node);
6006
+ if (marker.text.length === expectedLength) return;
6007
+ context.report({
6008
+ node,
6009
+ messageId: "unexpected",
6010
+ data: {
6011
+ expected: String(expectedLength),
6012
+ actual: String(marker.text.length)
6013
+ },
6014
+ fix(fixer) {
6015
+ const sequence = replacementSequence(marker);
6016
+ if (!sequence) return null;
6017
+ return fixer.replaceText(node, sequence);
6018
+ }
6019
+ });
6020
+ } };
6021
+ /**
6022
+ * Replace the sequence in the thematic break marker with the expected length.
6023
+ */
6024
+ function replacementSequence(marker) {
6025
+ if (marker.hasSpaces) {
6026
+ const pattern = inferSequencePattern(marker.text);
6027
+ if (pattern) return createThematicBreakFromPattern(pattern, expectedLength);
6028
+ return null;
6029
+ }
6030
+ return marker.kind.repeat(expectedLength);
6031
+ }
6032
+ /**
6033
+ * Infer sequence pattern from the original string.
6034
+ */
6035
+ function inferSequencePattern(original) {
6036
+ for (let length = 2; length < original.length; length++) {
6037
+ const pattern = original.slice(0, length);
6038
+ if (isValidThematicBreakPattern(pattern, original)) return pattern;
6039
+ }
6040
+ return null;
6041
+ }
6042
+ }
6043
+ });
6044
+
6045
+ //#endregion
6046
+ //#region src/rules/thematic-break-sequence-pattern.ts
6047
+ var thematic_break_sequence_pattern_default = createRule("thematic-break-sequence-pattern", {
6048
+ meta: {
6049
+ type: "layout",
6050
+ docs: {
6051
+ description: "enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown.",
6052
+ categories: [],
6053
+ listCategory: "Stylistic"
6054
+ },
6055
+ fixable: "code",
6056
+ hasSuggestions: false,
6057
+ schema: [{
6058
+ type: "object",
6059
+ properties: { pattern: { anyOf: [
6060
+ {
6061
+ type: "string",
6062
+ minLength: 1,
6063
+ pattern: "^\\-[ \\-]*$"
6064
+ },
6065
+ {
6066
+ type: "string",
6067
+ minLength: 1,
6068
+ pattern: "^\\*[ *]*$"
6069
+ },
6070
+ {
6071
+ type: "string",
6072
+ minLength: 1,
6073
+ pattern: "^_[ _]*$"
6074
+ }
6075
+ ] } },
6076
+ required: ["pattern"],
6077
+ additionalProperties: false
6078
+ }],
6079
+ messages: { inconsistentPattern: "Thematic break does not match the preferred repeating pattern '{{pattern}}'." }
6080
+ },
6081
+ create(context) {
6082
+ const option = context.options[0] || {};
6083
+ const pattern = option.pattern ?? "-";
6084
+ const sourceCode = context.sourceCode;
6085
+ const patterns = {
6086
+ "-": pattern.replaceAll(/[*_]/gu, "-"),
6087
+ "*": pattern.replaceAll(/[-_]/gu, "*"),
6088
+ _: pattern.replaceAll(/[*-]/gu, "_")
6089
+ };
6090
+ return { thematicBreak(node) {
6091
+ const marker = getThematicBreakMarker(sourceCode, node);
6092
+ const patternForKind = patterns[marker.kind];
6093
+ if (isValidThematicBreakPattern(patternForKind, marker.text)) return;
6094
+ context.report({
6095
+ node,
6096
+ messageId: "inconsistentPattern",
6097
+ data: { pattern },
6098
+ fix(fixer) {
6099
+ const replacement = createThematicBreakFromPattern(patternForKind, marker.text.length);
6100
+ if (!replacement) return null;
6101
+ return fixer.replaceText(node, replacement);
6102
+ }
6103
+ });
6104
+ } };
6105
+ }
6106
+ });
6107
+
5539
6108
  //#endregion
5540
6109
  //#region src/utils/rules.ts
5541
6110
  const rules$1 = [
@@ -5560,8 +6129,12 @@ const rules$1 = [
5560
6129
  prefer_inline_code_words_default,
5561
6130
  prefer_link_reference_definitions_default,
5562
6131
  prefer_linked_words_default,
6132
+ setext_heading_underline_length_default,
5563
6133
  sort_definitions_default,
5564
- table_header_casing_default
6134
+ table_header_casing_default,
6135
+ thematic_break_character_style_default,
6136
+ thematic_break_length_default,
6137
+ thematic_break_sequence_pattern_default
5565
6138
  ];
5566
6139
 
5567
6140
  //#endregion
@@ -5604,7 +6177,7 @@ __export(meta_exports, {
5604
6177
  version: () => version
5605
6178
  });
5606
6179
  const name = "eslint-plugin-markdown-preferences";
5607
- const version = "0.16.0";
6180
+ const version = "0.17.0";
5608
6181
 
5609
6182
  //#endregion
5610
6183
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,7 +34,7 @@
34
34
  "ts": "node --import=tsx",
35
35
  "mocha": "npm run ts -- ./node_modules/mocha/bin/mocha.js",
36
36
  "generate:version": "env-cmd -e version -- npm run update && npm run lint -- --fix",
37
- "changeset:version": "changeset version && npm run generate:version && git add --all",
37
+ "changeset:version": "env-cmd -e version -- changeset version && npm run generate:version && git add --all",
38
38
  "changeset:publish": "npm run build && changeset publish"
39
39
  },
40
40
  "repository": {