Files
awesome-chatgpt-prompts-pro…/scripts/generate-book-pdf.ts
Fatih Kadir Akın 2074f3832a book update
2026-02-06 12:02:02 +03:00

4252 lines
148 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env npx tsx
/**
* Generate PDF version of the Interactive Book of Prompting
*
* Usage:
* npx tsx scripts/generate-book-pdf.ts [locale]
*
* Examples:
* npx tsx scripts/generate-book-pdf.ts # Generate English version (default)
* npx tsx scripts/generate-book-pdf.ts tr # Generate Turkish version
* npx tsx scripts/generate-book-pdf.ts --all # Generate all locales
* npx tsx scripts/generate-book-pdf.ts --print # Print-ready with bleed & CMYK colors
* npx tsx scripts/generate-book-pdf.ts --all --print # All locales, print-ready
*/
import * as fs from 'fs';
import * as path from 'path';
import { parts, type Chapter } from '../src/lib/book/chapters';
import { getLocaleData, type LocaleData } from '../src/components/book/elements/locales';
// Configuration
const BOOK_DIR = path.join(process.cwd(), 'src/content/book');
const MESSAGES_DIR = path.join(process.cwd(), 'messages');
const OUTPUT_DIR = path.join(process.cwd(), 'public/book-pdf');
const SITE_URL = 'https://prompts.chat';
// Print-ready mode flag (set by --print CLI argument)
const PRINT_READY = process.argv.includes('--print');
// Bleed dimensions for print-ready output
const BLEED = '0.125in'; // 3mm standard bleed
const TRIM_WIDTH = '6in';
const TRIM_HEIGHT = '9in';
const BLEED_WIDTH = '6.25in'; // 6 + 0.125*2
const BLEED_HEIGHT = '9.25in'; // 9 + 0.125*2
// Components that truly need interactivity (API calls, complex animations)
// Everything else gets static rendering
const INTERACTIVE_ONLY_COMPONENTS = [
'PromptAnalyzer', // Needs live API calls
'RunPromptButton',
'CodeEditor',
];
/**
* Load UI messages from messages/*.json
*/
function loadMessages(locale: string): Record<string, unknown> {
try {
const filePath = path.join(MESSAGES_DIR, `${locale}.json`);
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch {
// Fallback to English
const filePath = path.join(MESSAGES_DIR, 'en.json');
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
}
/**
* Get a nested translation key from messages
*/
function t(messages: Record<string, unknown>, key: string): string {
const keys = key.split('.');
let current: unknown = messages;
for (const k of keys) {
if (current && typeof current === 'object' && k in (current as Record<string, unknown>)) {
current = (current as Record<string, unknown>)[k];
} else {
return key; // Fallback to key itself
}
}
return typeof current === 'string' ? current : key;
}
// Components with explicit static renderers (handled individually in transformMdxForPdf)
const STATICALLY_RENDERED_COMPONENTS = [
'TryIt', 'Quiz', 'Callout', 'InfoGrid', 'Checklist', 'Compare',
'FillInTheBlank', 'InteractiveChecklist', 'PromptDebugger',
'PromptChallenge', 'BeforeAfterEditor', 'DiffView', 'VersionDiff',
'TokenizerDemo', 'TokenPredictionDemo', 'ContextWindowDemo',
'TemperatureDemo', 'StructuredOutputDemo', 'FewShotDemo',
'JsonYamlDemo', 'IterativeRefinementDemo', 'CostCalculatorDemo',
'EmbeddingsDemo', 'LLMCapabilitiesDemo',
'JailbreakDemo', 'TextToImageDemo', 'TextToVideoDemo',
'SummarizationDemo', 'ContextPlayground',
'ValidationDemo', 'FallbackDemo', 'ContentPipelineDemo',
'ChainExample', 'ChainFlowDemo', 'ChainErrorDemo',
'FrameworkDemo', 'CRISPEFramework', 'BREAKFramework', 'RTFFramework',
'PromptBreakdown', 'SpecificitySpectrum', 'PrinciplesSummary',
'PromptBuilder', 'BookPartsNav',
'Collapsible', 'CopyableCode',
];
// Localization for the interactive notice
const INTERACTIVE_NOTICES: Record<string, string> = {
en: '📖 This is an interactive element. Visit prompts.chat/book to try it live!',
tr: '📖 Bu etkileşimli bir öğedir. Canlı denemek için prompts.chat/book adresini ziyaret edin!',
es: '📖 Este es un elemento interactivo. ¡Visita prompts.chat/book para probarlo en vivo!',
de: '📖 Dies ist ein interaktives Element. Besuchen Sie prompts.chat/book, um es live auszuprobieren!',
fr: '📖 Ceci est un élément interactif. Visitez prompts.chat/book pour l\'essayer en direct!',
pt: '📖 Este é um elemento interativo. Visite prompts.chat/book para experimentá-lo ao vivo!',
zh: '📖 这是一个互动元素。访问 prompts.chat/book 进行在线体验!',
ja: '📖 これはインタラクティブな要素です。prompts.chat/book でライブで試してみてください!',
ko: '📖 이것은 인터랙티브 요소입니다. prompts.chat/book을 방문하여 직접 체험해 보세요!',
ar: '📖 هذا عنصر تفاعلي. قم بزيارة prompts.chat/book لتجربته مباشرة!',
it: '📖 Questo è un elemento interattivo. Visita prompts.chat/book per provarlo dal vivo!',
ru: '📖 Это интерактивный элемент. Посетите prompts.chat/book, чтобы попробовать вживую!',
fa: '📖 این یک عنصر تعاملی است. برای امتحان زنده به prompts.chat/book مراجعه کنید!',
nl: '📖 Dit is een interactief element. Bezoek prompts.chat/book om het live te proberen!',
el: '📖 Αυτό είναι ένα διαδραστικό στοιχείο. Επισκεφθείτε το prompts.chat/book για να το δοκιμάσετε ζωντανά!',
az: '📖 Bu interaktiv elementdir. Canlı sınamaq üçün prompts.chat/book saytına daxil olun!',
he: '📖 זהו אלמנט אינטראקטיבי. בקרו ב-prompts.chat/book כדי לנסות אותו בזמן אמת!',
};
/**
* Inline SVG icons for print-ready mode (emojis don't render reliably in CMYK print).
* Each icon is a 14x14 inline SVG with currentColor stroke.
*/
const SVG_ICONS: Record<string, string> = {
zap: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
quiz: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>',
info: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
warning: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
tip: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17H8v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z"/></svg>',
pencil: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
search: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
trophy: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 010-5H6"/><path d="M18 9h1.5a2.5 2.5 0 000-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0012 0V2z"/></svg>',
refresh: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>',
palette: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor"/><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor"/><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor"/><circle cx="6.5" cy="12.5" r="0.5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 011.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
video: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
shield: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
book: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
gem: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6 3 18 3 22 9 12 22 2 9"/><line x1="2" y1="9" x2="22" y2="9"/><line x1="12" y1="22" x2="6" y2="9"/><line x1="12" y1="22" x2="18" y2="9"/></svg>',
target: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
crown: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 4l3 12h14l3-12-6 7-4-7-4 7-6-7z"/><line x1="2" y1="21" x2="22" y2="21"/></svg>',
compass: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>',
sparkles: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l1.912 5.813L20 12l-6.088 3.187L12 21l-1.912-5.813L4 12l6.088-3.187z"/></svg>',
ruler: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.73 18l-8-14a2 2 0 00-3.48 0l-8 14A2 2 0 004 21h16a2 2 0 001.73-3z"/></svg>',
check: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
checkbox: '<svg class="ico-sm" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="14" height="14" rx="2"/></svg>',
};
/**
* Return icon markup: SVG for print, emoji for screen
*/
function icon(name: string, emoji: string): string {
if (PRINT_READY && SVG_ICONS[name]) {
return SVG_ICONS[name];
}
return emoji;
}
// Fallback book titles (used if messages don't have book.title)
const BOOK_TITLES_FALLBACK: Record<string, string> = {
en: 'The Interactive Book of Prompting',
tr: 'İnteraktif Prompt Yazma Kitabı',
};
interface ProcessedChapter {
slug: string;
title: string;
part: string;
content: string;
}
/**
* Get available locales from the book directory
*/
function getAvailableLocales(): string[] {
const entries = fs.readdirSync(BOOK_DIR, { withFileTypes: true });
const locales = entries
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
// Add 'en' as it's the default (files in root)
if (!locales.includes('en')) {
locales.unshift('en');
}
return locales;
}
/**
* Get the path to MDX files for a locale
*/
function getLocalePath(locale: string): string {
if (locale === 'en') {
return BOOK_DIR;
}
return path.join(BOOK_DIR, locale);
}
/**
* Check if a chapter exists for a locale
*/
function chapterExists(locale: string, slug: string): boolean {
const localePath = getLocalePath(locale);
const filePath = path.join(localePath, `${slug}.mdx`);
return fs.existsSync(filePath);
}
/**
* Extract prompt content from TryIt component
*/
function extractTryItPrompt(content: string): string | null {
// Match prompt={`...`} or prompt="..."
const backtickMatch = content.match(/prompt=\{`([\s\S]*?)`\}/);
if (backtickMatch) return backtickMatch[1];
const quoteMatch = content.match(/prompt="([^"]*?)"/);
if (quoteMatch) return quoteMatch[1];
return null;
}
/**
* Extract Quiz content
*/
function extractQuizContent(content: string): { question: string; options: string[]; explanation: string } | null {
const questionMatch = content.match(/question="((?:[^"\\]|\\.)*)"/);
const optionsMatch = content.match(/options=\{\[([\s\S]*?)\]\}/);
const explanationMatch = content.match(/explanation="((?:[^"\\]|\\.)*)"/);
if (!questionMatch || !optionsMatch) return null;
const question = questionMatch[1].replace(/\\"/g, '"');
const optionsStr = optionsMatch[1];
const options: string[] = [];
const optRegex = /"((?:[^"\\]|\\.)*)"/g;
let m;
while ((m = optRegex.exec(optionsStr)) !== null) {
options.push(m[1].replace(/\\"/g, '"'));
}
const explanation = explanationMatch ? explanationMatch[1].replace(/\\"/g, '"') : '';
return { question, options, explanation };
}
/**
* Extract Callout content
*/
function extractCalloutContent(match: string): { type: string; title: string; content: string } {
const typeMatch = match.match(/type="([^"]*?)"/);
const titleMatch = match.match(/title="([^"]*?)"/);
// Extract content between > and </Callout>
const contentMatch = match.match(/>\s*([\s\S]*?)\s*<\/Callout>/);
return {
type: typeMatch ? typeMatch[1] : 'info',
title: titleMatch ? titleMatch[1] : '',
content: contentMatch ? contentMatch[1].trim() : '',
};
}
/**
* Extract InfoGrid items
*/
function extractInfoGridItems(content: string): { label: string; description: string }[] {
const itemsMatch = content.match(/items=\{\[([\s\S]*?)\]\}/);
if (!itemsMatch) return [];
const items: { label: string; description: string }[] = [];
// Handle escaped quotes inside strings: match "..." allowing \" inside
const itemRegex = /\{\s*label:\s*"((?:[^"\\]|\\.)*)"\s*,\s*description:\s*"((?:[^"\\]|\\.)*)"/g;
let match;
while ((match = itemRegex.exec(itemsMatch[1])) !== null) {
// Unescape the \" back to "
const label = match[1].replace(/\\"/g, '"');
const description = match[2].replace(/\\"/g, '"');
items.push({ label, description });
}
return items;
}
/**
* Extract Checklist items
*/
function extractChecklistItems(content: string): { title: string; items: string[] } {
const titleMatch = content.match(/title="([^"]*?)"/);
const itemsMatch = content.match(/items=\{\[([\s\S]*?)\]\}/);
const title = titleMatch ? titleMatch[1] : '';
const items: string[] = [];
if (itemsMatch) {
const textRegex = /text:\s*"([^"]*?)"/g;
let match;
while ((match = textRegex.exec(itemsMatch[1])) !== null) {
items.push(match[1]);
}
}
return { title, items };
}
/**
* Extract props from a JSX-like component string
*/
function extractProps(content: string): Record<string, string> {
const props: Record<string, string> = {};
// Simple string props: key="value" (handles escaped quotes)
const stringRegex = /(\w+)="((?:[^"\\]|\\.)*)"/g;
let m;
while ((m = stringRegex.exec(content)) !== null) {
props[m[1]] = m[2].replace(/\\"/g, '"');
}
// Template literal props: key={`value`}
const templateRegex = /(\w+)=\{`([\s\S]*?)`\}/g;
while ((m = templateRegex.exec(content)) !== null) {
props[m[1]] = m[2];
}
return props;
}
/**
* Extract array of objects from items={[...]} prop
*/
function extractArrayProp(content: string, propName: string): Record<string, string>[] {
const regex = new RegExp(`${propName}=\\{\\[([\\s\\S]*?)\\]\\}`, 'g');
const match = regex.exec(content);
if (!match) return [];
const items: Record<string, string>[] = [];
const objRegex = /\{([^{}]*?)\}/g;
let m;
while ((m = objRegex.exec(match[1])) !== null) {
const obj: Record<string, string> = {};
// Handle escaped quotes in field values
const fieldRegex = /(\w+):\s*"((?:[^"\\]|\\.)*)"/g;
let fm;
while ((fm = fieldRegex.exec(m[1])) !== null) {
obj[fm[1]] = fm[2].replace(/\\"/g, '"');
}
if (Object.keys(obj).length > 0) items.push(obj);
}
return items;
}
/**
* Transform MDX content for PDF - convert interactive components to static
*/
function transformMdxForPdf(content: string, locale: string, localeData?: LocaleData, messages?: Record<string, unknown>): string {
let result = content;
const noticeRaw = INTERACTIVE_NOTICES[locale] || INTERACTIVE_NOTICES.en;
const notice = PRINT_READY ? noticeRaw.replace('📖', icon('book', '📖')) : noticeRaw;
const msg = messages || {};
const ld = localeData || getLocaleData('en');
// Translated UI labels
const labels = {
tryIt: t(msg, 'book.interactive.tryIt') || 'Try This Prompt',
quiz: t(msg, 'book.interactive.quiz') || 'Quiz',
answer: t(msg, 'book.interactive.answer') || 'Answer',
prompt: t(msg, 'book.interactive.prompt') || 'Prompt',
output: t(msg, 'book.interactive.outputLabel') || 'Output',
answers: t(msg, 'book.interactive.answers') || 'Answers',
completePrompt: t(msg, 'book.interactive.completePrompt') || 'Complete Prompt',
examplePrompts: t(msg, 'book.interactive.examplePrompts') || 'Example prompts',
};
// In print mode, remove author social links from preface
if (PRINT_READY) {
result = result.replace(/<div className="flex gap-3 mt-3">[\s\S]*?<\/div>/g, '');
}
// Remove navigation components entirely
result = result.replace(/<NavButton[^>]*\/>/g, '');
result = result.replace(/<NavFooter[^>]*\/>/g, '');
result = result.replace(/<BookPartsNav[^>]*\/>/g, '');
// ============================================================
// TryIt - Show prompt as styled code block
// ============================================================
result = result.replace(/<TryIt\s+([\s\S]*?)\/>/g, (match, attrs) => {
const prompt = extractTryItPrompt(match);
const titleMatch = attrs.match(/title="([^"]*?)"/);
const descMatch = attrs.match(/description="([^"]*?)"/);
const title = titleMatch ? titleMatch[1] : labels.tryIt;
const desc = descMatch ? descMatch[1] : '';
if (prompt) {
const printablePrompt = convertPromptVariables(prompt);
const protectedPrompt = protectCodeBlock(escapeHtml(printablePrompt));
return `
<div class="tryit-box">
<div class="tryit-header">${icon('zap', '⚡')} ${escapeHtml(title)}</div>
${desc ? `<p class="tryit-desc">${escapeHtml(desc)}</p>` : ''}
<pre class="prompt-code">${protectedPrompt}</pre>
</div>
`;
}
return '';
});
// ============================================================
// Quiz - Show question, options, and answer
// ============================================================
result = result.replace(/<Quiz\s+([\s\S]*?)\/>/g, (match) => {
const quiz = extractQuizContent(match);
if (quiz) {
const correctIdx = match.match(/correctIndex=\{(\d+)\}/);
const correctNum = correctIdx ? parseInt(correctIdx[1]) : -1;
const optionsHtml = quiz.options.map((opt, i) => {
const marker = i === correctNum ? '●' : '○';
const cls = i === correctNum ? ' class="quiz-correct"' : '';
return `<div${cls}>${marker} ${opt}</div>`;
}).join('\n');
return `
<div class="quiz-box">
<div class="quiz-header">${icon('quiz', '📝')} Quiz</div>
<p class="quiz-question"><strong>${quiz.question}</strong></p>
<div class="quiz-options">${optionsHtml}</div>
${quiz.explanation ? `<p class="quiz-explanation"><strong>Answer:</strong> ${quiz.explanation}</p>` : ''}
</div>
`;
}
return '';
});
// ============================================================
// Callout - Static info boxes
// ============================================================
result = result.replace(/<Callout\s+([\s\S]*?)<\/Callout>/g, (match) => {
const callout = extractCalloutContent(match);
const calloutIcons: Record<string, [string, string]> = { info: ['info', ''], warning: ['warning', '⚠️'], tip: ['tip', '💡'], example: ['zap', '⚡'] };
const ci = calloutIcons[callout.type] || calloutIcons.info;
return `
<div class="callout callout-${callout.type}">
<div class="callout-header">${icon(ci[0], ci[1])} ${callout.title}</div>
<div class="callout-content">${callout.content}</div>
</div>
`;
});
// ============================================================
// InfoGrid - Static grid
// ============================================================
result = result.replace(/<InfoGrid\s+([\s\S]*?)\/>/g, (match) => {
const items = extractInfoGridItems(match);
if (items.length > 0) {
const itemsHtml = items.map(item =>
`<div class="info-item"><strong>${item.label}</strong>: ${item.description}</div>`
).join('\n');
return `<div class="info-grid">\n${itemsHtml}\n</div>`;
}
return '';
});
// ============================================================
// Checklist (simple) - Printable checkbox list
// ============================================================
result = result.replace(/<Checklist\s+([\s\S]*?)\/>/g, (match) => {
const checklist = extractChecklistItems(match);
const cb = icon('checkbox', '☐');
const itemsHtml = checklist.items.map(item => `<li>${cb} ${item}</li>`).join('\n');
return `
<div class="checklist">
${checklist.title ? `<div class="checklist-title">${checklist.title}</div>` : ''}
<ul>${itemsHtml}</ul>
</div>
`;
});
// ============================================================
// Compare - Side by side (handles object syntax {{ label, content }})
// ============================================================
result = result.replace(/<Compare\s+([\s\S]*?)\/>/g, (match) => {
// Try object syntax: before={{ label: "...", content: "..." or `...` }}
const extractSide = (side: string): { label: string; content: string } | null => {
const objRegex = new RegExp(`${side}=\\{\\{[\\s\\S]*?\\}\\}`);
const objMatch = match.match(objRegex);
if (!objMatch) return null;
const block = objMatch[0];
const labelMatch = block.match(/label:\s*"((?:[^"\\]|\\.)*)"/);
const contentQuote = block.match(/content:\s*"((?:[^"\\]|\\.)*)"/);
const contentBt = block.match(/content:\s*`([\s\S]*?)`/);
const label = labelMatch ? labelMatch[1].replace(/\\"/g, '"') : side;
const content = contentBt ? contentBt[1] : (contentQuote ? contentQuote[1].replace(/\\"/g, '"').replace(/\\n/g, '\n') : '');
return { label, content };
};
const before = extractSide('before');
const after = extractSide('after');
// Fallback: simple string props
if (!before || !after) {
const beforeSimple = match.match(/before="((?:[^"\\]|\\.)*)"/);
const afterSimple = match.match(/after="((?:[^"\\]|\\.)*)"/);
if (beforeSimple && afterSimple) {
return `
<div class="compare-box">
<div class="compare-item compare-before"><strong>Before:</strong> ${beforeSimple[1].replace(/\\"/g, '"')}</div>
<div class="compare-item compare-after"><strong>After:</strong> ${afterSimple[1].replace(/\\"/g, '"')}</div>
</div>
`;
}
return '';
}
const beforeContent = protectCodeBlock(escapeHtml(before.content));
const afterContent = protectCodeBlock(escapeHtml(after.content));
return `
<div class="compare-box">
<div class="compare-item compare-before"><strong>${escapeHtml(before.label)}</strong><pre class="prompt-code">${beforeContent}</pre></div>
<div class="compare-item compare-after"><strong>${escapeHtml(after.label)}</strong><pre class="prompt-code">${afterContent}</pre></div>
</div>
`;
});
// ============================================================
// FillInTheBlank - Printable exercise with blanks
// ============================================================
result = result.replace(/<FillInTheBlank\s+([\s\S]*?)\/>/g, (match) => {
const props = extractProps(match);
const title = props.title || 'Fill in the Blanks';
// Extract template
const templateMatch = match.match(/template="([^"]*?)"/);
const templateBt = match.match(/template=\{`([\s\S]*?)`\}/);
const template = templateBt ? templateBt[1] : (templateMatch ? templateMatch[1] : '');
// Extract blanks with correct answers
const blanks = extractArrayProp(match, 'blanks');
// Replace {{id}} in template with labeled blanks
let rendered = template.replace(/\{\{(\w+)\}\}/g, (_, id) => {
const blank = blanks.find(b => b.id === id);
const hint = blank?.correctAnswers || blank?.hint || '';
if (hint) {
return `_______ (${id}, e.g. ${hint})`;
}
return `_______ (${id})`;
});
const answersHtml = blanks.length > 0 ? blanks.map(b => {
const answers = b.correctAnswers || '';
return `<li><strong>${b.id}:</strong> ${answers}</li>`;
}).join('\n') : '';
const protectedTemplate = protectCodeBlock(escapeHtml(rendered));
return `
<div class="exercise-box">
<div class="exercise-header">${icon('pencil', '✏️')} ${escapeHtml(title)}</div>
<pre class="prompt-code">${protectedTemplate}</pre>
${answersHtml ? `<div class="exercise-answers"><strong>Answers:</strong><ul>${answersHtml}</ul></div>` : ''}
</div>
`;
});
// ============================================================
// InteractiveChecklist - Printable checklist
// ============================================================
result = result.replace(/<InteractiveChecklist\s+([\s\S]*?)\/>/g, (match) => {
const props = extractProps(match);
const title = props.title || 'Checklist';
const items = extractArrayProp(match, 'items');
const itemsHtml = items.map(item =>
`<li>${icon('checkbox', '☐')} <strong>${item.label || ''}</strong>${item.description ? `${item.description}` : ''}</li>`
).join('\n');
return `
<div class="checklist">
<div class="checklist-title">${escapeHtml(title)}</div>
<ul>${itemsHtml}</ul>
</div>
`;
});
// ============================================================
// PromptDebugger - Show prompt, bad output, and options
// ============================================================
result = result.replace(/<PromptDebugger\s+([\s\S]*?)\/>/g, (match) => {
const props = extractProps(match);
const title = props.title || 'Debug This Prompt';
const badPrompt = props.badPrompt || '';
const badOutput = props.badOutput || '';
const hint = props.hint || '';
const options = extractArrayProp(match, 'options');
const protectedBadPrompt = protectCodeBlock(escapeHtml(badPrompt));
const protectedBadOutput = protectCodeBlock(escapeHtml(badOutput));
const optionsHtml = options.map(opt => {
const marker = opt.isCorrect === 'true' ? '✓' : '○';
const cls = opt.isCorrect === 'true' ? ' class="quiz-correct"' : '';
return `<div${cls}>${marker} ${opt.label || ''}</div>`;
}).join('\n');
return `
<div class="exercise-box">
<div class="exercise-header">${icon('search', '🔍')} ${escapeHtml(title)}</div>
<div class="exercise-section"><strong>The Prompt:</strong></div>
<pre class="prompt-code">${protectedBadPrompt}</pre>
<div class="exercise-section"><strong>The Output (problematic):</strong></div>
<pre class="prompt-code prompt-code-error">${protectedBadOutput}</pre>
${hint ? `<p class="exercise-hint">${icon('tip', '💡')} Hint: ${escapeHtml(hint)}</p>` : ''}
<div class="exercise-section"><strong>What's wrong?</strong></div>
<div class="quiz-options">${optionsHtml}</div>
</div>
`;
});
// ============================================================
// PromptChallenge - Show task, criteria, and example solution
// ============================================================
result = result.replace(/<PromptChallenge\s+([\s\S]*?)\/>/g, (match) => {
const props = extractProps(match);
const title = props.title || 'Prompt Challenge';
const task = props.task || '';
const difficulty = props.difficulty || 'intermediate';
const exampleSolution = props.exampleSolution || '';
const criteria: string[] = [];
const criteriaMatch = match.match(/criteria=\{\[([\s\S]*?)\]\}/);
if (criteriaMatch) {
const strRegex = /"([^"]*?)"/g;
let cm;
while ((cm = strRegex.exec(criteriaMatch[1])) !== null) {
criteria.push(cm[1]);
}
}
const criteriaHtml = criteria.map(c => `<li>${icon('checkbox', '☐')} ${c}</li>`).join('\n');
return `
<div class="exercise-box">
<div class="exercise-header">${icon('trophy', '🏆')} ${escapeHtml(title)} <span class="difficulty-badge">${difficulty}</span></div>
<p>${escapeHtml(task)}</p>
${criteria.length > 0 ? `<div class="exercise-section"><strong>Criteria:</strong></div><ul>${criteriaHtml}</ul>` : ''}
${exampleSolution ? `<div class="exercise-section"><strong>Example Solution:</strong></div><pre class="prompt-code">${protectCodeBlock(escapeHtml(exampleSolution))}</pre>` : ''}
</div>
`;
});
// ============================================================
// BeforeAfterEditor - Show both prompts side by side
// ============================================================
result = result.replace(/<BeforeAfterEditor\s+([\s\S]*?)\/>/g, (match) => {
const props = extractProps(match);
const title = props.title || 'Before & After';
const badPrompt = props.badPrompt || '';
const idealPrompt = props.idealPrompt || '';
const task = props.task || '';
return `
<div class="exercise-box">
<div class="exercise-header">${icon('refresh', '🔄')} ${escapeHtml(title)}</div>
${task ? `<p>${escapeHtml(task)}</p>` : ''}
<div class="compare-box">
<div class="compare-item compare-before"><strong>Before:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(badPrompt))}</pre></div>
<div class="compare-item compare-after"><strong>After:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(idealPrompt))}</pre></div>
</div>
</div>
`;
});
// ============================================================
// DiffView - Show before/after comparison
// ============================================================
result = result.replace(/<DiffView\s+([\s\S]*?)\/>/g, (match) => {
const props = extractProps(match);
const beforeLabel = props.beforeLabel || 'Before';
const afterLabel = props.afterLabel || 'After';
const before = props.before || '';
const after = props.after || '';
return `
<div class="compare-box">
<div class="compare-item compare-before"><strong>${escapeHtml(beforeLabel)}:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(before))}</pre></div>
<div class="compare-item compare-after"><strong>${escapeHtml(afterLabel)}:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(after))}</pre></div>
</div>
`;
});
// ============================================================
// TokenizerDemo - Static example with colored tokens
// ============================================================
result = result.replace(/<TokenizerDemo\s*\/>/g, () => {
const sample = ld.tokenizer.samples[ld.tokenizer.default];
const example = sample?.text || 'Hello, world!';
const tokens = sample?.tokens || ['Hel', 'lo', ',', ' wor', 'ld', '!'];
const colors = ['#dbeafe', '#dcfce7', '#f3e8ff', '#fef3c7', '#fce7f3', '#cffafe'];
const tokensHtml = tokens.map((tk, i) =>
`<span style="background:${colors[i % colors.length]};padding:2px 6px;border-radius:3px;margin:2px;display:inline-block;font-family:var(--font-mono);font-size:8.5pt;">${tk === ' ' ? '␣' : escapeHtml(tk)}</span>`
).join('');
return `
<div class="demo-box">
<div class="demo-header">Tokenizer</div>
<p class="demo-label">Input: "${escapeHtml(example)}"</p>
<p class="demo-label">Tokens (${tokens.length}):</p>
<div style="margin:0.5em 0;">${tokensHtml}</div>
<p class="demo-note">${escapeHtml(ld.tokenizer.tryExamples)}</p>
</div>
`;
});
// ============================================================
// TokenPredictionDemo - Static next-token prediction
// ============================================================
result = result.replace(/<TokenPredictionDemo\s*\/>/g, () => {
const tp = ld.tokenPrediction;
const stepKeys = Object.keys(tp.predictions.steps);
const stepsHtml = stepKeys.slice(0, 3).map(key => {
const preds = tp.predictions.steps[key];
const predsHtml = preds.map(p => `<span class="prediction-token">${escapeHtml(p.token)} <span class="prediction-prob">${Math.round(p.probability * 100)}%</span></span>`).join(' ');
return `<div class="prediction-step"><div class="prediction-context">"${escapeHtml(key)} ▁▁▁"</div><div class="prediction-options">→ ${predsHtml}</div></div>`;
}).join('\n');
return `
<div class="demo-box">
<div class="demo-header">Next-Token Prediction</div>
<p class="demo-note">${escapeHtml(tp.fullText)}</p>
${stepsHtml}
</div>
`;
});
// ============================================================
// ContextWindowDemo - Static diagram
// ============================================================
result = result.replace(/<ContextWindowDemo\s*\/>/g, () => {
const cwLabel = t(msg, 'book.interactive.contextWindow') || 'Context Window';
const promptLabel = t(msg, 'book.interactive.prompt') || 'Prompt';
const responseLabel = t(msg, 'book.interactive.response') || 'Response';
const remainingLabel = t(msg, 'book.interactive.remaining') || 'Remaining';
const tipText = t(msg, 'book.interactive.contextTip') || '';
return `
<div class="demo-box">
<div class="demo-header">${escapeHtml(cwLabel)} — 8,000 tokens</div>
<table style="width:100%;border-collapse:collapse;font-size:8pt;margin:0.8em 0;">
<tr>
<td style="width:25%;padding:0.4em;border:1px solid #ccc;text-align:center;font-weight:600;">${escapeHtml(promptLabel)}<br/>2,000 tokens</td>
<td style="width:12.5%;padding:0.4em;border:1px solid #ccc;text-align:center;font-weight:600;">${escapeHtml(responseLabel)}<br/>1,000 tokens</td>
<td style="width:62.5%;padding:0.4em;border:1px solid #ccc;text-align:center;color:#666;">${escapeHtml(remainingLabel)} — 5,000 tokens</td>
</tr>
</table>
${tipText ? `<p class="demo-note">${escapeHtml(tipText)}</p>` : ''}
</div>
`;
});
// ============================================================
// TemperatureDemo - Show all temperature levels
// ============================================================
result = result.replace(/<TemperatureDemo\s*\/>/g, () => {
const te = ld.temperatureExamples;
const levels = [
{ temp: '0.00.2', label: t(msg, 'book.interactive.deterministic') || 'Deterministic', examples: te.lowTemp.slice(0, 2) },
{ temp: '0.50.7', label: t(msg, 'book.interactive.balanced') || 'Balanced', examples: te.mediumHighTemp.slice(0, 2) },
{ temp: '0.81.0', label: t(msg, 'book.interactive.veryCreative') || 'Creative', examples: te.highTemp.slice(0, 2) },
];
const levelsHtml = levels.map(l => `
<div class="temp-level">
<div class="temp-label"><strong>${l.temp}</strong> — ${escapeHtml(l.label)}</div>
<div class="temp-examples">${l.examples.map(e => `<div class="temp-example">"${escapeHtml(e)}"</div>`).join('')}</div>
</div>
`).join('');
return `
<div class="demo-box">
<div class="demo-header">${t(msg, 'book.interactive.temperatureDemo') || 'Temperature'}</div>
<p class="demo-note">${t(msg, 'book.interactive.prompt') || 'Prompt'}: "${escapeHtml(te.prompt)}"</p>
${levelsHtml}
</div>
`;
});
// ============================================================
// StructuredOutputDemo - Show all three formats
// ============================================================
result = result.replace(/<StructuredOutputDemo\s*\/>/g, () => {
const unstructured = 'Here are some popular programming languages: Python is great for data science and AI. JavaScript is used for web development. Rust is known for performance and safety.';
const json = `{
"languages": [
{ "name": "Python", "best_for": ["data science", "AI"], "difficulty": "easy" },
{ "name": "JavaScript", "best_for": ["web development"], "difficulty": "medium" },
{ "name": "Rust", "best_for": ["performance", "safety"], "difficulty": "hard" }
]
}`;
return `
<div class="demo-box">
<div class="demo-header">Structured Output Comparison</div>
<div class="demo-section"><strong>Unstructured:</strong></div>
<div class="demo-text">${escapeHtml(unstructured)}</div>
<div class="demo-section"><strong>Structured (JSON):</strong></div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(json))}</pre>
<p class="demo-note">Structured output allows programmatic parsing, comparison across queries, and integration into workflows.</p>
</div>
`;
});
// ============================================================
// FewShotDemo - Show progression of examples
// ============================================================
result = result.replace(/<FewShotDemo\s*\/>/g, () => {
return `
<div class="demo-box">
<div class="demo-header">Few-Shot Learning</div>
<p class="demo-note">More examples help the model understand the pattern:</p>
<table class="demo-table">
<thead><tr><th>Examples</th><th>Prediction</th><th>Confidence</th></tr></thead>
<tbody>
<tr><td>0 (zero-shot)</td><td>Positive ✗</td><td>45%</td></tr>
<tr><td>1 (one-shot)</td><td>Positive ✗</td><td>62%</td></tr>
<tr><td>2 (two-shot)</td><td>Mixed ✓</td><td>71%</td></tr>
<tr><td>3 (three-shot)</td><td>Mixed ✓</td><td>94%</td></tr>
</tbody>
</table>
<p class="demo-note">Test input: "Great quality but shipping was slow" → Expected: Mixed</p>
</div>
`;
});
// ============================================================
// JsonYamlDemo - Show all three formats
// ============================================================
result = result.replace(/<JsonYamlDemo\s*\/>/g, () => {
const ts = `interface ChatPersona {
name?: string;
role?: string;
tone?: PersonaTone | PersonaTone[];
expertise?: PersonaExpertise[];
}`;
const json = `{
"name": "CodeReviewer",
"role": "Senior Software Engineer",
"tone": ["professional", "analytical"],
"expertise": ["coding", "engineering"]
}`;
const yaml = `name: CodeReviewer
role: Senior Software Engineer
tone:
- professional
- analytical
expertise:
- coding
- engineering`;
return `
<div class="demo-box">
<div class="demo-header">Format Comparison: TypeScript / JSON / YAML</div>
<div class="demo-section"><strong>TypeScript (define schema):</strong></div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(ts))}</pre>
<div class="demo-section"><strong>JSON (APIs &amp; parsing):</strong></div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(json))}</pre>
<div class="demo-section"><strong>YAML (config files):</strong></div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(yaml))}</pre>
</div>
`;
});
// ============================================================
// IterativeRefinementDemo - Show all versions
// ============================================================
result = result.replace(/<IterativeRefinementDemo\s*\/>/g, () => {
const versions = [
{ v: 1, prompt: 'Write a product description.', output: 'This is a great product. It has many features. You should buy it.', quality: 20, issue: 'Too vague, no specific details' },
{ v: 2, prompt: 'Write a product description for wireless earbuds.', output: 'These wireless earbuds offer great sound quality and comfortable fit. They have long battery life.', quality: 45, issue: 'Better, but still generic' },
{ v: 3, prompt: 'Write a 50-word product description for premium wireless earbuds. Highlight: noise cancellation, 8-hour battery, water resistance.', output: 'Experience pure audio bliss with our premium wireless earbuds. Advanced noise cancellation blocks distractions while delivering crystal-clear sound.', quality: 72, issue: 'Good details, needs stronger hook' },
{ v: 4, prompt: 'Write a compelling 50-word product description for premium wireless earbuds.\nKey features: noise cancellation, 8-hour battery, IPX5\nTone: Premium but approachable\nStart with a benefit, end with a call to action.', output: 'Escape the noise and immerse yourself in studio-quality sound. Our premium wireless earbuds feature advanced noise cancellation, 8-hour battery life, and IPX5 water resistance.', quality: 95, issue: null },
];
const versionsHtml = versions.map(v => `
<div class="iteration-step">
<div class="iteration-header">Version ${v.v} — Quality: ${v.quality}%</div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(v.prompt))}</pre>
<div class="iteration-output">${escapeHtml(v.output)}</div>
${v.issue ? `<div class="iteration-issue">⚠ ${v.issue}</div>` : '<div class="iteration-success">✓ Strong prompt with clear structure</div>'}
</div>
`).join('');
return `
<div class="demo-box">
<div class="demo-header">Iterative Refinement</div>
<p class="demo-note">Watch how a prompt improves through successive iterations:</p>
${versionsHtml}
</div>
`;
});
// ============================================================
// CostCalculatorDemo - Static calculation example
// ============================================================
result = result.replace(/<CostCalculatorDemo\s*\/>/g, () => {
return `
<div class="demo-box">
<div class="demo-header">API Cost Calculator</div>
<table class="demo-table">
<thead><tr><th>Parameter</th><th>Value</th></tr></thead>
<tbody>
<tr><td>Input tokens per request</td><td>500</td></tr>
<tr><td>Output tokens per request</td><td>200</td></tr>
<tr><td>Input price</td><td>$0.15 / 1M tokens</td></tr>
<tr><td>Output price</td><td>$0.60 / 1M tokens</td></tr>
<tr><td>Requests per day</td><td>1,000</td></tr>
</tbody>
</table>
<div class="cost-results">
<div class="cost-item"><strong>Per request:</strong> $0.0002</div>
<div class="cost-item"><strong>Daily:</strong> $0.20</div>
<div class="cost-item"><strong>Monthly:</strong> $5.85</div>
</div>
<p class="demo-note" style="font-family:var(--font-mono);font-size:8pt;text-align:center;">(500 × $0.15/1M) + (200 × $0.60/1M) = $0.000195/request</p>
</div>
`;
});
// ============================================================
// EmbeddingsDemo - Show word vectors and similarity
// ============================================================
result = result.replace(/<EmbeddingsDemo\s*\/>/g, () => {
const words = ld.embeddingWords;
const rowsHtml = words.map(w =>
`<tr><td>${escapeHtml(w.word)}</td><td>[${w.vector.join(', ')}]</td><td>${escapeHtml(w.color)}</td></tr>`
).join('\n');
return `
<div class="demo-box">
<div class="demo-header">Word Embeddings</div>
<table class="demo-table">
<thead><tr><th>Word</th><th>Vector</th><th>Group</th></tr></thead>
<tbody>${rowsHtml}</tbody>
</table>
</div>
`;
});
// ============================================================
// LLMCapabilitiesDemo - Static capability list
// ============================================================
result = result.replace(/<LLMCapabilitiesDemo\s*\/>/g, () => {
const canDo = ld.capabilities.filter(c => c.canDo);
const cantDo = ld.capabilities.filter(c => !c.canDo);
return `
<div class="demo-box">
<div class="compare-box">
<div class="compare-item compare-after">
<strong>✓</strong>
<ul>${canDo.map(c => `<li><strong>${escapeHtml(c.title)}</strong> — ${escapeHtml(c.description)}</li>`).join('\n')}</ul>
</div>
<div class="compare-item compare-before">
<strong>✗</strong>
<ul>${cantDo.map(c => `<li><strong>${escapeHtml(c.title)}</strong> — ${escapeHtml(c.description)}</li>`).join('\n')}</ul>
</div>
</div>
</div>
`;
});
// ============================================================
// JailbreakDemo - Show attack/defense examples
// ============================================================
result = result.replace(/<JailbreakDemo\s*\/>/g, () => {
const examplesHtml = ld.jailbreakExamples.slice(0, 3).map(ex => `
<div class="jailbreak-example">
<div class="jailbreak-name"><strong>${escapeHtml(ex.name)}</strong> — ${escapeHtml(ex.description)}</div>
<div class="compare-box">
<div class="compare-item compare-after"><strong>${icon('shield', '🛡️')}</strong><br/>${escapeHtml(ex.systemPrompt)}</div>
<div class="compare-item compare-before"><strong>${icon('warning', '⚠️')}</strong><br/>${escapeHtml(ex.attack)}</div>
</div>
</div>
`).join('');
return `
<div class="demo-box">
${examplesHtml}
</div>
`;
});
// ============================================================
// Framework demos - CRISPE, BREAK, RTF with steps and example
// ============================================================
const fwColors = ['#dbeafe', '#dcfce7', '#f3e8ff', '#fef3c7', '#fce7f3', '#cffafe'];
const fwBorderColors = ['#93c5fd', '#86efac', '#c4b5fd', '#fcd34d', '#f9a8d4', '#67e8f9'];
const fwMap: Record<string, keyof typeof ld.frameworks> = {
CRISPEFramework: 'crispe',
BREAKFramework: 'break',
RTFFramework: 'rtf',
};
for (const [comp, fwKey] of Object.entries(fwMap)) {
const regex = new RegExp(`<${comp}\\s*/>`, 'g');
result = result.replace(regex, () => {
const fw = ld.frameworks[fwKey];
const stepsHtml = fw.steps.map((s, i) => `
<div class="fw-step">
<div class="fw-letter" style="background:${fwColors[i % fwColors.length]};border:1px solid ${fwBorderColors[i % fwBorderColors.length]};">${s.letter}</div>
<div class="fw-step-body">
<div class="fw-step-label"><strong>${escapeHtml(s.label)}</strong> — ${escapeHtml(s.description)}</div>
${s.example ? `<div class="fw-step-example">${escapeHtml(s.example)}</div>` : ''}
</div>
</div>
`).join('\n');
return `
<div class="demo-box">
<div class="demo-header">${escapeHtml(fw.name)}</div>
${stepsHtml}
<div class="demo-section"><strong>${labels.completePrompt}:</strong></div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(fw.examplePrompt))}</pre>
</div>
`;
});
}
// ============================================================
// PrinciplesSummary - Numbered principles list (from locale data)
// ============================================================
result = result.replace(/<PrinciplesSummary\s*\/>/g, () => {
const iconMap: Record<string, [string, string]> = {
Gem: ['gem', '💎'], Target: ['target', '🎯'], Crown: ['crown', '👑'], Compass: ['compass', '🧭'],
RefreshCw: ['refresh', '🔄'], Sparkles: ['sparkles', '✨'], Ruler: ['ruler', '📏'], CheckCircle: ['check', '✅'],
};
const itemsHtml = ld.principles.map(p => {
const ic = iconMap[p.iconName] || ['info', '•'];
return `<div class="principle-item"><span class="principle-icon">${icon(ic[0], ic[1])}</span><span><strong>${escapeHtml(p.title)}</strong> — ${escapeHtml(p.description)}</span></div>`;
}).join('\n');
return `
<div class="demo-box">
${itemsHtml}
</div>
`;
});
// ============================================================
// VersionDiff - Show versions with printable diff
// ============================================================
result = result.replace(/<VersionDiff\s+versions=\{\[([\s\S]*?)\]\}\s*\/>/g, (match, inner) => {
// Parse versions: { label: "...", content: `...`, note: "..." }
const versions: { label: string; content: string; note: string }[] = [];
const versionBlocks = inner.split(/\},\s*\{/).map((b: string) => b.replace(/^\{|\}$/g, ''));
for (const block of versionBlocks) {
const labelMatch = block.match(/label:\s*"([^"]*?)"/);
const noteMatch = block.match(/note:\s*"([^"]*?)"/);
// content can be quoted or template-literal
const contentQuoted = block.match(/content:\s*"([^"]*?)"/);
const contentBt = block.match(/content:\s*`([\s\S]*?)`/);
const content = contentBt ? contentBt[1] : (contentQuoted ? contentQuoted[1] : '');
if (labelMatch) {
versions.push({ label: labelMatch[1], content, note: noteMatch ? noteMatch[1] : '' });
}
}
if (versions.length === 0) return '';
const versionsHtml = versions.map((v, i) => `
<div class="version-block">
<div class="version-header">
<span class="version-label">${escapeHtml(v.label)}</span>
${v.note ? `<span class="version-note">${escapeHtml(v.note)}</span>` : ''}
</div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(v.content))}</pre>
</div>
`).join('\n');
return `
<div class="demo-box">
<div class="demo-header">Prompt Evolution</div>
${versionsHtml}
</div>
`;
});
// ============================================================
// ChainExample - Full chain with prompts and outputs per type
// ============================================================
result = result.replace(/<ChainExample\s*\n?\s*type="(\w+)"\s*\n?\s*steps=\{\[([\s\S]*?)\]\}\s*\n?\s*\/>/g, (match, type, stepsInner) => {
// Parse steps: { step: "...", prompt: "...", output: "..." }
const steps: { step: string; prompt: string; output: string }[] = [];
const stepRegex = /\{\s*step:\s*"([^"]*?)"\s*,\s*prompt:\s*"([^"]*?)"\s*,\s*output:\s*(?:'([^']*?)'|"([^"]*?)")/g;
let sm;
while ((sm = stepRegex.exec(stepsInner)) !== null) {
steps.push({ step: sm[1], prompt: sm[2], output: sm[3] || sm[4] || '' });
}
// If regex didn't match (complex multiline prompts), try a simpler approach
if (steps.length === 0) {
const blockRegex = /\{\s*step:\s*"([^"]*?)"/g;
let bm;
while ((bm = blockRegex.exec(stepsInner)) !== null) {
steps.push({ step: bm[1], prompt: '', output: '' });
}
}
const typeLabels: Record<string, string> = {
sequential: 'Sequential Chain',
parallel: 'Parallel Chain',
conditional: 'Conditional Chain',
iterative: 'Iterative Chain',
};
const typeIcons: Record<string, string> = {
sequential: '→',
parallel: '⇉',
conditional: '◇',
iterative: '↻',
};
const stepsHtml = steps.map((s, i) => {
const isSkipped = s.step.toLowerCase().includes('skipped');
const stepClass = isSkipped ? 'chain-step-skipped' : 'chain-step-item';
const connector = type === 'parallel' && i > 0 && i < steps.length - 1
? 'chain-connector-parallel'
: i < steps.length - 1 ? 'chain-connector' : '';
return `
<div class="${stepClass}">
<div class="chain-step-num">${isSkipped ? '✗' : (i + 1)}</div>
<div class="chain-step-body">
<div class="chain-step-name">${escapeHtml(s.step)}</div>
${!isSkipped && s.prompt ? `<div class="chain-step-prompt"><span class="chain-label">Prompt:</span> ${protectCodeBlock(escapeHtml(s.prompt))}</div>` : ''}
${!isSkipped && s.output ? `<div class="chain-step-output"><span class="chain-label">Output:</span> ${protectCodeBlock(escapeHtml(s.output))}</div>` : ''}
${isSkipped ? '<div class="chain-step-skipped-note">Skipped — condition not met</div>' : ''}
</div>
</div>${connector ? `<div class="${connector}"></div>` : ''}`;
}).join('\n');
const loopNote = type === 'iterative' ? '<div class="chain-loop-note">↻ Loop until quality threshold is met</div>' : '';
return `
<div class="chain-box chain-${type}">
<div class="chain-box-header">${typeIcons[type] || '→'} ${typeLabels[type] || type}</div>
${stepsHtml}
${loopNote}
</div>
`;
});
// ============================================================
// TextToImageDemo - Show prompt anatomy with all categories
// ============================================================
result = result.replace(/<TextToImageDemo\s*\/>/g, () => {
const imgOpts = ld.imagePromptOptions;
const imgLabels = ld.imageCategoryLabels;
const catColors: Record<string, string> = { subject: '#dbeafe', style: '#f3e8ff', lighting: '#fef3c7', composition: '#dcfce7', mood: '#fce7f3' };
const categories = Object.keys(imgOpts).map(key => ({
label: imgLabels[key] || key,
options: imgOpts[key],
color: catColors[key] || '#f5f5f4',
}));
const categoriesHtml = categories.map(cat => {
const optionsHtml = cat.options.map((opt, i) => {
const style = i === 0 ? ` style="background:${cat.color};border:1px solid #ccc;font-weight:600;"` : '';
return `<span class="image-option"${style}>${opt}</span>`;
}).join(' ');
return `<div class="image-category"><span class="image-cat-label" style="color:#555;">${cat.label}:</span> ${optionsHtml}</div>`;
}).join('\n');
const examplePrompts = [
{ prompt: 'a cat, photorealistic, golden hour, close-up portrait, peaceful', note: 'Realistic pet photography feel' },
{ prompt: 'a castle, oil painting, dramatic shadows, wide landscape, mysterious', note: 'Dark fantasy atmosphere' },
{ prompt: 'an astronaut, 3D render, neon glow, symmetrical, energetic', note: 'Sci-fi poster style' },
];
const examplesHtml = examplePrompts.map(ex => `
<div class="image-example">
<pre class="prompt-code">${protectCodeBlock(escapeHtml(ex.prompt))}</pre>
<p class="demo-note">${ex.note}</p>
</div>
`).join('');
const diffusionSteps = [
'1. Parse prompt → identify subject, style, and modifiers',
'2. Start with random noise (pure static)',
'3. Denoise step 1 → rough shapes emerge',
'4. Denoise step 2 → details and colors form',
'5. Denoise step 3 → final refinement and sharpness',
];
return `
<div class="demo-box">
<div class="demo-header">${icon('palette', '🎨')} ${t(msg, 'book.interactive.textToImageBuildPrompt') || 'Text-to-Image: Building an Image Prompt'}</div>
<p class="demo-note">Image generation prompts combine categories. Select one option from each row to build a complete prompt:</p>
${categoriesHtml}
<div class="demo-section"><strong>Example prompts built from these categories:</strong></div>
${examplesHtml}
<div class="demo-section"><strong>How Diffusion Models Work:</strong></div>
<div class="diffusion-steps">
${diffusionSteps.map(s => `<div class="diffusion-step">${s}</div>`).join('\n')}
</div>
<p class="demo-note">The model starts with random noise and gradually removes it, guided by your text prompt, until a coherent image forms. More specific prompts give the model stronger guidance at each step.</p>
</div>
`;
});
// ============================================================
// TextToVideoDemo - Show prompt anatomy for video
// ============================================================
result = result.replace(/<TextToVideoDemo\s*\/>/g, () => {
const vidOpts = ld.videoPromptOptions;
const vidLabels = ld.videoCategoryLabels;
const vidColors: Record<string, string> = { subject: '#dbeafe', action: '#dcfce7', camera: '#f3e8ff', duration: '#fef3c7' };
const categories = Object.keys(vidOpts).map(key => ({
label: vidLabels[key] || key,
options: vidOpts[key],
color: vidColors[key] || '#f5f5f4',
}));
const categoriesHtml = categories.map(cat => {
const optionsHtml = cat.options.map((opt, i) => {
const style = i === 0 ? ` style="background:${cat.color};border:1px solid #ccc;font-weight:600;"` : '';
return `<span class="image-option"${style}>${opt}</span>`;
}).join(' ');
return `<div class="image-category"><span class="image-cat-label" style="color:#555;">${cat.label}:</span> ${optionsHtml}</div>`;
}).join('\n');
const examplePrompts = [
{ prompt: 'A bird takes flight, slow pan left, 4 seconds', note: 'Nature documentary style' },
{ prompt: 'A wave crashes on rocks, static shot, 6 seconds', note: 'Dramatic landscape footage' },
{ prompt: 'A flower blooms in timelapse, dolly zoom, 8 seconds', note: 'Macro nature timelapse' },
];
const examplesHtml = examplePrompts.map(ex => `
<div class="image-example">
<pre class="prompt-code">${protectCodeBlock(escapeHtml(ex.prompt))}</pre>
<p class="demo-note">${ex.note}</p>
</div>
`).join('');
return `
<div class="demo-box">
<div class="demo-header">${icon('video', '🎬')} ${t(msg, 'book.interactive.textToVideoBuildPrompt') || 'Text-to-Video: Building a Video Prompt'}</div>
<p class="demo-note">Video prompts need subject, action, camera movement, and duration. Select one from each row:</p>
${categoriesHtml}
<div class="demo-section"><strong>Example prompts:</strong></div>
${examplesHtml}
<div class="demo-section"><strong>Key challenges for video models:</strong></div>
<ul style="font-size:9pt;margin:0.5em 0 0.5em 1.5em;">
<li><strong>Temporal consistency</strong> — keeping the subject looking the same across frames</li>
<li><strong>Natural motion</strong> — realistic movement physics and speed</li>
<li><strong>Camera coherence</strong> — smooth, intentional camera movement</li>
</ul>
</div>
`;
});
// ============================================================
// PromptBreakdown - Color-labeled prompt segments
// ============================================================
result = result.replace(/<PromptBreakdown\s+parts=\{\[([\s\S]*?)\]\}\s*\/>/g, (match, inner) => {
const colorValues: Record<string, string> = {
blue: '#dbeafe', green: '#dcfce7', purple: '#f3e8ff',
amber: '#fef3c7', pink: '#fce7f3', cyan: '#cffafe',
};
const borderColors: Record<string, string> = {
blue: '#93c5fd', green: '#86efac', purple: '#c4b5fd',
amber: '#fcd34d', pink: '#f9a8d4', cyan: '#67e8f9',
};
const defaultColorOrder = ['blue', 'green', 'purple', 'amber', 'pink', 'cyan'];
// Parse parts
const parts: { label: string; text: string; color: string }[] = [];
const partRegex = /\{\s*label:\s*"([^"]*?)"\s*,\s*text:\s*"([^"]*?)"(?:\s*,\s*color:\s*"([^"]*?)")?\s*\}/g;
let pm;
while ((pm = partRegex.exec(inner)) !== null) {
parts.push({ label: pm[1], text: pm[2], color: pm[3] || defaultColorOrder[parts.length % defaultColorOrder.length] });
}
if (parts.length === 0) return '';
const segmentsHtml = parts.map(p => {
const bg = colorValues[p.color] || colorValues.blue;
const border = borderColors[p.color] || borderColors.blue;
return `<span class="pb-segment"><span class="pb-label" style="color:${border};">${escapeHtml(p.label)}</span><span class="pb-text" style="border-bottom:2px solid ${border};background:${bg};">${escapeHtml(p.text)}</span></span>`;
}).join(' ');
return `
<div class="prompt-breakdown">
${segmentsHtml}
</div>
`;
});
// ============================================================
// SpecificitySpectrum - Show all levels from vague to specific
// ============================================================
result = result.replace(/<SpecificitySpectrum\s+levels=\{\[([\s\S]*?)\]\}\s*\/>/g, (match, inner) => {
const levels: { level: string; text: string }[] = [];
const levelRegex = /\{\s*level:\s*"([^"]*?)"\s*,\s*text:\s*"([^"]*?)"\s*\}/g;
let lm;
while ((lm = levelRegex.exec(inner)) !== null) {
levels.push({ level: lm[1], text: lm[2] });
}
if (levels.length === 0) return '';
const barColors = ['#ef4444', '#f97316', '#f59e0b', '#22c55e'];
const levelsHtml = levels.map((l, i) => {
const width = ((i + 1) / levels.length * 100).toFixed(0);
const color = barColors[i] || barColors[barColors.length - 1];
return `
<div class="spectrum-level">
<div class="spectrum-header">
<span class="spectrum-badge" style="background:${color};color:white;">${escapeHtml(l.level)}</span>
<span class="spectrum-bar-wrap"><span class="spectrum-bar" style="width:${width}%;background:${color};"></span></span>
</div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(l.text))}</pre>
</div>`;
}).join('\n');
return `
<div class="demo-box">
<div class="demo-header">Specificity Spectrum</div>
${levelsHtml}
</div>
`;
});
// ============================================================
// ChainFlowDemo - Static overview of all 4 chain types
// ============================================================
result = result.replace(/<ChainFlowDemo\s*\/>/g, () => {
const ct = ld.chainTypes;
const seq = ct.find(c => c.id === 'sequential') || ct[0];
const par = ct.find(c => c.id === 'parallel') || ct[1];
const cond = ct.find(c => c.id === 'conditional') || ct[2];
const iter = ct.find(c => c.id === 'iterative') || ct[3];
return `
<div class="demo-box">
<div class="chain-types-grid">
<div class="chain-type-card">
<div class="chain-type-name" style="color:#2563eb;">${escapeHtml(seq.name)}</div>
<div class="chain-type-desc">${escapeHtml(seq.description)}</div>
<div class="chain-type-diagram">
<span class="chain-type-step" style="background:#dbeafe;border-color:#93c5fd;">Extract</span>
<span class="chain-type-arrow">→</span>
<span class="chain-type-step" style="background:#dbeafe;border-color:#93c5fd;">Analyze</span>
<span class="chain-type-arrow">→</span>
<span class="chain-type-step" style="background:#dbeafe;border-color:#93c5fd;">Generate</span>
</div>
</div>
<div class="chain-type-card">
<div class="chain-type-name" style="color:#7c3aed;">${escapeHtml(par.name)}</div>
<div class="chain-type-desc">${escapeHtml(par.description)}</div>
<div class="chain-type-diagram chain-type-diagram-parallel">
<div style="text-align:center;"><span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Input</span></div>
<div class="chain-type-arrow">↓</div>
<div style="display:flex;justify-content:center;gap:0.3em;">
<span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Sentiment</span>
<span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Entities</span>
<span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Topics</span>
</div>
<div class="chain-type-arrow">↓</div>
<div style="text-align:center;"><span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Merge</span></div>
</div>
</div>
<div class="chain-type-card">
<div class="chain-type-name" style="color:#d97706;">${escapeHtml(cond.name)}</div>
<div class="chain-type-desc">${escapeHtml(cond.description)}</div>
<div class="chain-type-diagram chain-type-diagram-parallel">
<div style="text-align:center;"><span class="chain-type-step" style="background:#fef3c7;border-color:#fcd34d;">Classify</span></div>
<div style="display:flex;justify-content:center;gap:1em;">
<div style="text-align:center;"><div class="chain-type-arrow">↙</div><span class="chain-type-step" style="background:#fef3c7;border-color:#fcd34d;">If complaint</span></div>
<div style="text-align:center;"><div class="chain-type-arrow">↘</div><span class="chain-type-step" style="background:#fef3c7;border-color:#fcd34d;">If question</span></div>
</div>
</div>
</div>
<div class="chain-type-card">
<div class="chain-type-name" style="color:#16a34a;">${escapeHtml(iter.name)}</div>
<div class="chain-type-desc">${escapeHtml(iter.description)}</div>
<div class="chain-type-diagram">
<span class="chain-type-step" style="background:#dcfce7;border-color:#86efac;">Generate</span>
<span class="chain-type-arrow">→</span>
<span class="chain-type-step" style="background:#dcfce7;border-color:#86efac;">Evaluate</span>
<span class="chain-type-arrow">→</span>
<span class="chain-type-step" style="background:#dcfce7;border-color:#86efac;">Refine</span>
<span class="chain-type-arrow" style="font-size:10pt;">↻</span>
</div>
</div>
</div>
</div>
`;
});
// ============================================================
// CodeEditor - Static code block with filename header
// ============================================================
result = result.replace(/<CodeEditor\s*\n?\s*([\s\S]*?)\/>/g, (match, attrs) => {
const props = extractProps(match);
const lang = props.language || '';
const filename = props.filename || '';
// Extract code from code={`...`}
const codeMatch = match.match(/code=\{`([\s\S]*?)`\}/);
const code = codeMatch ? codeMatch[1] : '';
if (!code) return '';
return `
<div class="code-editor-box">
<div class="code-editor-header">
<span class="code-editor-dots"><span style="background:#ff5f56;"></span><span style="background:#ffbd2e;"></span><span style="background:#27c93f;"></span></span>
${filename ? `<span class="code-editor-filename">${escapeHtml(filename)}</span>` : ''}
<span class="code-editor-lang">${escapeHtml(lang)}</span>
</div>
<pre class="prompt-code">${protectCodeBlock(escapeHtml(code))}</pre>
</div>
`;
});
// ============================================================
// ChainErrorDemo - Show all 3 scenarios statically
// ============================================================
result = result.replace(/<ChainErrorDemo\s*\/>/g, () => {
const scenarios = ld.scenarios;
const steps = ld.steps;
const stepsStr = steps.map(s => escapeHtml(s.name)).join(' → ');
return `
<div class="demo-box">
<div class="chain-types-grid">
${scenarios.map(sc => `
<div class="chain-type-card">
<div class="chain-type-name">${escapeHtml(sc.name)}</div>
<div class="chain-type-desc">${escapeHtml(sc.description)}</div>
<div style="font-size:8pt;font-family:var(--font-mono);color:#666;">${stepsStr}</div>
</div>`).join('\n')}
</div>
</div>
`;
});
// ============================================================
// ValidationDemo - Show validation flow
// ============================================================
result = result.replace(/<ValidationDemo\s*\/>/g, () => {
const vd = ld.validationDemo;
return `
<div class="demo-box">
<div class="demo-header">${escapeHtml(vd.title)}</div>
<div class="chain-types-grid">
<div class="chain-type-card">
<div class="chain-type-name" style="color:#d97706;">${escapeHtml(vd.invalidRetry)}</div>
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
${vd.steps.map((s, i) => `${i + 1}. ${escapeHtml(s.name)}`).join('<br/>')}
<br/>✗ ${escapeHtml(vd.outputs.ageMustBeNumber)}<br/>↻ ${escapeHtml(vd.outputs.retryingWithFeedback)}<br/>✓ ${escapeHtml(vd.outputs.allFieldsValid)}<br/>✓ ${escapeHtml(vd.outputs.dataProcessedSuccessfully)}
</div>
</div>
<div class="chain-type-card">
<div class="chain-type-name" style="color:#16a34a;">${escapeHtml(vd.validData)}</div>
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
${vd.steps.map((s, i) => `${i + 1}. ${escapeHtml(s.name)}`).join('<br/>')}
<br/>✓ ${escapeHtml(vd.outputs.allFieldsValid)}<br/>✓ ${escapeHtml(vd.outputs.dataProcessedSuccessfully)}
</div>
</div>
</div>
</div>
`;
});
// ============================================================
// FallbackDemo - Show primary vs fallback paths
// ============================================================
result = result.replace(/<FallbackDemo\s*\/>/g, () => {
const fd = ld.fallbackDemo;
return `
<div class="demo-box">
<div class="demo-header">${escapeHtml(fd.title)}</div>
<div class="chain-types-grid">
<div class="chain-type-card">
<div class="chain-type-name" style="color:#16a34a;">${escapeHtml(fd.primarySucceeds)}</div>
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
${escapeHtml(fd.steps[0].name)} → ✓<br/>
${escapeHtml(fd.outputs.deepAnalysisComplete)}<br/>
${escapeHtml(fd.outputs.resultFromPrimary)}
</div>
</div>
<div class="chain-type-card">
<div class="chain-type-name" style="color:#7c3aed;">${escapeHtml(fd.useFallback)}</div>
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
${escapeHtml(fd.steps[0].name)} → ✗<br/>
${escapeHtml(fd.steps[1].name)} → ✓<br/>
${escapeHtml(fd.outputs.resultFromFallback)}
</div>
</div>
</div>
</div>
`;
});
// ============================================================
// ContentPipelineDemo - Show pipeline steps with prompts
// ============================================================
result = result.replace(/<ContentPipelineDemo\s*\/>/g, () => {
const cpd = ld.contentPipelineDemo;
const stepsHtml = cpd.steps.map((s, i) => {
const prompt = cpd.prompts[s.id] || '';
const outputKey = s.id as keyof typeof cpd.outputs;
const output = cpd.outputs[outputKey] || '';
return `
<div class="chain-step-item">
<div class="chain-step-num">${i + 1}</div>
<div class="chain-step-body">
<div class="chain-step-name">${escapeHtml(s.name)}</div>
${prompt && s.id !== 'input' ? `<div class="chain-step-prompt"><span class="chain-label">${labels.prompt}:</span> ${protectCodeBlock(escapeHtml(prompt))}</div>` : ''}
${output ? `<div class="chain-step-output"><span class="chain-label">${labels.output}:</span> ${escapeHtml(output)}</div>` : ''}
</div>
</div>${i < cpd.steps.length - 1 ? '<div class="chain-connector"></div>' : ''}`;
}).join('\n');
return `
<div class="chain-box chain-sequential">
<div class="chain-box-header">→ ${escapeHtml(cpd.title)}</div>
${stepsHtml}
</div>
`;
});
// ============================================================
// SummarizationDemo - Show all 4 strategies
// ============================================================
result = result.replace(/<SummarizationDemo\s*\/>/g, () => {
const html = ld.strategies.map(s => `
<div class="chain-type-card">
<div class="chain-type-name">${escapeHtml(s.name)}</div>
<div class="chain-type-desc">${escapeHtml(s.description)}</div>
${s.summary ? `<div style="font-size:8pt;font-family:var(--font-mono);color:#666;margin-top:0.3em;">${escapeHtml(s.summary)}</div>` : ''}
</div>
`).join('');
return `
<div class="demo-box">
<div class="chain-types-grid">${html}</div>
</div>
`;
});
// ============================================================
// ContextPlayground - Show context blocks with token counts
// ============================================================
result = result.replace(/<ContextPlayground\s*\/>/g, () => {
const blocks = ld.contextBlocks;
const total = blocks.filter(b => b.enabled).reduce((s, b) => s + b.tokens, 0);
const html = blocks.map(b => `
<div class="context-block ${b.enabled ? 'context-block-on' : 'context-block-off'}">
<div style="display:flex;justify-content:space-between;margin-bottom:0.2em;">
<span style="font-weight:600;">${b.enabled ? '✓' : '○'} ${escapeHtml(b.label)}</span>
<span style="color:#78716c;">${b.tokens} tokens</span>
</div>
<div style="font-family:var(--font-mono);font-size:7.5pt;color:#78716c;">${escapeHtml(b.content)}</div>
</div>
`).join('');
return `
<div class="demo-box">
<div class="demo-header">Context — ${total} / 200 tokens</div>
${html}
</div>
`;
});
// ============================================================
// PromptBuilder - Static template with writable fields
// ============================================================
result = result.replace(/<PromptBuilder\s*[^>]*\/>/g, (match) => {
const props = extractProps(match);
const title = props.title || 'Prompt Builder';
const fields = ld.builderFields.map(f => ({
label: f.label,
placeholder: f.placeholder,
hint: f.hint,
required: f.required || false,
}));
const fieldsHtml = fields.map(f => `
<div class="builder-field">
<div class="builder-field-label">${f.label}${f.required ? ' *' : ''}</div>
<div class="builder-field-hint">${f.hint}</div>
<div class="builder-field-input">${escapeHtml(f.placeholder)}</div>
</div>
`).join('');
return `
<div class="demo-box">
<div class="demo-header">${icon('pencil', '✏️')} ${escapeHtml(title)}</div>
<p class="demo-note">Fill in the fields below to construct your prompt. Not all fields are required — use what fits your task.</p>
${fieldsHtml}
</div>
`;
});
// ============================================================
// FrameworkDemo (generic, with props) — fallback
// ============================================================
const simpleStaticDemos: string[] = [];
for (const component of simpleStaticDemos) {
const selfClosingRegex = new RegExp(`<${component}\\s*[^>]*/>`, 'g');
result = result.replace(selfClosingRegex, `<p class="interactive-notice">${notice}</p>`);
const withContentRegex = new RegExp(`<${component}[^>]*>[\\s\\S]*?</${component}>`, 'g');
result = result.replace(withContentRegex, `<p class="interactive-notice">${notice}</p>`);
}
// ============================================================
// Truly interactive-only components → notice
// ============================================================
for (const component of INTERACTIVE_ONLY_COMPONENTS) {
const selfClosingRegex = new RegExp(`<${component}\\s*[^>]*/>`, 'g');
result = result.replace(selfClosingRegex, `<p class="interactive-notice">${notice}</p>`);
const withContentRegex = new RegExp(`<${component}[^>]*>[\\s\\S]*?</${component}>`, 'g');
result = result.replace(withContentRegex, `<p class="interactive-notice">${notice}</p>`);
}
// ============================================================
// Collapsible - render open
// ============================================================
result = result.replace(/<Collapsible\s+([\s\S]*?)>([\s\S]*?)<\/Collapsible>/g, (match, attrs, children) => {
const props = extractProps(attrs);
const title = props.title || '';
return `
<div class="callout callout-info">
<div class="callout-header">${escapeHtml(title)}</div>
<div class="callout-content">${children}</div>
</div>
`;
});
// ============================================================
// Multi-Agent Systems diagram
// ============================================================
result = result.replace(/<div className="my-6 p-6 bg-muted\/30 rounded-lg">\s*<div className="flex flex-col md:flex-row items-center justify-center gap-6">[\s\S]*?Coordinator[\s\S]*?Coder[\s\S]*?<\/div>\s*<\/div>/g, () => {
return `
<div class="demo-box" style="text-align:center;page-break-inside:avoid;">
<div class="demo-header">Multi-Agent System</div>
<div style="display:flex;align-items:center;justify-content:center;gap:1.5em;flex-wrap:wrap;margin:1em 0;">
<div style="padding:0.6em 1.2em;border:2px solid #999;border-radius:8px;font-family:var(--font-sans);font-weight:700;font-size:10pt;">
Coordinator<br/><span style="font-size:7pt;font-weight:400;color:#666;">Manages workflow</span>
</div>
<div style="font-size:16pt;color:#999;">&#x27F7;</div>
<div style="display:flex;gap:0.6em;flex-wrap:wrap;justify-content:center;">
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Researcher</div>
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Writer</div>
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Critic</div>
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Coder</div>
</div>
</div>
<p class="demo-note">Each agent has its own system prompt. The coordinator orchestrates their collaboration through structured messages.</p>
</div>
`;
});
// ============================================================
// Prompt Orchestration pipeline diagram
// ============================================================
result = result.replace(/<div className="my-6 flex flex-col items-center gap-2 p-6 bg-muted\/30 rounded-lg">\s*<div className="px-6 py-3 bg-slate[\s\S]*?User Request[\s\S]*?Final Output[\s\S]*?<\/div>\s*<\/div>/g, () => {
const steps = [
{ name: 'User Request', desc: '' },
{ name: 'Planner Agent', desc: 'Breaks down task' },
{ name: 'Researcher Agent', desc: 'Gathers information' },
{ name: 'Writer Agent', desc: 'Creates content' },
{ name: 'Reviewer Agent', desc: 'Quality checks' },
{ name: 'Final Output', desc: '' },
];
const stepsHtml = steps.map((s, i) => {
const isEndpoint = i === 0 || i === steps.length - 1;
const border = isEndpoint ? '2px solid #999' : '1px solid #ccc';
const weight = isEndpoint ? '700' : '600';
const arrow = i < steps.length - 1 ? '<div style="font-size:12pt;color:#999;margin:0.15em 0;">↓</div>' : '';
return `
<div style="padding:0.5em 1.5em;border:${border};border-radius:6px;font-family:var(--font-sans);font-size:9pt;font-weight:${weight};text-align:center;">
${s.name}${s.desc ? `<br/><span style="font-size:7pt;font-weight:400;color:#666;">${s.desc}</span>` : ''}
</div>
${arrow}`;
}).join('\n');
return `
<div class="demo-box" style="page-break-inside:avoid;">
<div class="demo-header">Prompt Orchestration Pipeline</div>
<div style="display:flex;flex-direction:column;align-items:center;margin:0.8em 0;">
${stepsHtml}
</div>
</div>
`;
});
// ============================================================
// Agent → Skills → Prompts hierarchy diagram
// ============================================================
result = result.replace(/<div className="my-8 p-6 bg-muted\/20 rounded-xl border">\s*<div className="flex flex-col items-center gap-6">[\s\S]*?Prompts are atoms[\s\S]*?<\/div>\s*<\/div>/g, () => {
return `
<div class="demo-box" style="text-align:center;">
<div class="demo-header">Agent → Skills → Prompts</div>
<div style="margin:1em 0;">
<div style="display:inline-block;padding:0.6em 1.5em;background:#dbeafe;border:2px solid #93c5fd;border-radius:50px;font-family:var(--font-sans);font-weight:700;font-size:11pt;">Agent</div>
<div style="font-size:8pt;color:#78716c;margin:0.2em 0;">Autonomous AI system</div>
</div>
<div style="font-size:9pt;color:#78716c;">powered by ↓</div>
<div style="display:flex;justify-content:center;gap:1em;margin:0.8em 0;">
<div style="display:inline-block;padding:0.5em 1.2em;background:#f3e8ff;border:2px solid #c4b5fd;border-radius:8px;font-family:var(--font-sans);font-weight:600;font-size:9pt;">Skill</div>
<div style="display:inline-block;padding:0.5em 1.2em;background:#f3e8ff;border:2px solid #c4b5fd;border-radius:8px;font-family:var(--font-sans);font-weight:600;font-size:9pt;">Skill</div>
<div style="display:inline-block;padding:0.5em 1.2em;background:#f3e8ff;border:2px solid #c4b5fd;border-radius:8px;font-family:var(--font-sans);font-weight:600;font-size:9pt;">Skill</div>
</div>
<div style="font-size:8pt;color:#78716c;margin:0.2em 0;">Reusable expertise packages</div>
<div style="font-size:9pt;color:#78716c;">composed of ↓</div>
<div style="display:flex;justify-content:center;gap:0.5em;margin:0.8em 0;flex-wrap:wrap;">
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
</div>
<div style="font-size:8pt;color:#78716c;font-style:italic;margin-top:0.8em;">Prompts are atoms → Skills are molecules → Agents are complete structures</div>
</div>
`;
});
// ============================================================
// Cleanup
// ============================================================
// Remove Icon components - they're decorative
result = result.replace(/<Icon[A-Za-z]+\s*[^>]*\/>/g, '');
// Clean up React className to class
result = result.replace(/className=/g, 'class=');
// Remove JSX expressions that won't render
result = result.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
// Clean up any remaining unknown self-closing components
result = result.replace(/<[A-Z][a-zA-Z]+\s*[^>]*\/>/g, '');
// Clean up any remaining unknown components with children
result = result.replace(/<[A-Z][a-zA-Z]+[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]+>/g, '');
// ============================================================
// Convert Tailwind className divs to inline styles for PDF
// ============================================================
result = convertTailwindToInline(result);
return result;
}
/**
* Convert common Tailwind class patterns in raw HTML blocks to inline styles
* so they render correctly in the PDF without a Tailwind runtime.
*/
function convertTailwindToInline(html: string): string {
// Map of Tailwind class → CSS property
const tw: Record<string, string> = {
// Display & Flex
'flex': 'display:flex;',
'grid': 'display:grid;',
'inline-block': 'display:inline-block;',
'inline-flex': 'display:inline-flex;',
'hidden': 'display:none;',
'block': 'display:block;',
'flex-col': 'flex-direction:column;',
'flex-row': 'flex-direction:row;',
'flex-wrap': 'flex-wrap:wrap;',
'flex-1': 'flex:1 1 0%;',
'items-center': 'align-items:center;',
'items-start': 'align-items:flex-start;',
'justify-center': 'justify-content:center;',
'justify-between': 'justify-content:space-between;',
'text-center': 'text-align:center;',
'text-left': 'text-align:left;',
'text-right': 'text-align:right;',
'shrink-0': 'flex-shrink:0;',
// Gaps
'gap-1': 'gap:0.25em;', 'gap-2': 'gap:0.5em;', 'gap-3': 'gap:0.75em;', 'gap-4': 'gap:1em;', 'gap-6': 'gap:1.5em;', 'gap-8': 'gap:2em;',
// Padding
'p-2': 'padding:0.5em;', 'p-3': 'padding:0.75em;', 'p-4': 'padding:1em;', 'p-6': 'padding:1.5em;',
'px-2': 'padding-left:0.5em;padding-right:0.5em;', 'px-3': 'padding-left:0.75em;padding-right:0.75em;', 'px-4': 'padding-left:1em;padding-right:1em;',
'py-2': 'padding-top:0.5em;padding-bottom:0.5em;', 'py-3': 'padding-top:0.75em;padding-bottom:0.75em;',
'pt-2': 'padding-top:0.5em;', 'pt-3': 'padding-top:0.75em;',
// Margin
'm-0!': 'margin:0!important;', 'mt-1': 'margin-top:0.25em;', 'mt-2': 'margin-top:0.5em;', 'mb-1': 'margin-bottom:0.25em;', 'mb-2': 'margin-bottom:0.5em;', 'mb-3': 'margin-bottom:0.75em;', 'mb-4': 'margin-bottom:1em;', 'mb-8': 'margin-bottom:2em;',
'my-4': 'margin-top:1em;margin-bottom:1em;', 'my-6': 'margin-top:1.5em;margin-bottom:1.5em;',
// Sizing
'w-full': 'width:100%;', 'w-24': 'width:6rem;', 'w-px': 'width:1px;', 'w-20': 'width:5rem;',
'min-w-24': 'min-width:6rem;', 'min-w-20': 'min-width:5rem;',
'h-4': 'height:1rem;', 'w-4': 'width:1rem;',
'h-5': 'height:1.25rem;', 'w-5': 'width:1.25rem;',
'w-32': 'width:3rem;', 'h-32': 'height:3rem;',
// Typography
'text-xs': 'font-size:8pt;', 'text-sm': 'font-size:9pt;', 'text-lg': 'font-size:12pt;', 'text-2xl': 'font-size:16pt;',
'font-medium': 'font-weight:500;', 'font-semibold': 'font-weight:600;', 'font-bold': 'font-weight:700;',
'capitalize': 'text-transform:capitalize;',
'whitespace-pre-wrap': 'white-space:pre-wrap;',
'font-mono': 'font-family:var(--font-mono);',
'leading-relaxed': 'line-height:1.65;',
'line-through': 'text-decoration:line-through;',
// Borders & Radius
'border': PRINT_READY ? 'border:1px solid #ccc;' : 'border:1px solid #e7e5e4;',
'border-t': PRINT_READY ? 'border-top:1px solid #ccc;' : 'border-top:1px solid #e7e5e4;',
'border-b': PRINT_READY ? 'border-bottom:1px solid #ccc;' : 'border-bottom:1px solid #e7e5e4;',
'rounded': 'border-radius:4px;', 'rounded-lg': 'border-radius:6px;', 'rounded-full': 'border-radius:9999px;',
'overflow-hidden': 'overflow:hidden;', 'overflow-x-auto': 'overflow-x:auto;',
// Spacing between children
'space-y-1': '', 'space-y-2': '', 'space-y-4': '',
// Colors — grayscale for print, RGB for screen
'text-muted-foreground': PRINT_READY ? 'color:#666;' : 'color:#78716c;',
'text-blue-700': PRINT_READY ? 'color:#333;' : 'color:#1d4ed8;',
'text-blue-600': PRINT_READY ? 'color:#333;' : 'color:#2563eb;',
'text-blue-400': PRINT_READY ? 'color:#666;' : 'color:#60a5fa;',
'text-green-700': PRINT_READY ? 'color:#333;' : 'color:#15803d;',
'text-green-600': PRINT_READY ? 'color:#333;' : 'color:#16a34a;',
'text-green-400': PRINT_READY ? 'color:#666;' : 'color:#4ade80;',
'text-purple-700': PRINT_READY ? 'color:#333;' : 'color:#7e22ce;',
'text-purple-600': PRINT_READY ? 'color:#333;' : 'color:#9333ea;',
'text-purple-400': PRINT_READY ? 'color:#666;' : 'color:#c084fc;',
'text-red-700': PRINT_READY ? 'color:#333;' : 'color:#b91c1c;',
'text-red-600': PRINT_READY ? 'color:#333;' : 'color:#dc2626;',
'text-red-400': PRINT_READY ? 'color:#666;' : 'color:#f87171;',
'text-amber-700': PRINT_READY ? 'color:#333;' : 'color:#b45309;',
'text-amber-400': PRINT_READY ? 'color:#666;' : 'color:#fbbf24;',
'text-cyan-700': PRINT_READY ? 'color:#333;' : 'color:#0e7490;',
'text-cyan-400': PRINT_READY ? 'color:#666;' : 'color:#22d3ee;',
'text-white': 'color:white;',
// Backgrounds
'bg-muted/30': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f5f5f4;',
'bg-muted/50': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f5f5f4;',
'bg-muted': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f0f0ee;',
'bg-background': 'background:white;', 'bg-card': 'background:white;',
'bg-blue-100': PRINT_READY ? 'background:#f2f2f2;' : 'background:#dbeafe;',
'bg-blue-50': PRINT_READY ? 'background:#fff;' : 'background:#eff6ff;',
'bg-blue-50/50': PRINT_READY ? 'background:#fff;' : 'background:#f7fbff;',
'bg-green-100': PRINT_READY ? 'background:#f2f2f2;' : 'background:#dcfce7;',
'bg-green-50': PRINT_READY ? 'background:#fff;' : 'background:#f0fdf4;',
'bg-green-50/50': PRINT_READY ? 'background:#fff;' : 'background:#f8fef9;',
'bg-purple-100': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f3e8ff;',
'bg-purple-50': PRINT_READY ? 'background:#fff;' : 'background:#faf5ff;',
'bg-purple-50/50': PRINT_READY ? 'background:#fff;' : 'background:#fdfaff;',
'bg-red-50/50': PRINT_READY ? 'background:#f2f2f2;' : 'background:#fef7f7;',
'bg-red-50': PRINT_READY ? 'background:#f2f2f2;' : 'background:#fef2f2;',
'bg-amber-50/50': PRINT_READY ? 'background:#fff;' : 'background:#fffdf7;',
'bg-amber-50': PRINT_READY ? 'background:#fff;' : 'background:#fffbeb;',
'bg-cyan-50/50': PRINT_READY ? 'background:#fff;' : 'background:#f0fdff;',
// Border colors
'border-blue-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#bfdbfe;',
'border-blue-800': '',
'border-blue-300': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#93c5fd;',
'border-green-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#bbf7d0;',
'border-green-900': '',
'border-green-300': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#86efac;',
'border-purple-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#e9d5ff;',
'border-purple-800': '',
'border-purple-300': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#c4b5fd;',
'border-red-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#fecaca;',
'border-red-900': '',
'border-amber-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#fde68a;',
'border-amber-900': '',
'border-cyan-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#a5f3fc;',
'border-cyan-900': '',
// Grid
'grid-cols-2': 'grid-template-columns:1fr 1fr;', 'grid-cols-4': 'grid-template-columns:1fr 1fr 1fr 1fr;',
'md:grid-cols-2': 'grid-template-columns:1fr 1fr;', 'md:grid-cols-3': 'grid-template-columns:1fr 1fr 1fr;', 'md:grid-cols-4': 'grid-template-columns:1fr 1fr 1fr 1fr;',
// Extra borders & bg
'border-2': 'border-width:2px;',
'border-primary': PRINT_READY ? 'border-color:#000;' : 'border-color:#7c3aed;',
'bg-primary/10': PRINT_READY ? 'background:#f2f2f2;' : 'background:rgba(124,58,237,0.1);',
};
// Process each element that has a class attribute
return html.replace(/class="([^"]+)"/g, (match, classes: string) => {
const classList = classes.split(/\s+/);
const styles: string[] = [];
const remainingClasses: string[] = [];
for (const cls of classList) {
// Skip dark mode variants
if (cls.startsWith('dark:')) continue;
// Skip responsive prefixes that we can't handle (except md: grid which we keep)
if (cls.startsWith('sm:') || cls.startsWith('lg:') || cls.startsWith('xl:')) continue;
// Handle md: prefix
const effectiveCls = cls.startsWith('md:') ? cls : cls;
if (tw[effectiveCls]) {
styles.push(tw[effectiveCls]);
} else {
remainingClasses.push(cls);
}
}
if (styles.length === 0) return match;
const styleStr = styles.filter(Boolean).join('');
const classStr = remainingClasses.length > 0 ? ` class="${remainingClasses.join(' ')}"` : '';
return `${classStr} style="${styleStr}"`.trim();
});
}
/**
* Convert all inline color values in style attributes to grayscale for print.
* Replaces colored hex values and rgba with gray equivalents.
*/
function convertStylesToGrayscale(html: string): string {
if (!PRINT_READY) return html;
// Map of specific hex colors to grayscale
const colorToGray: Record<string, string> = {
// Blues
'#3b82f6': '#666', '#2563eb': '#333', '#1d4ed8': '#333', '#60a5fa': '#999',
'#dbeafe': '#f2f2f2', '#bfdbfe': '#ccc', '#93c5fd': '#ccc', '#eff6ff': '#fff',
// Greens
'#22c55e': '#666', '#16a34a': '#333', '#15803d': '#333', '#4ade80': '#999',
'#dcfce7': '#f2f2f2', '#bbf7d0': '#ccc', '#86efac': '#ccc', '#f0fdf4': '#fff',
'#166534': '#333',
// Purples
'#7c3aed': '#000', '#9333ea': '#333', '#7e22ce': '#333', '#c084fc': '#999', '#a78bfa': '#666',
'#f3e8ff': '#f2f2f2', '#e9d5ff': '#ccc', '#c4b5fd': '#ccc', '#faf5ff': '#fff',
'#5b2d8e': '#333',
// Reds
'#dc2626': '#333', '#b91c1c': '#333', '#f87171': '#999',
'#fef2f2': '#f2f2f2', '#fecaca': '#ccc', '#450a0a': '#333',
// Ambers
'#f59e0b': '#666', '#d97706': '#333', '#b45309': '#333', '#fbbf24': '#999', '#92400e': '#333',
'#fef3c7': '#f2f2f2', '#fde68a': '#ccc', '#fffbeb': '#fff', '#fcd34d': '#ccc',
// Cyan
'#0e7490': '#333', '#22d3ee': '#999',
'#cffafe': '#f2f2f2', '#a5f3fc': '#ccc', '#67e8f9': '#ccc',
// Pinks
'#fce7f3': '#f2f2f2', '#f9a8d4': '#ccc',
// Rose
'#ef4444': '#333', '#f97316': '#555',
// Misc
'#e5e7eb': '#ccc',
'#78716c': '#666', '#c41d7f': '#333',
// Traffic light dots
'#ff5f56': '#999', '#ffbd2e': '#999', '#27c93f': '#999',
'#f8fafc': '#fff', '#f8fdf9': '#fff', '#faf8ff': '#fff', '#fffbf5': '#fff',
'#fefce8': '#fff',
};
// Replace hex colors in both inline style attributes and CSS
let result = html;
for (const [color, gray] of Object.entries(colorToGray)) {
result = result.split(color).join(gray);
}
// Replace rgba colors with gray
result = result.replace(/rgba\(\d+,\s*\d+,\s*\d+,\s*[\d.]+\)/g, '#f2f2f2');
// Replace linear-gradient colors
result = result.replace(/linear-gradient\([^)]*\)/g, (match) => {
let g = match;
for (const [color, gray] of Object.entries(colorToGray)) {
g = g.split(color).join(gray);
}
return g;
});
return result;
}
/**
* Convert prompt variables like ${field} and ${years:10} to printable blanks
* \${name} or ${name} → _______ (name)
* \${name:default} or ${name:default} → _______ (name, e.g. default)
*/
function convertPromptVariables(text: string): string {
return text.replace(/\\?\$\{([^:}]+)(?::([^}]*))?\}/g, (_, name, defaultVal) => {
if (defaultVal) {
return `_______ (${name}, e.g. ${defaultVal})`;
}
return `_______ (${name})`;
});
}
/**
* Escape HTML entities
*/
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Convert MDX to HTML
*/
function mdxToHtml(mdx: string): string {
let html = mdx;
// Code blocks FIRST - protect content from all markdown processing
// Handle quadruple-backtick blocks first (which can contain triple backticks)
// Fences must be at start of line
html = html.replace(/^````(\w*)\n([\s\S]*?)^````\s*$/gm, (_, lang, code) => {
const protectedCode = protectCodeBlock(escapeHtml(code.trim()));
return `<pre class="code-block${lang ? ` language-${lang}` : ''}"><code>${protectedCode}</code></pre>`;
});
// Then handle triple-backtick blocks (fences at start of line)
html = html.replace(/^```(\w*)\n([\s\S]*?)^```\s*$/gm, (_, lang, code) => {
const protectedCode = protectCodeBlock(escapeHtml(code.trim()));
return `<pre class="code-block${lang ? ` language-${lang}` : ''}"><code>${protectedCode}</code></pre>`;
});
// Inline code - protect before italic/bold
html = html.replace(/`([^`]+)`/g, (_, code) => {
const protectedCode = protectCodeBlock(escapeHtml(code));
return `<code>${protectedCode}</code>`;
});
// Headers
html = html.replace(/^######\s+(.*)$/gm, '<h6>$1</h6>');
html = html.replace(/^#####\s+(.*)$/gm, '<h5>$1</h5>');
html = html.replace(/^####\s+(.*)$/gm, '<h4>$1</h4>');
html = html.replace(/^###\s+(.*)$/gm, '<h3>$1</h3>');
html = html.replace(/^##\s+(.*)$/gm, '<h2>$1</h2>');
html = html.replace(/^#\s+(.*)$/gm, '<h1>$1</h1>');
// Bold and italic
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Underscore italic: only match single words wrapped in underscores, not runs of underscores
html = html.replace(/(?<![_\w])_([^_\n]+?)_(?![_\w])/g, '<em>$1</em>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Images
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
// Horizontal rules
html = html.replace(/^---$/gm, '<hr />');
// Lists (basic)
html = html.replace(/^-\s+(.*)$/gm, '<li>$1</li>');
html = html.replace(/^(\d+)\.\s+(.*)$/gm, '<li>$2</li>');
// Wrap consecutive <li> in <ul>
html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => `<ul>\n${match}</ul>\n`);
// Blockquotes: lines starting with >
html = html.replace(/(^>\s?.+$\n?)+/gm, (match) => {
const content = match.replace(/^>\s?/gm, '').trim();
return `<blockquote>${content}</blockquote>\n`;
});
// Paragraphs (lines that aren't already HTML)
html = html.split('\n\n').map(block => {
block = block.trim();
if (!block) return '';
if (block.startsWith('<')) return block;
if (block.match(/^<(div|p|ul|ol|h[1-6]|pre|blockquote|hr|table)/)) return block;
return `<p>${block}</p>`;
}).join('\n\n');
return html;
}
// Storage for protected code blocks that shouldn't be markdown-processed
const protectedBlocks: Map<string, string> = new Map();
let blockCounter = 0;
/**
* Protect a code block from markdown processing
* Uses a format that won't be affected by markdown conversion
*/
function protectCodeBlock(content: string): string {
const id = `<!--CODEBLOCK${blockCounter++}-->`;
protectedBlocks.set(id, content);
blockCounter++;
return id;
}
/**
* Restore protected code blocks after markdown processing
*/
function restoreProtectedBlocks(html: string): string {
let result = html;
for (const [id, content] of protectedBlocks) {
result = result.split(id).join(content);
}
protectedBlocks.clear();
return result;
}
/**
* Read and process a chapter
*/
function processChapter(locale: string, chapter: Chapter, localeData: LocaleData, messages: Record<string, unknown>): ProcessedChapter | null {
const localePath = getLocalePath(locale);
const filePath = path.join(localePath, `${chapter.slug}.mdx`);
if (!fs.existsSync(filePath)) {
console.log(` ⚠ Skipping ${chapter.slug} (not found for ${locale})`);
return null;
}
const rawContent = fs.readFileSync(filePath, 'utf-8');
const transformedContent = transformMdxForPdf(rawContent, locale, localeData, messages);
const htmlContent = mdxToHtml(transformedContent);
const restoredContent = restoreProtectedBlocks(htmlContent);
const withEndnotes = convertLinksToEndnotes(restoredContent, messages, locale);
const finalContent = convertStylesToGrayscale(withEndnotes);
return {
slug: chapter.slug,
title: chapter.title,
part: chapter.part,
content: finalContent,
};
}
/**
* Convert external <a href="..."> links to numbered endnotes at the end of the chapter.
* Internal links (starting with /) are left as plain text references.
*/
function convertLinksToEndnotes(html: string, messages: Record<string, unknown> = {}, _locale: string = 'en'): string {
const msgLinks = t(messages, 'book.interactive.links');
const linksLabel = (msgLinks !== 'book.interactive.links') ? msgLinks : 'Links';
const footnotes: { num: number; text: string; url: string }[] = [];
let counter = 0;
// Replace external links with text + superscript number
const processed = html.replace(/<a href="((?:https?:\/\/)[^"]+)"[^>]*>([^<]+)<\/a>/g, (_, url, text) => {
counter++;
footnotes.push({ num: counter, text, url });
return `${text}<sup class="fn-ref">${counter}</sup>`;
});
// Replace internal links with just the text (no footnote needed)
const cleaned = processed.replace(/<a href="\/[^"]*"[^>]*>([^<]+)<\/a>/g, '$1');
if (footnotes.length === 0) return cleaned;
// Build endnotes section
const notesHtml = footnotes.map(fn =>
`<div class="fn-item"><span class="fn-num">${fn.num}.</span> <span class="fn-url">${fn.url}</span></div>`
).join('\n');
return `${cleaned}
<div class="fn-section">
<div class="fn-title">${linksLabel}</div>
${notesHtml}
</div>`;
}
/**
* Generate the HTML document for PDF
*/
// Map part slugs to message keys for translation
const PART_TRANSLATION_KEYS: Record<string, string> = {
'Introduction': 'introduction',
'Foundations': 'foundations',
'Techniques': 'techniques',
'Advanced': 'advanced',
'Advanced Strategies': 'advanced',
'Best Practices': 'bestPractices',
'Use Cases': 'useCases',
'Conclusion': 'conclusion',
};
function generateHtmlDocument(chapters: ProcessedChapter[], locale: string, messages: Record<string, unknown> = {}): string {
// Print version uses shorter printTitle, screen uses full title
const titleKey = PRINT_READY ? 'book.printTitle' : 'book.title';
const msgTitle = t(messages, titleKey);
const title = (msgTitle !== titleKey) ? msgTitle : (BOOK_TITLES_FALLBACK[locale] || BOOK_TITLES_FALLBACK.en);
const subtitleKey = PRINT_READY ? 'book.printSubtitle' : 'book.subtitle';
const msgSubtitle = t(messages, subtitleKey);
const subtitle = (msgSubtitle !== subtitleKey) ? msgSubtitle : 'A Comprehensive Guide to AI Prompt Engineering';
const isRtl = ['ar', 'he', 'fa'].includes(locale);
// Helper to translate part name
const translatePart = (partName: string): string => {
const key = PART_TRANSLATION_KEYS[partName];
if (key) {
const translated = t(messages, `book.parts.${key}`);
if (translated !== `book.parts.${key}`) return translated;
}
return partName;
};
// Helper to translate chapter title
const translateChapter = (slug: string, fallback: string): string => {
const translated = t(messages, `book.chapters.${slug}`);
if (translated !== `book.chapters.${slug}`) return translated;
return fallback;
};
// TOC heading
const contentsLabel = t(messages, 'book.tableOfContents');
const tocTitle = (contentsLabel !== 'book.tableOfContents') ? contentsLabel : 'Contents';
// Group chapters by part — only break pages at new parts, not every chapter
let chapterNumber = 0;
let currentPart = '';
const chaptersHtml = chapters.map((chapter) => {
chapterNumber++;
const translatedPart = translatePart(chapter.part);
const translatedTitle = translateChapter(chapter.slug, chapter.title);
const isNewPart = chapter.part !== currentPart;
currentPart = chapter.part;
const sectionClass = isNewPart ? 'chapter chapter-new-part' : 'chapter';
return `
<section class="${sectionClass}" id="${chapter.slug}">
<div class="chapter-opener">
<div class="chapter-number">${chapterNumber}</div>
<div class="chapter-meta">
<span class="chapter-part">${translatedPart}</span>
<h1 class="chapter-title">${translatedTitle}</h1>
</div>
<div class="chapter-rule"></div>
</div>
<div class="chapter-content">
${chapter.content}
</div>
</section>
`;
}).join('\n');
return `<!DOCTYPE html>
<html lang="${locale}" dir="${isRtl ? 'rtl' : 'ltr'}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
/* ========================================
BOOK SIZE: 6" x 9" (US Trade)
${PRINT_READY ? 'PRINT-READY: includes 0.125in bleed on all sides' : 'Screen-optimized'}
======================================== */
@page {
size: ${PRINT_READY ? BLEED_WIDTH + ' ' + BLEED_HEIGHT : '6in 9in'};
margin: ${PRINT_READY ? '0.7in 0.65in 0.75in 0.65in' : '0.55in 0.5in 0.6in 0.5in'};
${PRINT_READY ? 'marks: crop cross;\n bleed: ' + BLEED + ';' : ''}
}
/* ========================================
BASE TYPOGRAPHY
======================================== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
/* ${PRINT_READY ? 'B&W grayscale palette for print' : 'Screen RGB palette'} */
--color-text: ${PRINT_READY ? '#000000' : '#1c1917'};
--color-text-muted: ${PRINT_READY ? '#333333' : '#57534e'};
--color-text-light: ${PRINT_READY ? '#666666' : '#78716c'};
--color-accent: ${PRINT_READY ? '#000000' : '#7c3aed'};
--color-accent-light: ${PRINT_READY ? '#666666' : '#a78bfa'};
--color-bg-subtle: ${PRINT_READY ? '#ffffff' : '#fafaf9'};
--color-bg-muted: ${PRINT_READY ? '#f2f2f2' : '#f5f5f4'};
--color-border: ${PRINT_READY ? '#cccccc' : '#e7e5e4'};
--color-border-dark: ${PRINT_READY ? '#999999' : '#d6d3d1'};
--font-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, Georgia, 'Times New Roman', serif;
--font-sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace;
}
body {
font-family: var(--font-serif);
font-size: 10.5pt;
line-height: 1.65;
color: var(--color-text);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-feature-settings: 'liga' 1, 'kern' 1;
hyphens: auto;
orphans: 3;
widows: 3;
}
/* ========================================
COVER PAGE
======================================== */
.cover {
page-break-after: always;
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 100vh;
padding: 0 2em 3em 2em;
}
.cover-rule {
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-light), transparent);
margin-bottom: 2em;
}
.cover h1 {
font-family: var(--font-sans);
font-size: 30pt;
font-weight: 800;
color: var(--color-text);
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 0.3em;
}
.cover .subtitle {
font-family: var(--font-serif);
font-size: 11pt;
font-style: italic;
color: var(--color-text-muted);
margin-bottom: 2.5em;
}
.cover-author {
display: flex;
align-items: center;
gap: 0.8em;
margin-bottom: 2em;
}
.cover-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
}
.cover-author-info {
line-height: 1.3;
}
.cover .author-name {
font-family: var(--font-sans);
font-size: 11pt;
font-weight: 600;
color: var(--color-text);
display: block;
}
.cover .author-desc {
font-family: var(--font-sans);
font-size: 8pt;
color: var(--color-text-light);
display: block;
}
.cover .url {
font-family: var(--font-sans);
font-size: 8pt;
color: var(--color-text-light);
letter-spacing: 0.02em;
}
/* ========================================
TABLE OF CONTENTS
======================================== */
.toc {
page-break-after: always;
padding-top: 1.5em;
}
.toc-title {
font-family: var(--font-sans);
font-size: 18pt;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1.5em;
padding-bottom: 0.5em;
border-bottom: 2px solid var(--color-text);
}
.toc-part {
font-family: var(--font-sans);
font-size: 10pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-accent);
margin-top: 1.5em;
margin-bottom: 0.6em;
padding-top: 0.8em;
border-top: 1px solid var(--color-border);
}
.toc-part:first-of-type {
border-top: none;
padding-top: 0;
}
.toc-chapter {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.35em;
padding-left: 1em;
}
.toc-chapter a {
font-family: var(--font-serif);
font-size: 10pt;
color: var(--color-text);
text-decoration: none;
flex: 1;
}
.toc-dots {
flex: 1;
border-bottom: 1px dotted var(--color-border-dark);
margin: 0 0.5em 0.3em 0.5em;
}
/* ========================================
CHAPTERS
======================================== */
.chapter {
page-break-before: always;
}
.chapter-opener {
margin-top: 2em;
margin-bottom: 1.5em;
padding-top: 1em;
}
.chapter-new-part .chapter-opener {
margin-top: 0;
padding-top: 1.5em;
}
.chapter-number {
font-family: var(--font-sans);
font-size: 42pt;
font-weight: 100;
color: var(--color-accent-light);
line-height: 1;
margin-bottom: 0.1em;
}
.chapter-meta {
padding: 0;
}
.chapter-part {
font-family: var(--font-sans);
font-size: 7.5pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--color-text-light);
display: block;
margin-bottom: 0.4em;
}
.chapter-title {
font-family: var(--font-sans);
font-size: 22pt;
font-weight: 700;
color: var(--color-text);
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0 0 0.5em 0;
}
.chapter-rule {
width: 3em;
height: 1px;
background: var(--color-accent);
}
.chapter-content {
columns: 1;
}
/* ========================================
HEADINGS
======================================== */
h1 {
font-family: var(--font-sans);
font-size: 15pt;
font-weight: 700;
color: var(--color-text);
margin-top: 2em;
margin-bottom: 0.5em;
line-height: 1.25;
letter-spacing: -0.01em;
}
h2 {
font-family: var(--font-sans);
font-size: 12.5pt;
font-weight: 700;
color: var(--color-text);
margin-top: 1.8em;
margin-bottom: 0.5em;
padding-bottom: 0.25em;
border-bottom: 1px solid var(--color-border);
line-height: 1.3;
letter-spacing: -0.005em;
}
h3 {
font-family: var(--font-sans);
font-size: 10.5pt;
font-weight: 700;
color: var(--color-text);
margin-top: 1.5em;
margin-bottom: 0.4em;
line-height: 1.35;
}
h4 {
font-family: var(--font-sans);
font-size: 10pt;
font-weight: 600;
color: var(--color-text-muted);
margin-top: 1.3em;
margin-bottom: 0.3em;
line-height: 1.4;
}
/* ========================================
BODY TEXT
======================================== */
p {
margin-bottom: 0.9em;
text-align: justify;
text-justify: inter-word;
}
/* First paragraph after heading - no indent, with drop cap option */
h1 + p, h2 + p, h3 + p, h4 + p,
.chapter-content > p:first-child {
text-indent: 0;
}
/* Subsequent paragraphs - indented */
p + p {
text-indent: 1.5em;
margin-top: -0.2em;
}
strong {
font-weight: 600;
}
em {
font-style: italic;
}
a {
color: var(--color-accent);
text-decoration: none;
border-bottom: 1px solid var(--color-accent-light);
}
/* ========================================
LISTS
======================================== */
ul, ol {
margin: 1em 0;
padding-left: 1.5em;
}
li {
margin-bottom: 0.4em;
line-height: 1.5;
}
li p {
margin-bottom: 0.3em;
}
/* ========================================
CODE
======================================== */
code {
font-family: var(--font-mono);
font-size: 0.85em;
background: var(--color-bg-muted);
padding: 0.15em 0.35em;
border-radius: 3px;
color: #c41d7f;
}
pre, .code-block {
font-family: var(--font-mono);
font-size: 8.5pt;
line-height: 1.5;
background: #1e1e1e;
color: #d4d4d4;
padding: 1em 1.2em;
border-radius: 4px;
margin: 1.2em 0;
overflow-x: auto;
page-break-inside: avoid;
break-inside: avoid;
}
pre code, .code-block code {
background: none;
padding: 0;
color: inherit;
font-size: inherit;
}
.prompt-code {
font-family: var(--font-mono);
font-size: 8.5pt;
line-height: 1.5;
background: #1e1e1e;
color: #d4d4d4;
padding: 1em 1.2em;
border-radius: 4px;
white-space: pre-wrap;
margin: 0.8em 0;
page-break-inside: avoid;
break-inside: avoid;
}
/* ========================================
TRY IT BOX
======================================== */
.tryit-box {
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
border: 1px solid #e9d5ff;
border-left: 4px solid var(--color-accent);
border-radius: 0 6px 6px 0;
padding: 1.2em;
margin: 1.5em 0;
}
.tryit-header {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-accent);
margin-bottom: 0.6em;
}
.tryit-desc {
font-size: 9pt;
color: var(--color-text-muted);
margin-bottom: 0.8em;
font-style: italic;
}
/* ========================================
QUIZ BOX
======================================== */
.quiz-box {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 1.2em;
margin: 1.5em 0;
}
.quiz-header {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
margin-bottom: 0.8em;
}
.quiz-question {
font-size: 10pt;
margin-bottom: 0.8em;
}
.quiz-options {
font-size: 9.5pt;
margin: 0.8em 0;
padding-left: 0.5em;
white-space: pre-line;
line-height: 1.8;
}
.quiz-explanation {
font-size: 9pt;
color: var(--color-text-muted);
font-style: italic;
margin-top: 1em;
padding-top: 0.8em;
border-top: 1px dashed var(--color-border);
}
/* ========================================
CALLOUT BOXES
======================================== */
.callout {
background: var(--color-bg-subtle);
border-radius: 4px;
padding: 1em 1.2em;
margin: 1.5em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.callout-info {
background: #f8fafc;
}
.callout-warning {
background: #fffbf5;
}
.callout-tip {
background: #f8fdf9;
}
.callout-example {
background: #faf8ff;
}
.callout-header {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
margin-bottom: 0.5em;
color: var(--color-text-muted);
}
.callout-content {
font-size: 9.5pt;
line-height: 1.55;
}
/* ========================================
INFO GRID
======================================== */
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.8em;
margin: 1.5em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.info-item {
background: var(--color-bg-muted);
padding: 0.8em;
border-radius: 4px;
font-size: 9pt;
line-height: 1.5;
page-break-inside: avoid;
break-inside: avoid;
}
.info-item strong {
font-family: var(--font-sans);
font-size: 8.5pt;
}
/* ========================================
CHECKLIST
======================================== */
.checklist {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 1em 1.2em;
margin: 1.5em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.checklist-title {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
margin-bottom: 0.6em;
}
.checklist ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.checklist li {
font-size: 9.5pt;
margin-bottom: 0.3em;
padding-left: 0.3em;
}
/* ========================================
COMPARE BOX
======================================== */
.compare-box {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8em;
margin: 1.5em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.compare-item {
padding: 0.8em;
border-radius: 4px;
font-size: 9pt;
line-height: 1.5;
}
.compare-before {
background: #fef2f2;
border: 1px solid #fecaca;
}
.compare-after {
background: #f0fdf4;
border: 1px solid #bbf7d0;
}
/* ========================================
DEMO BOXES (static rendered components)
======================================== */
.demo-box {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 1.2em;
margin: 1.5em 0;
}
.demo-header {
font-family: var(--font-sans);
font-size: 9.5pt;
font-weight: 600;
color: var(--color-text-muted);
margin-bottom: 0.8em;
padding-bottom: 0.5em;
border-bottom: 1px solid var(--color-border);
}
.demo-label {
font-size: 9pt;
color: var(--color-text-muted);
margin: 0.4em 0;
}
.demo-note {
font-size: 8.5pt;
color: var(--color-text-light);
font-style: italic;
margin: 0.6em 0;
}
.demo-section {
font-size: 9pt;
margin-top: 1em;
margin-bottom: 0.3em;
}
.demo-text {
font-size: 9pt;
background: var(--color-bg-muted);
padding: 0.6em;
border-radius: 4px;
margin: 0.3em 0;
}
.demo-table {
width: 100%;
border-collapse: collapse;
font-size: 8.5pt;
margin: 0.8em 0;
}
.demo-table th {
font-family: var(--font-sans);
background: var(--color-bg-muted);
padding: 0.5em 0.8em;
text-align: left;
border-bottom: 2px solid var(--color-border);
font-weight: 600;
}
.demo-table td {
padding: 0.4em 0.8em;
border-bottom: 1px solid var(--color-border);
}
/* ========================================
EXERCISE BOXES (fill-in-blank, debugger, challenges)
======================================== */
.exercise-box {
background: #fefce8;
border: 1px solid #fde68a;
border-radius: 6px;
padding: 1.2em;
margin: 1.5em 0;
}
.exercise-header {
font-family: var(--font-sans);
font-size: 9.5pt;
font-weight: 600;
color: #92400e;
margin-bottom: 0.8em;
}
.exercise-section {
font-size: 9pt;
margin-top: 0.8em;
margin-bottom: 0.3em;
}
.exercise-answers {
font-size: 8.5pt;
margin-top: 0.8em;
padding-top: 0.6em;
border-top: 1px dashed #fde68a;
}
.exercise-hint {
font-size: 8.5pt;
color: #92400e;
font-style: italic;
margin: 0.6em 0;
}
.difficulty-badge {
font-size: 7pt;
padding: 2px 6px;
border-radius: 10px;
background: var(--color-bg-muted);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
vertical-align: middle;
}
.prompt-code-error {
border: 1px solid #fecaca;
background: #450a0a;
}
/* ========================================
PREDICTION TOKENS
======================================== */
.prediction-step {
display: flex;
align-items: baseline;
gap: 0.8em;
margin: 0.5em 0;
font-size: 9pt;
}
.prediction-context {
font-family: var(--font-mono);
font-size: 8.5pt;
color: var(--color-text-muted);
min-width: 40%;
}
.prediction-options {
font-size: 8.5pt;
}
.prediction-token {
display: inline-block;
background: var(--color-bg-muted);
padding: 1px 6px;
border-radius: 3px;
margin: 0 2px;
font-family: var(--font-mono);
}
.prediction-prob {
font-size: 7pt;
color: var(--color-text-light);
}
/* ========================================
QUIZ ENHANCEMENTS
======================================== */
.quiz-correct {
font-weight: 600;
}
.quiz-options div {
font-size: 9pt;
padding: 0.15em 0;
line-height: 1.4;
}
/* ========================================
TEMPERATURE LEVELS
======================================== */
.temp-level {
margin: 0.8em 0;
padding: 0.6em;
background: var(--color-bg-muted);
border-radius: 4px;
page-break-inside: avoid;
break-inside: avoid;
}
.temp-label {
font-family: var(--font-sans);
font-size: 9pt;
margin-bottom: 0.3em;
}
.temp-example {
font-size: 8.5pt;
font-family: var(--font-mono);
color: var(--color-text-muted);
margin: 0.2em 0;
}
.temp-use {
font-size: 8pt;
color: var(--color-text-light);
font-style: italic;
margin-top: 0.3em;
}
/* ========================================
ITERATION STEPS
======================================== */
.iteration-step {
margin: 1em 0;
padding: 0.8em;
background: var(--color-bg-muted);
border-radius: 4px;
page-break-inside: avoid;
break-inside: avoid;
}
.iteration-header {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
margin-bottom: 0.5em;
}
.iteration-output {
font-size: 9pt;
color: var(--color-text-muted);
padding: 0.5em;
background: white;
border-radius: 3px;
margin-top: 0.5em;
}
.iteration-issue {
font-size: 8.5pt;
color: #92400e;
margin-top: 0.4em;
}
.iteration-success {
font-size: 8.5pt;
color: #166534;
margin-top: 0.4em;
}
/* ========================================
COST RESULTS
======================================== */
.cost-results {
display: flex;
gap: 0.8em;
margin: 1em 0;
}
.cost-item {
flex: 1;
text-align: center;
padding: 0.6em;
background: var(--color-bg-muted);
border-radius: 4px;
font-size: 9pt;
}
/* ========================================
CHAIN BOXES (ChainExample)
======================================== */
.chain-box {
border: 1px solid var(--color-border);
border-radius: 6px;
margin: 1.2em 0;
overflow: hidden;
}
.chain-box-header {
font-family: var(--font-sans);
font-size: 8.5pt;
font-weight: 500;
color: var(--color-text-muted);
padding: 0.4em 1em;
background: var(--color-bg-muted);
border-bottom: 1px solid var(--color-border);
}
.chain-step-item, .chain-step-skipped {
display: flex;
gap: 0.8em;
padding: 0.6em 1em;
}
.chain-step-skipped {
opacity: 0.5;
}
.chain-step-num {
font-family: var(--font-sans);
font-size: 8pt;
font-weight: 600;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-bg-muted);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2px;
}
.chain-step-body {
flex: 1;
min-width: 0;
}
.chain-step-name {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
margin-bottom: 0.3em;
}
.chain-step-prompt, .chain-step-output {
font-family: var(--font-mono);
font-size: 7.5pt;
line-height: 1.4;
padding: 0.4em 0.6em;
border-radius: 3px;
margin: 0.2em 0;
white-space: pre-wrap;
word-break: break-word;
}
.chain-step-prompt {
background: var(--color-bg-muted);
}
.chain-step-output {
background: #f0fdf4;
border: 1px solid #bbf7d0;
}
.chain-label {
font-family: var(--font-sans);
font-size: 7pt;
font-weight: 600;
color: var(--color-text-light);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.chain-step-skipped-note {
font-size: 8pt;
font-style: italic;
color: var(--color-text-light);
}
.chain-connector {
height: 1px;
background: var(--color-border);
margin: 0 1em 0 2.6em;
}
.chain-connector-parallel {
height: 1px;
background: var(--color-border);
margin: 0 1em 0 2.6em;
border-top-style: dashed;
}
.chain-loop-note {
font-family: var(--font-sans);
font-size: 8pt;
color: var(--color-text-light);
text-align: center;
padding: 0.5em;
border-top: 1px dashed var(--color-border);
}
/* ========================================
CHAIN FLOW DEMO (type overview)
======================================== */
.chain-types-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8em;
}
.chain-type-card {
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 0.8em;
page-break-inside: avoid;
break-inside: avoid;
}
.chain-type-name {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 700;
margin-bottom: 0.2em;
}
.chain-type-desc {
font-size: 8pt;
color: var(--color-text-muted);
margin-bottom: 0.5em;
}
.chain-type-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 0.2em;
flex-wrap: wrap;
}
.chain-type-diagram-parallel {
flex-direction: column;
}
.chain-type-step {
display: inline-block;
font-family: var(--font-sans);
font-size: 7pt;
font-weight: 500;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid;
}
.chain-type-arrow {
font-size: 8pt;
color: var(--color-text-light);
}
/* ========================================
FRAMEWORK STEPS
======================================== */
.fw-step {
display: flex;
gap: 0.8em;
margin: 0.6em 0;
align-items: flex-start;
page-break-inside: avoid;
break-inside: avoid;
}
.fw-letter {
font-family: var(--font-sans);
font-size: 12pt;
font-weight: 700;
width: 2em;
height: 2em;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
flex-shrink: 0;
}
.fw-step-body {
flex: 1;
min-width: 0;
padding-top: 0.2em;
}
.fw-step-label {
font-size: 9pt;
margin-bottom: 0.15em;
}
.fw-step-example {
font-size: 8pt;
font-family: var(--font-mono);
color: var(--color-text-muted);
}
/* ========================================
PRINCIPLES LIST
======================================== */
.principle-item {
display: flex;
align-items: center;
gap: 0.6em;
padding: 0.4em 0;
font-size: 9pt;
border-bottom: 1px solid var(--color-border);
}
.principle-item:last-child {
border-bottom: none;
}
.principle-icon {
font-size: 11pt;
flex-shrink: 0;
}
/* ========================================
VERSION DIFF
======================================== */
.version-block {
margin: 0.6em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.version-header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.3em;
}
.version-label {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
}
.version-note {
font-size: 7.5pt;
color: var(--color-text-light);
font-style: italic;
}
/* ========================================
JAILBREAK EXAMPLES
======================================== */
.jailbreak-example {
margin: 0.8em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.jailbreak-name {
font-family: var(--font-sans);
font-size: 9pt;
margin-bottom: 0.4em;
}
/* ========================================
PROMPT BREAKDOWN
======================================== */
.prompt-breakdown {
margin: 1.5em 0;
padding: 1.5em 1em 1em 1em;
border: 1px solid var(--color-border);
border-radius: 6px;
font-family: var(--font-mono);
font-size: 9pt;
line-height: 2.2;
page-break-inside: avoid;
break-inside: avoid;
}
.pb-segment {
display: inline;
position: relative;
white-space: nowrap;
}
.pb-label {
font-family: var(--font-sans);
font-size: 7pt;
font-weight: 600;
display: block;
margin-bottom: -2px;
}
.pb-text {
padding: 1px 4px;
border-radius: 2px;
}
/* ========================================
SPECIFICITY SPECTRUM
======================================== */
.spectrum-level {
margin: 0.6em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.spectrum-header {
display: flex;
align-items: center;
gap: 0.6em;
margin-bottom: 0.3em;
}
.spectrum-badge {
font-family: var(--font-sans);
font-size: 7.5pt;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
}
.spectrum-bar-wrap {
flex: 1;
height: 4px;
background: var(--color-bg-muted);
border-radius: 2px;
overflow: hidden;
}
.spectrum-bar {
display: block;
height: 100%;
border-radius: 2px;
}
.spectrum-level .prompt-code {
margin: 0.2em 0;
font-size: 8pt;
padding: 0.6em 0.8em;
}
/* ========================================
IMAGE / VIDEO PROMPT BUILDER
======================================== */
.image-category {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.4em;
margin: 0.5em 0;
font-size: 8.5pt;
}
.image-cat-label {
font-family: var(--font-sans);
font-weight: 600;
min-width: 6em;
font-size: 8.5pt;
}
.image-option {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 8pt;
background: var(--color-bg-muted);
border: 1px solid transparent;
}
.image-example {
margin: 0.6em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.image-example .prompt-code {
margin: 0.3em 0;
}
.image-example .demo-note {
margin: 0.2em 0;
}
.diffusion-steps {
margin: 0.5em 0;
padding-left: 0.5em;
}
.diffusion-step {
font-size: 8.5pt;
padding: 0.25em 0;
color: var(--color-text-muted);
}
/* ========================================
CODE EDITOR
======================================== */
.code-editor-box {
border: 1px solid #3c3c3c;
border-radius: 6px;
overflow: hidden;
margin: 1.2em 0;
}
.code-editor-header {
display: flex;
align-items: center;
gap: 0.6em;
padding: 0.5em 0.8em;
background: #252526;
border-bottom: 1px solid #3c3c3c;
font-size: 8pt;
}
.code-editor-dots {
display: flex;
gap: 4px;
}
.code-editor-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
${PRINT_READY ? 'background: #999 !important;' : ''}
}
.code-editor-filename {
font-family: var(--font-mono);
color: #ccc;
margin-left: 0.4em;
}
.code-editor-lang {
margin-left: auto;
text-transform: uppercase;
color: #6e6e6e;
font-family: var(--font-sans);
}
.code-editor-box .prompt-code {
margin: 0;
border-radius: 0;
}
/* ========================================
CONTEXT BLOCKS
======================================== */
.context-block {
padding: 0.6em 0.8em;
border-radius: 4px;
margin: 0.4em 0;
font-size: 8.5pt;
page-break-inside: avoid;
break-inside: avoid;
}
.context-block-on {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
}
.context-block-off {
background: var(--color-bg-muted);
opacity: 0.5;
border: 1px dashed var(--color-border);
}
/* ========================================
PROMPT BUILDER
======================================== */
.builder-field {
margin: 0.8em 0;
page-break-inside: avoid;
break-inside: avoid;
}
.builder-field-label {
font-family: var(--font-sans);
font-size: 9pt;
font-weight: 600;
margin-bottom: 0.15em;
}
.builder-field-hint {
font-size: 7.5pt;
color: var(--color-text-light);
font-style: italic;
margin-bottom: 0.3em;
}
.builder-field-input {
font-family: var(--font-mono);
font-size: 8pt;
color: var(--color-text-light);
padding: 0.5em 0.6em;
border: 1px dashed var(--color-border);
border-radius: 4px;
min-height: 2em;
background: white;
}
/* ========================================
INTERACTIVE NOTICE
======================================== */
.interactive-notice {
font-family: var(--font-sans);
font-size: 8pt;
color: var(--color-accent);
background: linear-gradient(135deg, #faf5ff, #f3e8ff);
border: 1px dashed var(--color-accent-light);
border-radius: 4px;
padding: 0.6em 1em;
margin: 0.8em 0;
text-align: center;
page-break-inside: avoid;
break-inside: avoid;
}
/* ========================================
HORIZONTAL RULES & IMAGES
======================================== */
/* ========================================
INLINE SVG ICONS (print-ready mode)
======================================== */
.ico {
width: 14px;
height: 14px;
display: inline-block;
vertical-align: -2px;
margin-right: 3px;
}
.ico-sm {
width: 10px;
height: 10px;
display: inline-block;
vertical-align: -1px;
margin-right: 2px;
}
/* ========================================
ENDNOTES / FOOTNOTES
======================================== */
.fn-ref {
font-family: var(--font-sans);
font-size: 7pt;
color: var(--color-accent);
vertical-align: super;
line-height: 0;
margin-left: 1px;
}
.fn-section {
margin-top: 2em;
padding-top: 1em;
border-top: 1px solid var(--color-border);
page-break-inside: avoid;
}
.fn-title {
font-family: var(--font-sans);
font-size: 8pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-text-light);
margin-bottom: 0.5em;
}
.fn-item {
font-size: 7.5pt;
line-height: 1.5;
margin-bottom: 0.2em;
word-break: break-all;
}
.fn-num {
font-family: var(--font-sans);
font-weight: 600;
color: var(--color-accent);
min-width: 1.5em;
display: inline-block;
}
.fn-url {
font-family: var(--font-mono);
color: var(--color-text-muted);
}
blockquote {
margin: 1.2em 0;
padding: 0.6em 1em;
border-left: 3px solid var(--color-border-dark);
color: var(--color-text-muted);
font-style: italic;
page-break-inside: avoid;
break-inside: avoid;
}
blockquote p {
margin: 0;
text-indent: 0;
}
hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2em 0;
}
img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 1em 0;
}
/* ========================================
BACK MATTER
======================================== */
.back-matter {
page-break-before: always;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
padding: 3em 2em;
}
.back-matter h2 {
font-family: var(--font-sans);
font-size: 18pt;
font-weight: 700;
border: none;
margin-bottom: 0.8em;
}
.back-matter p {
text-align: left;
font-size: 10pt;
}
.back-matter ul {
list-style: none;
padding: 0;
margin: 1.2em 0;
}
.back-matter li {
font-size: 10pt;
margin-bottom: 0.4em;
padding-left: 1.2em;
position: relative;
}
.back-matter li::before {
content: "—";
position: absolute;
left: 0;
color: var(--color-accent);
}
.colophon {
font-size: 7.5pt;
color: var(--color-text-light);
margin-top: 4em;
padding-top: 1.5em;
border-top: 1px solid var(--color-border);
}
/* ========================================
HALF TITLE PAGE (before TOC)
======================================== */
.half-title {
page-break-after: always;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
}
.half-title h1 {
font-family: var(--font-sans);
font-size: 18pt;
font-weight: 600;
color: var(--color-text);
border: none;
margin: 0;
}
.half-title p {
text-align: center;
font-size: 9pt;
color: var(--color-text-light);
margin-top: 0.5em;
}
/* ========================================
PAGE BREAKS
======================================== */
.page-break {
page-break-after: always;
}
/* ========================================
PRINT OPTIMIZATIONS
======================================== */
@media print {
body {
font-size: 10pt;
}
/* Only new parts get page breaks */
.chapter-new-part {
page-break-before: always;
}
pre, .code-block, .prompt-code {
white-space: pre-wrap;
word-wrap: break-word;
}
a {
text-decoration: none;
border-bottom: none;
}
/* Small elements: avoid page breaks inside */
.callout,
.info-item,
.checklist,
.interactive-notice,
.chain-type-card,
.prompt-breakdown,
.chapter-opener,
.fw-step,
.iteration-step,
.version-block,
.builder-field,
.context-block,
.image-example,
.jailbreak-example,
.compare-box,
.fn-section {
page-break-inside: avoid;
break-inside: avoid;
}
/* Large elements: ALLOW page breaks inside to avoid huge gaps.
These can be multi-page so forcing avoid wastes space. */
/* .demo-box, .chain-box, .exercise-box, .code-editor-box — intentionally no avoid */
/* Keep headings with following content */
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
break-after: avoid;
}
/* Ensure cover and half-title use full pages */
.cover, .half-title {
page-break-after: always;
}
}
/* ========================================
RTL SUPPORT
======================================== */
[dir="rtl"] {
text-align: right;
}
[dir="rtl"] p {
text-align: right;
}
[dir="rtl"] p + p {
text-indent: 0;
}
[dir="rtl"] .tryit-box {
border-left: 1px solid #e9d5ff;
border-right: 4px solid var(--color-accent);
border-radius: 6px 0 0 6px;
}
[dir="rtl"] ul, [dir="rtl"] ol {
padding-left: 0;
padding-right: 1.5em;
}
[dir="rtl"] .toc-chapter {
padding-left: 0;
padding-right: 1em;
}
[dir="rtl"] .chapter-opener {
flex-direction: row-reverse;
}
${PRINT_READY ? `
/* ========================================
PRINT-READY OVERRIDES
B&W grayscale + bleed extensions
======================================== */
/* Print: no border radius anywhere */
* {
border-radius: 0 !important;
}
/* Print: remove outer borders from interactive containers */
.demo-box,
.exercise-box,
.tryit-box,
.quiz-box,
.chain-box,
.code-editor-box,
.checklist,
.prompt-breakdown,
.compare-box {
border: none !important;
border-top: 1px solid #ccc !important;
border-bottom: 1px solid #ccc !important;
background: transparent !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-top: 1.2em !important;
margin-bottom: 1.2em !important;
padding-top: 0.8em !important;
padding-bottom: 0.8em !important;
}
/* Nested items inside bordered containers: no borders */
.compare-item,
.chain-type-card,
.info-item,
.chain-step-item,
.context-block,
.iteration-step,
.version-block,
.temp-level,
.image-example,
.jailbreak-example,
.fw-step,
.builder-field,
.prediction-step {
border: none !important;
background: transparent !important;
}
/* Code blocks: no bg, no border, black text */
pre, .code-block, .prompt-code {
background: transparent !important;
color: #000000 !important;
border: none !important;
}
pre code, .code-block code {
color: #000000 !important;
background: transparent !important;
}
.code-editor-box {
border: none !important;
}
.code-editor-header {
background: transparent !important;
border: none !important;
}
.code-editor-filename { color: #333 !important; }
.code-editor-lang { color: #666 !important; }
/* TryIt: no border in print */
/* Callouts: all white with gray left border */
.callout, .callout-info, .callout-warning, .callout-tip, .callout-example {
background: #f2f2f2 !important;
border: none !important;
padding: 1em 1.2em !important;
}
/* Inline code: no bg */
code {
color: #000 !important;
background: transparent !important;
}
/* Exercise elements */
.exercise-header {
color: #333 !important;
}
.exercise-hint {
color: #333 !important;
}
.exercise-answers {
border-top: none !important;
}
/* Prompt code error variant */
.prompt-code-error {
background: #fff !important;
border-color: #999 !important;
}
/* Quiz: white */
/* Info items, chain cards: no bg */
.info-item {
background: transparent !important;
}
.chain-type-card {
border: none !important;
background: transparent !important;
}
/* Compare boxes: no bg, no border in print */
/* Cover and chapter rules: black */
.cover-rule {
background: #000000 !important;
}
.chapter-rule {
background: #000000 !important;
}
/* Chain elements: grayscale */
.chain-step-output {
background: #f2f2f2 !important;
border-color: #cccccc !important;
}
.chain-step-prompt {
background: #f2f2f2 !important;
}
/* Iteration steps */
.iteration-step {
background: #f2f2f2 !important;
}
.iteration-output {
background: #ffffff !important;
}
/* Temperature levels */
.temp-level {
background: #f2f2f2 !important;
}
/* Context blocks */
.context-block-on {
background: #ffffff !important;
border-color: #cccccc !important;
}
.context-block-off {
background: #f2f2f2 !important;
border-color: #cccccc !important;
}
/* Builder fields */
.builder-field-input {
background: #ffffff !important;
border-color: #cccccc !important;
}
/* Prompt breakdown segments: grayscale */
.pb-text {
background: #f2f2f2 !important;
border-bottom-color: #000 !important;
}
.pb-label {
color: #333 !important;
}
/* Spectrum badges: grayscale */
.spectrum-badge {
background: #333 !important;
color: #fff !important;
}
.spectrum-bar {
background: #333 !important;
}
/* Framework letters: grayscale */
.fw-letter {
background: #f2f2f2 !important;
border-color: #cccccc !important;
}
/* Image/video option pills */
.image-option {
background: #f2f2f2 !important;
border-color: #ccc !important;
}
/* Footnote refs */
.fn-ref {
color: #000 !important;
}
.fn-num {
color: #000 !important;
}
/* Blockquotes */
blockquote {
border-left-color: #999 !important;
color: #333 !important;
}
/* Interactive notice */
.interactive-notice {
background: #f2f2f2 !important;
border-color: #ccc !important;
color: #333 !important;
}
/* Links */
a {
color: #000 !important;
}
/* Print: avoid breaking interactive elements across pages */
.chapter-opener,
.tryit-box,
.quiz-box,
.callout,
.demo-box,
.exercise-box,
.chain-box,
.chain-type-card,
.code-editor-box,
.compare-box,
.info-grid,
.prompt-breakdown,
.chain-step-item,
.fw-step,
.iteration-step,
.version-block,
.builder-field,
.context-block,
.image-example,
.jailbreak-example,
blockquote {
page-break-inside: avoid !important;
break-inside: avoid !important;
}
` : ''}
</style>
</head>
<body>
<!-- Cover Page -->
<div class="cover">
<div class="cover-rule"></div>
<h1>${title}</h1>
<p class="subtitle">${subtitle}</p>
<div class="cover-author">
<img class="cover-avatar" src="https://github.com/f.png" alt="Fatih Kadir Akın" />
<div class="cover-author-info">
<span class="author-name">Fatih Kadir Akın</span>
<span class="author-desc">Creator of prompts.chat, GitHub Star</span>
</div>
</div>
<p class="url">${SITE_URL}/book</p>
</div>
<!-- Half Title -->
<div class="half-title">
<h1>${title}</h1>
<p>${SITE_URL}</p>
</div>
<!-- Table of Contents -->
<div class="toc">
<h2 class="toc-title">${tocTitle}</h2>
${parts.map(part => `
<div class="toc-part">${translatePart(part.title)}</div>
${part.chapters.map(ch => `
<div class="toc-chapter">
<a href="#${ch.slug}">${translateChapter(ch.slug, ch.title)}</a>
<span class="toc-dots"></span>
</div>
`).join('')}
`).join('')}
</div>
<!-- Chapters -->
${chaptersHtml}
<!-- Back Matter -->
<div class="back-matter">
<h2>Thank You for Reading</h2>
<p>This book was designed as a companion to <strong>${SITE_URL}/book</strong>, where you can experience the full interactive version:</p>
<ul>
<li>Try every prompt directly in your browser</li>
<li>Interactive quizzes with instant feedback</li>
<li>Live demos and hands-on coding tools</li>
<li>Available in 17+ languages</li>
</ul>
<p style="margin-top: 1.5em;">If you found this book helpful, consider sharing it with others or contributing to the open-source project on GitHub.</p>
<div class="colophon">
<p>${title}</p>
<p>© ${new Date().getFullYear()} Fatih Kadir Akın — prompts.chat</p>
<p style="margin-top: 0.6em;">
Set in Palatino and Helvetica Neue. 6″ × 9″
</p>
</div>
</div>
</body>
</html>`;
}
/**
* Main function
*/
async function main() {
const args = process.argv.slice(2);
const generateAll = args.includes('--all');
const requestedLocale = args.find(arg => !arg.startsWith('--')) || 'en';
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
const availableLocales = getAvailableLocales();
const localesToGenerate = generateAll ? availableLocales : [requestedLocale];
console.log('📚 The Interactive Book of Prompting - PDF Generator\n');
if (!generateAll && !availableLocales.includes(requestedLocale)) {
console.error(`❌ Locale '${requestedLocale}' not found.`);
console.log(`Available locales: ${availableLocales.join(', ')}`);
process.exit(1);
}
for (const locale of localesToGenerate) {
console.log(`\n📖 Generating PDF for locale: ${locale}`);
// Process all chapters
const chapters: ProcessedChapter[] = [];
// Load locale data and messages for this locale
const localeData = getLocaleData(locale);
const messages = loadMessages(locale);
for (const part of parts) {
for (const chapter of part.chapters) {
const processed = processChapter(locale, chapter, localeData, messages);
if (processed) {
chapters.push(processed);
console.log(`${chapter.title}`);
}
}
}
if (chapters.length === 0) {
console.log(` ⚠ No chapters found for ${locale}, skipping...`);
continue;
}
// Generate HTML
const html = generateHtmlDocument(chapters, locale, messages);
// Write HTML file (can be converted to PDF with browser print or puppeteer)
const suffix = PRINT_READY ? '-print' : '';
const htmlPath = path.join(OUTPUT_DIR, `book-${locale}${suffix}.html`);
// Apply grayscale conversion to the entire document for print-ready mode
const finalHtml = convertStylesToGrayscale(html);
fs.writeFileSync(htmlPath, finalHtml, 'utf-8');
console.log(`\n 📄 HTML saved to: ${htmlPath}`);
console.log(`\n To generate PDF:`);
console.log(` 1. Open ${htmlPath} in a browser`);
console.log(` 2. Press Cmd/Ctrl + P to print`);
console.log(` 3. Select "Save as PDF"`);
console.log(` Or use: npx puppeteer print ${htmlPath} book-${locale}.pdf`);
}
console.log('\n✅ Done!\n');
}
// Run
main().catch(console.error);