eslint-plugin-markdown-preferences 0.25.0 → 0.26.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
@@ -13,18 +13,24 @@ A specialized ESLint plugin that helps enforce consistent writing style and form
13
13
 
14
14
  ## 📛 Features
15
15
 
16
- - **⚡ Effortless automation** - Transform your Markdown with auto-fixing that handles formatting, casing, and style consistency automatically
17
- - **📖 Professional documentation** - Enforce consistent headings, table headers, and organize link definitions for enterprise-ready documentation
18
- - **🎨 Clean formatting** - Remove trailing spaces, control line breaks, standardize code blocks, and ensure consistent list numbering for polished output
19
- - **⚙️ Flexible customization** - Configure casing styles (Title Case, Sentence case), code block languages, emoji notation, and more with extensive options
20
-
21
- **Try it live:** Check out the [Online Demo](https://eslint-online-playground.netlify.app/#eslint-plugin-markdown-preferences) to see the plugin in action!
16
+ - 📝 **Comprehensive style enforcement**\
17
+ Unifies document expression and description style: heading casing, table header casing, inline code/link usage, emoji notation, and more.
18
+ - 🧩 **Notation and formatting consistency**\
19
+ Standardizes Markdown notation: list markers, code fences, link/reference style, thematic breaks, and table formatting.
20
+ - 🎨 **Whitespace and decorative rules**\
21
+ Controls indentation, spacing, line breaks, trailing spaces, and decorative elements for clean, readable Markdown.
22
+ - 🔧 **Auto-fix support**\
23
+ Most rules support ESLint's `--fix` option for effortless formatting and correction.
24
+ - ⚙️ **Flexible configuration**\
25
+ Provides both "recommended" and "standard" configs, and allows you to finely customize formatting and rules to suit your preferences and Markdown style.
26
+ - 🌐 **Live demo & documentation**\
27
+ Try it instantly in the [Online Demo](https://eslint-online-playground.netlify.app/#eslint-plugin-markdown-preferences) and see [full documentation][documentation site].
22
28
 
23
29
  <!--DOCS_IGNORE_START-->
24
30
 
25
31
  ## 📖 Documentation
26
32
 
27
- For detailed usage instructions, rule configurations, and examples, visit our comprehensive [documentation site](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/).
33
+ For detailed usage instructions, rule configurations, and examples, visit our comprehensive [documentation site].
28
34
 
29
35
  ## 💿 Installation
30
36
 
@@ -205,5 +211,6 @@ Please use GitHub's Issues/PRs.
205
211
 
206
212
  See the [LICENSE](./LICENSE) file for license rights and limitations (MIT).
207
213
 
214
+ [documentation site]: https://ota-meshi.github.io/eslint-plugin-markdown-preferences/
208
215
  [npm-package]: https://www.npmjs.com/package/eslint-plugin-markdown-preferences
209
216
  [npmtrends]: http://www.npmtrends.com/eslint-plugin-markdown-preferences
package/lib/index.d.ts CHANGED
@@ -49,7 +49,7 @@ interface RuleOptions {
49
49
  * require link definitions and footnote definitions to be placed at the end of the document
50
50
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/definitions-last.html
51
51
  */
52
- 'markdown-preferences/definitions-last'?: Linter.RuleEntry<[]>;
52
+ 'markdown-preferences/definitions-last'?: Linter.RuleEntry<MarkdownPreferencesDefinitionsLast>;
53
53
  /**
54
54
  * enforce consistent emoji notation style in Markdown files.
55
55
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html
@@ -275,6 +275,16 @@ type MarkdownPreferencesCodeFenceLength = [] | [{
275
275
  type MarkdownPreferencesCodeFenceStyle = [] | [{
276
276
  style?: ("backtick" | "tilde");
277
277
  }];
278
+ type MarkdownPreferencesDefinitionsLast = [] | [{
279
+ linkDefinitionPlacement?: {
280
+ referencedFromSingleSection?: ("document-last" | "section-last");
281
+ referencedFromMultipleSections?: ("document-last" | "first-reference-section-last" | "last-reference-section-last");
282
+ };
283
+ footnoteDefinitionPlacement?: {
284
+ referencedFromSingleSection?: ("document-last" | "section-last");
285
+ referencedFromMultipleSections?: ("document-last" | "first-reference-section-last" | "last-reference-section-last");
286
+ };
287
+ }];
278
288
  type MarkdownPreferencesEmojiNotation = [] | [{
279
289
  prefer?: ("unicode" | "colon");
280
290
  ignoreUnknown?: boolean;
@@ -488,7 +498,7 @@ declare namespace meta_d_exports {
488
498
  export { name, version };
489
499
  }
490
500
  declare const name: "eslint-plugin-markdown-preferences";
491
- declare const version: "0.25.0";
501
+ declare const version: "0.26.0";
492
502
  //#endregion
493
503
  //#region src/index.d.ts
494
504
  declare const configs: {
package/lib/index.js CHANGED
@@ -36,6 +36,26 @@ function getParent(sourceCode, node) {
36
36
  return sourceCode.getParent(node);
37
37
  }
38
38
  /**
39
+ * Get the previous sibling of a node.
40
+ */
41
+ function getPrevSibling(sourceCode, node) {
42
+ const parent = getParent(sourceCode, node);
43
+ if (!parent) return null;
44
+ const index = parent.children.indexOf(node);
45
+ if (index <= 0) return null;
46
+ return parent.children[index - 1];
47
+ }
48
+ /**
49
+ * Get the next sibling of a node.
50
+ */
51
+ function getNextSibling(sourceCode, node) {
52
+ const parent = getParent(sourceCode, node);
53
+ if (!parent) return null;
54
+ const index = parent.children.indexOf(node);
55
+ if (index < 0 || index >= parent.children.length - 1) return null;
56
+ return parent.children[index + 1];
57
+ }
58
+ /**
39
59
  * Get the kind of heading.
40
60
  */
41
61
  function getHeadingKind(sourceCode, node) {
@@ -798,7 +818,7 @@ function getOtherMarker(unavailableMarker) {
798
818
  /**
799
819
  * Parse rule options.
800
820
  */
801
- function parseOptions$4(options) {
821
+ function parseOptions$5(options) {
802
822
  const primary = options.primary || "-";
803
823
  const secondary = options.secondary || getOtherMarker(primary);
804
824
  if (primary === secondary) throw new Error(`\`primary\` and \`secondary\` cannot be the same (primary: "${primary}", secondary: "${secondary}").`);
@@ -866,7 +886,7 @@ var bullet_list_marker_style_default = createRule("bullet-list-marker-style", {
866
886
  },
867
887
  create(context) {
868
888
  const sourceCode = context.sourceCode;
869
- const options = parseOptions$4(context.options[0] || {});
889
+ const options = parseOptions$5(context.options[0] || {});
870
890
  let containerStack = {
871
891
  node: sourceCode.ast,
872
892
  level: 1,
@@ -1307,6 +1327,23 @@ var code_fence_style_default = createRule("code-fence-style", {
1307
1327
 
1308
1328
  //#endregion
1309
1329
  //#region src/rules/definitions-last.ts
1330
+ /**
1331
+ * Parse options with defaults.
1332
+ */
1333
+ function parseOptions$4(options) {
1334
+ const linkDefinitionPlacement = {
1335
+ referencedFromSingleSection: options?.linkDefinitionPlacement?.referencedFromSingleSection || "document-last",
1336
+ referencedFromMultipleSections: options?.linkDefinitionPlacement?.referencedFromMultipleSections || "document-last"
1337
+ };
1338
+ const footnoteDefinitionPlacement = {
1339
+ referencedFromSingleSection: options?.footnoteDefinitionPlacement?.referencedFromSingleSection || "document-last",
1340
+ referencedFromMultipleSections: options?.footnoteDefinitionPlacement?.referencedFromMultipleSections || "document-last"
1341
+ };
1342
+ return {
1343
+ linkDefinitionPlacement,
1344
+ footnoteDefinitionPlacement
1345
+ };
1346
+ }
1310
1347
  var definitions_last_default = createRule("definitions-last", {
1311
1348
  meta: {
1312
1349
  type: "layout",
@@ -1317,31 +1354,238 @@ var definitions_last_default = createRule("definitions-last", {
1317
1354
  },
1318
1355
  fixable: "code",
1319
1356
  hasSuggestions: false,
1320
- schema: [],
1321
- messages: {}
1357
+ schema: [{
1358
+ type: "object",
1359
+ properties: {
1360
+ linkDefinitionPlacement: {
1361
+ type: "object",
1362
+ properties: {
1363
+ referencedFromSingleSection: {
1364
+ type: "string",
1365
+ enum: ["document-last", "section-last"]
1366
+ },
1367
+ referencedFromMultipleSections: {
1368
+ type: "string",
1369
+ enum: [
1370
+ "document-last",
1371
+ "first-reference-section-last",
1372
+ "last-reference-section-last"
1373
+ ]
1374
+ }
1375
+ },
1376
+ additionalProperties: false
1377
+ },
1378
+ footnoteDefinitionPlacement: {
1379
+ type: "object",
1380
+ properties: {
1381
+ referencedFromSingleSection: {
1382
+ type: "string",
1383
+ enum: ["document-last", "section-last"]
1384
+ },
1385
+ referencedFromMultipleSections: {
1386
+ type: "string",
1387
+ enum: [
1388
+ "document-last",
1389
+ "first-reference-section-last",
1390
+ "last-reference-section-last"
1391
+ ]
1392
+ }
1393
+ },
1394
+ additionalProperties: false
1395
+ }
1396
+ },
1397
+ additionalProperties: false
1398
+ }],
1399
+ messages: {
1400
+ definitionsDocumentLast: "Definition or footnote definition should be placed at the end of the document.",
1401
+ definitionsSectionLast: "Definition or footnote definition should be placed at the end of the section ({{at}}).",
1402
+ definitionsLastSectionLast: "Definition or footnote definition should be placed at the end of the last section (the end of the document)."
1403
+ }
1322
1404
  },
1323
1405
  create(context) {
1324
1406
  const sourceCode = context.sourceCode;
1325
- const lastNonDefinition = sourceCode.ast.children.findLast((node) => node.type !== "definition" && node.type !== "footnoteDefinition" && !(node.type === "html" && (node.value.startsWith("<!--") || node.value.startsWith("<script") || node.value.startsWith("<style"))));
1326
- if (!lastNonDefinition) return {};
1327
- const lastNonDefinitionRange = sourceCode.getRange(lastNonDefinition);
1328
- return { "definition, footnoteDefinition"(node) {
1407
+ const options = parseOptions$4(context.options[0]);
1408
+ /**
1409
+ * Determine whether a node can be placed as the last node of the document or a section.
1410
+ */
1411
+ function canBePlacedLastNode(node) {
1412
+ return node.type === "definition" || node.type === "footnoteDefinition" || node.type === "html" && (node.value.startsWith("<!--") || node.value.startsWith("<script") || node.value.startsWith("<style"));
1413
+ }
1414
+ const beforeLastNode = sourceCode.ast.children.findLast((node) => !canBePlacedLastNode(node));
1415
+ if (!beforeLastNode) return {};
1416
+ let lastSection = {
1417
+ heading: null,
1418
+ linkReferenceIds: /* @__PURE__ */ new Set(),
1419
+ footnoteReferenceIds: /* @__PURE__ */ new Set(),
1420
+ nextHeading: null
1421
+ };
1422
+ const sections = [lastSection];
1423
+ const definitions = [];
1424
+ /**
1425
+ * Get the expected placement of a definition or footnote definition node.
1426
+ */
1427
+ function getExpectedPlacement(node) {
1428
+ let referencedFromSingleSection;
1429
+ let referencedFromMultipleSections;
1430
+ let referenceIdsNs;
1431
+ if (node.type === "definition") {
1432
+ ({referencedFromSingleSection, referencedFromMultipleSections} = options.linkDefinitionPlacement);
1433
+ referenceIdsNs = "linkReferenceIds";
1434
+ } else if (node.type === "footnoteDefinition") {
1435
+ ({referencedFromSingleSection, referencedFromMultipleSections} = options.footnoteDefinitionPlacement);
1436
+ referenceIdsNs = "footnoteReferenceIds";
1437
+ } else return { type: "document-last" };
1438
+ if (referencedFromSingleSection === "document-last" && referencedFromSingleSection === referencedFromMultipleSections) return { type: "document-last" };
1439
+ const referencedSections = [];
1440
+ for (const section of sections) {
1441
+ if (!section[referenceIdsNs].has(node.identifier)) continue;
1442
+ referencedSections.push(section);
1443
+ if (referencedSections.length > 1 && referencedFromMultipleSections !== "last-reference-section-last") return {
1444
+ type: referencedFromMultipleSections,
1445
+ section: referencedSections[0]
1446
+ };
1447
+ }
1448
+ if (referencedSections.length === 0) return { type: "document-last" };
1449
+ if (referencedSections.length === 1) return {
1450
+ type: referencedFromSingleSection,
1451
+ section: referencedSections[0]
1452
+ };
1453
+ return {
1454
+ type: referencedFromMultipleSections,
1455
+ section: referencedSections[referencedSections.length - 1]
1456
+ };
1457
+ }
1458
+ /**
1459
+ * Verify the position of a definition or footnote definition node.
1460
+ */
1461
+ function verifyDefinitionPosition(node) {
1462
+ const expectedPlacement = getExpectedPlacement(node);
1463
+ if (expectedPlacement.type === "document-last") verifyDefinitionOnDocumentLast(node, "definitionsDocumentLast");
1464
+ else if (expectedPlacement.type === "section-last" || expectedPlacement.type === "first-reference-section-last" || expectedPlacement.type === "last-reference-section-last") if (!expectedPlacement.section.nextHeading) verifyDefinitionOnDocumentLast(node, "definitionsLastSectionLast");
1465
+ else verifyDefinitionOnSectionLast(node, expectedPlacement.section.nextHeading);
1466
+ }
1467
+ /**
1468
+ * Verify that a definition or footnote definition node is at the end of the document.
1469
+ */
1470
+ function verifyDefinitionOnDocumentLast(node, messageId) {
1471
+ if (!beforeLastNode) return;
1329
1472
  const range = sourceCode.getRange(node);
1330
- if (lastNonDefinitionRange[1] <= range[0]) return;
1473
+ if (sourceCode.getRange(beforeLastNode)[1] <= range[0]) return;
1331
1474
  context.report({
1332
1475
  node,
1333
- message: "Definition or footnote definition should be placed at the end of the document.",
1334
- *fix(fixer) {
1335
- let rangeStart = range[0];
1336
- for (let index = range[0] - 1; index >= 0; index--) {
1337
- if (sourceCode.text[index].trim()) break;
1338
- rangeStart = index;
1339
- }
1340
- yield fixer.removeRange([rangeStart, range[1]]);
1341
- yield fixer.insertTextAfterRange(lastNonDefinitionRange, sourceCode.text.slice(rangeStart, range[1]));
1476
+ messageId,
1477
+ fix(fixer) {
1478
+ return fixToMoveFromBeforeLastOfSectionToLastOfSection(fixer, beforeLastNode, node);
1342
1479
  }
1343
1480
  });
1344
- } };
1481
+ }
1482
+ /**
1483
+ * Verify that a definition or footnote definition node is at the end of its section.
1484
+ */
1485
+ function verifyDefinitionOnSectionLast(node, nextSectionHeading) {
1486
+ const beforeLastOfSectionNode = getSectionBeforeLastNode(nextSectionHeading);
1487
+ if (!beforeLastOfSectionNode) return;
1488
+ const range = sourceCode.getRange(node);
1489
+ const beforeLastOfSectionRange = sourceCode.getRange(beforeLastOfSectionNode);
1490
+ const nextSectionHeadingRange = sourceCode.getRange(nextSectionHeading);
1491
+ if (beforeLastOfSectionRange[1] <= range[0] && range[1] <= nextSectionHeadingRange[0]) return;
1492
+ const expectedStartLine = sourceCode.getLoc(beforeLastOfSectionNode).end.line + 1;
1493
+ const expectedEndLine = sourceCode.getLoc(nextSectionHeading).start.line - 1;
1494
+ context.report({
1495
+ node,
1496
+ messageId: "definitionsSectionLast",
1497
+ data: { at: expectedStartLine === expectedEndLine ? `L${expectedStartLine}` : `between L${expectedStartLine} and L${expectedEndLine}` },
1498
+ fix(fixer) {
1499
+ if (range[0] < beforeLastOfSectionRange[1]) return fixToMoveFromBeforeLastOfSectionToLastOfSection(fixer, beforeLastOfSectionNode, node);
1500
+ return fixToMoveFromAfterLastOfSectionToLastOfSection(fixer, nextSectionHeading, node);
1501
+ }
1502
+ });
1503
+ }
1504
+ /**
1505
+ * Get the node before the last node of a section.
1506
+ */
1507
+ function getSectionBeforeLastNode(nextSectionHeading) {
1508
+ let candidate = getPrevSibling(sourceCode, nextSectionHeading);
1509
+ while (candidate && canBePlacedLastNode(candidate)) candidate = getPrevSibling(sourceCode, candidate);
1510
+ return candidate;
1511
+ }
1512
+ /**
1513
+ * Fixer to move a definition or footnote definition node from before the last of the document/section
1514
+ * to the last of the document/section.
1515
+ */
1516
+ function fixToMoveFromBeforeLastOfSectionToLastOfSection(fixer, prev, node) {
1517
+ const next = getNextSibling(sourceCode, prev);
1518
+ return fixToMove(fixer, prev, next, node);
1519
+ }
1520
+ /**
1521
+ * Fixer to move a definition or footnote definition node from after the last of the document/section
1522
+ * to the last of the document/section.
1523
+ */
1524
+ function fixToMoveFromAfterLastOfSectionToLastOfSection(fixer, next, node) {
1525
+ const prev = getPrevSibling(sourceCode, next);
1526
+ if (!prev) return null;
1527
+ return fixToMove(fixer, prev, next, node);
1528
+ }
1529
+ /**
1530
+ * Fixer to move a definition or footnote definition node to after the prev node.
1531
+ */
1532
+ function* fixToMove(fixer, prev, next, node) {
1533
+ const range = sourceCode.getRange(node);
1534
+ const loc = sourceCode.getLoc(node);
1535
+ const lineStart = range[0] - loc.start.column + 1;
1536
+ let rangeStart = lineStart;
1537
+ let lineFeeds = 0;
1538
+ for (let index = rangeStart - 1; index >= 0; index--) {
1539
+ const c = sourceCode.text[index];
1540
+ if (c.trim()) break;
1541
+ rangeStart = index;
1542
+ if (c === "\n") lineFeeds++;
1543
+ }
1544
+ yield fixer.removeRange([rangeStart, range[1]]);
1545
+ let insertText = sourceCode.text.slice(rangeStart, lineStart) + sourceCode.text.slice(...range);
1546
+ if (prev.type === "footnoteDefinition" && node.type !== "footnoteDefinition" && lineFeeds <= 1) insertText = `\n${insertText}`;
1547
+ if (next && node.type === "footnoteDefinition" && next.type !== "footnoteDefinition") {
1548
+ const prevLoc = sourceCode.getLoc(prev);
1549
+ const nextLoc = sourceCode.getLoc(next);
1550
+ if (!(prevLoc.end.line + 1 < nextLoc.start.line)) insertText = `${insertText}\n`;
1551
+ }
1552
+ yield fixer.insertTextAfter(prev, insertText);
1553
+ }
1554
+ const containerStack = [];
1555
+ return {
1556
+ "blockquote, listItem, footnoteDefinition"(node) {
1557
+ containerStack.push(node);
1558
+ },
1559
+ "blockquote, listItem, footnoteDefinition:exit"() {
1560
+ containerStack.pop();
1561
+ },
1562
+ heading(node) {
1563
+ if (containerStack.length > 0) return;
1564
+ lastSection.nextHeading = node;
1565
+ lastSection = {
1566
+ heading: node,
1567
+ linkReferenceIds: /* @__PURE__ */ new Set(),
1568
+ footnoteReferenceIds: /* @__PURE__ */ new Set(),
1569
+ nextHeading: null
1570
+ };
1571
+ sections.push(lastSection);
1572
+ },
1573
+ linkReference(node) {
1574
+ lastSection.linkReferenceIds.add(node.identifier);
1575
+ },
1576
+ imageReference(node) {
1577
+ lastSection.linkReferenceIds.add(node.identifier);
1578
+ },
1579
+ footnoteReference(node) {
1580
+ lastSection.footnoteReferenceIds.add(node.identifier);
1581
+ },
1582
+ "definition, footnoteDefinition"(node) {
1583
+ definitions.push(node);
1584
+ },
1585
+ "root:exit"() {
1586
+ for (const node of definitions) verifyDefinitionPosition(node);
1587
+ }
1588
+ };
1345
1589
  }
1346
1590
  });
1347
1591
 
@@ -6233,11 +6477,11 @@ var link_bracket_newline_default = createRule("link-bracket-newline", {
6233
6477
  },
6234
6478
  create(context) {
6235
6479
  const sourceCode = context.sourceCode;
6236
- const optionProvider = parseOptions$5(context.options[0]);
6480
+ const optionProvider = parseOptions$6(context.options[0]);
6237
6481
  /**
6238
6482
  * Parse the options.
6239
6483
  */
6240
- function parseOptions$5(option) {
6484
+ function parseOptions$6(option) {
6241
6485
  const newline = option?.newline ?? "never";
6242
6486
  const multiline = option?.multiline ?? false;
6243
6487
  return (bracketsRange) => {
@@ -6711,11 +6955,11 @@ var link_paren_newline_default = createRule("link-paren-newline", {
6711
6955
  },
6712
6956
  create(context) {
6713
6957
  const sourceCode = context.sourceCode;
6714
- const optionProvider = parseOptions$5(context.options[0]);
6958
+ const optionProvider = parseOptions$6(context.options[0]);
6715
6959
  /**
6716
6960
  * Parse the options.
6717
6961
  */
6718
- function parseOptions$5(option) {
6962
+ function parseOptions$6(option) {
6719
6963
  const newline = option?.newline ?? "never";
6720
6964
  const multiline = option?.multiline ?? false;
6721
6965
  return (openingParenIndex, closingParenIndex) => {
@@ -10022,26 +10266,121 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10022
10266
  create(context) {
10023
10267
  const sourceCode = context.sourceCode;
10024
10268
  const columnOption = (context.options[0] || {}).column || "minimum";
10269
+ class TableContext {
10270
+ rows;
10271
+ columnCount;
10272
+ _cacheHasSpaceBetweenContentAndTrailingPipe = /* @__PURE__ */ new Map();
10273
+ _cacheExpectedPipePosition = /* @__PURE__ */ new Map();
10274
+ constructor(parsed) {
10275
+ const rows = [parsedTableRowToRowData(parsed.headerRow), parsedTableDelimiterRowToRowData(parsed.delimiterRow)];
10276
+ for (const bodyRow of parsed.bodyRows) rows.push(parsedTableRowToRowData(bodyRow));
10277
+ this.rows = rows;
10278
+ let columnCount = 0;
10279
+ for (const row of rows) columnCount = Math.max(columnCount, row.cells.length);
10280
+ this.columnCount = columnCount;
10281
+ }
10282
+ /**
10283
+ * Get the expected pipe position for the index
10284
+ */
10285
+ getExpectedPipePosition(pipeIndex) {
10286
+ let v = this._cacheExpectedPipePosition.get(pipeIndex);
10287
+ if (v !== void 0) return v;
10288
+ v = this._computeExpectedPipePositionWithoutCache(pipeIndex);
10289
+ this._cacheExpectedPipePosition.set(pipeIndex, v);
10290
+ return v;
10291
+ }
10292
+ /**
10293
+ * Check if there is at least one space between content and trailing pipe
10294
+ * for the index
10295
+ *
10296
+ * This is used to determine if the pipe should be aligned with a space before it.
10297
+ */
10298
+ hasSpaceBetweenContentAndTrailingPipe(pipeIndex) {
10299
+ if (pipeIndex === 0) return false;
10300
+ let v = this._cacheHasSpaceBetweenContentAndTrailingPipe.get(pipeIndex);
10301
+ if (v != null) return v;
10302
+ v = this._hasSpaceBetweenContentAndTrailingPipeWithoutCache(pipeIndex);
10303
+ this._cacheHasSpaceBetweenContentAndTrailingPipe.set(pipeIndex, v);
10304
+ return v;
10305
+ }
10306
+ /**
10307
+ * Get the expected pipe position for the index
10308
+ */
10309
+ _computeExpectedPipePositionWithoutCache(pipeIndex) {
10310
+ if (pipeIndex === 0) {
10311
+ const firstCell = this.rows[0].cells[0];
10312
+ const firstToken = firstCell.leadingPipe ?? firstCell.content;
10313
+ if (!firstToken) return null;
10314
+ return getTextWidth(sourceCode.lines[firstToken.loc.start.line - 1].slice(0, firstToken.loc.start.column - 1));
10315
+ }
10316
+ if (columnOption === "minimum") return this.getMinimumPipePosition(pipeIndex);
10317
+ else if (columnOption === "consistent") {
10318
+ const columnIndex = pipeIndex - 1;
10319
+ for (const row of this.rows) {
10320
+ if (row.cells.length <= columnIndex) continue;
10321
+ const cell = row.cells[columnIndex];
10322
+ if (cell.type === "delimiter" || !cell.trailingPipe) continue;
10323
+ const width = getTextWidth(sourceCode.lines[cell.trailingPipe.loc.start.line - 1].slice(0, cell.trailingPipe.loc.start.column - 1));
10324
+ return Math.max(width, this.getMinimumPipePosition(pipeIndex) || 0);
10325
+ }
10326
+ }
10327
+ return null;
10328
+ }
10329
+ /**
10330
+ * Get the minimum pipe position for the index
10331
+ */
10332
+ getMinimumPipePosition(pipeIndex) {
10333
+ const needSpaceBeforePipe = this.hasSpaceBetweenContentAndTrailingPipe(pipeIndex);
10334
+ let maxWidth = 0;
10335
+ const columnIndex = pipeIndex - 1;
10336
+ for (const row of this.rows) {
10337
+ if (row.cells.length <= columnIndex) continue;
10338
+ const cell = row.cells[columnIndex];
10339
+ let width;
10340
+ if (cell.type === "delimiter") {
10341
+ const minimumDelimiterLength = getMinimumDelimiterLength(cell.align);
10342
+ width = getTextWidth(sourceCode.lines[cell.delimiter.loc.start.line - 1].slice(0, cell.delimiter.loc.start.column - 1)) + minimumDelimiterLength;
10343
+ } else {
10344
+ if (!cell.content) continue;
10345
+ width = getTextWidth(sourceCode.lines[cell.content.loc.end.line - 1].slice(0, cell.content.loc.end.column - 1));
10346
+ }
10347
+ if (needSpaceBeforePipe) width += 1;
10348
+ maxWidth = Math.max(maxWidth, width);
10349
+ }
10350
+ return maxWidth;
10351
+ }
10352
+ /**
10353
+ * Check if there is at least one space between content and trailing pipe
10354
+ */
10355
+ _hasSpaceBetweenContentAndTrailingPipeWithoutCache(pipeIndex) {
10356
+ const columnIndex = pipeIndex - 1;
10357
+ for (const row of this.rows) {
10358
+ if (row.cells.length <= columnIndex) continue;
10359
+ const cell = row.cells[columnIndex];
10360
+ if (!cell.trailingPipe) continue;
10361
+ let content;
10362
+ if (cell.type === "delimiter") content = cell.delimiter;
10363
+ else {
10364
+ if (!cell.content) continue;
10365
+ content = cell.content;
10366
+ }
10367
+ if (content.range[1] < cell.trailingPipe.range[0]) continue;
10368
+ return false;
10369
+ }
10370
+ return true;
10371
+ }
10372
+ }
10025
10373
  /**
10026
10374
  * Verify the table pipes
10027
10375
  */
10028
- function verifyTablePipes(rows) {
10029
- let columnCount = 0;
10030
- for (const row of rows) columnCount = Math.max(columnCount, row.cells.length);
10031
- let targetRows = [...rows];
10032
- for (let pipeIndex = 0; pipeIndex <= columnCount; pipeIndex++) {
10033
- const expected = getExpectedPipePosition(rows, pipeIndex);
10034
- if (expected == null) continue;
10035
- const unreportedRows = [];
10036
- for (const row of targetRows) if (verifyRowPipe(row, pipeIndex, expected)) unreportedRows.push(row);
10037
- targetRows = unreportedRows;
10038
- if (targetRows.length === 0) break;
10039
- }
10376
+ function verifyTablePipes(table) {
10377
+ const targetRows = [...table.rows];
10378
+ for (const row of targetRows) for (let pipeIndex = 0; pipeIndex <= table.columnCount; pipeIndex++) if (!verifyRowPipe(row, pipeIndex, table)) break;
10040
10379
  }
10041
10380
  /**
10042
10381
  * Verify the pipe in the row
10043
10382
  */
10044
- function verifyRowPipe(row, pipeIndex, expected) {
10383
+ function verifyRowPipe(row, pipeIndex, table) {
10045
10384
  let cellIndex;
10046
10385
  let pipe;
10047
10386
  if (pipeIndex === 0) {
@@ -10055,15 +10394,14 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10055
10394
  const cell = row.cells[cellIndex];
10056
10395
  const pipeToken = cell[pipe];
10057
10396
  if (!pipeToken) return true;
10058
- return verifyPipe(pipeToken, expected, {
10059
- cell,
10060
- pipeIndex
10061
- });
10397
+ return verifyPipe(pipeToken, pipeIndex, table, cell);
10062
10398
  }
10063
10399
  /**
10064
10400
  * Verify the pipe position
10065
10401
  */
10066
- function verifyPipe(pipe, expected, ctx) {
10402
+ function verifyPipe(pipe, pipeIndex, table, cell) {
10403
+ const expected = table.getExpectedPipePosition(pipeIndex);
10404
+ if (expected == null) return true;
10067
10405
  const actual = getTextWidth(sourceCode.lines[pipe.loc.start.line - 1].slice(0, pipe.loc.start.column - 1));
10068
10406
  const diff = expected - actual;
10069
10407
  if (diff === 0) return true;
@@ -10077,22 +10415,22 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10077
10415
  },
10078
10416
  fix(fixer) {
10079
10417
  if (diff > 0) {
10080
- if (ctx.pipeIndex === 0 || ctx.cell.type === "cell") return fixer.insertTextBeforeRange(pipe.range, " ".repeat(diff));
10081
- return fixer.insertTextAfterRange([ctx.cell.delimiter.range[0], ctx.cell.delimiter.range[0] + 1], "-".repeat(diff));
10418
+ if (pipeIndex === 0 || cell.type === "cell") return fixer.insertTextBeforeRange(pipe.range, " ".repeat(diff));
10419
+ return fixer.insertTextAfterRange([cell.delimiter.range[0], cell.delimiter.range[0] + 1], "-".repeat(diff));
10082
10420
  }
10083
10421
  const baseEdit = fixRemoveSpaces();
10084
10422
  if (baseEdit) return baseEdit;
10085
- if (ctx.pipeIndex === 0 || ctx.cell.type === "cell") return null;
10086
- const beforeDelimiter = sourceCode.lines[ctx.cell.delimiter.loc.start.line - 1].slice(0, ctx.cell.delimiter.loc.start.column - 1);
10423
+ if (pipeIndex === 0 || cell.type === "cell") return null;
10424
+ const beforeDelimiter = sourceCode.lines[cell.delimiter.loc.start.line - 1].slice(0, cell.delimiter.loc.start.column - 1);
10087
10425
  const widthBeforeDelimiter = getTextWidth(beforeDelimiter);
10088
10426
  const newLength = expected - widthBeforeDelimiter;
10089
- const minimumDelimiterLength = getMinimumDelimiterLength(ctx.cell.align);
10090
- const spaceAfter = isNeedSpaceAfterContent(ctx.cell) ? " " : "";
10427
+ const minimumDelimiterLength = getMinimumDelimiterLength(cell.align);
10428
+ const spaceAfter = table.hasSpaceBetweenContentAndTrailingPipe(pipeIndex) ? " " : "";
10091
10429
  if (newLength < minimumDelimiterLength + spaceAfter.length) return null;
10092
- const delimiterPrefix = ctx.cell.align === "left" || ctx.cell.align === "center" ? ":" : "";
10093
- const delimiterSuffix = (ctx.cell.align === "right" || ctx.cell.align === "center" ? ":" : "") + spaceAfter;
10430
+ const delimiterPrefix = cell.align === "left" || cell.align === "center" ? ":" : "";
10431
+ const delimiterSuffix = (cell.align === "right" || cell.align === "center" ? ":" : "") + spaceAfter;
10094
10432
  const newDelimiter = "-".repeat(newLength - delimiterPrefix.length - delimiterSuffix.length);
10095
- return fixer.replaceTextRange([ctx.cell.delimiter.range[0], pipe.range[0]], delimiterPrefix + newDelimiter + delimiterSuffix);
10433
+ return fixer.replaceTextRange([cell.delimiter.range[0], pipe.range[0]], delimiterPrefix + newDelimiter + delimiterSuffix);
10096
10434
  /**
10097
10435
  * Fixer to remove spaces before the pipe
10098
10436
  */
@@ -10102,7 +10440,7 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10102
10440
  const spacesBeforePipeLength = beforePipe.length - trimmedBeforePipe.length;
10103
10441
  const widthBeforePipe = getTextWidth(trimmedBeforePipe);
10104
10442
  const newSpacesLength = expected - widthBeforePipe;
10105
- if (newSpacesLength < (ctx.pipeIndex > 0 && isNeedSpaceAfterContent(ctx.cell) ? 1 : 0)) return null;
10443
+ if (newSpacesLength < (table.hasSpaceBetweenContentAndTrailingPipe(pipeIndex) ? 1 : 0)) return null;
10106
10444
  return fixer.replaceTextRange([pipe.range[0] - spacesBeforePipeLength, pipe.range[0]], " ".repeat(newSpacesLength));
10107
10445
  }
10108
10446
  }
@@ -10110,106 +10448,47 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10110
10448
  return false;
10111
10449
  }
10112
10450
  /**
10113
- * Get the expected pipe position for the index
10114
- */
10115
- function getExpectedPipePosition(rows, pipeIndex) {
10116
- if (pipeIndex === 0) {
10117
- const firstCell = rows[0].cells[0];
10118
- const firstToken = firstCell.leadingPipe ?? firstCell.content;
10119
- if (!firstToken) return null;
10120
- return getTextWidth(sourceCode.lines[firstToken.loc.start.line - 1].slice(0, firstToken.loc.start.column - 1));
10121
- }
10122
- if (columnOption === "minimum") return getMinimumPipePosition(rows, pipeIndex);
10123
- else if (columnOption === "consistent") {
10124
- const columnIndex = pipeIndex - 1;
10125
- for (const row of rows) {
10126
- if (row.cells.length <= columnIndex) continue;
10127
- const cell = row.cells[columnIndex];
10128
- if (cell.type === "delimiter" || !cell.trailingPipe) continue;
10129
- const width = getTextWidth(sourceCode.lines[cell.trailingPipe.loc.start.line - 1].slice(0, cell.trailingPipe.loc.start.column - 1));
10130
- return Math.max(width, getMinimumPipePosition(rows, pipeIndex) || 0);
10131
- }
10132
- }
10133
- return null;
10134
- }
10135
- /**
10136
- * Get the minimum pipe position for the index
10137
- */
10138
- function getMinimumPipePosition(rows, pipeIndex) {
10139
- let maxWidth = 0;
10140
- const columnIndex = pipeIndex - 1;
10141
- for (const row of rows) {
10142
- if (row.cells.length <= columnIndex) continue;
10143
- const cell = row.cells[columnIndex];
10144
- let width;
10145
- if (cell.type === "delimiter") {
10146
- const minimumDelimiterLength = getMinimumDelimiterLength(cell.align);
10147
- width = getTextWidth(sourceCode.lines[cell.delimiter.loc.start.line - 1].slice(0, cell.delimiter.loc.start.column - 1)) + minimumDelimiterLength;
10148
- } else {
10149
- if (!cell.content) continue;
10150
- width = getTextWidth(sourceCode.lines[cell.content.loc.end.line - 1].slice(0, cell.content.loc.end.column - 1));
10151
- }
10152
- if (isNeedSpaceAfterContent(cell)) width += 1;
10153
- maxWidth = Math.max(maxWidth, width);
10154
- }
10155
- return maxWidth;
10156
- }
10157
- /**
10158
10451
  * Get the minimum delimiter length based on alignment
10159
10452
  */
10160
10453
  function getMinimumDelimiterLength(align) {
10161
10454
  return align === "none" ? 1 : align === "center" ? 3 : 2;
10162
10455
  }
10163
- /**
10164
- * Check if a cell needs a space after its content
10165
- */
10166
- function isNeedSpaceAfterContent(cell) {
10167
- let content;
10168
- if (cell.type === "delimiter") content = cell.delimiter;
10169
- else {
10170
- if (!cell.content) return false;
10171
- content = cell.content;
10172
- }
10173
- return cell.trailingPipe && content.range[1] < cell.trailingPipe.range[0];
10174
- }
10175
- /**
10176
- * Convert a parsed table row to row data
10177
- */
10178
- function parsedTableRowToRowData(parsedRow) {
10179
- return { cells: parsedRow.cells.map((cell, index) => {
10180
- const nextCell = index + 1 < parsedRow.cells.length ? parsedRow.cells[index + 1] : null;
10181
- return {
10182
- type: "cell",
10183
- leadingPipe: cell.leadingPipe,
10184
- content: cell.cell,
10185
- trailingPipe: nextCell ? nextCell.leadingPipe : parsedRow.trailingPipe
10186
- };
10187
- }) };
10188
- }
10189
- /**
10190
- * Convert a parsed table delimiter row to row data
10191
- */
10192
- function parsedTableDelimiterRowToRowData(parsedDelimiterRow) {
10193
- return { cells: parsedDelimiterRow.delimiters.map((cell, index) => {
10194
- const nextCell = index + 1 < parsedDelimiterRow.delimiters.length ? parsedDelimiterRow.delimiters[index + 1] : null;
10195
- return {
10196
- type: "delimiter",
10197
- leadingPipe: cell.leadingPipe,
10198
- delimiter: cell.delimiter,
10199
- align: cell.delimiter.align,
10200
- trailingPipe: nextCell ? nextCell.leadingPipe : parsedDelimiterRow.trailingPipe
10201
- };
10202
- }) };
10203
- }
10204
10456
  return { table(node) {
10205
10457
  const parsed = parseTable(sourceCode, node);
10206
10458
  if (!parsed) return;
10207
- const rows = [parsedTableRowToRowData(parsed.headerRow), parsedTableDelimiterRowToRowData(parsed.delimiterRow)];
10208
- for (const bodyRow of parsed.bodyRows) rows.push(parsedTableRowToRowData(bodyRow));
10209
- verifyTablePipes(rows);
10459
+ verifyTablePipes(new TableContext(parsed));
10210
10460
  } };
10211
10461
  }
10212
10462
  });
10463
+ /**
10464
+ * Convert a parsed table row to row data
10465
+ */
10466
+ function parsedTableRowToRowData(parsedRow) {
10467
+ return { cells: parsedRow.cells.map((cell, index) => {
10468
+ const nextCell = index + 1 < parsedRow.cells.length ? parsedRow.cells[index + 1] : null;
10469
+ return {
10470
+ type: "cell",
10471
+ leadingPipe: cell.leadingPipe,
10472
+ content: cell.cell,
10473
+ trailingPipe: nextCell ? nextCell.leadingPipe : parsedRow.trailingPipe
10474
+ };
10475
+ }) };
10476
+ }
10477
+ /**
10478
+ * Convert a parsed table delimiter row to row data
10479
+ */
10480
+ function parsedTableDelimiterRowToRowData(parsedDelimiterRow) {
10481
+ return { cells: parsedDelimiterRow.delimiters.map((cell, index) => {
10482
+ const nextCell = index + 1 < parsedDelimiterRow.delimiters.length ? parsedDelimiterRow.delimiters[index + 1] : null;
10483
+ return {
10484
+ type: "delimiter",
10485
+ leadingPipe: cell.leadingPipe,
10486
+ delimiter: cell.delimiter,
10487
+ align: cell.delimiter.align,
10488
+ trailingPipe: nextCell ? nextCell.leadingPipe : parsedDelimiterRow.trailingPipe
10489
+ };
10490
+ }) };
10491
+ }
10213
10492
 
10214
10493
  //#endregion
10215
10494
  //#region src/rules/table-pipe-spacing.ts
@@ -10844,7 +11123,7 @@ const rules$1 = [
10844
11123
 
10845
11124
  //#endregion
10846
11125
  //#region src/configs/recommended.ts
10847
- var recommended_exports = __export({
11126
+ var recommended_exports = /* @__PURE__ */ __export({
10848
11127
  files: () => files$1,
10849
11128
  language: () => language$1,
10850
11129
  languageOptions: () => languageOptions$1,
@@ -10874,7 +11153,7 @@ const rules$3 = {
10874
11153
 
10875
11154
  //#endregion
10876
11155
  //#region src/configs/standard.ts
10877
- var standard_exports = __export({
11156
+ var standard_exports = /* @__PURE__ */ __export({
10878
11157
  files: () => files,
10879
11158
  language: () => language,
10880
11159
  languageOptions: () => languageOptions,
@@ -10935,12 +11214,12 @@ const rules$2 = {
10935
11214
 
10936
11215
  //#endregion
10937
11216
  //#region src/meta.ts
10938
- var meta_exports = __export({
11217
+ var meta_exports = /* @__PURE__ */ __export({
10939
11218
  name: () => name,
10940
11219
  version: () => version
10941
11220
  });
10942
11221
  const name = "eslint-plugin-markdown-preferences";
10943
- const version = "0.25.0";
11222
+ const version = "0.26.0";
10944
11223
 
10945
11224
  //#endregion
10946
11225
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {