diff --git a/actions/setup/js/markdown_code_region_balancer.cjs b/actions/setup/js/markdown_code_region_balancer.cjs index a732c971fc9..995efbca872 100644 --- a/actions/setup/js/markdown_code_region_balancer.cjs +++ b/actions/setup/js/markdown_code_region_balancer.cjs @@ -104,12 +104,26 @@ function balanceCodeRegions(markdown) { } // Third pass: Match fences, detecting and fixing nested patterns - // Key insight: Find ALL valid closers for each opener. If there are multiple, - // use the LAST one and increase fence length so middle ones become invalid. + // Strategy: + // 1. Process fences in order + // 2. For each opener, find ALL potential closers at the same indentation + // 3. If there are multiple closers, the user intended the LAST one, so escape middle ones + // 4. Skip closers inside already-paired blocks + // 5. Respect indentation: only match fences at the same indentation level const fenceLengthAdjustments = new Map(); // lineIndex -> new length const processed = new Set(); const unclosedFences = []; - const pairedBlocks = []; // Track paired blocks + const pairedBlocks = []; // Track paired blocks with their line ranges + + // Helper function to check if a line is inside any paired block + const isInsideBlock = lineIndex => { + for (const block of pairedBlocks) { + if (lineIndex > block.start && lineIndex < block.end) { + return true; + } + } + return false; + }; let i = 0; while (i < fences.length) { @@ -121,28 +135,17 @@ function balanceCodeRegions(markdown) { const openFence = fences[i]; processed.add(i); - // Look for ALL valid closers - const allMatchingClosers = []; // Track all potential closers + // Find ALL potential closers at same indentation that are NOT inside existing blocks + const potentialClosers = []; + const openIndentLength = openFence.indent.length; for (let j = i + 1; j < fences.length; j++) { if (processed.has(j)) continue; const fence = fences[j]; - // If this fence has a language specifier and matches our char, it's a nested block - if (fence.language !== "" && fence.char === openFence.char) { - // Process this nested block with language - processed.add(j); - - // Find its closer - for (let k = j + 1; k < fences.length; k++) { - if (processed.has(k)) continue; - const nestedCloser = fences[k]; - if (nestedCloser.char === fence.char && nestedCloser.length >= fence.length && nestedCloser.language === "") { - processed.add(k); - break; - } - } + // Skip if this fence is inside a paired block + if (isInsideBlock(fence.lineIndex)) { continue; } @@ -150,13 +153,18 @@ function balanceCodeRegions(markdown) { const canClose = fence.char === openFence.char && fence.length >= openFence.length && fence.language === ""; if (canClose) { - allMatchingClosers.push({ index: j, length: fence.length }); + const fenceIndentLength = fence.indent.length; + + // Only consider fences at the SAME indentation as potential closers + if (fenceIndentLength === openIndentLength) { + potentialClosers.push({ index: j, length: fence.length }); + } } } - if (allMatchingClosers.length > 0) { - // Use the LAST valid closer - const closerIndex = allMatchingClosers[allMatchingClosers.length - 1].index; + if (potentialClosers.length > 0) { + // Use the LAST potential closer (farthest from opener) + const closerIndex = potentialClosers[potentialClosers.length - 1].index; processed.add(closerIndex); pairedBlocks.push({ @@ -166,17 +174,17 @@ function balanceCodeRegions(markdown) { closeIndex: closerIndex, }); - // If there are multiple closers, we have nested fences - if (allMatchingClosers.length > 1) { + // If there are multiple potential closers, we have nested fences that need escaping + if (potentialClosers.length > 1) { // Increase fence length so middle closers can no longer close - const maxLength = Math.max(...allMatchingClosers.map(c => c.length), openFence.length); + const maxLength = Math.max(...potentialClosers.map(c => c.length), openFence.length); const newLength = maxLength + 1; fenceLengthAdjustments.set(fences[i].lineIndex, newLength); fenceLengthAdjustments.set(fences[closerIndex].lineIndex, newLength); - // Mark middle closers as processed - for (let k = 0; k < allMatchingClosers.length - 1; k++) { - processed.add(allMatchingClosers[k].index); + // Mark middle closers as processed (they're now treated as content) + for (let k = 0; k < potentialClosers.length - 1; k++) { + processed.add(potentialClosers[k].index); } } @@ -185,16 +193,8 @@ function balanceCodeRegions(markdown) { } else { // No closer found - check if this fence is inside a paired block const fenceLine = fences[i].lineIndex; - let isInsideBlock = false; - - for (const block of pairedBlocks) { - if (fenceLine > block.start && fenceLine < block.end) { - isInsideBlock = true; - break; - } - } - if (!isInsideBlock) { + if (!isInsideBlock(fenceLine)) { unclosedFences.push(openFence); } diff --git a/actions/setup/js/markdown_code_region_balancer.test.cjs b/actions/setup/js/markdown_code_region_balancer.test.cjs index c6594f63c74..e171997e20b 100644 --- a/actions/setup/js/markdown_code_region_balancer.test.cjs +++ b/actions/setup/js/markdown_code_region_balancer.test.cjs @@ -222,13 +222,9 @@ Example: nested \`\`\` \`\`\``; - const expected = `\`\`\`\`markdown -Example: - \`\`\` - nested - \`\`\` -\`\`\`\``; - expect(balancer.balanceCodeRegions(input)).toBe(expected); + // Indented fences inside a markdown block are treated as content (examples), not active fences + // No escaping needed + expect(balancer.balanceCodeRegions(input)).toBe(input); }); it("should preserve indentation when escaping", () => { @@ -237,12 +233,9 @@ Example: indented nested \`\`\` \`\`\``; - const expected = `\`\`\`\`markdown - \`\`\` - indented nested - \`\`\` -\`\`\`\``; - expect(balancer.balanceCodeRegions(input)).toBe(expected); + // Indented fences inside a markdown block are treated as content (examples), not active fences + // No escaping needed + expect(balancer.balanceCodeRegions(input)).toBe(input); }); }); @@ -389,6 +382,42 @@ More text // No changes expected - the javascript block is separate from the markdown block expect(balancer.balanceCodeRegions(input)).toBe(input); }); + + it("should not modify markdown block containing indented bare fences as examples (issue #11081)", () => { + // This reproduces the issue from GitHub issue #11081 + // A markdown code block containing examples of code blocks with indentation + const input = `**Add to AGENTS.md:** + +\`\`\`markdown +## Safe Outputs Schema Synchronization + +**CRITICAL: When modifying safe output templates or handlers:** + +1. **Update all related files:** + - Source: \`actions/setup/js/handle_*.cjs\` + - Schema: \`pkg/workflow/js/safe_outputs_tools.json\` + +2. **Schema sync checklist:** + \`\`\` + # After modifying any handle_*.cjs file: + cd actions/setup/js + npm test # MUST pass + \`\`\` + +3. **Common pitfalls:** + - ❌ Changing issue titles without updating schema + +4. **Pattern to follow:** + \`\`\` + # Find all related definitions + grep -r "your-new-text" actions/setup/js/ + \`\`\` +\`\`\` + +## Historical Context`; + // No changes expected - the indented bare ``` inside the markdown block are examples + expect(balancer.balanceCodeRegions(input)).toBe(input); + }); }); describe("edge cases", () => {