fix(tools): make fence-language fixer aware of outer fences (avoid touching nested triple backticks) and add safety for unmatched fences

This commit is contained in:
Keimpe de Jong 2025-10-29 05:05:18 +00:00
parent 15c37fd120
commit 9dfe22151c
1 changed files with 82 additions and 39 deletions

View File

@ -89,8 +89,14 @@ function fixFile(filePath) {
const fixes = []; const fixes = [];
let modified = false; let modified = false;
let inFence = false;
let fenceStart = -1; // Track any outer fence (of any backtick length >=3) to avoid touching nested content
const fenceStack = [];
// State for a target triple-backtick fence without language that we intend to fix
let fixing = false;
let fixFenceStart = -1;
let fixOpenIndent = '';
let fenceContent = []; let fenceContent = [];
const newLines = []; const newLines = [];
@ -98,54 +104,91 @@ function fixFile(filePath) {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
// Detect fence start // If we are currently fixing a fence (collecting content until closing ```)
if (/^\s*```\s*$/.test(line)) { if (fixing) {
if (!inFence) { const closeMatch = line.match(/^(\s*)(`{3})(\s*)$/);
// Opening fence without language if (closeMatch) {
inFence = true; // Closing the target fence
fenceStart = i;
fenceContent = [];
// We'll hold this line and potentially modify it
continue;
} else {
// Closing fence
const language = detectLanguage(fenceContent.join('\n')); const language = detectLanguage(fenceContent.join('\n'));
const fixedOpenLine = `${fixOpenIndent}\`\`\`${language}`;
// Add the opening fence with detected language
const indent = lines[fenceStart].match(/^(\s*)/)[1];
const fixedOpenLine = `${indent}\`\`\`${language}`;
newLines.push(fixedOpenLine); newLines.push(fixedOpenLine);
// Add the content
newLines.push(...fenceContent); newLines.push(...fenceContent);
// Add the closing fence
newLines.push(line); newLines.push(line);
fixes.push({ fixes.push({
line: fenceStart + 1, line: fixFenceStart + 1,
original: lines[fenceStart], original: '```',
fixed: fixedOpenLine, fixed: fixedOpenLine,
detectedLanguage: language, detectedLanguage: language,
contentPreview: fenceContent.slice(0, 2).join('\n').substring(0, 60) + '...' contentPreview: fenceContent.slice(0, 2).join('\n').substring(0, 60) + '...'
}); });
modified = true; modified = true;
inFence = false; fixing = false;
fenceStart = -1; fixFenceStart = -1;
fixOpenIndent = '';
fenceContent = []; fenceContent = [];
continue; continue;
} else {
fenceContent.push(line);
continue;
} }
} }
if (inFence) { // Not currently fixing; detect any fence line (opening or closing)
// Collecting fence content const fenceLineMatch = line.match(/^(\s*)(`{3,})(.*)$/);
fenceContent.push(line); if (fenceLineMatch) {
} else { const indent = fenceLineMatch[1] || '';
// Regular line outside fence const ticks = fenceLineMatch[2] || '';
const rest = (fenceLineMatch[3] || '').trim();
const hasLanguage = rest.length > 0; // simplistic but effective for our cases
// Determine if this is a closing fence for the current outer fence
if (fenceStack.length > 0 && fenceStack[fenceStack.length - 1].ticks === ticks) {
// Closing existing fence scope
fenceStack.pop();
newLines.push(line);
continue;
}
// If inside any outer fence, don't attempt to fix nested fences
if (fenceStack.length > 0) {
newLines.push(line);
// Start a nested fence scope if this appears to be an opening fence
fenceStack.push({ ticks });
continue;
}
// Outside any fence
if (ticks === '```' && !hasLanguage) {
// Target: opening triple backtick without language; begin fixing mode
fixing = true;
fixFenceStart = i;
fixOpenIndent = indent;
fenceContent = [];
// Do not push the original opening line; we'll emit the fixed one at close
continue;
}
// Any other fence (with language or more backticks): treat as an outer fence start
fenceStack.push({ ticks });
newLines.push(line); newLines.push(line);
continue;
} }
// Regular non-fence line
newLines.push(line);
}
// If we ended while "fixing" and never saw a closing fence, abort changes for safety
if (fixing) {
return {
filePath,
fixes: [],
modified: false,
newContent: content
};
} }
return { return {