153 lines
5.9 KiB
JavaScript
153 lines
5.9 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
const { execSync } = require('child_process');
|
||
|
|
const { marked } = require('marked');
|
||
|
|
|
||
|
|
// Custom script to handle Mermaid rendering and fix internal links
|
||
|
|
async function convertMarkdownToPDF() {
|
||
|
|
const inputFile = process.argv[2] || 'complete-documentation.md';
|
||
|
|
const outputFile = inputFile.replace('.md', '.pdf');
|
||
|
|
|
||
|
|
console.log(`Converting ${inputFile} to PDF with Mermaid support...`);
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Read the markdown file
|
||
|
|
let content = fs.readFileSync(`/app/docs/${inputFile}`, 'utf8');
|
||
|
|
|
||
|
|
// Remove YAML front matter if present
|
||
|
|
content = content.replace(/^---\n[\s\S]*?\n---\n/, '');
|
||
|
|
|
||
|
|
// Remove emojis that don't render well in PDF
|
||
|
|
content = content.replace(/📋|🛠️|🔧|📊|🏗️|🎉|✅|❌|🚀|📝|📚|🔍|⚠️|💡/g, '');
|
||
|
|
|
||
|
|
// Helper function to generate section ID from text
|
||
|
|
function generateSectionId(text) {
|
||
|
|
return text
|
||
|
|
.toLowerCase()
|
||
|
|
.replace(/[^\w\s-]/g, '') // Remove special chars
|
||
|
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||
|
|
.replace(/-+/g, '-') // Collapse multiple hyphens
|
||
|
|
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fix internal links to work within the same document
|
||
|
|
// Convert [text](filename.md) to [text](#section-name)
|
||
|
|
let linkCount = 0;
|
||
|
|
content = content.replace(/\[([^\]]+)\]\((\d+-[^)]+\.md)\)/g, (match, linkText, filename) => {
|
||
|
|
// Use the link text to generate the section ID (since that's what the header will be)
|
||
|
|
const sectionId = generateSectionId(linkText);
|
||
|
|
linkCount++;
|
||
|
|
return `[${linkText}](#${sectionId})`;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Fix relative links ./filename.md
|
||
|
|
content = content.replace(/\[([^\]]+)\]\(\.\/(\d+-[^)]+\.md)\)/g, (match, linkText, filename) => {
|
||
|
|
const sectionId = generateSectionId(linkText);
|
||
|
|
linkCount++;
|
||
|
|
return `[${linkText}](#${sectionId})`;
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(`✅ Processed ${linkCount} internal links`);
|
||
|
|
|
||
|
|
// Process Mermaid diagrams
|
||
|
|
let mermaidCounter = 0;
|
||
|
|
content = content.replace(/```mermaid\n([\s\S]*?)\n```/g, (match, mermaidCode) => {
|
||
|
|
mermaidCounter++;
|
||
|
|
const diagramFile = `/app/docs/diagram-${mermaidCounter}.mmd`;
|
||
|
|
const imageFile = `/app/docs/diagram-${mermaidCounter}.png`;
|
||
|
|
|
||
|
|
// Write mermaid code to temporary file
|
||
|
|
fs.writeFileSync(diagramFile, mermaidCode.trim());
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Generate PNG from mermaid
|
||
|
|
execSync(`mmdc -i ${diagramFile} -o ${imageFile} -w 800 -H 600 --backgroundColor white`);
|
||
|
|
|
||
|
|
// Replace with image reference
|
||
|
|
return ``;
|
||
|
|
} catch (error) {
|
||
|
|
console.warn(`Warning: Failed to render Mermaid diagram ${mermaidCounter}:`, error.message);
|
||
|
|
// Return original code block if mermaid fails
|
||
|
|
return match;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Configure marked to generate proper IDs for headers
|
||
|
|
marked.use({
|
||
|
|
renderer: {
|
||
|
|
heading(text, level) {
|
||
|
|
// Generate ID from text content
|
||
|
|
const id = text
|
||
|
|
.toLowerCase()
|
||
|
|
.replace(/[^\w\s-]/g, '') // Remove special chars
|
||
|
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||
|
|
.replace(/-+/g, '-') // Collapse multiple hyphens
|
||
|
|
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||
|
|
|
||
|
|
return `<h${level} id="${id}">${text}</h${level}>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Convert markdown to HTML
|
||
|
|
const html = marked(content, {
|
||
|
|
gfm: true,
|
||
|
|
breaks: false,
|
||
|
|
headerIds: true,
|
||
|
|
mangle: false
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create complete HTML document with CSS
|
||
|
|
const cssContent = fs.readFileSync('/app/docs/custom-styles.css', 'utf8');
|
||
|
|
const fullHtml = `
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<title>DoczyAI Documentation</title>
|
||
|
|
<style>
|
||
|
|
${cssContent}
|
||
|
|
body { margin: 0; padding: 20px; }
|
||
|
|
@page { margin: 2cm; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
${html}
|
||
|
|
</body>
|
||
|
|
</html>`;
|
||
|
|
|
||
|
|
// Write HTML to temporary file
|
||
|
|
const htmlFile = `/app/docs/temp-${inputFile.replace('.md', '.html')}`;
|
||
|
|
fs.writeFileSync(htmlFile, fullHtml);
|
||
|
|
|
||
|
|
// Use Chromium to generate PDF with clickable links (suppress stderr)
|
||
|
|
execSync(`/usr/bin/chromium-browser --headless --disable-gpu --no-sandbox --run-all-compositor-stages-before-draw --virtual-time-budget=10000 --print-to-pdf=/app/docs/${outputFile} --print-to-pdf-no-header file://${htmlFile} 2>/dev/null`);
|
||
|
|
|
||
|
|
console.log(`✅ PDF generated: ${outputFile}`);
|
||
|
|
|
||
|
|
// Clean up temporary files
|
||
|
|
try {
|
||
|
|
fs.unlinkSync(htmlFile);
|
||
|
|
for (let i = 1; i <= mermaidCounter; i++) {
|
||
|
|
try {
|
||
|
|
fs.unlinkSync(`/app/docs/diagram-${i}.mmd`);
|
||
|
|
// Keep PNG files for debugging, remove in production
|
||
|
|
// fs.unlinkSync(`/app/docs/diagram-${i}.png`);
|
||
|
|
} catch (e) {
|
||
|
|
// Ignore cleanup errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
// Ignore cleanup errors
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('❌ Error converting to PDF:', error.message);
|
||
|
|
console.error(error.stack);
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
convertMarkdownToPDF();
|