mdrefine 0.2.0 → 0.2.2
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 +56 -0
- package/CONTRIBUTING.md +13 -0
- package/README.md +1 -1
- package/dist/core/engine.js +73 -10
- package/dist/core/engine.js.map +1 -1
- package/dist/rules/blocks.js +10 -3
- package/dist/rules/blocks.js.map +1 -1
- package/dist/rules/dialects.js +12 -2
- package/dist/rules/dialects.js.map +1 -1
- package/dist/rules/lists.js +17 -0
- package/dist/rules/lists.js.map +1 -1
- package/dist/rules/style.js +118 -35
- package/dist/rules/style.js.map +1 -1
- package/dist/syntax/misread-lists.d.ts +46 -0
- package/dist/syntax/misread-lists.js +100 -0
- package/dist/syntax/misread-lists.js.map +1 -0
- package/dist/syntax/parse.d.ts +27 -3
- package/dist/syntax/parse.js +55 -7
- package/dist/syntax/parse.js.map +1 -1
- package/docs/design.md +43 -21
- package/docs/dialects.md +13 -9
- package/docs/plugins.md +4 -4
- package/docs/quick-start.md +1 -1
- package/docs/rules.md +57 -17
- package/package.json +4 -1
package/dist/rules/style.js
CHANGED
|
@@ -2,7 +2,8 @@ import { walk } from "../syntax/walk.js";
|
|
|
2
2
|
import stringWidth from "string-width";
|
|
3
3
|
import { directive } from "../core/directives.js";
|
|
4
4
|
import { blockIdentifier } from "../syntax/block-identifier.js";
|
|
5
|
-
import {
|
|
5
|
+
import { misreadLists } from "../syntax/misread-lists.js";
|
|
6
|
+
import { codeSpanText, joinsTomlFrontMatter, labelEnd, lazyIndentation, parse, range, startsWithMarker, } from "../syntax/parse.js";
|
|
6
7
|
import { changesForgeHeading, headingAttributeBlock } from "../workspace/heading-attributes.js";
|
|
7
8
|
import { takesFinalLineEnding, takesLines, underlinedHeadings, underlineEdit } from "./blocks.js";
|
|
8
9
|
export function optionsSchema(properties) {
|
|
@@ -128,11 +129,13 @@ function markersLoosenRuns() {
|
|
|
128
129
|
/**
|
|
129
130
|
* The delimiter runs at `positions`, sorted, with the characters that `at`
|
|
130
131
|
* gives. Whether a run can open or close follows micromark, which also lets a
|
|
131
|
-
* run next to a `~` in GFM do both
|
|
132
|
-
*
|
|
133
|
-
*
|
|
132
|
+
* run next to a `~` in GFM do both, and in some versions one next to another
|
|
133
|
+
* `*` or `_`; with `commonmark`, it follows CommonMark, where those characters
|
|
134
|
+
* are punctuation. Container markers such as `>` count as punctuation here,
|
|
135
|
+
* while micromark sees the line ending before them; the caller compares the
|
|
136
|
+
* result with the parse.
|
|
134
137
|
*/
|
|
135
|
-
function delimiterRuns(positions, at, gfm) {
|
|
138
|
+
function delimiterRuns(positions, at, gfm, commonmark = false) {
|
|
136
139
|
const runs = [];
|
|
137
140
|
for (const position of positions) {
|
|
138
141
|
const last = runs.at(-1);
|
|
@@ -147,8 +150,9 @@ function delimiterRuns(positions, at, gfm) {
|
|
|
147
150
|
close: false,
|
|
148
151
|
});
|
|
149
152
|
}
|
|
150
|
-
const loosen = markersLoosenRuns();
|
|
151
|
-
const attention = (character) => (gfm && character === "~") ||
|
|
153
|
+
const loosen = !commonmark && markersLoosenRuns();
|
|
154
|
+
const attention = (character) => (!commonmark && gfm && character === "~") ||
|
|
155
|
+
(loosen && (character === "*" || character === "_"));
|
|
152
156
|
for (const run of runs) {
|
|
153
157
|
const previous = at(run.start - 1);
|
|
154
158
|
const next = at(run.end);
|
|
@@ -165,21 +169,23 @@ function delimiterRuns(positions, at, gfm) {
|
|
|
165
169
|
* The emphasis and strong emphasis that micromark makes of `runs`, as
|
|
166
170
|
* `start:end:size` strings: each closing run, in order, pairs with the nearest
|
|
167
171
|
* opening run of its marker before it, except where the rule of three forbids
|
|
168
|
-
* it, and the runs between them become text.
|
|
172
|
+
* it, and the runs between them become text. micromark applies the rule to the
|
|
173
|
+
* parts of two runs that are left; with `whole`, it counts the whole runs, as
|
|
174
|
+
* CommonMark, GitHub, and the reference parsers do (issue #323).
|
|
169
175
|
*/
|
|
170
|
-
function pairings(runs) {
|
|
171
|
-
const left = runs.map((run) => ({ ...run }));
|
|
176
|
+
function pairings(runs, whole = false) {
|
|
177
|
+
const left = runs.map((run) => ({ ...run, size: run.end - run.start }));
|
|
172
178
|
const pairs = [];
|
|
173
179
|
for (let index = 0; index < left.length; index++) {
|
|
174
180
|
const closer = left[index];
|
|
175
181
|
while (closer.close && closer.end > closer.start) {
|
|
176
|
-
const closeSize = closer.end - closer.start;
|
|
182
|
+
const closeSize = whole ? closer.size : closer.end - closer.start;
|
|
177
183
|
let found = index - 1;
|
|
178
184
|
for (; found >= 0; found--) {
|
|
179
185
|
const opener = left[found];
|
|
180
|
-
|
|
181
|
-
if (!opener.open || !openSize || opener.marker !== closer.marker)
|
|
186
|
+
if (!opener.open || opener.end === opener.start || opener.marker !== closer.marker)
|
|
182
187
|
continue;
|
|
188
|
+
const openSize = whole ? opener.size : opener.end - opener.start;
|
|
183
189
|
if ((opener.close || closer.open) && closeSize % 3 && !((openSize + closeSize) % 3))
|
|
184
190
|
continue;
|
|
185
191
|
break;
|
|
@@ -187,7 +193,7 @@ function pairings(runs) {
|
|
|
187
193
|
if (found < 0)
|
|
188
194
|
break;
|
|
189
195
|
const opener = left[found];
|
|
190
|
-
const use = opener.end - opener.start > 1 &&
|
|
196
|
+
const use = opener.end - opener.start > 1 && closer.end - closer.start > 1 ? 2 : 1;
|
|
191
197
|
opener.end -= use;
|
|
192
198
|
closer.start += use;
|
|
193
199
|
pairs.push(`${opener.end}:${closer.start}:${use}`);
|
|
@@ -298,6 +304,25 @@ function eachInline(tree, type, visitor) {
|
|
|
298
304
|
};
|
|
299
305
|
step(tree, tree);
|
|
300
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Whether the text in `node`, outside code and other literal nodes, holds an
|
|
309
|
+
* unescaped run of tildes that could mark strikethrough in `dialect`, one or
|
|
310
|
+
* two tildes or in Obsidian two, while `scope`, where such runs pair, holds
|
|
311
|
+
* another.
|
|
312
|
+
*/
|
|
313
|
+
function holdsStrikethroughRun(node, scope, source, dialect) {
|
|
314
|
+
const pattern = dialect === "obsidian" ? "(?<![\\\\~])~~(?!~)" : "(?<![\\\\~])~{1,2}(?!~)";
|
|
315
|
+
const runs = located(scope) ? source.slice(...range(scope)).match(new RegExp(pattern, "g")) : [];
|
|
316
|
+
if (located(scope) && (runs?.length ?? 0) < 2)
|
|
317
|
+
return false;
|
|
318
|
+
const run = new RegExp(pattern);
|
|
319
|
+
let found = false;
|
|
320
|
+
walk(node, "text", (text) => {
|
|
321
|
+
if (!found && located(text) && run.test(source.slice(...range(text))))
|
|
322
|
+
found = true;
|
|
323
|
+
});
|
|
324
|
+
return found;
|
|
325
|
+
}
|
|
301
326
|
function markerRule(type, fallback) {
|
|
302
327
|
return {
|
|
303
328
|
description: `Choose ${type} delimiters without reprinting their contents.`,
|
|
@@ -378,6 +403,27 @@ function markerRule(type, fallback) {
|
|
|
378
403
|
reaches(start + size, 1) ||
|
|
379
404
|
reaches(end - size - 1, -1)))
|
|
380
405
|
return;
|
|
406
|
+
// micromark pairs emphasis and strikethrough in separate passes, in the
|
|
407
|
+
// order that their first delimiters appear in the paragraph, while
|
|
408
|
+
// GitHub pairs whichever closes first. So emphasis whose text holds a
|
|
409
|
+
// run of tildes that could mark strikethrough may be strikethrough and
|
|
410
|
+
// text there, and its markers what GitHub shows (issue #320).
|
|
411
|
+
if (gfm && holdsStrikethroughRun(node, scope, source, document.dialect))
|
|
412
|
+
return;
|
|
413
|
+
// GitHub reads a `_` that a run of tildes separates from a letter or
|
|
414
|
+
// digit as inside a word, where it neither opens nor closes, while the
|
|
415
|
+
// parser reads emphasis there (issue #322).
|
|
416
|
+
const tildesToWord = (index, step) => {
|
|
417
|
+
if (source[index] !== "~")
|
|
418
|
+
return false;
|
|
419
|
+
while (source[index] === "~")
|
|
420
|
+
index += step;
|
|
421
|
+
return /[\p{L}\p{N}]/u.test(source[index] ?? "");
|
|
422
|
+
};
|
|
423
|
+
if (gfm &&
|
|
424
|
+
(marker[0] === "_" || original[0] === "_") &&
|
|
425
|
+
(tildesToWord(start - 1, -1) || tildesToWord(end, 1)))
|
|
426
|
+
return;
|
|
381
427
|
const list = candidates.get(scope) ?? [];
|
|
382
428
|
list.push([start, end]);
|
|
383
429
|
candidates.set(scope, list);
|
|
@@ -401,7 +447,11 @@ function markerRule(type, fallback) {
|
|
|
401
447
|
// share a run of markers, as in `***a***` or `*a****c***`.
|
|
402
448
|
const { positions, emphasis, suspicious } = scopeDelimiters(source, scope);
|
|
403
449
|
const expected = emphasis.join(" ");
|
|
404
|
-
|
|
450
|
+
// micromark's pairing, or with `commonmark` CommonMark's.
|
|
451
|
+
const pairing = (changed, commonmark = false) => pairings(delimiterRuns(positions, (at) => (changed.has(at) ? marker[0] : source[at]), gfm, commonmark), commonmark).join(" ");
|
|
452
|
+
// Both micromark's pairing and CommonMark's, which renderers follow,
|
|
453
|
+
// have to stay the same with the new markers.
|
|
454
|
+
const kept = (changed) => pairing(changed) === expected && pairing(changed, true) === expected;
|
|
405
455
|
// micromark pairs `~` in a pass of its own, which can come first, and
|
|
406
456
|
// lets a run next to a `~` pair with one of the new marker, as in `_f~*a*e_`.
|
|
407
457
|
const tilde = gfm && source.slice(...range(scope)).includes("~");
|
|
@@ -409,12 +459,16 @@ function markerRule(type, fallback) {
|
|
|
409
459
|
if (!modelled)
|
|
410
460
|
accepted =
|
|
411
461
|
suspicious || (tilde && positions.some((at) => source[at] === marker[0])) ? [] : list;
|
|
412
|
-
|
|
462
|
+
// Where CommonMark pairs the text as written differently, the parser
|
|
463
|
+
// misread it, and renderers show other emphasis (issue #323).
|
|
464
|
+
else if (pairing(new Set(), true) !== expected)
|
|
465
|
+
accepted = [];
|
|
466
|
+
else if (!kept(markers(list))) {
|
|
413
467
|
accepted = [];
|
|
414
468
|
let changed = new Set();
|
|
415
469
|
for (const item of list) {
|
|
416
470
|
const trial = new Set([...changed, ...markers([item])]);
|
|
417
|
-
if (
|
|
471
|
+
if (!kept(trial))
|
|
418
472
|
continue;
|
|
419
473
|
changed = trial;
|
|
420
474
|
accepted.push(item);
|
|
@@ -702,7 +756,15 @@ function inlineAtoms(paragraph, start, wholeLinks) {
|
|
|
702
756
|
visit(child);
|
|
703
757
|
}
|
|
704
758
|
else if (child.type === "inlineCode")
|
|
705
|
-
atoms.push({
|
|
759
|
+
atoms.push({
|
|
760
|
+
from: a,
|
|
761
|
+
to: b,
|
|
762
|
+
// The spaces that start a lazy continuation line stay in the span,
|
|
763
|
+
// which joining would drop, as after `>* ` with no space (issue #339).
|
|
764
|
+
joining: lazyIndentation(child)
|
|
765
|
+
? { reason: "code span continuing on an indented lazy line" }
|
|
766
|
+
: "code",
|
|
767
|
+
});
|
|
706
768
|
else if ((child.type === "image" || child.type === "imageReference") && labelEnd(child)) {
|
|
707
769
|
const close = labelEnd(child);
|
|
708
770
|
atoms.push({ from: a, to: close, joining: "description" });
|
|
@@ -1109,6 +1171,11 @@ const wrap = {
|
|
|
1109
1171
|
// keeps. A lazy line, which keeps them, is not supported above.
|
|
1110
1172
|
if (joining === "description" && /(?: {2}|\\)$/.test(last))
|
|
1111
1173
|
failure = "hard line break";
|
|
1174
|
+
// In a code span, a backslash before the line ending is a hard
|
|
1175
|
+
// break wherever the backticks do not pair, as on GitHub after an
|
|
1176
|
+
// unpaired longer run (issue #335).
|
|
1177
|
+
if (joining === "code" && last.endsWith("\\"))
|
|
1178
|
+
failure = "backslash at a line end in a code span";
|
|
1112
1179
|
const trimmed = line.replace(/^[ \t]+/, "");
|
|
1113
1180
|
lineAt += line.length - trimmed.length;
|
|
1114
1181
|
line = trimmed;
|
|
@@ -1230,19 +1297,19 @@ const table = {
|
|
|
1230
1297
|
};
|
|
1231
1298
|
/** A line holding nothing but block quote markers and blank space. */
|
|
1232
1299
|
const blankLine = /^[ \t>]*$/;
|
|
1233
|
-
|
|
1234
|
-
* A list item or quote marker followed by another list marker on its line,
|
|
1235
|
-
* including the ones that cannot open a list there, such as `* * *`.
|
|
1236
|
-
*/
|
|
1237
|
-
const nestedListStart = /^[ \t]*(?:[-+*]|\d{1,9}[.)]|>)[ \t>]*(?:[-+*]|\d{1,9}[.)])(?:[ \t]|$)/;
|
|
1300
|
+
const blockContainers = ["blockquote", "list", "listItem", "footnoteDefinition"];
|
|
1238
1301
|
/** The innermost last block of a container, where its text ends. */
|
|
1239
1302
|
function lastBlock(node) {
|
|
1240
|
-
while (
|
|
1241
|
-
"children" in node &&
|
|
1242
|
-
node.children.length)
|
|
1303
|
+
while (blockContainers.includes(node.type) && "children" in node && node.children.length)
|
|
1243
1304
|
node = node.children.at(-1);
|
|
1244
1305
|
return node;
|
|
1245
1306
|
}
|
|
1307
|
+
/** The innermost first block of a container, where its text starts. */
|
|
1308
|
+
function firstBlock(node) {
|
|
1309
|
+
while (blockContainers.includes(node.type) && "children" in node && node.children.length)
|
|
1310
|
+
node = node.children[0];
|
|
1311
|
+
return node;
|
|
1312
|
+
}
|
|
1246
1313
|
/**
|
|
1247
1314
|
* Whether two neighboring blocks have to stay as they are, although one of
|
|
1248
1315
|
* them is a heading or a list, because a blank line between them would change
|
|
@@ -1294,17 +1361,24 @@ function together(document, parent, index, directiveLines) {
|
|
|
1294
1361
|
last.type === "footnoteDefinition" &&
|
|
1295
1362
|
!last.children.length)
|
|
1296
1363
|
return true;
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
// a
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1364
|
+
// Renderers may read a list where the parser read text that starts with a
|
|
1365
|
+
// list marker, the blocks indented below it inside that list's last item,
|
|
1366
|
+
// and a list after those as more of its items (issue #191). A blank line
|
|
1367
|
+
// above a list or quote that starts with such text, as `- -` or `- 2. Step`
|
|
1368
|
+
// directly below text, would make the parser read the list too, and one
|
|
1369
|
+
// between the blocks of such a list would make it loose.
|
|
1370
|
+
const misread = misreadLists(document);
|
|
1371
|
+
if (misread.nested.has(after) ||
|
|
1372
|
+
(after.type === "list" && misread.next.has(after)) ||
|
|
1373
|
+
misread.text.has(firstBlock(after)))
|
|
1374
|
+
return true;
|
|
1375
|
+
// Directly below text or a link reference definition, `-` underlined with
|
|
1376
|
+
// `---` is a heading, and below a blank line an empty list item and a
|
|
1377
|
+
// thematic break.
|
|
1378
|
+
if (after.type !== "heading" || !["paragraph", "definition"].includes(lastBlock(before).type))
|
|
1303
1379
|
return false;
|
|
1304
1380
|
const firstLine = source.slice(afterStart, lineBounds(source, afterStart, afterStart)[1]);
|
|
1305
|
-
|
|
1306
|
-
return !/^[ \t]*#/.test(firstLine);
|
|
1307
|
-
return (after.type === "list" || after.type === "blockquote") && nestedListStart.test(firstLine);
|
|
1381
|
+
return !/^[ \t]*#/.test(firstLine);
|
|
1308
1382
|
}
|
|
1309
1383
|
const blockSpacing = {
|
|
1310
1384
|
description: "Separate headings and lists from neighboring blocks with a blank line.",
|
|
@@ -1416,6 +1490,11 @@ export const styleRules = {
|
|
|
1416
1490
|
// marker, which holds an Obsidian callout's title.
|
|
1417
1491
|
if (titles.some(([a, b]) => start >= a && start < b))
|
|
1418
1492
|
return;
|
|
1493
|
+
// Where its backticks do not pair, as on GitHub after an unpaired
|
|
1494
|
+
// longer run, a backslash at the end of a line is a hard line break
|
|
1495
|
+
// (issue #335).
|
|
1496
|
+
if (/\\(?:\r\n|\r|\n)/.test(source.slice(start, end)))
|
|
1497
|
+
return;
|
|
1419
1498
|
if (document.dialect === "obsidian") {
|
|
1420
1499
|
// Obsidian reads highlights and wikilinks only within a line. Once
|
|
1421
1500
|
// the lines are one, an `==` or `[[` left as text could open one that
|
|
@@ -1540,6 +1619,10 @@ export const styleRules = {
|
|
|
1540
1619
|
}
|
|
1541
1620
|
if (underlined || !first || !last)
|
|
1542
1621
|
return;
|
|
1622
|
+
// Renderers without TOML front matter read its lines as part of a
|
|
1623
|
+
// heading underlined directly below it (issue #333).
|
|
1624
|
+
if (joinsTomlFrontMatter(document.tree, node))
|
|
1625
|
+
return;
|
|
1543
1626
|
// Below link reference definitions, the heading's position starts at
|
|
1544
1627
|
// the first definition; its own text starts on a later line. Headings
|
|
1545
1628
|
// that are indented or inside lists or block quotes are left as written.
|