@@ -100,4 +100,111 @@ describe("parseMarkdownToBlocks — code fences", () => {
100100 expect ( blocks [ 0 ] . language ) . toBe ( "ts" ) ;
101101 expect ( blocks [ 0 ] . content ) . toBe ( "" ) ;
102102 } ) ;
103+
104+ /**
105+ * Indented code fences (e.g. inside list items in a plan) must close when
106+ * the closing fence is equally indented. Before the fix, the closing fence
107+ * regex required backticks at column 0, so indented ``` never matched and
108+ * the code block swallowed everything to EOF.
109+ */
110+ test ( "indented closing fence closes the code block" , ( ) => {
111+ const md = "- Replace with reads:\n ```ts\n const x = 1;\n ```\n- Next item" ;
112+ const blocks = parseMarkdownToBlocks ( md ) ;
113+ const types = blocks . map ( ( b ) => b . type ) ;
114+ expect ( types ) . toEqual ( [ "list-item" , "code" , "list-item" ] ) ;
115+ expect ( blocks [ 1 ] . content ) . toBe ( " const x = 1;" ) ;
116+ expect ( blocks [ 2 ] . content ) . toBe ( "Next item" ) ;
117+ } ) ;
118+
119+ /**
120+ * A closing fence with trailing text (e.g. ``` some comment) should still
121+ * close the block. Before the fix, the regex required only whitespace after
122+ * backticks, so trailing text caused the fence to swallow everything to EOF.
123+ */
124+ test ( "closing fence with trailing text still closes the block" , ( ) => {
125+ const md = "```js\nconst x = 1;\n``` this is ignored\nafter" ;
126+ const blocks = parseMarkdownToBlocks ( md ) ;
127+ expect ( blocks ) . toHaveLength ( 2 ) ;
128+ expect ( blocks [ 0 ] . type ) . toBe ( "code" ) ;
129+ expect ( blocks [ 0 ] . content ) . toBe ( "const x = 1;" ) ;
130+ expect ( blocks [ 1 ] . type ) . toBe ( "paragraph" ) ;
131+ expect ( blocks [ 1 ] . content ) . toBe ( "after" ) ;
132+ } ) ;
133+
134+ test ( "deeply indented fence closes correctly" , ( ) => {
135+ const md = " ```py\n print('hi')\n ```\nafter" ;
136+ const blocks = parseMarkdownToBlocks ( md ) ;
137+ expect ( blocks [ 0 ] . type ) . toBe ( "code" ) ;
138+ expect ( blocks [ 0 ] . language ) . toBe ( "py" ) ;
139+ expect ( blocks [ 0 ] . content ) . toBe ( " print('hi')" ) ;
140+ expect ( blocks [ 1 ] . type ) . toBe ( "paragraph" ) ;
141+ expect ( blocks [ 1 ] . content ) . toBe ( "after" ) ;
142+ } ) ;
143+ } ) ;
144+
145+ describe ( "parseMarkdownToBlocks — tables" , ( ) => {
146+ test ( "pipe-delimited table parses correctly" , ( ) => {
147+ const md = "| A | B |\n|---|---|\n| 1 | 2 |" ;
148+ const blocks = parseMarkdownToBlocks ( md ) ;
149+ expect ( blocks ) . toHaveLength ( 1 ) ;
150+ expect ( blocks [ 0 ] . type ) . toBe ( "table" ) ;
151+ } ) ;
152+
153+ /**
154+ * Prose containing pipe characters (e.g. TypeScript union types in inline
155+ * code) must NOT be treated as a table. Before the fix, the regex
156+ * matched any line with 2+ pipes.
157+ */
158+ test ( "paragraph with inline code containing pipes is not a table" , ( ) => {
159+ const md = "The type is `'scroll' | 'wrap'` and supports `'a' | 'b' | 'c'` values." ;
160+ const blocks = parseMarkdownToBlocks ( md ) ;
161+ expect ( blocks ) . toHaveLength ( 1 ) ;
162+ expect ( blocks [ 0 ] . type ) . toBe ( "paragraph" ) ;
163+ expect ( blocks [ 0 ] . content ) . toBe ( md ) ;
164+ } ) ;
165+
166+ test ( "paragraph with multiple pipes in prose is not a table" , ( ) => {
167+ const md = "Use option A | B | C depending on context." ;
168+ const blocks = parseMarkdownToBlocks ( md ) ;
169+ expect ( blocks ) . toHaveLength ( 1 ) ;
170+ expect ( blocks [ 0 ] . type ) . toBe ( "paragraph" ) ;
171+ } ) ;
172+
173+ test ( "real-world plan: prose with union types is not a table" , ( ) => {
174+ const md = "`@pierre/diffs` supports `overflow: 'scroll' | 'wrap'` plus options, but Plannotator doesn't expose any of them." ;
175+ const blocks = parseMarkdownToBlocks ( md ) ;
176+ expect ( blocks ) . toHaveLength ( 1 ) ;
177+ expect ( blocks [ 0 ] . type ) . toBe ( "paragraph" ) ;
178+ } ) ;
179+ } ) ;
180+
181+ describe ( "parseMarkdownToBlocks — real-world plan regression" , ( ) => {
182+ test ( "indented code fence inside list does not swallow rest of plan" , ( ) => {
183+ const md = [
184+ "### 5. Migrate App.tsx" ,
185+ "" ,
186+ "- **Remove** `useState` for `diffStyle`" ,
187+ "- **Replace** with ConfigStore reads:" ,
188+ " ```ts" ,
189+ " const diffStyle = useConfigValue('diffStyle');" ,
190+ " ```" ,
191+ "- **Update** toolbar toggle handler" ,
192+ "" ,
193+ "### 6. Update DiffViewer" ,
194+ ] . join ( "\n" ) ;
195+ const blocks = parseMarkdownToBlocks ( md ) ;
196+ const types = blocks . map ( ( b ) => b . type ) ;
197+ expect ( types ) . toEqual ( [
198+ "heading" , // ### 5. Migrate App.tsx
199+ "list-item" , // - **Remove**
200+ "list-item" , // - **Replace**
201+ "code" , // ```ts ... ```
202+ "list-item" , // - **Update**
203+ "heading" , // ### 6. Update DiffViewer
204+ ] ) ;
205+ expect ( blocks [ 3 ] . type ) . toBe ( "code" ) ;
206+ expect ( blocks [ 3 ] . language ) . toBe ( "ts" ) ;
207+ expect ( blocks [ 4 ] . type ) . toBe ( "list-item" ) ;
208+ expect ( blocks [ 5 ] . type ) . toBe ( "heading" ) ;
209+ } ) ;
103210} ) ;
0 commit comments