Files
nanobot/tui/src/latex.test.ts
T

159 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-08-21 23:41:38 +08:00
import { describe, expect, test } from "bun:test"
import { renderLatexAsUnicode } from "./latex"
describe("terminal LaTeX rendering", () => {
test("renders the cache-rate example as readable Unicode text", () => {
expect(renderLatexAsUnicode([
"实际公式是:",
"",
"\\[",
"\\text{缓存率}=\\frac{\\text{cached input tokens}}{\\text{total input tokens}}",
"\\]",
"",
"缓存命中:\\(66{,}000 \\times 94\\% \\approx 62{,}040\\) tokens",
].join("\n"))).toBe([
"实际公式是:",
"",
"缓存率 = cached input tokens / total input tokens",
"",
"缓存命中:66,000 × 94% ≈ 62,040 tokens",
].join("\n"))
})
test("converts common symbols, scripts, roots, fractions, and accents", () => {
const source = "$\\hat{f}(\\xi)=\\sum_{n=0}^{\\infty} \\frac{x^n}{n!}$ and \\(x_1=\\sqrt[3]{\\alpha+\\beta}\\)"
expect(renderLatexAsUnicode(source)).toBe(
"f̂(ξ) = ∑ₙ₌₀^(∞) xⁿ / n! and x₁ = ³√(α+β)",
)
})
test("preserves precedence for compound fraction sides", () => {
expect(renderLatexAsUnicode("\\(\\frac{a-b}{c} + \\frac{a}{b \\times c}\\)"))
.toBe("(a-b) / c + a / (b × c)")
})
test("keeps currency and price ranges literal while converting guarded dollar math", () => {
const source = [
"Costs are $24 today or $10-20 later; variables $x$ and $2^n$ are math.",
"Shipping costs $5+$10 and paths use $HOME/$USER.",
2026-08-22 00:07:43 +08:00
"Mixed currency and variables like $5+$x$ remain literal.",
"Commands and groups stay literal: $5+$\\alpha$, $5+${x}$, and $5+$(x)$.",
2026-08-21 23:41:38 +08:00
"Matrix $A$ maps $V$ to $W$.",
].join("\n")
expect(renderLatexAsUnicode(source)).toBe([
"Costs are $24 today or $10-20 later; variables x and 2ⁿ are math.",
"Shipping costs $5+$10 and paths use $HOME/$USER.",
2026-08-22 00:07:43 +08:00
"Mixed currency and variables like $5+$x$ remain literal.",
"Commands and groups stay literal: $5+$\\alpha$, $5+${x}$, and $5+$(x)$.",
2026-08-21 23:41:38 +08:00
"Matrix A maps V to W.",
].join("\n"))
})
test("supports display dollars and aligned line breaks", () => {
const source = "$$\\begin{aligned}a&=b+c\\\\d&\\le e\\end{aligned}$$"
expect(renderLatexAsUnicode(source)).toBe("a = b+c\nd ≤ e")
})
test("leaves inline and fenced code unchanged", () => {
const source = [
"Inline `\\(x^2\\)` stays literal, but \\(y^2\\) renders.",
"",
"```tex",
"\\[\\frac{a}{b}\\]",
"```",
].join("\n")
expect(renderLatexAsUnicode(source)).toBe([
"Inline `\\(x^2\\)` stays literal, but y² renders.",
"",
"```tex",
"\\[\\frac{a}{b}\\]",
"```",
].join("\n"))
2026-08-22 00:07:43 +08:00
expect(renderLatexAsUnicode("`$x$``")).toBe("`x``")
2026-08-21 23:41:38 +08:00
})
test("leaves fenced code inside block quotes and lists unchanged", () => {
const source = [
"> ~~~tex",
"> $x^2$",
"> ~~~",
"",
"- ```tex",
" \\(y_1\\)",
" ```",
"",
"1. item",
"",
" ~~~tex",
" $z^2$",
" ~~~",
].join("\n")
expect(renderLatexAsUnicode(source)).toBe(source)
})
test("leaves unmatched delimiters available during streaming", () => {
expect(renderLatexAsUnicode("Working on \\(x^2")).toBe("Working on \\(x^2")
expect(renderLatexAsUnicode("Working $$x$")).toBe("Working $$x$")
expect(renderLatexAsUnicode("An escaped \\$5 stays literal.")).toBe("An escaped \\$5 stays literal.")
})
test("handles many unmatched openers without changing them", () => {
const openers = "\\(".repeat(20_000)
const backslashes = "\\".repeat(20_000)
2026-08-22 00:07:43 +08:00
const backticks = `text ${"`".repeat(20_000)} then $x$`
const descendingBackticks = "text " + Array.from(
{ length: 500 },
(_, index) => "`".repeat(500 - index),
).join("x") + " $x$"
const linkDestinations = `${"](".repeat(20_000)} then $x$`
2026-08-21 23:41:38 +08:00
expect(renderLatexAsUnicode(openers)).toBe(openers)
expect(renderLatexAsUnicode(backslashes)).toBe(backslashes)
2026-08-22 00:07:43 +08:00
expect(renderLatexAsUnicode(backticks)).toBe(backticks.replace("$x$", "x"))
expect(renderLatexAsUnicode(descendingBackticks))
.toBe(descendingBackticks.replace("$x$", "x"))
expect(renderLatexAsUnicode(linkDestinations)).toBe(linkDestinations.replace("$x$", "x"))
2026-08-21 23:41:38 +08:00
})
test("leaves unsupported, malformed, and deeply nested math unchanged", () => {
const deep = `{`.repeat(100) + "x" + `}`.repeat(100)
const source = [
"$\\color{red}{x}$",
"$\\frac{a}$",
`\\(${deep}\\)`,
"\\(\\displaystyle x^2\\)",
].join(" ")
expect(renderLatexAsUnicode(source)).toBe([
"$\\color{red}{x}$",
"$\\frac{a}$",
`\\(${deep}\\)`,
"x²",
].join(" "))
})
test("does not rewrite Markdown link destinations or autolinks", () => {
const source = [
"[formula $x$](https://example.test/$x$/\\(raw\\))",
"<https://example.test/$y$>",
"bare https://example.test/$z$ remains intact",
"<span title=\"$raw$\">HTML</span>",
"if a < $x$ > b",
].join("\n")
expect(renderLatexAsUnicode(source)).toBe([
"[formula x](https://example.test/$x$/\\(raw\\))",
"<https://example.test/$y$>",
"bare https://example.test/$z$ remains intact",
"<span title=\"$raw$\">HTML</span>",
"if a < x > b",
].join("\n"))
})
})