eslint-cannoli-plugins 1.0.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.
Files changed (38) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +75 -0
  3. package/dist/plugins/markdown/enforce-link-convention.d.ts +7 -0
  4. package/dist/plugins/markdown/enforce-link-convention.d.ts.map +1 -0
  5. package/dist/plugins/markdown/enforce-link-convention.js +120 -0
  6. package/dist/plugins/markdown/enforce-link-convention.js.map +1 -0
  7. package/dist/plugins/markdown/index.ts +5 -0
  8. package/dist/plugins/markdown/inline-math-alone-on-line.d.ts +10 -0
  9. package/dist/plugins/markdown/inline-math-alone-on-line.d.ts.map +1 -0
  10. package/dist/plugins/markdown/inline-math-alone-on-line.js +70 -0
  11. package/dist/plugins/markdown/inline-math-alone-on-line.js.map +1 -0
  12. package/dist/plugins/markdown/no-h1-headers.d.ts +3 -0
  13. package/dist/plugins/markdown/no-h1-headers.d.ts.map +1 -0
  14. package/dist/plugins/markdown/no-h1-headers.js +41 -0
  15. package/dist/plugins/markdown/no-h1-headers.js.map +1 -0
  16. package/dist/plugins/markdown/require-blank-line-after-html.d.ts +3 -0
  17. package/dist/plugins/markdown/require-blank-line-after-html.d.ts.map +1 -0
  18. package/dist/plugins/markdown/require-blank-line-after-html.js +164 -0
  19. package/dist/plugins/markdown/require-blank-line-after-html.js.map +1 -0
  20. package/dist/plugins/markdown/require-display-math-formatting.d.ts +12 -0
  21. package/dist/plugins/markdown/require-display-math-formatting.d.ts.map +1 -0
  22. package/dist/plugins/markdown/require-display-math-formatting.js +108 -0
  23. package/dist/plugins/markdown/require-display-math-formatting.js.map +1 -0
  24. package/dist/plugins/markdown/require-frontmatter.d.ts +3 -0
  25. package/dist/plugins/markdown/require-frontmatter.d.ts.map +1 -0
  26. package/dist/plugins/markdown/require-frontmatter.js +39 -0
  27. package/dist/plugins/markdown/require-frontmatter.js.map +1 -0
  28. package/dist/plugins/markdown/utils.d.ts +41 -0
  29. package/dist/plugins/markdown/utils.d.ts.map +1 -0
  30. package/dist/plugins/markdown/utils.js +181 -0
  31. package/dist/plugins/markdown/utils.js.map +1 -0
  32. package/dist/plugins/markdown/validate-latex-delimiters.d.ts +8 -0
  33. package/dist/plugins/markdown/validate-latex-delimiters.d.ts.map +1 -0
  34. package/dist/plugins/markdown/validate-latex-delimiters.js +83 -0
  35. package/dist/plugins/markdown/validate-latex-delimiters.js.map +1 -0
  36. package/index.d.ts +7 -0
  37. package/index.js +7 -0
  38. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # ESLint Markdown Cannoli Plugins
2
+
3
+ A collection of ESLint plugins for linting Markdown files with custom rules.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev eslint-md-cannoli-plugins @eslint/markdown eslint
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add the plugins to your ESLint configuration (`eslint.config.js` or `eslint.config.ts`):
14
+
15
+ ```javascript
16
+ import markdown from "@eslint/markdown";
17
+ import {
18
+ enforceLinkConvention,
19
+ inlineMathAloneOnLine,
20
+ noH1Headers,
21
+ requireBlankLineAfterHtml,
22
+ requireDisplayMathFormatting,
23
+ requireFrontmatter,
24
+ validateLatexDelimiters,
25
+ } from "eslint-md-cannoli-plugins";
26
+
27
+ export default [
28
+ {
29
+ files: ["**/*.md"],
30
+ plugins: {
31
+ markdown,
32
+ cannoli: {
33
+ rules: {
34
+ "require-frontmatter": requireFrontmatter,
35
+ "no-h1-headers": noH1Headers,
36
+ "require-blank-line-after-html": requireBlankLineAfterHtml,
37
+ "require-display-math-formatting": requireDisplayMathFormatting,
38
+ "inline-math-alone-on-line": inlineMathAloneOnLine,
39
+ "validate-latex-delimiters": validateLatexDelimiters,
40
+ "enforce-link-convention": enforceLinkConvention,
41
+ },
42
+ },
43
+ },
44
+ language: "markdown/gfm",
45
+ languageOptions: {
46
+ parser: "@eslint/markdown",
47
+ frontmatter: "yaml",
48
+ },
49
+ extends: ["markdown/recommended"],
50
+ rules: {
51
+ "cannoli/require-frontmatter": "error",
52
+ "cannoli/no-h1-headers": "error",
53
+ "cannoli/require-blank-line-after-html": "error",
54
+ "cannoli/require-display-math-formatting": "error",
55
+ "cannoli/inline-math-alone-on-line": "warn",
56
+ "cannoli/validate-latex-delimiters": "error",
57
+ "cannoli/enforce-link-convention": "warn",
58
+ },
59
+ },
60
+ ];
61
+ ```
62
+
63
+ ## Available Rules
64
+
65
+ - **require-frontmatter** - Ensure markdown files have frontmatter
66
+ - **no-h1-headers** - Disallow H1 headers (use frontmatter title instead)
67
+ - **require-blank-line-after-html** - Require blank lines after HTML blocks
68
+ - **require-display-math-formatting** - Enforce proper display math formatting
69
+ - **inline-math-alone-on-line** - Ensure inline math equations are on their own line
70
+ - **validate-latex-delimiters** - Validate LaTeX delimiter usage
71
+ - **enforce-link-convention** - Enforce link naming conventions
72
+
73
+ ## License
74
+
75
+ ISC
@@ -0,0 +1,7 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Enforce link convention: all lowercase and no offending characters.
4
+ * Suggests slugified version as the proper convention.
5
+ */
6
+ export declare const enforceLinkConvention: Rule.RuleModule;
7
+ //# sourceMappingURL=enforce-link-convention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforce-link-convention.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/enforce-link-convention.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAInC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC,UAqIxC,CAAC"}
@@ -0,0 +1,120 @@
1
+ import { FencedCodeBlockTracker, getFrontmatterEndLine, isValidLinkFormat, slugify } from "./utils.js";
2
+ /**
3
+ * Enforce link convention: all lowercase and no offending characters.
4
+ * Suggests slugified version as the proper convention.
5
+ */
6
+ export const enforceLinkConvention = {
7
+ meta: {
8
+ type: "suggestion",
9
+ docs: {
10
+ description: "Enforce that links are all lowercase and contain only valid characters (no spaces, special chars, etc.)",
11
+ },
12
+ },
13
+ create(context) {
14
+ let alreadyProcessed = false;
15
+ return {
16
+ "*": (node) => {
17
+ if (alreadyProcessed || node.type !== "root")
18
+ return;
19
+ alreadyProcessed = true;
20
+ const sourceCode = context.sourceCode;
21
+ if (!sourceCode)
22
+ return;
23
+ const text = sourceCode.getText();
24
+ const lines = text.split("\n");
25
+ const frontmatterEndLine = getFrontmatterEndLine(text);
26
+ const codeBlockTracker = new FencedCodeBlockTracker(text);
27
+ for (let i = frontmatterEndLine; i < lines.length; i++) {
28
+ const line = lines[i];
29
+ if (codeBlockTracker.isLineInFencedCodeBlock(i)) {
30
+ continue;
31
+ }
32
+ // Match markdown links: [text](link) and [text]: link
33
+ const inlineLinksRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
34
+ const referenceLinkRegex = /^\s*\[([^\]]+)\]:\s*(.+?)(?:\s+"[^"]*")?$/;
35
+ // Check inline links [text](link)
36
+ let inlineMatch;
37
+ while ((inlineMatch = inlineLinksRegex.exec(line)) !== null) {
38
+ const link = inlineMatch[2];
39
+ // Calculate column position: find the opening ( and start from the first char after it
40
+ const fullMatch = inlineMatch[0];
41
+ const openParenIndex = fullMatch.indexOf("(");
42
+ const linkColumn = inlineMatch.index + openParenIndex + 1;
43
+ checkLink(link, i, linkColumn, context);
44
+ }
45
+ // Check reference links [text]: link
46
+ const refMatch = line.match(referenceLinkRegex);
47
+ if (refMatch) {
48
+ const link = refMatch[2].trim();
49
+ // Calculate exact column position where the link starts
50
+ const linkPosition = line.indexOf(link);
51
+ checkLink(link, i, linkPosition, context);
52
+ }
53
+ }
54
+ function slugifyPath(path) {
55
+ // Split by slash and slugify each component separately to preserve directory structure
56
+ const parts = path.split("/");
57
+ return parts
58
+ .map((part) => {
59
+ // Don't slugify empty parts or special directory references
60
+ if (part === "" || part === "." || part === "..") {
61
+ return part;
62
+ }
63
+ return slugify(part);
64
+ })
65
+ .join("/");
66
+ }
67
+ function extractExtension(path) {
68
+ // Find the last slash to separate directory from filename
69
+ const lastSlashIndex = path.lastIndexOf("/");
70
+ const filename = path.substring(lastSlashIndex + 1);
71
+ // Find the last dot in the filename
72
+ const lastDotIndex = filename.lastIndexOf(".");
73
+ // Only extract extension if dot is not at the start (e.g., not ".gitignore")
74
+ if (lastDotIndex > 0) {
75
+ const basename = filename.substring(0, lastDotIndex);
76
+ const extension = filename.substring(lastDotIndex);
77
+ const dirPart = lastSlashIndex >= 0 ? path.substring(0, lastSlashIndex + 1) : "";
78
+ return {
79
+ basename: dirPart + basename,
80
+ extension: extension,
81
+ };
82
+ }
83
+ return { basename: path, extension: "" };
84
+ }
85
+ function checkLink(link, lineIndex, columnIndex, ctx) {
86
+ // Skip external links (any URL protocol) and anchors
87
+ const externalProtocols = [
88
+ "http://",
89
+ "https://",
90
+ "mailto:",
91
+ "ftp://",
92
+ "ftps://",
93
+ "sftp://",
94
+ "ssh://",
95
+ "tel:",
96
+ "sms:",
97
+ "data:",
98
+ ];
99
+ if (externalProtocols.some((proto) => link.startsWith(proto)) || link.startsWith("#")) {
100
+ return;
101
+ }
102
+ // Check if link is valid format
103
+ if (!isValidLinkFormat(link)) {
104
+ // Separate basename and extension to preserve the extension
105
+ const { basename, extension } = extractExtension(link);
106
+ const suggestion = slugifyPath(basename) + extension;
107
+ ctx.report({
108
+ loc: {
109
+ start: { line: lineIndex + 1, column: columnIndex },
110
+ end: { line: lineIndex + 1, column: columnIndex + link.length },
111
+ },
112
+ message: `Link contains invalid characters or uppercase letters. Links should be lowercase and contain only alphanumeric characters, hyphens, underscores, slashes, and dots. Suggested format: "${suggestion}"`,
113
+ });
114
+ }
115
+ }
116
+ },
117
+ };
118
+ },
119
+ };
120
+ //# sourceMappingURL=enforce-link-convention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforce-link-convention.js","sourceRoot":"","sources":["../../../src/plugins/markdown/enforce-link-convention.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file Automatically generated by barrelsby.
3
+ */
4
+
5
+ export * from "./index";
@@ -0,0 +1,10 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Detect inline math expressions ($...$) that exist alone on their own line.
4
+ * This usually indicates the expression was meant to be display/block math ($$...$$).
5
+ *
6
+ * Bad: $x = 2$
7
+ * Good: $$x = 2$$ OR inline text $x = 2$ more text
8
+ */
9
+ export declare const inlineMathAloneOnLine: Rule.RuleModule;
10
+ //# sourceMappingURL=inline-math-alone-on-line.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-math-alone-on-line.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/inline-math-alone-on-line.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAInC;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC,UA0ExC,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { FencedCodeBlockTracker, getFrontmatterEndLine } from "./utils.js";
2
+ /**
3
+ * Detect inline math expressions ($...$) that exist alone on their own line.
4
+ * This usually indicates the expression was meant to be display/block math ($$...$$).
5
+ *
6
+ * Bad: $x = 2$
7
+ * Good: $$x = 2$$ OR inline text $x = 2$ more text
8
+ */
9
+ export const inlineMathAloneOnLine = {
10
+ meta: {
11
+ type: "suggestion",
12
+ fixable: "code",
13
+ docs: {
14
+ description: "Detect inline math expressions that exist alone on their own line (likely meant to be display math)",
15
+ },
16
+ },
17
+ create(context) {
18
+ let alreadyProcessed = false;
19
+ return {
20
+ "*": (node) => {
21
+ if (alreadyProcessed || node.type !== "root")
22
+ return;
23
+ alreadyProcessed = true;
24
+ const sourceCode = context.sourceCode;
25
+ if (!sourceCode)
26
+ return;
27
+ const text = sourceCode.getText();
28
+ const lines = text.split("\n");
29
+ const frontmatterEndLine = getFrontmatterEndLine(text);
30
+ const codeBlockTracker = new FencedCodeBlockTracker(text);
31
+ for (let i = frontmatterEndLine; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ // Skip lines inside fenced code blocks
34
+ if (codeBlockTracker.isLineInFencedCodeBlock(i)) {
35
+ continue;
36
+ }
37
+ const trimmed = line.trim();
38
+ // Skip empty lines
39
+ if (!trimmed) {
40
+ continue;
41
+ }
42
+ // Check for display math ($$) - not what we're looking for
43
+ if (trimmed.includes("$$")) {
44
+ continue;
45
+ }
46
+ // Check for inline math ($...$)
47
+ const singleDollarRegex = /^\$[^$]+\$\s*$/;
48
+ const match = trimmed.match(singleDollarRegex);
49
+ if (match) {
50
+ // Found inline math alone on a line
51
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
52
+ const expression = trimmed.slice(1, -1); // Remove the $...$
53
+ context.report({
54
+ loc: { line: i + 1, column: 0 },
55
+ message: "Inline math expression found alone on its own line. Did you mean to use display math ($$...$$) instead?",
56
+ fix(fixer) {
57
+ // Convert from $...$ to $$...$$
58
+ const replacement = `${indent}$$${expression}$$`;
59
+ const lineStart = sourceCode.getIndexFromLoc({ line: i + 1, column: 1 });
60
+ const lineEnd = sourceCode.getIndexFromLoc({ line: i + 1, column: line.length + 1 });
61
+ return fixer.replaceTextRange([lineStart, lineEnd], replacement);
62
+ },
63
+ });
64
+ }
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
70
+ //# sourceMappingURL=inline-math-alone-on-line.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-math-alone-on-line.js","sourceRoot":"","sources":["../../../src/plugins/markdown/inline-math-alone-on-line.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const noH1Headers: Rule.RuleModule;
3
+ //# sourceMappingURL=no-h1-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-h1-headers.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/no-h1-headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,WAAW,EAAE,IAAI,CAAC,UA6C9B,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { FencedCodeBlockTracker, getFrontmatterEndLine } from "./utils.js";
2
+ export const noH1Headers = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Disallow h1 headers in markdown files since the frontmatter title already serves that purpose",
7
+ },
8
+ },
9
+ create(context) {
10
+ let alreadyProcessed = false;
11
+ return {
12
+ "*": (node) => {
13
+ if (alreadyProcessed || node.type !== "root")
14
+ return;
15
+ alreadyProcessed = true;
16
+ const sourceCode = context.sourceCode;
17
+ if (!sourceCode)
18
+ return;
19
+ const text = sourceCode.getText();
20
+ const lines = text.split("\n");
21
+ const frontmatterEndLine = getFrontmatterEndLine(text);
22
+ const codeBlockTracker = new FencedCodeBlockTracker(text);
23
+ // check for h1 headers
24
+ for (let i = frontmatterEndLine; i < lines.length; i++) {
25
+ const line = lines[i];
26
+ // skip lines inside fenced code blocks
27
+ if (codeBlockTracker.isLineInFencedCodeBlock(i)) {
28
+ continue;
29
+ }
30
+ if (/^#\s+/.test(line)) {
31
+ context.report({
32
+ loc: { line: i + 1, column: 0 },
33
+ message: "H1 headings are not allowed in Astro markdown files; use the frontmatter title field instead",
34
+ });
35
+ }
36
+ }
37
+ },
38
+ };
39
+ },
40
+ };
41
+ //# sourceMappingURL=no-h1-headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-h1-headers.js","sourceRoot":"","sources":["../../../src/plugins/markdown/no-h1-headers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,CAAC,MAAM,WAAW,GAAoB;IAC1C,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,+FAA+F;SAClG;KACO;IACV,MAAM,CAAC,OAAyB;QAC9B,IAAI,gBAAgB,GAAG,KAAK,CAAC;QAE7B,OAAO;YACL,GAAG,EAAE,CAAC,IAAe,EAAE,EAAE;gBACvB,IAAI,gBAAgB,IAAK,IAAoC,CAAC,IAAI,KAAK,MAAM;oBAAE,OAAO;gBAEtF,gBAAgB,GAAG,IAAI,CAAC;gBAExB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;gBACtC,IAAI,CAAC,UAAU;oBAAE,OAAO;gBAExB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;gBAClC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC/B,MAAM,kBAAkB,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;gBACvD,MAAM,gBAAgB,GAAG,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC;gBAE1D,uBAAuB;gBACvB,KAAK,IAAI,CAAC,GAAG,kBAAkB,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACvD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;oBAEtB,uCAAuC;oBACvC,IAAI,gBAAgB,CAAC,uBAAuB,CAAC,CAAC,CAAC,EAAE,CAAC;wBAChD,SAAS;oBACX,CAAC;oBAED,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBACvB,OAAO,CAAC,MAAM,CAAC;4BACb,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;4BAC/B,OAAO,EACL,8FAA8F;yBACjG,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const requireBlankLineAfterHtml: Rule.RuleModule;
3
+ //# sourceMappingURL=require-blank-line-after-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-blank-line-after-html.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/require-blank-line-after-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAuCnC,eAAO,MAAM,yBAAyB,EAAE,IAAI,CAAC,UAyJ5C,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { FencedCodeBlockTracker, getFrontmatterEndLine } from "./utils.js";
2
+ /**
3
+ * Check if a line contains a problematic markdown construct that requires a blank line before/after HTML.
4
+ * Block-level constructs like headings are excluded since they don't interfere with HTML rendering.
5
+ */
6
+ function isProblematicMarkdownConstruct(line) {
7
+ const trimmed = line.trim();
8
+ // Blockquote
9
+ if (/^>/.test(trimmed))
10
+ return true;
11
+ // List (-, *, +)
12
+ if (/^[-*+]\s/.test(trimmed))
13
+ return true;
14
+ // Image
15
+ if (/^!\[/.test(trimmed))
16
+ return true;
17
+ // Link (starts with [)
18
+ if (/^\[/.test(trimmed))
19
+ return true;
20
+ // Inline code (starts with backtick)
21
+ if (/^`/.test(trimmed))
22
+ return true;
23
+ // Bold/Strong (**, __)
24
+ if (/^\*\*/.test(trimmed) || /^__/.test(trimmed))
25
+ return true;
26
+ // Emphasis (*, _) - but not list markers (those are checked above with space)
27
+ if (/^\*[^\s*]/.test(trimmed) || /^_[^\s_]/.test(trimmed))
28
+ return true;
29
+ // Do NOT include headings - they're block-level structural elements
30
+ // Do NOT include normal text - it's not problematic
31
+ // Do NOT flag HTML tags here - they're handled separately
32
+ return false;
33
+ }
34
+ export const requireBlankLineAfterHtml = {
35
+ meta: {
36
+ type: "problem",
37
+ docs: {
38
+ description: "Require blank line after closing HTML tags unless followed by another HTML tag",
39
+ },
40
+ },
41
+ create(context) {
42
+ let alreadyProcessed = false;
43
+ return {
44
+ "*": (node) => {
45
+ if (alreadyProcessed || node.type !== "root")
46
+ return;
47
+ alreadyProcessed = true;
48
+ const sourceCode = context.sourceCode;
49
+ if (!sourceCode)
50
+ return;
51
+ const text = sourceCode.getText();
52
+ const lines = text.split("\n");
53
+ const frontmatterEndLine = getFrontmatterEndLine(text);
54
+ const codeBlockTracker = new FencedCodeBlockTracker(text);
55
+ for (let i = frontmatterEndLine; i < lines.length; i++) {
56
+ const line = lines[i];
57
+ // Skip lines inside fenced code blocks
58
+ if (codeBlockTracker.isLineInFencedCodeBlock(i)) {
59
+ continue;
60
+ }
61
+ // Check if current line is a problematic markdown construct
62
+ if (isProblematicMarkdownConstruct(line)) {
63
+ // Look back to find previous non-empty line
64
+ let prevNonEmptyLine = null;
65
+ let prevNonEmptyLineNum = -1;
66
+ for (let j = i - 1; j >= frontmatterEndLine; j--) {
67
+ if (lines[j].trim()) {
68
+ prevNonEmptyLine = lines[j];
69
+ prevNonEmptyLineNum = j;
70
+ break;
71
+ }
72
+ }
73
+ // If previous line is directly before (no blank line)
74
+ if (prevNonEmptyLine && prevNonEmptyLineNum === i - 1) {
75
+ // Check if previous line is an HTML closing tag
76
+ const prevIsHtmlClosingTag = /<\/\w+>\s*$/.test(prevNonEmptyLine.trim());
77
+ if (prevIsHtmlClosingTag) {
78
+ context.report({
79
+ loc: { line: i + 1, column: 0 },
80
+ message: `Blank line required before markdown construct when preceded by closing HTML tag`,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ // Check for HTML tags (opening or closing)
86
+ const htmlTagMatch = line.match(/^(.*)(<\/?(\w+)>)(.*)$/);
87
+ if (htmlTagMatch) {
88
+ const beforeTag = htmlTagMatch[1];
89
+ const htmlTag = htmlTagMatch[2];
90
+ const afterTag = htmlTagMatch[4];
91
+ // If there's content before the tag on the same line, it's inline - check after
92
+ if (beforeTag.trim()) {
93
+ continue;
94
+ }
95
+ // If there's content after the tag on the same line, no check needed
96
+ if (afterTag.trim()) {
97
+ continue;
98
+ }
99
+ // Look backward to find the previous non-empty line
100
+ let prevNonEmptyLine = null;
101
+ let prevNonEmptyLineNum = -1;
102
+ for (let j = i - 1; j >= frontmatterEndLine; j--) {
103
+ if (lines[j].trim()) {
104
+ prevNonEmptyLine = lines[j];
105
+ prevNonEmptyLineNum = j;
106
+ break;
107
+ }
108
+ }
109
+ // Check if previous line is a problematic markdown construct (not headings)
110
+ if (prevNonEmptyLine && prevNonEmptyLineNum === i - 1) {
111
+ // Previous line is directly before (no blank line)
112
+ const prevIsProblematicConstruct = isProblematicMarkdownConstruct(prevNonEmptyLine);
113
+ if (prevIsProblematicConstruct) {
114
+ context.report({
115
+ loc: { line: i + 1, column: 0 },
116
+ message: `Blank line required before HTML tag "${htmlTag}" when preceded by markdown construct`,
117
+ });
118
+ // Continue to next iteration to avoid reporting multiple errors
119
+ continue;
120
+ }
121
+ }
122
+ // Look ahead to find the next non-empty line (for closing tags)
123
+ let nextNonEmptyLine = null;
124
+ let nextNonEmptyLineNum = -1;
125
+ for (let j = i + 1; j < lines.length; j++) {
126
+ if (lines[j].trim()) {
127
+ nextNonEmptyLine = lines[j];
128
+ nextNonEmptyLineNum = j;
129
+ break;
130
+ }
131
+ }
132
+ // If no next line found, no error
133
+ if (!nextNonEmptyLine) {
134
+ continue;
135
+ }
136
+ // Only check blank line after for closing tags
137
+ if (/<\//.test(htmlTag)) {
138
+ // Check if the next line is another HTML tag
139
+ const isNextLineHtmlTag = /^<[a-zA-Z/]/.test(nextNonEmptyLine.trim());
140
+ // If next line is an HTML tag, blank line is optional (exception)
141
+ if (isNextLineHtmlTag) {
142
+ continue;
143
+ }
144
+ // If there are blank lines between closing tag and next content, no error
145
+ if (nextNonEmptyLineNum > i + 1) {
146
+ continue;
147
+ }
148
+ // Check if the next line is a problematic markdown construct that requires blank line
149
+ const nextIsProblematicConstruct = isProblematicMarkdownConstruct(nextNonEmptyLine);
150
+ // If next line is a problematic markdown construct, require blank line (report error)
151
+ if (nextIsProblematicConstruct) {
152
+ context.report({
153
+ loc: { line: i + 1, column: 0 },
154
+ message: `Blank line required after closing HTML tag "${htmlTag}" when followed by markdown construct`,
155
+ });
156
+ }
157
+ }
158
+ }
159
+ }
160
+ },
161
+ };
162
+ },
163
+ };
164
+ //# sourceMappingURL=require-blank-line-after-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-blank-line-after-html.js","sourceRoot":"","sources":["../../../src/plugins/markdown/require-blank-line-after-html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Check if a line contains display math ($$) that's not properly formatted.
4
+ * Display math should have $$ on its own line, not inline with the expression.
5
+ *
6
+ * Bad: $$2+2$$
7
+ * Good: $$
8
+ * 2+2
9
+ * $$
10
+ */
11
+ export declare const requireDisplayMathFormatting: Rule.RuleModule;
12
+ //# sourceMappingURL=require-display-math-formatting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-display-math-formatting.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/require-display-math-formatting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAInC;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,EAAE,IAAI,CAAC,UA8G/C,CAAC"}
@@ -0,0 +1,108 @@
1
+ import { FencedCodeBlockTracker, getFrontmatterEndLine } from "./utils.js";
2
+ /**
3
+ * Check if a line contains display math ($$) that's not properly formatted.
4
+ * Display math should have $$ on its own line, not inline with the expression.
5
+ *
6
+ * Bad: $$2+2$$
7
+ * Good: $$
8
+ * 2+2
9
+ * $$
10
+ */
11
+ export const requireDisplayMathFormatting = {
12
+ meta: {
13
+ type: "problem",
14
+ fixable: "code",
15
+ docs: {
16
+ description: "Require display math ($$) to be on separate lines from the expression",
17
+ },
18
+ },
19
+ create(context) {
20
+ let alreadyProcessed = false;
21
+ return {
22
+ "*": (node) => {
23
+ if (alreadyProcessed || node.type !== "root")
24
+ return;
25
+ alreadyProcessed = true;
26
+ const sourceCode = context.sourceCode;
27
+ if (!sourceCode)
28
+ return;
29
+ const text = sourceCode.getText();
30
+ const lines = text.split("\n");
31
+ const frontmatterEndLine = getFrontmatterEndLine(text);
32
+ const codeBlockTracker = new FencedCodeBlockTracker(text);
33
+ for (let i = frontmatterEndLine; i < lines.length; i++) {
34
+ const line = lines[i];
35
+ // Skip lines inside fenced code blocks
36
+ if (codeBlockTracker.isLineInFencedCodeBlock(i)) {
37
+ continue;
38
+ }
39
+ const trimmed = line.trim();
40
+ // Remove inline code (backtick-enclosed content) before checking for $$
41
+ const withoutInlineCode = trimmed.replace(/`[^`]*`/g, "");
42
+ // Check for display math markers
43
+ if (!withoutInlineCode.includes("$$")) {
44
+ continue;
45
+ }
46
+ // Count $$ occurrences in the line (excluding those in inline code)
47
+ const dollarCount = (withoutInlineCode.match(/\$\$/g) || []).length;
48
+ // If there's only one $$, it might be an opening or closing on its own line (good)
49
+ if (dollarCount === 1) {
50
+ // Check if the line is ONLY $$ (possibly with whitespace)
51
+ const isOnlyDollarSigns = /^\$\$\s*$/.test(withoutInlineCode);
52
+ if (isOnlyDollarSigns) {
53
+ // Good: $$ is on its own line
54
+ continue;
55
+ }
56
+ // If there's content on the same line as $$, that's bad
57
+ // Example: "$$2+2" or "expression$$"
58
+ if (withoutInlineCode !== "$$") {
59
+ context.report({
60
+ loc: { line: i + 1, column: 0 },
61
+ message: "Display math ($$) should be on its own line, separate from the expression",
62
+ });
63
+ }
64
+ }
65
+ else if (dollarCount === 2) {
66
+ // Two $$ on the same line
67
+ // Check if they form a complete math block on one line: $$expression$$
68
+ const doubleRegex = /^(\s*)\$\$(.*?)\$\$\s*$/;
69
+ const match = withoutInlineCode.match(doubleRegex);
70
+ if (match) {
71
+ // The entire line is $$something$$, which is bad
72
+ const indent = match[1];
73
+ const expression = match[2];
74
+ context.report({
75
+ loc: { line: i + 1, column: 0 },
76
+ message: 'Display math delimiters ($$) must be on separate lines. Use:\n$$\nexpression\n$$',
77
+ fix(fixer) {
78
+ // Build the replacement text - just the three lines without trailing newline
79
+ // The line itself will have its trailing newline preserved by ESLint
80
+ const replacement = `${indent}$$\n${indent}${expression}\n${indent}$$`;
81
+ // Get the start of the line and the end (before the newline)
82
+ const lineStart = sourceCode.getIndexFromLoc({ line: i + 1, column: 1 });
83
+ const lineEnd = sourceCode.getIndexFromLoc({ line: i + 1, column: line.length + 1 });
84
+ return fixer.replaceTextRange([lineStart, lineEnd], replacement);
85
+ },
86
+ });
87
+ }
88
+ else {
89
+ // Two $$ but not in the expected format - could be overlapping or weird
90
+ context.report({
91
+ loc: { line: i + 1, column: 0 },
92
+ message: "Malformed display math notation. Display math ($$) should be on separate lines",
93
+ });
94
+ }
95
+ }
96
+ else if (dollarCount > 2) {
97
+ // Multiple $$ pairs on the same line - likely multiple math expressions or malformed
98
+ context.report({
99
+ loc: { line: i + 1, column: 0 },
100
+ message: "Multiple display math expressions ($$) found on same line. Each should be on separate lines",
101
+ });
102
+ }
103
+ }
104
+ },
105
+ };
106
+ },
107
+ };
108
+ //# sourceMappingURL=require-display-math-formatting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-display-math-formatting.js","sourceRoot":"","sources":["../../../src/plugins/markdown/require-display-math-formatting.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const requireFrontmatter: Rule.RuleModule;
3
+ //# sourceMappingURL=require-frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-frontmatter.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/require-frontmatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,kBAAkB,EAAE,IAAI,CAAC,UAsCrC,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { extractFrontmatter } from "./utils.js";
2
+ export const requireFrontmatter = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Require title field in frontmatter",
7
+ },
8
+ },
9
+ create(context) {
10
+ let alreadyProcessed = false;
11
+ return {
12
+ "*": (node) => {
13
+ if (alreadyProcessed || node.type !== "root")
14
+ return;
15
+ alreadyProcessed = true;
16
+ const sourceCode = context.sourceCode;
17
+ if (!sourceCode)
18
+ return;
19
+ const text = sourceCode.getText();
20
+ const frontmatter = extractFrontmatter(text);
21
+ if (frontmatter) {
22
+ if (!/^\s*title\s*:/m.test(frontmatter)) {
23
+ context.report({
24
+ loc: { line: 1, column: 0 },
25
+ message: "Missing required 'title' field in frontmatter",
26
+ });
27
+ }
28
+ }
29
+ else {
30
+ context.report({
31
+ loc: { line: 1, column: 0 },
32
+ message: "Missing frontmatter",
33
+ });
34
+ }
35
+ },
36
+ };
37
+ },
38
+ };
39
+ //# sourceMappingURL=require-frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-frontmatter.js","sourceRoot":"","sources":["../../../src/plugins/markdown/require-frontmatter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,CAAC,MAAM,kBAAkB,GAAoB;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,oCAAoC;SAClD;KACO;IACV,MAAM,CAAC,OAAyB;QAC9B,IAAI,gBAAgB,GAAG,KAAK,CAAC;QAE7B,OAAO;YACL,GAAG,EAAE,CAAC,IAAe,EAAE,EAAE;gBACvB,IAAI,gBAAgB,IAAK,IAAoC,CAAC,IAAI,KAAK,MAAM;oBAAE,OAAO;gBAEtF,gBAAgB,GAAG,IAAI,CAAC;gBAExB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;gBACtC,IAAI,CAAC,UAAU;oBAAE,OAAO;gBAExB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;gBAClC,MAAM,WAAW,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;gBAE7C,IAAI,WAAW,EAAE,CAAC;oBAChB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;wBACxC,OAAO,CAAC,MAAM,CAAC;4BACb,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;4BAC3B,OAAO,EAAE,+CAA+C;yBACzD,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,MAAM,CAAC;wBACb,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;wBAC3B,OAAO,EAAE,qBAAqB;qBAC/B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Returns the frontmatter string (without the --- delimiters) if it exists
3
+ */
4
+ export declare function extractFrontmatter(text: string): string | null;
5
+ /**
6
+ * Find the line number where frontmatter ends after the closing --- delimiter
7
+ */
8
+ export declare function getFrontmatterEndLine(text: string): number;
9
+ export declare class FencedCodeBlockTracker {
10
+ private ranges;
11
+ private currentRangeIndex;
12
+ constructor(text: string);
13
+ /**
14
+ * Check if a given line index falls within any fenced code block range.
15
+ */
16
+ isLineInFencedCodeBlock(lineIndex: number): boolean;
17
+ }
18
+ /**
19
+ * Remove escaped LaTeX delimiters from text before counting
20
+ * Handles both inline ($) and display ($$) math delimiters
21
+ */
22
+ export declare function removeEscapedDelimiters(text: string): string;
23
+ /**
24
+ * Count occurrences of a delimiter in text (after removing escaped versions)
25
+ * Returns the count and tracks the line where the count becomes odd
26
+ */
27
+ export interface DelimiterCount {
28
+ count: number;
29
+ firstUnclosedLine: number;
30
+ }
31
+ /**
32
+ * Slugify a filename by converting to lowercase, replacing spaces/underscores/slashes with hyphens,
33
+ * removing special characters, and normalizing diacritics to ASCII.
34
+ * Ported from Python: scripts/organizer/util/__init__.py
35
+ */
36
+ export declare function slugify(name: string): string;
37
+ /**
38
+ * Check if a string contains only valid local file link characters (lowercase, alphanumeric, hyphens, underscores, slashes, dots)
39
+ */
40
+ export declare function isValidLinkFormat(link: string): boolean;
41
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAc1D;AAyCD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,iBAAiB,CAAa;gBAE1B,IAAI,EAAE,MAAM;IAIxB;;OAEG;IACH,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;CAyBpD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+B5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIvD"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Returns the frontmatter string (without the --- delimiters) if it exists
3
+ */
4
+ export function extractFrontmatter(text) {
5
+ const frontmatterMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
6
+ return frontmatterMatch ? frontmatterMatch[1] : null;
7
+ }
8
+ /**
9
+ * Find the line number where frontmatter ends after the closing --- delimiter
10
+ */
11
+ export function getFrontmatterEndLine(text) {
12
+ const lines = text.split("\n");
13
+ if (lines[0] !== "---") {
14
+ return 0;
15
+ }
16
+ for (let i = 1; i < lines.length; i++) {
17
+ if (lines[i] === "---") {
18
+ return i + 1;
19
+ }
20
+ }
21
+ return 0; // no closing --- found
22
+ }
23
+ /**
24
+ * Find all fenced code block ranges in the text
25
+ * Returns an array of [startLine, endLine] pairs (0-indexed)
26
+ * Correctly handles indented code blocks (e.g., nested under list items)
27
+ * because fence delimiters are detected anywhere on the line after trimming
28
+ */
29
+ function getFencedCodeBlockRanges(text) {
30
+ const lines = text.split("\n");
31
+ const ranges = [];
32
+ let inCodeBlock = false;
33
+ let blockStartLine = -1;
34
+ for (let i = 0; i < lines.length; i++) {
35
+ const line = lines[i];
36
+ const trimmed = line.trim();
37
+ // Check for fence delimiter regardless of indentation level
38
+ if (/^(```|~~~)/.test(trimmed)) {
39
+ if (inCodeBlock) {
40
+ // End of code block
41
+ ranges.push([blockStartLine, i]);
42
+ inCodeBlock = false;
43
+ }
44
+ else {
45
+ // Start of code block
46
+ inCodeBlock = true;
47
+ blockStartLine = i;
48
+ }
49
+ }
50
+ }
51
+ // If there's an unclosed code block at EOF, still track it
52
+ if (inCodeBlock) {
53
+ ranges.push([blockStartLine, lines.length - 1]);
54
+ }
55
+ return ranges;
56
+ }
57
+ export class FencedCodeBlockTracker {
58
+ constructor(text) {
59
+ this.currentRangeIndex = 0;
60
+ this.ranges = getFencedCodeBlockRanges(text);
61
+ }
62
+ /**
63
+ * Check if a given line index falls within any fenced code block range.
64
+ */
65
+ isLineInFencedCodeBlock(lineIndex) {
66
+ if (this.ranges.length === 0) {
67
+ return false;
68
+ }
69
+ while (this.currentRangeIndex < this.ranges.length) {
70
+ const [start, end] = this.ranges[this.currentRangeIndex];
71
+ if (lineIndex < start) {
72
+ return false;
73
+ }
74
+ if (lineIndex >= start && lineIndex <= end) {
75
+ return true;
76
+ }
77
+ if (lineIndex > end) {
78
+ this.currentRangeIndex++;
79
+ continue;
80
+ }
81
+ }
82
+ // lineIndex is past all ranges
83
+ return false;
84
+ }
85
+ }
86
+ /**
87
+ * Remove escaped LaTeX delimiters from text before counting
88
+ * Handles both inline ($) and display ($$) math delimiters
89
+ */
90
+ export function removeEscapedDelimiters(text) {
91
+ // Remove escaped dollar signs (\$) before counting delimiters
92
+ return text.replace(/\\\$/g, "");
93
+ }
94
+ /**
95
+ * Slugify a filename by converting to lowercase, replacing spaces/underscores/slashes with hyphens,
96
+ * removing special characters, and normalizing diacritics to ASCII.
97
+ * Ported from Python: scripts/organizer/util/__init__.py
98
+ */
99
+ export function slugify(name) {
100
+ let slug = name;
101
+ // replace C++ with 'cpp'
102
+ slug = slug.replace(/(_+)?c\+\+/gi, "$1cpp");
103
+ slug = slug.replace("---", "___");
104
+ // Replace & with ' and '
105
+ slug = slug.replace(/&/g, " and ");
106
+ // Replace spaces, underscores, and forward slashes with hyphens
107
+ slug = slug.replace(/ /g, "-").replace(/\//g, "-");
108
+ // Normalize Unicode (decompose diacritics)
109
+ slug = slug
110
+ .normalize("NFKD")
111
+ .split("")
112
+ .filter((char) => char.charCodeAt(0) < 128)
113
+ .join("");
114
+ // Replace non-alphanumeric characters (except hyphen, underscore, plus) with hyphens
115
+ slug = slug.replace(/[^-+_a-zA-Z0-9]/g, "-");
116
+ // Collapse multiple consecutive hyphens into a single hyphen
117
+ slug = slug.replace(/-+/g, "-");
118
+ slug = slug.replace(/-*(_+)-*/g, "$1");
119
+ slug = normalizeCamelPascalCase(slug);
120
+ // Strip leading/trailing hyphens and lowercase
121
+ return slug.replace(/^-+|-+$/g, "").toLowerCase();
122
+ }
123
+ /**
124
+ * Check if a string contains only valid local file link characters (lowercase, alphanumeric, hyphens, underscores, slashes, dots)
125
+ */
126
+ export function isValidLinkFormat(link) {
127
+ // Allow: lowercase alphanumeric, hyphens, underscores, slashes, dots, hash, question mark
128
+ // For local files, we don't allow & or = (those are query params for external URLs)
129
+ return /^[a-z0-9\-_/.#?]+$/.test(link);
130
+ }
131
+ /**
132
+ * Insert dashes at camelCase transitions for preparation before slugifying.
133
+ * Ported from Python: scripts/organizer/util/__init__.py
134
+ */
135
+ function normalizeCamelPascalCase(text) {
136
+ if (text.length < 2) {
137
+ return text;
138
+ }
139
+ // Handle PascalCase starting with single uppercase letter
140
+ if (text[0] === text[0].toUpperCase() && text[1] === text[1].toLowerCase()) {
141
+ text = text[0].toLowerCase() + text.slice(1);
142
+ }
143
+ const r1 = /([a-z0-9]+)([A-Z]+[a-z0-9]*)/;
144
+ const r2 = /([A-Z]+)([A-Z][a-z])/;
145
+ const m1 = text.match(r1);
146
+ const m2 = text.match(r2);
147
+ if (!m1 && !m2) {
148
+ return text;
149
+ }
150
+ let result = "";
151
+ if (m1) {
152
+ const [left, right] = splitBetweenTwoGroups(m1, text);
153
+ const part1 = normalizeCamelPascalCase(left);
154
+ const part2 = normalizeCamelPascalCase(right);
155
+ result = part1 + "-" + part2;
156
+ }
157
+ else if (m2) {
158
+ const [left, right] = splitBetweenTwoGroups(m2, text);
159
+ const part1 = normalizeCamelPascalCase(left);
160
+ const part2 = normalizeCamelPascalCase(right);
161
+ result = part1 + "-" + part2;
162
+ }
163
+ if (result === "") {
164
+ throw new Error("Result string should not be empty");
165
+ }
166
+ return result;
167
+ }
168
+ /**
169
+ * Split original regex match string into (left, right) at the boundary between 2 adjacent capture groups.
170
+ * Ported from Python: scripts/organizer/util/__init__.py
171
+ */
172
+ function splitBetweenTwoGroups(match, originalString) {
173
+ if (match.index === undefined) {
174
+ throw new Error("Match index is undefined");
175
+ }
176
+ // Find the boundary between the two groups
177
+ const firstGroupLength = match[1].length;
178
+ const boundary = match.index + firstGroupLength;
179
+ return [originalString.slice(0, boundary), originalString.slice(boundary)];
180
+ }
181
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/plugins/markdown/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Validate that LaTeX delimiters ($...$) and ($$...$$) are balanced.
4
+ * Catches broken math rendering from mismatched or unclosed delimiters.
5
+ * Works with the original source text to properly handle escaped sequences.
6
+ */
7
+ export declare const validateLatexDelimiters: Rule.RuleModule;
8
+ //# sourceMappingURL=validate-latex-delimiters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-latex-delimiters.d.ts","sourceRoot":"","sources":["../../../src/plugins/markdown/validate-latex-delimiters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAKnC;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,IAAI,CAAC,UAkF1C,CAAC"}
@@ -0,0 +1,83 @@
1
+ import { removeEscapedDelimiters } from "./utils.js";
2
+ import { FencedCodeBlockTracker } from "./utils.js";
3
+ /**
4
+ * Validate that LaTeX delimiters ($...$) and ($$...$$) are balanced.
5
+ * Catches broken math rendering from mismatched or unclosed delimiters.
6
+ * Works with the original source text to properly handle escaped sequences.
7
+ */
8
+ export const validateLatexDelimiters = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description: "Validate that LaTeX delimiters ($...$ and $$...$$) are balanced and properly paired",
13
+ },
14
+ },
15
+ create(context) {
16
+ let alreadyProcessed = false;
17
+ return {
18
+ "*": (node) => {
19
+ if (alreadyProcessed || node.type !== "root")
20
+ return;
21
+ alreadyProcessed = true;
22
+ const sourceCode = context.sourceCode;
23
+ if (!sourceCode)
24
+ return;
25
+ const text = sourceCode.getText();
26
+ const lines = text.split("\n");
27
+ const codeBlockTracker = new FencedCodeBlockTracker(text);
28
+ // Track unclosed delimiters
29
+ let inlineDelimiterCount = 0;
30
+ let displayDelimiterCount = 0;
31
+ let inlineStartLine = -1;
32
+ let displayStartLine = -1;
33
+ for (let i = 0; i < lines.length; i++) {
34
+ const line = lines[i];
35
+ // Skip lines inside fenced code blocks
36
+ if (codeBlockTracker.isLineInFencedCodeBlock(i)) {
37
+ continue;
38
+ }
39
+ // Remove escaped dollar signs before counting
40
+ const withoutEscaped = removeEscapedDelimiters(line);
41
+ // Count $$ first (to avoid counting them as two $)
42
+ const displayMatches = withoutEscaped.match(/\$\$/g);
43
+ if (displayMatches) {
44
+ displayDelimiterCount += displayMatches.length;
45
+ if (displayDelimiterCount % 2 === 1 && displayStartLine === -1) {
46
+ displayStartLine = i;
47
+ }
48
+ else if (displayDelimiterCount % 2 === 0) {
49
+ displayStartLine = -1;
50
+ }
51
+ }
52
+ // Count remaining $ (after removing $$)
53
+ const withoutDisplay = withoutEscaped.replace(/\$\$/g, "");
54
+ const inlineMatches = withoutDisplay.match(/\$/g);
55
+ if (inlineMatches) {
56
+ inlineDelimiterCount += inlineMatches.length;
57
+ if (inlineDelimiterCount % 2 === 1 && inlineStartLine === -1) {
58
+ inlineStartLine = i;
59
+ }
60
+ else if (inlineDelimiterCount % 2 === 0) {
61
+ inlineStartLine = -1;
62
+ }
63
+ }
64
+ }
65
+ // Report unclosed display math
66
+ if (displayDelimiterCount % 2 !== 0) {
67
+ context.report({
68
+ loc: { line: displayStartLine + 1, column: 0 },
69
+ message: `Unclosed display math delimiter ($$). Expected closing $$ to match the opening on line ${displayStartLine + 1}.`,
70
+ });
71
+ }
72
+ // Report unclosed inline math
73
+ if (inlineDelimiterCount % 2 !== 0) {
74
+ context.report({
75
+ loc: { line: inlineStartLine + 1, column: 0 },
76
+ message: `Unclosed inline math delimiter ($). Expected closing $ to match the opening on line ${inlineStartLine + 1}.`,
77
+ });
78
+ }
79
+ },
80
+ };
81
+ },
82
+ };
83
+ //# sourceMappingURL=validate-latex-delimiters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-latex-delimiters.js","sourceRoot":"","sources":["../../../src/plugins/markdown/validate-latex-delimiters.ts"],"names":[],"mappings":"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"}
package/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { enforceLinkConvention } from "./dist/plugins/markdown/enforce-link-convention";
2
+ export { inlineMathAloneOnLine } from "./dist/plugins/markdown/inline-math-alone-on-line";
3
+ export { noH1Headers } from "./dist/plugins/markdown/no-h1-headers";
4
+ export { requireBlankLineAfterHtml } from "./dist/plugins/markdown/require-blank-line-after-html";
5
+ export { requireDisplayMathFormatting } from "./dist/plugins/markdown/require-display-math-formatting";
6
+ export { requireFrontmatter } from "./dist/plugins/markdown/require-frontmatter";
7
+ export { validateLatexDelimiters } from "./dist/plugins/markdown/validate-latex-delimiters";
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { enforceLinkConvention } from "./dist/plugins/markdown/enforce-link-convention.js";
2
+ export { inlineMathAloneOnLine } from "./dist/plugins/markdown/inline-math-alone-on-line.js";
3
+ export { noH1Headers } from "./dist/plugins/markdown/no-h1-headers.js";
4
+ export { requireBlankLineAfterHtml } from "./dist/plugins/markdown/require-blank-line-after-html.js";
5
+ export { requireDisplayMathFormatting } from "./dist/plugins/markdown/require-display-math-formatting.js";
6
+ export { requireFrontmatter } from "./dist/plugins/markdown/require-frontmatter.js";
7
+ export { validateLatexDelimiters } from "./dist/plugins/markdown/validate-latex-delimiters.js";
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "eslint-cannoli-plugins",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "generate-exports": "node scripts/generate-exports.js",
9
+ "prepublishOnly": "npm run build && npm run generate-exports",
10
+ "test": "python3 test_eslint_plugins.py",
11
+ "validate-rules": "python3 src/tests/validate_rules.py"
12
+ },
13
+ "author": "OccasionalCoderByTrade",
14
+ "license": "ISC",
15
+ "description": "ESLint plugins for linting Markdown files",
16
+ "keywords": [
17
+ "eslint",
18
+ "eslint-plugin",
19
+ "markdown",
20
+ "linting"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/OccasionalCoderByTrade/eslint-md-plugins"
25
+ },
26
+ "files": [
27
+ "dist/",
28
+ "index.js",
29
+ "index.d.ts"
30
+ ],
31
+ "devDependencies": {
32
+ "@eslint/js": "^10.0.1",
33
+ "@eslint/markdown": "^7.5.1",
34
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
35
+ "eslint": "^10.0.3",
36
+ "globals": "^17.4.0",
37
+ "jiti": "^2.6.1",
38
+ "prettier": "^3.8.1",
39
+ "remark-parse": "^11.0.0",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "peerDependencies": {
43
+ "@eslint/markdown": "^7.0.0",
44
+ "eslint": "^10.0.3"
45
+ }
46
+ }