Raiju/cli/make.php
2026-10-25 06:05:42 +03:00

300 lines
10 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
/**
* Raiju Page Scaffolding Tool
*
* Generates page boilerplate (JSON content file + optional PHP template).
*
* Usage:
* php cli/make.php --slug=about --lang=en [options]
*/
// Command-line tools must never run as a web request, even where a server ignores .htaccess.
if (PHP_SAPI !== 'cli' && isset($_SERVER['REQUEST_METHOD'])) {
http_response_code(404);
exit;
}
// Must run from project root
$projectRoot = dirname(__DIR__);
require_once $projectRoot . '/includes/page-path.php';
if (!file_exists($projectRoot . '/route.php')) {
fwrite(STDERR, "Error: Run this script from the Raiju project root.\n");
fwrite(STDERR, " cd /path/to/raiju && php cli/make.php --slug=about\n");
exit(1);
}
// ── CLI argument parsing ──────────────────────────────────────────────
$opts = [];
for ($i = 1; $i < count($argv); $i++) {
$a = $argv[$i];
if (preg_match('/^--([a-z-]+)=(.+)$/', $a, $m)) {
$opts[$m[1]] = $m[2];
} elseif (preg_match('/^--([a-z-]+)$/', $a, $m)) {
$opts[$m[1]] = true;
}
}
// ── Help ──────────────────────────────────────────────────────────────
if (isset($opts['help']) || !isset($opts['slug'])) {
echo <<<USAGE
Raiju Page Scaffolding Tool
Usage:
php cli/make.php --slug=about --lang=en [options]
Options:
--slug=PATH Page path (required; e.g. products/vitamin-d)
--lang=CODE Language code (default: en)
--type=TYPE Page type: standard or custom (default: standard)
--title=TEXT Page title (default: derived from slug)
--description=TEXT SEO meta description (default: empty)
--nav=LOCATIONS Comma-separated nav locations (e.g. header,footer)
Use --nav=none to hide from all navigation
Default: header (omits "nav" field, auto-discovered in header)
--breadcrumb=PATH Breadcrumb trail as "Label:href,Label:href,Current Label"
Last item has no href (current page). Example:
--breadcrumb="Docs:docs,API Reference"
--dry-run Show what would be generated without writing files
--force Overwrite existing files
--help Show this help
Page types:
standard JSON content file only. Served by the front controller using
renderAllSections(). No PHP template needed.
custom PHP template + JSON file. Full control over HTML layout
using editableText(), editableImage(), etc.
Examples:
php cli/make.php --slug=about --lang=en --title="About Us"
php cli/make.php --slug=services --lang=de --type=custom
php cli/make.php --slug=terms --lang=en --nav=none --dry-run
php cli/make.php --slug=api --lang=en --nav=header,footer --breadcrumb="Docs:docs,API Reference"
USAGE;
exit(isset($opts['help']) ? 0 : 1);
}
// ── Input validation ──────────────────────────────────────────────────
$slug = $opts['slug'];
$lang = $opts['lang'] ?? 'en';
$type = $opts['type'] ?? 'standard';
$dryRun = isset($opts['dry-run']);
$force = isset($opts['force']);
$navLocations = null;
if (isset($opts['nav'])) {
$navLocations = $opts['nav'] === 'none' ? [] : array_map('trim', explode(',', $opts['nav']));
}
// Parse breadcrumb: "Label:href,Label:href,Current Label"
$breadcrumb = null;
if (isset($opts['breadcrumb'])) {
$breadcrumb = [];
foreach (explode(',', $opts['breadcrumb']) as $part) {
$parts = explode(':', trim($part), 2);
$crumb = ['label' => $parts[0]];
if (isset($parts[1]) && $parts[1] !== '') $crumb['href'] = $parts[1];
$breadcrumb[] = $crumb;
}
}
// Sanitize every path segment while preserving deliberate nesting.
$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 is required and must contain alphanumeric characters.\n");
exit(1);
}
if (!preg_match('/^[a-z]{2}$/', $lang)) {
fwrite(STDERR, "Error: --lang must be a 2-letter lowercase code (e.g. en, de, es).\n");
exit(1);
}
if (!in_array($type, ['standard', 'custom'])) {
fwrite(STDERR, "Error: --type must be 'standard' or 'custom'.\n");
exit(1);
}
$titleSlug = basename(str_replace('/', DIRECTORY_SEPARATOR, $slug));
$title = $opts['title'] ?? ucfirst(str_replace('-', ' ', $titleSlug));
$description = $opts['description'] ?? '';
// ── Path computation ──────────────────────────────────────────────────
$pageKey = raijuPageContentKey($lang, $slug);
$jsonPath = $projectRoot . '/content/pages/' . $pageKey . '.json';
$templatePath = raijuPageTemplatePath($projectRoot, $lang, $slug);
// Check for existing files (without --force)
if (!$dryRun && !$force) {
if (file_exists($jsonPath)) {
fwrite(STDERR, "Error: $jsonPath already exists. Use --force to overwrite.\n");
exit(1);
}
if ($type === 'custom' && file_exists($templatePath)) {
fwrite(STDERR, "Error: $templatePath already exists. Use --force to overwrite.\n");
exit(1);
}
}
// ── Generate JSON content ─────────────────────────────────────────────
$json = [
'page' => $pageKey,
'path' => $slug,
'lang' => $lang,
'title' => $title,
'description' => $description,
'lastModified' => null,
];
if ($navLocations !== null) {
$json['nav'] = $navLocations;
}
if ($breadcrumb !== null) {
$json['breadcrumb'] = $breadcrumb;
}
if ($type === 'standard') {
$json['sections'] = [
[
'id' => 's1',
'type' => 'heading',
'text' => $title,
'level' => 'h1',
],
[
'id' => 's2',
'type' => 'text',
'title' => '',
'content' => '<p>Add your content here.</p>',
],
];
} else {
$json['hero'] = [
'title' => $title,
'subtitle' => 'Your tagline here.',
];
$json['content'] = [
'heading' => 'Content',
'text' => '<p>Add your content here.</p>',
];
}
$jsonContent = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
// ── Generate PHP template (custom only) ───────────────────────────────
$templateContent = '';
if ($type === 'custom') {
$safeTitle = addcslashes($title, "'");
$safeDesc = addcslashes($description, "'");
$includePrefix = str_repeat('../', substr_count($slug, '/') + 1);
$templateContent = <<<PHP
<?php
\$pageTitle = '$safeTitle';
\$pageDescription = '$safeDesc';
\$currentLang = '$lang';
\$currentPage = '$slug';
\$contentPage = '$pageKey';
\$basePath = \$basePath ?? '$includePrefix';
include '{$includePrefix}includes/header.php';
include '{$includePrefix}includes/content-loader.php';
\$_p = \$contentPage;
?>
<main class="main-content">
<div class="content-inner">
<!-- Hero -->
<section class="hero">
<h1><?php echo editableText(\$_p, 'hero.title', '$safeTitle'); ?></h1>
<p><?php echo editableText(\$_p, 'hero.subtitle', 'Your tagline here.'); ?></p>
</section>
<!-- Content -->
<section>
<h2><?php echo editableText(\$_p, 'content.heading', 'Content'); ?></h2>
<?php echo editableHtml(\$_p, 'content.text', '<p>Add your content here.</p>'); ?>
</section>
</div>
</main>
<?php include '{$includePrefix}includes/footer.php'; ?>
PHP;
}
// ── Output ────────────────────────────────────────────────────────────
if ($dryRun) {
echo "── DRY RUN ──\n\n";
echo "Would create: $jsonPath\n";
echo str_repeat('─', 60) . "\n";
echo $jsonContent;
echo "\n";
if ($type === 'custom') {
echo "Would create: $templatePath\n";
echo str_repeat('─', 60) . "\n";
echo $templateContent;
echo "\n";
}
echo "── No files written ──\n";
exit(0);
}
// ── Write files ───────────────────────────────────────────────────────
// JSON content file
file_put_contents($jsonPath, $jsonContent, LOCK_EX);
echo " \033[32m✓\033[0m $jsonPath\n";
// PHP template (custom only)
if ($type === 'custom') {
$templateDir = dirname($templatePath);
if (!is_dir($templateDir)) {
mkdir($templateDir, 0755, true);
}
file_put_contents($templatePath, $templateContent, LOCK_EX);
echo " \033[32m✓\033[0m $templatePath\n";
}
// Summary
echo "\nPage '$slug' created for language '$lang'.\n";
if ($type === 'standard') {
echo " Served automatically by the front controller.\n";
} else {
echo " Edit the PHP template to customize the layout.\n";
}
$urlPath = ($lang === (defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : '')) ? $slug : "$lang/$slug";
echo " URL: /$urlPath\n";
if ($navLocations !== null) {
if (empty($navLocations)) {
echo " Navigation: hidden from all menus.\n";
} else {
echo " Navigation: " . implode(', ', $navLocations) . "\n";
}
} else {
echo " Navigation: header (default auto-discovery).\n";
}
if ($breadcrumb !== null) {
$trail = implode(' / ', array_column($breadcrumb, 'label'));
echo " Breadcrumb: $trail\n";
}