#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import { resolve, dirname, basename, extname } from 'node:path';
import process from 'node:process';
async function run(command, args = [], options = {}) {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) resolvePromise(undefined);
else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
});
});
}
function parseArgs(argv) {
const out = {};
for (const arg of argv.slice(2)) {
if (!arg.startsWith('--')) continue;
const [k, v] = arg.replace(/^--/, '').split('=');
out[k] = v === undefined ? true : v;
}
return out;
}
function slugify(text) {
return String(text || '')
.normalize('NFKD')
.replace(/\p{Diacritic}+/gu, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 120) || 'article';
}
async function checkPandocInstalled() {
try {
await run('pandoc', ['--version'], { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
async function readMdxFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
return content;
} catch (error) {
console.warn(`Warning: Could not read ${filePath}:`, error.message);
return '';
}
}
function extractFrontmatter(content) {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
if (!frontmatterMatch) return { frontmatter: {}, content };
const frontmatterText = frontmatterMatch[1];
const contentWithoutFrontmatter = content.replace(frontmatterMatch[0], '');
// Simple YAML parsing for basic fields
const frontmatter = {};
const lines = frontmatterText.split('\n');
let currentKey = null;
let currentValue = '';
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.includes(':') && !trimmed.startsWith('-')) {
if (currentKey) {
frontmatter[currentKey] = currentValue.trim();
}
const [key, ...valueParts] = trimmed.split(':');
currentKey = key.trim();
currentValue = valueParts.join(':').trim();
} else if (currentKey) {
currentValue += '\n' + trimmed;
}
}
if (currentKey) {
frontmatter[currentKey] = currentValue.trim();
}
return { frontmatter, content: contentWithoutFrontmatter };
}
function cleanMdxToMarkdown(content) {
// Remove import statements
content = content.replace(/^import .+?;?\s*$/gm, '');
// Remove JSX component calls like
content = content.replace(/<[A-Z][a-zA-Z0-9]*\s*\/>/g, '');
// Convert JSX components to simpler markdown
// Handle Sidenote components specially
content = content.replace(/([\s\S]*?)<\/Sidenote>/g, (match, innerContent) => {
// Extract main content and aside content
const asideMatch = innerContent.match(/([\s\S]*?)<\/Fragment>/);
const mainContent = innerContent.replace(/[\s\S]*?<\/Fragment>/, '').trim();
const asideContent = asideMatch ? asideMatch[1].trim() : '';
let result = mainContent;
if (asideContent) {
result += `\n\n> **Note:** ${asideContent}`;
}
return result;
});
// Handle Note components
content = content.replace(/]*>([\s\S]*?)<\/Note>/g, (match, innerContent) => {
return `\n> **Note:** ${innerContent.trim()}\n`;
});
// Handle Wide and FullWidth components
content = content.replace(/<(Wide|FullWidth)>([\s\S]*?)<\/\1>/g, '$2');
// Handle HtmlEmbed components (convert to simple text)
content = content.replace(/]*\/>/g, '*[Interactive content not available in LaTeX]*');
// Remove remaining JSX fragments
content = content.replace(/]*>([\s\S]*?)<\/Fragment>/g, '$1');
content = content.replace(/<[A-Z][a-zA-Z0-9]*[^>]*>([\s\S]*?)<\/[A-Z][a-zA-Z0-9]*>/g, '$1');
// Clean up className attributes
content = content.replace(/className="[^"]*"/g, '');
// Clean up extra whitespace
content = content.replace(/\n{3,}/g, '\n\n');
return content.trim();
}
async function processChapterImports(content, contentDir) {
let processedContent = content;
// First, extract all import statements and their corresponding component calls
const importPattern = /import\s+(\w+)\s+from\s+["']\.\/chapters\/([^"']+)["'];?/g;
const imports = new Map();
let match;
// Collect all imports
while ((match = importPattern.exec(content)) !== null) {
const [fullImport, componentName, chapterPath] = match;
imports.set(componentName, { path: chapterPath, importStatement: fullImport });
}
// Remove all import statements
processedContent = processedContent.replace(importPattern, '');
// Process each component call
for (const [componentName, { path: chapterPath }] of imports) {
const componentCallPattern = new RegExp(`<${componentName}\\s*\\/>`, 'g');
try {
const chapterFile = resolve(contentDir, 'chapters', chapterPath);
const chapterContent = await readMdxFile(chapterFile);
const { content: chapterMarkdown } = extractFrontmatter(chapterContent);
const cleanChapter = cleanMdxToMarkdown(chapterMarkdown);
processedContent = processedContent.replace(componentCallPattern, cleanChapter);
console.log(`✅ Processed chapter: ${chapterPath}`);
} catch (error) {
console.warn(`Warning: Could not process chapter ${chapterPath}:`, error.message);
processedContent = processedContent.replace(componentCallPattern, `\n*[Chapter ${chapterPath} could not be loaded]*\n`);
}
}
return processedContent;
}
function createLatexPreamble(frontmatter) {
const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'Untitled Article';
const subtitle = frontmatter.subtitle || '';
const authors = frontmatter.authors || '';
const date = frontmatter.published || '';
return `\\documentclass[11pt,a4paper]{article}
\\usepackage[utf8]{inputenc}
\\usepackage[T1]{fontenc}
\\usepackage{amsmath,amsfonts,amssymb}
\\usepackage{graphicx}
\\usepackage{hyperref}
\\usepackage{booktabs}
\\usepackage{longtable}
\\usepackage{array}
\\usepackage{multirow}
\\usepackage{wrapfig}
\\usepackage{float}
\\usepackage{colortbl}
\\usepackage{pdflscape}
\\usepackage{tabu}
\\usepackage{threeparttable}
\\usepackage{threeparttablex}
\\usepackage{ulem}
\\usepackage{makecell}
\\usepackage{xcolor}
\\usepackage{listings}
\\usepackage{fancyvrb}
\\usepackage{geometry}
\\geometry{margin=1in}
\\title{${title}${subtitle ? `\\\\\\large ${subtitle}` : ''}}
${authors ? `\\author{${authors}}` : ''}
${date ? `\\date{${date}}` : ''}
\\begin{document}
\\maketitle
\\tableofcontents
\\newpage
`;
}
async function main() {
const cwd = process.cwd();
const args = parseArgs(process.argv);
// Check if pandoc is installed
const hasPandoc = await checkPandocInstalled();
if (!hasPandoc) {
console.error('❌ Pandoc is not installed. Please install it first:');
console.error(' macOS: brew install pandoc');
console.error(' Ubuntu: apt-get install pandoc');
console.error(' Windows: choco install pandoc');
process.exit(1);
}
const contentDir = resolve(cwd, 'src/content');
const articleFile = resolve(contentDir, 'article.mdx');
// Check if article.mdx exists
try {
await fs.access(articleFile);
} catch {
console.error(`❌ Could not find article.mdx at ${articleFile}`);
process.exit(1);
}
console.log('> Reading article content...');
const articleContent = await readMdxFile(articleFile);
const { frontmatter, content } = extractFrontmatter(articleContent);
console.log('> Processing chapters...');
const processedContent = await processChapterImports(content, contentDir);
console.log('> Converting MDX to Markdown...');
const markdownContent = cleanMdxToMarkdown(processedContent);
// Generate output filename
const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'article';
const outFileBase = args.filename ? String(args.filename).replace(/\.(tex|pdf)$/i, '') : slugify(title);
// Create temporary markdown file
const tempMdFile = resolve(cwd, 'temp-article.md');
await fs.writeFile(tempMdFile, markdownContent);
console.log('> Converting to LaTeX with Pandoc...');
const outputLatex = resolve(cwd, 'dist', `${outFileBase}.tex`);
// Ensure dist directory exists
await fs.mkdir(resolve(cwd, 'dist'), { recursive: true });
// Pandoc conversion arguments
const pandocArgs = [
tempMdFile,
'-o', outputLatex,
'--from=markdown',
'--to=latex',
'--standalone',
'--toc',
'--number-sections',
'--highlight-style=tango',
'--listings'
];
// Add bibliography if it exists
const bibFile = resolve(contentDir, 'bibliography.bib');
try {
await fs.access(bibFile);
pandocArgs.push('--bibliography', bibFile);
pandocArgs.push('--citeproc');
console.log('✅ Found bibliography file, including citations');
} catch {
console.log('ℹ️ No bibliography file found');
}
try {
await run('pandoc', pandocArgs);
console.log(`✅ LaTeX generated: ${outputLatex}`);
// Optionally compile to PDF if requested
if (args.pdf) {
console.log('> Compiling LaTeX to PDF...');
const outputPdf = resolve(cwd, 'dist', `${outFileBase}.pdf`);
await run('pdflatex', ['-output-directory', resolve(cwd, 'dist'), outputLatex]);
console.log(`✅ PDF generated: ${outputPdf}`);
}
} catch (error) {
console.error('❌ Pandoc conversion failed:', error.message);
process.exit(1);
} finally {
// Clean up temporary file
try {
await fs.unlink(tempMdFile);
} catch { }
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});