mdrefine 0.2.0-rc.3 → 0.2.1

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.
@@ -128,11 +128,13 @@ function markersLoosenRuns() {
128
128
  /**
129
129
  * The delimiter runs at `positions`, sorted, with the characters that `at`
130
130
  * 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.
131
+ * run next to a `~` in GFM do both, and in some versions one next to another
132
+ * `*` or `_`; with `commonmark`, it follows CommonMark, where those characters
133
+ * are punctuation. Container markers such as `>` count as punctuation here,
134
+ * while micromark sees the line ending before them; the caller compares the
135
+ * result with the parse.
134
136
  */
135
- function delimiterRuns(positions, at, gfm) {
137
+ function delimiterRuns(positions, at, gfm, commonmark = false) {
136
138
  const runs = [];
137
139
  for (const position of positions) {
138
140
  const last = runs.at(-1);
@@ -147,8 +149,9 @@ function delimiterRuns(positions, at, gfm) {
147
149
  close: false,
148
150
  });
149
151
  }
150
- const loosen = markersLoosenRuns();
151
- const attention = (character) => (gfm && character === "~") || (loosen && (character === "*" || character === "_"));
152
+ const loosen = !commonmark && markersLoosenRuns();
153
+ const attention = (character) => (!commonmark && gfm && character === "~") ||
154
+ (loosen && (character === "*" || character === "_"));
152
155
  for (const run of runs) {
153
156
  const previous = at(run.start - 1);
154
157
  const next = at(run.end);
@@ -165,21 +168,23 @@ function delimiterRuns(positions, at, gfm) {
165
168
  * The emphasis and strong emphasis that micromark makes of `runs`, as
166
169
  * `start:end:size` strings: each closing run, in order, pairs with the nearest
167
170
  * opening run of its marker before it, except where the rule of three forbids
168
- * it, and the runs between them become text.
171
+ * it, and the runs between them become text. micromark applies the rule to the
172
+ * parts of two runs that are left; with `whole`, it counts the whole runs, as
173
+ * CommonMark, GitHub, and the reference parsers do (issue #323).
169
174
  */
170
- function pairings(runs) {
171
- const left = runs.map((run) => ({ ...run }));
175
+ function pairings(runs, whole = false) {
176
+ const left = runs.map((run) => ({ ...run, size: run.end - run.start }));
172
177
  const pairs = [];
173
178
  for (let index = 0; index < left.length; index++) {
174
179
  const closer = left[index];
175
180
  while (closer.close && closer.end > closer.start) {
176
- const closeSize = closer.end - closer.start;
181
+ const closeSize = whole ? closer.size : closer.end - closer.start;
177
182
  let found = index - 1;
178
183
  for (; found >= 0; found--) {
179
184
  const opener = left[found];
180
- const openSize = opener.end - opener.start;
181
- if (!opener.open || !openSize || opener.marker !== closer.marker)
185
+ if (!opener.open || opener.end === opener.start || opener.marker !== closer.marker)
182
186
  continue;
187
+ const openSize = whole ? opener.size : opener.end - opener.start;
183
188
  if ((opener.close || closer.open) && closeSize % 3 && !((openSize + closeSize) % 3))
184
189
  continue;
185
190
  break;
@@ -187,7 +192,7 @@ function pairings(runs) {
187
192
  if (found < 0)
188
193
  break;
189
194
  const opener = left[found];
190
- const use = opener.end - opener.start > 1 && closeSize > 1 ? 2 : 1;
195
+ const use = opener.end - opener.start > 1 && closer.end - closer.start > 1 ? 2 : 1;
191
196
  opener.end -= use;
192
197
  closer.start += use;
193
198
  pairs.push(`${opener.end}:${closer.start}:${use}`);
@@ -298,6 +303,25 @@ function eachInline(tree, type, visitor) {
298
303
  };
299
304
  step(tree, tree);
300
305
  }
306
+ /**
307
+ * Whether the text in `node`, outside code and other literal nodes, holds an
308
+ * unescaped run of tildes that could mark strikethrough in `dialect`, one or
309
+ * two tildes or in Obsidian two, while `scope`, where such runs pair, holds
310
+ * another.
311
+ */
312
+ function holdsStrikethroughRun(node, scope, source, dialect) {
313
+ const pattern = dialect === "obsidian" ? "(?<![\\\\~])~~(?!~)" : "(?<![\\\\~])~{1,2}(?!~)";
314
+ const runs = located(scope) ? source.slice(...range(scope)).match(new RegExp(pattern, "g")) : [];
315
+ if (located(scope) && (runs?.length ?? 0) < 2)
316
+ return false;
317
+ const run = new RegExp(pattern);
318
+ let found = false;
319
+ walk(node, "text", (text) => {
320
+ if (!found && located(text) && run.test(source.slice(...range(text))))
321
+ found = true;
322
+ });
323
+ return found;
324
+ }
301
325
  function markerRule(type, fallback) {
302
326
  return {
303
327
  description: `Choose ${type} delimiters without reprinting their contents.`,
@@ -378,6 +402,27 @@ function markerRule(type, fallback) {
378
402
  reaches(start + size, 1) ||
379
403
  reaches(end - size - 1, -1)))
380
404
  return;
405
+ // micromark pairs emphasis and strikethrough in separate passes, in the
406
+ // order that their first delimiters appear in the paragraph, while
407
+ // GitHub pairs whichever closes first. So emphasis whose text holds a
408
+ // run of tildes that could mark strikethrough may be strikethrough and
409
+ // text there, and its markers what GitHub shows (issue #320).
410
+ if (gfm && holdsStrikethroughRun(node, scope, source, document.dialect))
411
+ return;
412
+ // GitHub reads a `_` that a run of tildes separates from a letter or
413
+ // digit as inside a word, where it neither opens nor closes, while the
414
+ // parser reads emphasis there (issue #322).
415
+ const tildesToWord = (index, step) => {
416
+ if (source[index] !== "~")
417
+ return false;
418
+ while (source[index] === "~")
419
+ index += step;
420
+ return /[\p{L}\p{N}]/u.test(source[index] ?? "");
421
+ };
422
+ if (gfm &&
423
+ (marker[0] === "_" || original[0] === "_") &&
424
+ (tildesToWord(start - 1, -1) || tildesToWord(end, 1)))
425
+ return;
381
426
  const list = candidates.get(scope) ?? [];
382
427
  list.push([start, end]);
383
428
  candidates.set(scope, list);
@@ -401,7 +446,11 @@ function markerRule(type, fallback) {
401
446
  // share a run of markers, as in `***a***` or `*a****c***`.
402
447
  const { positions, emphasis, suspicious } = scopeDelimiters(source, scope);
403
448
  const expected = emphasis.join(" ");
404
- const pairing = (changed) => pairings(delimiterRuns(positions, (at) => (changed.has(at) ? marker[0] : source[at]), gfm)).join(" ");
449
+ // micromark's pairing, or with `commonmark` CommonMark's.
450
+ const pairing = (changed, commonmark = false) => pairings(delimiterRuns(positions, (at) => (changed.has(at) ? marker[0] : source[at]), gfm, commonmark), commonmark).join(" ");
451
+ // Both micromark's pairing and CommonMark's, which renderers follow,
452
+ // have to stay the same with the new markers.
453
+ const kept = (changed) => pairing(changed) === expected && pairing(changed, true) === expected;
405
454
  // micromark pairs `~` in a pass of its own, which can come first, and
406
455
  // lets a run next to a `~` pair with one of the new marker, as in `_f~*a*e_`.
407
456
  const tilde = gfm && source.slice(...range(scope)).includes("~");
@@ -409,12 +458,16 @@ function markerRule(type, fallback) {
409
458
  if (!modelled)
410
459
  accepted =
411
460
  suspicious || (tilde && positions.some((at) => source[at] === marker[0])) ? [] : list;
412
- else if (pairing(markers(list)) !== expected) {
461
+ // Where CommonMark pairs the text as written differently, the parser
462
+ // misread it, and renderers show other emphasis (issue #323).
463
+ else if (pairing(new Set(), true) !== expected)
464
+ accepted = [];
465
+ else if (!kept(markers(list))) {
413
466
  accepted = [];
414
467
  let changed = new Set();
415
468
  for (const item of list) {
416
469
  const trial = new Set([...changed, ...markers([item])]);
417
- if (pairing(trial) !== expected)
470
+ if (!kept(trial))
418
471
  continue;
419
472
  changed = trial;
420
473
  accepted.push(item);
@@ -654,9 +707,11 @@ function holds(node, type) {
654
707
  * The inline nodes of a paragraph, from `start` on, that reflow keeps whole,
655
708
  * sorted by position, and the hard line breaks anywhere in it. A container
656
709
  * that holds inline HTML is kept whole too, because HTML can make the
657
- * whitespace inside it significant.
710
+ * whitespace inside it significant. With `wholeLinks`, the text of a link is
711
+ * kept whole as an image's description is: Obsidian's Live Preview shows a
712
+ * link whose text spans lines as Markdown source (issue #306).
658
713
  */
659
- function inlineAtoms(paragraph, start) {
714
+ function inlineAtoms(paragraph, start, wholeLinks) {
660
715
  const atoms = [];
661
716
  const breaks = [];
662
717
  let unlocated = false;
@@ -674,6 +729,21 @@ function inlineAtoms(paragraph, start) {
674
729
  continue;
675
730
  if (child.type === "break")
676
731
  breaks.push([a, b]);
732
+ else if (wholeLinks &&
733
+ (child.type === "link" || child.type === "linkReference") &&
734
+ child.children.length &&
735
+ labelEnd(child) &&
736
+ !holds(child, "html")) {
737
+ // The code spans, images, and other atoms in the text keep their own
738
+ // joining; between them, a line ending joins with the space that it
739
+ // renders as, as in an image's description.
740
+ const close = labelEnd(child);
741
+ const inner = atoms.length;
742
+ visit(child);
743
+ const within = atoms.splice(inner).sort((x, y) => x.from - y.from);
744
+ atoms.push({ from: a, to: close, joining: { within } });
745
+ atoms.push({ from: close, to: b, joining: destination });
746
+ }
677
747
  else if (wrappable.has(child.type) &&
678
748
  "children" in child &&
679
749
  child.children.length &&
@@ -945,7 +1015,7 @@ const wrap = {
945
1015
  }
946
1016
  : {}),
947
1017
  };
948
- const inline = inlineAtoms(node, start);
1018
+ const inline = inlineAtoms(node, start, document.dialect === "obsidian");
949
1019
  const protection = new Protection(start, end);
950
1020
  for (const atom of inline.atoms)
951
1021
  protection.add(atom.from, atom.to);
@@ -1074,9 +1144,11 @@ const wrap = {
1074
1144
  break;
1075
1145
  }
1076
1146
  const last = words.at(-1);
1077
- const joining = protection.has(lineEnd)
1147
+ let joining = protection.has(lineEnd)
1078
1148
  ? (atomAt(inline.atoms, lineEnd)?.joining ?? { reason: "line break in inline syntax" })
1079
1149
  : undefined;
1150
+ if (joining && typeof joining === "object" && "within" in joining)
1151
+ joining = atomAt(joining.within, lineEnd)?.joining ?? "description";
1080
1152
  if (!joining)
1081
1153
  words.push(" ");
1082
1154
  else if (last === undefined || /^[ \t]+$/.test(last))
@@ -1096,7 +1168,7 @@ const wrap = {
1096
1168
  words[words.length - 1] = `${joining === "code" ? last : last.trimEnd()} `;
1097
1169
  }
1098
1170
  else
1099
- failure = joining.reason;
1171
+ failure = "reason" in joining ? joining.reason : "line break in inline syntax";
1100
1172
  }
1101
1173
  words.push(...protection.split(line, lineAt));
1102
1174
  lineEnd = lineAt + line.length;