UNPKG

@rettangoli/sites

Version:

Generate static sites using Markdown and YAML. Straightforward, zero-complexity. Complete toolkit for landing pages, blogs, documentation, admin dashboards, and more.git remote add origin git@github.com:yuusoft-org/sitic.git

351 lines (294 loc) 13.1 kB
import { convertToHtml } from 'yahtml'; import { parseAndRender } from 'jempl'; import path from 'path'; import yaml from 'js-yaml'; import MarkdownIt from 'markdown-it'; export function createSiteBuilder({ fs, rootDir = '.', mdRender, functions = {} }) { return function build() { // Use provided mdRender or default to standard markdown-it const md = mdRender || MarkdownIt(); // Read all partials and create a JSON object const partialsDir = path.join(rootDir, 'partials'); const partials = {}; if (fs.existsSync(partialsDir)) { const files = fs.readdirSync(partialsDir); files.forEach(file => { const filePath = path.join(partialsDir, file); const fileContent = fs.readFileSync(filePath, 'utf8'); const nameWithoutExt = path.basename(file, path.extname(file)); // Convert partial content from YAML string to JSON partials[nameWithoutExt] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA }); }); } // Read all data files and create a JSON object const dataDir = path.join(rootDir, 'data'); const globalData = {}; if (fs.existsSync(dataDir)) { const files = fs.readdirSync(dataDir); files.forEach(file => { if (file.endsWith('.yaml') || file.endsWith('.yml')) { const filePath = path.join(dataDir, file); const fileContent = fs.readFileSync(filePath, 'utf8'); const nameWithoutExt = path.basename(file, path.extname(file)); // Load YAML content and store under filename key globalData[nameWithoutExt] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA }); } }); } // Read all templates and create a JSON object const templatesDir = path.join(rootDir, 'templates'); const templates = {}; function readTemplatesRecursively(dir, basePath = '') { if (!fs.existsSync(dir)) return; const items = fs.readdirSync(dir, { withFileTypes: true }); items.forEach(item => { const itemPath = path.join(dir, item.name); if (item.isDirectory()) { // Recursively read subdirectories const newBasePath = basePath ? `${basePath}/${item.name}` : item.name; readTemplatesRecursively(itemPath, newBasePath); } else if (item.isFile() && item.name.endsWith('.yaml')) { // Read and convert YAML file const fileContent = fs.readFileSync(itemPath, 'utf8'); const nameWithoutExt = path.basename(item.name, '.yaml'); const templateKey = basePath ? `${basePath}/${nameWithoutExt}` : nameWithoutExt; templates[templateKey] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA }); } }); } readTemplatesRecursively(templatesDir); // Function to extract frontmatter from a page file function extractFrontmatter(pagePath) { const pageFileContent = fs.readFileSync(pagePath, 'utf8'); const lines = pageFileContent.split('\n'); let frontmatterStart = -1; let frontmatterEnd = -1; let frontmatterCount = 0; for (let i = 0; i < lines.length; i++) { if (lines[i].trim() === '---') { frontmatterCount++; if (frontmatterCount === 1) { frontmatterStart = i + 1; } else if (frontmatterCount === 2) { frontmatterEnd = i; break; } } } let frontmatter = {}; if (frontmatterStart > 0 && frontmatterEnd > frontmatterStart) { const frontmatterContent = lines.slice(frontmatterStart, frontmatterEnd).join('\n'); frontmatter = yaml.load(frontmatterContent, { schema: yaml.JSON_SCHEMA }) || {}; } return frontmatter; } // Function to scan all pages and build collections function buildCollections() { const collections = {}; const pagesDir = path.join(rootDir, 'pages'); function scanPages(dir, basePath = '') { const fullDir = path.join(pagesDir, basePath); if (!fs.existsSync(fullDir)) return; const items = fs.readdirSync(fullDir, { withFileTypes: true }); for (const item of items) { const itemPath = path.join(fullDir, item.name); const relativePath = basePath ? path.join(basePath, item.name) : item.name; if (item.isDirectory()) { // Recursively scan subdirectories scanPages(dir, relativePath); } else if (item.isFile() && (item.name.endsWith('.yaml') || item.name.endsWith('.md'))) { // Extract frontmatter const frontmatter = extractFrontmatter(itemPath); // Calculate URL const outputFileName = item.name.replace(/\.(yaml|md)$/, '.html'); const outputRelativePath = basePath ? path.join(basePath, outputFileName) : outputFileName; const url = '/' + outputRelativePath.replace(/\\/g, '/'); // Process tags if (frontmatter.tags) { // Normalize tags to array const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [frontmatter.tags]; // Add to collections tags.forEach(tag => { if (typeof tag === 'string' && tag.trim()) { const trimmedTag = tag.trim(); if (!collections[trimmedTag]) { collections[trimmedTag] = []; } collections[trimmedTag].push({ data: frontmatter, url: url }); } }); } } } } scanPages(''); return collections; } // Build collections in first pass console.log('Building collections...'); const collections = buildCollections(); // Function to process a single page file function processPage(pagePath, outputRelativePath, isMarkdown = false) { console.log(`Processing ${pagePath}...`); // Read page content const pageFileContent = fs.readFileSync(pagePath, 'utf8'); // Extract frontmatter and content const lines = pageFileContent.split('\n'); let frontmatterStart = -1; let frontmatterEnd = -1; let frontmatterCount = 0; for (let i = 0; i < lines.length; i++) { if (lines[i].trim() === '---') { frontmatterCount++; if (frontmatterCount === 1) { frontmatterStart = i + 1; } else if (frontmatterCount === 2) { frontmatterEnd = i; break; } } } // Store frontmatter let frontmatter = {}; if (frontmatterStart > 0 && frontmatterEnd > frontmatterStart) { const frontmatterContent = lines.slice(frontmatterStart, frontmatterEnd).join('\n'); frontmatter = yaml.load(frontmatterContent, { schema: yaml.JSON_SCHEMA }) || {}; } // Get content after frontmatter const contentStart = frontmatterEnd + 1; const rawContent = lines.slice(contentStart).join('\n').trim(); // Merge global data with frontmatter and collections for the page context const pageData = { ...globalData, ...frontmatter, collections }; let processedPageContent; if (isMarkdown) { // Process markdown content with MarkdownIt const htmlContent = md.render(rawContent); // For markdown, store as raw HTML that will be inserted directly processedPageContent = { __html: htmlContent }; } else { // Convert YAML content to JSON const pageContent = yaml.load(rawContent, { schema: yaml.JSON_SCHEMA }); // Process the page content to resolve any $partial references with page data processedPageContent = parseAndRender(pageContent, pageData, { partials, functions }); } // Find the template specified in frontmatter let templateToUse = null; if (frontmatter.template) { // Look up template by exact path templateToUse = templates[frontmatter.template]; if (!templateToUse) { throw new Error(`Template "${frontmatter.template}" not found in ${pagePath}. Available templates: ${Object.keys(templates).join(', ')}`); } } // Use the template with jempl to render the processed page content let htmlString; if (isMarkdown) { if (templateToUse) { // For markdown with template, use a placeholder and replace after const placeholder = '___MARKDOWN_CONTENT_PLACEHOLDER___'; const templateData = { ...pageData, content: placeholder, collections }; const templateResult = parseAndRender(templateToUse, templateData, { partials, functions }); htmlString = convertToHtml(templateResult); // Replace the placeholder with actual HTML content htmlString = htmlString.replace(placeholder, processedPageContent.__html); } else { // Markdown without template - use HTML directly htmlString = processedPageContent.__html; } } else { // YAML content const templateData = { ...pageData, content: processedPageContent, collections }; const result = templateToUse ? parseAndRender(templateToUse, templateData, { partials, functions }) : processedPageContent; // Ensure result is an array for convertToHtml const resultArray = Array.isArray(result) ? result : [result]; htmlString = convertToHtml(resultArray); } // Create output directory if it doesn't exist const outputPath = path.join(rootDir, '_site', outputRelativePath); const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write HTML to output file fs.writeFileSync(outputPath, htmlString); console.log(` -> Written to ${outputPath}`); } // Process all YAML and Markdown files in pages directory recursively function processAllPages(dir, basePath = '') { const pagesDir = path.join(rootDir, 'pages'); const fullDir = path.join(pagesDir, basePath); if (!fs.existsSync(fullDir)) return; const items = fs.readdirSync(fullDir, { withFileTypes: true }); for (const item of items) { const itemPath = path.join(fullDir, item.name); const relativePath = basePath ? path.join(basePath, item.name) : item.name; if (item.isDirectory()) { // Recursively process subdirectories processAllPages(dir, relativePath); } else if (item.isFile()) { if (item.name.endsWith('.yaml')) { // Process YAML file const outputFileName = item.name.replace('.yaml', '.html'); const outputRelativePath = basePath ? path.join(basePath, outputFileName) : outputFileName; processPage(itemPath, outputRelativePath, false); } else if (item.name.endsWith('.md')) { // Process Markdown file const outputFileName = item.name.replace('.md', '.html'); const outputRelativePath = basePath ? path.join(basePath, outputFileName) : outputFileName; processPage(itemPath, outputRelativePath, true); } // Ignore other file types } } } // Function to copy static files recursively function copyStaticFiles() { const staticDir = path.join(rootDir, 'static'); const outputDir = path.join(rootDir, '_site'); if (!fs.existsSync(staticDir)) { return; } // Ensure output directory exists if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } function copyRecursive(src, dest) { const stats = fs.statSync(src); if (stats.isDirectory()) { // Create directory if it doesn't exist if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } // Copy all items in directory const items = fs.readdirSync(src); items.forEach(item => { copyRecursive(path.join(src, item), path.join(dest, item)); }); } else if (stats.isFile()) { // Copy file fs.copyFileSync(src, dest); console.log(` -> Copied ${src} to ${dest}`); } } console.log('Copying static files...'); const items = fs.readdirSync(staticDir); items.forEach(item => { const srcPath = path.join(staticDir, item); const destPath = path.join(outputDir, item); copyRecursive(srcPath, destPath); }); } // Start build process console.log('Starting build process...'); // Copy static files first (they can be overwritten by pages) copyStaticFiles(); // Process all pages (can overwrite static files) processAllPages(''); console.log('Build complete!'); }; }