/** * Regex-based YAML frontmatter parser for Claude Code .md files. * Handles YAML frontmatter (--- delimited) and basic YAML parsing. * Zero external dependencies. */ /** * Parse YAML frontmatter from markdown content. * @param {string} content * @returns {{ frontmatter: object | null, body: string, bodyStartLine: number }} */ export function parseFrontmatter(content) { const match = content.match(/^---\r?\n([\s\S]*?)(?:\r?\n)?---(?:\r?\n|$)/); if (!match) { return { frontmatter: null, body: content, bodyStartLine: 1 }; } const raw = match[1]; const bodyStartLine = raw.split('\n').length + 3; // 2 for --- lines + 1-based const body = content.slice(match[0].length); const frontmatter = parseSimpleYaml(raw); return { frontmatter, body, bodyStartLine }; } /** * Parse simple YAML key-value pairs (no nesting beyond arrays). * @param {string} yaml * @returns {object} */ export function parseSimpleYaml(yaml) { const result = {}; const lines = yaml.split('\n'); let currentKey = null; let multiLineValue = ''; let inMultiLine = false; for (const line of lines) { // Skip comments and empty lines if (line.trim().startsWith('#') || line.trim() === '') { if (inMultiLine) multiLineValue += '\n'; continue; } // Key-value pair const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/); if (kvMatch && !inMultiLine) { if (currentKey && multiLineValue) { result[normalizeKey(currentKey)] = multiLineValue.trim(); } currentKey = kvMatch[1]; const value = kvMatch[2].trim(); if (value === '|' || value === '>') { inMultiLine = true; multiLineValue = ''; continue; } result[normalizeKey(currentKey)] = parseValue(value); currentKey = null; continue; } // Multi-line continuation if (inMultiLine) { if (line.match(/^\s+/)) { multiLineValue += (multiLineValue ? '\n' : '') + line.trim(); } else { result[normalizeKey(currentKey)] = multiLineValue.trim(); inMultiLine = false; multiLineValue = ''; // Re-process this line as a new key const reMatch = line.match(/^(\w[\w-]*):\s*(.*)/); if (reMatch) { currentKey = reMatch[1]; result[normalizeKey(currentKey)] = parseValue(reMatch[2].trim()); currentKey = null; } } } } // Flush remaining multi-line if (inMultiLine && currentKey) { result[normalizeKey(currentKey)] = multiLineValue.trim(); } // Normalize arrays for known list fields for (const field of ['allowed_tools', 'tools', 'paths', 'globs']) { if (typeof result[field] === 'string') { result[field] = result[field].split(',').map(s => s.trim()).filter(Boolean); } } return result; } /** * Parse a YAML value string. */ function parseValue(str) { if (str === '' || str === '~' || str === 'null') return null; if (str === 'true') return true; if (str === 'false') return false; if (/^\d+$/.test(str)) return parseInt(str, 10); if (/^\d+\.\d+$/.test(str)) return parseFloat(str); // Inline array: [a, b, c] if (str.startsWith('[') && str.endsWith(']')) { return str.slice(1, -1).split(',').map(s => { const v = s.trim(); return v.replace(/^["']|["']$/g, ''); }).filter(Boolean); } // Quoted string if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) { return str.slice(1, -1); } return str; } /** * Normalize key: hyphens to underscores. */ function normalizeKey(key) { return key.replace(/-/g, '_'); } /** * Parse a JSON file content. Returns null on error. * @param {string} content * @returns {object | null} */ export function parseJson(content) { try { return JSON.parse(content); } catch { return null; } } /** * Find @import references in CLAUDE.md content. * @param {string} content * @returns {{ path: string, line: number }[]} */ export function findImports(content) { const imports = []; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const match = lines[i].match(/^@(.+)$/); if (match) { imports.push({ path: match[1].trim(), line: i + 1 }); } } return imports; } /** * Extract markdown sections (## headings) from content. * @param {string} content * @returns {{ heading: string, level: number, line: number }[]} */ export function extractSections(content) { const sections = []; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const match = lines[i].match(/^(#{1,6})\s+(.+)/); if (match) { sections.push({ heading: match[2].trim(), level: match[1].length, line: i + 1, }); } } return sections; }