#!/usr/bin/env php
[options]
Options:
--slug=NAME Page slug (default: derived from filename)
--lang=CODE Language code (default: en)
--title=TEXT Page title (default: from
or first )
--description=TEXT SEO description (default: from )
--dry-run Show what would be generated without writing files
--json-only Only generate JSON content file, no PHP template
--no-css Skip CSS extraction
--force Overwrite existing files
--help Show this help
Examples:
php cli/convert.php landing-page.html --slug=landing --lang=en
php cli/convert.php about.html --dry-run
php cli/convert.php index.html --slug=home --title="Welcome"
USAGE;
exit(0);
}
$inputFile = $args[0];
if (!file_exists($inputFile)) {
fwrite(STDERR, "Error: File not found: $inputFile\n");
exit(1);
}
$slug = $opts['slug'] ?? pathinfo($inputFile, PATHINFO_FILENAME);
$slugParts = array_map(static function (string $part): string {
$part = preg_replace('/[^a-z0-9-]/', '-', strtolower(trim($part)));
return trim((string)preg_replace('/-+/', '-', (string)$part), '-');
}, explode('/', trim((string)$slug, '/')));
$slug = implode('/', array_filter($slugParts, static fn(string $part): bool => $part !== ''));
if (!raijuPageIsValidPath($slug)) {
fwrite(STDERR, "Error: --slug must be a valid lowercase URL path.\n");
exit(1);
}
$lang = $opts['lang'] ?? 'en';
$dryRun = isset($opts['dry-run']);
$jsonOnly = isset($opts['json-only']);
$noCss = isset($opts['no-css']);
$force = isset($opts['force']);
$html = file_get_contents($inputFile);
// ── Converter class ───────────────────────────────────────────────────
class HtmlToRaiju
{
private string $html;
private string $slug;
private string $lang;
private ?string $optTitle;
private ?string $optDescription;
private DOMDocument $dom;
private DOMXPath $xpath;
private string $pageTitle = '';
private string $pageDescription = '';
/** @var array{key: string, elements: array}[] */
private array $sections = [];
/** @var array Collected JSON content */
private array $jsonData = [];
/** @var string[] PHP template lines for the body */
private array $templateLines = [];
/** @var array Track used field keys to avoid duplicates */
private array $usedKeys = [];
/** @var string[] Collected CSS blocks */
private array $cssBlocks = [];
/** @var string[] External stylesheet paths found in the HTML */
private array $linkedStylesheets = [];
/** @var int Counter for generated inline-style classes */
private int $inlineStyleCounter = 0;
/** @var string Path to the input HTML file (for resolving relative stylesheet paths) */
private string $inputDir = '';
/** @var bool Whether to extract CSS */
private bool $extractCssEnabled = true;
public function __construct(string $html, string $slug, string $lang, ?string $title = null, ?string $description = null, string $inputDir = '')
{
$this->html = $html;
$this->slug = $slug;
$this->lang = $lang;
$this->optTitle = $title;
$this->optDescription = $description;
$this->inputDir = $inputDir;
}
public function setExtractCss(bool $enabled): void
{
$this->extractCssEnabled = $enabled;
}
public function convert(): void
{
if ($this->extractCssEnabled) {
$this->extractCss();
}
$this->parseHtml();
$this->extractMeta();
$this->extractSections();
$this->processSections();
}
public function getPageTitle(): string { return $this->pageTitle; }
public function getPageDescription(): string { return $this->pageDescription; }
public function getSections(): array { return $this->sections; }
public function getJsonData(): array { return $this->jsonData; }
public function getLinkedStylesheets(): array { return $this->linkedStylesheets; }
public function hasCss(): bool { return !empty($this->cssBlocks); }
public function generateJson(): string
{
$data = [
'page' => raijuPageContentKey($this->lang, $this->slug),
'path' => $this->slug,
'lang' => $this->lang,
'title' => $this->pageTitle,
];
if ($this->pageDescription) {
$data['description'] = $this->pageDescription;
}
$data['lastModified'] = null;
$data = array_merge($data, $this->jsonData);
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
}
public function generateTemplate(): string
{
$contentPage = raijuPageContentKey($this->lang, $this->slug);
$title = addslashes($this->pageTitle);
$desc = addslashes($this->pageDescription);
$cssFile = 'css/page-' . str_replace('/', '--', $this->slug) . '.css';
$includePrefix = str_repeat('../', substr_count($this->slug, '/') + 1);
$php = "lang}';\n";
$php .= "\$currentPage = '{$this->slug}';\n";
$php .= "\$contentPage = '$contentPage';\n";
$php .= "\$basePath = \$basePath ?? '$includePrefix';\n";
if ($this->hasCss()) {
$php .= "\$pageStylesheet = '$cssFile';\n";
}
// External CDN stylesheets (Google Fonts etc.)
$externalUrls = array_filter($this->linkedStylesheets, fn($u) => preg_match('#^https?://#', $u));
if (!empty($externalUrls)) {
$php .= "\$pageExternalStyles = [\n";
foreach ($externalUrls as $url) {
$php .= " '" . addslashes($url) . "',\n";
}
$php .= "];\n";
}
$php .= "\n";
$php .= "include '{$includePrefix}includes/header.php';\n";
$php .= "include '{$includePrefix}includes/content-loader.php';\n";
$php .= "\$_p = \$contentPage;\n";
$php .= "?>\n";
$php .= " \n";
foreach ($this->templateLines as $line) {
$php .= $line . "\n";
}
$php .= " \n";
$php .= "\n";
return $php;
}
public function generateCss(): string
{
if (empty($this->cssBlocks)) {
return '';
}
$css = "/*\n";
$css .= " * Extracted styles for page: {$this->slug}\n";
$css .= " * Generated by Raiju HTML-to-Editable Converter\n";
$css .= " *\n";
$css .= " * Review and adjust these styles:\n";
$css .= " * - Replace hardcoded colors with CSS custom properties from css/style.css\n";
$css .= " * - Replace hardcoded spacing with --spacing-* tokens\n";
$css .= " * - Check that font families match your project's typography\n";
$css .= " */\n\n";
$css .= implode("\n\n", $this->cssBlocks) . "\n";
return $css;
}
// ── CSS Extraction ─────────────────────────────────────────────────
private function extractCss(): void
{
// 1. Extract