@meldscience/meld
Version:
pipeable one-shot prompt scripting toolkit
138 lines (115 loc) • 3.92 kB
text/typescript
/**
* markdownImporter.ts
*
* Extracts a section of a markdown file from a given heading to the next heading.
* Ignores headings within fenced code blocks.
*/
import { readFileSync } from 'fs';
export interface MarkdownImportOptions {
filePath: string;
heading?: string;
headingLevelOverride?: string;
headingTextOverride?: string;
}
export interface MarkdownImportResult {
content: string;
headingFound: boolean;
duplicateHeadings: boolean;
}
export function importMarkdownSection(options: MarkdownImportOptions): MarkdownImportResult {
const { filePath, heading, headingLevelOverride, headingTextOverride } = options;
try {
const content = readFileSync(filePath, 'utf-8');
// If no heading is provided, return the entire file
if (!heading) {
return {
content,
headingFound: true,
duplicateHeadings: false
};
}
const lines = content.split('\n');
const result: MarkdownImportResult = {
content: '',
headingFound: false,
duplicateHeadings: false
};
// Track heading locations
const headingLocations: number[] = [];
let inCodeBlock = false;
// Find all matching headings (ignoring those in code blocks)
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Track code blocks
if (line.startsWith('```')) {
inCodeBlock = !inCodeBlock;
continue;
}
// Skip lines in code blocks
if (inCodeBlock) continue;
// Look for headings
if (line.startsWith('#')) {
const match = line.match(/^#+\s+(.*)$/);
if (match && match[1].trim() === heading) {
headingLocations.push(i);
}
}
}
// Handle results
if (headingLocations.length === 0) {
return result;
}
if (headingLocations.length > 1) {
result.headingFound = true;
result.duplicateHeadings = true;
return result;
}
result.headingFound = true;
// Extract content from the heading until the next heading or end of file
const startLine = headingLocations[0];
let endLine = lines.length;
// Find the next heading at the same or higher level
const currentLevel = lines[startLine].match(/^(#+)/)?.[0].length || 1;
inCodeBlock = false;
for (let i = startLine + 1; i < lines.length; i++) {
const line = lines[i].trim();
// Track code blocks
if (line.startsWith('```')) {
inCodeBlock = !inCodeBlock;
continue;
}
// Skip lines in code blocks
if (inCodeBlock) continue;
// Look for next heading at same or higher level
if (line.startsWith('#')) {
const nextLevel = line.match(/^(#+)/)?.[0].length || 1;
// Stop at the next heading of the same or higher level
if (nextLevel <= currentLevel) {
endLine = i;
break;
}
}
}
// Extract the content and apply heading level and text overrides if specified
let extractedContent = lines.slice(startLine, endLine).join('\n');
if (headingLevelOverride || headingTextOverride) {
// Get the original heading line
const headingLine = lines[startLine];
// Get the original heading level markers
const originalLevel = headingLine.match(/^#+/)?.[0] || '#';
// Get the new heading text (either override or original)
const newText = headingTextOverride || heading;
// Get the new level markers (either override or original)
const newLevel = headingLevelOverride || originalLevel;
// Replace the first heading line
extractedContent = extractedContent.replace(headingLine, `${newLevel} ${newText}`);
}
result.content = extractedContent;
return result;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error('File not found');
}
throw error;
}
}