| author | |
| committer | |
| log | 773a4563ab1ae1b5bbea046264fb214e2f49986e |
| tree | 6eb6fa71b6e8b7d3ff64ad19cca7f528ee54d1c6 |
| parent | 37b340018b2ef7f2193a5f0680a68803f575ddda |
| signature |
2 files changed, 25 insertions(+), 12 deletions(-)
src/Predict.ts+13-4| ... | ... | @@ -35,6 +35,7 @@ interface TailState { |
| 35 | 35 | delims: DelimState[]; |
| 36 | 36 | exclusive: ExclusiveState | null; |
| 37 | 37 | links: LinkState[]; |
| 38 | pendingDelim: DelimState | null; | |
| 38 | 39 | } |
| 39 | 40 | |
| 40 | 41 | const NESTABLE_DELIMS = new Set<DelimToken>(["*", "**", "_", "__"]); |
| ... | ... | @@ -45,6 +46,7 @@ function parseTail(tail: string): TailState { |
| 45 | 46 | delims: [], |
| 46 | 47 | exclusive: null, |
| 47 | 48 | links: [], |
| 49 | pendingDelim: null, | |
| 48 | 50 | }; |
| 49 | 51 | |
| 50 | 52 | for (let i = 0; i < tail.length; i++) { |
| ... | ... | @@ -179,6 +181,11 @@ function consumeDelim(state: TailState, tail: string, start: number) { |
| 179 | 181 | return start + token.length; |
| 180 | 182 | } |
| 181 | 183 | |
| 184 | if (start + token.length === tail.length) { | |
| 185 | state.pendingDelim = { start, token }; | |
| 186 | return start + token.length; | |
| 187 | } | |
| 188 | ||
| 182 | 189 | if (!canOpenDelim(tail, start, token)) return start + token.length; |
| 183 | 190 | |
| 184 | 191 | state.delims.push({ start, token }); |
| ... | ... | @@ -277,14 +284,16 @@ function renderTail(tail: string, state: TailState) { |
| 277 | 284 | if (nestedClosers) return appendCloser(tail, nestedClosers); |
| 278 | 285 | |
| 279 | 286 | const open = findLastOpen(state); |
| 280 | if (!open) return tail; | |
| 287 | if (!open) return state.pendingDelim ? tail.slice(0, state.pendingDelim.start) : tail; | |
| 281 | 288 | |
| 282 | 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 | 291 | return appendCloserBeforeTrailingInlineWhitespace(tail, open.token); |
| 285 | 292 | } |
| 286 | 293 | |
| 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 | 297 | if (open.kind === "fence") return tail; |
| 289 | 298 | if (open.kind === "code") return appendCloser(tail, open.token); |
| 290 | 299 | if (open.token === "$$") return appendCloser(tail, (tail.endsWith("\n") ? "" : "\n") + "$$"); |
| ... | ... | @@ -296,7 +305,7 @@ function renderOpenLink(tail: string, state: LinkState) { |
| 296 | 305 | const before = tail.slice(0, state.start); |
| 297 | 306 | |
| 298 | 307 | if (state.phase === "text") { |
| 299 | if (!hasContentAfter(tail, state.start, 1)) return tail; | |
| 308 | if (!hasContentAfter(tail, state.start, 1)) return before; | |
| 300 | 309 | return before + tail.slice(state.start + 1); |
| 301 | 310 | } |
| 302 | 311 |
tests/prediction.test.ts+12-8| ... | ... | @@ -177,6 +177,10 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => { |
| 177 | 177 | ])("%s", (_name, input, expected) => check(input, expected)); |
| 178 | 178 | |
| 179 | 179 | describe("whitespace around emphasis delimiters", () => { |
| 180 | test.each(["* ", "** ", "_ ", "__ ", "~~ "])("keeps disqualified opener %j visible", (input) => | |
| 181 | check(input, input), | |
| 182 | ); | |
| 183 | ||
| 180 | 184 | test.each([ |
| 181 | 185 | "hello * world", |
| 182 | 186 | "hello* world", |
| ... | ... | @@ -329,14 +333,14 @@ describe.each(["stateless", "stateful"] as const)("%s processing", (mode) => { |
| 329 | 333 | |
| 330 | 334 | describe("opener-only inputs", () => { |
| 331 | 335 | 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", "[", ""], | |
| 340 | 344 | ["completed label without url at bol", "[x]", "x"], |
| 341 | 345 | ["bare url opener at bol", "[x](", "x"], |
| 342 | 346 | ])("%s", (_name, input, expected) => check(input, expected)); |