Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
64415f8
test(markdown-codec): kill list-id numId mutants and drop an unreacha…
Mearman Sep 12, 2026
8e972fe
test(markdown-codec): cover parseHeadingStyleId's integer and positiv…
Mearman Sep 12, 2026
1aa56ae
test(markdown-codec): pin lowerTable's column-width division and abse…
Mearman Sep 12, 2026
32e3868
test(markdown-codec): add direct coverage for matchMathInlineSpan's g…
Mearman Sep 12, 2026
df18b60
test(markdown-codec): add direct coverage for the footnote label/mark…
Mearman Sep 12, 2026
8e296b3
test(markdown-codec): pin resolveMarkdownImage's independent width/he…
Mearman Sep 12, 2026
93d4536
test(markdown-codec): pin emitImage's alt fallback for altText-less i…
Mearman Sep 12, 2026
9657592
test(markdown-codec): add direct coverage for isMarkdownBlockNode/isM…
Mearman Sep 12, 2026
50d7dde
refactor(markdown-codec): always splice readMarkdown's own definition…
Mearman Sep 12, 2026
df1e1e7
refactor(markdown-codec): drop two unobservable branches in LineCursor
Mearman Sep 12, 2026
ec62848
test(markdown-codec): add direct coverage for InlineNode's linked-lis…
Mearman Sep 12, 2026
98930ae
refactor(markdown-codec): drop unobservable guards in list-marker/tig…
Mearman Sep 12, 2026
cc1ac4d
refactor(markdown-codec): make link primitives' loop bounds mutation-…
Mearman Sep 12, 2026
df2e73c
refactor(markdown-codec): remove three provably-unreachable guards fr…
Mearman Sep 12, 2026
2a86eb9
refactor(markdown-codec): drop two redundant '<'-prefix guards in the…
Mearman Sep 12, 2026
cd4d1cd
test(markdown-codec): add direct coverage for BlockNode's own methods…
Mearman Sep 12, 2026
76b9386
refactor(markdown-codec): remove length bounds absorbed by charAt's o…
Mearman Sep 12, 2026
36babc0
refactor(markdown-codec): remove two more redundant guards, add codep…
Mearman Sep 12, 2026
e4fde3f
refactor(markdown-codec): drop table.ts's redundant escaped-pipe look…
Mearman Sep 13, 2026
b5739f6
refactor(markdown-codec): drop list.ts's redundant marker-match fields
Mearman Sep 13, 2026
dc86eac
refactor(markdown-codec): drop skipInlineWhitespace's redundant range…
Mearman Sep 13, 2026
ceca56c
refactor(markdown-codec): drop unescapeString's redundant '&'-prefix …
Mearman Sep 13, 2026
f3e07be
test(markdown-codec): pin next() leaving cursor state untouched past end
Mearman Sep 13, 2026
c46e66b
test(markdown-codec): pin table cell diagnostics and drop two redunda…
Mearman Sep 13, 2026
0ed2dcb
fix(markdown-codec): remove an exhausted closer's delimiter before it…
Mearman Sep 13, 2026
2006bcf
test(markdown-codec): add a dedicated unit suite for the corpus loader
Mearman Sep 13, 2026
2c5254d
refactor(markdown-codec): drop definitions.ts's redundant label-lengt…
Mearman Sep 13, 2026
e563b25
test(markdown-codec): pin throw-tier error classes' own fields
Mearman Sep 14, 2026
b0b1d91
test(markdown-codec): pin construct-extent, marker-balance, and write…
Mearman Sep 14, 2026
926bb6b
test(markdown-codec): kill diagnostics.ts's this.name assignment mutants
Mearman Sep 14, 2026
f45ca4c
test(markdown-codec): kill emit/front-matter.ts's quoting and escapin…
Mearman Sep 14, 2026
454ac8c
fix(markdown-codec): stop building the function-with-fields test fixt…
Mearman Sep 15, 2026
3775612
refactor(markdown-codec): merge lowerInlineNodes' text and entity cases
Mearman Sep 15, 2026
ff239f2
refactor(markdown-codec): drop render.ts's unkillable guards and case…
Mearman Sep 15, 2026
98f57e3
test(markdown-codec): add direct coverage for lowerInlineNodes' own l…
Mearman Sep 15, 2026
232deea
test(markdown-codec): add direct coverage for the HTML render oracle
Mearman Sep 15, 2026
1e84023
refactor(markdown-codec): drop render.ts's cr() calls that can never …
Mearman Sep 15, 2026
5cec021
test(markdown-codec): pin the remaining render.ts mutants directly
Mearman Sep 15, 2026
c78c150
refactor(markdown-codec): drop codePointAt's unreachable undefined guard
Mearman Sep 15, 2026
3590a6c
test(markdown-codec): add direct coverage for chars.ts's own boundaries
Mearman Sep 15, 2026
3b0f743
test(markdown-codec): add direct coverage for front-matter parsing
Mearman Sep 15, 2026
214ca0a
test(markdown-codec): pin mathBlock's and footnoteDefinition's own cr()
Mearman Sep 15, 2026
b7a908c
refactor(markdown-codec): drop two more of chars.ts's unkillable guards
Mearman Sep 15, 2026
ed57efd
test(markdown-codec): pin chars.ts's own boundaries the fresh run found
Mearman Sep 15, 2026
a15c22d
refactor(markdown-codec): drop front-matter.ts's own unkillable loops
Mearman Sep 15, 2026
667ef81
test(markdown-codec): pin front-matter.ts's remaining boundaries
Mearman Sep 15, 2026
86f9a9f
test(markdown-codec): cover image.ts's base64 codec and format-sniffi…
Mearman Sep 15, 2026
897e174
refactor(markdown-codec): drop emit.ts's own unkillable length-bound …
Mearman Sep 15, 2026
491d055
test(markdown-codec): cover emit.ts's terminatesCleanly, isQuotableSt…
Mearman Sep 15, 2026
e409be2
refactor(markdown-codec): drop canInterruptOpenParagraph's own
Mearman Sep 15, 2026
8f22a2c
test(markdown-codec): cover emit.ts's quote/fence/tab-stop boundaries
Mearman Sep 15, 2026
be41980
test(markdown-codec): assert diagnostic message content for
Mearman Sep 15, 2026
c1cbdd1
test(markdown-codec): cover emit.ts's line-break-collapse message
Mearman Sep 15, 2026
71f79b3
test(markdown-codec): cover checkbox glyph stripping and
Mearman Sep 15, 2026
706a37d
test(markdown-codec): cover renderConstruct's unrepresentable shapes
Mearman Sep 15, 2026
8d7e29a
test(markdown-codec): cover divisionDepth's own restore-on-exit and
Mearman Sep 15, 2026
5ed448f
refactor(markdown-codec): drop renderListRegion's own unreachable
Mearman Sep 15, 2026
b03c541
test(markdown-codec): cover the ballot-box-glyph/task-flag guard and
Mearman Sep 15, 2026
fafe2c8
test(markdown-codec): cover blank-line indentation for a LATER
Mearman Sep 15, 2026
bb5f6fb
test(markdown-codec): assert message content for the two remaining
Mearman Sep 15, 2026
29f9b7f
test(markdown-codec): fix the UNCHECKED glyph test to actually
Mearman Sep 15, 2026
d6bd32f
test(markdown-codec): cover lastStyleIdOf's own last-child lookup
Mearman Sep 15, 2026
9acb91c
test(markdown-codec): cover the nested sub-list's own last-block
Mearman Sep 15, 2026
3685d5b
test(markdown-codec): cover openMemberships' own same-level pop
Mearman Sep 15, 2026
90f5f3e
test(markdown-codec): cover isMaterialisedDivision's own every-vs-some
Mearman Sep 15, 2026
006eac9
test(markdown-codec): cover constructCarriesListItemId's own
Mearman Sep 15, 2026
798a016
test(markdown-codec): cover validateRunConstructExtents' own
Mearman Sep 15, 2026
366a603
refactor(markdown-codec): drop collectListItem's own unkillable
Mearman Sep 15, 2026
124d06d
test(markdown-codec): cover willRenderAsSetext's own level boundary
Mearman Sep 15, 2026
10144b8
refactor(markdown-codec): drop renderConstruct's own redundant
Mearman Sep 15, 2026
446e072
test(markdown-codec): cover firstBlockCheckbox's own stripGlyph:
Mearman Sep 15, 2026
68f014c
test(markdown-codec): cover the link construct's own exact-one-child
Mearman Sep 15, 2026
37845ea
test(markdown-codec): cover HEADING_LEVEL_CLAMPED's own false case and
Mearman Sep 15, 2026
b1d3508
test(markdown-codec): cover interruptsSetextParagraph's own
Mearman Sep 15, 2026
eda3aac
refactor(markdown-codec): drop validateRunConstructExtents' own
Mearman Sep 15, 2026
a5d7778
test(markdown-codec): cover non-paragraph list children interrupting …
Mearman Sep 15, 2026
7e7c96e
test(markdown-codec): cover an empty construct child defaulting to in…
Mearman Sep 15, 2026
bfb10ef
refactor(markdown-codec): derive stripGlyph from the detected checkbo…
Mearman Sep 15, 2026
239f72b
test(markdown-codec): cover the unsafe-setext branch requiring setext…
Mearman Sep 15, 2026
3891aef
refactor(markdown-codec): strip the checkbox glyph once, in firstBloc…
Mearman Sep 15, 2026
d30088a
test(markdown-codec): cover the unsafe-setext branches own level ceiling
Mearman Sep 15, 2026
3d0e050
refactor(markdown-codec): remove three dead split-result fallbacks wi…
Mearman Sep 15, 2026
36c0b49
refactor(markdown-codec): return the code-indent threshold boolean di…
Mearman Sep 15, 2026
00f708e
refactor(markdown-codec): remove leadingIndentReachesCodeThreshold's …
Mearman Sep 15, 2026
570c697
fix(markdown-codec): stop an unterminated inline tag repeating its ce…
Mearman Sep 20, 2026
33ca4a6
refactor(markdown-codec): match the html-table reader's open and clos…
Mearman Sep 20, 2026
cdfc6ba
test(markdown-codec): cover the html-table reader's refusals and the …
Mearman Sep 20, 2026
1986219
refactor(markdown-codec): read base64 groups and JPEG markers only at…
Mearman Sep 20, 2026
a7611b6
test(markdown-codec): cover base64 padding boundaries and the JPEG ma…
Mearman Sep 20, 2026
3e7de79
refactor(markdown-codec): end gfm-autolink's scans on the empty strin…
Mearman Sep 20, 2026
1518de0
test(markdown-codec): cover gfm-autolink's start boundaries, trimming…
Mearman Sep 20, 2026
cd77f13
refactor(markdown-codec): drop the markdown writers' length bounds an…
Mearman Sep 20, 2026
3b35423
test(markdown-codec): cover the markdown writers' escaping, delimiter…
Mearman Sep 20, 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
Loading