markdown-stream.test.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { describe, expect, test } from "bun:test"
  2. import { canReusePendingBlock, project, stream } from "./markdown-stream"
  3. describe("markdown stream", () => {
  4. test("heals incomplete emphasis while streaming", () => {
  5. expect(stream("hello **world", true)).toEqual([{ raw: "hello **world", src: "hello **world**", mode: "live" }])
  6. expect(stream("say `code", true)).toEqual([{ raw: "say `code", src: "say `code`", mode: "live" }])
  7. })
  8. test("keeps incomplete links non-clickable until they finish", () => {
  9. expect(stream("see [docs](https://example.com/gu", true)).toEqual([
  10. { raw: "see [docs](https://example.com/gu", src: "see docs", mode: "live" },
  11. ])
  12. })
  13. test("splits an unfinished trailing code fence from stable content", () => {
  14. expect(stream("before\n\n```ts\nconst x = 1", true)).toEqual([
  15. { raw: "before\n\n", src: "before\n\n", mode: "full" },
  16. { raw: "```ts\nconst x = 1", src: "const x = 1", mode: "code", language: "ts" },
  17. ])
  18. })
  19. test("fully parses a code fence once it closes", () => {
  20. const text = "before\n\n```ts\nconst x = 1\n```"
  21. expect(stream(text, true)).toEqual([
  22. { raw: "before\n\n", src: "before\n\n", mode: "full" },
  23. { raw: "```ts\nconst x = 1\n```", src: "const x = 1", mode: "code", language: "ts", complete: true },
  24. ])
  25. })
  26. test("keeps a completed code fence in worker-rendered code mode when prose follows", () => {
  27. expect(stream("```ts\nconst x = 1\n```\n\nafter", true)).toEqual([
  28. { raw: "```ts\nconst x = 1\n```\n\n", src: "const x = 1", mode: "code", language: "ts", complete: true },
  29. { raw: "after", src: "after", mode: "live" },
  30. ])
  31. })
  32. test("freezes completed top-level blocks and only keeps the tail live", () => {
  33. expect(stream("# Plan\n\nFinished paragraph.\n\n- live item", true)).toEqual([
  34. { raw: "# Plan\n\n", src: "# Plan\n\n", mode: "full" },
  35. { raw: "Finished paragraph.\n\n", src: "Finished paragraph.\n\n", mode: "full" },
  36. { raw: "- live item", src: "- live item", mode: "live" },
  37. ])
  38. })
  39. test("keeps a growing table together until a later block freezes it", () => {
  40. expect(stream("| a | b |\n|---|---|\n| 1 | 2 |", true)).toEqual([
  41. { raw: "| a | b |\n|---|---|\n| 1 | 2 |", src: "| a | b |\n|---|---|\n| 1 | 2 |", mode: "live" },
  42. ])
  43. })
  44. test("reprojects non-prefix replacements from current content", () => {
  45. expect(stream("# Replacement\n\nNew body", true)).toEqual([
  46. { raw: "# Replacement\n\n", src: "# Replacement\n\n", mode: "full" },
  47. { raw: "New body", src: "New body", mode: "live" },
  48. ])
  49. })
  50. test("reprojects truncation without retaining removed blocks", () => {
  51. expect(stream("Only the restored prefix", true)).toEqual([
  52. { raw: "Only the restored prefix", src: "Only the restored prefix", mode: "live" },
  53. ])
  54. })
  55. test("shifts later blocks when an earlier block is inserted", () => {
  56. expect(stream("# Inserted\n\nFirst body\n\nSecond body", true)).toEqual([
  57. { raw: "# Inserted\n\n", src: "# Inserted\n\n", mode: "full" },
  58. { raw: "First body\n\n", src: "First body\n\n", mode: "full" },
  59. { raw: "Second body", src: "Second body", mode: "live" },
  60. ])
  61. })
  62. test("keeps reference-style markdown as one block", () => {
  63. expect(stream("[docs][1]\n\n[1]: https://example.com", true)).toEqual([
  64. {
  65. raw: "[docs][1]\n\n[1]: https://example.com",
  66. src: "[docs][1]\n\n[1]: https://example.com",
  67. mode: "live",
  68. },
  69. ])
  70. })
  71. test("keeps compact and indented reference definitions with their uses", () => {
  72. expect(stream("[docs]\n\n [docs]:/guide", true)).toEqual([
  73. {
  74. raw: "[docs]\n\n [docs]:/guide",
  75. src: "[docs]\n\n [docs]:/guide",
  76. mode: "live",
  77. },
  78. ])
  79. })
  80. test("keeps multiline reference definitions with their uses", () => {
  81. expect(stream("[docs][id]\n\n[id]:\n /guide", true)).toEqual([
  82. {
  83. raw: "[docs][id]\n\n[id]:\n /guide",
  84. src: "[docs][id]\n\n[id]:\n /guide",
  85. mode: "live",
  86. },
  87. ])
  88. })
  89. test("uses only the language portion of fence metadata", () => {
  90. expect(stream("```ts title=example\nconst x = 1", true)).toEqual([
  91. {
  92. raw: "```ts title=example\nconst x = 1",
  93. src: "const x = 1",
  94. mode: "code",
  95. language: "ts",
  96. },
  97. ])
  98. })
  99. test("preserves trailing newlines in open code fences", () => {
  100. expect(stream("```ts\nconst x = 1\n", true)).toEqual([
  101. {
  102. raw: "```ts\nconst x = 1\n",
  103. src: "const x = 1\n",
  104. mode: "code",
  105. language: "ts",
  106. },
  107. ])
  108. })
  109. test("only reuses pending blocks with compatible identity and content", () => {
  110. expect(
  111. canReusePendingBlock({ mode: "full", raw: "First\n\n" }, { mode: "full", raw: "# Inserted\n\n", src: "" }),
  112. ).toBe(false)
  113. expect(
  114. canReusePendingBlock({ mode: "code", raw: "```ts\none" }, { mode: "code", raw: "```ts\none two", src: "" }),
  115. ).toBe(true)
  116. expect(canReusePendingBlock({ mode: "code", raw: "```ts\none" }, { mode: "live", raw: "one", src: "" })).toBe(false)
  117. })
  118. test("appends plain code deltas without reprojecting frozen blocks", () => {
  119. const previous = project(undefined, "# Plan\n\n```ts\nconst one = 1\n", true)
  120. const next = project(previous, `${previous.text}const two = 2\n`, true)
  121. expect(next.blocks[0]).toBe(previous.blocks[0])
  122. expect(next.blocks.at(-1)).toEqual({
  123. raw: "```ts\nconst one = 1\nconst two = 2\n",
  124. src: "const one = 1\nconst two = 2\n",
  125. mode: "code",
  126. language: "ts",
  127. })
  128. })
  129. test("does not add a blank line before the first streamed code", () => {
  130. const previous = project(undefined, "```ts\n", true)
  131. const next = project(previous, `${previous.text}const x = 1`, true)
  132. expect(next.blocks.at(-1)).toEqual({
  133. raw: "```ts\nconst x = 1",
  134. src: "const x = 1",
  135. mode: "code",
  136. language: "ts",
  137. })
  138. })
  139. test("closes code fences split across provider deltas", () => {
  140. const open = project(undefined, "```ts\nconst x = 1\n", true)
  141. const one = project(open, `${open.text}\``, true)
  142. const two = project(one, `${one.text}\``, true)
  143. const closed = project(two, `${two.text}\``, true)
  144. const prose = project(closed, `${closed.text}\nafter`, true)
  145. expect(closed.blocks.at(-1)).toEqual({
  146. raw: "```ts\nconst x = 1\n```",
  147. src: "const x = 1",
  148. mode: "code",
  149. language: "ts",
  150. complete: true,
  151. })
  152. expect(prose.blocks).toEqual([
  153. { raw: "```ts\nconst x = 1\n```\n", src: "const x = 1", mode: "code", language: "ts", complete: true },
  154. { raw: "after", src: "after", mode: "live" },
  155. ])
  156. })
  157. test("closes tilde fences split across provider deltas", () => {
  158. const open = project(undefined, "~~~ts\nconst x = 1\n", true)
  159. const one = project(open, `${open.text}~`, true)
  160. const two = project(one, `${one.text}~`, true)
  161. const closed = project(two, `${two.text}~`, true)
  162. expect(closed.blocks.at(-1)).toEqual({
  163. raw: "~~~ts\nconst x = 1\n~~~",
  164. src: "const x = 1",
  165. mode: "code",
  166. language: "ts",
  167. complete: true,
  168. })
  169. })
  170. })