#!/usr/bin/env node /** * Generate Card Presentation Sections from Markdown Documents * * Parses markdown files and creates structured sections for card-based UI presentation. * Handles H2/H3 headers, converts to HTML, generates excerpts, estimates reading time. * * Usage: node scripts/generate-card-sections.js [--update-db] * * Example: node scripts/generate-card-sections.js introduction-to-the-tractatus-framework.md --update-db */ // Load environment variables require('dotenv').config(); const fs = require('fs'); const path = require('path'); const { marked } = require('marked'); // Configuration const WORDS_PER_MINUTE = 200; // Average reading speed /** * Extract sections from markdown content * @param {string} markdown - Raw markdown content * @returns {Array} Array of section objects */ function extractSections(markdown) { const lines = markdown.split('\n'); const sections = []; let currentSection = null; let contentBuffer = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Match H2 headers (## Title) const h2Match = line.match(/^## (.+)$/); if (h2Match) { // Save previous section if exists if (currentSection) { currentSection.content_md = contentBuffer.join('\n').trim(); sections.push(currentSection); } // Start new section currentSection = { title: h2Match[1].trim(), content_md: '', subsections: [] }; contentBuffer = []; continue; } // Collect content for current section if (currentSection) { contentBuffer.push(line); } } // Save final section if (currentSection) { currentSection.content_md = contentBuffer.join('\n').trim(); sections.push(currentSection); } return sections; } /** * Generate excerpt from markdown content * @param {string} markdown - Markdown content * @param {number} maxLength - Maximum excerpt length * @returns {string} Excerpt text */ function generateExcerpt(markdown, maxLength = 150) { // Remove markdown formatting let text = markdown .replace(/^#+\s+/gm, '') // Remove headers .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold .replace(/\*(.+?)\*/g, '$1') // Remove italic .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links .replace(/`(.+?)`/g, '$1') // Remove inline code .replace(/^[-*+]\s+/gm, '') // Remove list markers .replace(/^\d+\.\s+/gm, '') // Remove numbered lists .replace(/\n{2,}/g, ' ') // Collapse multiple newlines .trim(); // Truncate to maxLength if (text.length > maxLength) { text = text.substring(0, maxLength).trim(); // Find last complete sentence const lastPeriod = text.lastIndexOf('.'); if (lastPeriod > maxLength * 0.7) { text = text.substring(0, lastPeriod + 1); } else { text += '...'; } } return text; } /** * Estimate reading time based on word count * @param {string} text - Text content * @returns {number} Reading time in minutes */ function estimateReadingTime(text) { const wordCount = text.split(/\s+/).length; const minutes = Math.ceil(wordCount / WORDS_PER_MINUTE); return Math.max(1, minutes); // Minimum 1 minute } /** * Classify section category based on content analysis * @param {string} title - Section title * @param {string} content - Section content * @returns {string} Category (conceptual|practical|technical|reference|critical) */ function classifySection(title, content) { const titleLower = title.toLowerCase(); const contentLower = content.toLowerCase(); // Critical: Security, limitations, failures, warnings if ( titleLower.includes('limitation') || titleLower.includes('failure') || titleLower.includes('warning') || titleLower.includes('security') || titleLower.includes('risk') || content.match(/āš ļø|critical|warning|caution|danger/gi) ) { return 'critical'; } // Reference: Glossaries, definitions, specifications if ( titleLower.includes('glossary') || titleLower.includes('reference') || titleLower.includes('contact') || titleLower.includes('license') || titleLower.includes('getting started') ) { return 'reference'; } // Technical: Code, APIs, architecture, implementation details if ( titleLower.includes('technical') || titleLower.includes('architecture') || titleLower.includes('implementation') || titleLower.includes('integration') || titleLower.includes('api') || content.match(/```|`[a-z]+`|function|class|const|import/gi) ) { return 'technical'; } // Practical: How-to, tutorials, guides, use cases if ( titleLower.includes('how') || titleLower.includes('guide') || titleLower.includes('tutorial') || titleLower.includes('example') || titleLower.includes('use case') || titleLower.includes('should use') || titleLower.includes('contributing') ) { return 'practical'; } // Default to conceptual: Theory, principles, explanations return 'conceptual'; } /** * Determine technical level based on content complexity * @param {string} content - Section content * @returns {string} Technical level (beginner|intermediate|advanced) */ function determineTechnicalLevel(content) { const contentLower = content.toLowerCase(); // Advanced: Code examples, APIs, complex architecture if ( content.match(/```[\s\S]+```/g) || contentLower.includes('api') || contentLower.includes('implementation') || contentLower.includes('integration') || contentLower.includes('architecture') ) { return 'advanced'; } // Intermediate: Technical concepts without code if ( contentLower.includes('service') || contentLower.includes('component') || contentLower.includes('system') || contentLower.includes('framework') ) { return 'intermediate'; } // Beginner: High-level concepts, introductions return 'beginner'; } /** * Generate slug from title * @param {string} title - Section title * @returns {string} URL-friendly slug */ function generateSlug(title) { return title .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } /** * Process markdown file and generate card sections * @param {string} filePath - Path to markdown file * @returns {Array} Array of section objects ready for MongoDB */ async function processMarkdownFile(filePath) { console.log(`\nšŸ“„ Processing: ${filePath}`); // Read markdown file const markdown = fs.readFileSync(filePath, 'utf8'); // Extract sections const rawSections = extractSections(markdown); console.log(` Found ${rawSections.length} sections`); // Process each section const sections = []; for (let i = 0; i < rawSections.length; i++) { const raw = rawSections[i]; // Skip empty sections if (!raw.content_md.trim()) { continue; } // Convert markdown to HTML const content_html = marked(raw.content_md); // Generate metadata const excerpt = generateExcerpt(raw.content_md); const readingTime = estimateReadingTime(raw.content_md); const category = classifySection(raw.title, raw.content_md); const technicalLevel = determineTechnicalLevel(raw.content_md); const slug = generateSlug(raw.title); const section = { number: i + 1, title: raw.title, slug, content_html, excerpt, readingTime, technicalLevel, category }; sections.push(section); console.log(` ${i + 1}. ${section.title}`); console.log(` Category: ${category} | Level: ${technicalLevel} | ${readingTime} min`); } return sections; } /** * Update document in MongoDB with generated sections * @param {string} slug - Document slug * @param {Array} sections - Array of section objects */ async function updateDatabase(slug, sections) { try { // Get Document model (uses MongoDB driver directly, not Mongoose) const Document = require('../src/models/Document.model.js'); // Find document by slug const doc = await Document.findBySlug(slug); if (!doc) { console.error(` āŒ Document not found: ${slug}`); return false; } // Update sections const success = await Document.update(doc._id.toString(), { sections }); if (!success) { console.error(` āŒ Failed to update document`); return false; } console.log(` āœ… Updated document in MongoDB: ${doc.title}`); console.log(` šŸ“Š Sections: ${sections.length}`); return true; } catch (error) { console.error(` āŒ Database error: ${error.message}`); return false; } } /** * Main execution */ async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.error('Usage: node scripts/generate-card-sections.js [--update-db]'); console.error('Example: node scripts/generate-card-sections.js introduction-to-the-tractatus-framework.md --update-db'); process.exit(1); } const markdownFile = args[0]; const updateDb = args.includes('--update-db'); if (!fs.existsSync(markdownFile)) { console.error(`āŒ File not found: ${markdownFile}`); process.exit(1); } // Generate sections const sections = await processMarkdownFile(markdownFile); // Output JSON console.log(`\nšŸ“¦ Generated ${sections.length} sections\n`); if (!updateDb) { console.log(JSON.stringify(sections, null, 2)); console.log('\nšŸ’” To update database, add --update-db flag'); } else { // Extract slug from filename const slug = path.basename(markdownFile, '.md'); const success = await updateDatabase(slug, sections); if (success) { console.log(`\nāœ… Card presentation sections updated successfully!`); } else { console.log(`\nāŒ Failed to update database`); process.exit(1); } } } // Run if called directly if (require.main === module) { main().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } module.exports = { processMarkdownFile, extractSections, classifySection };