@@ -26,6 +26,81 @@ struct ChatMarkdownBlockSegmenterTests {
2626 ] )
2727 }
2828
29+ // MARK: - Headings
30+
31+ @Test func `all ATX heading levels become native blocks`() {
32+ for level in 1 ... 6 {
33+ let markdown = " \( String ( repeating: " # " , count: level) ) Heading \( level) "
34+ #expect( self . segments ( markdown) == [
35+ . heading( ChatMarkdownHeading ( level: level, markdown: markdown) ) ,
36+ ] )
37+ }
38+ }
39+
40+ @Test func `Setext headings preserve their complet e source`() {
41+ #expect( self . segments ( " Primary \n ======= " ) == [
42+ . heading( ChatMarkdownHeading ( level: 1 , markdown: " Primary \n ======= " ) ) ,
43+ ] )
44+ #expect( self . segments ( " Secondary \n --------- " ) == [
45+ . heading( ChatMarkdownHeading ( level: 2 , markdown: " Secondary \n --------- " ) ) ,
46+ ] )
47+ }
48+
49+ @Test func `streaming ATX heading keeps the current source`() {
50+ let markdown = " ### Streamed heading "
51+ #expect( self . segments ( markdown, isComplete: false ) == [
52+ . heading( ChatMarkdownHeading ( level: 3 , markdown: markdown) ) ,
53+ ] )
54+ }
55+
56+ @Test func `heading keeps inline markdo wn source and surrounding prose`() {
57+ let heading = " ## **Status** with `code` and [docs](https://example.com) ## "
58+ #expect( self . segments ( " before \n \n \( heading) \n \n after " ) == [
59+ . prose( " before " ) ,
60+ . heading( ChatMarkdownHeading ( level: 2 , markdown: heading) ) ,
61+ . prose( " after " ) ,
62+ ] )
63+ }
64+
65+ @Test func `headings nested in containers stay on the prose path`() {
66+ for markdown in [ " > # Quoted " , " - # Listed " ] {
67+ #expect( self . segments ( markdown) == [ . prose( markdown) ] )
68+ }
69+ }
70+
71+ @Test func `reference heading keeps do cument scoped definition`() {
72+ let markdown = " # [Docs][docs] \n \n [docs]: https://example.com "
73+ #expect( self . segments ( markdown) == [ . prose( markdown) ] )
74+ }
75+
76+ @Test func `heading composes with an unchanged native table`() {
77+ let blocks = self . segments ( " # Results \n \n | Name | Count | \n | --- | ---: | \n | Claw | 2 | " )
78+ #expect( blocks == [
79+ . heading( ChatMarkdownHeading ( level: 1 , markdown: " # Results " ) ) ,
80+ . table( ChatMarkdownTable (
81+ header: [ " Name " , " Count " ] ,
82+ alignments: [ . leading, . trailing] ,
83+ rows: [ [ " Claw " , " 2 " ] ] ) ) ,
84+ ] )
85+ }
86+
87+ @Test @MainActor func `render snapshot preserves heading inline attributes`() throws {
88+ let snapshot = ChatMarkdownRenderSnapshot (
89+ text: " ## **Status** and [docs](https://example.com) " ,
90+ isComplete: true )
91+ guard case let . heading( level, prose) = try #require( snapshot. blocks. first) else {
92+ Issue . record ( " expected heading block " )
93+ return
94+ }
95+
96+ #expect( level == 2 )
97+ #expect( String ( prose. attributed. characters) == " Status and docs " )
98+ #expect( prose. attributed. runs. contains { $0. link != nil } )
99+ #expect( prose. attributed. runs. contains {
100+ $0. inlinePresentationIntent? . contains ( . stronglyEmphasized) == true
101+ } )
102+ }
103+
29104 // MARK: - Fenced code
30105
31106 @Test func `fence with language and surrounding prose`() {
0 commit comments