Pq234/robot-learning-tutorial
0
1#!/usr/bin/env node2import { spawn } from 'node:child_process';3import { promises as fs } from 'node:fs';4import { resolve, dirname, basename, extname } from 'node:path';5import process from 'node:process';6 7async function run(command, args = [], options = {}) {8 return new Promise((resolvePromise, reject) => {9 const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });10 child.on('error', reject);11 child.on('exit', (code) => {12 if (code === 0) resolvePromise(undefined);13 else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));14 });15 });16}17 18function parseArgs(argv) {19 const out = {};20 for (const arg of argv.slice(2)) {21 if (!arg.startsWith('--')) continue;22 const [k, v] = arg.replace(/^--/, '').split('=');23 out[k] = v === undefined ? true : v;24 }25 return out;26}27 28function slugify(text) {29 return String(text || '')30 .normalize('NFKD')31 .replace(/\p{Diacritic}+/gu, '')32 .toLowerCase()33 .replace(/[^a-z0-9]+/g, '-')34 .replace(/^-+|-+$/g, '')35 .slice(0, 120) || 'article';36}37 38async function checkPandocInstalled() {39 try {40 await run('pandoc', ['--version'], { stdio: 'pipe' });41 return true;42 } catch {43 return false;44 }45}46 47async function readMdxFile(filePath) {48 try {49 const content = await fs.readFile(filePath, 'utf-8');50 return content;51 } catch (error) {52 console.warn(`Warning: Could not read ${filePath}:`, error.message);53 return '';54 }55}56 57function extractFrontmatter(content) {58 const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);59 if (!frontmatterMatch) return { frontmatter: {}, content };60 61 const frontmatterText = frontmatterMatch[1];62 const contentWithoutFrontmatter = content.replace(frontmatterMatch[0], '');63 64 // Simple YAML parsing for basic fields65 const frontmatter = {};66 const lines = frontmatterText.split('\n');67 let currentKey = null;68 let currentValue = '';69 70 for (const line of lines) {71 const trimmed = line.trim();72 if (trimmed.includes(':') && !trimmed.startsWith('-')) {73 if (currentKey) {74 frontmatter[currentKey] = currentValue.trim();75 }76 const [key, ...valueParts] = trimmed.split(':');77 currentKey = key.trim();78 currentValue = valueParts.join(':').trim();79 } else if (currentKey) {80 currentValue += '\n' + trimmed;81 }82 }83 84 if (currentKey) {85 frontmatter[currentKey] = currentValue.trim();86 }87 88 return { frontmatter, content: contentWithoutFrontmatter };89}90 91function cleanMdxToMarkdown(content) {92 // Remove import statements93 content = content.replace(/^import .+?;?\s*$/gm, '');94 95 // Remove JSX component calls like <ComponentName />96 content = content.replace(/<[A-Z][a-zA-Z0-9]*\s*\/>/g, '');97 98 // Convert JSX components to simpler markdown99 // Handle Sidenote components specially100 content = content.replace(/<Sidenote>([\s\S]*?)<\/Sidenote>/g, (match, innerContent) => {101 // Extract main content and aside content102 const asideMatch = innerContent.match(/<Fragment slot="aside">([\s\S]*?)<\/Fragment>/);103 const mainContent = innerContent.replace(/<Fragment slot="aside">[\s\S]*?<\/Fragment>/, '').trim();104 const asideContent = asideMatch ? asideMatch[1].trim() : '';105 106 let result = mainContent;107 if (asideContent) {108 result += `\n\n> **Note:** ${asideContent}`;109 }110 return result;111 });112 113 // Handle Note components114 content = content.replace(/<Note[^>]*>([\s\S]*?)<\/Note>/g, (match, innerContent) => {115 return `\n> **Note:** ${innerContent.trim()}\n`;116 });117 118 // Handle Wide and FullWidth components119 content = content.replace(/<(Wide|FullWidth)>([\s\S]*?)<\/\1>/g, '$2');120 121 // Handle HtmlEmbed components (convert to simple text)122 content = content.replace(/<HtmlEmbed[^>]*\/>/g, '*[Interactive content not available in LaTeX]*');123 124 // Remove remaining JSX fragments125 content = content.replace(/<Fragment[^>]*>([\s\S]*?)<\/Fragment>/g, '$1');126 content = content.replace(/<[A-Z][a-zA-Z0-9]*[^>]*>([\s\S]*?)<\/[A-Z][a-zA-Z0-9]*>/g, '$1');127 128 // Clean up className attributes129 content = content.replace(/className="[^"]*"/g, '');130 131 // Clean up extra whitespace132 content = content.replace(/\n{3,}/g, '\n\n');133 134 return content.trim();135}136 137async function processChapterImports(content, contentDir) {138 let processedContent = content;139 140 // First, extract all import statements and their corresponding component calls141 const importPattern = /import\s+(\w+)\s+from\s+["']\.\/chapters\/([^"']+)["'];?/g;142 const imports = new Map();143 let match;144 145 // Collect all imports146 while ((match = importPattern.exec(content)) !== null) {147 const [fullImport, componentName, chapterPath] = match;148 imports.set(componentName, { path: chapterPath, importStatement: fullImport });149 }150 151 // Remove all import statements152 processedContent = processedContent.replace(importPattern, '');153 154 // Process each component call155 for (const [componentName, { path: chapterPath }] of imports) {156 const componentCallPattern = new RegExp(`<${componentName}\\s*\\/>`, 'g');157 158 try {159 const chapterFile = resolve(contentDir, 'chapters', chapterPath);160 const chapterContent = await readMdxFile(chapterFile);161 const { content: chapterMarkdown } = extractFrontmatter(chapterContent);162 const cleanChapter = cleanMdxToMarkdown(chapterMarkdown);163 164 processedContent = processedContent.replace(componentCallPattern, cleanChapter);165 console.log(`✅ Processed chapter: ${chapterPath}`);166 } catch (error) {167 console.warn(`Warning: Could not process chapter ${chapterPath}:`, error.message);168 processedContent = processedContent.replace(componentCallPattern, `\n*[Chapter ${chapterPath} could not be loaded]*\n`);169 }170 }171 172 return processedContent;173}174 175function createLatexPreamble(frontmatter) {176 const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'Untitled Article';177 const subtitle = frontmatter.subtitle || '';178 const authors = frontmatter.authors || '';179 const date = frontmatter.published || '';180 181 return `\\documentclass[11pt,a4paper]{article}182\\usepackage[utf8]{inputenc}183\\usepackage[T1]{fontenc}184\\usepackage{amsmath,amsfonts,amssymb}185\\usepackage{graphicx}186\\usepackage{hyperref}187\\usepackage{booktabs}188\\usepackage{longtable}189\\usepackage{array}190\\usepackage{multirow}191\\usepackage{wrapfig}192\\usepackage{float}193\\usepackage{colortbl}194\\usepackage{pdflscape}195\\usepackage{tabu}196\\usepackage{threeparttable}197\\usepackage{threeparttablex}198\\usepackage{ulem}199\\usepackage{makecell}200\\usepackage{xcolor}201\\usepackage{listings}202\\usepackage{fancyvrb}203\\usepackage{geometry}204\\geometry{margin=1in}205 206\\title{${title}${subtitle ? `\\\\\\large ${subtitle}` : ''}}207${authors ? `\\author{${authors}}` : ''}208${date ? `\\date{${date}}` : ''}209 210\\begin{document}211\\maketitle212\\tableofcontents213\\newpage214 215`;216}217 218async function main() {219 const cwd = process.cwd();220 const args = parseArgs(process.argv);221 222 // Check if pandoc is installed223 const hasPandoc = await checkPandocInstalled();224 if (!hasPandoc) {225 console.error('❌ Pandoc is not installed. Please install it first:');226 console.error(' macOS: brew install pandoc');227 console.error(' Ubuntu: apt-get install pandoc');228 console.error(' Windows: choco install pandoc');229 process.exit(1);230 }231 232 const contentDir = resolve(cwd, 'src/content');233 const articleFile = resolve(contentDir, 'article.mdx');234 235 // Check if article.mdx exists236 try {237 await fs.access(articleFile);238 } catch {239 console.error(`❌ Could not find article.mdx at ${articleFile}`);240 process.exit(1);241 }242 243 console.log('> Reading article content...');244 const articleContent = await readMdxFile(articleFile);245 const { frontmatter, content } = extractFrontmatter(articleContent);246 247 console.log('> Processing chapters...');248 const processedContent = await processChapterImports(content, contentDir);249 250 console.log('> Converting MDX to Markdown...');251 const markdownContent = cleanMdxToMarkdown(processedContent);252 253 // Generate output filename254 const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'article';255 const outFileBase = args.filename ? String(args.filename).replace(/\.(tex|pdf)$/i, '') : slugify(title);256 257 // Create temporary markdown file258 const tempMdFile = resolve(cwd, 'temp-article.md');259 await fs.writeFile(tempMdFile, markdownContent);260 261 262 console.log('> Converting to LaTeX with Pandoc...');263 const outputLatex = resolve(cwd, 'dist', `${outFileBase}.tex`);264 265 // Ensure dist directory exists266 await fs.mkdir(resolve(cwd, 'dist'), { recursive: true });267 268 // Pandoc conversion arguments269 const pandocArgs = [270 tempMdFile,271 '-o', outputLatex,272 '--from=markdown',273 '--to=latex',274 '--standalone',275 '--toc',276 '--number-sections',277 '--highlight-style=tango',278 '--listings'279 ];280 281 // Add bibliography if it exists282 const bibFile = resolve(contentDir, 'bibliography.bib');283 try {284 await fs.access(bibFile);285 pandocArgs.push('--bibliography', bibFile);286 pandocArgs.push('--citeproc');287 console.log('✅ Found bibliography file, including citations');288 } catch {289 console.log('ℹ️ No bibliography file found');290 }291 292 try {293 await run('pandoc', pandocArgs);294 console.log(`✅ LaTeX generated: ${outputLatex}`);295 296 // Optionally compile to PDF if requested297 if (args.pdf) {298 console.log('> Compiling LaTeX to PDF...');299 const outputPdf = resolve(cwd, 'dist', `${outFileBase}.pdf`);300 await run('pdflatex', ['-output-directory', resolve(cwd, 'dist'), outputLatex]);301 console.log(`✅ PDF generated: ${outputPdf}`);302 }303 304 } catch (error) {305 console.error('❌ Pandoc conversion failed:', error.message);306 process.exit(1);307 } finally {308 // Clean up temporary file309 try {310 await fs.unlink(tempMdFile);311 } catch { }312 }313}314 315main().catch((err) => {316 console.error(err);317 process.exit(1);318});319 