diff --git a/tools/loop-sync/dist/sync.d.ts b/tools/loop-sync/dist/sync.d.ts index eef59397..3b63ff42 100644 --- a/tools/loop-sync/dist/sync.d.ts +++ b/tools/loop-sync/dist/sync.d.ts @@ -29,6 +29,13 @@ export interface SyncOptions { help?: boolean; json?: boolean; } +/** + * Extract frontmatter from markdown files + */ +export declare function extractFrontmatter(content: string): { + frontmatter: Record; + body: string; +}; /** * Main sync function */ diff --git a/tools/loop-sync/dist/sync.js b/tools/loop-sync/dist/sync.js index 3389ab71..d0d33241 100644 --- a/tools/loop-sync/dist/sync.js +++ b/tools/loop-sync/dist/sync.js @@ -36,15 +36,15 @@ async function readFileContent(filePath) { /** * Extract frontmatter from markdown files */ -function extractFrontmatter(content) { +export function extractFrontmatter(content) { const frontmatter = {}; let body = content; - if (content.startsWith('---')) { - const endIndex = content.indexOf('---', 3); + if (content.startsWith('---\n') || content.startsWith('---\r\n')) { + const endIndex = content.indexOf('\n---', 3); if (endIndex !== -1) { const fmContent = content.slice(3, endIndex); - body = content.slice(endIndex + 3); - for (const line of fmContent.split('\n')) { + body = content.slice(endIndex + 4); + for (const line of fmContent.split(/\r?\n/)) { const colonIndex = line.indexOf(':'); if (colonIndex !== -1) { const key = line.slice(0, colonIndex).trim(); diff --git a/tools/loop-sync/src/sync.ts b/tools/loop-sync/src/sync.ts index 5ee6de13..653192ac 100644 --- a/tools/loop-sync/src/sync.ts +++ b/tools/loop-sync/src/sync.ts @@ -63,7 +63,7 @@ async function readFileContent(filePath: string): Promise { /** * Extract frontmatter from markdown files */ -function extractFrontmatter(content: string): { frontmatter: Record; body: string } { +export function extractFrontmatter(content: string): { frontmatter: Record; body: string } { const frontmatter: Record = {}; let body = content; diff --git a/tools/loop-sync/test/sync.test.mjs b/tools/loop-sync/test/sync.test.mjs index 8a7a139f..ecd80102 100644 --- a/tools/loop-sync/test/sync.test.mjs +++ b/tools/loop-sync/test/sync.test.mjs @@ -4,7 +4,7 @@ import path from 'node:path'; import { mkdir, writeFile, rm } from 'node:fs/promises'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { runSync, formatReport } from '../dist/sync.js'; +import { runSync, formatReport, extractFrontmatter } from '../dist/sync.js'; const testDir = path.join(process.cwd(), '.test-tmp'); const exec = promisify(execFile); @@ -83,6 +83,31 @@ describe('runSync', () => { }); }); +describe('extractFrontmatter', () => { + test('parses LF frontmatter', () => { + const { frontmatter, body } = extractFrontmatter('---\nname: demo\nversion: 1.0.0\n---\nBody text\n'); + + assert.deepEqual(frontmatter, { name: 'demo', version: '1.0.0' }); + assert.match(body, /Body text/); + }); + + test('parses CRLF frontmatter without trailing \\r in keys or values', () => { + const { frontmatter, body } = extractFrontmatter('---\r\nname: demo\r\nversion: 1.0.0\r\n---\r\nBody text\r\n'); + + assert.deepEqual(frontmatter, { name: 'demo', version: '1.0.0' }); + assert.ok(Object.keys(frontmatter).every((key) => !key.endsWith('\r'))); + assert.match(body, /Body text/); + }); + + test('rejects opening fence without newline', () => { + const content = '---hello\nname: demo\n---\nBody text\n'; + const { frontmatter, body } = extractFrontmatter(content); + + assert.deepEqual(frontmatter, {}); + assert.equal(body, content); + }); +}); + describe('formatReport', () => { test('formats healthy report', () => { const formatted = formatReport({