mdrefine 0.2.0-rc.2 → 0.2.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/CHANGELOG.md +577 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +189 -16
- package/README.md +100 -154
- package/SECURITY.md +15 -13
- package/config.schema.json +91 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +247 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/main.js +270 -150
- package/dist/cli/main.js.map +1 -1
- package/dist/config/load.js +67 -6
- package/dist/config/load.js.map +1 -1
- package/dist/config/problems.d.ts +7 -0
- package/dist/config/problems.js +208 -0
- package/dist/config/problems.js.map +1 -0
- package/dist/config/resolve.d.ts +42 -109
- package/dist/config/resolve.js +193 -52
- package/dist/config/resolve.js.map +1 -1
- package/dist/core/directives.d.ts +7 -0
- package/dist/core/directives.js +25 -13
- package/dist/core/directives.js.map +1 -1
- package/dist/core/engine.d.ts +42 -4
- package/dist/core/engine.js +518 -79
- package/dist/core/engine.js.map +1 -1
- package/dist/core/frozen.d.ts +6 -0
- package/dist/core/frozen.js +15 -0
- package/dist/core/frozen.js.map +1 -0
- package/dist/core/scope.d.ts +30 -0
- package/dist/core/scope.js +122 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +202 -23
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -0
- package/dist/node.js.map +1 -0
- package/dist/rules/blocks.d.ts +34 -0
- package/dist/rules/blocks.js +550 -0
- package/dist/rules/blocks.js.map +1 -0
- package/dist/rules/content.d.ts +2 -0
- package/dist/rules/content.js +85 -0
- package/dist/rules/content.js.map +1 -0
- package/dist/rules/dialects.js +10 -6
- package/dist/rules/dialects.js.map +1 -1
- package/dist/rules/links.js +302 -6
- package/dist/rules/links.js.map +1 -1
- package/dist/rules/lists.d.ts +2 -0
- package/dist/rules/lists.js +924 -0
- package/dist/rules/lists.js.map +1 -0
- package/dist/rules/structure.js +127 -1
- package/dist/rules/structure.js.map +1 -1
- package/dist/rules/style.d.ts +8 -2
- package/dist/rules/style.js +1137 -165
- package/dist/rules/style.js.map +1 -1
- package/dist/syntax/block-identifier.d.ts +18 -0
- package/dist/syntax/block-identifier.js +24 -0
- package/dist/syntax/block-identifier.js.map +1 -0
- package/dist/syntax/obsidian.d.ts +29 -11
- package/dist/syntax/obsidian.js +9 -10
- package/dist/syntax/obsidian.js.map +1 -1
- package/dist/syntax/parse.d.ts +87 -3
- package/dist/syntax/parse.js +247 -19
- package/dist/syntax/parse.js.map +1 -1
- package/dist/workspace/access.d.ts +12 -3
- package/dist/workspace/access.js +33 -5
- package/dist/workspace/access.js.map +1 -1
- package/dist/workspace/discovery.d.ts +85 -0
- package/dist/workspace/discovery.js +155 -0
- package/dist/workspace/discovery.js.map +1 -0
- package/dist/workspace/encoding.d.ts +24 -0
- package/dist/workspace/encoding.js +113 -0
- package/dist/workspace/encoding.js.map +1 -0
- package/dist/workspace/files.d.ts +92 -6
- package/dist/workspace/files.js +715 -59
- package/dist/workspace/files.js.map +1 -1
- package/dist/workspace/git.d.ts +54 -0
- package/dist/workspace/git.js +198 -0
- package/dist/workspace/git.js.map +1 -0
- package/dist/workspace/index.d.ts +58 -3
- package/dist/workspace/index.js +136 -37
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/selection.d.ts +2 -2
- package/dist/workspace/selection.js +3 -3
- package/dist/workspace/selection.js.map +1 -1
- package/dist/workspace/slug.js +8 -1
- package/dist/workspace/slug.js.map +1 -1
- package/dist/workspace/vault.js +3 -1
- package/dist/workspace/vault.js.map +1 -1
- package/docs/api.md +239 -0
- package/docs/benchmarks.md +140 -200
- package/docs/changelog.md +1 -0
- package/docs/cli.md +281 -89
- package/docs/configuration.md +193 -480
- package/docs/design.md +163 -284
- package/docs/dialects.md +169 -0
- package/docs/examples.md +151 -109
- package/docs/index.md +21 -37
- package/docs/integrations.md +144 -0
- package/docs/obsidian.md +91 -115
- package/docs/plugins.md +147 -212
- package/docs/quick-start.md +68 -74
- package/docs/rules.md +1237 -0
- package/docs/versions.md +56 -0
- package/examples/README.md +17 -14
- package/examples/automation/summarize-report.mjs +5 -2
- package/examples/library/lint-folder.mjs +5 -4
- package/examples/library/lint-workspace.mjs +32 -0
- package/examples/plugin/notes/release-checklist.md +2 -2
- package/examples/plugin/rules.mjs +17 -23
- package/examples/workspaces/obsidian-vault/after/mdtools.config.jsonc +2 -2
- package/examples/workspaces/obsidian-vault/before/mdtools.config.jsonc +2 -2
- package/examples/workspaces/repository-docs/after/README.md +2 -2
- package/examples/workspaces/repository-docs/after/mdtools.config.jsonc +0 -2
- package/examples/workspaces/repository-docs/before/README.md +2 -2
- package/examples/workspaces/repository-docs/before/docs/guide.md +7 -7
- package/examples/workspaces/repository-docs/before/docs/reference.md +3 -1
- package/examples/workspaces/repository-docs/before/mdtools.config.jsonc +0 -2
- package/package.json +38 -12
- package/docs/contributing.md +0 -218
- package/docs/releases/0.1.0-beta.1.md +0 -39
- package/docs/releases/0.1.0-beta.2.md +0 -83
- package/docs/releases/0.1.0-beta.3.md +0 -61
- package/docs/releases/0.1.0-beta.4.md +0 -50
- package/docs/releases/0.1.0-rc.1.md +0 -99
- package/docs/releases/0.2.0-rc.1.md +0 -141
- package/docs/releases/0.2.0-rc.2.md +0 -225
- package/docs/releases.md +0 -144
- package/docs/testing.md +0 -93
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import { optionsSchema } from "./style.js";
|
|
2
|
+
const tabStop = (column) => column + 4 - (column % 4);
|
|
3
|
+
const isSpace = (character) => character === " " || character === "\t";
|
|
4
|
+
/** The column reached from `column` at `from` after the characters up to `to`. */
|
|
5
|
+
function columnAt(source, from, to, column) {
|
|
6
|
+
for (let at = from; at < to; at++)
|
|
7
|
+
column = source[at] === "\t" ? tabStop(column) : column + 1;
|
|
8
|
+
return column;
|
|
9
|
+
}
|
|
10
|
+
function splitLines(source) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
let start = 0;
|
|
13
|
+
for (const match of source.matchAll(/\r\n|\r|\n/g)) {
|
|
14
|
+
lines.push({ start, end: match.index });
|
|
15
|
+
start = match.index + match[0].length;
|
|
16
|
+
}
|
|
17
|
+
lines.push({ start, end: source.length });
|
|
18
|
+
return lines;
|
|
19
|
+
}
|
|
20
|
+
const located = (node) => node.position?.start.offset !== undefined && node.position.end.offset !== undefined;
|
|
21
|
+
const isContainer = (node) => node.type === "blockquote" || node.type === "listItem" || node.type === "footnoteDefinition";
|
|
22
|
+
const analyses = new WeakMap();
|
|
23
|
+
/** The lists of a document and the means to read their lines, shared by the list rules. */
|
|
24
|
+
function analyze(document) {
|
|
25
|
+
const known = analyses.get(document);
|
|
26
|
+
if (known)
|
|
27
|
+
return known;
|
|
28
|
+
const { source } = document;
|
|
29
|
+
const lines = splitLines(source);
|
|
30
|
+
const units = [];
|
|
31
|
+
const items = new Map();
|
|
32
|
+
const containers = new Map();
|
|
33
|
+
const sensitive = new Set();
|
|
34
|
+
/** The innermost container of each line, by line index. */
|
|
35
|
+
const innermost = [];
|
|
36
|
+
const visit = (node, parent, unit) => {
|
|
37
|
+
const children = node.children;
|
|
38
|
+
let group;
|
|
39
|
+
let run;
|
|
40
|
+
let before;
|
|
41
|
+
for (let index = 0; index < children.length; index++) {
|
|
42
|
+
const child = children[index];
|
|
43
|
+
if (!located(child)) {
|
|
44
|
+
if (unit)
|
|
45
|
+
unit.valid = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const firstLine = child.position.start.line - 1;
|
|
49
|
+
let lastLine = child.position.end.line - 1;
|
|
50
|
+
// A list item in a footnote definition can end past the next item's
|
|
51
|
+
// start; a block ends before the next one starts.
|
|
52
|
+
const next = children[index + 1];
|
|
53
|
+
if (next && located(next))
|
|
54
|
+
lastLine = Math.min(lastLine, next.position.start.line - 2);
|
|
55
|
+
if (parent)
|
|
56
|
+
lastLine = Math.min(lastLine, containers.get(parent).last);
|
|
57
|
+
if (child.type === "list") {
|
|
58
|
+
let target = unit;
|
|
59
|
+
if (!target) {
|
|
60
|
+
if (children[index - 1]?.type !== "list" || !group) {
|
|
61
|
+
group = { lists: [], runs: [], valid: true };
|
|
62
|
+
units.push(group);
|
|
63
|
+
}
|
|
64
|
+
target = group;
|
|
65
|
+
}
|
|
66
|
+
const previous = children[index - 1];
|
|
67
|
+
if (!run || previous?.type !== "list" || previous.ordered !== child.ordered) {
|
|
68
|
+
run = [];
|
|
69
|
+
target.runs.push(run);
|
|
70
|
+
}
|
|
71
|
+
const list = {
|
|
72
|
+
node: child,
|
|
73
|
+
items: [],
|
|
74
|
+
parent,
|
|
75
|
+
previous: children[index - 1]?.type === "list" ? before : undefined,
|
|
76
|
+
};
|
|
77
|
+
before = list;
|
|
78
|
+
run.push(list);
|
|
79
|
+
target.lists.push(list);
|
|
80
|
+
for (const [position, entry] of child.children.entries()) {
|
|
81
|
+
const item = readItem(entry, list, position, target);
|
|
82
|
+
if (item)
|
|
83
|
+
list.items.push(item);
|
|
84
|
+
}
|
|
85
|
+
visit(child, parent, target);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
run = undefined;
|
|
89
|
+
if (isContainer(child)) {
|
|
90
|
+
containers.set(child, { parent, first: firstLine, last: Math.max(firstLine, lastLine) });
|
|
91
|
+
for (let line = firstLine; line <= lastLine; line++)
|
|
92
|
+
innermost[line] = child;
|
|
93
|
+
visit(child, child, unit);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Code and other blocks that hold text as written have no inline nodes.
|
|
97
|
+
if (unit && !("value" in child))
|
|
98
|
+
collectInline(child, unit);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The later lines of nodes that keep the whitespace at the start of their
|
|
103
|
+
* lines, such as code spans, math, and titles, and nodes without positions.
|
|
104
|
+
*/
|
|
105
|
+
const collectInline = (node, unit) => {
|
|
106
|
+
if (!located(node)) {
|
|
107
|
+
unit.valid = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const keeps = node.type === "definition" ||
|
|
111
|
+
("title" in node && typeof node.title === "string") ||
|
|
112
|
+
("value" in node &&
|
|
113
|
+
typeof node.value === "string" &&
|
|
114
|
+
node.type !== "text" &&
|
|
115
|
+
node.type !== "html");
|
|
116
|
+
if (keeps)
|
|
117
|
+
for (let line = node.position.start.line; line < node.position.end.line; line++)
|
|
118
|
+
sensitive.add(line);
|
|
119
|
+
if ("children" in node)
|
|
120
|
+
for (const child of node.children)
|
|
121
|
+
collectInline(child, unit);
|
|
122
|
+
};
|
|
123
|
+
const readItem = (node, list, index, unit) => {
|
|
124
|
+
if (!located(node)) {
|
|
125
|
+
unit.valid = false;
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const marker = node.position.start.offset;
|
|
129
|
+
const first = node.position.start.line - 1;
|
|
130
|
+
const line = lines[first];
|
|
131
|
+
let markerEnd = marker;
|
|
132
|
+
let digits = "";
|
|
133
|
+
if (list.node.ordered) {
|
|
134
|
+
digits = /^\d{1,9}/.exec(source.slice(marker, marker + 10))?.[0] ?? "";
|
|
135
|
+
markerEnd = marker + digits.length;
|
|
136
|
+
if (!digits || (source[markerEnd] !== "." && source[markerEnd] !== ")"))
|
|
137
|
+
unit.valid = false;
|
|
138
|
+
markerEnd++;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (!"-+*".includes(source[marker] ?? "_"))
|
|
142
|
+
unit.valid = false;
|
|
143
|
+
markerEnd++;
|
|
144
|
+
}
|
|
145
|
+
let spaceEnd = markerEnd;
|
|
146
|
+
while (spaceEnd < line.end && isSpace(source[spaceEnd]))
|
|
147
|
+
spaceEnd++;
|
|
148
|
+
const markerColumn = columnAt(source, line.start, marker, 0);
|
|
149
|
+
const markerEndColumn = markerColumn + markerEnd - marker;
|
|
150
|
+
const item = {
|
|
151
|
+
node,
|
|
152
|
+
list,
|
|
153
|
+
index,
|
|
154
|
+
first,
|
|
155
|
+
last: first,
|
|
156
|
+
marker,
|
|
157
|
+
markerEnd,
|
|
158
|
+
digits,
|
|
159
|
+
spaceEnd,
|
|
160
|
+
markerColumn,
|
|
161
|
+
markerEndColumn,
|
|
162
|
+
spacing: columnAt(source, markerEnd, spaceEnd, markerEndColumn) - markerEndColumn,
|
|
163
|
+
blankFirst: spaceEnd === line.end,
|
|
164
|
+
};
|
|
165
|
+
items.set(node, item);
|
|
166
|
+
return item;
|
|
167
|
+
};
|
|
168
|
+
visit(document.tree, undefined, undefined);
|
|
169
|
+
for (const item of items.values())
|
|
170
|
+
item.last = containers.get(item.node)?.last ?? item.first;
|
|
171
|
+
for (const unit of units)
|
|
172
|
+
for (const list of unit.lists)
|
|
173
|
+
for (const item of list.items)
|
|
174
|
+
if (item.last < item.first || (list.items[item.index + 1]?.first ?? Infinity) <= item.last)
|
|
175
|
+
unit.valid = false;
|
|
176
|
+
const enclosing = (line) => {
|
|
177
|
+
const chain = [];
|
|
178
|
+
for (let node = innermost[line]; node; node = containers.get(node)?.parent)
|
|
179
|
+
chain.unshift(node);
|
|
180
|
+
return chain;
|
|
181
|
+
};
|
|
182
|
+
const chains = new Map();
|
|
183
|
+
/** A line with nothing but whitespace and `>`, which starts no container. */
|
|
184
|
+
const blank = (line) => {
|
|
185
|
+
const { start, end } = lines[line];
|
|
186
|
+
const node = innermost[line];
|
|
187
|
+
return ((!node || containers.get(node).first !== line) && /^[ \t>]*$/.test(source.slice(start, end)));
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* The containers that a line passes through. A blank line belongs to every
|
|
191
|
+
* container that the line before it is in, until a later line closes them,
|
|
192
|
+
* though the tree ends a container at its last line with content: in a list
|
|
193
|
+
* item that ends with indented code, a blank line's spaces beyond the code's
|
|
194
|
+
* indentation become part of the code.
|
|
195
|
+
*/
|
|
196
|
+
const open = (line) => {
|
|
197
|
+
let from = line;
|
|
198
|
+
while (from > 0 && !chains.has(from) && !chains.has(from - 1) && blank(from))
|
|
199
|
+
from--;
|
|
200
|
+
for (let at = from; at <= line; at++) {
|
|
201
|
+
if (chains.has(at))
|
|
202
|
+
continue;
|
|
203
|
+
let chain = enclosing(at);
|
|
204
|
+
const before = at > 0 && blank(at) ? chains.get(at - 1) : undefined;
|
|
205
|
+
if (before && chain.every((node, index) => before[index] === node))
|
|
206
|
+
chain = before;
|
|
207
|
+
chains.set(at, chain);
|
|
208
|
+
}
|
|
209
|
+
return chains.get(line);
|
|
210
|
+
};
|
|
211
|
+
const memo = new Map();
|
|
212
|
+
const match = (line) => {
|
|
213
|
+
const known = memo.get(line);
|
|
214
|
+
if (known)
|
|
215
|
+
return known;
|
|
216
|
+
const chain = open(line);
|
|
217
|
+
const levels = [];
|
|
218
|
+
const result = { chain, levels };
|
|
219
|
+
memo.set(line, result);
|
|
220
|
+
let at = lines[line].start;
|
|
221
|
+
let column = 0;
|
|
222
|
+
for (const node of chain) {
|
|
223
|
+
const level = matchLevel(node, line, at, column);
|
|
224
|
+
levels.push(level);
|
|
225
|
+
if (level.kind !== "content")
|
|
226
|
+
break;
|
|
227
|
+
at = level.end;
|
|
228
|
+
column = level.endColumn;
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
/** How `line`, from `start` at `column`, passes through the prefix of `node`. */
|
|
233
|
+
const matchLevel = (node, line, start, column) => {
|
|
234
|
+
const { end: lineEnd } = lines[line];
|
|
235
|
+
const level = {
|
|
236
|
+
node,
|
|
237
|
+
kind: "unsupported",
|
|
238
|
+
start,
|
|
239
|
+
startColumn: column,
|
|
240
|
+
end: start,
|
|
241
|
+
endColumn: column,
|
|
242
|
+
indent: 0,
|
|
243
|
+
};
|
|
244
|
+
const info = containers.get(node);
|
|
245
|
+
if (node.type === "listItem") {
|
|
246
|
+
const item = items.get(node);
|
|
247
|
+
if (!item)
|
|
248
|
+
return level;
|
|
249
|
+
if (line === item.first) {
|
|
250
|
+
if (!/^[ \t]*$/.test(source.slice(start, item.marker)))
|
|
251
|
+
return level;
|
|
252
|
+
const offset = item.markerColumn - column;
|
|
253
|
+
if (offset > 3)
|
|
254
|
+
return level;
|
|
255
|
+
const content = item.blankFirst || item.spacing >= 5
|
|
256
|
+
? item.markerEndColumn + 1
|
|
257
|
+
: item.markerEndColumn + item.spacing;
|
|
258
|
+
item.start = start;
|
|
259
|
+
item.offset = offset;
|
|
260
|
+
item.width = content - column;
|
|
261
|
+
if (item.blankFirst)
|
|
262
|
+
return { ...level, kind: "content", end: lineEnd, endColumn: content };
|
|
263
|
+
if (item.spacing >= 5) {
|
|
264
|
+
// A tab right after the marker gives one of its columns to the marker.
|
|
265
|
+
if (source[item.markerEnd] !== " ")
|
|
266
|
+
return level;
|
|
267
|
+
return { ...level, kind: "content", end: item.markerEnd + 1, endColumn: content };
|
|
268
|
+
}
|
|
269
|
+
return { ...level, kind: "content", end: item.spaceEnd, endColumn: content };
|
|
270
|
+
}
|
|
271
|
+
if (item.width === undefined)
|
|
272
|
+
match(item.first);
|
|
273
|
+
if (item.width === undefined)
|
|
274
|
+
return level;
|
|
275
|
+
return indented(level, item.width, lineEnd);
|
|
276
|
+
}
|
|
277
|
+
if (node.type === "blockquote") {
|
|
278
|
+
let at = start;
|
|
279
|
+
let reached = column;
|
|
280
|
+
if (line === info.first)
|
|
281
|
+
at = node.position.start.offset;
|
|
282
|
+
else
|
|
283
|
+
while (at < lineEnd && isSpace(source[at])) {
|
|
284
|
+
const next = source[at] === "\t" ? tabStop(reached) : reached + 1;
|
|
285
|
+
if (next - column > 3)
|
|
286
|
+
break;
|
|
287
|
+
reached = next;
|
|
288
|
+
at++;
|
|
289
|
+
}
|
|
290
|
+
if (source[at] !== ">" || !/^[ \t]*$/.test(source.slice(start, at)))
|
|
291
|
+
return /^[ \t]*$/.test(source.slice(start, lineEnd))
|
|
292
|
+
? { ...level, kind: "blank", indent: columnAt(source, start, lineEnd, column) - column }
|
|
293
|
+
: line === info.first
|
|
294
|
+
? level
|
|
295
|
+
: { ...level, kind: "lazy", indent: columnAt(source, start, at, column) - column };
|
|
296
|
+
reached = columnAt(source, start, at, column) + 1;
|
|
297
|
+
at++;
|
|
298
|
+
if (source[at] === "\t")
|
|
299
|
+
return level;
|
|
300
|
+
if (source[at] === " ") {
|
|
301
|
+
at++;
|
|
302
|
+
reached++;
|
|
303
|
+
}
|
|
304
|
+
return { ...level, kind: "content", end: at, endColumn: reached };
|
|
305
|
+
}
|
|
306
|
+
// A footnote definition: its first line's content starts with its first
|
|
307
|
+
// block, and its other lines need four columns.
|
|
308
|
+
if (line === info.first) {
|
|
309
|
+
const child = node.children[0];
|
|
310
|
+
const end = child && located(child) && child.position.start.line - 1 === line
|
|
311
|
+
? child.position.start.offset
|
|
312
|
+
: lineEnd;
|
|
313
|
+
return {
|
|
314
|
+
...level,
|
|
315
|
+
kind: "content",
|
|
316
|
+
end,
|
|
317
|
+
endColumn: columnAt(source, start, end, column),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return indented(level, 4, lineEnd);
|
|
321
|
+
};
|
|
322
|
+
/** A line that needs `width` columns of indentation to continue a container. */
|
|
323
|
+
const indented = (level, width, lineEnd) => {
|
|
324
|
+
let at = level.start;
|
|
325
|
+
let column = level.startColumn;
|
|
326
|
+
if (/^[ \t]*$/.test(source.slice(at, lineEnd)))
|
|
327
|
+
return {
|
|
328
|
+
...level,
|
|
329
|
+
kind: "blank",
|
|
330
|
+
indent: columnAt(source, at, lineEnd, column) - column,
|
|
331
|
+
};
|
|
332
|
+
while (column - level.startColumn < width && isSpace(source[at])) {
|
|
333
|
+
const next = source[at] === "\t" ? tabStop(column) : column + 1;
|
|
334
|
+
if (next - level.startColumn > width)
|
|
335
|
+
return level;
|
|
336
|
+
column = next;
|
|
337
|
+
at++;
|
|
338
|
+
}
|
|
339
|
+
if (column - level.startColumn < width)
|
|
340
|
+
return { ...level, kind: "lazy", indent: column - level.startColumn };
|
|
341
|
+
return { ...level, kind: "content", end: at, endColumn: column };
|
|
342
|
+
};
|
|
343
|
+
const analysis = { source, lines, units, items, containers, sensitive, match };
|
|
344
|
+
analyses.set(document, analysis);
|
|
345
|
+
return analysis;
|
|
346
|
+
}
|
|
347
|
+
/** A tab among the whitespace and container markers that start the rest of a line. */
|
|
348
|
+
function structuralTab(source, from, to) {
|
|
349
|
+
const prefix = /^(?:[ \t>]|(?:[-+*]|\d{1,9}[.)])(?=[ \t]|$))*/.exec(source.slice(from, to))[0];
|
|
350
|
+
return prefix.includes("\t");
|
|
351
|
+
}
|
|
352
|
+
const onlySpaces = (source, from, to) => from <= to && /^ *$/.test(source.slice(from, to));
|
|
353
|
+
/**
|
|
354
|
+
* Whether a lazy line can join its item: once the item needs fewer columns,
|
|
355
|
+
* the line may have enough and continue the item's paragraph from inside it.
|
|
356
|
+
* Text that starts with a letter, or a number that is no list marker, does
|
|
357
|
+
* that; anything else could start a block there, and inline code or math
|
|
358
|
+
* would keep the indentation that the line no longer has.
|
|
359
|
+
*/
|
|
360
|
+
function lazySafe(analysis, line, level) {
|
|
361
|
+
const { source, lines } = analysis;
|
|
362
|
+
const text = source.slice(level.start, lines[line].end).trimStart();
|
|
363
|
+
return (!analysis.sensitive.has(line) &&
|
|
364
|
+
!text.includes("|") &&
|
|
365
|
+
/^(?:\p{L}|[0-9]+(?![.)0-9]))/u.test(text));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Whether the item's column changes are possible without moving content: an
|
|
369
|
+
* item's lines must be read exactly, and none of the lines around it may
|
|
370
|
+
* start or stop belonging to it. `delta` gives the change of each item's
|
|
371
|
+
* column, `minimum` the fewest columns that an item can need once the list
|
|
372
|
+
* rules are done, and `siblings` says whether the other items of the list
|
|
373
|
+
* move to the start of their container's part of the line too.
|
|
374
|
+
*/
|
|
375
|
+
function feasible(analysis, list, delta, mode, minimum, siblings) {
|
|
376
|
+
const { source, lines } = analysis;
|
|
377
|
+
for (const item of list.items)
|
|
378
|
+
analysis.match(item.first);
|
|
379
|
+
for (const item of list.items) {
|
|
380
|
+
const change = delta.get(item) ?? 0;
|
|
381
|
+
if (!change)
|
|
382
|
+
continue;
|
|
383
|
+
if (item.width === undefined)
|
|
384
|
+
return false;
|
|
385
|
+
for (const line of linesWithBlanks(analysis, item.first + 1, item.last)) {
|
|
386
|
+
const { chain, levels } = analysis.match(line);
|
|
387
|
+
const at = chain.indexOf(item.node);
|
|
388
|
+
if (at < 0) {
|
|
389
|
+
if (line > item.last)
|
|
390
|
+
continue;
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
const level = levels[at];
|
|
394
|
+
if (!level) {
|
|
395
|
+
const last = levels.at(-1);
|
|
396
|
+
if (!last || last.kind === "unsupported")
|
|
397
|
+
return false;
|
|
398
|
+
if (last.kind === "blank" && !blankFeasible(analysis, line, chain, levels.length - 1))
|
|
399
|
+
return false;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (level.kind === "unsupported")
|
|
403
|
+
return false;
|
|
404
|
+
if (level.kind === "blank") {
|
|
405
|
+
if (!blankFeasible(analysis, line, chain, at))
|
|
406
|
+
return false;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (level.kind === "lazy") {
|
|
410
|
+
if (change < 0 && level.indent >= minimum(item) && !lazySafe(analysis, line, level))
|
|
411
|
+
return false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const lineEnd = lines[line].end;
|
|
415
|
+
if (mode === "start") {
|
|
416
|
+
if (change < 0 && !onlySpaces(source, level.start, level.start - change))
|
|
417
|
+
return false;
|
|
418
|
+
if (structuralTab(source, level.start, lineEnd))
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
if (change < 0 && !onlySpaces(source, level.end + change, level.end))
|
|
423
|
+
return false;
|
|
424
|
+
if (structuralTab(source, level.end + Math.min(0, change), lineEnd))
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (change < 0 && !captureFree(analysis, item, change, minimum, siblings))
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The items and footnote definitions whose prefixes a blank line's
|
|
435
|
+
* whitespace stands for, from the level where the line is blank down to the
|
|
436
|
+
* deepest container still open there; a block quote that the line has no `>`
|
|
437
|
+
* for ends there. Undefined when an item's columns are not known.
|
|
438
|
+
*/
|
|
439
|
+
function blankPath(analysis, chain, from) {
|
|
440
|
+
const path = [];
|
|
441
|
+
let width = 0;
|
|
442
|
+
for (const node of chain.slice(from)) {
|
|
443
|
+
if (node.type === "blockquote")
|
|
444
|
+
break;
|
|
445
|
+
if (node.type === "footnoteDefinition") {
|
|
446
|
+
path.push(undefined);
|
|
447
|
+
width += 4;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const item = analysis.items.get(node);
|
|
451
|
+
if (item?.width === undefined)
|
|
452
|
+
return undefined;
|
|
453
|
+
path.push(item);
|
|
454
|
+
width += item.width;
|
|
455
|
+
}
|
|
456
|
+
return { items: path, width };
|
|
457
|
+
}
|
|
458
|
+
/** Whether a blank line can keep its whitespace beyond its containers' prefixes. */
|
|
459
|
+
function blankFeasible(analysis, line, chain, from) {
|
|
460
|
+
const path = blankPath(analysis, chain, from);
|
|
461
|
+
if (!path)
|
|
462
|
+
return false;
|
|
463
|
+
const level = analysis.match(line).levels[from];
|
|
464
|
+
return (!path.items.length ||
|
|
465
|
+
!analysis.source.slice(level.start, analysis.lines[line].end).includes("\t"));
|
|
466
|
+
}
|
|
467
|
+
/** The line indexes from `first` to `last`, and the blank lines after them. */
|
|
468
|
+
function* linesWithBlanks(analysis, first, last) {
|
|
469
|
+
const { source, lines } = analysis;
|
|
470
|
+
for (let line = first; line <= last; line++)
|
|
471
|
+
yield line;
|
|
472
|
+
for (let line = last + 1; line < lines.length; line++) {
|
|
473
|
+
if (!/^[ \t>]*$/.test(source.slice(lines[line].start, lines[line].end)))
|
|
474
|
+
break;
|
|
475
|
+
yield line;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Whether the line after an item that needs fewer columns stays out of it:
|
|
480
|
+
* the next item of its list, unless that moves too, or the first line after
|
|
481
|
+
* the item that its container continues with.
|
|
482
|
+
*/
|
|
483
|
+
function captureFree(analysis, item, change, minimum, siblings) {
|
|
484
|
+
const { source, lines } = analysis;
|
|
485
|
+
const next = item.list.items[item.index + 1];
|
|
486
|
+
if (next) {
|
|
487
|
+
if (siblings === "move")
|
|
488
|
+
return true;
|
|
489
|
+
return next.offset !== undefined && next.offset < item.width + change;
|
|
490
|
+
}
|
|
491
|
+
const { parent } = item.list;
|
|
492
|
+
const last = parent ? analysis.containers.get(parent).last : lines.length - 1;
|
|
493
|
+
for (let line = item.last + 1; line <= last; line++) {
|
|
494
|
+
let start = lines[line].start;
|
|
495
|
+
let column = 0;
|
|
496
|
+
if (parent) {
|
|
497
|
+
const { chain, levels } = analysis.match(line);
|
|
498
|
+
const at = chain.indexOf(parent);
|
|
499
|
+
if (at < 0)
|
|
500
|
+
return false;
|
|
501
|
+
const level = levels[at];
|
|
502
|
+
const reached = level ?? levels.at(-1);
|
|
503
|
+
if (!reached || reached.kind === "unsupported")
|
|
504
|
+
return false;
|
|
505
|
+
if (reached.kind === "blank")
|
|
506
|
+
continue;
|
|
507
|
+
if (!level || level.kind === "lazy")
|
|
508
|
+
return true;
|
|
509
|
+
start = level.end;
|
|
510
|
+
column = level.endColumn;
|
|
511
|
+
}
|
|
512
|
+
const text = source.slice(start, lines[line].end);
|
|
513
|
+
if (/^[ \t]*$/.test(text))
|
|
514
|
+
continue;
|
|
515
|
+
const indent = /^[ \t]*/.exec(text)[0];
|
|
516
|
+
return columnAt(source, start, start + indent.length, column) - column < minimum(item);
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The changes to the lines of items whose columns change by `delta`: the
|
|
522
|
+
* columns are removed or added at the start of each item's part of a line,
|
|
523
|
+
* or at its end, where its content starts. A blank line in code keeps the
|
|
524
|
+
* whitespace that the code holds there.
|
|
525
|
+
*/
|
|
526
|
+
function shiftOps(analysis, unit, delta, mode) {
|
|
527
|
+
const ops = [];
|
|
528
|
+
if (![...delta.values()].some(Boolean))
|
|
529
|
+
return ops;
|
|
530
|
+
const { lines } = analysis;
|
|
531
|
+
let firstLine = Infinity;
|
|
532
|
+
let lastLine = -1;
|
|
533
|
+
for (const { items } of unit.lists) {
|
|
534
|
+
firstLine = Math.min(firstLine, items[0]?.first ?? Infinity);
|
|
535
|
+
lastLine = Math.max(lastLine, items.at(-1)?.last ?? -1);
|
|
536
|
+
}
|
|
537
|
+
for (const line of linesWithBlanks(analysis, firstLine, lastLine)) {
|
|
538
|
+
const { chain, levels } = analysis.match(line);
|
|
539
|
+
for (const [at, level] of levels.entries()) {
|
|
540
|
+
if (level.kind === "blank") {
|
|
541
|
+
// The whitespace beyond the prefixes of the containers still open
|
|
542
|
+
// stays: in code, it is part of the code.
|
|
543
|
+
const path = blankPath(analysis, chain, at);
|
|
544
|
+
if (!path)
|
|
545
|
+
break;
|
|
546
|
+
const change = path.items.reduce((sum, item) => sum + ((item && delta.get(item)) ?? 0), 0);
|
|
547
|
+
const indent = level.indent >= path.width
|
|
548
|
+
? level.indent + change
|
|
549
|
+
: Math.min(level.indent, path.width + change);
|
|
550
|
+
if (change && indent !== level.indent)
|
|
551
|
+
ops.push({ start: level.start, end: lines[line].end, text: " ".repeat(indent) });
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
if (level.kind !== "content")
|
|
555
|
+
break;
|
|
556
|
+
if (level.node.type !== "listItem")
|
|
557
|
+
continue;
|
|
558
|
+
const item = analysis.items.get(level.node);
|
|
559
|
+
const change = delta.get(item) ?? 0;
|
|
560
|
+
if (!change || line === item.first)
|
|
561
|
+
continue;
|
|
562
|
+
if (mode === "start")
|
|
563
|
+
ops.push(change > 0
|
|
564
|
+
? { start: level.start, end: level.start, text: " ".repeat(change) }
|
|
565
|
+
: { start: level.start, end: level.start - change, text: "" });
|
|
566
|
+
else if (change > 0)
|
|
567
|
+
ops.push({ start: level.end, end: level.end, text: " ".repeat(change) });
|
|
568
|
+
else
|
|
569
|
+
ops.push({ start: level.end + change, end: level.end, text: "" });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return ops;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* One finding for a unit's changes, with the edit that makes all of them and
|
|
576
|
+
* the message of the first change that has one.
|
|
577
|
+
*/
|
|
578
|
+
function unitFinding(source, ops) {
|
|
579
|
+
if (!ops.length)
|
|
580
|
+
return undefined;
|
|
581
|
+
ops.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
582
|
+
for (let index = 1; index < ops.length; index++)
|
|
583
|
+
if (ops[index].start < ops[index - 1].end)
|
|
584
|
+
return undefined;
|
|
585
|
+
const start = ops[0].start;
|
|
586
|
+
const end = ops.at(-1).end;
|
|
587
|
+
let text = "";
|
|
588
|
+
let cursor = start;
|
|
589
|
+
for (const op of ops) {
|
|
590
|
+
text += source.slice(cursor, op.start) + op.text;
|
|
591
|
+
cursor = op.end;
|
|
592
|
+
}
|
|
593
|
+
text += source.slice(cursor, end);
|
|
594
|
+
const message = ops.find((op) => op.message)?.message ?? "Format the list.";
|
|
595
|
+
return { start, end, message, edit: { start, end, text } };
|
|
596
|
+
}
|
|
597
|
+
const bullets = ["-", "*", "+"];
|
|
598
|
+
/** The marker after `marker` in the order `-`, `*`, `+`. */
|
|
599
|
+
const nextBullet = (marker) => bullets[(bullets.indexOf(marker) + 1) % 3];
|
|
600
|
+
/** A line that reads as a thematic break. */
|
|
601
|
+
const thematicBreak = /^([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
602
|
+
/** The number of bullet lists that a list is nested in. */
|
|
603
|
+
function bulletDepth(analysis, list) {
|
|
604
|
+
let depth = 0;
|
|
605
|
+
for (let node = list.parent; node; node = analysis.containers.get(node)?.parent)
|
|
606
|
+
if (node.type === "listItem" && !analysis.items.get(node)?.list.node.ordered)
|
|
607
|
+
depth++;
|
|
608
|
+
return depth;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* The marker that each bullet list of a document gets before lists that
|
|
612
|
+
* follow one another alternate: the configured one, the first list's with
|
|
613
|
+
* `consistent`, or with `sublist` the first list's at each depth, unless the
|
|
614
|
+
* depth above has that marker, in which case the next one.
|
|
615
|
+
*/
|
|
616
|
+
function bulletMarkers(analysis, marker) {
|
|
617
|
+
if (marker !== "consistent" && marker !== "sublist")
|
|
618
|
+
return () => marker;
|
|
619
|
+
const { source } = analysis;
|
|
620
|
+
const lists = analysis.units.flatMap((unit) => unit.lists.filter((list) => !list.node.ordered && bullets.includes(source[list.items[0]?.marker ?? -1])));
|
|
621
|
+
const written = (list) => source[list.items[0].marker];
|
|
622
|
+
if (marker === "consistent") {
|
|
623
|
+
const first = lists[0] ? written(lists[0]) : "-";
|
|
624
|
+
return () => first;
|
|
625
|
+
}
|
|
626
|
+
const byDepth = [];
|
|
627
|
+
const depths = new Map(lists.map((list) => [list, bulletDepth(analysis, list)]));
|
|
628
|
+
for (const list of lists) {
|
|
629
|
+
const depth = depths.get(list);
|
|
630
|
+
// The lists that a list is nested in come before it.
|
|
631
|
+
if (byDepth[depth] !== undefined)
|
|
632
|
+
continue;
|
|
633
|
+
const own = written(list);
|
|
634
|
+
byDepth[depth] = depth && own === byDepth[depth - 1] ? nextBullet(own) : own;
|
|
635
|
+
}
|
|
636
|
+
return (list) => byDepth[depths.get(list) ?? 0] ?? "-";
|
|
637
|
+
}
|
|
638
|
+
const listMarker = {
|
|
639
|
+
description: "Use one bullet list marker, and the next one for a list that follows another.",
|
|
640
|
+
kind: "style",
|
|
641
|
+
phase: "block",
|
|
642
|
+
schema: optionsSchema({ marker: { enum: [...bullets, "consistent", "sublist"] } }),
|
|
643
|
+
check({ document, options }) {
|
|
644
|
+
const analysis = analyze(document);
|
|
645
|
+
const { source, lines } = analysis;
|
|
646
|
+
const base = bulletMarkers(analysis, String(options.marker ?? "-"));
|
|
647
|
+
const findings = [];
|
|
648
|
+
for (const unit of analysis.units) {
|
|
649
|
+
if (!unit.valid)
|
|
650
|
+
continue;
|
|
651
|
+
const target = new Map();
|
|
652
|
+
const runOf = new Map();
|
|
653
|
+
for (const run of unit.runs) {
|
|
654
|
+
if (run[0].node.ordered)
|
|
655
|
+
continue;
|
|
656
|
+
const preferred = base(run[0]);
|
|
657
|
+
run.forEach((list, index) => {
|
|
658
|
+
for (const item of list.items) {
|
|
659
|
+
target.set(item, index % 2 ? nextBullet(preferred) : preferred);
|
|
660
|
+
runOf.set(item, run);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
// A run keeps its markers where a new one would turn a line into a
|
|
665
|
+
// thematic break, also together with the markers of lists nested on it.
|
|
666
|
+
const kept = new Set();
|
|
667
|
+
const byLine = new Map();
|
|
668
|
+
for (const item of target.keys())
|
|
669
|
+
byLine.set(item.first, [...(byLine.get(item.first) ?? []), item]);
|
|
670
|
+
for (let changed = true; changed;) {
|
|
671
|
+
changed = false;
|
|
672
|
+
for (const [line, onLine] of byLine) {
|
|
673
|
+
const marked = (item) => kept.has(runOf.get(item)) ? source[item.marker] : target.get(item);
|
|
674
|
+
if (onLine.every((item) => marked(item) === source[item.marker]))
|
|
675
|
+
continue;
|
|
676
|
+
const { start, end } = lines[line];
|
|
677
|
+
let text = source.slice(start, end);
|
|
678
|
+
for (const item of onLine)
|
|
679
|
+
text =
|
|
680
|
+
text.slice(0, item.marker - start) +
|
|
681
|
+
marked(item) +
|
|
682
|
+
text.slice(item.marker - start + 1);
|
|
683
|
+
if (onLine.some((item) => thematicBreak.test(text.slice(item.marker - start)))) {
|
|
684
|
+
for (const item of onLine)
|
|
685
|
+
if (marked(item) !== source[item.marker])
|
|
686
|
+
kept.add(runOf.get(item));
|
|
687
|
+
changed = true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const ops = [];
|
|
692
|
+
for (const [item, marker] of target)
|
|
693
|
+
if (!kept.has(runOf.get(item)) && source[item.marker] !== marker)
|
|
694
|
+
ops.push({
|
|
695
|
+
start: item.marker,
|
|
696
|
+
end: item.marker + 1,
|
|
697
|
+
text: marker,
|
|
698
|
+
message: `Use ${JSON.stringify(marker)} as the bullet list marker.`,
|
|
699
|
+
});
|
|
700
|
+
const finding = unitFinding(source, ops);
|
|
701
|
+
if (finding)
|
|
702
|
+
findings.push(finding);
|
|
703
|
+
}
|
|
704
|
+
return findings;
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
/** The numbers that `numbering` gives a list's items, or undefined to keep them. */
|
|
708
|
+
function renumbered(digits, numbering) {
|
|
709
|
+
if (digits.length < 2 || digits.some((value) => value.length > 1 && value.startsWith("0")))
|
|
710
|
+
return undefined;
|
|
711
|
+
const first = Number(digits[0]);
|
|
712
|
+
const same = numbering === "same" || (numbering !== "ordered" && Number(digits[1]) === first);
|
|
713
|
+
const numbers = digits.map((value, index) => index === 0 ? first : same ? first : first + index);
|
|
714
|
+
if (numbers.some((value) => value > 999_999_999))
|
|
715
|
+
return undefined;
|
|
716
|
+
return numbers.map((value, index) => (index === 0 ? digits[0] : String(value)));
|
|
717
|
+
}
|
|
718
|
+
const orderedList = {
|
|
719
|
+
description: "Use one delimiter after list numbers, and number list items consistently.",
|
|
720
|
+
kind: "style",
|
|
721
|
+
phase: "block",
|
|
722
|
+
schema: optionsSchema({
|
|
723
|
+
delimiter: { enum: [".", ")", "consistent"] },
|
|
724
|
+
numbering: { enum: ["auto", "ordered", "same"] },
|
|
725
|
+
}),
|
|
726
|
+
check({ document, options }) {
|
|
727
|
+
const analysis = analyze(document);
|
|
728
|
+
const { source } = analysis;
|
|
729
|
+
let preferred = String(options.delimiter ?? ".");
|
|
730
|
+
if (preferred === "consistent") {
|
|
731
|
+
// The delimiter of the document's first ordered list.
|
|
732
|
+
const first = analysis.units
|
|
733
|
+
.flatMap((unit) => unit.lists)
|
|
734
|
+
.find((list) => list.node.ordered && list.items.length);
|
|
735
|
+
preferred = source[(first?.items[0]?.markerEnd ?? 0) - 1] === ")" ? ")" : ".";
|
|
736
|
+
}
|
|
737
|
+
const other = preferred === "." ? ")" : ".";
|
|
738
|
+
const numbering = String(options.numbering ?? "auto");
|
|
739
|
+
const findings = [];
|
|
740
|
+
for (const unit of analysis.units) {
|
|
741
|
+
if (!unit.valid)
|
|
742
|
+
continue;
|
|
743
|
+
const delimiter = new Map();
|
|
744
|
+
for (const run of unit.runs)
|
|
745
|
+
if (run[0].node.ordered)
|
|
746
|
+
run.forEach((list, index) => delimiter.set(list, index % 2 ? other : preferred));
|
|
747
|
+
const ops = [];
|
|
748
|
+
const delta = new Map();
|
|
749
|
+
for (const list of unit.lists) {
|
|
750
|
+
if (!list.node.ordered)
|
|
751
|
+
continue;
|
|
752
|
+
const digits = list.items.map((item) => item.digits);
|
|
753
|
+
let numbers = renumbered(digits, numbering) ?? digits;
|
|
754
|
+
const changes = new Map();
|
|
755
|
+
for (const item of list.items) {
|
|
756
|
+
const change = numbers[item.index].length - item.digits.length;
|
|
757
|
+
if (change)
|
|
758
|
+
changes.set(item, change);
|
|
759
|
+
}
|
|
760
|
+
const minimum = (item) => numbers[item.index].length + 2;
|
|
761
|
+
if (changes.size) {
|
|
762
|
+
// The marker line's content moves with the number; a tab after it
|
|
763
|
+
// would change its width instead.
|
|
764
|
+
const readable = [...changes.keys()].every((item) => !structuralTab(source, item.markerEnd, analysis.lines[item.first].end));
|
|
765
|
+
if (!readable || !feasible(analysis, list, changes, "end", minimum, "stay")) {
|
|
766
|
+
numbers = digits;
|
|
767
|
+
changes.clear();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const [item, change] of changes)
|
|
771
|
+
delta.set(item, change);
|
|
772
|
+
const mark = delimiter.get(list) ?? preferred;
|
|
773
|
+
for (const item of list.items) {
|
|
774
|
+
const text = numbers[item.index] + mark;
|
|
775
|
+
if (source.slice(item.marker, item.markerEnd) === text)
|
|
776
|
+
continue;
|
|
777
|
+
ops.push({
|
|
778
|
+
start: item.marker,
|
|
779
|
+
end: item.markerEnd,
|
|
780
|
+
text,
|
|
781
|
+
message: source[item.markerEnd - 1] !== mark
|
|
782
|
+
? `Use ${JSON.stringify(mark)} after list item numbers.`
|
|
783
|
+
: `Number this list item ${numbers[item.index]}.`,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
for (const op of shiftOps(analysis, unit, delta, "end"))
|
|
788
|
+
ops.push(op);
|
|
789
|
+
const finding = unitFinding(source, ops);
|
|
790
|
+
if (finding)
|
|
791
|
+
findings.push(finding);
|
|
792
|
+
}
|
|
793
|
+
return findings;
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
/** The columns from an item's marker to its text. */
|
|
797
|
+
const reachOf = (item, spacing = item.spacing) => item.markerEnd - item.marker + (item.blankFirst || item.spacing >= 5 ? 1 : spacing);
|
|
798
|
+
const spaces = { type: "integer", minimum: 1, maximum: 4 };
|
|
799
|
+
const listIndent = {
|
|
800
|
+
description: "Indent list items consistently, with the configured spaces after the marker.",
|
|
801
|
+
kind: "style",
|
|
802
|
+
phase: "block",
|
|
803
|
+
schema: optionsSchema({
|
|
804
|
+
bulletSpaces: spaces,
|
|
805
|
+
orderedSpaces: spaces,
|
|
806
|
+
bulletSpacesMultiline: spaces,
|
|
807
|
+
orderedSpacesMultiline: spaces,
|
|
808
|
+
nestedIndent: { type: "integer", minimum: 1, maximum: 8 },
|
|
809
|
+
}),
|
|
810
|
+
check({ document, options }) {
|
|
811
|
+
const analysis = analyze(document);
|
|
812
|
+
const { source, lines } = analysis;
|
|
813
|
+
const bullet = Number(options.bulletSpaces ?? 1);
|
|
814
|
+
const ordered = Number(options.orderedSpaces ?? 1);
|
|
815
|
+
const spacing = {
|
|
816
|
+
bullet: [bullet, Number(options.bulletSpacesMultiline ?? bullet)],
|
|
817
|
+
ordered: [ordered, Number(options.orderedSpacesMultiline ?? ordered)],
|
|
818
|
+
};
|
|
819
|
+
const nested = options.nestedIndent === undefined ? undefined : Number(options.nestedIndent);
|
|
820
|
+
const findings = [];
|
|
821
|
+
for (const unit of analysis.units) {
|
|
822
|
+
if (!unit.valid)
|
|
823
|
+
continue;
|
|
824
|
+
const ops = [];
|
|
825
|
+
const delta = new Map();
|
|
826
|
+
// The columns from each item's marker to its text once the rule is done,
|
|
827
|
+
// and the fewest columns that its lines can need then.
|
|
828
|
+
const reach = new Map();
|
|
829
|
+
const lowest = new Map();
|
|
830
|
+
for (const list of unit.lists) {
|
|
831
|
+
for (const item of list.items)
|
|
832
|
+
analysis.match(item.first);
|
|
833
|
+
// As in markdownlint, a list whose items each take one line, with no
|
|
834
|
+
// blank line between them, gets the spacing for single lines.
|
|
835
|
+
const single = list.items.every((item, index) => item.first === item.last && (!index || item.first === list.items[index - 1].last + 1));
|
|
836
|
+
const wanted = spacing[list.node.ordered ? "ordered" : "bullet"][single ? 0 : 1];
|
|
837
|
+
const holder = list.parent?.type === "listItem" ? analysis.items.get(list.parent) : undefined;
|
|
838
|
+
// A nested list starts under its item's text, or as many columns from
|
|
839
|
+
// the item's marker as configured, where CommonMark lets it start. On
|
|
840
|
+
// the item's own marker line, columns before the nested marker would
|
|
841
|
+
// be the item's spacing, so a list that starts there stays. A first
|
|
842
|
+
// line that the last item of the list before it would continue would
|
|
843
|
+
// join that item.
|
|
844
|
+
const before = list.previous?.items.at(-1);
|
|
845
|
+
const indent = nested !== undefined && holder && list.items[0]?.first !== holder.first
|
|
846
|
+
? Math.max(0, Math.min(3, nested - (reach.get(holder) ?? reachOf(holder)), (before ? (lowest.get(before) ?? 0) : Infinity) - 1))
|
|
847
|
+
: 0;
|
|
848
|
+
const marks = [];
|
|
849
|
+
const changes = new Map();
|
|
850
|
+
let possible = true;
|
|
851
|
+
for (const item of list.items) {
|
|
852
|
+
if (item.width === undefined || item.start === undefined) {
|
|
853
|
+
possible = false;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
const offset = item.offset;
|
|
857
|
+
// The spacing stays where the item starts with indented code, which
|
|
858
|
+
// depends on it, or with a blank line, which has none.
|
|
859
|
+
const keep = item.blankFirst || item.spacing >= 5;
|
|
860
|
+
const target = keep ? item.spacing : wanted;
|
|
861
|
+
if (offset === indent && target === item.spacing)
|
|
862
|
+
continue;
|
|
863
|
+
const lineEnd = lines[item.first].end;
|
|
864
|
+
if ((offset !== indent && !onlySpaces(source, item.start, item.marker)) ||
|
|
865
|
+
(target !== item.spacing && !onlySpaces(source, item.markerEnd, item.spaceEnd)) ||
|
|
866
|
+
structuralTab(source, item.start, lineEnd)) {
|
|
867
|
+
possible = false;
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
const count = (value) => (value === 1 ? "one space" : `${value} spaces`);
|
|
871
|
+
if (offset !== indent)
|
|
872
|
+
marks.push({
|
|
873
|
+
start: item.start,
|
|
874
|
+
end: item.marker,
|
|
875
|
+
text: " ".repeat(indent),
|
|
876
|
+
message: indent
|
|
877
|
+
? `Indent the list marker by ${count(indent)}.`
|
|
878
|
+
: "Remove the indentation before the list marker.",
|
|
879
|
+
});
|
|
880
|
+
if (target !== item.spacing)
|
|
881
|
+
marks.push({
|
|
882
|
+
start: item.markerEnd,
|
|
883
|
+
end: item.spaceEnd,
|
|
884
|
+
text: " ".repeat(target),
|
|
885
|
+
message: `Put ${count(target)} after the list marker.`,
|
|
886
|
+
});
|
|
887
|
+
const change = indent - offset + (keep ? 0 : target - item.spacing);
|
|
888
|
+
if (change)
|
|
889
|
+
changes.set(item, change);
|
|
890
|
+
}
|
|
891
|
+
const minimum = (item) => indent +
|
|
892
|
+
(!list.node.ordered ? 1 : item.index ? 2 : item.digits.length + 1) +
|
|
893
|
+
(item.blankFirst || item.spacing >= 5 ? 1 : wanted);
|
|
894
|
+
const accepted = possible &&
|
|
895
|
+
marks.length > 0 &&
|
|
896
|
+
feasible(analysis, list, changes, "start", minimum, "move");
|
|
897
|
+
for (const item of list.items) {
|
|
898
|
+
reach.set(item, accepted ? reachOf(item, wanted) : reachOf(item));
|
|
899
|
+
const narrowest = (!list.node.ordered ? 1 : item.index ? 2 : item.digits.length + 1) +
|
|
900
|
+
(item.blankFirst || item.spacing >= 5 ? 1 : accepted ? wanted : item.spacing);
|
|
901
|
+
lowest.set(item, (accepted ? indent : (item.offset ?? 0)) + narrowest);
|
|
902
|
+
}
|
|
903
|
+
if (!accepted)
|
|
904
|
+
continue;
|
|
905
|
+
for (const op of marks)
|
|
906
|
+
ops.push(op);
|
|
907
|
+
for (const [item, change] of changes)
|
|
908
|
+
delta.set(item, change);
|
|
909
|
+
}
|
|
910
|
+
for (const op of shiftOps(analysis, unit, delta, "start"))
|
|
911
|
+
ops.push(op);
|
|
912
|
+
const finding = unitFinding(source, ops);
|
|
913
|
+
if (finding)
|
|
914
|
+
findings.push(finding);
|
|
915
|
+
}
|
|
916
|
+
return findings;
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
export const listRules = {
|
|
920
|
+
"style/list-indent": listIndent,
|
|
921
|
+
"style/list-marker": listMarker,
|
|
922
|
+
"style/ordered-list": orderedList,
|
|
923
|
+
};
|
|
924
|
+
//# sourceMappingURL=lists.js.map
|