MokoOnyx v01.00.00 — initial release (successor to MokoCassiopeia)
Some checks failed
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 4s
Standards Compliance / Repository Structure Validation (push) Successful in 5s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 4s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 2s
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 1m9s
Standards Compliance / Binary File Detection (push) Successful in 4s
CodeQL Security Scanning / Analyze (actions) (push) Failing after 1m11s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 5s
Standards Compliance / Broken Link Detection (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 7s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Successful in 3s
Standards Compliance / Repository Health Check (push) Successful in 4s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
CodeQL Security Scanning / Security Scan Summary (push) Successful in 1s
Standards Compliance / Compliance Summary (push) Successful in 1s
Repo Health / Access control (push) Successful in 1s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Successful in 4s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
Some checks failed
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 4s
Standards Compliance / Repository Structure Validation (push) Successful in 5s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 4s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 2s
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 1m9s
Standards Compliance / Binary File Detection (push) Successful in 4s
CodeQL Security Scanning / Analyze (actions) (push) Failing after 1m11s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 5s
Standards Compliance / Broken Link Detection (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 7s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Successful in 3s
Standards Compliance / Repository Health Check (push) Successful in 4s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
CodeQL Security Scanning / Security Scan Summary (push) Successful in 1s
Standards Compliance / Compliance Summary (push) Successful in 1s
Repo Health / Access control (push) Successful in 1s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Successful in 4s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
All files renamed from mokocassiopeia to mokoonyx. Update server points to MokoOnyx repo. Bridge migration removed (clean standalone template). Version reset to 01.00.00. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
494
src/sync_custom_vars.php
Normal file
494
src/sync_custom_vars.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/**
|
||||
* CSS Variable Sync Utility
|
||||
* Compares a user's custom palette file against the template starter file and
|
||||
* injects any missing CSS variable declarations. Existing user values are
|
||||
* never overwritten — only genuinely new variables are added.
|
||||
* Usage (CLI):
|
||||
* php sync_custom_vars.php
|
||||
* Usage (from Joomla script.php or plugin):
|
||||
* require_once __DIR__ . '/sync_custom_vars.php';
|
||||
* MokoCssVarSync::run();
|
||||
* The script auto-detects Joomla's root by walking up from __DIR__.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or define('MOKO_CLI', true);
|
||||
|
||||
final class MokoCssVarSync
|
||||
{
|
||||
/**
|
||||
* Template name used in Joomla's media path.
|
||||
*/
|
||||
private const TPL = 'mokoonyx';
|
||||
|
||||
/**
|
||||
* Palette pairs: [starter template path relative to this file, user file relative to Joomla root].
|
||||
*/
|
||||
private const PALETTES = [
|
||||
[
|
||||
'starter' => 'media/css/theme/light.standard.css',
|
||||
'user' => 'media/templates/site/%s/css/theme/light.custom.css',
|
||||
],
|
||||
[
|
||||
'starter' => 'media/css/theme/dark.standard.css',
|
||||
'user' => 'media/templates/site/%s/css/theme/dark.custom.css',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Run the sync for all palette pairs.
|
||||
*
|
||||
* @param string|null $joomlaRoot Absolute path to Joomla root (auto-detected if null).
|
||||
* @return array<string, array{added: string[], skipped: string[]}> Results keyed by file path.
|
||||
*/
|
||||
public static function run(?string $joomlaRoot = null): array
|
||||
{
|
||||
$tplDir = self::resolveTemplateDir();
|
||||
$root = $joomlaRoot ?? self::detectJoomlaRoot();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach (self::PALETTES as $pair) {
|
||||
$starterPath = $tplDir . '/' . $pair['starter'];
|
||||
$userPath = $root . '/' . sprintf($pair['user'], self::TPL);
|
||||
|
||||
if (!is_file($starterPath)) {
|
||||
self::log("SKIP starter not found: {$starterPath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_file($userPath)) {
|
||||
self::log("SKIP user file not found (custom palette not deployed): {$userPath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = self::syncFile($starterPath, $userPath);
|
||||
$results[$userPath] = $result;
|
||||
|
||||
$addedCount = count($result['added']);
|
||||
if ($addedCount > 0) {
|
||||
self::log("ADDED {$addedCount} variable(s) to {$userPath}");
|
||||
foreach ($result['added'] as $var) {
|
||||
self::log(" + {$var}");
|
||||
}
|
||||
} else {
|
||||
self::log("OK {$userPath} — all variables present");
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a starter file against a user file and inject missing variables.
|
||||
*
|
||||
* @param string $starterPath Absolute path to the starter template CSS.
|
||||
* @param string $userPath Absolute path to the user's custom CSS.
|
||||
* @return array{added: string[], skipped: string[]}
|
||||
*/
|
||||
private static function syncFile(string $starterPath, string $userPath): array
|
||||
{
|
||||
$starterVars = self::extractVarsWithContext($starterPath);
|
||||
$userVarsMap = self::extractVarsWithContext($userPath);
|
||||
$userNames = self::extractVarNames($userPath);
|
||||
|
||||
// Find missing variables
|
||||
$missing = [];
|
||||
foreach ($starterVars as $name => $declaration) {
|
||||
if (!isset($userNames[$name])) {
|
||||
$missing[$name] = $declaration;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the entire :root block in starter file order.
|
||||
// User's custom values are preserved; missing vars get starter defaults.
|
||||
$reordered = self::rebuildInStarterOrder($starterPath, $userVarsMap, $missing);
|
||||
|
||||
// Replace the :root block in the user file with the reordered version.
|
||||
$userCss = file_get_contents($userPath);
|
||||
$userCss = self::replaceRootBlock($userCss, $reordered);
|
||||
|
||||
// Write back (atomic: write to .tmp then rename).
|
||||
$tmpPath = $userPath . '.tmp';
|
||||
file_put_contents($tmpPath, $userCss);
|
||||
rename($tmpPath, $userPath);
|
||||
|
||||
return ['added' => array_keys($missing), 'skipped' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild all variables in the order they appear in the starter file.
|
||||
* User values are preserved; missing vars use starter defaults.
|
||||
*
|
||||
* @param string $starterPath Path to starter file.
|
||||
* @param array $userVars User's variable name => declaration.
|
||||
* @param array $missing Missing variable name => starter declaration.
|
||||
* @return string Complete CSS content for inside :root { }.
|
||||
*/
|
||||
private static function rebuildInStarterOrder(string $starterPath, array $userVars, array $missing): string
|
||||
{
|
||||
$lines = file($starterPath, FILE_IGNORE_NEW_LINES);
|
||||
$output = [];
|
||||
$inRoot = false;
|
||||
$depth = 0;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Track when we enter :root (brace may be on same line)
|
||||
if (!$inRoot && preg_match('/:root/', $line)) {
|
||||
$inRoot = true;
|
||||
// If { is on this same line, don't skip it — just continue processing
|
||||
if (strpos($line, '{') === false) {
|
||||
continue;
|
||||
}
|
||||
// Fall through to process the rest of this line
|
||||
}
|
||||
|
||||
if (!$inRoot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track braces (skip lines that are ONLY a brace)
|
||||
$trimmed = trim($line);
|
||||
if ($trimmed === '{') {
|
||||
continue;
|
||||
}
|
||||
if ($trimmed === '}') {
|
||||
break; // End of :root
|
||||
}
|
||||
|
||||
// Section comment headers — always include
|
||||
if (preg_match('/\/\*\s*=+\s*.+?\s*=+\s*\*\//', $line)) {
|
||||
$output[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular comments — include
|
||||
if (preg_match('/^\s*\/\*/', $line) || preg_match('/^\s*\*/', $line)) {
|
||||
$output[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blank lines — include
|
||||
if (trim($line) === '') {
|
||||
$output[] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Variable declaration
|
||||
if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
|
||||
$name = trim($m[1]);
|
||||
if (isset($userVars[$name])) {
|
||||
// Use the user's custom value
|
||||
$output[] = $userVars[$name];
|
||||
} elseif (isset($missing[$name])) {
|
||||
// New variable — use starter default
|
||||
$output[] = $missing[$name];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other lines (e.g. color-scheme) — include as-is
|
||||
$output[] = $line;
|
||||
}
|
||||
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the content inside :root { ... } with new content.
|
||||
*/
|
||||
private static function replaceRootBlock(string $css, string $newContent): string
|
||||
{
|
||||
$rootStart = preg_match('/:root[^{]*\{/', $css, $m, PREG_OFFSET_CAPTURE);
|
||||
if (!$rootStart) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
$openBrace = $m[0][1] + strlen($m[0][0]);
|
||||
$closeBrace = self::findRootClosingBrace($css);
|
||||
|
||||
if ($closeBrace === false) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
return substr($css, 0, $openBrace) . "\n" . $newContent . "\n" . substr($css, $closeBrace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract CSS custom property declarations with their full text (name: value).
|
||||
* Only extracts from the first :root block.
|
||||
*
|
||||
* @return array<string, string> Variable name => full declaration line.
|
||||
*/
|
||||
private static function extractVarsWithContext(string $filePath): array
|
||||
{
|
||||
$css = file_get_contents($filePath);
|
||||
$vars = [];
|
||||
|
||||
// Match --variable-name: value (possibly spanning multiple lines until ;)
|
||||
if (preg_match_all('/^\s*(--[\w-]+)\s*:\s*([^;]+);/m', $css, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $m) {
|
||||
$name = trim($m[1]);
|
||||
$value = trim($m[2]);
|
||||
$vars[$name] = "{$name}: {$value};";
|
||||
}
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract just the variable names present in a CSS file.
|
||||
*
|
||||
* @return array<string, true>
|
||||
*/
|
||||
private static function extractVarNames(string $filePath): array
|
||||
{
|
||||
$css = file_get_contents($filePath);
|
||||
$vars = [];
|
||||
|
||||
if (preg_match_all('/^\s*(--[\w-]+)\s*:/m', $css, $matches)) {
|
||||
foreach ($matches[1] as $name) {
|
||||
$vars[trim($name)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group missing variables by the section comment they appear under in the starter file.
|
||||
*
|
||||
* @param array<string, string> $missing Variable name => declaration.
|
||||
* @param string $starterPath Path to starter file.
|
||||
* @return array<string, string[]> Section header => list of declarations.
|
||||
*/
|
||||
private static function groupBySection(array $missing, string $starterPath): array
|
||||
{
|
||||
$lines = file($starterPath, FILE_IGNORE_NEW_LINES);
|
||||
$section = 'Uncategorised';
|
||||
|
||||
// Walk the starter file in order — this preserves the original
|
||||
// variable ordering so injected variables match the standard theme layout.
|
||||
$sections = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Detect section comment headers like /* ===== HERO VARIANTS ===== */
|
||||
if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) {
|
||||
$section = trim($m[1]);
|
||||
}
|
||||
// Detect variable declaration — only include if it's missing from user file
|
||||
if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
|
||||
$name = trim($m[1]);
|
||||
if (isset($missing[$name])) {
|
||||
$sections[$section][] = $missing[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a CSS block from grouped sections ready for injection.
|
||||
*/
|
||||
private static function buildInjectionBlock(array $sections): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = '';
|
||||
$lines[] = '/* ===== VARIABLES ADDED BY SYNC (' . date('Y-m-d') . ') ===== */';
|
||||
|
||||
foreach ($sections as $sectionName => $declarations) {
|
||||
$lines[] = '';
|
||||
$lines[] = "/* -- {$sectionName} -- */";
|
||||
foreach ($declarations as $decl) {
|
||||
$lines[] = $decl;
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a block of CSS just before the closing } of the :root[data-bs-theme] rule.
|
||||
*/
|
||||
private static function injectBeforeRootClose(string $css, string $block): string
|
||||
{
|
||||
// Find the :root block's closing brace. The :root rule is the first major
|
||||
// rule in the file; its closing } is on its own line.
|
||||
// Strategy: find the LAST } that is preceded only by CSS variable content.
|
||||
// More robustly: find the first } that appears on its own line (possibly
|
||||
// with whitespace), which closes the :root block.
|
||||
|
||||
// Walk backwards from each } to see if it's inside the :root block.
|
||||
// Simple approach: the :root closing } is the first bare } on its own line.
|
||||
$pos = self::findRootClosingBrace($css);
|
||||
|
||||
if ($pos === false) {
|
||||
// Fallback: append before last }
|
||||
$pos = strrpos($css, '}');
|
||||
}
|
||||
|
||||
if ($pos === false) {
|
||||
// Last resort: append to end
|
||||
return $css . $block;
|
||||
}
|
||||
|
||||
return substr($css, 0, $pos) . $block . substr($css, $pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the byte position of the closing } for the :root rule.
|
||||
*/
|
||||
private static function findRootClosingBrace(string $css): int|false
|
||||
{
|
||||
// Find where :root starts
|
||||
$rootStart = preg_match('/:root\b/', $css, $m, PREG_OFFSET_CAPTURE);
|
||||
if (!$rootStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$offset = $m[0][1];
|
||||
$depth = 0;
|
||||
$len = strlen($css);
|
||||
|
||||
for ($i = $offset; $i < $len; $i++) {
|
||||
if ($css[$i] === '{') {
|
||||
$depth++;
|
||||
} elseif ($css[$i] === '}') {
|
||||
$depth--;
|
||||
if ($depth === 0) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the template source directory (where this file lives).
|
||||
*/
|
||||
private static function resolveTemplateDir(): string
|
||||
{
|
||||
return dirname(__FILE__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect Joomla root by walking up from template dir looking for
|
||||
* configuration.php or the media/templates directory structure.
|
||||
*/
|
||||
private static function detectJoomlaRoot(): string
|
||||
{
|
||||
$dir = dirname(__FILE__);
|
||||
|
||||
// Walk up max 10 levels
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
if (is_file($dir . '/configuration.php')) {
|
||||
return $dir;
|
||||
}
|
||||
// Also check for the media/templates structure (works in dev too)
|
||||
if (is_dir($dir . '/media/templates')) {
|
||||
return $dir;
|
||||
}
|
||||
$parent = dirname($dir);
|
||||
if ($parent === $dir) {
|
||||
break;
|
||||
}
|
||||
$dir = $parent;
|
||||
}
|
||||
|
||||
// Fallback for dev: if JPATH_ROOT is defined, use it
|
||||
if (defined('JPATH_ROOT')) {
|
||||
return JPATH_ROOT;
|
||||
}
|
||||
|
||||
self::log('WARNING: Could not auto-detect Joomla root. Pass it explicitly.');
|
||||
return dirname(__FILE__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message (CLI: stdout, web: Joomla enqueueMessage if available).
|
||||
*/
|
||||
private static function log(string $message): void
|
||||
{
|
||||
if (defined('MOKO_CLI') || PHP_SAPI === 'cli') {
|
||||
echo $message . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry-run mode: report what would be added without writing.
|
||||
*
|
||||
* @return array<string, string[]> File path => list of missing variable names.
|
||||
*/
|
||||
public static function dryRun(?string $joomlaRoot = null): array
|
||||
{
|
||||
$tplDir = self::resolveTemplateDir();
|
||||
$root = $joomlaRoot ?? self::detectJoomlaRoot();
|
||||
$report = [];
|
||||
|
||||
foreach (self::PALETTES as $pair) {
|
||||
$starterPath = $tplDir . '/' . $pair['starter'];
|
||||
$userPath = $root . '/' . sprintf($pair['user'], self::TPL);
|
||||
|
||||
if (!is_file($starterPath) || !is_file($userPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$starterVars = self::extractVarsWithContext($starterPath);
|
||||
$userVars = self::extractVarNames($userPath);
|
||||
|
||||
$missing = [];
|
||||
foreach ($starterVars as $name => $declaration) {
|
||||
if (!isset($userVars[$name])) {
|
||||
$missing[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missing)) {
|
||||
$report[$userPath] = $missing;
|
||||
}
|
||||
}
|
||||
|
||||
return $report;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
if (PHP_SAPI === 'cli' && realpath($argv[0] ?? '') === realpath(__FILE__)) {
|
||||
$dryRun = in_array('--dry-run', $argv, true);
|
||||
|
||||
echo "MokoOnyx CSS Variable Sync\n";
|
||||
echo str_repeat('─', 40) . "\n\n";
|
||||
|
||||
if ($dryRun) {
|
||||
echo "DRY RUN — no files will be modified\n\n";
|
||||
$report = MokoCssVarSync::dryRun();
|
||||
if (empty($report)) {
|
||||
echo "All custom palettes are up to date.\n";
|
||||
} else {
|
||||
foreach ($report as $file => $vars) {
|
||||
echo "MISSING in {$file}:\n";
|
||||
foreach ($vars as $var) {
|
||||
echo " - {$var}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MokoCssVarSync::run();
|
||||
}
|
||||
|
||||
echo "\nDone.\n";
|
||||
}
|
||||
Reference in New Issue
Block a user