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 +4 -0
- package/lib/index.d.ts +35 -1
- package/lib/index.js +590 -17
- package/package.json +2 -2
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.
|
|
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/
|
|
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.
|
|
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.
|
|
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": {
|