Преглед на файлове

fix: separate paragraphs with \n\n in combined text so GFM resolves block-level rules correctly

contentToDoc splits each line into its own PM paragraph, but buildCombinedText
joined them with \n, causing GFM to merge consecutive non-blank lines into a
single Paragraph node. This broke task detection (TODO on a non-first line was
invisible) and multi-line blockquote/callout classification.

- buildCombinedText now joins with \n\n (+2 boundary for non-last paragraphs)
- TASK_RE changed to \b word boundary for robustness
- TaskParser.finish() uses match.index for correct afterMarker offset
Zander Hawke преди 4 дни
родител
ревизия
97b1254d14

+ 3 - 3
packages/editor/src/composables/__tests__/markdown-decorations.test.ts

@@ -239,9 +239,9 @@ describe("Markdown decorations plugin", () => {
       }
     }
 
-    // Combined-parsing: one node wrapper covers the full callout (both lines
-    // merged into a single md-callout block), not one per paragraph.
-    expect(mdCalloutCount).toBe(1)
+    // Combined-parsing with \n\n separator: each paragraph gets its own
+    // node wrapper, so callout + continuation = 2 md-callout decorations.
+    expect(mdCalloutCount).toBe(2)
   })
 
   it("builds decorations for inline math", () => {

+ 8 - 5
packages/editor/src/composables/useMarkdownDecorations.ts

@@ -107,7 +107,7 @@ function collectParagraphs(doc: ProsemirrorNode): ParagraphInfo[] {
 }
 
 /**
- * Build the combined text (joined by \n) and boundary offsets.
+ * Build the combined text (joined by \n\n) and boundary offsets.
  * boundaries[0] = 0, boundaries[i] = start offset of paragraph i,
  * boundaries[N] = total length (last para end, no trailing \n).
  */
@@ -117,11 +117,14 @@ function buildCombinedText(paragraphs: ParagraphInfo[]): {
 } {
   const parts: string[] = []
   const boundaries: number[] = [0]
-  for (const p of paragraphs) {
-    parts.push(p.text)
-    boundaries.push(boundaries[boundaries.length - 1]! + p.text.length + 1)
+  for (let i = 0; i < paragraphs.length; i++) {
+    parts.push(paragraphs[i]!.text)
+    const sepLen = i < paragraphs.length - 1 ? 2 : 1
+    boundaries.push(
+      boundaries[boundaries.length - 1]! + paragraphs[i]!.text.length + sepLen,
+    )
   }
-  return { text: parts.join("\n"), boundaries }
+  return { text: parts.join("\n\n"), boundaries }
 }
 
 /**

+ 4 - 3
packages/editor/src/lib/markdown-extensions.ts

@@ -230,7 +230,7 @@ const TASK_STATES = [
   "CANCELLED",
 ]
 
-const TASK_RE = new RegExp(`^\\s*(${TASK_STATES.join("|")})\\s+`, "i")
+const TASK_RE = new RegExp(`(?:^|\\b)(${TASK_STATES.join("|")})\\s+`, "i")
 
 class TaskParser implements LeafBlockParser {
   nextLine() {
@@ -246,8 +246,9 @@ class TaskParser implements LeafBlockParser {
     // biome-ignore lint/style/noNonNullAssertion: match confirmed by TASK_RE above
     const markerLen = match[1]!.length
     // biome-ignore lint/style/noNonNullAssertion: match confirmed by TASK_RE above
-    const afterMarker = match[0]!.length
-    const markerStart = leaf.start + leading
+    const matchIndex = match.index!
+    const afterMarker = matchIndex + match[0]!.length
+    const markerStart = leaf.start + leading + matchIndex
     cx.addLeafElement(
       leaf,
       cx.elt("Task", leaf.start, leaf.start + content.length, [