UNPKG

@emanuelsan/mosaic-js

Version:

Composable Markdown-based AI instruction engine for Node.js

111 lines 5.37 kB
import matter from "gray-matter"; import { Effect, pipe, Console } from "effect"; import { getTemplateContent } from "./getTemplateContent"; import { normalizeToRelativeSelector } from "./normalizeToRelativeSelector"; // Step 1: Get the file content const getContentRelative = (templateSelector) => getTemplateContent({ templateSelector, type: "relative" }); // Step 2: Parse frontmatter const parseFrontmatter = (templateContent) => Effect.gen(function* () { const parsed = matter(templateContent); const frontmatter = Object.keys(parsed.data).length > 0 ? parsed.data : null; const content = parsed.content; return { frontmatter, content }; }); // Step 3: Extract variables const extractVariables = ({ content, ...rest }) => Effect.sync(() => { const variableRegex = /\{\{\s*\$([a-zA-Z0-9_\-]+)\s*\}\}/g; const variables = []; let match; while ((match = variableRegex.exec(content)) !== null) { variables.push(match[1]); // match[1] is variable name without $ } return { ...rest, content, variables }; }); // Core reference extraction and normalization logic export const extractReferencesFromContent = (content, options = {}) => Effect.gen(function* () { const { currentPath, ancestors = [], removeLoopedReferences = false, normalizeInContent = false, } = options; const referenceRegex = /\{\{\s*([^\}]+)\s*\}\}/g; let match; const replacements = []; const normalizedReferences = []; const referencesToRemove = []; let updatedContent = content; // Find and normalize all non-variable references while ((match = referenceRegex.exec(content)) !== null) { const ref = match[1].trim(); // Skip variable references (those starting with $) if (!/^\$[a-zA-Z0-9_\-]+$/.test(ref)) { const normalized = yield* normalizeToRelativeSelector(ref); const normalizedRef = normalized ?? ref; // Check for self-references and ancestor loops if loop detection is enabled if (removeLoopedReferences && currentPath) { if (normalizedRef === currentPath) { yield* Console.warn(`[LoopDetectedError] Self-reference detected in content at ${currentPath}`); yield* Console.warn(`Removing self-reference: ${ref}`); referencesToRemove.push(ref); continue; } else if (ancestors.includes(normalizedRef)) { yield* Console.warn(`[LoopDetectedError] Ancestor loop detected in content at ${currentPath} with reference: ${normalizedRef}`); yield* Console.warn(`Removing looped reference: ${ref}`); referencesToRemove.push(ref); continue; } } normalizedReferences.push(normalizedRef); // Add to replacements if we need to normalize in content if (normalizeInContent) { replacements.push({ start: match.index, end: referenceRegex.lastIndex, replacement: `{{ ${normalizedRef} }}`, }); } } } // Remove any self-references or looped references from content if (removeLoopedReferences && referencesToRemove.length > 0) { const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); for (const refToRemove of referencesToRemove) { const regex = new RegExp(`\\{\\{\\s*${escapeRegExp(refToRemove)}\\s*\\}\\}`, "g"); updatedContent = updatedContent.replace(regex, ""); } } // Replace all references in content with normalized versions (from last to first to not mess up indices) if (normalizeInContent && replacements.length > 0) { let contentArr = updatedContent.split(""); for (let i = replacements.length - 1; i >= 0; i--) { const { start, end, replacement } = replacements[i]; contentArr.splice(start, end - start, replacement); } updatedContent = contentArr.join(""); } return { references: [...new Set(normalizedReferences)], // Remove duplicates content: updatedContent, }; }); // Step 4: Extract and normalize references const extractAndNormalizeReferences = ({ content, ...rest }) => Effect.gen(function* () { const { references, content: newContent } = yield* extractReferencesFromContent(content, { normalizeInContent: true, }); return { ...rest, content: newContent, references, }; }); // Main Exportable Program export const parseMarkdown = (templateSelector) => pipe(getContentRelative(templateSelector), Effect.flatMap((content) => Effect.if(content !== null, { onTrue: () => pipe(Effect.succeed(content), // Type assertion since we know content is not null; This might be solved by using branded types (unsure) Effect.flatMap(parseFrontmatter), Effect.flatMap(extractVariables), Effect.flatMap(extractAndNormalizeReferences), Effect.map((templateNode) => ({ ...templateNode, path: templateSelector }))), onFalse: () => Effect.succeed({ path: templateSelector, frontmatter: null, content: '', variables: [], references: [], }), }))); //# sourceMappingURL=parseMarkdownTemplate.js.map