Files
openclaw/src/markdown/ir.blockquote-spacing.test.ts
Hudson 1d6abddb9f fix(signal): outbound formatting and markdown IR rendering improvements (#9781)
* fix: Signal and markdown formatting improvements

Markdown IR fixes:
- Fix list-paragraph spacing (extra newline between list items and following paragraphs)
- Fix nested list indentation and newline handling
- Fix blockquote_close emitting redundant newline (inner content handles spacing)
- Render horizontal rules as visible ─── separator instead of silent drop
- Strip inner cell styles in code-mode tables to prevent overlapping with code_block span

Signal formatting fixes:
- Normalize URLs for dedup comparison (strip protocol, www., trailing slash)
- Render headings as bold text (headingStyle: 'bold')
- Add '> ' prefix to blockquotes for visual distinction
- Re-chunk after link expansion to respect chunk size limits

Tests:
- 51 new tests for markdown IR (spacing, lists, blockquotes, tables, HR)
- 18 new tests for Signal formatting (URL dedup, headings, blockquotes, HR, chunking)
- Update Slack nested list test expectation to match corrected IR output

* refactor: style-aware Signal text chunker

Replace indexOf-based chunk position tracking with deterministic
cursor tracking. The new splitSignalFormattedText:

- Splits at whitespace/newline boundaries within the limit
- Avoids breaking inside parentheses (preserves expanded link URLs)
- Slices style ranges at chunk boundaries with correct local offsets
- Tracks position via offset arithmetic instead of fragile indexOf

Removes dependency on chunkText from auto-reply/chunk.

Tests: 19 new tests covering style preservation across chunk boundaries,
edge cases (empty text, under limit, exact split points), and integration
with link expansion.

* fix: correct Signal style offsets with multiple link expansions

applyInsertionsToStyles() was using original coordinates for each
insertion without tracking cumulative shift from prior insertions.
This caused bold/italic/etc styles to drift to wrong text positions
when multiple markdown links expanded in a single message.

Added cumulative shift tracking and a regression test.

* test: clean up test noise and fix ineffective assertions

- Remove console.log from ir.list-spacing and ir.hr-spacing tests
- Fix ir.nested-lists.test.ts: remove ineffective regex assertion
- Fix ir.hr-spacing.test.ts: add actual assertions to edge case test

* refactor: split Signal formatting tests (#9781) (thanks @heyhudson)

---------

Co-authored-by: Hudson <258693705+hudson-rivera@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 16:57:20 +01:00

203 lines
7.0 KiB
TypeScript

/**
* Blockquote Spacing Tests
*
* Per CommonMark spec (§5.1 Block quotes), blockquotes are "container blocks" that
* contain other block-level elements (paragraphs, code blocks, etc.).
*
* In plaintext rendering, the expected spacing between block-level elements is
* a single blank line (double newline `\n\n`). This is the standard paragraph
* separation used throughout markdown.
*
* CORRECT behavior:
* - Blockquote content followed by paragraph: "quote\n\nparagraph" (double \n)
* - Two consecutive blockquotes: "first\n\nsecond" (double \n)
*
* BUG (current behavior):
* - Produces triple newlines: "quote\n\n\nparagraph"
*
* Root cause:
* 1. `paragraph_close` inside blockquote adds `\n\n` (correct)
* 2. `blockquote_close` adds another `\n` (incorrect)
* 3. Result: `\n\n\n` (triple newlines - incorrect)
*
* The fix: `blockquote_close` should NOT add `\n` because:
* - Blockquotes are container blocks, not leaf blocks
* - The inner content (paragraph, heading, etc.) already provides block separation
* - Container closings shouldn't add their own spacing
*/
import { describe, it, expect } from "vitest";
import { markdownToIR } from "./ir.js";
describe("blockquote spacing", () => {
describe("blockquote followed by paragraph", () => {
it("should have double newline (one blank line) between blockquote and paragraph", () => {
const input = "> quote\n\nparagraph";
const result = markdownToIR(input);
// CORRECT: "quote\n\nparagraph" (double newline)
// BUG: "quote\n\n\nparagraph" (triple newline)
expect(result.text).toBe("quote\n\nparagraph");
});
it("should not produce triple newlines", () => {
const input = "> quote\n\nparagraph";
const result = markdownToIR(input);
expect(result.text).not.toContain("\n\n\n");
});
});
describe("consecutive blockquotes", () => {
it("should have double newline between two blockquotes", () => {
const input = "> first\n\n> second";
const result = markdownToIR(input);
expect(result.text).toBe("first\n\nsecond");
});
it("should not produce triple newlines between blockquotes", () => {
const input = "> first\n\n> second";
const result = markdownToIR(input);
expect(result.text).not.toContain("\n\n\n");
});
});
describe("nested blockquotes", () => {
it("should handle nested blockquotes correctly", () => {
const input = "> outer\n>> inner";
const result = markdownToIR(input);
// Inner blockquote becomes separate paragraph
expect(result.text).toBe("outer\n\ninner");
});
it("should not produce triple newlines in nested blockquotes", () => {
const input = "> outer\n>> inner\n\nparagraph";
const result = markdownToIR(input);
expect(result.text).not.toContain("\n\n\n");
});
it("should handle deeply nested blockquotes", () => {
const input = "> level 1\n>> level 2\n>>> level 3";
const result = markdownToIR(input);
// Each nested level is a new paragraph
expect(result.text).not.toContain("\n\n\n");
});
});
describe("blockquote followed by other block elements", () => {
it("should have double newline between blockquote and heading", () => {
const input = "> quote\n\n# Heading";
const result = markdownToIR(input);
expect(result.text).toBe("quote\n\nHeading");
expect(result.text).not.toContain("\n\n\n");
});
it("should have double newline between blockquote and list", () => {
const input = "> quote\n\n- item";
const result = markdownToIR(input);
// The list item becomes "• item"
expect(result.text).toBe("quote\n\n• item");
expect(result.text).not.toContain("\n\n\n");
});
it("should have double newline between blockquote and code block", () => {
const input = "> quote\n\n```\ncode\n```";
const result = markdownToIR(input);
// Code blocks preserve their trailing newline
expect(result.text.startsWith("quote\n\ncode")).toBe(true);
expect(result.text).not.toContain("\n\n\n");
});
it("should have double newline between blockquote and horizontal rule", () => {
const input = "> quote\n\n---\n\nparagraph";
const result = markdownToIR(input);
// HR just adds a newline in IR, but should not create triple newlines
expect(result.text).not.toContain("\n\n\n");
});
});
describe("blockquote with multi-paragraph content", () => {
it("should handle multi-paragraph blockquote followed by paragraph", () => {
const input = "> first paragraph\n>\n> second paragraph\n\nfollowing paragraph";
const result = markdownToIR(input);
// Multi-paragraph blockquote should have proper internal spacing
// AND proper spacing with following content
expect(result.text).toContain("first paragraph\n\nsecond paragraph");
expect(result.text).not.toContain("\n\n\n");
});
});
describe("blockquote prefix option", () => {
it("should include prefix and maintain proper spacing", () => {
const input = "> quote\n\nparagraph";
const result = markdownToIR(input, { blockquotePrefix: "> " });
// With prefix, should still have proper spacing
expect(result.text).toBe("> quote\n\nparagraph");
expect(result.text).not.toContain("\n\n\n");
});
});
describe("edge cases", () => {
it("should handle empty blockquote followed by paragraph", () => {
const input = ">\n\nparagraph";
const result = markdownToIR(input);
expect(result.text).not.toContain("\n\n\n");
});
it("should handle blockquote at end of document", () => {
const input = "paragraph\n\n> quote";
const result = markdownToIR(input);
// No trailing triple newlines
expect(result.text).not.toContain("\n\n\n");
});
it("should handle multiple blockquotes with paragraphs between", () => {
const input = "> first\n\nparagraph\n\n> second";
const result = markdownToIR(input);
expect(result.text).toBe("first\n\nparagraph\n\nsecond");
expect(result.text).not.toContain("\n\n\n");
});
});
});
describe("comparison with other block elements (control group)", () => {
it("paragraphs should have double newline separation", () => {
const input = "paragraph 1\n\nparagraph 2";
const result = markdownToIR(input);
expect(result.text).toBe("paragraph 1\n\nparagraph 2");
expect(result.text).not.toContain("\n\n\n");
});
it("list followed by paragraph should have double newline", () => {
const input = "- item 1\n- item 2\n\nparagraph";
const result = markdownToIR(input);
// Lists already work correctly
expect(result.text).toContain("• item 2\n\nparagraph");
expect(result.text).not.toContain("\n\n\n");
});
it("heading followed by paragraph should have double newline", () => {
const input = "# Heading\n\nparagraph";
const result = markdownToIR(input);
expect(result.text).toBe("Heading\n\nparagraph");
expect(result.text).not.toContain("\n\n\n");
});
});