mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-25 22:48:38 +00:00
* 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>
203 lines
7.0 KiB
TypeScript
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");
|
|
});
|
|
});
|