Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
5edef4a
test(markdown-codec): kill list-id numId mutants and drop an unreacha…
Mearman Sep 12, 2026
955892a
test(markdown-codec): cover parseHeadingStyleId's integer and positiv…
Mearman Sep 12, 2026
64e15e2
test(markdown-codec): pin lowerTable's column-width division and abse…
Mearman Sep 12, 2026
dfdd940
test(markdown-codec): add direct coverage for matchMathInlineSpan's g…
Mearman Sep 12, 2026
dc3a4cf
test(markdown-codec): add direct coverage for the footnote label/mark…
Mearman Sep 12, 2026
ab5c4cd
test(markdown-codec): pin resolveMarkdownImage's independent width/he…
Mearman Sep 12, 2026
00ad099
test(markdown-codec): pin emitImage's alt fallback for altText-less i…
Mearman Sep 12, 2026
0bc6400
test(markdown-codec): add direct coverage for isMarkdownBlockNode/isM…
Mearman Sep 12, 2026
0c90d22
refactor(markdown-codec): always splice readMarkdown's own definition…
Mearman Sep 12, 2026
2959259
refactor(markdown-codec): drop two unobservable branches in LineCursor
Mearman Sep 12, 2026
50b87a1
test(markdown-codec): add direct coverage for InlineNode's linked-lis…
Mearman Sep 12, 2026
b0964c5
refactor(markdown-codec): drop unobservable guards in list-marker/tig…
Mearman Sep 12, 2026
3d6295f
refactor(markdown-codec): make link primitives' loop bounds mutation-…
Mearman Sep 12, 2026
ca379c0
refactor(markdown-codec): remove three provably-unreachable guards fr…
Mearman Sep 12, 2026
cbddfa9
refactor(markdown-codec): drop two redundant '<'-prefix guards in the…
Mearman Sep 12, 2026
118d935
test(markdown-codec): add direct coverage for BlockNode's own methods…
Mearman Sep 12, 2026
337fba2
refactor(markdown-codec): remove length bounds absorbed by charAt's o…
Mearman Sep 12, 2026
f0934e9
refactor(markdown-codec): remove two more redundant guards, add codep…
Mearman Sep 12, 2026
4cae109
refactor(markdown-codec): drop table.ts's redundant escaped-pipe look…
Mearman Sep 13, 2026
90c8822
refactor(markdown-codec): drop list.ts's redundant marker-match fields
Mearman Sep 13, 2026
8bccabf
refactor(markdown-codec): drop skipInlineWhitespace's redundant range…
Mearman Sep 13, 2026
09dce3e
refactor(markdown-codec): drop unescapeString's redundant '&'-prefix …
Mearman Sep 13, 2026
95c0021
test(markdown-codec): pin next() leaving cursor state untouched past end
Mearman Sep 13, 2026
1e07560
test(markdown-codec): pin table cell diagnostics and drop two redunda…
Mearman Sep 13, 2026
26bcdad
fix(markdown-codec): remove an exhausted closer's delimiter before it…
Mearman Sep 13, 2026
603516e
test(markdown-codec): add a dedicated unit suite for the corpus loader
Mearman Sep 13, 2026
a4e1772
refactor(markdown-codec): drop definitions.ts's redundant label-lengt…
Mearman Sep 13, 2026
b8c7bff
test(markdown-codec): pin throw-tier error classes' own fields
Mearman Sep 14, 2026
0529999
test(markdown-codec): pin construct-extent, marker-balance, and write…
Mearman Sep 14, 2026
acaa42b
test(markdown-codec): kill diagnostics.ts's this.name assignment mutants
Mearman Sep 14, 2026
71e3922
test(markdown-codec): kill emit/front-matter.ts's quoting and escapin…
Mearman Sep 14, 2026
0843a22
fix(markdown-codec): stop building the function-with-fields test fixt…
Mearman Sep 15, 2026
518ead6
refactor(markdown-codec): merge lowerInlineNodes' text and entity cases
Mearman Sep 15, 2026
487a489
refactor(markdown-codec): drop render.ts's unkillable guards and case…
Mearman Sep 15, 2026
60e8776
test(markdown-codec): add direct coverage for lowerInlineNodes' own l…
Mearman Sep 15, 2026
134d293
test(markdown-codec): add direct coverage for the HTML render oracle
Mearman Sep 15, 2026
d15cb7c
refactor(markdown-codec): drop render.ts's cr() calls that can never …
Mearman Sep 15, 2026
fbaff7f
test(markdown-codec): pin the remaining render.ts mutants directly
Mearman Sep 15, 2026
eaa65e9
refactor(markdown-codec): drop codePointAt's unreachable undefined guard
Mearman Sep 15, 2026
85c5fa5
test(markdown-codec): add direct coverage for chars.ts's own boundaries
Mearman Sep 15, 2026
5fd8d74
test(markdown-codec): add direct coverage for front-matter parsing
Mearman Sep 15, 2026
9c7a598
test(markdown-codec): pin mathBlock's and footnoteDefinition's own cr()
Mearman Sep 15, 2026
7d47a18
refactor(markdown-codec): drop two more of chars.ts's unkillable guards
Mearman Sep 15, 2026
5422db6
test(markdown-codec): pin chars.ts's own boundaries the fresh run found
Mearman Sep 15, 2026
ecac838
refactor(markdown-codec): drop front-matter.ts's own unkillable loops
Mearman Sep 15, 2026
edda639
test(markdown-codec): pin front-matter.ts's remaining boundaries
Mearman Sep 15, 2026
1952940
test(markdown-codec): cover image.ts's base64 codec and format-sniffi…
Mearman Sep 15, 2026
970aeed
refactor(markdown-codec): drop emit.ts's own unkillable length-bound …
Mearman Sep 15, 2026
71c2ae1
test(markdown-codec): cover emit.ts's terminatesCleanly, isQuotableSt…
Mearman Sep 15, 2026
3a530c8
refactor(markdown-codec): drop canInterruptOpenParagraph's own
Mearman Sep 15, 2026
b9c83f8
test(markdown-codec): cover emit.ts's quote/fence/tab-stop boundaries
Mearman Sep 15, 2026
53d3de1
test(markdown-codec): assert diagnostic message content for
Mearman Sep 15, 2026
5a05c6b
test(markdown-codec): cover emit.ts's line-break-collapse message
Mearman Sep 15, 2026
a9535d3
test(markdown-codec): cover checkbox glyph stripping and
Mearman Sep 15, 2026
376ec0d
test(markdown-codec): cover renderConstruct's unrepresentable shapes
Mearman Sep 15, 2026
d62cd3e
test(markdown-codec): cover divisionDepth's own restore-on-exit and
Mearman Sep 15, 2026
be82eec
refactor(markdown-codec): drop renderListRegion's own unreachable
Mearman Sep 15, 2026
0cd6198
test(markdown-codec): cover the ballot-box-glyph/task-flag guard and
Mearman Sep 15, 2026
7b547c5
test(markdown-codec): cover blank-line indentation for a LATER
Mearman Sep 15, 2026
33d5996
test(markdown-codec): assert message content for the two remaining
Mearman Sep 15, 2026
2dd1ac4
test(markdown-codec): fix the UNCHECKED glyph test to actually
Mearman Sep 15, 2026
7227c89
test(markdown-codec): cover lastStyleIdOf's own last-child lookup
Mearman Sep 15, 2026
0442655
test(markdown-codec): cover the nested sub-list's own last-block
Mearman Sep 15, 2026
9df1e0a
test(markdown-codec): cover openMemberships' own same-level pop
Mearman Sep 15, 2026
f693741
test(markdown-codec): cover isMaterialisedDivision's own every-vs-some
Mearman Sep 15, 2026
8ae547f
test(markdown-codec): cover constructCarriesListItemId's own
Mearman Sep 15, 2026
6567e26
test(markdown-codec): cover validateRunConstructExtents' own
Mearman Sep 15, 2026
72c742e
refactor(markdown-codec): drop collectListItem's own unkillable
Mearman Sep 15, 2026
5516e26
test(markdown-codec): cover willRenderAsSetext's own level boundary
Mearman Sep 15, 2026
b694fad
refactor(markdown-codec): drop renderConstruct's own redundant
Mearman Sep 15, 2026
94fe915
test(markdown-codec): cover firstBlockCheckbox's own stripGlyph:
Mearman Sep 15, 2026
52fb492
test(markdown-codec): cover the link construct's own exact-one-child
Mearman Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/markdown-codec/src/ast/ast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { isMarkdownBlockNode, isMarkdownInlineNode } from "./ast";
import type { MarkdownNode } from "./ast";

const blockNode: MarkdownNode = { type: "paragraph", children: [] };
const inlineNode: MarkdownNode = { type: "text", value: "hi" };

describe("isMarkdownBlockNode / isMarkdownInlineNode", () => {
it("classifies a block node as a block and not inline", () => {
expect(isMarkdownBlockNode(blockNode)).toBe(true);
expect(isMarkdownInlineNode(blockNode)).toBe(false);
});

it("classifies an inline node as inline and not a block", () => {
expect(isMarkdownBlockNode(inlineNode)).toBe(false);
expect(isMarkdownInlineNode(inlineNode)).toBe(true);
});

it("recognises every real block node type named in the table, not just one representative", () => {
const types: MarkdownNode["type"][] = [
"document",
"paragraph",
"heading",
"blockquote",
"list",
"listItem",
"codeBlock",
"thematicBreak",
"htmlBlock",
"table",
"tableRow",
"tableCell",
"mathBlock",
"footnoteDefinition",
];
for (const type of types) {
// isMarkdownBlockNode reads only `.type`, so a bare-type fixture is a faithful runtime input; the cast is unavoidable since a real MarkdownNode variant also carries fields (children, value, ...) this loop has no reason to construct per type.
expect(isMarkdownBlockNode({ type } as MarkdownNode)).toBe(true);
}
});
});
10 changes: 10 additions & 0 deletions packages/markdown-codec/src/block/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,16 @@ describe("recover-tier diagnostics", () => {
).toBe(true);
});

it("reports each duplicate definition's own line, counted from how many newlines precede it within the paragraph", () => {
const collector = createDiagnosticCollector();
parseMarkdown("[a]: /1\n[a]: /2\n[a]: /3", { sink: collector.sink });
const duplicates = collector.diagnostics.filter(
(diagnostic) =>
diagnostic.code === MarkdownDiagnosticCodes.DUPLICATE_LINK_REFERENCE,
);
expect(duplicates.map((diagnostic) => diagnostic.line)).toEqual([2, 3]);
});

it("reports a math block never closed by a matching $$ before end-of-input", () => {
const collector = createDiagnosticCollector();
parseMarkdown("$$\nx^2", { sink: collector.sink });
Expand Down
46 changes: 46 additions & 0 deletions packages/markdown-codec/src/block/definitions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Direct tests for extractDefinitions -- the higher-level parseMarkdown suite (src/block/block.test.ts) exercises this through whole documents, which never isolates the exact cursor arithmetic that decides where one definition ends and the residual paragraph content begins.

import { describe, expect, it } from "vitest";
import { extractDefinitions } from "./definitions";
import type { LinkReferenceDefinition } from "../inline/link";

describe("extractDefinitions", () => {
it("leaves ordinary text on a following line as the residual paragraph content", () => {
const references = new Map<string, LinkReferenceDefinition>();
const rest = extractDefinitions("[a]: /url\nsome text", references);
expect(rest).toBe("some text");
expect(references.get("A")).toEqual({ destination: "/url" });
});

it("ends the definition at the real line's own newline, not merely one past where the destination itself finished, when trailing spaces sit between them", () => {
const references = new Map<string, LinkReferenceDefinition>();
const rest = extractDefinitions("[a]: /url \nsome text", references);
expect(rest).toBe("some text");
});

it("consumes a definition with no trailing newline entirely, leaving nothing behind", () => {
const references = new Map<string, LinkReferenceDefinition>();
const rest = extractDefinitions("[a]: /url", references);
expect(rest).toBe("");
expect(references.get("A")).toEqual({ destination: "/url" });
});

it("does not treat a label with only whitespace between its brackets as a definition at all", () => {
const references = new Map<string, LinkReferenceDefinition>();
const rest = extractDefinitions("[ ]: /url\nrest", references);
expect(rest).toBe("[ ]: /url\nrest");
expect(references.size).toBe(0);
});

it("reports the exact duplicate-definition message, naming the losing label", () => {
const messages: string[] = [];
extractDefinitions(
"[a]: /1\n[a]: /2",
new Map<string, LinkReferenceDefinition>(),
(diagnostic) => messages.push(diagnostic.message),
);
expect(messages).toEqual([
'link reference definition "A" was already defined earlier in the document; this later definition is ignored',
]);
});
});
15 changes: 2 additions & 13 deletions packages/markdown-codec/src/block/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ import {
skipInlineWhitespace,
} from "../inline/link";

// A definition needs a label with at least one non-whitespace character between its brackets, so the shortest possible match is `[x]` -- three characters.
const MIN_DEFINITION_LABEL_LENGTH = 3;

interface ParsedDefinition {
readonly label: string;
readonly definition: LinkReferenceDefinition;
Expand All @@ -32,10 +29,8 @@ function parseDefinition(
content: string,
start: number,
): ParsedDefinition | undefined {
// No separate "is the label at least [x] long" length guard: matchLinkLabel returns 0 (no bracket at all) or a real bracket-pair length of 2 or more, and a length-2 match ("[]") slices to an empty inner label just as a length-0 match's own empty slice does -- both already fall out of the label.length === 0 check below, so a dedicated minimum-length rejection could never see a case the empty-label check doesn't already reject.
const labelLength = matchLinkLabel(content, start);
if (labelLength < MIN_DEFINITION_LABEL_LENGTH) {
return undefined;
}
const label = normalizeLinkLabel(content.slice(start, start + labelLength));
if (label.length === 0) {
return undefined;
Expand Down Expand Up @@ -109,11 +104,5 @@ export function extractDefinitions(
}

function countNewlines(content: string, upTo: number): number {
let count = 0;
for (let index = 0; index < upTo && index < content.length; index += 1) {
if (content.charAt(index) === "\n") {
count += 1;
}
}
return count;
return content.slice(0, upTo).split("\n").length - 1;
}
16 changes: 16 additions & 0 deletions packages/markdown-codec/src/block/line.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { LineCursor } from "./line";

describe("LineCursor", () => {
it("reports an empty line as blank as soon as it is constructed", () => {
expect(new LineCursor("").blank).toBe(true);
});

it("reports a non-empty line as not blank", () => {
expect(new LineCursor("foo").blank).toBe(false);
});

it("reports a whitespace-only line as blank", () => {
expect(new LineCursor(" ").blank).toBe(true);
});
});
8 changes: 4 additions & 4 deletions packages/markdown-codec/src/block/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export class LineCursor {
private readonly cursor: MarkdownScanCursor;
private nextNonspaceMark: MarkdownScanMark;
private nextNonspaceColumn = 0;
private lineIsBlank = false;
// No default value: the constructor unconditionally calls findNextNonspace() below, which always assigns this before any getter can read it, so a placeholder default would be overwritten on every construction path and could never be observed to differ.
private lineIsBlank!: boolean;

constructor(text: string) {
this.text = text;
Expand Down Expand Up @@ -78,10 +79,9 @@ export class LineCursor {

// Advances up to `columns` columns, stopping at end of line. A tab straddling the target is consumed only as far as needed, leaving its remaining columns for rest() to materialise -- which is exactly how `>\tfoo` puts three columns of indentation, not a whole tab, into the block quote's content.
advance(columns: number): void {
// No early exit at end of line: MarkdownScanCursor.next() is already a side-effect-free no-op once rawOffset reaches the source length (src/scan/scan.ts), so looping the remaining count down regardless produces the identical end state as returning early -- an early-return branch here would be unobservable by any test, on purpose or not.
for (let remaining = columns; remaining > 0; remaining -= 1) {
if (this.cursor.next() === undefined) {
return;
}
this.cursor.next();
}
}

Expand Down
102 changes: 102 additions & 0 deletions packages/markdown-codec/src/block/list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import { finalizeListTightness, listsMatch } from "./list";
import { BlockNode } from "./node";
import type { ListMarkerData } from "./node";

const bullet = (bulletChar: "-" | "*" | "+"): ListMarkerData => ({
type: "bullet",
bulletChar,
padding: 2,
markerOffset: 0,
});

const ordered = (delimiter: "." | ")"): ListMarkerData => ({
type: "ordered",
delimiter,
padding: 3,
markerOffset: 0,
});

describe("listsMatch", () => {
it("matches two bullet markers with the same bullet character", () => {
expect(listsMatch(bullet("-"), bullet("-"))).toBe(true);
});

it("never matches a bullet marker against an ordered one, even if every other field happened to line up", () => {
expect(listsMatch(bullet("-"), ordered("."))).toBe(false);
});

it("does not match two ordered markers with different delimiters", () => {
expect(listsMatch(ordered("."), ordered(")"))).toBe(false);
});

it("does not match two bullet markers with different bullet characters", () => {
expect(listsMatch(bullet("-"), bullet("*"))).toBe(false);
});
});

describe("finalizeListTightness's own lastLineChecked memoisation", () => {
it("marks a descended list/listItem node's own lastLineChecked, so a later finalisation over the same chain does not re-walk it", () => {
const list = new BlockNode("list", 1);
// item1 is the one endsWithBlankLine is actually called on: finalizeListTightness only checks an item that has a FOLLOWING sibling (item1, since item2 follows it), never the last item in the list on its own account.
const item1 = new BlockNode("listItem", 1);
const leaf = new BlockNode("paragraph", 1);
item1.appendChild(leaf);
const item2 = new BlockNode("listItem", 2);
list.appendChild(item1);
list.appendChild(item2);

expect(item1.lastLineChecked).toBe(false);
expect(leaf.lastLineChecked).toBe(false);

finalizeListTightness(list);

// item1 is a listItem, so descending into it (to check its own lastChild for a trailing blank line) must have marked it checked; leaf is not list/listItem-kinded, so it is marked checked at the point the descent stops on it rather than being descended into.
expect(item1.lastLineChecked).toBe(true);
expect(leaf.lastLineChecked).toBe(true);
});

it("keeps a list tight when nothing is blank", () => {
const list = new BlockNode("list", 1);
const item1 = new BlockNode("listItem", 1);
const item2 = new BlockNode("listItem", 2);
list.appendChild(item1);
list.appendChild(item2);

finalizeListTightness(list);

expect(list.tight).toBe(true);
});

it("marks a list loose when an earlier item ends with a blank line before a following item", () => {
const list = new BlockNode("list", 1);
const item1 = new BlockNode("listItem", 1);
item1.lastLineBlank = true;
const item2 = new BlockNode("listItem", 2);
list.appendChild(item1);
list.appendChild(item2);

finalizeListTightness(list);

expect(list.tight).toBe(false);
});

it("descends through a listItem, not just a nested list, to find a blank line one level further down", () => {
const list = new BlockNode("list", 1);
const item1 = new BlockNode("listItem", 1);
const nestedList = new BlockNode("list", 1);
const nestedItem = new BlockNode("listItem", 1);
const deepLeaf = new BlockNode("paragraph", 1);
deepLeaf.lastLineBlank = true;
nestedItem.appendChild(deepLeaf);
nestedList.appendChild(nestedItem);
item1.appendChild(nestedList);
const item2 = new BlockNode("listItem", 2);
list.appendChild(item1);
list.appendChild(item2);

finalizeListTightness(list);

expect(list.tight).toBe(false);
});
});
55 changes: 15 additions & 40 deletions packages/markdown-codec/src/block/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@ const NON_SPACE_PATTERN = /[^ \t\f\v\r\n]/;
// An ordered list may interrupt a paragraph only when it starts at 1 (spec 0.31.2: "In order for a list to interrupt a paragraph, it must start with 1").
const INTERRUPTING_ORDERED_START = 1;

function isBulletMarker(char: string): char is MarkdownBulletMarker {
return char === "-" || char === "*" || char === "+";
}

function isOrderedDelimiter(
char: string,
): char is MarkdownOrderedListDelimiter {
return char === "." || char === ")";
}

interface MarkerMatch {
readonly length: number;
readonly data: Omit<ListMarkerData, "padding">;
Expand All @@ -49,26 +39,20 @@ function matchMarker(
): MarkerMatch | undefined {
const bullet = BULLET_MARKER_PATTERN.exec(rest);
if (bullet !== null) {
const char = bullet[0];
if (!isBulletMarker(char)) {
return undefined;
}
// BULLET_MARKER_PATTERN's own character class (`[*+-]`) is exactly MarkdownBulletMarker's three members, so a match's own char is never anything else -- no runtime check could ever see the "else" side of that, only TypeScript's own indexed-access typing needs told.
const char = bullet[0] as MarkdownBulletMarker;
return {
length: bullet[0].length,
data: { type: "bullet", bulletChar: char, markerOffset: indent },
};
}
const ordered = ORDERED_MARKER_PATTERN.exec(rest);
const digits = ordered?.[1];
const delimiter = ordered?.[2];
if (
ordered === null ||
digits === undefined ||
delimiter === undefined ||
!isOrderedDelimiter(delimiter)
) {
if (ordered === null) {
return undefined;
}
// Neither capturing group in ORDERED_MARKER_PATTERN is optional, so a successful match always populates both -- TypeScript's own RegExpExecArray typing has no way to say that (every capture reads as possibly-undefined, alternation or not), so both reads are cast the same way the bullet branch above already casts its own single capture.
const digits = ordered[1]!;
const delimiter = ordered[2] as MarkdownOrderedListDelimiter;
const start = Number.parseInt(digits, 10);
if (containerIsParagraph && start !== INTERRUPTING_ORDERED_START) {
return undefined;
Expand Down Expand Up @@ -110,41 +94,32 @@ export function parseListMarker(
line.advanceToNextNonspace();
line.advance(match.length);

// Measure the spaces following the marker in COLUMNS, stopping at the code-indent threshold: past that point the exact count no longer changes the answer, and a single tab can supply all of them at once. The threshold IS the code indent, not a number of its own -- spaces past it make the content indented code rather than the item's own content indent.
// Measure the spaces following the marker in COLUMNS. No cap at the code-indent threshold here -- the branch below already resets the cursor back to afterMarkerMark and re-derives the item's own content indent from scratch whenever followingSpaces turns out to exceed it (or the rest of the line is blank), so a mid-scan cap would only change how many spaces this loop itself walks past, never the value parseListMarker returns or the cursor position it leaves behind.
const afterMarkerMark = line.mark();
const afterMarkerColumn = line.column;
// LineCursor.peek() reports a tab as a single space, one column at a time (src/scan), so testing for a space alone covers both -- there is no '\t' to compare against at this level.
do {
line.advance(1);
} while (
line.column - afterMarkerColumn <= CODE_INDENT_COLUMNS &&
line.peek() === " "
);
} while (line.peek() === " ");
const followingSpaces = line.column - afterMarkerColumn;
const startsBlank = line.atEnd;

if (
followingSpaces > CODE_INDENT_COLUMNS ||
followingSpaces < 1 ||
startsBlank
) {
// No separate `followingSpaces < 1` disjunct: the do-while above always runs its body at least once, and LineCursor.advance() only ever leaves `line.column` unchanged when the cursor was already at the absolute end of input before that call -- so followingSpaces can never come out to 0 without startsBlank also being true, and a disjunct that can never be true on its own is not a real second condition.
if (followingSpaces > CODE_INDENT_COLUMNS || startsBlank) {
// Either the content is indented code (5+ columns past the marker) or there is no content on this line at all: the item's own content indent is the marker plus a single column, and everything past that is content.
line.reset(afterMarkerMark);
if (line.peek() === " ") {
line.advance(1);
}
// Unconditional, not `if (line.peek() === " ") line.advance(1)`: the marker-follows-by check above already guarantees the character right after the marker is a space/tab or end of line, so this is either consuming that one space/tab (the followingSpaces > 4 case) or a no-op past the end of input (the startsBlank case) -- never a third, unguarded shape.
line.advance(1);
return { ...match.data, padding: match.length + 1 };
}
return { ...match.data, padding: match.length + followingSpaces };
}

// Whether a newly started item continues the list that is already open, or starts a fresh one. spec 0.31.2: "a list is a sequence of list items of the same type" -- changing the bullet character or the ordered delimiter starts a new list, even with no blank line in between.
//
// No separate a.type === b.type check: bulletChar is set only on a "bullet" marker and delimiter only on an "ordered" one (see ListMarkerData), so whenever the two markers are different variants exactly one of the two comparisons below pits a real value against undefined and is already false -- a same-type comparison could never survive that pairing without the field comparisons already agreeing too.
export function listsMatch(a: ListMarkerData, b: ListMarkerData): boolean {
return (
a.type === b.type &&
a.delimiter === b.delimiter &&
a.bulletChar === b.bulletChar
);
return a.delimiter === b.delimiter && a.bulletChar === b.bulletChar;
}

// Whether `block` ends with a blank line, looking through the last child of a list or list item to reach the block that actually recorded one. Memoised through BlockNode.lastLineChecked so a deeply nested list is descended at most once per finalisation rather than once per item.
Expand Down
Loading