Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .changeset/spotty-eyes-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"remend": minor
---

Rework code-region detection and double-underscore counting.

A shared single-pass scanner now classifies fences and inline code spans, replacing the per-character rescans that made healing quadratic on delimiter-heavy input. Fence and span detection follows CommonMark, so `~~~` fences, list-indented fences, CRLF line endings, and multi-backtick spans are all recognized, and content inside code is never healed as prose.

Double underscores are counted per maximal run with flanking rules, so identifiers containing `__` (like `snake__case`) no longer invent or swallow emphasis closers.

Healing is now idempotent. Healed output re-heals to itself, and text-only link mode resolves every unmatched bracket in one call.
22 changes: 22 additions & 0 deletions packages/remend/__benchmarks__/remend.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,25 @@ describe("Streamed Code Blocks", () => {
{ iterations: 10 }
);
});

// Delimiter-heavy input at doubling sizes. Cost should grow in proportion to
// input size: a doubling that more than doubles the time signals a
// superlinear rescan in a handler.
describe("Scaling", () => {
const unit =
"word snake__case text __bold__ and _it_ plus `code` *star* ~~del~~ ".repeat(
30
);
const sizes = [1, 2, 4, 8] as const;

for (const mult of sizes) {
const doc = `${unit.repeat(mult)}__open`;
bench(
`delimiter-heavy ${doc.length} chars`,
() => {
remend(doc);
},
{ iterations: 200 }
);
}
});
3 changes: 2 additions & 1 deletion packages/remend/__tests__/broken-markdown-variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ describe("multiple incomplete links", () => {
});

it("should handle two incomplete links in text-only mode", () => {
// Fixed-point healing resolves both unmatched brackets in one call
const result = remend("[link1 and [link2", { linkMode: "text-only" });
expect(result).toBe("link1 and [link2");
expect(result).toBe("link1 and link2");
});
});

Expand Down
55 changes: 5 additions & 50 deletions packages/remend/__tests__/code-block-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,9 @@
import { describe, expect, it } from "vitest";
import { isInsideCodeBlock } from "../src/code-block-utils";

// Reference implementation: the previous per-call scan, kept verbatim so the
// lookup-based rewrite can be checked against it position by position.
const referenceIsInsideCodeBlock = (
text: string,
position: number
): boolean => {
let inInlineCode = false;
let inMultilineCode = false;

for (let i = 0; i < position; i += 1) {
if (text[i] === "\\" && i + 1 < text.length && text[i + 1] === "`") {
i += 1;
continue;
}
if (text.substring(i, i + 3) === "```") {
inMultilineCode = !inMultilineCode;
i += 2;
continue;
}
if (!inMultilineCode && text[i] === "`") {
inInlineCode = !inInlineCode;
}
}

return inInlineCode || inMultilineCode;
};

// Returns positions where the rewrite disagrees with the reference scan.
const parityMismatches = (text: string): number[] => {
const mismatches: number[] = [];
for (let p = 0; p <= text.length + 1; p += 1) {
if (isInsideCodeBlock(text, p) !== referenceIsInsideCodeBlock(text, p)) {
mismatches.push(p);
}
}
return mismatches;
};

describe("isInsideCodeBlock", () => {
it("reports positions inside a fenced code block", () => {
const text = "before ```js\nconst x = arr[0];\n``` after";
const text = "before\n```js\nconst x = arr[0];\n```\nafter";
expect(isInsideCodeBlock(text, text.indexOf("arr"))).toBe(true);
expect(isInsideCodeBlock(text, text.indexOf("before"))).toBe(false);
expect(isInsideCodeBlock(text, text.indexOf("after"))).toBe(false);
Expand All @@ -64,17 +26,10 @@ describe("isInsideCodeBlock", () => {
expect(isInsideCodeBlock(text, text.length)).toBe(true);
});

it("matches the per-call scan at every position on mixed input", () => {
const cases = [
"a `b` c ```\nd [e] `f`\n``` g \\` h ``` i",
"``````",
"\\`",
"`unclosed inline [x]",
"text \\``real` code",
];
for (const text of cases) {
expect(parityMismatches(text)).toEqual([]);
}
it("treats a backtick run that is not at line start as inline code", () => {
const text = "before ```js const x = arr[0]; ``` after";
expect(isInsideCodeBlock(text, text.indexOf("arr"))).toBe(true);
expect(isInsideCodeBlock(text, text.indexOf("after"))).toBe(false);
});

it("stays correct when queried texts alternate", () => {
Expand Down
16 changes: 11 additions & 5 deletions packages/remend/__tests__/coverage-gaps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ describe("link handler edge cases", () => {
expect(remend("](partial")).toBe("](partial");
});

it("should skip image brackets in text-only mode", () => {
expect(remend("![img [text", { linkMode: "text-only" })).toBe("![img text");
it("should heal an incomplete image exposed in text-only mode", () => {
// Stripping the inner bracket exposes an incomplete image, which gets
// the placeholder in the same call
expect(remend("![img [text", { linkMode: "text-only" })).toBe(
"![img text](streamdown:incomplete-image)"
);
});

it("should skip complete links in text-only mode", () => {
Expand Down Expand Up @@ -196,9 +200,11 @@ describe("double underscore half-complete in code block", () => {
});
});

describe("double underscore half-complete with even pairs", () => {
it("should not complete when __ pairs are balanced", () => {
expect(remend("__a__ __b__content_")).toBe("__a__ __b__content_");
describe("double underscore half-complete with word-internal run", () => {
it("should complete the opener left unmatched by a word-internal run", () => {
// b__content is word-internal, so the __ before b is an unmatched
// opener and the trailing _ is its half-typed closer
expect(remend("__a__ __b__content_")).toBe("__a__ __b__content__");
});
});

Expand Down
116 changes: 116 additions & 0 deletions packages/remend/__tests__/fence-semantics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, expect, it } from "vitest";
import remend from "../src";

describe("tilde fences", () => {
it("should not heal emphasis inside a complete tilde fence", () => {
expect(remend("~~~\ncode with __stuff\n~~~\ndone")).toBe(
"~~~\ncode with __stuff\n~~~\ndone"
);
});

it("should not heal emphasis inside an open tilde fence", () => {
expect(remend("~~~js\nx = a__b")).toBe("~~~js\nx = a__b");
});

it("should heal strikethrough after a complete tilde fence", () => {
expect(remend("~~~\ncode\n~~~\nafter ~~open")).toBe(
"~~~\ncode\n~~~\nafter ~~open~~"
);
});

it("should treat a mid-line tilde run as strikethrough context, not a fence", () => {
expect(remend("prose ~~struck~~ more prose __bold")).toBe(
"prose ~~struck~~ more prose __bold__"
);
});
});

describe("fence opener position", () => {
it("should recognize a fence indented up to three spaces", () => {
expect(remend(" ```\n__code\n ```\n__open")).toBe(
" ```\n__code\n ```\n__open__"
);
});

it("should treat mid-line triple backticks as inline code", () => {
// A fence can only open at the start of a line, so a mid-line run is an
// inline code span and heals by completing its closing run
expect(remend("see ```inline code``")).toBe("see ```inline code```");
});
});

describe("fence closer length", () => {
it("should not close a fence with a shorter run", () => {
// The ``` run is shorter than the ```` opener, so the fence is still
// open and its content is not healed
expect(remend("````\ncode\n```\nstill __code")).toBe(
"````\ncode\n```\nstill __code"
);
});

it("should close a fence with a longer run", () => {
expect(remend("```\ncode\n````\nafter __bold")).toBe(
"```\ncode\n````\nafter __bold__"
);
});
});

describe("fence info strings", () => {
it("should not heal emphasis in an info string", () => {
expect(remend("```python__hint\ncode")).toBe("```python__hint\ncode");
});
});

describe("inline code span run lengths", () => {
it("should complete a double-backtick span with a double run", () => {
expect(remend("``code`")).toBe("``code``");
});

it("should complete only the missing part of the closing run", () => {
expect(remend("``code")).toBe("``code``");
});

it("should leave a longer literal run inside an open span alone", () => {
// The trailing run is longer than the opener, so appending backticks
// could never close the span
expect(remend("`a``")).toBe("`a``");
});
});

describe("list-indented fences", () => {
it("should recognize a fence indented inside a list item", () => {
expect(remend("1. Install:\n ```bash\n npm install foo")).toBe(
"1. Install:\n ```bash\n npm install foo"
);
});

it("should not heal emphasis inside a list-indented fence", () => {
expect(remend("- step\n - nested\n ```js\n const x = a__b")).toBe(
"- step\n - nested\n ```js\n const x = a__b"
);
});
});

describe("CRLF line endings", () => {
it("should recognize a fence opener on a CRLF line", () => {
expect(remend("```js\r\nconst a = 1")).toBe("```js\r\nconst a = 1");
});

it("should close a CRLF fence and heal after it", () => {
expect(remend("```\r\ncode\r\n```\r\n__open")).toBe(
"```\r\ncode\r\n```\r\n__open__"
);
});
});

describe("spans across paragraphs", () => {
it("should leave an unmatched run literal once its paragraph ends", () => {
expect(remend("use ``` to open a block\n\nmore **bold streaming")).toBe(
"use ``` to open a block\n\nmore **bold streaming**"
);
});

it("should still complete an open span in the last paragraph", () => {
expect(remend("intro\n\nrun `npm i")).toBe("intro\n\nrun `npm i`");
});
});
12 changes: 12 additions & 0 deletions packages/remend/__tests__/katex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,15 @@ plain trailing text.`;
);
});
});

describe("dollar signs inside code", () => {
it("should not let a $ in inline code suppress later healing", () => {
expect(remend("`$` _hello")).toBe("`$` _hello_");
});

it("should not let a $ in a fence suppress later healing", () => {
expect(remend("```\nprice = $5\n```\n_hello")).toBe(
"```\nprice = $5\n```\n_hello_"
);
});
});
5 changes: 3 additions & 2 deletions packages/remend/__tests__/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,11 @@ describe("link handling with linkMode: text-only", () => {
});

it("should handle nested brackets without matching closing bracket", () => {
// Fixed-point healing resolves every unmatched bracket in one call
expect(remend("Text [outer [inner", textOnlyOptions)).toBe(
"Text outer [inner"
"Text outer inner"
);
expect(remend("[foo [bar [baz", textOnlyOptions)).toBe("foo [bar [baz");
expect(remend("[foo [bar [baz", textOnlyOptions)).toBe("foo bar baz");
expect(remend("Text [outer [inner]", textOnlyOptions)).toBe(
"Text outer [inner]"
);
Expand Down
Loading