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 {
3535 delims: DelimState[];
3636 exclusive: ExclusiveState | null;
3737 links: LinkState[];
38 pendingDelim: DelimState | null;
3839}
3940
4041const NESTABLE_DELIMS = new Set<DelimToken>(["*", "**", "_", "__"]);
......@@ -45,6 +46,7 @@ function parseTail(tail: string): TailState {
4546 delims: [],
4647 exclusive: null,
4748 links: [],
49 pendingDelim: null,
4850 };
4951
5052 for (let i = 0; i < tail.length; i++) {
......@@ -179,6 +181,11 @@ function consumeDelim(state: TailState, tail: string, start: number) {
179181 return start + token.length;
180182 }
181183
184 if (start + token.length === tail.length) {
185 state.pendingDelim = { start, token };
186 return start + token.length;
187 }
188
182189 if (!canOpenDelim(tail, start, token)) return start + token.length;
183190
184191 state.delims.push({ start, token });
......@@ -277,14 +284,16 @@ function renderTail(tail: string, state: TailState) {
277284 if (nestedClosers) return appendCloser(tail, nestedClosers);
278285
279286 const open = findLastOpen(state);
280 if (!open) return tail;
287 if (!open) return state.pendingDelim ? tail.slice(0, state.pendingDelim.start) : tail;
281288
282289 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);
284291 return appendCloserBeforeTrailingInlineWhitespace(tail, open.token);
285292 }
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 }
288297 if (open.kind === "fence") return tail;
289298 if (open.kind === "code") return appendCloser(tail, open.token);
290299 if (open.token === "$$") return appendCloser(tail, (tail.endsWith("\n") ? "" : "\n") + "$$");
......@@ -296,7 +305,7 @@ function renderOpenLink(tail: string, state: LinkState) {
296305 const before = tail.slice(0, state.start);
297306
298307 if (state.phase === "text") {
299 if (!hasContentAfter(tail, state.start, 1)) return tail;
308 if (!hasContentAfter(tail, state.start, 1)) return before;
300309 return before + tail.slice(state.start + 1);
301310 }
302311
tests/prediction.test.ts+12-8
......@@ -177,6 +177,10 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => {
177177 ])("%s", (_name, input, expected) => check(input, expected));
178178
179179 describe("whitespace around emphasis delimiters", () => {
180 test.each(["* ", "** ", "_ ", "__ ", "~~ "])("keeps disqualified opener %j visible", (input) =>
181 check(input, input),
182 );
183
180184 test.each([
181185 "hello * world",
182186 "hello* world",
......@@ -329,14 +333,14 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => {
329333
330334 describe("opener-only inputs", () => {
331335 test.each([
332 ["lone italic delimiter", "*", "*"],
333 ["lone bold delimiter", "**", "**"],
334 ["lone underscore delimiter", "_", "_"],
335 ["lone double underscore delimiter", "__", "__"],
336 ["lone strike delimiter", "~~", "~~"],
337 ["lone code delimiter", "`", "`"],
338 ["lone inline math delimiter", "$", "$"],
339 ["bare open bracket", "[", "["],
336 ["lone italic delimiter", "*", ""],
337 ["lone bold delimiter", "**", ""],
338 ["lone underscore delimiter", "_", ""],
339 ["lone double underscore delimiter", "__", ""],
340 ["lone strike delimiter", "~~", ""],
341 ["lone code delimiter", "`", ""],
342 ["lone inline math delimiter", "$", ""],
343 ["bare open bracket", "[", ""],
340344 ["completed label without url at bol", "[x]", "x"],
341345 ["bare url opener at bol", "[x](", "x"],
342346 ])("%s", (_name, input, expected) => check(input, expected));