@emanuelsan/mosaic-js
Version:
Composable Markdown-based AI instruction engine for Node.js
111 lines • 5.37 kB
JavaScript
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