eslint-plugin-markdown-preferences 0.37.0 → 0.38.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
@@ -144,15 +144,16 @@ The rules with the following 💄 are included in the `standard` config.
144
144
 
145
145
  <!-- prettier-ignore-start -->
146
146
 
147
- | Rule ID | Description | Fixable | Config |
148
- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------- | :-----: | :----: |
149
- | [markdown-preferences/canonical-code-block-language](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html) | enforce canonical language names in code blocks | 🔧 | |
150
- | [markdown-preferences/emoji-notation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html) | enforce consistent emoji notation style in Markdown files. | 🔧 | |
151
- | [markdown-preferences/heading-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html) | enforce consistent casing in headings. | 🔧 | |
152
- | [markdown-preferences/ordered-list-marker-start](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-start.html) | enforce that ordered list markers start with 1 or 0 | 🔧 | 💄 |
153
- | [markdown-preferences/prefer-inline-code-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html) | enforce the use of inline code for specific words. | 🔧 | |
154
- | [markdown-preferences/prefer-linked-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html) | enforce the specified word to be a link. | 🔧 | |
155
- | [markdown-preferences/table-header-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html) | enforce consistent casing in table header cells. | 🔧 | |
147
+ | Rule ID | Description | Fixable | Config |
148
+ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------- | :-----: | :----: |
149
+ | [markdown-preferences/canonical-code-block-language](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html) | enforce canonical language names in code blocks | 🔧 | |
150
+ | [markdown-preferences/emoji-notation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html) | enforce consistent emoji notation style in Markdown files. | 🔧 | |
151
+ | [markdown-preferences/heading-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html) | enforce consistent casing in headings. | 🔧 | |
152
+ | [markdown-preferences/no-heading-trailing-punctuation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-heading-trailing-punctuation.html) | disallow trailing punctuation in headings. | | |
153
+ | [markdown-preferences/ordered-list-marker-start](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-start.html) | enforce that ordered list markers start with 1 or 0 | 🔧 | 💄 |
154
+ | [markdown-preferences/prefer-inline-code-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html) | enforce the use of inline code for specific words. | 🔧 | |
155
+ | [markdown-preferences/prefer-linked-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html) | enforce the specified word to be a link. | 🔧 | |
156
+ | [markdown-preferences/table-header-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html) | enforce consistent casing in table header cells. | 🔧 | |
156
157
 
157
158
  <!-- prettier-ignore-end -->
158
159
 
@@ -203,7 +204,7 @@ The rules with the following 💄 are included in the `standard` config.
203
204
  | [markdown-preferences/list-marker-alignment](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html) | enforce consistent alignment of list markers | 🔧 | ⭐💄 |
204
205
  | [markdown-preferences/no-multi-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-multi-spaces.html) | disallow multiple spaces | 🔧 | 💄 |
205
206
  | [markdown-preferences/no-multiple-empty-lines](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-multiple-empty-lines.html) | disallow multiple empty lines in Markdown files. | 🔧 | 💄 |
206
- | [markdown-preferences/no-tabs](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-tabs.html) | Disallow tab characters in Markdown files. | 🔧 | 💄 |
207
+ | [markdown-preferences/no-tabs](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-tabs.html) | disallow tab characters in Markdown files. | 🔧 | 💄 |
207
208
  | [markdown-preferences/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html) | disallow trailing whitespace at the end of lines in Markdown files. | 🔧 | 💄 |
208
209
  | [markdown-preferences/padded-custom-containers](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/padded-custom-containers.html) | disallow or require padding inside custom containers | 🔧 | 💄 |
209
210
  | [markdown-preferences/padding-line-between-blocks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/padding-line-between-blocks.html) | require or disallow padding lines between blocks | 🔧 | 💄 |
package/lib/index.d.ts CHANGED
@@ -140,6 +140,11 @@ interface RuleOptions {
140
140
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html
141
141
  */
142
142
  'markdown-preferences/list-marker-alignment'?: Linter.RuleEntry<MarkdownPreferencesListMarkerAlignment>;
143
+ /**
144
+ * disallow trailing punctuation in headings.
145
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-heading-trailing-punctuation.html
146
+ */
147
+ 'markdown-preferences/no-heading-trailing-punctuation'?: Linter.RuleEntry<MarkdownPreferencesNoHeadingTrailingPunctuation>;
143
148
  /**
144
149
  * disallow implicit block closing for fenced code blocks, math blocks, and custom containers
145
150
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-implicit-block-closing.html
@@ -161,7 +166,7 @@ interface RuleOptions {
161
166
  */
162
167
  'markdown-preferences/no-multiple-empty-lines'?: Linter.RuleEntry<MarkdownPreferencesNoMultipleEmptyLines>;
163
168
  /**
164
- * Disallow tab characters in Markdown files.
169
+ * disallow tab characters in Markdown files.
165
170
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-tabs.html
166
171
  */
167
172
  'markdown-preferences/no-tabs'?: Linter.RuleEntry<MarkdownPreferencesNoTabs>;
@@ -398,6 +403,32 @@ type MarkdownPreferencesLinkTitleStyle = [] | [{
398
403
  type MarkdownPreferencesListMarkerAlignment = [] | [{
399
404
  align?: ("left" | "right");
400
405
  }];
406
+ type MarkdownPreferencesNoHeadingTrailingPunctuation = [] | [{
407
+ punctuation?: (string | {
408
+ "1"?: string;
409
+ "2"?: string;
410
+ "3"?: string;
411
+ "4"?: string;
412
+ "5"?: string;
413
+ "6"?: string;
414
+ default?: string;
415
+ "1-2"?: string;
416
+ "1-3"?: string;
417
+ "1-4"?: string;
418
+ "1-5"?: string;
419
+ "1-6"?: string;
420
+ "2-3"?: string;
421
+ "2-4"?: string;
422
+ "2-5"?: string;
423
+ "2-6"?: string;
424
+ "3-4"?: string;
425
+ "3-5"?: string;
426
+ "3-6"?: string;
427
+ "4-5"?: string;
428
+ "4-6"?: string;
429
+ "5-6"?: string;
430
+ });
431
+ }];
401
432
  type MarkdownPreferencesNoMultipleEmptyLines = [] | [{
402
433
  max?: number;
403
434
  maxEOF?: number;
@@ -553,7 +584,7 @@ declare namespace meta_d_exports {
553
584
  export { name, version };
554
585
  }
555
586
  declare const name: "eslint-plugin-markdown-preferences";
556
- declare const version: "0.37.0";
587
+ declare const version: "0.38.0";
557
588
  //#endregion
558
589
  //#region src/language/ast-types.d.ts
559
590
  type Node = mdast.Node;
package/lib/index.js CHANGED
@@ -7690,6 +7690,153 @@ var list_marker_alignment_default = createRule("list-marker-alignment", {
7690
7690
  }
7691
7691
  });
7692
7692
 
7693
+ //#endregion
7694
+ //#region src/rules/no-heading-trailing-punctuation.ts
7695
+ const DEFAULT_PUNCTUATION = ".,;:!。、,;:!。、";
7696
+ const LEVEL_RANGE_MAP = {
7697
+ "1": [1, 1],
7698
+ "2": [2, 2],
7699
+ "3": [3, 3],
7700
+ "4": [4, 4],
7701
+ "5": [5, 5],
7702
+ "6": [6, 6],
7703
+ "1-2": [1, 2],
7704
+ "1-3": [1, 3],
7705
+ "1-4": [1, 4],
7706
+ "1-5": [1, 5],
7707
+ "1-6": [1, 6],
7708
+ "2-3": [2, 3],
7709
+ "2-4": [2, 4],
7710
+ "2-5": [2, 5],
7711
+ "2-6": [2, 6],
7712
+ "3-4": [3, 4],
7713
+ "3-5": [3, 5],
7714
+ "3-6": [3, 6],
7715
+ "4-5": [4, 5],
7716
+ "4-6": [4, 6],
7717
+ "5-6": [5, 6]
7718
+ };
7719
+ const HEADING_LEVELS = [
7720
+ 1,
7721
+ 2,
7722
+ 3,
7723
+ 4,
7724
+ 5,
7725
+ 6
7726
+ ];
7727
+ /**
7728
+ * Parse heading level ranges and build a map of level -> punctuation string
7729
+ */
7730
+ function buildPunctuationMap(option, segmenter$1) {
7731
+ if (typeof option === "string" || option === void 0) {
7732
+ const punctuation = option ?? DEFAULT_PUNCTUATION;
7733
+ const chars = new Set([...segmenter$1.segment(punctuation)].map((s) => s.segment));
7734
+ return Object.fromEntries(HEADING_LEVELS.map((level) => [level, chars]));
7735
+ }
7736
+ const defaultPunctuation = option.default ?? DEFAULT_PUNCTUATION;
7737
+ const defaultChars = new Set([...segmenter$1.segment(defaultPunctuation)].map((s) => s.segment));
7738
+ const map = Object.fromEntries(HEADING_LEVELS.map((level) => [level, defaultChars]));
7739
+ for (const [key, value] of Object.entries(option)) {
7740
+ if (key === "default" || value == null) continue;
7741
+ const range = LEVEL_RANGE_MAP[key];
7742
+ if (!range) continue;
7743
+ const chars = new Set([...segmenter$1.segment(value)].map((s) => s.segment));
7744
+ for (let level = range[0]; level <= range[1]; level++) map[level] = chars;
7745
+ }
7746
+ return map;
7747
+ }
7748
+ var no_heading_trailing_punctuation_default = createRule("no-heading-trailing-punctuation", {
7749
+ meta: {
7750
+ type: "suggestion",
7751
+ docs: {
7752
+ description: "disallow trailing punctuation in headings.",
7753
+ categories: [],
7754
+ listCategory: "Preference"
7755
+ },
7756
+ hasSuggestions: true,
7757
+ schema: [{
7758
+ type: "object",
7759
+ properties: { punctuation: { oneOf: [{
7760
+ type: "string",
7761
+ description: "String of punctuation characters to disallow at the end of headings."
7762
+ }, {
7763
+ type: "object",
7764
+ properties: {
7765
+ default: {
7766
+ type: "string",
7767
+ description: "Default punctuation characters for all heading levels."
7768
+ },
7769
+ ...Object.fromEntries(Object.entries(LEVEL_RANGE_MAP).map(([key, [start, end]]) => [key, {
7770
+ type: "string",
7771
+ description: start === end ? `Punctuation characters for heading level ${start}.` : `Punctuation characters for heading levels ${start} to ${end}.`
7772
+ }]))
7773
+ },
7774
+ additionalProperties: false
7775
+ }] } },
7776
+ additionalProperties: false
7777
+ }],
7778
+ messages: {
7779
+ trailingPunctuation: "Headings should not end with punctuation '{{char}}'.",
7780
+ removePunctuation: "Remove trailing punctuation '{{char}}'."
7781
+ }
7782
+ },
7783
+ create(context) {
7784
+ const sourceCode = context.sourceCode;
7785
+ const segmenter$1 = new Intl.Segmenter("en", { granularity: "grapheme" });
7786
+ const punctuationMap = buildPunctuationMap(context.options[0]?.punctuation, segmenter$1);
7787
+ return { heading(node) {
7788
+ if (!node.children.length) return;
7789
+ const punctuationChars = punctuationMap[node.depth];
7790
+ if (punctuationChars.size === 0) return;
7791
+ const lastTextNode = findLastTextNode(node.children);
7792
+ if (!lastTextNode) return;
7793
+ const trimmedText = sourceCode.getText(lastTextNode).trimEnd();
7794
+ if (!trimmedText.length) return;
7795
+ const segments = [...segmenter$1.segment(trimmedText)];
7796
+ const lastSegment = segments[segments.length - 1];
7797
+ if (!lastSegment) return;
7798
+ const lastChar = lastSegment.segment;
7799
+ if (!punctuationChars.has(lastChar)) return;
7800
+ const [nodeStart] = sourceCode.getRange(lastTextNode);
7801
+ const punctuationIndex = nodeStart + lastSegment.index;
7802
+ context.report({
7803
+ node: lastTextNode,
7804
+ messageId: "trailingPunctuation",
7805
+ data: { char: lastChar },
7806
+ loc: {
7807
+ start: sourceCode.getLocFromIndex(punctuationIndex),
7808
+ end: sourceCode.getLocFromIndex(punctuationIndex + lastChar.length)
7809
+ },
7810
+ suggest: [{
7811
+ messageId: "removePunctuation",
7812
+ data: { char: lastChar },
7813
+ fix(fixer) {
7814
+ return fixer.removeRange([punctuationIndex, punctuationIndex + lastChar.length]);
7815
+ }
7816
+ }]
7817
+ });
7818
+ } };
7819
+ /**
7820
+ * Find the last text node within a node's children.
7821
+ * Recursively traverses nested structures (emphasis, strong, link, etc.).
7822
+ */
7823
+ function findLastTextNode(children) {
7824
+ for (let i = children.length - 1; i >= 0; i--) {
7825
+ const child = children[i];
7826
+ if (child.type === "text") {
7827
+ if (child.value.trim()) return child;
7828
+ continue;
7829
+ }
7830
+ if ("children" in child && Array.isArray(child.children)) {
7831
+ const nestedLast = findLastTextNode(child.children);
7832
+ if (nestedLast) return nestedLast;
7833
+ }
7834
+ }
7835
+ return null;
7836
+ }
7837
+ }
7838
+ });
7839
+
7693
7840
  //#endregion
7694
7841
  //#region src/rules/no-implicit-block-closing.ts
7695
7842
  var no_implicit_block_closing_default = createRule("no-implicit-block-closing", {
@@ -8504,7 +8651,7 @@ var no_tabs_default = createRule("no-tabs", {
8504
8651
  meta: {
8505
8652
  type: "layout",
8506
8653
  docs: {
8507
- description: "Disallow tab characters in Markdown files.",
8654
+ description: "disallow tab characters in Markdown files.",
8508
8655
  categories: ["standard"],
8509
8656
  listCategory: "Whitespace"
8510
8657
  },
@@ -12260,6 +12407,7 @@ const rules$1 = [
12260
12407
  link_paren_spacing_default,
12261
12408
  link_title_style_default,
12262
12409
  list_marker_alignment_default,
12410
+ no_heading_trailing_punctuation_default,
12263
12411
  no_implicit_block_closing_default,
12264
12412
  no_laziness_blockquotes_default,
12265
12413
  no_multi_spaces_default,
@@ -12393,7 +12541,7 @@ var meta_exports = /* @__PURE__ */ __export({
12393
12541
  version: () => version
12394
12542
  });
12395
12543
  const name = "eslint-plugin-markdown-preferences";
12396
- const version = "0.37.0";
12544
+ const version = "0.38.0";
12397
12545
 
12398
12546
  //#endregion
12399
12547
  //#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.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -126,7 +126,7 @@
126
126
  "stylelint-config-recommended-vue": "^1.6.0",
127
127
  "stylelint-config-standard": "^39.0.0",
128
128
  "stylelint-config-standard-vue": "^1.0.0",
129
- "tsdown": "^0.16.0",
129
+ "tsdown": "^0.17.0",
130
130
  "tsx": "^4.19.3",
131
131
  "twoslash-eslint": "^0.3.1",
132
132
  "type-fest": "^5.0.0",