eslint-plugin-markdown-preferences 0.25.0 → 0.26.1

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.1";
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,243 @@ 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
+ const startColumnOffset = loc.start.column - 1;
1546
+ const insertLines = sourceCode.lines.slice(loc.start.line - 1, loc.end.line).map((lineText, i, arr) => {
1547
+ if (i === arr.length - 1) return lineText.slice(startColumnOffset, loc.end.column - 1);
1548
+ return lineText.slice(startColumnOffset);
1549
+ });
1550
+ let insertText = sourceCode.text.slice(rangeStart, lineStart) + insertLines.join(/\r?\n/u.exec(sourceCode.text)?.[0] ?? "\n");
1551
+ if (prev.type === "footnoteDefinition" && node.type !== "footnoteDefinition" && lineFeeds <= 1) insertText = `\n${insertText}`;
1552
+ if (next && node.type === "footnoteDefinition" && next.type !== "footnoteDefinition") {
1553
+ const prevLoc = sourceCode.getLoc(prev);
1554
+ const nextLoc = sourceCode.getLoc(next);
1555
+ if (!(prevLoc.end.line + 1 < nextLoc.start.line)) insertText = `${insertText}\n`;
1556
+ }
1557
+ yield fixer.insertTextAfter(prev, insertText);
1558
+ }
1559
+ const containerStack = [];
1560
+ return {
1561
+ "blockquote, listItem, footnoteDefinition"(node) {
1562
+ containerStack.push(node);
1563
+ },
1564
+ "blockquote, listItem, footnoteDefinition:exit"() {
1565
+ containerStack.pop();
1566
+ },
1567
+ heading(node) {
1568
+ if (containerStack.length > 0) return;
1569
+ lastSection.nextHeading = node;
1570
+ lastSection = {
1571
+ heading: node,
1572
+ linkReferenceIds: /* @__PURE__ */ new Set(),
1573
+ footnoteReferenceIds: /* @__PURE__ */ new Set(),
1574
+ nextHeading: null
1575
+ };
1576
+ sections.push(lastSection);
1577
+ },
1578
+ linkReference(node) {
1579
+ lastSection.linkReferenceIds.add(node.identifier);
1580
+ },
1581
+ imageReference(node) {
1582
+ lastSection.linkReferenceIds.add(node.identifier);
1583
+ },
1584
+ footnoteReference(node) {
1585
+ lastSection.footnoteReferenceIds.add(node.identifier);
1586
+ },
1587
+ "definition, footnoteDefinition"(node) {
1588
+ definitions.push(node);
1589
+ },
1590
+ "root:exit"() {
1591
+ for (const node of definitions) verifyDefinitionPosition(node);
1592
+ }
1593
+ };
1345
1594
  }
1346
1595
  });
1347
1596
 
@@ -6233,11 +6482,11 @@ var link_bracket_newline_default = createRule("link-bracket-newline", {
6233
6482
  },
6234
6483
  create(context) {
6235
6484
  const sourceCode = context.sourceCode;
6236
- const optionProvider = parseOptions$5(context.options[0]);
6485
+ const optionProvider = parseOptions$6(context.options[0]);
6237
6486
  /**
6238
6487
  * Parse the options.
6239
6488
  */
6240
- function parseOptions$5(option) {
6489
+ function parseOptions$6(option) {
6241
6490
  const newline = option?.newline ?? "never";
6242
6491
  const multiline = option?.multiline ?? false;
6243
6492
  return (bracketsRange) => {
@@ -6711,11 +6960,11 @@ var link_paren_newline_default = createRule("link-paren-newline", {
6711
6960
  },
6712
6961
  create(context) {
6713
6962
  const sourceCode = context.sourceCode;
6714
- const optionProvider = parseOptions$5(context.options[0]);
6963
+ const optionProvider = parseOptions$6(context.options[0]);
6715
6964
  /**
6716
6965
  * Parse the options.
6717
6966
  */
6718
- function parseOptions$5(option) {
6967
+ function parseOptions$6(option) {
6719
6968
  const newline = option?.newline ?? "never";
6720
6969
  const multiline = option?.multiline ?? false;
6721
6970
  return (openingParenIndex, closingParenIndex) => {
@@ -10022,26 +10271,121 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10022
10271
  create(context) {
10023
10272
  const sourceCode = context.sourceCode;
10024
10273
  const columnOption = (context.options[0] || {}).column || "minimum";
10274
+ class TableContext {
10275
+ rows;
10276
+ columnCount;
10277
+ _cacheHasSpaceBetweenContentAndTrailingPipe = /* @__PURE__ */ new Map();
10278
+ _cacheExpectedPipePosition = /* @__PURE__ */ new Map();
10279
+ constructor(parsed) {
10280
+ const rows = [parsedTableRowToRowData(parsed.headerRow), parsedTableDelimiterRowToRowData(parsed.delimiterRow)];
10281
+ for (const bodyRow of parsed.bodyRows) rows.push(parsedTableRowToRowData(bodyRow));
10282
+ this.rows = rows;
10283
+ let columnCount = 0;
10284
+ for (const row of rows) columnCount = Math.max(columnCount, row.cells.length);
10285
+ this.columnCount = columnCount;
10286
+ }
10287
+ /**
10288
+ * Get the expected pipe position for the index
10289
+ */
10290
+ getExpectedPipePosition(pipeIndex) {
10291
+ let v = this._cacheExpectedPipePosition.get(pipeIndex);
10292
+ if (v !== void 0) return v;
10293
+ v = this._computeExpectedPipePositionWithoutCache(pipeIndex);
10294
+ this._cacheExpectedPipePosition.set(pipeIndex, v);
10295
+ return v;
10296
+ }
10297
+ /**
10298
+ * Check if there is at least one space between content and trailing pipe
10299
+ * for the index
10300
+ *
10301
+ * This is used to determine if the pipe should be aligned with a space before it.
10302
+ */
10303
+ hasSpaceBetweenContentAndTrailingPipe(pipeIndex) {
10304
+ if (pipeIndex === 0) return false;
10305
+ let v = this._cacheHasSpaceBetweenContentAndTrailingPipe.get(pipeIndex);
10306
+ if (v != null) return v;
10307
+ v = this._hasSpaceBetweenContentAndTrailingPipeWithoutCache(pipeIndex);
10308
+ this._cacheHasSpaceBetweenContentAndTrailingPipe.set(pipeIndex, v);
10309
+ return v;
10310
+ }
10311
+ /**
10312
+ * Get the expected pipe position for the index
10313
+ */
10314
+ _computeExpectedPipePositionWithoutCache(pipeIndex) {
10315
+ if (pipeIndex === 0) {
10316
+ const firstCell = this.rows[0].cells[0];
10317
+ const firstToken = firstCell.leadingPipe ?? firstCell.content;
10318
+ if (!firstToken) return null;
10319
+ return getTextWidth(sourceCode.lines[firstToken.loc.start.line - 1].slice(0, firstToken.loc.start.column - 1));
10320
+ }
10321
+ if (columnOption === "minimum") return this.getMinimumPipePosition(pipeIndex);
10322
+ else if (columnOption === "consistent") {
10323
+ const columnIndex = pipeIndex - 1;
10324
+ for (const row of this.rows) {
10325
+ if (row.cells.length <= columnIndex) continue;
10326
+ const cell = row.cells[columnIndex];
10327
+ if (cell.type === "delimiter" || !cell.trailingPipe) continue;
10328
+ const width = getTextWidth(sourceCode.lines[cell.trailingPipe.loc.start.line - 1].slice(0, cell.trailingPipe.loc.start.column - 1));
10329
+ return Math.max(width, this.getMinimumPipePosition(pipeIndex) || 0);
10330
+ }
10331
+ }
10332
+ return null;
10333
+ }
10334
+ /**
10335
+ * Get the minimum pipe position for the index
10336
+ */
10337
+ getMinimumPipePosition(pipeIndex) {
10338
+ const needSpaceBeforePipe = this.hasSpaceBetweenContentAndTrailingPipe(pipeIndex);
10339
+ let maxWidth = 0;
10340
+ const columnIndex = pipeIndex - 1;
10341
+ for (const row of this.rows) {
10342
+ if (row.cells.length <= columnIndex) continue;
10343
+ const cell = row.cells[columnIndex];
10344
+ let width;
10345
+ if (cell.type === "delimiter") {
10346
+ const minimumDelimiterLength = getMinimumDelimiterLength(cell.align);
10347
+ width = getTextWidth(sourceCode.lines[cell.delimiter.loc.start.line - 1].slice(0, cell.delimiter.loc.start.column - 1)) + minimumDelimiterLength;
10348
+ } else {
10349
+ if (!cell.content) continue;
10350
+ width = getTextWidth(sourceCode.lines[cell.content.loc.end.line - 1].slice(0, cell.content.loc.end.column - 1));
10351
+ }
10352
+ if (needSpaceBeforePipe) width += 1;
10353
+ maxWidth = Math.max(maxWidth, width);
10354
+ }
10355
+ return maxWidth;
10356
+ }
10357
+ /**
10358
+ * Check if there is at least one space between content and trailing pipe
10359
+ */
10360
+ _hasSpaceBetweenContentAndTrailingPipeWithoutCache(pipeIndex) {
10361
+ const columnIndex = pipeIndex - 1;
10362
+ for (const row of this.rows) {
10363
+ if (row.cells.length <= columnIndex) continue;
10364
+ const cell = row.cells[columnIndex];
10365
+ if (!cell.trailingPipe) continue;
10366
+ let content;
10367
+ if (cell.type === "delimiter") content = cell.delimiter;
10368
+ else {
10369
+ if (!cell.content) continue;
10370
+ content = cell.content;
10371
+ }
10372
+ if (content.range[1] < cell.trailingPipe.range[0]) continue;
10373
+ return false;
10374
+ }
10375
+ return true;
10376
+ }
10377
+ }
10025
10378
  /**
10026
10379
  * Verify the table pipes
10027
10380
  */
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
- }
10381
+ function verifyTablePipes(table) {
10382
+ const targetRows = [...table.rows];
10383
+ for (const row of targetRows) for (let pipeIndex = 0; pipeIndex <= table.columnCount; pipeIndex++) if (!verifyRowPipe(row, pipeIndex, table)) break;
10040
10384
  }
10041
10385
  /**
10042
10386
  * Verify the pipe in the row
10043
10387
  */
10044
- function verifyRowPipe(row, pipeIndex, expected) {
10388
+ function verifyRowPipe(row, pipeIndex, table) {
10045
10389
  let cellIndex;
10046
10390
  let pipe;
10047
10391
  if (pipeIndex === 0) {
@@ -10055,15 +10399,14 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10055
10399
  const cell = row.cells[cellIndex];
10056
10400
  const pipeToken = cell[pipe];
10057
10401
  if (!pipeToken) return true;
10058
- return verifyPipe(pipeToken, expected, {
10059
- cell,
10060
- pipeIndex
10061
- });
10402
+ return verifyPipe(pipeToken, pipeIndex, table, cell);
10062
10403
  }
10063
10404
  /**
10064
10405
  * Verify the pipe position
10065
10406
  */
10066
- function verifyPipe(pipe, expected, ctx) {
10407
+ function verifyPipe(pipe, pipeIndex, table, cell) {
10408
+ const expected = table.getExpectedPipePosition(pipeIndex);
10409
+ if (expected == null) return true;
10067
10410
  const actual = getTextWidth(sourceCode.lines[pipe.loc.start.line - 1].slice(0, pipe.loc.start.column - 1));
10068
10411
  const diff = expected - actual;
10069
10412
  if (diff === 0) return true;
@@ -10077,22 +10420,22 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10077
10420
  },
10078
10421
  fix(fixer) {
10079
10422
  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));
10423
+ if (pipeIndex === 0 || cell.type === "cell") return fixer.insertTextBeforeRange(pipe.range, " ".repeat(diff));
10424
+ return fixer.insertTextAfterRange([cell.delimiter.range[0], cell.delimiter.range[0] + 1], "-".repeat(diff));
10082
10425
  }
10083
10426
  const baseEdit = fixRemoveSpaces();
10084
10427
  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);
10428
+ if (pipeIndex === 0 || cell.type === "cell") return null;
10429
+ const beforeDelimiter = sourceCode.lines[cell.delimiter.loc.start.line - 1].slice(0, cell.delimiter.loc.start.column - 1);
10087
10430
  const widthBeforeDelimiter = getTextWidth(beforeDelimiter);
10088
10431
  const newLength = expected - widthBeforeDelimiter;
10089
- const minimumDelimiterLength = getMinimumDelimiterLength(ctx.cell.align);
10090
- const spaceAfter = isNeedSpaceAfterContent(ctx.cell) ? " " : "";
10432
+ const minimumDelimiterLength = getMinimumDelimiterLength(cell.align);
10433
+ const spaceAfter = table.hasSpaceBetweenContentAndTrailingPipe(pipeIndex) ? " " : "";
10091
10434
  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;
10435
+ const delimiterPrefix = cell.align === "left" || cell.align === "center" ? ":" : "";
10436
+ const delimiterSuffix = (cell.align === "right" || cell.align === "center" ? ":" : "") + spaceAfter;
10094
10437
  const newDelimiter = "-".repeat(newLength - delimiterPrefix.length - delimiterSuffix.length);
10095
- return fixer.replaceTextRange([ctx.cell.delimiter.range[0], pipe.range[0]], delimiterPrefix + newDelimiter + delimiterSuffix);
10438
+ return fixer.replaceTextRange([cell.delimiter.range[0], pipe.range[0]], delimiterPrefix + newDelimiter + delimiterSuffix);
10096
10439
  /**
10097
10440
  * Fixer to remove spaces before the pipe
10098
10441
  */
@@ -10102,7 +10445,7 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10102
10445
  const spacesBeforePipeLength = beforePipe.length - trimmedBeforePipe.length;
10103
10446
  const widthBeforePipe = getTextWidth(trimmedBeforePipe);
10104
10447
  const newSpacesLength = expected - widthBeforePipe;
10105
- if (newSpacesLength < (ctx.pipeIndex > 0 && isNeedSpaceAfterContent(ctx.cell) ? 1 : 0)) return null;
10448
+ if (newSpacesLength < (table.hasSpaceBetweenContentAndTrailingPipe(pipeIndex) ? 1 : 0)) return null;
10106
10449
  return fixer.replaceTextRange([pipe.range[0] - spacesBeforePipeLength, pipe.range[0]], " ".repeat(newSpacesLength));
10107
10450
  }
10108
10451
  }
@@ -10110,106 +10453,47 @@ var table_pipe_alignment_default = createRule("table-pipe-alignment", {
10110
10453
  return false;
10111
10454
  }
10112
10455
  /**
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
10456
  * Get the minimum delimiter length based on alignment
10159
10457
  */
10160
10458
  function getMinimumDelimiterLength(align) {
10161
10459
  return align === "none" ? 1 : align === "center" ? 3 : 2;
10162
10460
  }
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
10461
  return { table(node) {
10205
10462
  const parsed = parseTable(sourceCode, node);
10206
10463
  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);
10464
+ verifyTablePipes(new TableContext(parsed));
10210
10465
  } };
10211
10466
  }
10212
10467
  });
10468
+ /**
10469
+ * Convert a parsed table row to row data
10470
+ */
10471
+ function parsedTableRowToRowData(parsedRow) {
10472
+ return { cells: parsedRow.cells.map((cell, index) => {
10473
+ const nextCell = index + 1 < parsedRow.cells.length ? parsedRow.cells[index + 1] : null;
10474
+ return {
10475
+ type: "cell",
10476
+ leadingPipe: cell.leadingPipe,
10477
+ content: cell.cell,
10478
+ trailingPipe: nextCell ? nextCell.leadingPipe : parsedRow.trailingPipe
10479
+ };
10480
+ }) };
10481
+ }
10482
+ /**
10483
+ * Convert a parsed table delimiter row to row data
10484
+ */
10485
+ function parsedTableDelimiterRowToRowData(parsedDelimiterRow) {
10486
+ return { cells: parsedDelimiterRow.delimiters.map((cell, index) => {
10487
+ const nextCell = index + 1 < parsedDelimiterRow.delimiters.length ? parsedDelimiterRow.delimiters[index + 1] : null;
10488
+ return {
10489
+ type: "delimiter",
10490
+ leadingPipe: cell.leadingPipe,
10491
+ delimiter: cell.delimiter,
10492
+ align: cell.delimiter.align,
10493
+ trailingPipe: nextCell ? nextCell.leadingPipe : parsedDelimiterRow.trailingPipe
10494
+ };
10495
+ }) };
10496
+ }
10213
10497
 
10214
10498
  //#endregion
10215
10499
  //#region src/rules/table-pipe-spacing.ts
@@ -10844,7 +11128,7 @@ const rules$1 = [
10844
11128
 
10845
11129
  //#endregion
10846
11130
  //#region src/configs/recommended.ts
10847
- var recommended_exports = __export({
11131
+ var recommended_exports = /* @__PURE__ */ __export({
10848
11132
  files: () => files$1,
10849
11133
  language: () => language$1,
10850
11134
  languageOptions: () => languageOptions$1,
@@ -10874,7 +11158,7 @@ const rules$3 = {
10874
11158
 
10875
11159
  //#endregion
10876
11160
  //#region src/configs/standard.ts
10877
- var standard_exports = __export({
11161
+ var standard_exports = /* @__PURE__ */ __export({
10878
11162
  files: () => files,
10879
11163
  language: () => language,
10880
11164
  languageOptions: () => languageOptions,
@@ -10935,12 +11219,12 @@ const rules$2 = {
10935
11219
 
10936
11220
  //#endregion
10937
11221
  //#region src/meta.ts
10938
- var meta_exports = __export({
11222
+ var meta_exports = /* @__PURE__ */ __export({
10939
11223
  name: () => name,
10940
11224
  version: () => version
10941
11225
  });
10942
11226
  const name = "eslint-plugin-markdown-preferences";
10943
- const version = "0.25.0";
11227
+ const version = "0.26.1";
10944
11228
 
10945
11229
  //#endregion
10946
11230
  //#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.1",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {