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.
@@ -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 { codeSpanText, labelEnd, lazyIndentation, parse, range, startsWithMarker, } from "../syntax/parse.js";
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. Container markers such as `>` count as
132
- * punctuation here, while micromark sees the line ending before them; the
133
- * caller compares the result with the parse.
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 === "~") || (loosen && (character === "*" || 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
- const openSize = opener.end - opener.start;
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 && closeSize > 1 ? 2 : 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
- const pairing = (changed) => pairings(delimiterRuns(positions, (at) => (changed.has(at) ? marker[0] : source[at]), gfm)).join(" ");
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
- else if (pairing(markers(list)) !== expected) {
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 (pairing(trial) !== expected)
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({ from: a, to: b, joining: "code" });
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 (["blockquote", "list", "listItem", "footnoteDefinition"].includes(node.type) &&
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
- // Directly below text or a link reference definition, the parser does not
1298
- // start a list that would be empty or not start at 1, though it does below
1299
- // a blank line: there, `- -` is a list item holding the text `-`, and `-`
1300
- // underlined with `---` is a heading. Such a list could start on the first
1301
- // line of a list or quote, and such a line could begin an underlined heading.
1302
- if (!["paragraph", "definition"].includes(lastBlock(before).type))
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
- if (after.type === "heading")
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.