import type { FrontmatterValue } from '../components/Inspector' import { canonicalFrontmatterWriteKey, frontmatterKeysMatch } from '../utils/systemMetadata' type VaultPath = string type MarkdownContent = string type FrontmatterKey = string type YamlKey = string type YamlValue = string type YamlLine = string type ReplacementLine = string | null type LineEnding = '\n' | '\r\n' interface ParsedFrontmatter { fm: MarkdownContent rest: MarkdownContent lineEnding: LineEnding } function canonicalWriteKey(key: FrontmatterKey): FrontmatterKey { return canonicalFrontmatterWriteKey(key) } function formatYamlValue(value: FrontmatterValue): YamlValue { if (Array.isArray(value)) return '\n' + value.map(v => ` - "${v}"`).join('\n') if (typeof value === 'boolean') return value ? 'true' : 'false' if (value === null) return 'null' return String(value) } function formatYamlKey(key: FrontmatterKey): YamlKey { return key.includes(' ') ? `"${key}"` : key } function frontmatterOpening(content: MarkdownContent): { bodyStart: number; lineEnding: LineEnding } | null { if (content.startsWith('---\r\n')) return { bodyStart: 5, lineEnding: '\r\n' } if (content.startsWith('---\n')) return { bodyStart: 4, lineEnding: '\n' } return null } function parseFrontmatter(content: MarkdownContent): ParsedFrontmatter | null { const opening = frontmatterOpening(content) if (!opening) return null const afterOpening = content.slice(opening.bodyStart) if (afterOpening.startsWith('---')) { return { fm: '', rest: afterOpening.slice(3), lineEnding: opening.lineEnding } } const closeMarker = `${opening.lineEnding}---` const fmEnd = afterOpening.indexOf(closeMarker) if (fmEnd === -1) return null return { fm: afterOpening.slice(0, fmEnd), rest: afterOpening.slice(fmEnd + closeMarker.length), lineEnding: opening.lineEnding, } } function formatKeyValue(yamlKey: YamlKey, yamlValue: YamlValue, isArray: boolean): YamlLine { return isArray ? `${yamlKey}:${yamlValue}` : `${yamlKey}: ${yamlValue}` } function quotedYamlKey(raw: YamlLine, quote: '"' | "'"): FrontmatterKey | null { const rest = raw.slice(1) const end = rest.indexOf(quote) if (end === -1) return null return rest.slice(end + 1).trimStart().startsWith(':') ? rest.slice(0, end) : null } function isIndentedYamlLine(line: YamlLine): boolean { return line.startsWith(' ') || line.startsWith('\t') } function parseBareYamlKey(trimmed: YamlLine): FrontmatterKey | null { if (!trimmed.includes(':')) return null const [key] = trimmed.split(':', 1) return key.trim() || null } function parseTrimmedYamlKey(trimmed: YamlLine): FrontmatterKey | null { if (trimmed.startsWith('"')) return quotedYamlKey(trimmed, '"') if (trimmed.startsWith("'")) return quotedYamlKey(trimmed, "'") return parseBareYamlKey(trimmed) } function parseYamlKey(line: YamlLine): FrontmatterKey | null { if (isIndentedYamlLine(line)) return null const trimmed = line.trimStart() return parseTrimmedYamlKey(trimmed) } function lineMatchesKey(line: YamlLine, key: FrontmatterKey): boolean { const yamlKey = parseYamlKey(line) return yamlKey !== null && frontmatterKeysMatch(yamlKey, key) } function isArrayItemLine(line: YamlLine): boolean { return line.startsWith(' - ') } function skipArrayItemLines(lines: YamlLine[], start: number): number { let next = start while (next < lines.length && isArrayItemLine(lines.at(next) ?? '')) next++ return next } function appendReplacement(lines: YamlLine[], replacement: ReplacementLine): void { if (replacement !== null) lines.push(replacement) } function hasMatchingKey(lines: YamlLine[], key: FrontmatterKey): boolean { return lines.some(line => lineMatchesKey(line, key)) } function frontmatterLines(fm: MarkdownContent, lineEnding: LineEnding): YamlLine[] { return fm === '' ? [] : fm.split(lineEnding) } function formatFrontmatterBlock(lines: YamlLine[], lineEnding: LineEnding, rest: MarkdownContent): MarkdownContent { return `---${lineEnding}${lines.join(lineEnding)}${lineEnding}---${rest}` } function processKeyInLines(lines: YamlLine[], key: FrontmatterKey, replacement: ReplacementLine): YamlLine[] { const newLines: YamlLine[] = [] let i = 0 while (i < lines.length) { const line = lines.at(i) ?? '' if (lineMatchesKey(line, key)) { i = skipArrayItemLines(lines, i + 1) appendReplacement(newLines, replacement) continue } newLines.push(line) i++ } return newLines } export function updateMockFrontmatter(path: VaultPath, key: FrontmatterKey, value: FrontmatterValue): MarkdownContent { const content = (window.__mockContent ? Reflect.get(window.__mockContent, path) as string | undefined : undefined) || '' const writeKey = canonicalWriteKey(key) const yamlKey = formatYamlKey(writeKey) const yamlValue = formatYamlValue(value) const isArray = Array.isArray(value) const parsed = parseFrontmatter(content) if (!parsed) { return `---\n${formatKeyValue(yamlKey, yamlValue, isArray)}\n---\n${content}` } const { fm, rest, lineEnding } = parsed const lines = frontmatterLines(fm, lineEnding) const replacement = formatKeyValue(yamlKey, yamlValue, isArray) if (hasMatchingKey(lines, key)) { const newLines = processKeyInLines(lines, key, replacement) return formatFrontmatterBlock(newLines, lineEnding, rest) } return formatFrontmatterBlock([...lines, replacement], lineEnding, rest) } export function deleteMockFrontmatterProperty(path: VaultPath, key: FrontmatterKey): MarkdownContent { const content = (window.__mockContent ? Reflect.get(window.__mockContent, path) as string | undefined : undefined) || '' const parsed = parseFrontmatter(content) if (!parsed) return content const { fm, rest, lineEnding } = parsed const newLines = processKeyInLines(frontmatterLines(fm, lineEnding), key, null) return formatFrontmatterBlock(newLines, lineEnding, rest) }