#!/usr/bin/env node /** * Indent nested Markdown code fences (``` ... ```) that appear inside other fenced code blocks * to ensure proper rendering on GitHub. Only modifies .md/.prompt.md/.instructions.md files * under the specified folders (prompts/, instructions/, collections/). * * Strategy: * - Parse each file line-by-line * - Detect outer fenced code blocks (up to 3 leading spaces + backticks >= 3) * - Within an outer block, find any inner lines that also start with a fence marker (```...) * that are not the true closing line of the outer block (same tick length and no language info), * and treat them as the start of a nested block * - Indent the inner block from its opening fence line through its next fence line (closing) * by prefixing each of those lines with four spaces * - Repeat for multiple nested "inner blocks" within the same outer block * * Notes: * - We only consider backtick fences (```). Tilde fences (~~~) are uncommon in repo, and excluded * - We preserve existing content and whitespace beyond the added indentation for nested fences */ const fs = require('fs'); const path = require('path'); const ROOT = process.cwd(); const TARGET_DIRS = ['prompts', 'instructions', 'collections']; const VALID_EXTS = new Set(['.md', '.prompt.md', '.instructions.md']); function walk(dir) { const results = []; const stack = [dir]; while (stack.length) { const current = stack.pop(); let entries = []; try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const ent of entries) { const full = path.join(current, ent.name); if (ent.isDirectory()) { stack.push(full); } else if (ent.isFile()) { const ext = getEffectiveExt(ent.name); if (VALID_EXTS.has(ext)) { results.push(full); } } } } return results; } function getEffectiveExt(filename) { if (filename.endsWith('.prompt.md')) return '.prompt.md'; if (filename.endsWith('.instructions.md')) return '.instructions.md'; return path.extname(filename).toLowerCase(); } // Regex helpers // up to 3 spaces + ``` + anything const fenceLineRe = /^(? {0,3})(?`{3,})(?.*)$/; function processFile(filePath) { const original = fs.readFileSync(filePath, 'utf8'); const lines = original.split(/\r?\n/); let inOuter = false; let outerIndent = ''; let outerTicksLen = 0; let i = 0; let changed = false; while (i < lines.length) { const line = lines[i]; const m = line.match(fenceLineRe); if (!inOuter) { // Look for start of an outer fence if (m) { const rest = m.groups.rest || ''; // Check if this is a single-line fence (opening and closing on same line) const closingMatch = rest.match(new RegExp(`\`{${m.groups.ticks.length},}\\s*$`)); if (closingMatch) { // Single-line code block, skip it i++; continue; } inOuter = true; outerIndent = m.groups.indent || ''; outerTicksLen = m.groups.ticks.length; } i++; continue; } // We're inside an outer fence if (m) { // Is this the true closing fence for the current outer block? const indentLen = (m.groups.indent || '').length; const ticksLen = m.groups.ticks.length; const restTrim = (m.groups.rest || '').trim(); const isOuterCloser = indentLen <= outerIndent.length && ticksLen === outerTicksLen && restTrim === ''; if (isOuterCloser) { // End of outer block inOuter = false; outerIndent = ''; outerTicksLen = 0; i++; continue; } // Potential nested fence - check if it has language info (opening fence) // If rest is empty or just whitespace, it's likely a sloppy closing fence, not a nested opening const hasLangInfo = restTrim.length > 0; if (!hasLangInfo) { // Sloppy closing fence without nested content - don't indent, just skip i++; continue; } // This is an actual nested opening fence; indent til matching inner fence (inclusive) changed = true; const innerTicksLen = ticksLen; lines[i] = ' ' + lines[i]; i++; // Indent lines until we find a fence line with the same tick length (closing the inner block) while (i < lines.length) { const innerLine = lines[i]; const m2 = innerLine.match(fenceLineRe); lines[i] = ' ' + innerLine; i++; // we've indented the closing inner fence; stop if (m2 && m2.groups.ticks.length === innerTicksLen) break; } continue; } // Regular line inside outer block i++; } if (changed) { fs.writeFileSync(filePath, lines.join('\n')); return true; } return false; } function main() { const roots = TARGET_DIRS.map(d => path.join(ROOT, d)); let files = []; for (const d of roots) { if (fs.existsSync(d) && fs.statSync(d).isDirectory()) { files = files.concat(walk(d)); } } let modified = 0; for (const f of files) { try { if (processFile(f)) modified++; } catch (err) { // Log and continue console.error(`Error processing ${f}:`, err.message); } } console.log(`Processed ${files.length} files. Modified ${modified} file(s).`); } if (require.main === module) { main(); }