/** * Regression tests for Telegram UI/compat rendering helpers * Covers nested lists, code blocks, tables, links, quotes, chunking, and other Telegram-specific render edge cases */ import assert from "node:assert/strict"; import test from "node:test"; import { chunkHtmlPreservingTags, escapeHtml, escapeHtmlAttribute, MAX_MESSAGE_LENGTH, renderTelegramMessage, } from "../lib/rendering.ts"; function countMatches(text: string, pattern: RegExp): number { return text.match(pattern)?.length ?? 0; } test("HTML helpers escape text and attribute contexts", () => { assert.equal(escapeHtml("&value"), "<tag>&value"); assert.equal( escapeHtmlAttribute(`"quoted" 'value' & `), ""quoted" 'value' & <tag>", ); }); test("HTML helpers chunk long text while preserving open tags", () => { const chunks = chunkHtmlPreservingTags(`${"x".repeat(10)}`, 12); assert.deepEqual(chunks, ["xxxxx", "xxxxx"]); }); test("HTML helpers do not reopen void tags across chunks", () => { const chunks = chunkHtmlPreservingTags(`a
${"b".repeat(10)}`, 8); assert.deepEqual(chunks, ["a
bbb", "bbbbbbbb".slice(0, 7)]); }); test("Malformed and boundary markdown renders safely", () => { const cases = [ "", "```ts\nconst raw = '**not bold**'", `${"x".repeat(MAX_MESSAGE_LENGTH + 250)} end`, `surrogate-boundary: ${"🙂".repeat(120)}\uD83D`, [ "> level 1", "> > level 2", "> > > level 3", "> > > > level 4", "> **bold quote", ].join("\n"), ]; for (const markdown of cases) { let chunks: ReturnType = []; assert.doesNotThrow(() => { chunks = renderTelegramMessage(markdown, { mode: "markdown" }); }); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( countMatches(chunk.text, /
/g), countMatches(chunk.text, /<\/blockquote>/g), ); assert.equal( countMatches(chunk.text, //g), countMatches(chunk.text, /<\/b>/g), ); assert.equal( countMatches(chunk.text, //g), countMatches(chunk.text, /<\/i>/g), ); assert.equal( countMatches(chunk.text, /
/g),
        countMatches(chunk.text, /<\/pre>/g),
      );
    }
  }
});

test("Nested lists stay out of code blocks", () => {
  const chunks = renderTelegramMessage(
    "- Level 1\n  - Level 2\n    - Level 3 with **bold** text",
    { mode: "markdown" },
  );
  assert.ok(chunks.length > 0);
  assert.equal(
    chunks.some((chunk) => chunk.text.includes("
")),
    false,
  );
  assert.equal(
    chunks.some((chunk) =>
      chunk.text.includes("- Level 3 with bold text"),
    ),
    true,
  );
});

test("Fenced code blocks preserve literal markdown", () => {
  const chunks = renderTelegramMessage('~~~ts\nconst value = "**raw**";\n~~~', {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /
/);
  assert.match(chunks[0]?.text ?? "", /\*\*raw\*\*/);
});

test("Underscores inside words do not become italic", () => {
  const chunks = renderTelegramMessage("Path: foo_bar_baz.txt and **bold**", {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.equal((chunks[0]?.text ?? "").includes("bar"), false);
  assert.match(chunks[0]?.text ?? "", /bold<\/b>/);
});

test("Bold markdown can span soft line breaks", () => {
  const chunks = renderTelegramMessage(
    "Скорее: **архитектурно — да,\nпрактически — почти**.",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /Скорее: архитектурно — да,\nпрактически — почти<\/b>\./,
  );
});

test("Quoted nested lists stay in blockquote rendering", () => {
  const chunks = renderTelegramMessage(
    "> Quoted intro\n> - nested item\n>   - deeper item",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /
/); assert.match(chunks[0]?.text ?? "", /nested item/); assert.match(chunks[0]?.text ?? "", /-<\/code> nested item/); assert.equal((chunks[0]?.text ?? "").includes("
"), false);
});

test("Numbered lists use monospace numeric markers", () => {
  const chunks = renderTelegramMessage("1. first\n  2. second", {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /1\.<\/code> first/);
  assert.match(chunks[0]?.text ?? "", /2\.<\/code> second/);
});

test("Ordered task lists preserve numeric markers in rendering", () => {
  const markdown = "1. [x] first\n2. [ ] second";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /1\.<\/code> \[x\]<\/code> first/,
  );
  assert.match(
    chunks[0]?.text ?? "",
    /2\.<\/code> \[ \]<\/code> second/,
  );
});

test("Leading indentation on the first markdown line stays intact", () => {
  const markdown = "  - nested bullet\n    - nested child";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /^\u00A0\u00A0-<\/code> nested bullet/m,
  );
  assert.match(
    chunks[0]?.text ?? "",
    /^\u00A0\u00A0\u00A0\u00A0-<\/code> nested child/m,
  );
});

test("UI/compat rendering preserves multiple blank lines between blocks", () => {
  const markdown = "# Title\n\n\nParagraph\n\n\n> Quote";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.match(
    chunks[0]?.text ?? "",
    /Title<\/b>\n\n\nParagraph\n\n\n
Quote<\/blockquote>/, ); }); test("UI/compat rendering preserves original blank-line spacing across block transitions", () => { const cases = [ { markdown: "Para\n\n\n```ts\nconst x = 1\n```", finalText: 'Para\n\n\n
const x = 1
', }, { markdown: "```ts\nconst x = 1\n```\n\n\nPara", finalText: '
const x = 1
\n\n\nPara', }, { markdown: "Para\n\n\n- item", finalText: "Para\n\n\n- item", }, { markdown: "Para\n\n\n> Quote", finalText: "Para\n\n\n
Quote
", }, ]; for (const testCase of cases) { const finalChunks = renderTelegramMessage(testCase.markdown, { mode: "markdown", }); assert.equal(finalChunks.length, 1); assert.equal(finalChunks[0]?.text ?? "", testCase.finalText); } }); test("Headings keep visible spacing before following code blocks even without source blank lines", () => { const markdown = "### Title\n```ts\nconst x = 1\n```"; const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.equal(chunks.length, 1); assert.match( chunks[0]?.text ?? "", /Title<\/b>\n\n
const x = 1<\/code><\/pre>/,
  );
});

test("Standalone checkbox-looking prose stays literal outside task lists", () => {
  const markdown = "Use [ ] as a placeholder and keep [x] literal";
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.equal((chunks[0]?.text ?? "").includes("[ ]"), false);
  assert.equal((chunks[0]?.text ?? "").includes("[x]"), false);
  assert.match(chunks[0]?.text ?? "", /Use \[ \] as a placeholder/);
  assert.match(chunks[0]?.text ?? "", /keep \[x\] literal/);
});

test("Nested blockquotes flatten into one Telegram blockquote with indentation", () => {
  const chunks = renderTelegramMessage("> outer\n>> inner\n>>> deepest", {
    mode: "markdown",
  });
  assert.equal(chunks.length, 1);
  assert.equal((chunks[0]?.text.match(/
/g) ?? []).length, 1); assert.equal((chunks[0]?.text.match(/<\/blockquote>/g) ?? []).length, 1); assert.match(chunks[0]?.text ?? "", /outer/); assert.match(chunks[0]?.text ?? "", /\u00A0\u00A0inner/); assert.match(chunks[0]?.text ?? "", /\u00A0\u00A0\u00A0\u00A0deepest/); }); test("UI/compat Markdown tables render as literal monospace blocks without outer side borders", () => { const chunks = renderTelegramMessage( "| Name | Value |\n| --- | --- |\n| **x** | `y` |", { mode: "markdown" }, ); assert.equal(chunks.length, 1); assert.match(chunks[0]?.text ?? "", /
/);
  assert.equal((chunks[0]?.text ?? "").includes("x"), false);
  assert.match(chunks[0]?.text ?? "", /Name\s+\|\s+Value/);
  assert.match(chunks[0]?.text ?? "", /x\s+\|\s+y/);
  assert.equal((chunks[0]?.text ?? "").includes("| Name |"), false);
  assert.equal((chunks[0]?.text ?? "").includes("| x |"), false);
});

test("Markdown table padding uses emoji grapheme display width", () => {
  const chunks = renderTelegramMessage(
    "| Icon | Name |\n| --- | --- |\n| 👩‍💻 | dev |\n| ✅ | done |\n| 1️⃣ | key |",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /Icon \| Name/);
  assert.match(chunks[0]?.text ?? "", /👩‍💻 {3}\| dev /u);
  assert.match(chunks[0]?.text ?? "", /✅ {3}\| done/u);
  assert.match(chunks[0]?.text ?? "", /1️⃣ {3}\| key /u);
});

test("Markdown table padding handles CJK and combining marks", () => {
  const chunks = renderTelegramMessage(
    "| Word | Note |\n| --- | --- |\n| é | acute |\n| 東京 | city |",
    { mode: "markdown" },
  );
  assert.equal(chunks.length, 1);
  assert.match(chunks[0]?.text ?? "", /Word \| Note /);
  assert.match(chunks[0]?.text ?? "", /é {4}\| acute/u);
  assert.match(chunks[0]?.text ?? "", /東京 \| city /u);
});

test("Markdown table padding measures wide, zero-width, and control code points", () => {
  // [cell, expected display width]
  const cells: Array<[string, number]> = [
    ["a", 1],
    ["\u{d55c}", 2], // Hangul syllable
    ["\u{ff21}", 2], // fullwidth Latin
    ["\u{2329}", 2], // angle bracket
    ["\u{f900}", 2], // CJK compatibility ideograph
    ["\u{fe10}", 2], // vertical form
    ["\u{fe30}", 2], // CJK compatibility form
    ["\u{ffe0}", 2], // fullwidth sign
    ["\u{1100}", 2], // Hangul Jamo
    ["é", 1], // combining mark
    ["a‍b", 2], // zero-width joiner
    ["a️", 1], // variation selector
    ["a\u0001b", 2], // control character
    ["ab", 2],
  ];
  const markdown = [
    "| W | N |",
    "| --- | --- |",
    ...cells.map(([cell], index) => `| ${cell} | c${index} |`),
  ].join("\n");
  const chunks = renderTelegramMessage(markdown, { mode: "markdown" });
  assert.equal(chunks.length, 1);
  const expectedRows = [
    "W   | N  ",
    "--- | ---",
    ...cells.map(
      ([cell, width], index) =>
        `${cell}${" ".repeat(3 - width)} | ${`c${index}`.padEnd(3)}`,
    ),
  ];
  assert.equal(
    chunks[0]?.text,
    `
${expectedRows.join("\n")}
`, ); }); test("Markdown link targets accept valid destinations and titles and reject malformed ones", () => { const link = (href: string) => `a`; const cases: Array<[string, string]> = [ // plain destinations ["[a](https://x.com)", link("https://x.com")], ["[a]( https://x.com )", link("https://x.com")], ["[a](https://x.com/a_(b))", link("https://x.com/a_(b)")], [String.raw`[a](https://x.com/a\)b)`, link(String.raw`https://x.com/a\)b`)], [String.raw`[a](https://x.com/a\ b)`, link(String.raw`https://x.com/a\ b`)], ["[a](https://x.com/a b)", "[a](https://x.com/a b)"], ["[a](https://x.com/a(b)", "[a](https://x.com/a(b)"], // angle-bracket destinations ["[a]()", link("https://x.com/a b")], [String.raw`[a](b>)`, link(String.raw`https://x.com/a\>b`)], ["[a]()", "[a](<>)"], // missing destination or end of input ["[a]()", "[a]()"], ["[a](", "[a]("], ["[a]( ", "[a]( "], // titles ['[a](https://x.com "T")', link("https://x.com")], ["[a](https://x.com 'T')", link("https://x.com")], ["[a](https://x.com (T))", link("https://x.com")], ['[a]( "T")', link("https://x.com")], ["[a](https://x.com )", link("https://x.com")], [String.raw`[a](https://x.com "T\"q")`, link("https://x.com")], ['[a](https://x.com "T)', '[a](https://x.com "T)'], [String.raw`[a](https://x.com "T\")`, String.raw`[a](https://x.com "T\")`], ["[a](https://x.com T)", "[a](https://x.com T)"], ]; for (const [input, expected] of cases) { const chunks = renderTelegramMessage(input, { mode: "markdown" }); assert.equal(chunks[0]?.text, expected, input); } }); test("Links, code spans, and underscore-heavy text coexist safely", () => { const chunks = renderTelegramMessage( "See [docs](https://example.com), run `foo_bar()` and keep foo_bar.txt literal", { mode: "markdown" }, ); assert.equal(chunks.length, 1); assert.match( chunks[0]?.text ?? "", /docs<\/a>/, ); assert.match(chunks[0]?.text ?? "", /foo_bar\(\)<\/code>/); assert.equal((chunks[0]?.text ?? "").includes("bar"), false); }); test("HTML attributes are escaped or sanitized in generated Telegram markup", () => { const linkChunks = renderTelegramMessage( '[quoted]()', { mode: "markdown" }, ); assert.match( linkChunks[0]?.text ?? "", /quoted<\/a>/, ); const codeChunks = renderTelegramMessage( '```ts"onclick=bad\nconst value = 1;\n```', { mode: "markdown" }, ); assert.match( codeChunks[0]?.text ?? "", /
/,
  );
  assert.equal((codeChunks[0]?.text ?? "").includes('"onclick'), false);
});

test("HTML mode chunks long messages below Telegram limits", () => {
  const chunks = renderTelegramMessage("x".repeat(5000), {
    mode: "html",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.equal(chunk.parseMode, "HTML");
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
  }
});

test("HTML mode keeps tags balanced across long chunk boundaries", () => {
  const chunks = renderTelegramMessage(
    `
${"x".repeat(5000)}
`, { mode: "html" }, ); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.equal(chunk.parseMode, "HTML"); assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, (chunk.text.match(/<\/b>/g) ?? []).length, ); } }); test("Links degrade or normalize safely across supported and unsupported markdown forms", () => { const markdown = [ "[**Bold** label](https://example.com/path)", "[Docs](https://example.com/a_(b))", '[Title](https://example.com/path "Tooltip")', "[Relative](./docs/README.md)", "[Ref][docs]", "", "[docs]: https://example.com/ref", "", "Footnote[^1]", "", "[^1]: Footnote body", ].join("\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.equal(chunks.length, 1); assert.match( chunks[0]?.text ?? "", /Bold label<\/a>/, ); assert.match( chunks[0]?.text ?? "", /Docs<\/a>/, ); assert.match( chunks[0]?.text ?? "", /Title<\/a>/, ); assert.equal( (chunks[0]?.text ?? "").includes(''), false, ); assert.match(chunks[0]?.text ?? "", /Relative/); assert.equal( (chunks[0]?.text ?? "").includes(''), false, ); assert.match(chunks[0]?.text ?? "", /\[Ref\]\[docs\]/); assert.match(chunks[0]?.text ?? "", /Footnote\[\^1\]/); assert.match(chunks[0]?.text ?? "", /\[\^1\]: Footnote body/); }); test("Long quoted blocks stay chunked with balanced blockquote tags", () => { const markdown = Array.from( { length: 500 }, (_, index) => `> quoted **${index}** line`, ).join("\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); } }); test("Long UI/compat Markdown messages stay chunked below Telegram limits", () => { const markdown = Array.from( { length: 600 }, (_, index) => `- item **${index}**`, ).join("\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(//g) ?? []).length, (chunk.text.match(/<\/b>/g) ?? []).length, ); } }); test("Long mixed links and code spans stay chunked with balanced inline tags", () => { const markdown = Array.from( { length: 450 }, (_, index) => `Paragraph ${index}: see [docs ${index}](https://example.com/${index}), run \`code_${index}()\`, and keep foo_bar_${index}.txt literal`, ).join("\n\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(//g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, (chunk.text.match(/<\/code>/g) ?? []).length, ); assert.equal((chunk.text ?? "").includes("bar"), false); } }); test("Long multi-block markdown keeps quotes and code fences structurally balanced", () => { const markdown = Array.from({ length: 120 }, (_, index) => { return [ `## Section ${index}`, `> quoted **${index}** line`, `- item ${index}`, "```ts", `const value_${index} = "**raw**";`, "```", ].join("\n"); }).join("\n\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(/
<\/pre>/g) ?? []).length,
    );
  }
});

test("Chunked mixed block transitions keep quote and list structure balanced", () => {
  const markdown = Array.from({ length: 260 }, (_, index) => {
    return [
      `> quoted **${index}** intro`,
      `> continuation ${index}`,
      `- item ${index}`,
      `plain paragraph ${index} with [link](https://example.com/${index})`,
    ].join("\n");
  }).join("\n\n");
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
    assert.equal(
      (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, ); } }); test("Chunked code fence transitions keep code blocks closed before following prose", () => { const markdown = Array.from({ length: 220 }, (_, index) => { return [ "```ts", `const block_${index} = "value_${index}";`, "```", `After code **${index}** and \`inline_${index}()\``, ].join("\n"); }).join("\n\n"); const chunks = renderTelegramMessage(markdown, { mode: "markdown", }); assert.ok(chunks.length > 1); for (const chunk of chunks) { assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH); assert.equal( (chunk.text.match(/
<\/pre>/g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
      (chunk.text.match(/<\/code>/g) ?? []).length,
    );
  }
});

test("Long inline formatting paragraphs stay balanced across chunk boundaries", () => {
  const markdown = Array.from({ length: 500 }, (_, index) => {
    return `Segment ${index} keeps **bold_${index}** with \`code_${index}()\`, [link_${index}](https://example.com/${index}), and foo_bar_${index}.txt literal.`;
  }).join(" ");
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
      (chunk.text.match(/<\/b>/g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(//g) ?? []).length,
      (chunk.text.match(/<\/code>/g) ?? []).length,
    );
    assert.equal(chunk.text.includes("bar"), false);
  }
});

test("Realistic Markdown fixtures render safely for UI/compat surfaces", () => {
  const fixtures = [
    [
      "Here is the UI action plan:",
      "",
      "1. [x] Snapshot the database",
      "2. [ ] Apply the patch with `pnpm db:migrate`",
      "3. [ ] Verify [Grafana](https://example.com/dashboards/db)",
      "",
      "> Note: keep `user_id` and `team_id` untouched.",
      "",
      "```sql",
      "ALTER TABLE users ADD COLUMN team_id uuid; -- **literal**",
      "```",
    ].join("\n"),
    [
      "### Comparison",
      "| Option | Pros | Cons |",
      "| --- | --- | --- |",
      "| A | Fast | More foo_bar flags |",
      "| B | Safer | Requires rollback notes |",
      "",
      "Unsupported relative link: [local](./notes.md)",
      "Supported link: [docs](https://example.com/path?q=a_b)",
    ].join("\n"),
    [
      "The JSON payload should stay literal:",
      "",
      "```json",
      '{ "markdown": "**not bold**", "path": "foo_bar/baz" }',
      "```",
      "",
      "Nested quote from interactive compatibility message:",
      "> outer",
      ">> inner with **bold**",
    ].join("\n"),
  ];
  for (const markdown of fixtures) {
    const chunks = renderTelegramMessage(markdown, {
      mode: "markdown",
    });
    assert.ok(chunks.length > 0);
    for (const chunk of chunks) {
      assert.equal(chunk.parseMode, "HTML");
      assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
      assert.equal(
        countMatches(chunk.text, //g),
      );
      assert.equal(
        countMatches(chunk.text, /
/g), countMatches(chunk.text, /<\/blockquote>/g), ); assert.equal( countMatches(chunk.text, /
<\/pre>/g),
      );
      assert.equal(chunk.text.includes("bar"), false);
    }
  }
});

test("Chunked list, code, quote, and prose cycles stay balanced across transitions", () => {
  const markdown = Array.from({ length: 180 }, (_, index) => {
    return [
      `- list item **${index}**`,
      "```ts",
      `const cycle_${index} = "value_${index}";`,
      "```",
      `> quoted ${index} with [link](https://example.com/${index})`,
      `Plain paragraph ${index} with \`inline_${index}()\``,
    ].join("\n");
  }).join("\n\n");
  const chunks = renderTelegramMessage(markdown, {
    mode: "markdown",
  });
  assert.ok(chunks.length > 1);
  for (const chunk of chunks) {
    assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
    assert.equal(
      (chunk.text.match(/
<\/pre>/g) ?? []).length,
    );
    assert.equal(
      (chunk.text.match(/
/g) ?? []).length, (chunk.text.match(/<\/blockquote>/g) ?? []).length, ); assert.equal( (chunk.text.match(//g) ?? []).length, ); } });