@@ -70,3 +70,170 @@ fn symmetric_round_trip_list_without_empty_line_before_list(input: &str) {
7070 let result = crate :: printer:: render_markdown ( & doc, config) ;
7171 assert_eq ! ( input, result) ;
7272}
73+
74+ // Regression test: fenced code blocks inside list items should preserve internal indentation
75+ // and formatting should be idempotent (format(format(x)) == format(x))
76+ // Fix for bug: fenced code blocks inside lists were losing indentation on each render pass
77+ // because literal '\n' characters prevented nest() from applying indentation properly.
78+ #[ rstest(
79+ input,
80+ // Basic code block in ordered list item
81+ case(
82+ r#" 1. **Example:**
83+
84+ ```rust
85+ fn test() {
86+ println!("hello");
87+ }
88+ ```"#
89+ ) ,
90+ // Code block with multiple indentation levels
91+ case(
92+ r#" 1. Item with code:
93+
94+ ```python
95+ def foo():
96+ if True:
97+ return bar()
98+ ```"#
99+ ) ,
100+ // Nested list with code block (2 levels)
101+ case(
102+ r#" - Outer item
103+ - Inner item with code:
104+
105+ ```js
106+ function test() {
107+ console.log("nested");
108+ }
109+ ```"#
110+ ) ,
111+ // Empty code block in list item
112+ case(
113+ r#" - Empty code block:
114+
115+ ```rust
116+ ```"#
117+ ) ,
118+ // Code block with blank lines inside
119+ case(
120+ r#" - Code with blank lines:
121+
122+ ```python
123+ def foo():
124+ pass
125+
126+ def bar():
127+ pass
128+ ```"#
129+ ) ,
130+ // Deeply nested list with code block (3 levels)
131+ case(
132+ r#" - Level 1
133+ - Level 2
134+ - Level 3 with code:
135+
136+ ```rust
137+ fn deep() {
138+ nested();
139+ }
140+ ```"#
141+ ) ,
142+ // Unordered list with asterisk marker
143+ case(
144+ r#" * Item with asterisk:
145+
146+ ```rust
147+ fn asterisk() {}
148+ ```"#
149+ ) ,
150+ // Unordered list with plus marker
151+ case(
152+ r#" + Item with plus:
153+
154+ ```rust
155+ fn plus() {}
156+ ```"#
157+ ) ,
158+ // Multiple code blocks in one list item
159+ case(
160+ r#" - Multiple blocks:
161+
162+ First:
163+
164+ ```rust
165+ fn first() {}
166+ ```
167+
168+ Second:
169+
170+ ```rust
171+ fn second() {}
172+ ```"#
173+ ) ,
174+ // Code block with unusual info string
175+ case(
176+ r#" - Item:
177+
178+ ```rust,no_run,edition=2021
179+ fn info_string() {}
180+ ```"#
181+ ) ,
182+ ) ]
183+ fn fenced_code_block_in_list_idempotent ( input : & str ) {
184+ // First pass
185+ let doc1 = crate :: parser:: parse_markdown ( crate :: parser:: MarkdownParserState :: default ( ) , input)
186+ . unwrap ( ) ;
187+ let pass1 = crate :: printer:: render_markdown ( & doc1, crate :: printer:: config:: Config :: default ( ) ) ;
188+
189+ // Second pass - should be identical to first pass (idempotent)
190+ let doc2 = crate :: parser:: parse_markdown ( crate :: parser:: MarkdownParserState :: default ( ) , & pass1)
191+ . unwrap ( ) ;
192+ let pass2 = crate :: printer:: render_markdown ( & doc2, crate :: printer:: config:: Config :: default ( ) ) ;
193+
194+ assert_eq ! (
195+ pass1, pass2,
196+ "Formatting should be idempotent.\n Input:\n {}\n \n First pass:\n {}\n \n Second pass:\n {}" ,
197+ input, pass1, pass2
198+ ) ;
199+ }
200+
201+ // Test that code blocks in blockquotes are also idempotent
202+ #[ rstest(
203+ input,
204+ // Code block in blockquote
205+ case(
206+ r#"> Quote with code:
207+ >
208+ > ```rust
209+ > fn quoted() {
210+ > println!("in quote");
211+ > }
212+ > ```"#
213+ ) ,
214+ // Code block in GitHub alert
215+ case(
216+ r#"> [!NOTE]
217+ > Alert with code:
218+ >
219+ > ```python
220+ > def alert():
221+ > pass
222+ > ```"#
223+ ) ,
224+ ) ]
225+ fn fenced_code_block_in_blockquote_idempotent ( input : & str ) {
226+ let doc1 = crate :: parser:: parse_markdown ( crate :: parser:: MarkdownParserState :: default ( ) , input)
227+ . unwrap ( ) ;
228+ let pass1 = crate :: printer:: render_markdown ( & doc1, crate :: printer:: config:: Config :: default ( ) ) ;
229+
230+ let doc2 = crate :: parser:: parse_markdown ( crate :: parser:: MarkdownParserState :: default ( ) , & pass1)
231+ . unwrap ( ) ;
232+ let pass2 = crate :: printer:: render_markdown ( & doc2, crate :: printer:: config:: Config :: default ( ) ) ;
233+
234+ assert_eq ! (
235+ pass1, pass2,
236+ "Formatting should be idempotent.\n Input:\n {}\n \n First pass:\n {}\n \n Second pass:\n {}" ,
237+ input, pass1, pass2
238+ ) ;
239+ }
0 commit comments