@blumintinc/eslint-plugin-blumint 1.20.191 → 1.20.192

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.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.191',
226
+ version: '1.20.192',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -2,32 +2,104 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceTypescriptMarkdownCodeBlocks = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
- const FENCE = '```';
6
- function isFenceLine(text, fenceIndex, indent) {
7
- const lineEnd = text.indexOf('\n', fenceIndex);
8
- const afterFence = lineEnd === -1
9
- ? text.slice(fenceIndex + FENCE.length)
10
- : text.slice(fenceIndex + FENCE.length, lineEnd);
11
- return (afterFence.trim().length === 0 &&
12
- text.slice(text.lastIndexOf('\n', fenceIndex - 1) + 1, fenceIndex) ===
13
- indent);
5
+ const BACKTICK = '`';
6
+ const TILDE = '~';
7
+ /** Only a run of exactly three backticks is labelable; longer runs are declined. */
8
+ const LABELABLE_FENCE_LENGTH = 3;
9
+ /** CommonMark opens a fenced block on a run of three or more of either marker. */
10
+ const MIN_FENCE_LENGTH = 3;
11
+ /**
12
+ * CommonMark allows a fence to be indented at most three columns. At four or
13
+ * more the line opens an indented code block, so its backticks are literal
14
+ * document content that must never be rewritten.
15
+ */
16
+ const MAX_FENCE_INDENT_COLUMNS = 3;
17
+ /** CommonMark advances a tab to the next multiple of four when measuring indent. */
18
+ const TAB_STOP = 4;
19
+ function splitLines(text) {
20
+ const lines = [];
21
+ let start = 0;
22
+ for (;;) {
23
+ const terminator = text.indexOf('\n', start);
24
+ const end = terminator === -1 ? text.length : terminator;
25
+ lines.push({ start, end, text: text.slice(start, end) });
26
+ if (terminator === -1) {
27
+ return lines;
28
+ }
29
+ start = terminator + 1;
30
+ }
14
31
  }
15
- function findClosingFence(text, startIndex, indent) {
16
- let searchIndex = startIndex;
17
- while (searchIndex < text.length) {
18
- const candidate = text.indexOf(FENCE, searchIndex);
19
- if (candidate === -1) {
20
- return null;
32
+ /**
33
+ * Reads the fence a line opens, or null when the line cannot open one.
34
+ * Indentation is measured in columns rather than characters so that a tab is
35
+ * treated as the four-column indent CommonMark says it is.
36
+ */
37
+ function readFence(line) {
38
+ let indentColumns = 0;
39
+ let offset = 0;
40
+ while (offset < line.text.length) {
41
+ const char = line.text[offset];
42
+ if (char === ' ') {
43
+ indentColumns += 1;
21
44
  }
22
- if (isFenceLine(text, candidate, indent)) {
23
- return candidate;
45
+ else if (char === '\t') {
46
+ indentColumns += TAB_STOP - (indentColumns % TAB_STOP);
47
+ }
48
+ else {
49
+ break;
50
+ }
51
+ offset += 1;
52
+ }
53
+ if (indentColumns > MAX_FENCE_INDENT_COLUMNS) {
54
+ return null;
55
+ }
56
+ const marker = line.text[offset];
57
+ if (marker !== BACKTICK && marker !== TILDE) {
58
+ return null;
59
+ }
60
+ let runLength = 0;
61
+ while (line.text[offset + runLength] === marker) {
62
+ runLength += 1;
63
+ }
64
+ if (runLength < MIN_FENCE_LENGTH) {
65
+ return null;
66
+ }
67
+ return {
68
+ runStart: line.start + offset,
69
+ marker,
70
+ runLength,
71
+ indent: line.text.slice(0, offset),
72
+ infoString: line.text.slice(offset + runLength),
73
+ };
74
+ }
75
+ /**
76
+ * The line a block closes on, per CommonMark: a run of at least the opening
77
+ * length, of the SAME marker, at a fence indent, with nothing but whitespace
78
+ * after it. This locates the block's END, which is a separate question from
79
+ * whether the rule is willing to LABEL it.
80
+ */
81
+ function findFenceCloser(lines, fromLine, opener) {
82
+ for (let index = fromLine; index < lines.length; index++) {
83
+ const fence = readFence(lines[index]);
84
+ if (fence !== null &&
85
+ fence.marker === opener.marker &&
86
+ fence.runLength >= opener.runLength &&
87
+ fence.infoString.trim().length === 0) {
88
+ return { line: index, fence };
24
89
  }
25
- searchIndex = candidate + FENCE.length;
26
90
  }
27
91
  return null;
28
92
  }
29
- function isIndentOnly(value) {
30
- return /^[\t ]*$/.test(value);
93
+ /**
94
+ * The rule labels only a block it can delimit exactly: three backticks closed
95
+ * by three backticks at the same indent. A longer closing run or a differently
96
+ * indented one is left unlabeled by design — but the block is still SKIPPED
97
+ * whole, because a block the rule declines to label is a block it must not
98
+ * read.
99
+ */
100
+ function isExactlyDelimited(opener, closer) {
101
+ return (closer.runLength === LABELABLE_FENCE_LENGTH &&
102
+ closer.indent === opener.indent);
31
103
  }
32
104
  exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
33
105
  name: 'enforce-typescript-markdown-code-blocks',
@@ -53,45 +125,51 @@ exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
53
125
  Program() {
54
126
  const sourceCode = context.sourceCode;
55
127
  const text = sourceCode.getText();
128
+ const lines = splitLines(text);
56
129
  let index = 0;
57
- while (index < text.length) {
58
- const openingFence = text.indexOf(FENCE, index);
59
- if (openingFence === -1) {
60
- break;
61
- }
62
- const lineStart = text.lastIndexOf('\n', openingFence - 1) + 1;
63
- const indent = text.slice(lineStart, openingFence);
64
- if (!isIndentOnly(indent)) {
65
- index = openingFence + FENCE.length;
130
+ while (index < lines.length) {
131
+ const openingLine = lines[index];
132
+ const fence = readFence(openingLine);
133
+ if (fence === null) {
134
+ index += 1;
66
135
  continue;
67
136
  }
68
- const lineEnd = text.indexOf('\n', openingFence + FENCE.length);
69
- if (lineEnd === -1) {
70
- break;
71
- }
72
- const infoString = text.slice(openingFence + FENCE.length, lineEnd);
73
- const closingFence = findClosingFence(text, lineEnd + 1, indent);
74
- if (closingFence === null) {
75
- index = lineEnd + 1;
76
- continue;
137
+ const closing = findFenceCloser(lines, index + 1, fence);
138
+ // An unclosed fence runs to the end of the file, so everything after
139
+ // it is the block's literal content and there is nothing left to
140
+ // scan. Resuming on the next line would read the block's interior.
141
+ if (closing === null) {
142
+ return;
77
143
  }
78
- const content = text.slice(lineEnd + 1, closingFence);
79
- const hasContent = content.trim().length > 0;
80
- const hasLanguage = infoString.trim().length > 0;
81
- if (!hasLanguage && hasContent) {
82
- const locStart = sourceCode.getLocFromIndex(openingFence);
83
- const hasCarriageReturn = lineEnd > 0 && text[lineEnd - 1] === '\r';
84
- context.report({
85
- loc: {
86
- start: locStart,
87
- end: sourceCode.getLocFromIndex(lineEnd),
88
- },
89
- messageId: 'missingLanguageSpecifier',
90
- data: { line: locStart.line },
91
- fix: (fixer) => fixer.replaceTextRange([openingFence + FENCE.length, lineEnd], hasCarriageReturn ? 'typescript\r' : 'typescript'),
92
- });
144
+ // A tilde fence and a run of four or more backticks are blocks this
145
+ // rule declines to label, and declining to label a block means
146
+ // declining to read it: its interior is literal text, triple
147
+ // backticks included. So is the interior of a triple-backtick block
148
+ // this rule cannot delimit exactly. Every one of them is skipped from
149
+ // its opening line to past its closing line.
150
+ const labelable = fence.marker === BACKTICK &&
151
+ fence.runLength === LABELABLE_FENCE_LENGTH &&
152
+ isExactlyDelimited(fence, closing.fence);
153
+ if (labelable) {
154
+ const content = text.slice(openingLine.end + 1, lines[closing.line].start);
155
+ const hasContent = content.trim().length > 0;
156
+ const hasLanguage = fence.infoString.trim().length > 0;
157
+ if (!hasLanguage && hasContent) {
158
+ const lineEnd = openingLine.end;
159
+ const locStart = sourceCode.getLocFromIndex(fence.runStart);
160
+ const hasCarriageReturn = lineEnd > 0 && text[lineEnd - 1] === '\r';
161
+ context.report({
162
+ loc: {
163
+ start: locStart,
164
+ end: sourceCode.getLocFromIndex(lineEnd),
165
+ },
166
+ messageId: 'missingLanguageSpecifier',
167
+ data: { line: locStart.line },
168
+ fix: (fixer) => fixer.replaceTextRange([fence.runStart + LABELABLE_FENCE_LENGTH, lineEnd], hasCarriageReturn ? 'typescript\r' : 'typescript'),
169
+ });
170
+ }
93
171
  }
94
- index = closingFence + FENCE.length;
172
+ index = closing.line + 1;
95
173
  }
96
174
  },
97
175
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.191",
3
+ "version": "1.20.192",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.192",
4
+ "date": "2026-08-30T01:20:08.891Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-typescript-markdown-code-blocks",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2213
11
+ ],
12
+ "summary": "never write inside a declined block (closes #2213)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.191",
4
18
  "date": "2026-08-29T19:38:08.123Z",