markdown-stream.ts 1.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. import { marked, type Tokens } from "marked"
  2. import remend from "remend"
  3. export type Block = {
  4. raw: string
  5. src: string
  6. mode: "full" | "live"
  7. }
  8. function refs(text: string) {
  9. return /^\[[^\]]+\]:\s+\S+/m.test(text) || /^\[\^[^\]]+\]:\s+/m.test(text)
  10. }
  11. function open(raw: string) {
  12. const match = raw.match(/^[ \t]{0,3}(`{3,}|~{3,})/)
  13. if (!match) return false
  14. const mark = match[1]
  15. if (!mark) return false
  16. const char = mark[0]
  17. const size = mark.length
  18. const last = raw.trimEnd().split("\n").at(-1)?.trim() ?? ""
  19. return !new RegExp(`^[\\t ]{0,3}${char}{${size},}[\\t ]*$`).test(last)
  20. }
  21. function heal(text: string) {
  22. return remend(text, { linkMode: "text-only" })
  23. }
  24. export function stream(text: string, live: boolean) {
  25. if (!live) return [{ raw: text, src: text, mode: "full" }] satisfies Block[]
  26. const src = heal(text)
  27. if (refs(text)) return [{ raw: text, src, mode: "live" }] satisfies Block[]
  28. const tokens = marked.lexer(text)
  29. const tail = tokens.findLastIndex((token) => token.type !== "space")
  30. if (tail < 0) return [{ raw: text, src, mode: "live" }] satisfies Block[]
  31. const last = tokens[tail]
  32. if (!last || last.type !== "code") return [{ raw: text, src, mode: "live" }] satisfies Block[]
  33. const code = last as Tokens.Code
  34. if (!open(code.raw)) return [{ raw: text, src, mode: "live" }] satisfies Block[]
  35. const head = tokens
  36. .slice(0, tail)
  37. .map((token) => token.raw)
  38. .join("")
  39. if (!head) return [{ raw: code.raw, src: code.raw, mode: "live" }] satisfies Block[]
  40. return [
  41. { raw: head, src: heal(head), mode: "live" },
  42. { raw: code.raw, src: code.raw, mode: "live" },
  43. ] satisfies Block[]
  44. }