eslint-plugin-markdown-preferences 0.34.2 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -545,7 +545,7 @@ declare namespace meta_d_exports {
545
545
  export { name, version };
546
546
  }
547
547
  declare const name: "eslint-plugin-markdown-preferences";
548
- declare const version: "0.34.2";
548
+ declare const version: "0.35.0";
549
549
  //#endregion
550
550
  //#region src/language/ast-types.d.ts
551
551
  type Node = mdast.Node;
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import { __export } from "./chunk-CTAAG5j7.js";
2
2
  import stringWidth from "string-width";
3
3
  import emojiRegex from "emoji-regex-xs";
4
4
  import path from "node:path";
5
+ import diffSequence from "diff-sequences";
5
6
  import markdown from "@eslint/markdown";
6
7
  import { fromMarkdown } from "mdast-util-from-markdown";
7
8
  import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
@@ -10280,6 +10281,55 @@ var setext_heading_underline_length_default = createRule("setext-heading-underli
10280
10281
  }
10281
10282
  });
10282
10283
 
10284
+ //#endregion
10285
+ //#region src/utils/calc-shortest-edit-script.ts
10286
+ /**
10287
+ * Given the contents of two sequences, returns diff information.
10288
+ * @param a The first sequence.
10289
+ * @param b The second sequence.
10290
+ * @param [options] The options object.
10291
+ * @param [options.isEqual] A function that returns true if two elements are equal.
10292
+ * @returns An array of diff objects.
10293
+ */
10294
+ function calcShortestEditScript(a, b) {
10295
+ let aStartIndex = 0;
10296
+ let bStartIndex = 0;
10297
+ const result = [];
10298
+ (diffSequence.default || diffSequence)(a.length, b.length, (aIndex, bIndex) => a[aIndex] === b[bIndex], (nCommon, aCommon, bCommon) => {
10299
+ pushDelIns(aStartIndex, aCommon, bStartIndex, bCommon);
10300
+ aStartIndex = aCommon + nCommon;
10301
+ bStartIndex = bCommon + nCommon;
10302
+ if (nCommon > 0) for (let index = 0; index < nCommon; index++) {
10303
+ const elementA = a[aCommon + index];
10304
+ const elementB = b[bCommon + index];
10305
+ result.push({
10306
+ type: "common",
10307
+ a: elementA,
10308
+ b: elementB
10309
+ });
10310
+ }
10311
+ });
10312
+ pushDelIns(aStartIndex, a.length, bStartIndex, b.length);
10313
+ return result;
10314
+ /**
10315
+ * Pushes delete and insert sequences to the result.
10316
+ * @param aStart The start index of the delete sequence in `a`.
10317
+ * @param aEnd The end index of the delete sequence in `a`.
10318
+ * @param bStart The start index of the insert sequence in `b`.
10319
+ * @param bEnd The end index of the insert sequence in `b`.
10320
+ */
10321
+ function pushDelIns(aStart, aEnd, bStart, bEnd) {
10322
+ for (const element of a.slice(aStart, aEnd)) result.push({
10323
+ type: "delete",
10324
+ a: element
10325
+ });
10326
+ for (const element of b.slice(bStart, bEnd)) result.push({
10327
+ type: "insert",
10328
+ b: element
10329
+ });
10330
+ }
10331
+ }
10332
+
10283
10333
  //#endregion
10284
10334
  //#region src/rules/sort-definitions.ts
10285
10335
  var sort_definitions_default = createRule("sort-definitions", {
@@ -10327,7 +10377,10 @@ var sort_definitions_default = createRule("sort-definitions", {
10327
10377
  },
10328
10378
  additionalProperties: false
10329
10379
  }],
10330
- messages: { shouldBefore: "The definition '{{currentKey}}' should be before '{{prevKey}}'." }
10380
+ messages: {
10381
+ shouldBeBefore: "The definition '{{currentKey}}' should be before '{{prevKey}}'.",
10382
+ shouldBeAfter: "The definition '{{currentKey}}' should be after '{{nextKey}}'."
10383
+ }
10331
10384
  },
10332
10385
  create(context) {
10333
10386
  const sourceCode = context.sourceCode;
@@ -10348,12 +10401,12 @@ var sort_definitions_default = createRule("sort-definitions", {
10348
10401
  return `[^${node.identifier}]: ${childrenText}`;
10349
10402
  }
10350
10403
  /** Report */
10351
- function report(node, previousNode, definitions) {
10404
+ function reportFixToMoveUp(node, previousNode, definitions) {
10352
10405
  const currentKey = getDefinitionText(node);
10353
10406
  const prevKey = getDefinitionText(previousNode);
10354
10407
  context.report({
10355
10408
  node,
10356
- messageId: "shouldBefore",
10409
+ messageId: "shouldBeBefore",
10357
10410
  data: {
10358
10411
  currentKey,
10359
10412
  prevKey
@@ -10370,33 +10423,146 @@ var sort_definitions_default = createRule("sort-definitions", {
10370
10423
  ...previousNodes,
10371
10424
  ...after
10372
10425
  ];
10373
- return movedNodes.map((moveNode, index) => {
10374
- let text = sourceCode.getText(moveNode);
10375
- if (moveNode.type === "definition" && index > 0) {
10376
- if (movedNodes[index - 1].type === "footnoteDefinition") {
10377
- const footnoteLoc = sourceCode.getLoc(definitions[index - 1]);
10378
- if (sourceCode.getLoc(definitions[index]).start.line - footnoteLoc.end.line <= 1) text = `\n${text}`;
10379
- }
10380
- }
10381
- return fixer.replaceText(definitions[index], text);
10382
- });
10426
+ return fixReplaceDefinitions(fixer, definitions, movedNodes);
10427
+ }
10428
+ });
10429
+ }
10430
+ /** Report */
10431
+ function reportFixToMoveDown(node, nextNode, definitions) {
10432
+ const currentKey = getDefinitionText(node);
10433
+ const nextKey = getDefinitionText(nextNode);
10434
+ context.report({
10435
+ node,
10436
+ messageId: "shouldBeAfter",
10437
+ data: {
10438
+ currentKey,
10439
+ nextKey
10440
+ },
10441
+ fix(fixer) {
10442
+ const nextNodeIndex = definitions.indexOf(nextNode);
10443
+ const targetNodeIndex = definitions.indexOf(node);
10444
+ const nextNodes = definitions.slice(targetNodeIndex + 1, nextNodeIndex + 1);
10445
+ const before = definitions.slice(0, targetNodeIndex);
10446
+ const after = definitions.slice(nextNodeIndex + 1);
10447
+ const movedNodes = [
10448
+ ...before,
10449
+ ...nextNodes,
10450
+ node,
10451
+ ...after
10452
+ ];
10453
+ return fixReplaceDefinitions(fixer, definitions, movedNodes);
10383
10454
  }
10384
10455
  });
10385
10456
  }
10386
10457
  /**
10458
+ * Fix by replacing the definition array.
10459
+ */
10460
+ function fixReplaceDefinitions(fixer, definitions, newDefinitions) {
10461
+ const fixes = [];
10462
+ let removeLine = null;
10463
+ for (let index = 0; index < newDefinitions.length; index++) {
10464
+ const newNode = newDefinitions[index];
10465
+ const oldNode = definitions[index];
10466
+ let newText = sourceCode.getText(newNode);
10467
+ if (needLineBreakBetweenKind(index)) {
10468
+ const oldLoc = sourceCode.getLoc(oldNode);
10469
+ newText = `\n${sourceCode.lines[oldLoc.start.line - 1].slice(0, oldLoc.start.column - 1).replaceAll(/[^\s>]/g, " ")}${newText}`;
10470
+ } else if (newNode === oldNode) continue;
10471
+ fixes.push(fixer.replaceText(oldNode, newText));
10472
+ if (removeLine) {
10473
+ fixes.push(fixer.removeRange(removeLine));
10474
+ removeLine = null;
10475
+ }
10476
+ }
10477
+ return fixes;
10478
+ /**
10479
+ * Determine if a line break is needed between different kinds of definitions.
10480
+ */
10481
+ function needLineBreakBetweenKind(index) {
10482
+ if (index === 0) return false;
10483
+ if (newDefinitions[index].type !== "definition") return false;
10484
+ if (newDefinitions[index - 1].type !== "footnoteDefinition") return false;
10485
+ const oldPrevNode = definitions[index - 1];
10486
+ const oldNode = definitions[index];
10487
+ const footnoteLoc = sourceCode.getLoc(oldPrevNode);
10488
+ const linkLoc = sourceCode.getLoc(oldNode);
10489
+ if (linkLoc.start.line - footnoteLoc.end.line > 1) return false;
10490
+ if (index + 1 < newDefinitions.length) {
10491
+ if (newDefinitions[index + 1].type === "definition") {
10492
+ const oldNextNode = definitions[index + 1];
10493
+ const oldNextLoc = sourceCode.getLoc(oldNextNode);
10494
+ if (oldNextLoc.start.line - linkLoc.end.line > 1) {
10495
+ const parsedLine = getParsedLines(sourceCode).get(oldNextLoc.start.line - 1);
10496
+ if (!parsedLine.text.replaceAll(">", "").trim()) removeLine = parsedLine.range;
10497
+ }
10498
+ }
10499
+ }
10500
+ return true;
10501
+ }
10502
+ }
10503
+ /**
10387
10504
  * Verify definitions and footnote definitions.
10388
10505
  */
10389
10506
  function verify(definitions) {
10390
10507
  if (definitions.length === 0) return;
10391
- const validPreviousNodes = [];
10392
- for (const definition of definitions) {
10393
- if (option.ignore(definition)) continue;
10394
- const invalidPreviousNode = validPreviousNodes.find((previousNode) => option.compare(previousNode, definition) > 0);
10395
- if (invalidPreviousNode) {
10396
- report(definition, invalidPreviousNode, definitions);
10397
- continue;
10508
+ const sorted = [...definitions].sort(option.compare);
10509
+ const editScript = calcShortestEditScript(definitions, sorted);
10510
+ for (let index = 0; index < editScript.length; index++) {
10511
+ const edit = editScript[index];
10512
+ if (edit.type !== "delete") continue;
10513
+ const insertEditIndex = editScript.findIndex((e) => e.type === "insert" && e.b === edit.a);
10514
+ if (insertEditIndex === -1) continue;
10515
+ if (index < insertEditIndex) {
10516
+ const target = findInsertAfterTarget(edit.a, insertEditIndex);
10517
+ if (!target) continue;
10518
+ reportFixToMoveDown(edit.a, target, definitions);
10519
+ } else {
10520
+ const target = findInsertBeforeTarget(edit.a, insertEditIndex);
10521
+ if (!target) continue;
10522
+ reportFixToMoveUp(edit.a, target, definitions);
10523
+ }
10524
+ }
10525
+ /**
10526
+ * Find insert after target
10527
+ */
10528
+ function findInsertAfterTarget(def, insertEditIndex) {
10529
+ for (let index = insertEditIndex - 1; index >= 0; index--) {
10530
+ const edit = editScript[index];
10531
+ if (edit.type === "delete" && edit.a === def) break;
10532
+ if (edit.type !== "common") continue;
10533
+ return edit.a;
10534
+ }
10535
+ let lastTarget = null;
10536
+ for (let index = definitions.indexOf(def) + 1; index < definitions.length; index++) {
10537
+ const element = definitions[index];
10538
+ if (option.compare(element, def) <= 0) {
10539
+ lastTarget = element;
10540
+ continue;
10541
+ }
10542
+ return lastTarget;
10543
+ }
10544
+ return lastTarget;
10545
+ }
10546
+ /**
10547
+ * Find insert before target
10548
+ */
10549
+ function findInsertBeforeTarget(def, insertEditIndex) {
10550
+ for (let index = insertEditIndex + 1; index < editScript.length; index++) {
10551
+ const edit = editScript[index];
10552
+ if (edit.type === "delete" && edit.a === def) break;
10553
+ if (edit.type !== "common") continue;
10554
+ return edit.a;
10555
+ }
10556
+ let lastTarget = null;
10557
+ for (let index = definitions.indexOf(def) - 1; index >= 0; index--) {
10558
+ const element = definitions[index];
10559
+ if (option.compare(def, element) <= 0) {
10560
+ lastTarget = element;
10561
+ continue;
10562
+ }
10563
+ return lastTarget;
10398
10564
  }
10399
- validPreviousNodes.push(definition);
10565
+ return lastTarget;
10400
10566
  }
10401
10567
  }
10402
10568
  return {
@@ -10412,7 +10578,7 @@ var sort_definitions_default = createRule("sort-definitions", {
10412
10578
  if (node.type === "definition" || node.type === "footnoteDefinition") group.push(node);
10413
10579
  },
10414
10580
  "root:exit"() {
10415
- verify(group);
10581
+ verify(group.filter((definition) => !option.ignore(definition)));
10416
10582
  }
10417
10583
  };
10418
10584
  /** Parse options */
@@ -11990,7 +12156,7 @@ var meta_exports = /* @__PURE__ */ __export({
11990
12156
  version: () => version
11991
12157
  });
11992
12158
  const name = "eslint-plugin-markdown-preferences";
11993
- const version = "0.34.2";
12159
+ const version = "0.35.0";
11994
12160
 
11995
12161
  //#endregion
11996
12162
  //#region src/language/extensions/micromark-custom-container.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -62,6 +62,7 @@
62
62
  "eslint": ">=9.0.0"
63
63
  },
64
64
  "dependencies": {
65
+ "diff-sequences": "^29.6.3",
65
66
  "emoji-regex-xs": "^2.0.1",
66
67
  "mdast-util-from-markdown": "^2.0.2",
67
68
  "mdast-util-frontmatter": "^2.0.1",
@@ -104,7 +105,7 @@
104
105
  "eslint-plugin-json-schema-validator": "^5.3.1",
105
106
  "eslint-plugin-jsonc": "^2.19.1",
106
107
  "eslint-plugin-markdown": "^5.1.0",
107
- "eslint-plugin-markdown-links": "^0.5.0",
108
+ "eslint-plugin-markdown-links": "^0.6.0",
108
109
  "eslint-plugin-n": "^17.16.2",
109
110
  "eslint-plugin-node-dependencies": "^1.0.0",
110
111
  "eslint-plugin-prettier": "^5.2.3",