authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 01:08:24-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 01:17:10-07:00
log773a4563ab1ae1b5bbea046264fb214e2f49986e
tree6eb6fa71b6e8b7d3ff64ad19cca7f528ee54d1c6
parent37b340018b2ef7f2193a5f0680a68803f575ddda
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

feat: hide lone delims like `*` when predicting


2 files changed, 25 insertions(+), 12 deletions(-)

src/Predict.ts+13-4
...@@ -35,6 +35,7 @@ interface TailState {...@@ -35,6 +35,7 @@ interface TailState {
35 delims: DelimState[];35 delims: DelimState[];
36 exclusive: ExclusiveState | null;36 exclusive: ExclusiveState | null;
37 links: LinkState[];37 links: LinkState[];
38 pendingDelim: DelimState | null;
38}39}
3940
40const NESTABLE_DELIMS = new Set<DelimToken>(["*", "**", "_", "__"]);41const NESTABLE_DELIMS = new Set<DelimToken>(["*", "**", "_", "__"]);
...@@ -45,6 +46,7 @@ function parseTail(tail: string): TailState {...@@ -45,6 +46,7 @@ function parseTail(tail: string): TailState {
45 delims: [],46 delims: [],
46 exclusive: null,47 exclusive: null,
47 links: [],48 links: [],
49 pendingDelim: null,
48 };50 };
4951
50 for (let i = 0; i < tail.length; i++) {52 for (let i = 0; i < tail.length; i++) {
...@@ -179,6 +181,11 @@ function consumeDelim(state: TailState, tail: string, start: number) {...@@ -179,6 +181,11 @@ function consumeDelim(state: TailState, tail: string, start: number) {
179 return start + token.length;181 return start + token.length;
180 }182 }
181183
184 if (start + token.length === tail.length) {
185 state.pendingDelim = { start, token };
186 return start + token.length;
187 }
188
182 if (!canOpenDelim(tail, start, token)) return start + token.length;189 if (!canOpenDelim(tail, start, token)) return start + token.length;
183190
184 state.delims.push({ start, token });191 state.delims.push({ start, token });
...@@ -277,14 +284,16 @@ function renderTail(tail: string, state: TailState) {...@@ -277,14 +284,16 @@ function renderTail(tail: string, state: TailState) {
277 if (nestedClosers) return appendCloser(tail, nestedClosers);284 if (nestedClosers) return appendCloser(tail, nestedClosers);
278285
279 const open = findLastOpen(state);286 const open = findLastOpen(state);
280 if (!open) return tail;287 if (!open) return state.pendingDelim ? tail.slice(0, state.pendingDelim.start) : tail;
281288
282 if (open.kind === "delim") {289 if (open.kind === "delim") {
283 if (!hasContentAfter(tail, open.start, open.token.length)) return tail;290 if (!hasContentAfter(tail, open.start, open.token.length)) return tail.slice(0, open.start);
284 return appendCloserBeforeTrailingInlineWhitespace(tail, open.token);291 return appendCloserBeforeTrailingInlineWhitespace(tail, open.token);
285 }292 }
286293
287 if (!hasContentAfter(tail, open.start, open.token.length)) return tail;294 if (!hasContentAfter(tail, open.start, open.token.length)) {
295 return open.kind === "fence" ? tail : tail.slice(0, open.start);
296 }
288 if (open.kind === "fence") return tail;297 if (open.kind === "fence") return tail;
289 if (open.kind === "code") return appendCloser(tail, open.token);298 if (open.kind === "code") return appendCloser(tail, open.token);
290 if (open.token === "$$") return appendCloser(tail, (tail.endsWith("\n") ? "" : "\n") + "$$");299 if (open.token === "$$") return appendCloser(tail, (tail.endsWith("\n") ? "" : "\n") + "$$");
...@@ -296,7 +305,7 @@ function renderOpenLink(tail: string, state: LinkState) {...@@ -296,7 +305,7 @@ function renderOpenLink(tail: string, state: LinkState) {
296 const before = tail.slice(0, state.start);305 const before = tail.slice(0, state.start);
297306
298 if (state.phase === "text") {307 if (state.phase === "text") {
299 if (!hasContentAfter(tail, state.start, 1)) return tail;308 if (!hasContentAfter(tail, state.start, 1)) return before;
300 return before + tail.slice(state.start + 1);309 return before + tail.slice(state.start + 1);
301 }310 }
302311
tests/prediction.test.ts+12-8
...@@ -177,6 +177,10 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => {...@@ -177,6 +177,10 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => {
177 ])("%s", (_name, input, expected) => check(input, expected));177 ])("%s", (_name, input, expected) => check(input, expected));
178178
179 describe("whitespace around emphasis delimiters", () => {179 describe("whitespace around emphasis delimiters", () => {
180 test.each(["* ", "** ", "_ ", "__ ", "~~ "])("keeps disqualified opener %j visible", (input) =>
181 check(input, input),
182 );
183
180 test.each([184 test.each([
181 "hello * world",185 "hello * world",
182 "hello* world",186 "hello* world",
...@@ -329,14 +333,14 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => {...@@ -329,14 +333,14 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => {
329333
330 describe("opener-only inputs", () => {334 describe("opener-only inputs", () => {
331 test.each([335 test.each([
332 ["lone italic delimiter", "*", "*"],336 ["lone italic delimiter", "*", ""],
333 ["lone bold delimiter", "**", "**"],337 ["lone bold delimiter", "**", ""],
334 ["lone underscore delimiter", "_", "_"],338 ["lone underscore delimiter", "_", ""],
335 ["lone double underscore delimiter", "__", "__"],339 ["lone double underscore delimiter", "__", ""],
336 ["lone strike delimiter", "~~", "~~"],340 ["lone strike delimiter", "~~", ""],
337 ["lone code delimiter", "`", "`"],341 ["lone code delimiter", "`", ""],
338 ["lone inline math delimiter", "$", "$"],342 ["lone inline math delimiter", "$", ""],
339 ["bare open bracket", "[", "["],343 ["bare open bracket", "[", ""],
340 ["completed label without url at bol", "[x]", "x"],344 ["completed label without url at bol", "[x]", "x"],
341 ["bare url opener at bol", "[x](", "x"],345 ["bare url opener at bol", "[x](", "x"],
342 ])("%s", (_name, input, expected) => check(input, expected));346 ])("%s", (_name, input, expected) => check(input, expected));