From 80d6832fcd480af42a7eab227e98cf0f8cffc7a0 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 17 Oct 2024 23:04:58 +0300 Subject: [PATCH 01/30] Add ScssFixerCommand --- module/FinnaConsole/config/module.config.php | 2 + .../Command/Util/ScssFixerCommand.php | 394 ++++++++++++++++++ .../Command/Util/ScssFixerCommandFactory.php | 71 ++++ 3 files changed, 467 insertions(+) create mode 100644 module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php create mode 100644 module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php diff --git a/module/FinnaConsole/config/module.config.php b/module/FinnaConsole/config/module.config.php index d3a6661c294..cdcb90b69e1 100644 --- a/module/FinnaConsole/config/module.config.php +++ b/module/FinnaConsole/config/module.config.php @@ -24,6 +24,7 @@ 'FinnaConsole\Command\Util\ProcessRecordStatsLog' => 'FinnaConsole\Command\Util\ProcessRecordStatsLogFactory', 'FinnaConsole\Command\Util\ProcessStatsQueue' => 'FinnaConsole\Command\Util\ProcessStatsQueueFactory', 'FinnaConsole\Command\Util\ScheduledAlerts' => 'VuFindConsole\Command\ScheduledSearch\NotifyCommandFactory', + 'FinnaConsole\Command\Util\ScssFixerCommand' => 'FinnaConsole\Command\Util\ScssFixerCommandFactory', 'FinnaConsole\Command\Util\VerifyRecordLinks' => 'FinnaConsole\Command\Util\VerifyRecordLinksFactory', 'FinnaConsole\Command\Util\VerifyResourceMetadata' => 'FinnaConsole\Command\Util\VerifyResourceMetadataFactory', ], @@ -41,6 +42,7 @@ 'util/import_comments' => 'FinnaConsole\Command\Util\ImportComments', 'util/online_payment_monitor' => 'FinnaConsole\Command\Util\OnlinePaymentMonitor', 'util/process_record_stats' => 'FinnaConsole\Command\Util\ProcessRecordStatsLog', + 'util/scss_fixer' => 'FinnaConsole\Command\Util\ScssFixerCommand', 'util/verify_record_links' => 'FinnaConsole\Command\Util\VerifyRecordLinks', 'util/verify_resource_metadata' => 'FinnaConsole\Command\Util\VerifyResourceMetadata', diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php new file mode 100644 index 00000000000..8f0feb40f1b --- /dev/null +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php @@ -0,0 +1,394 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace FinnaConsole\Command\Util; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Console command: fix SCSS variable declarations. + * + * @category VuFind + * @package Console + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +#[AsCommand( + name: 'util/scssFixer', + description: 'SCSS fixer' +)] +class ScssFixerCommand extends Command +{ + const VARIABLE_CHARS = '[a-zA-Z_-]'; + + /** + * Include paths + * + * @var array + */ + protected $includePaths = []; + + /** + * Console output + * + * @var OutputInterface + */ + protected $output = null; + + /** + * All variables with the last occurrence taking precedence (like in lesscss) + * + * @var array + */ + protected $allVars = []; + + /** + * Base dir for the main SCSS file + * + * @var string + */ + protected $scssBaseDir = ''; + + /** + * Configure the command. + * + * @return void + */ + protected function configure() + { + $this + ->setHelp('Fixes variable declarations in SCSS files.') + ->addOption( + 'overrides_file', + null, + InputOption::VALUE_REQUIRED, + 'File for SCSS variable overrides' + ) + ->addOption( + 'include_path', + 'I', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Include directories' + ) + ->addOption( + 'exclude', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Files not to be touched (in addition to the ones outside of the starting directory)' + ) + ->addArgument( + 'scss_file', + InputArgument::REQUIRED, + 'Name of main scss file to use as an entry point' + ); + } + + /** + * Run the command. + * + * @param InputInterface $input Input object + * @param OutputInterface $output Output object + * + * @return int 0 for success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->includePaths = $input->getOption('include_path'); + $this->output = $output; + $mainFile = $input->getArgument('scss_file'); + $this->allVars = []; + $this->scssBaseDir = realpath(dirname($mainFile)); + // First read all vars: + if (!$this->processFile($mainFile, $this->allVars, true, false)) { + return Command::FAILURE; + } + // Now do changes: + $currentVars = []; + if (!$this->processFile($mainFile, $currentVars, false, true)) { + $this->error('Stop on failure'); + return Command::FAILURE; + } + return Command::SUCCESS; + } + + /** + * Process a file + * + * @param string $fileName File name + * @param array $vars Currently defined variables + * @param OutputInterface $output Output object + * @param bool $discover Whether to just discover variable + * @param bool $write Whether to write changes + * + * @return bool + */ + protected function processFile(string $fileName, array &$vars, bool $discover, bool $write): bool + { + if (!$this->isReadableFile($fileName)) { + $this->error("File $fileName does not exist or is not a readable file"); + return false; + } + $fileDir = dirname($fileName); + $lineNo = 0; + $this->debug( + "Start processing $fileName" . ($write ? '' : ' (read only)'), + $write ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_DEBUG + ); + $lines = file($fileName); + $inMixin = 0; + $requiredVars = []; + foreach ($lines as $idx => $line) { + ++$lineNo; + $parts = explode('//', $line, 2); + $line = $parts[0]; + $comments = $parts[1] ?? null; + + // variable declaration + if (preg_match('/^\s*\$(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { + [, $var, $value] = $matches; + $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); + if (array_key_exists($var, $vars)) { + $this->debug( + "$fileName:$lineNo: $var: '$value' overrides existing value '" . $vars[$var] . "'", + OutputInterface::VERBOSITY_DEBUG + ); + } else { + $this->debug("$fileName:$lineNo: found '$var': '$value'", OutputInterface::VERBOSITY_DEBUG); + } + $vars[$var] = $value; + // @import + } elseif (preg_match('/^\s*@import\s+"([^"]+)"\s*;/', $line, $matches)) { + $import = $matches[1]; + if (!($pathInfo = $this->resolveImportFileName($import, $fileDir))) { + $this->error("$fileName:$lineNo: import file $import not found"); + return false; + } else { + $this->debug( + "$fileName:$lineNo: import $pathInfo[fullPath] as $import" + . ($pathInfo['inBaseDir'] ? ' (IN BASE)' : ''), + OutputInterface::VERBOSITY_DEBUG + ); + if (!$this->processFile($pathInfo['fullPath'], $vars, $discover, $pathInfo['inBaseDir'])) { + return false; + } + } + } + + if ($discover || !$write) { + continue; + } + + if (str_starts_with(trim($line), '@mixin ')) { + $inMixin = $this->getBlockLevelChange($line); + continue; + } + if ($inMixin) { + $inMixin += $this->getBlockLevelChange($line); + } + + if ($inMixin) { + continue; + } + + // Collect variables that need to be defined: + if ($newVars = $this->checkVariables("$fileName:$lineNo", $line, $vars)) { + $requiredVars = [ + ...$requiredVars, + ...$newVars + ]; + } + $lines[$idx] = $line . ($comments ? "//$comments" : ''); + } + if (!$discover && $write) { + // Prepend required variables: + if ($requiredVars) { + $linesToAdd = [ + '// The following variables were automatically added in SCSS conversion' . PHP_EOL + ]; + $addedVars = []; + foreach (array_reverse($requiredVars) as $current) { + $var = $current['var']; + if (!in_array($var, $addedVars)) { + $value = $current['value']; + $linesToAdd[] = "\$$var: $value;" . PHP_EOL; + $addedVars[] = $var; + } + } + $linesToAdd[] = PHP_EOL; + array_unshift($lines, ...$linesToAdd); + } + // Write the updated file: + file_put_contents($fileName, implode('', $lines)); + } + return true; + } + + /** + * Replace variables that are defined later with their last values + * + * @param string $line Line + * @param array $vars Currently defined variables + * + * @return ?array Array of required variables and their valuesm, or null on error + */ + protected function checkVariables(string $lineId, string $line, array $vars): ?array + { + $ok = true; + $required = []; + do { + $lastLine = $line; + $line = preg_replace_callback( + '/\$(' . static::VARIABLE_CHARS . '+)(?!.*:)\\b/', + function ($matches) use ($vars, $lineId, &$ok, &$required) { + $var = $matches[1]; + $lastVal = $this->allVars[$var] ?? null; + if (isset($vars[$var]) && $vars[$var] === $lastVal) { + // Previous definition contains the correct value, return as is: + $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERBOSE); + return $matches[0]; + } + if (null === $lastVal) { + $this->error("$lineId: Value for variable '$var' not found"); + $ok = false; + return $matches[0]; + } + // Use last defined value: + $this->debug("$lineId: Need $lastVal for $var"); + $required[] = [ + 'var' => $var, + 'value' => $lastVal, + ]; + return $lastVal; + }, + $line + ); + } while ($ok && $lastLine !== $line); + return $ok ? $required : null; + } + + /** + * Get block level (depth) change + * + * @param string $line Line + * + * @return int + */ + protected function getBlockLevelChange(string $line): int + { + $level = 0; + foreach (str_split($line) as $ch) { + if ('{' === $ch) { + ++$level; + } elseif ('}' === $ch) { + --$level; + } + } + return $level; + } + + /** + * Find import file + * + * @param string $fileName Relative file name + * @param string $baseDir Base directory + * + * @return ?array + */ + protected function resolveImportFileName(string $fileName, string $baseDir): ?array + { + if (!str_ends_with($fileName, '.scss')) { + $fileName .= '.scss'; + } + $allDirs = [ + $baseDir, + ...$this->includePaths + ]; + foreach ($allDirs as $dir) { + // full import + $fullPath = "$dir/$fileName"; + if (!$this->isReadableFile($fullPath)) { + // reference import + $fullPath = dirname($fullPath) . '/_' . basename($fullPath); + } + if ($this->isReadableFile($fullPath)) { + return [ + 'fullPath' => $fullPath, + 'inBaseDir' => str_starts_with(realpath($fullPath), $this->scssBaseDir . '/'), + ]; + } + } + return null; + } + + /** + * Output a debug message + * + * @param string $msg Message + * @param int $verbosity Verbosity level + * + * @return void + */ + protected function debug(string $msg, int $verbosity = OutputInterface::VERBOSITY_VERBOSE): void + { + $this->output->writeln($msg, $verbosity); + } + + /** + * Output an error message + * + * @param string $msg Message + * + * @return void + */ + protected function error(string $msg): void + { + if ($this->output) { + $this->output->writeln('' . OutputFormatter::escape($msg) . ''); + } + } + + /** + * Check if file name points to a readable file + * + * @param string $fileName File name + * + * @return bool + */ + protected function isReadableFile(string $fileName): bool + { + return file_exists($fileName) && (is_file($fileName) || is_link($fileName)); + } +} diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php new file mode 100644 index 00000000000..ddd4f5d47f6 --- /dev/null +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php @@ -0,0 +1,71 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:developer_manual Wiki + */ + +namespace FinnaConsole\Command\Util; + +use Finna\Db\Service\FinnaRecordServiceInterface; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface as ContainerException; +use Psr\Container\ContainerInterface; + +/** + * Factory for the "scss fixer" task. + * + * @category VuFind + * @package Service + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:developer_manual Wiki + */ +class ScssFixerCommandFactory implements FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke( + ContainerInterface $container, + $requestedName, + array $options = null + ) { + return new $requestedName(); + } +} From 21b6130c47bdb8653a787a7993294f7125278f6e Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Fri, 18 Oct 2024 23:04:13 +0300 Subject: [PATCH 02/30] scssFixer fully functional --- Gruntfile.local.js | 79 +-- .../Command/Util/ScssFixerCommand.php | 546 +++++++++++++++--- themes/finna2/scss/finna/feed.scss | 2 +- 3 files changed, 491 insertions(+), 136 deletions(-) diff --git a/Gruntfile.local.js b/Gruntfile.local.js index c2b85db1bba..479fcd77292 100644 --- a/Gruntfile.local.js +++ b/Gruntfile.local.js @@ -2,6 +2,7 @@ module.exports = function(grunt) { const fs = require("fs"); const path = require("path"); const os = require("node:os"); + const fsPromises = require("node:fs/promises"); grunt.registerTask("finna:scss", function finnaScssFunc() { const config = getFinnaSassConfig({ @@ -48,19 +49,38 @@ module.exports = function(grunt) { ext: '.scss' }; - let themeDir = grunt.option('theme-dir'); - if (themeDir) { - themeDir = path.resolve(themeDir); - } - const files = themeDir - ? [ + let files = []; + let viewsDir = grunt.option('views-dir'); + let themeDirs = grunt.option('theme-dirs'); + if (viewsDir) { + const isViewDir = function (dir) { + const stat = fs.lstatSync(dir); + return stat.isDirectory() && fs.existsSync(dir + '/themes/custom'); + }; + const entries = grunt.file.expand( { + filter: isViewDir, + }, + viewsDir + '/*/*' + ); + for (const viewDir of entries) { + files.push({ + ...sharedFileOpts, + cwd: viewDir + '/themes/custom/less', + dest: viewDir + '/themes/custom/scss' + }); + } + } else if (themeDirs) { + Object.entries(themeDirs.split(',')).forEach(([, themeDir]) => { + themeDir = path.resolve(themeDir); + files.push({ ...sharedFileOpts, cwd: themeDir + '/less', dest: themeDir + '/scss' - } - ] - : [ + }); + }); + } else { + files = [ { ...sharedFileOpts, cwd: 'themes/finna2/less', @@ -72,6 +92,7 @@ module.exports = function(grunt) { dest: 'themes/custom/scss' }, ]; + } const replacements = [ // Activate SCSS @@ -188,49 +209,13 @@ module.exports = function(grunt) { } ); } - if (grunt.option('replace-vars')) { - const vars = { - 'action-link-color': '#007c90', - 'gray-lighter': '#d1d1d1', - 'gray-ultralight': '#f7f7f7', - 'gray-light': '#595959', - 'gray-darker': '#000', - 'gray-dark': '#121212', - 'gray': '#2b2b2b', - 'body-bg': '#fff', - 'screen-xs': '480px', - 'screen-xs-min': '480px', - 'screen-xs-max': '767px', - 'screen-sm': '768px', - 'screen-sm-min': '768px', - 'screen-md': '992px', - 'screen-md-min': '992px', - 'navbar-default-link-color': '#fff', - 'content-font-size-base': '16px', - 'content-headings-font-size-h1': '28px', - 'content-headings-font-size-h2': '24px', - 'content-headings-font-size-h3': '21px', - 'content-headings-font-size-h4': '18px', - }; - let order = 20; - // Change variables where used (but not where declared!): - Object.entries(vars).forEach(([src, dst]) => { - replacements.push( - { - pattern: new RegExp("(.+)\\$(" + src + ")\\b", "g"), - replacement: '$1' + dst + ' /* $2 */', - order: order - } - ); - ++order; - }); - } - console.log(themeDir ? "Converting theme " + themeDir : "Converting Finna default themes"); + console.log(themeDirs || viewsDir ? "Converting specified themes" : "Converting Finna default themes"); grunt.config.set('lessToSass', { convert: { files: files, options: { + excludes: ['important'], replacements: replacements } } diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php index 8f0feb40f1b..ca21b9f53fd 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php @@ -29,6 +29,7 @@ namespace FinnaConsole\Command\Util; +use PHPMD\Console\Output; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -82,6 +83,20 @@ class ScssFixerCommand extends Command */ protected $scssBaseDir = ''; + /** + * An array tracking all processed files + * + * @var array + */ + protected $allFiles = []; + + /** + * File to use for all added variables + * + * @var ?string + */ + protected $variablesFile = null; + /** * Configure the command. * @@ -92,10 +107,10 @@ protected function configure() $this ->setHelp('Fixes variable declarations in SCSS files.') ->addOption( - 'overrides_file', + 'variables_file', null, InputOption::VALUE_REQUIRED, - 'File for SCSS variable overrides' + 'File to use for added SCSS variables' ) ->addOption( 'include_path', @@ -112,7 +127,7 @@ protected function configure() ->addArgument( 'scss_file', InputArgument::REQUIRED, - 'Name of main scss file to use as an entry point' + 'Name of main SCSS file to use as an entry point' ); } @@ -126,6 +141,7 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + $this->variablesFile = $input->getOption('variables_file'); $this->includePaths = $input->getOption('include_path'); $this->output = $output; $mainFile = $input->getArgument('scss_file'); @@ -141,76 +157,63 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->error('Stop on failure'); return Command::FAILURE; } + + // Write out the modified files: + if (!$this->updateModifiedFiles()) { + return Command::FAILURE; + } + return Command::SUCCESS; } /** * Process a file * - * @param string $fileName File name - * @param array $vars Currently defined variables - * @param OutputInterface $output Output object - * @param bool $discover Whether to just discover variable - * @param bool $write Whether to write changes + * @param string $filename File name + * @param array $vars Currently defined variables + * @param OutputInterface $output Output object + * @param bool $discover Whether to just discover files and their content + * @param bool $change Whether to do changes to the file * * @return bool */ - protected function processFile(string $fileName, array &$vars, bool $discover, bool $write): bool - { - if (!$this->isReadableFile($fileName)) { - $this->error("File $fileName does not exist or is not a readable file"); + protected function processFile( + string $filename, + array &$vars, + bool $discover, + bool $change + ): bool { + if (!$this->isReadableFile($filename)) { + $this->error("File $filename does not exist or is not a readable file"); return false; } - $fileDir = dirname($fileName); + $filename = str_starts_with($filename, '/') ? $filename : realpath($filename); + $fileDir = dirname($filename); $lineNo = 0; $this->debug( - "Start processing $fileName" . ($write ? '' : ' (read only)'), - $write ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_DEBUG + "Start processing $filename" . ($discover ? ' (discovery)' : ($change ? '' : ' (read only)')), + $change ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_DEBUG ); - $lines = file($fileName); + $lines = file($filename, FILE_IGNORE_NEW_LINES); + $this->updateFileCollection($filename, compact('lines')); + + // Process string substitutions + if ($change) { + $this->processSubstitutions($filename, $lines); + $this->updateFileCollection($filename, compact('lines')); + } + + $this->updateFileCollection($filename, compact('lines')); + $inMixin = 0; $requiredVars = []; foreach ($lines as $idx => $line) { ++$lineNo; + $lineId = "$filename:$lineNo"; $parts = explode('//', $line, 2); $line = $parts[0]; $comments = $parts[1] ?? null; - // variable declaration - if (preg_match('/^\s*\$(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { - [, $var, $value] = $matches; - $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); - if (array_key_exists($var, $vars)) { - $this->debug( - "$fileName:$lineNo: $var: '$value' overrides existing value '" . $vars[$var] . "'", - OutputInterface::VERBOSITY_DEBUG - ); - } else { - $this->debug("$fileName:$lineNo: found '$var': '$value'", OutputInterface::VERBOSITY_DEBUG); - } - $vars[$var] = $value; - // @import - } elseif (preg_match('/^\s*@import\s+"([^"]+)"\s*;/', $line, $matches)) { - $import = $matches[1]; - if (!($pathInfo = $this->resolveImportFileName($import, $fileDir))) { - $this->error("$fileName:$lineNo: import file $import not found"); - return false; - } else { - $this->debug( - "$fileName:$lineNo: import $pathInfo[fullPath] as $import" - . ($pathInfo['inBaseDir'] ? ' (IN BASE)' : ''), - OutputInterface::VERBOSITY_DEBUG - ); - if (!$this->processFile($pathInfo['fullPath'], $vars, $discover, $pathInfo['inBaseDir'])) { - return false; - } - } - } - - if ($discover || !$write) { - continue; - } - if (str_starts_with(trim($line), '@mixin ')) { $inMixin = $this->getBlockLevelChange($line); continue; @@ -223,8 +226,19 @@ protected function processFile(string $fileName, array &$vars, bool $discover, b continue; } + // Process variable declarations: + $this->processVariables($lineId, $line, $vars); + // Process import: + if (!$this->processImport($lineId, $fileDir, $line, $vars, $discover)) { + return false; + } + + if ($discover || !$change) { + continue; + } + // Collect variables that need to be defined: - if ($newVars = $this->checkVariables("$fileName:$lineNo", $line, $vars)) { + if ($newVars = $this->checkVariables($lineId, $line, $vars)) { $requiredVars = [ ...$requiredVars, ...$newVars @@ -232,26 +246,82 @@ protected function processFile(string $fileName, array &$vars, bool $discover, b } $lines[$idx] = $line . ($comments ? "//$comments" : ''); } - if (!$discover && $write) { - // Prepend required variables: - if ($requiredVars) { - $linesToAdd = [ - '// The following variables were automatically added in SCSS conversion' . PHP_EOL - ]; - $addedVars = []; - foreach (array_reverse($requiredVars) as $current) { - $var = $current['var']; - if (!in_array($var, $addedVars)) { - $value = $current['value']; - $linesToAdd[] = "\$$var: $value;" . PHP_EOL; - $addedVars[] = $var; - } - } - $linesToAdd[] = PHP_EOL; - array_unshift($lines, ...$linesToAdd); + + if (!$discover && $change && $requiredVars || $this->allFiles[$filename]['lines'] !== $lines) { + $this->allFiles[$filename]['modified'] = true; + $this->allFiles[$filename]['lines'] = $lines; + $this->allFiles[$filename]['requiredVars'] = array_merge( + $this->allFiles[$filename]['requiredVars'], + $requiredVars + ); + } + + return true; + } + + + /** + * Find variables + * + * @param string $lineId Line identifier for logging + * @param string $line Line + * @param array $vars Currently defined variables + * + * @return ?array Array of required variables and their valuesm, or null on error + */ + protected function processVariables(string $lineId, string $line, array &$vars): void + { + if (!preg_match('/^\s*\$(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { + return; + } + [, $var, $value] = $matches; + $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); + if (array_key_exists($var, $vars)) { + $this->debug( + "$lineId: $var: '$value' overrides existing value '" . $vars[$var] . "'", + OutputInterface::VERBOSITY_DEBUG + ); + } else { + $this->debug("$lineId: found '$var': '$value'", OutputInterface::VERBOSITY_DEBUG); + } + $vars[$var] = $value; + } + + /** + * Process @import + * + * @param string $lineId Line identifier for logging + * @param string $fileDir Current file directory + * @param string $line Line + * @param array $vars Currently defined variables + * @param bool $discover Whether to just discover files and their content + * + * @return bool + */ + protected function processImport(string $lineId, string $fileDir, string $line, array &$vars, bool $discover): bool + { + if (!preg_match("/^\s*@import\s+['\"]([^'\"]+)['\"]\s*;/", $line, $matches)) { + // Check for LESS import reference: + if (!preg_match("/^\s*@import \/\*\(reference\)\*\/ ['\"]([^'\"]+)['\"]\s*;/", $line, $matches)) { + return true; + } + } + $import = $matches[1]; + if (str_ends_with($import, '.css')) { + $this->debug("$lineId: skipping .css import"); + return true; + } + if (!($pathInfo = $this->resolveImportFileName($import, $fileDir))) { + $this->error("$lineId: import file $import not found"); + return false; + } else { + $this->debug( + "$lineId: import $pathInfo[fullPath] as $import" . ($pathInfo['inBaseDir'] ? ' (IN BASE)' : ''), + OutputInterface::VERBOSITY_DEBUG + ); + if (!$this->processFile($pathInfo['fullPath'], $vars, $discover, $pathInfo['inBaseDir'])) { + return false; } - // Write the updated file: - file_put_contents($fileName, implode('', $lines)); } return true; } @@ -259,14 +329,14 @@ protected function processFile(string $fileName, array &$vars, bool $discover, b /** * Replace variables that are defined later with their last values * - * @param string $line Line - * @param array $vars Currently defined variables + * @param string $lineId Line identifier for logging + * @param string $line Line + * @param array $vars Currently defined variables * - * @return ?array Array of required variables and their valuesm, or null on error + * @return ?array Array of required variables and their values, or null on error */ protected function checkVariables(string $lineId, string $line, array $vars): ?array { - $ok = true; $required = []; do { $lastLine = $line; @@ -277,12 +347,11 @@ function ($matches) use ($vars, $lineId, &$ok, &$required) { $lastVal = $this->allVars[$var] ?? null; if (isset($vars[$var]) && $vars[$var] === $lastVal) { // Previous definition contains the correct value, return as is: - $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERBOSE); + $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERY_VERBOSE); return $matches[0]; } if (null === $lastVal) { - $this->error("$lineId: Value for variable '$var' not found"); - $ok = false; + $this->warning("$lineId: Value for variable '$var' not found"); return $matches[0]; } // Use last defined value: @@ -295,8 +364,8 @@ function ($matches) use ($vars, $lineId, &$ok, &$required) { }, $line ); - } while ($ok && $lastLine !== $line); - return $ok ? $required : null; + } while ($lastLine !== $line); + return $required; } /** @@ -322,15 +391,15 @@ protected function getBlockLevelChange(string $line): int /** * Find import file * - * @param string $fileName Relative file name + * @param string $filename Relative file name * @param string $baseDir Base directory * * @return ?array */ - protected function resolveImportFileName(string $fileName, string $baseDir): ?array + protected function resolveImportFileName(string $filename, string $baseDir): ?array { - if (!str_ends_with($fileName, '.scss')) { - $fileName .= '.scss'; + if (!str_ends_with($filename, '.scss')) { + $filename .= '.scss'; } $allDirs = [ $baseDir, @@ -338,7 +407,7 @@ protected function resolveImportFileName(string $fileName, string $baseDir): ?ar ]; foreach ($allDirs as $dir) { // full import - $fullPath = "$dir/$fileName"; + $fullPath = "$dir/$filename"; if (!$this->isReadableFile($fullPath)) { // reference import $fullPath = dirname($fullPath) . '/_' . basename($fullPath); @@ -353,6 +422,293 @@ protected function resolveImportFileName(string $fileName, string $baseDir): ?ar return null; } + /** + * Update any modified files + * + * @return bool + */ + protected function updateModifiedFiles(): bool + { + // If we have a variables file, collect all variables needed by later files and add them: + if ($this->variablesFile) { + $variablesFile = realpath($this->variablesFile) ?: $this->variablesFile; + $variablesFileIndex = $this->allFiles[$variablesFile]['index'] ?? PHP_INT_MAX; + + $allRequiredVars = []; + foreach ($this->allFiles as $filename => &$fileSpec) { + // Check if the file is included before the variables file (if so, we must add the variables in + // that file): + if ($fileSpec['index'] < $variablesFileIndex) { + continue; + } + array_push($allRequiredVars, ...$fileSpec['requiredVars']); + $fileSpec['requiredVars'] = []; + } + unset($fileSpec); + $this->updateFileCollection( + $variablesFile, + [ + 'requiredVars' => $allRequiredVars, + 'modified' => true, + ] + ); + $this->debug(count($allRequiredVars) . " variables added to $variablesFile"); + } + + foreach ($this->allFiles as $filename => $fileSpec) { + if (!$fileSpec['modified'] && !$fileSpec['requiredVars']) { + continue; + } + $lines = $fileSpec['lines']; + + // Prepend required variables: + if ($fileSpec['requiredVars']) { + $linesToAdd = ['// The following variables were automatically added in SCSS conversion']; + $addedVars = []; + foreach (array_reverse($fileSpec['requiredVars']) as $current) { + $var = $current['var']; + if (!in_array($var, $addedVars)) { + $value = $current['value']; + $linesToAdd[] = "\$$var: $value;"; + $addedVars[] = $var; + } + } + $linesToAdd[] = ''; + array_unshift($lines, ...$linesToAdd); + } + // Write the updated file: + if (false === file_put_contents($filename, implode(PHP_EOL, $lines))) { + $this->error("Could not write file $filename"); + } + $this->debug("$filename updated"); + } + + return true; + } + + /** + * Update a file in the all files collection + * + * @param string $filename File name + * @param array $values Values to set + * + * @return void; + */ + protected function updateFileCollection(string $filename, array $values): void + { + if (null === ($oldValues = $this->allFiles[$filename] ?? null)) { + $oldValues = [ + 'modified' => false, + 'requiredVars' => [], + ]; + $values['index'] = count($this->allFiles); + } + if (!isset($oldValues['lines']) && !isset($values['lines'])) { + // Read in any existing file: + if (file_exists($filename)) { + if (!$this->isReadableFile($filename)) { + throw new \Exception("$filename is not readable"); + } + $values['lines'] = file($filename, FILE_IGNORE_NEW_LINES); + } + } + // Set modified flag if needed: + if (isset($oldValues['lines']) && isset($values['lines']) && $oldValues['lines'] !== $values['lines']) { + $values['modified'] = true; + } + $this->allFiles[$filename] = array_merge($oldValues, $values); + } + + /** + * Process string substitutions + * + * @param string $filename File name + * @param array $lines File contents + */ + protected function processSubstitutions(string $filename, array &$lines): void + { + $this->debug("$filename: start processing substitutions", OutputInterface::VERBOSITY_DEBUG); + $contents = implode(PHP_EOL, $lines); + foreach ($this->getSubstitutions() as $i => $substitution) { + $this->debug("$filename: processing substitution $i", OutputInterface::VERBOSITY_DEBUG); + if (str_starts_with($substitution['pattern'], '/')) { + // Regexp + if (is_string($substitution['replacement'])) { + $contents = preg_replace($substitution['pattern'], $substitution['replacement'], $contents); + } else { + $contents = preg_replace_callback( + $substitution['pattern'], + $substitution['replacement'], + $contents + ); + } + if (null === $contents) { + throw new \Exception( + "Failed to process regexp substitution $i: " . $substitution['pattern'] + . ': ' . preg_last_error_msg() + ); + } + } else { + // String + $contents = str_replace($substitution['pattern'], $substitution['replacement'], $contents); + } + } + + $lines = explode(PHP_EOL, $contents); + $this->debug("$filename: done processing substitutions", OutputInterface::VERBOSITY_DEBUG); + } + + /** + * Get substitutions + * + * @return array; + */ + protected function getSubstitutions(): array + { + return [ + [ // Revert invalid @ => $ changes for css rules: + 'pattern' => '/\$(supports|container) \(/i', + 'replacement' => '@$1 (', + ], + [ // Revert @if => $if change: + 'pattern' => '/\$if \(/i', + 'replacement' => '@if (', + ], + [ // Revert @use => $use change: + 'pattern' => "/\$use '/i", + 'replacement' => "@use '", + ], + [ // Revert @supports => $supports change: + 'pattern' => "/\$supports '/i", + 'replacement' => "@supports '", + ], + [ // Revert @page => $page change: + 'pattern' => '$page ', + 'replacement' => "@page ", + ], + [ // Fix comparison: + 'pattern' => '/ ==< /i', + 'replacement' => ' <= ', + ], + [ // Remove !important from variables: + 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*;/m', + 'replacement' => '$1:$2;', + ], + [ // Remove !important from functions: + 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*\)/m', + 'replacement' => '$1:$2;', + ], + [ // fadein => fade-in: + 'pattern' => '/fadein\((\S+),\s*(\S+)\)/', + 'replacement' => function ($matches) { + return 'fade-in(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; + }, + ], + [ // fadeout => fade-out: + 'pattern' => '/fadeout\((\S+),\s*(\S+)\)/', + 'replacement' => function ($matches) { + return 'fade-out(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; + }, + ], + [ // replace invalid characters in variable names: + 'pattern' => '/\$([^: };\/]+)/', + 'replacement' => function ($matches) { + return '$' . str_replace('.', '__', $matches[1]); + }, + ], + [ // remove invalid &: + 'pattern' => '/([a-zA-Z])&:/', + 'replacement' => '$1:', + ], + [ // remove (reference) from import): + 'pattern' => '/@import\s+\(reference\)\s*/', + 'replacement' => '@import /*(reference)*/ ', + ], + [ // fix missing semicolon from background-image rule: + 'pattern' => '/(\$background-image:([^;]+?))\n/', + 'replacement' => '$1;\n', + ], + [ // remove broken (and useless) rule: + 'pattern' => '/\.feed-container \.list-feed \@include feed-header\(\);/', + 'replacement' => '', + ], + [ // interpolate variables in media queries: + 'pattern' => '/\@media (\$[^ ]+)/', + 'replacement' => '@media #{$1}', + ], + [ // missing semicolon: + 'pattern' => '/(.+:.*auto)\n/', + 'replacement' => "$1;\n", + ], + [ // lost space in mixin declarations: + 'pattern' => '/(\@mixin.+){/', + 'replacement' => '$1 {', + ], + [ // special cases: media query variables + 'pattern' => '/(\$(mobile-portrait|mobile|tablet|desktop):\s*)(.*?);/s', + 'replacement' => '$1"$2";', + ], + [ // special cases: mobile mixin + 'pattern' => '/\.mobile\(\{(.*?)\}\);/s', + 'replacement' => '@media #{$mobile} { & { $1 } }', + ], + [ // special cases: mobile mixin 2 + 'pattern' => '@mixin mobile($rules){', + 'replacement' => '@mixin mobile {', + ], + [ // special cases: mobile mixin 3 + 'pattern' => '$rules();', + 'replacement' => '@content;', + ], + [ // invalid mixin name + 'pattern' => 'text(uppercase)', + 'replacement' => 'text-uppercase', + ], + [ // when isnumber + 'pattern' => '& when (isnumber($z-index))', + 'replacement' => '@if $z-index != null', + ], + [ // blocks extending container + 'pattern' => '@include container();', + 'replacement' => '@extend .container;', + ], + [ // blocks extending more-link + 'pattern' => '@include more-link();', + 'replacement' => '@extend .more-link;', + ], + [ // fix math operations + 'pattern' => '/(\s+)(\(.+\/.+\))/', + 'replacement' => '$1calc$2', + ], + [ // typo + 'pattern' => '$carousel-header-color none;', + 'replacement' => '$carousel-header-color: none;', + ], + [ // typo + 'pattern' => '$brand-primary // $link-color;', + 'replacement' => '$brand-primary; // $link-color', + ], + [ // typo + 'pattern' => '- aukioloaikojen otsikko', + 'replacement' => '{ /* aukioloaikojen otsikko */ }', + ], + [ // typo + 'pattern' => '$link-hover-color: $tut-a-hover,', + 'replacement' => '$link-hover-color: $tut-a-hover;', + ], + [ // math without calc + 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', + 'replacement' => function ($matches) { + [$full, $pre, $math] = $matches; + if (str_contains($matches[1], '(')) { + return $full; + } + return $pre . "calc($math)"; + }, + ], + ]; + } + /** * Output a debug message * @@ -380,15 +736,29 @@ protected function error(string $msg): void } } + /** + * Output a warning message + * + * @param string $msg Message + * + * @return void + */ + protected function warning(string $msg): void + { + if ($this->output) { + $this->output->writeln('' . OutputFormatter::escape($msg) . ''); + } + } + /** * Check if file name points to a readable file * - * @param string $fileName File name + * @param string $filename File name * * @return bool */ - protected function isReadableFile(string $fileName): bool + protected function isReadableFile(string $filename): bool { - return file_exists($fileName) && (is_file($fileName) || is_link($fileName)); + return file_exists($filename) && (is_file($filename) || is_link($filename)); } } diff --git a/themes/finna2/scss/finna/feed.scss b/themes/finna2/scss/finna/feed.scss index 8f3d6faf4cb..c7d79da8147 100644 --- a/themes/finna2/scss/finna/feed.scss +++ b/themes/finna2/scss/finna/feed.scss @@ -982,7 +982,7 @@ finna-feed.carousel-slider.splide--ltr { @container (max-width: $slider-stacked-breakpoint) { finna-feed.carousel-slider.splide--ltr { .feed-item-holder[style] { - height: var(--height !important); + height: var(--height) !important; } .feed-link { flex-direction: column; From 8130508cd390f54cf4c1c5640ff025efd7c5bed8 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 22 Oct 2024 10:44:07 +0300 Subject: [PATCH 03/30] Add fixes, variable resolution. --- .../Command/Util/ScssFixerCommand.php | 220 +++++++++++++++--- 1 file changed, 182 insertions(+), 38 deletions(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php index ca21b9f53fd..a5be7f1479f 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php @@ -195,7 +195,7 @@ protected function processFile( $change ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_DEBUG ); $lines = file($filename, FILE_IGNORE_NEW_LINES); - $this->updateFileCollection($filename, compact('lines')); + $this->updateFileCollection($filename, compact('lines', 'vars')); // Process string substitutions if ($change) { @@ -203,8 +203,6 @@ protected function processFile( $this->updateFileCollection($filename, compact('lines')); } - $this->updateFileCollection($filename, compact('lines')); - $inMixin = 0; $requiredVars = []; foreach ($lines as $idx => $line) { @@ -248,12 +246,7 @@ protected function processFile( } if (!$discover && $change && $requiredVars || $this->allFiles[$filename]['lines'] !== $lines) { - $this->allFiles[$filename]['modified'] = true; - $this->allFiles[$filename]['lines'] = $lines; - $this->allFiles[$filename]['requiredVars'] = array_merge( - $this->allFiles[$filename]['requiredVars'], - $requiredVars - ); + $this->updateFileCollection($filename, compact('lines', 'requiredVars') + ['modified' => true]); } return true; @@ -278,11 +271,11 @@ protected function processVariables(string $lineId, string $line, array &$vars): $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); if (array_key_exists($var, $vars)) { $this->debug( - "$lineId: $var: '$value' overrides existing value '" . $vars[$var] . "'", + "$lineId: '$var: $value' overrides existing value '" . $vars[$var] . "'", OutputInterface::VERBOSITY_DEBUG ); } else { - $this->debug("$lineId: found '$var': '$value'", OutputInterface::VERBOSITY_DEBUG); + $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); } $vars[$var] = $value; } @@ -422,6 +415,39 @@ protected function resolveImportFileName(string $filename, string $baseDir): ?ar return null; } + /** + * Update a file in the all files collection + * + * @param string $filename File name + * @param array $values Values to set + * + * @return void; + */ + protected function updateFileCollection(string $filename, array $values): void + { + if (null === ($oldValues = $this->allFiles[$filename] ?? null)) { + $oldValues = [ + 'modified' => false, + 'requiredVars' => [], + ]; + $values['index'] = count($this->allFiles); + } + if (!isset($oldValues['lines']) && !isset($values['lines'])) { + // Read in any existing file: + if (file_exists($filename)) { + if (!$this->isReadableFile($filename)) { + throw new \Exception("$filename is not readable"); + } + $values['lines'] = file($filename, FILE_IGNORE_NEW_LINES); + } + } + // Set modified flag if needed: + if (isset($oldValues['lines']) && isset($values['lines']) && $oldValues['lines'] !== $values['lines']) { + $values['modified'] = true; + } + $this->allFiles[$filename] = array_merge($oldValues, $values); + } + /** * Update any modified files * @@ -463,9 +489,10 @@ protected function updateModifiedFiles(): bool // Prepend required variables: if ($fileSpec['requiredVars']) { + $requiredVars = $this->resolveVariableDependencies($fileSpec['requiredVars'], $fileSpec['vars']); $linesToAdd = ['// The following variables were automatically added in SCSS conversion']; $addedVars = []; - foreach (array_reverse($fileSpec['requiredVars']) as $current) { + foreach (array_reverse($requiredVars) as $current) { $var = $current['var']; if (!in_array($var, $addedVars)) { $value = $current['value']; @@ -477,7 +504,7 @@ protected function updateModifiedFiles(): bool array_unshift($lines, ...$linesToAdd); } // Write the updated file: - if (false === file_put_contents($filename, implode(PHP_EOL, $lines))) { + if (false === file_put_contents($filename, implode(PHP_EOL, $lines)) . PHP_EOL) { $this->error("Could not write file $filename"); } $this->debug("$filename updated"); @@ -487,36 +514,49 @@ protected function updateModifiedFiles(): bool } /** - * Update a file in the all files collection + * Resolve requirements for variables that depend on other variables * - * @param string $filename File name - * @param array $values Values to set + * @param array $vars Variables to resolve + * @param array $knownVars Vars that are already available * - * @return void; + * @return array */ - protected function updateFileCollection(string $filename, array $values): void + protected function resolveVariableDependencies(array $vars, array $knownVars): array { - if (null === ($oldValues = $this->allFiles[$filename] ?? null)) { - $oldValues = [ - 'modified' => false, - 'requiredVars' => [], - ]; - $values['index'] = count($this->allFiles); - } - if (!isset($oldValues['lines']) && !isset($values['lines'])) { - // Read in any existing file: - if (file_exists($filename)) { - if (!$this->isReadableFile($filename)) { - throw new \Exception("$filename is not readable"); + $result = $vars; + foreach ($vars as $current) { + $var = $current['var']; + $varDefinition = $current['value']; + $loop = 0; + while (preg_match('/\$(' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { + $requiredVar = $matches[1]; + if (in_array($requiredVar, $knownVars)) { + $this->debug( + "Existing definition found for '$requiredVar' required by '$var: $varDefinition'", + OutputInterface::VERBOSITY_DEBUG + ); + continue; + } + if ($requiredVarValue = $this->allVars[$requiredVar] ?? null) { + $this->debug("'$var: $varDefinition' requires '$requiredVar: $requiredVarValue'"); + $result[] = [ + 'var' => $requiredVar, + 'value' => $requiredVarValue, + ]; + $varDefinition = $requiredVarValue; + } else { + $this->warning( + "Could not resolve dependency for variable '$var'; definition missing for '$requiredVar'" + ); + break; + } + if (++$loop >= 10) { + $this->warning("Value definition loop detected ($var -> $requiredVar)"); + break; } - $values['lines'] = file($filename, FILE_IGNORE_NEW_LINES); } } - // Set modified flag if needed: - if (isset($oldValues['lines']) && isset($values['lines']) && $oldValues['lines'] !== $values['lines']) { - $values['modified'] = true; - } - $this->allFiles[$filename] = array_merge($oldValues, $values); + return $result; } /** @@ -626,7 +666,7 @@ protected function getSubstitutions(): array ], [ // fix missing semicolon from background-image rule: 'pattern' => '/(\$background-image:([^;]+?))\n/', - 'replacement' => '$1;\n', + 'replacement' => "\$1;\n", ], [ // remove broken (and useless) rule: 'pattern' => '/\.feed-container \.list-feed \@include feed-header\(\);/', @@ -638,7 +678,7 @@ protected function getSubstitutions(): array ], [ // missing semicolon: 'pattern' => '/(.+:.*auto)\n/', - 'replacement' => "$1;\n", + 'replacement' => "\$1;\n", ], [ // lost space in mixin declarations: 'pattern' => '/(\@mixin.+){/', @@ -696,6 +736,110 @@ protected function getSubstitutions(): array 'pattern' => '$link-hover-color: $tut-a-hover,', 'replacement' => '$link-hover-color: $tut-a-hover;', ], + [ // typo + 'pattern' => 'rgba(43,65,98,0,9)', + 'replacement' => 'rgba(43,65,98,0.9)', + ], + [ // typo $input-bg: ##ff8d0f; + 'pattern' => '/:\s*##+/', + 'replacement' => ': #', + ], + [ // typo + 'pattern' => '!importanti', + 'replacement' => '!important', + ], + [ // typo + 'pattern' => '$brand-secondary: #;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '$brand-secondary: ###;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '#00000;', + 'replacement' => '#000000;', + ], + [ // typo + 'pattern' => 'background-color: ;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '$header-background-color #fff;', + 'replacement' => '$header-background-color: #fff;', + ], + [ // typo + 'pattern' => '$action-link-color #FFF;', + 'replacement' => '$action-link-color: #FFF;', + ], + [ // typo + 'pattern' => '$finna-browsebar-background (selaa palkin taustaväri)', + 'replacement' => '//$finna-browsebar-background (selaa palkin taustaväri)', + ], + [ // typo + 'pattern' => '$finna-browsebar-link-color(selaa palkin linkin)', + 'replacement' => '//$finna-browsebar-link-color(selaa palkin linkin)', + ], + [ // typo + 'pattern' => '$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', + 'replacement' => '//$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', + ], + [ // typo + 'pattern' => '$home-2_fi {', + 'replacement' => '.home-2_fi {', + ], + [ // disable unsupported extend + 'pattern' => '@extend .finna-panel-default .panel-heading;', + 'replacement' => '// Not supported in SCSS: @extend .finna-panel-default .panel-heading;', + ], + + + [ // gradient mixin call + 'pattern' => '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', + 'replacement' => 'gradient-vertical($background-start-color, $background-end-color, $background-start-percent, $background-end-percent);', + ], + [ // common typo in home column styles + 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?\& .right-column-content \{.*?\}.*?\})/s', + 'replacement' => "\$1\$2\n}", + ], + [ // another typo in home column styles + 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', + 'replacement' => "\$1\$2", + ], + [ // missing semicolon: display: none + 'pattern' => '/display: none\n/', + 'replacement' => 'display: none;', + ], + [ // missing semicolon in variable definitions + 'pattern' => '/(\n\s*\$' . static::VARIABLE_CHARS . '+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', + 'replacement' => "\$1;\$2", + ], + [ // missing semicolon: $header-text-color: #000000 + 'pattern' => '/$header-text-color: #000000\n/', + 'replacement' => '$header-text-color: #000000;', + ], + [ // missing semicolon: clip: rect(0px,1200px,1000px,0px) + 'pattern' => '/clip: rect\(0px,1200px,1000px,0px\)\n/', + 'replacement' => "clip: rect(0px,1200px,1000px,0px);\n", + ], + [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // + 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', + 'replacement' => "\$finna-feedback-background: darken(#d80073, 10%);\$1", + ], + [ // invalid (and obsolete) rule + 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', + 'replacement' => "// Invalid rule commented out by SCSS conversion\n/*\n\$1\n*/", + ], + + [ // literal fix + 'pattern' => "~ ')'", + 'replacement' => ')', + ], + [ // literal fix + 'pattern' => 'calc(100vh - "#{$navbar-height}~")', + 'replacement' => 'calc(100vh - #{$navbar-height})', + ], + [ // math without calc 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', 'replacement' => function ($matches) { From 28508f2d03578c538d768a4f84a8ffbc0249ca6b Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 22 Oct 2024 21:58:07 +0300 Subject: [PATCH 04/30] lessToSass partially functional --- .../FinnaConsole/config/lessToSass.config.php | 279 ++++++ module/FinnaConsole/config/module.config.php | 2 + .../Command/Util/LessToSassCommand.php | 810 ++++++++++++++++++ .../Command/Util/LessToSassCommandFactory.php | 70 ++ 4 files changed, 1161 insertions(+) create mode 100644 module/FinnaConsole/config/lessToSass.config.php create mode 100644 module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php create mode 100644 module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php diff --git a/module/FinnaConsole/config/lessToSass.config.php b/module/FinnaConsole/config/lessToSass.config.php new file mode 100644 index 00000000000..22b9653774b --- /dev/null +++ b/module/FinnaConsole/config/lessToSass.config.php @@ -0,0 +1,279 @@ + '/(?!@debug|@import|@media|@keyframes|@font-face|@include|@extend|@mixin|@supports|@container|@if|@use|@page|@-\w)@/i', + 'replacement' => '$', + ], + [ // when => if + 'pattern' => '/\.([\w_-]*)\s*\((.*)\)\s*when\s*\((.*)=(.*)\)\s*\{(\s*)([^}]+)}[;]?/i', + 'replacement' => '@if $3==$4 {$5@mixin $1($2){$5$6}}', + ], + [ // .class => @extend .class + 'pattern' => '/\.([[a-zA-Z-_]*)\s*;/i', + 'replacement' => '@extend .$1;', + ], + [ // Remove .less extension from imports + 'pattern' => "/\@import\s*[\"'](.*).less[\"']/i", + 'replacement' => function ($matches) { + return '@import \'' . str_replace('/less/', '/scss/', $matches[1]) . '\''; + }, + ], + [ // Nested include + 'pattern' => '/(\s*)\#([\w\-]*)\s*>\s*\@include\s+(.*);/i', + 'replacement' => '$1@include $2-$3;', + ], + [ // Include mixin + 'pattern' => '/(\s+)\.([\w\-]*)\s*\((.*)\);/i', + 'replacement' => '$1@include $2($3);', + ], + [ // Mixin declaration + 'pattern' => '/\.([\w\-]*)\s*\((.*)\)\s*\{/i', + 'replacement' => '@mixin $1($2){', + ], + [ + 'pattern' => '/spin\((.+),(.+)\)/i', + 'replacement' => 'adjust-hue($1,$2)', + ], + [ // shade/tint + 'pattern' => '/(shade|tint)\(([^,]+),\s?([\d%]+)\)/i', + 'replacement' => function ($matches) { + [, $method, $color2, $weight] = $matches; + $color1 = $method === 'shade' ? '#000000' : '#ffffff'; + return "mix($color1, $color2, $weight);"; + }, + ], + [ // fade + 'pattern' => '/fade\((.*),\s?([\d]+)\%\)/mi', + 'replacement' => 'rgba($1, ($2/100))', + ], + [ // literal + 'pattern' => '/~"(.*)"/i', + 'replacement' => 'unquote("$1")', + ], + + [ // Fix comparison: + 'pattern' => '/ ==< /i', + 'replacement' => ' <= ', + ], + [ // Remove !important from variables: + 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*;/m', + 'replacement' => '$1:$2;', + ], +/* [ // Remove !important from functions: + 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*\)/m', + 'replacement' => '$1:$2;', + ],*/ + [ // fadein => fade-in: + 'pattern' => '/fadein\((\S+),\s*(\S+)\)/', + 'replacement' => function ($matches) { + return 'fade-in(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; + }, + ], + [ // fadeout => fade-out: + 'pattern' => '/fadeout\((\S+),\s*(\S+)\)/', + 'replacement' => function ($matches) { + return 'fade-out(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; + }, + ], + [ // replace invalid characters in variable names: + 'pattern' => '/\$([^: };\/]+)/', + 'replacement' => function ($matches) { + return '$' . str_replace('.', '__', $matches[1]); + }, + ], + [ // remove invalid &: + 'pattern' => '/([a-zA-Z])&:/', + 'replacement' => '$1:', + ], + [ // remove (reference) from import): + 'pattern' => '/@import\s+\(reference\)\s*/', + 'replacement' => '@import /*(reference)*/ ', + ], + [ // fix missing semicolon from background-image rule: + 'pattern' => '/(\$background-image:([^;]+?))\n/', + 'replacement' => "\$1;\n", + ], + [ // remove broken (and useless) rule: + 'pattern' => '/\.feed-container \.list-feed \@include feed-header\(\);/', + 'replacement' => '', + ], + [ // interpolate variables in media queries: + 'pattern' => '/\@media (\$[^ ]+)/', + 'replacement' => '@media #{$1}', + ], + [ // missing semicolon: + 'pattern' => '/(.+:.*auto)\n/', + 'replacement' => "\$1;\n", + ], + [ // lost space in mixin declarations: + 'pattern' => '/(\@mixin.+){/', + 'replacement' => '$1 {', + ], + [ // special cases: media query variables + 'pattern' => '/(\$(mobile-portrait|mobile|tablet|desktop):\s*)(.*?);/s', + 'replacement' => '$1"$2";', + ], + [ // special cases: mobile mixin + 'pattern' => '/\.mobile\(\{(.*?)\}\);/s', + 'replacement' => '@media #{$mobile} { & { $1 } }', + ], + [ // special cases: mobile mixin 2 + 'pattern' => '@mixin mobile($rules){', + 'replacement' => '@mixin mobile {', + ], + [ // special cases: mobile mixin 3 + 'pattern' => '$rules();', + 'replacement' => '@content;', + ], + [ // invalid mixin name + 'pattern' => 'text(uppercase)', + 'replacement' => 'text-uppercase', + ], + [ // when isnumber + 'pattern' => '& when (isnumber($z-index))', + 'replacement' => '@if $z-index != null', + ], + [ // blocks extending container + 'pattern' => '@include container();', + 'replacement' => '@extend .container;', + ], + [ // blocks extending more-link + 'pattern' => '@include more-link();', + 'replacement' => '@extend .more-link;', + ], + [ // fix math operations + 'pattern' => '/(\s+)(\(.+\/.+\))/', + 'replacement' => '$1calc$2', + ], + [ // typo + 'pattern' => '$carousel-header-color none;', + 'replacement' => '$carousel-header-color: none;', + ], + [ // typo + 'pattern' => '$brand-primary // $link-color;', + 'replacement' => '$brand-primary; // $link-color', + ], + [ // typo + 'pattern' => '- aukioloaikojen otsikko', + 'replacement' => '{ /* aukioloaikojen otsikko */ }', + ], + [ // typo + 'pattern' => '$link-hover-color: $tut-a-hover,', + 'replacement' => '$link-hover-color: $tut-a-hover;', + ], + [ // typo + 'pattern' => 'rgba(43,65,98,0,9)', + 'replacement' => 'rgba(43,65,98,0.9)', + ], + [ // typo $input-bg: ##ff8d0f; + 'pattern' => '/:\s*##+/', + 'replacement' => ': #', + ], + [ // typo + 'pattern' => '!importanti', + 'replacement' => '!important', + ], + [ // typo + 'pattern' => '$brand-secondary: #;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '$brand-secondary: ###;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '#00000;', + 'replacement' => '#000000;', + ], + [ // typo + 'pattern' => 'background-color: ;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '$header-background-color #fff;', + 'replacement' => '$header-background-color: #fff;', + ], + [ // typo + 'pattern' => '$action-link-color #FFF;', + 'replacement' => '$action-link-color: #FFF;', + ], + [ // typo + 'pattern' => '$finna-browsebar-background (selaa palkin taustaväri)', + 'replacement' => '//$finna-browsebar-background (selaa palkin taustaväri)', + ], + [ // typo + 'pattern' => '$finna-browsebar-link-color(selaa palkin linkin)', + 'replacement' => '//$finna-browsebar-link-color(selaa palkin linkin)', + ], + [ // typo + 'pattern' => '$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', + 'replacement' => '//$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', + ], + [ // typo + 'pattern' => '$home-2_fi {', + 'replacement' => '.home-2_fi {', + ], + [ // disable unsupported extend + 'pattern' => '@extend .finna-panel-default .panel-heading;', + 'replacement' => '// Not supported in SCSS: @extend .finna-panel-default .panel-heading;', + ], + + [ // gradient mixin call + 'pattern' => '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', + 'replacement' => 'gradient-vertical($background-start-color, $background-end-color, $background-start-percent, $background-end-percent);', + ], + [ // common typo in home column styles + 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?\& .right-column-content \{.*?\}.*?\})/s', + 'replacement' => "\$1\$2\n}", + ], + [ // another typo in home column styles + 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', + 'replacement' => "\$1\$2", + ], + [ // missing semicolon: display: none + 'pattern' => '/display: none\n/', + 'replacement' => 'display: none;', + ], + [ // missing semicolon in variable definitions + 'pattern' => '/(\n\s*\$' . static::VARIABLE_CHARS . '+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', + 'replacement' => "\$1;\$2", + ], + [ // missing semicolon: $header-text-color: #000000 + 'pattern' => '/$header-text-color: #000000\n/', + 'replacement' => '$header-text-color: #000000;', + ], + [ // missing semicolon: clip: rect(0px,1200px,1000px,0px) + 'pattern' => '/clip: rect\(0px,1200px,1000px,0px\)\n/', + 'replacement' => "clip: rect(0px,1200px,1000px,0px);\n", + ], + [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // + 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', + 'replacement' => "\$finna-feedback-background: darken(#d80073, 10%);\$1", + ], + [ // invalid (and obsolete) rule + 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', + 'replacement' => "// Invalid rule commented out by SCSS conversion\n/*\n\$1\n*/", + ], + + [ // literal fix + 'pattern' => "~ ')'", + 'replacement' => ')', + ], + [ // literal fix + 'pattern' => 'calc(100vh - "#{$navbar-height}~")', + 'replacement' => 'calc(100vh - #{$navbar-height})', + ], + + [ // math without calc + 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', + 'replacement' => function ($matches) { + [$full, $pre, $math] = $matches; + if (str_contains($matches[1], '(')) { + return $full; + } + return $pre . "calc($math)"; + }, + ], +]; diff --git a/module/FinnaConsole/config/module.config.php b/module/FinnaConsole/config/module.config.php index cdcb90b69e1..f633b79b3fc 100644 --- a/module/FinnaConsole/config/module.config.php +++ b/module/FinnaConsole/config/module.config.php @@ -20,6 +20,7 @@ 'FinnaConsole\Command\Util\ExpireFinnaCacheCommand' => 'FinnaConsole\Command\Util\ExpireFinnaCacheCommandFactory', 'FinnaConsole\Command\Util\ExpireUsers' => 'FinnaConsole\Command\Util\ExpireUsersFactory', 'FinnaConsole\Command\Util\ImportComments' => 'FinnaConsole\Command\Util\ImportCommentsFactory', + 'FinnaConsole\Command\Util\LessToSassCommand' => 'FinnaConsole\Command\Util\LessToSassCommandFactory', 'FinnaConsole\Command\Util\OnlinePaymentMonitor' => 'FinnaConsole\Command\Util\OnlinePaymentMonitorFactory', 'FinnaConsole\Command\Util\ProcessRecordStatsLog' => 'FinnaConsole\Command\Util\ProcessRecordStatsLogFactory', 'FinnaConsole\Command\Util\ProcessStatsQueue' => 'FinnaConsole\Command\Util\ProcessStatsQueueFactory', @@ -40,6 +41,7 @@ 'util/expire_finna_cache' => 'FinnaConsole\Command\Util\ExpireFinnaCacheCommand', 'util/expire_users' => 'FinnaConsole\Command\Util\ExpireUsers', 'util/import_comments' => 'FinnaConsole\Command\Util\ImportComments', + 'util/less_to_sass' => 'FinnaConsole\Command\Util\LessToSassCommand', 'util/online_payment_monitor' => 'FinnaConsole\Command\Util\OnlinePaymentMonitor', 'util/process_record_stats' => 'FinnaConsole\Command\Util\ProcessRecordStatsLog', 'util/scss_fixer' => 'FinnaConsole\Command\Util\ScssFixerCommand', diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php new file mode 100644 index 00000000000..f66d915bac3 --- /dev/null +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php @@ -0,0 +1,810 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace FinnaConsole\Command\Util; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use VuFind\Config\PathResolver; + +/** + * Console command: convert style files from LESS to SASS. + * + * @category VuFind + * @package Console + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +#[AsCommand( + name: 'util/lessToSass', + description: 'LESS to SASS conversion' +)] +class LessToSassCommand extends Command +{ + const VARIABLE_CHARS = '[a-zA-Z_-]'; + + /** + * Include paths + * + * @var array + */ + protected $includePaths = []; + + /** + * Console output + * + * @var OutputInterface + */ + protected $output = null; + + /** + * All variables with the last occurrence taking precedence (like in lesscss) + * + * @var array + */ + protected $allLessVars = []; + + /** + * Source directory (LESS) + * + * @var string + */ + protected $sourceDir = ''; + + /** + * Target directory (SCSS) + * + * @var string + */ + protected $targetDir = ''; + + /** + * An array tracking all processed files + * + * @var array + */ + protected $allFiles = []; + + /** + * File to use for all added variables + * + * @var ?string + */ + protected $variablesFile = null; + + /** + * Files excluded from output + * + * @var array + */ + protected $excludedFiles = []; + + /** + * Constructor + * + * @param PathResolver $pathResolver Config path resolver + */ + public function __construct(protected PathResolver $pathResolver) + { + parent::__construct(); + } + + /** + * Configure the command. + * + * @return void + */ + protected function configure() + { + $this + ->setHelp('Converts LESS styles to SCSS') + ->addOption( + 'variables_file', + null, + InputOption::VALUE_REQUIRED, + 'File to use for added SCSS variables (may be relative to ' + ) + ->addOption( + 'include_path', + 'I', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Include directories for SCSS parser' + ) + ->addOption( + 'exclude', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Files not to be touched in the target directory (in addition to the ones outside of the starting' + . ' directory)' + ) + ->addOption( + 'less_file', + null, + InputOption::VALUE_REQUIRED, + 'Name of main LESS file to use as an entry point (relative to source_dir).', + 'compiled.less' + ) + ->addOption( + 'scss_file', + null, + InputOption::VALUE_REQUIRED, + 'Name of existing main SCSS file to use as an entry point (relative to target_dir).', + 'compiled.less' + ) + ->addArgument( + 'theme_dir', + InputArgument::REQUIRED, + 'Theme directory' + ); + } + + /** + * Run the command. + * + * @param InputInterface $input Input object + * @param OutputInterface $output Output object + * + * @return int 0 for success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->output = $output; + $this->includePaths = $input->getOption('include_path'); + $this->sourceDir = $input->getArgument('theme_dir') . '/less'; + $this->targetDir = $input->getArgument('theme_dir') . '/scss'; + if ($this->excludedFiles = $input->getOption('exclude')) { + $this->excludedFiles = array_map( + function ($s) { + return $this->targetDir . '/' . $s; + }, + $this->excludedFiles + ); + } + if ($variablesFile = $input->getOption('variables_file')) { + $this->variablesFile = $this->targetDir . '/' . $variablesFile; + } + $mainFile = $this->sourceDir . '/' . $input->getOption('less_file'); + // First read all vars: + if (!$this->discoverLess($mainFile, $this->allLessVars)) { + return Command::FAILURE; + } + if ($scssFile = $input->getOption('scss_file')) { + $mainFile = $this->targetDir . '/' . $scssFile; + } + // Now do changes: + $currentVars = []; + if (!$this->processFile($mainFile, $currentVars)) { + $this->error('Stop on failure'); + return Command::FAILURE; + } + + // Write out the target files: + if (!$this->writeTargetFiles()) { + return Command::FAILURE; + } + + return Command::SUCCESS; + } + + /** + * Discover less variables + * + * @param string $filename File name + * @param array $vars Currently defined variables + * + * @return bool + */ + protected function discoverLess(string $filename, array &$vars): bool + { + if (!$this->isReadableFile($filename)) { + $this->error("File $filename does not exist or is not a readable file"); + return false; + } + $fileDir = dirname($filename); + $lineNo = 0; + $this->debug("Start processing $filename (discovery)", OutputInterface::VERBOSITY_DEBUG); + $lines = file($filename, FILE_IGNORE_NEW_LINES); + + $inMixin = 0; + $inComment = false; + foreach ($lines as $line) { + ++$lineNo; + $lineId = "$filename:$lineNo"; + $parts = explode('//', $line, 2); + $line = $parts[0]; + + $cStart = strpos($line, '/*'); + $cEnd = strrpos($line, '*/'); + if (false !== $cStart && (false === $cEnd || $cEnd < $cStart)) { + $inComment = true; + } elseif (false !== $cEnd) { + $inComment = false; + } + if ($inComment) { + continue; + } + + if (preg_match('/\.([\w\-]*)\s*\((.*)\)\s*\{/i', trim($line))) { + $inMixin = $this->getBlockLevelChange($line); + continue; + } + if ($inMixin) { + $inMixin += $this->getBlockLevelChange($line); + } + if ($inMixin) { + continue; + } + + // Process variable declarations: + $this->processLessVariables($lineId, $line, $vars); + // Process import: + if (!$this->processImports($lineId, $fileDir, $line, $vars, true)) { + return false; + } + } + return true; + } + + /** + * Process a file + * + * @param string $filename File name + * @param array $vars Currently defined variables + * + * @return bool + */ + protected function processFile(string $filename, array &$vars): bool + { + if (!$this->isReadableFile($filename)) { + $this->error("File $filename does not exist or is not a readable file"); + return false; + } + $fileDir = dirname($filename); + $lineNo = 0; + $this->debug("Start processing $filename (conversion)", OutputInterface::VERBOSITY_DEBUG); + $lines = file($filename, FILE_IGNORE_NEW_LINES); + + // Process string substitutions + if (!str_ends_with($filename, '.scss')) { + $this->processSubstitutions($filename, $lines); + $this->updateFileCollection($filename, compact('lines', 'vars')); + } + + $inMixin = 0; + $requiredVars = []; + foreach ($lines as $idx => $line) { + ++$lineNo; + $lineId = "$filename:$lineNo"; + $parts = explode('//', $line, 2); + $line = $parts[0]; + $comments = $parts[1] ?? null; + + if (str_starts_with(trim($line), '@mixin ')) { + $inMixin = $this->getBlockLevelChange($line); + continue; + } + if ($inMixin) { + $inMixin += $this->getBlockLevelChange($line); + } + + if ($inMixin) { + continue; + } + + // Process variable declarations: + $this->processScssVariables($lineId, $line, $vars); + // Process imports: + if (!$this->processImports($lineId, $fileDir, $line, $vars, false)) { + return false; + } + + // Collect variables that need to be defined: + if (str_starts_with($fileDir, $this->targetDir) && !str_contains($fileDir, '..')) { + if ($newVars = $this->checkVariables($lineId, $line, $vars)) { + $requiredVars = [ + ...$requiredVars, + ...$newVars + ]; + } + } + $lines[$idx] = $line . ($comments ? "//$comments" : ''); + } + + $this->updateFileCollection($filename, compact('lines', 'requiredVars')); + + return true; + } + + /** + * Find variables in LESS + * + * @param string $lineId Line identifier for logging + * @param string $line Line + * @param array $vars Currently defined variables + * + * @return ?array Array of required variables and their valuesm, or null on error + */ + protected function processLessVariables(string $lineId, string $line, array &$vars): void + { + if (!preg_match('/^\s*\@(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { + return; + } + [, $var, $value] = $matches; + $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); + if (array_key_exists($var, $vars)) { + $this->debug( + "$lineId: '$var: $value' overrides existing value '" . $vars[$var] . "'", + OutputInterface::VERBOSITY_DEBUG + ); + } else { + $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); + } + $tmp = [$value]; + $this->processSubstitutions('', $tmp); + $vars[$var] = $tmp[0]; + } + + /** + * Find variables + * + * @param string $lineId Line identifier for logging + * @param string $line Line + * @param array $vars Currently defined variables + * + * @return ?array Array of required variables and their valuesm, or null on error + */ + protected function processScssVariables(string $lineId, string $line, array &$vars): void + { + if (!preg_match('/^\s*\$(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { + return; + } + [, $var, $value] = $matches; + $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value, -1, $count); + $default = $count > 0; + $existing = $vars[$var] ?? null; + if ($existing) { + if ($existing['default'] && !$default) { + $this->debug( + "$lineId: '$var: $value' overrides default value '" . $vars[$var] . "'", + OutputInterface::VERBOSITY_DEBUG + ); + } else { + return; + } + } else { + $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); + } + $vars[$var] = compact('value', 'default'); + } + + /** + * Process imports + * + * @param string $lineId Line identifier for logging + * @param string $fileDir Current file directory + * @param string $line Line + * @param array $vars Currently defined variables + * @param bool $discover Whether to just discover files and their content + * + * @return bool + */ + protected function processImports(string $lineId, string $fileDir, string $line, array &$vars, bool $discover): bool + { + if (!preg_match("/^\s*@import\s+['\"]([^'\"]+)['\"]\s*;/", $line, $matches)) { + // Check for LESS import reference: + if (!preg_match("/^\s*@import \/\*\(reference\)\*\/ ['\"]([^'\"]+)['\"]\s*;/", $line, $matches)) { + return true; + } + } + $import = $matches[1]; + if (str_ends_with($import, '.css')) { + $this->debug("$lineId: skipping .css import"); + return true; + } + if (!($pathInfo = $this->resolveImportFileName($import, $fileDir))) { + $targetFileDir = str_replace($this->sourceDir, $this->targetDir, $fileDir); + $targetFileDir = str_replace('/less', '/scss', $targetFileDir); + $targetImport = str_replace('/less/', '/scss/', $import); + if (!($pathInfo = $this->resolveImportFileName($targetImport, $targetFileDir))) { + $this->error("$lineId: import file $import not found"); + return false; + } + } else { + $this->debug( + "$lineId: import $pathInfo[fullPath] as $import" . ($pathInfo['inBaseDir'] ? ' (IN BASE)' : ''), + OutputInterface::VERBOSITY_DEBUG + ); + if ($discover) { + if (!$this->discoverLess($pathInfo['fullPath'], $vars)) { + return false; + } + } else { + if (!$this->processFile($pathInfo['fullPath'], $vars, $discover, $pathInfo['inBaseDir'])) { + return false; + } + } + } + return true; + } + + /** + * Replace variables that are defined later with their last values + * + * @param string $lineId Line identifier for logging + * @param string $line Line + * @param array $vars Currently defined variables + * + * @return ?array Array of required variables and their values, or null on error + */ + protected function checkVariables(string $lineId, string $line, array $vars): ?array + { + $required = []; + preg_match_all('/\$(' . static::VARIABLE_CHARS . '+)(?!.*:)\\b/', $line, $allMatches); + foreach ($allMatches[1] ?? [] as $var) { + $lessVal = $this->allLessVars[$var] ?? null; + if (isset($vars[$var]) && $vars[$var]['value'] === $lessVal) { + // Previous definition contains the correct value: + $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERY_VERBOSE); + continue; + } + if (null === $lessVal) { + $this->warning("$lineId: Value for variable '$var' not found"); + continue; + } + // Use last defined value: + $this->debug("$lineId: Need $lessVal for $var"); + $required[] = [ + 'var' => $var, + 'value' => $lessVal, + ]; + } + return $required; + } + + /** + * Resolve requirements for variables that depend on other variables + * + * @param array $vars Variables to resolve + * @param array $knownVars Vars that are already available + * + * @return array + */ + protected function resolveVariableDependencies(array $vars, array $knownVars): array + { + $result = $vars; + foreach ($vars as $current) { + $var = $current['var']; + $varDefinition = $current['value']; + $loop = 0; + while (preg_match('/\$(' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { + $requiredVar = $matches[1]; + if (in_array($requiredVar, $knownVars)) { + $this->debug( + "Existing definition found for '$requiredVar' required by '$var: $varDefinition'", + OutputInterface::VERBOSITY_DEBUG + ); + continue; + } + if ($requiredVarValue = $this->allLessVars[$requiredVar] ?? null) { + $this->debug("'$var: $varDefinition' requires '$requiredVar: $requiredVarValue'"); + $result[] = [ + 'var' => $requiredVar, + 'value' => $requiredVarValue, + ]; + $varDefinition = $requiredVarValue; + } else { + $this->warning( + "Could not resolve dependency for variable '$var'; definition missing for '$requiredVar'" + ); + break; + } + if (++$loop >= 10) { + $this->warning("Value definition loop detected ($var -> $requiredVar)"); + break; + } + } + } + return $result; + } + + /** + * Get block level (depth) change + * + * @param string $line Line + * + * @return int + */ + protected function getBlockLevelChange(string $line): int + { + $level = 0; + foreach (str_split($line) as $ch) { + if ('{' === $ch) { + ++$level; + } elseif ('}' === $ch) { + --$level; + } + } + return $level; + } + + /** + * Find import file + * + * @param string $filename Relative file name + * @param string $baseDir Base directory + * + * @return ?array + */ + protected function resolveImportFileName(string $filename, string $baseDir): ?array + { + $allDirs = [ + $baseDir, + ...$this->includePaths + ]; + $filename = preg_replace('/\.(less|scss)$/', '', $filename); + foreach (['less', 'scss'] as $extension) { + foreach ($allDirs as $dir) { + // full path + $fullPath = "$dir/$filename.$extension"; + if (!$this->isReadableFile($fullPath)) { + // reference import + $fullPath = dirname($fullPath) . '/_' . basename($fullPath); + } + if ($this->isReadableFile($fullPath)) { + return [ + 'fullPath' => $fullPath, + 'inBaseDir' => str_starts_with(realpath($fullPath), $this->sourceDir . '/'), + ]; + } + } + } + return null; + } + + /** + * Update a file in the all files collection + * + * @param string $filename File name + * @param array $values Values to set + * + * @return void; + */ + protected function updateFileCollection(string $filename, array $values): void + { + $barename = preg_replace('/\.(less|scss)$/', '', $filename); + if (null === ($oldValues = $this->allFiles[$barename] ?? null)) { + $oldValues = [ + 'requiredVars' => [], + ]; + $values['index'] = count($this->allFiles); + } + if (!isset($oldValues['lines']) && !isset($values['lines'])) { + // Read in any existing file: + if (file_exists($filename)) { + if (!$this->isReadableFile($filename)) { + throw new \Exception("$filename is not readable"); + } + $values['lines'] = file($filename, FILE_IGNORE_NEW_LINES); + } + } + $this->allFiles[$barename] = array_merge($oldValues, $values); + } + + /** + * Write target files + * + * @return bool + */ + protected function writeTargetFiles(): bool + { + // If we have a variables file, collect all variables needed by later files and add them: + if ($this->variablesFile) { + $variablesFileIndex = $this->allFiles[$this->variablesFile]['index'] ?? PHP_INT_MAX; + + $allRequiredVars = []; + foreach ($this->allFiles as $filename => &$fileSpec) { + // Check if the file is included before the variables file (if so, we must add the variables in + // that file): + if ($fileSpec['index'] < $variablesFileIndex) { + continue; + } + array_push($allRequiredVars, ...$fileSpec['requiredVars']); + $fileSpec['requiredVars'] = []; + } + unset($fileSpec); + $this->updateFileCollection( + $this->variablesFile, + [ + 'requiredVars' => $allRequiredVars, + ] + ); + $this->debug(count($allRequiredVars) . " variables added to $this->variablesFile"); + } + + foreach ($this->allFiles as $filename => $fileSpec) { + if (str_starts_with($filename, '/')) { + continue; + } + $fullPath = str_replace($this->sourceDir, $this->targetDir, $filename); + if (!str_ends_with($fullPath, '.scss')) { + $fullPath .= '.scss'; + } + if (str_contains($filename, '..') || in_array($fullPath, $this->excludedFiles)) { + continue; + } + $lines = $fileSpec['lines'] ?? []; + + // Prepend required variables: + if ($fileSpec['requiredVars']) { + $requiredVars = $this->resolveVariableDependencies($fileSpec['requiredVars'], $fileSpec['vars'] ?? []); + $linesToAdd = ['// The following variables were automatically added in SCSS conversion']; + $addedVars = []; + foreach (array_reverse($requiredVars) as $current) { + $var = $current['var']; + if (!in_array($var, $addedVars)) { + $value = $current['value']; + $linesToAdd[] = "\$$var: $value;"; + $addedVars[] = $var; + } + } + $linesToAdd[] = ''; + array_unshift($lines, ...$linesToAdd); + } + // Write the updated file: + if (false === file_put_contents($fullPath, implode(PHP_EOL, $lines)) . PHP_EOL) { + $this->error("Could not write file $fullPath"); + } + $this->debug("$fullPath updated"); + } + + return true; + } + + /** + * Process string substitutions + * + * @param string $filename File name + * @param array $lines File contents + */ + protected function processSubstitutions(string $filename, array &$lines): void + { + if ($filename) { + $this->debug("$filename: start processing substitutions", OutputInterface::VERBOSITY_DEBUG); + } + $contents = implode(PHP_EOL, $lines); + foreach ($this->getSubstitutions() as $i => $substitution) { + if ($filename) { + $this->debug("$filename: processing substitution $i", OutputInterface::VERBOSITY_DEBUG); + } + if (str_starts_with($substitution['pattern'], '/')) { + // Regexp + if (is_string($substitution['replacement'])) { + $contents = preg_replace($substitution['pattern'], $substitution['replacement'], $contents); + } else { + $contents = preg_replace_callback( + $substitution['pattern'], + $substitution['replacement'], + $contents + ); + } + if (null === $contents) { + throw new \Exception( + "Failed to process regexp substitution $i: " . $substitution['pattern'] + . ': ' . preg_last_error_msg() + ); + } + } else { + // String + $contents = str_replace($substitution['pattern'], $substitution['replacement'], $contents); + } + } + + $lines = explode(PHP_EOL, $contents); + if ($filename) { + $this->debug("$filename: done processing substitutions", OutputInterface::VERBOSITY_DEBUG); + } + } + + /** + * Get substitutions + * + * @return array; + */ + protected function getSubstitutions(): array + { + if ($localConfigFile = $this->pathResolver->getLocalConfigPath('lessToSass.config.php')) { + $this->debug("Using local config file $localConfigFile", OutputInterface::VERBOSITY_DEBUG); + $config = include $localConfigFile; + } else { + $configFile = dirname(__FILE__) . '/../../../../config/lessToSass.config.php'; + $this->debug("Using shared config file $configFile", OutputInterface::VERBOSITY_DEBUG); + $config = include $configFile; + } + return $config; + } + + /** + * Output a debug message + * + * @param string $msg Message + * @param int $verbosity Verbosity level + * + * @return void + */ + protected function debug(string $msg, int $verbosity = OutputInterface::VERBOSITY_VERBOSE): void + { + $this->output->writeln($msg, $verbosity); + } + + /** + * Output an error message + * + * @param string $msg Message + * + * @return void + */ + protected function error(string $msg): void + { + if ($this->output) { + $this->output->writeln('' . OutputFormatter::escape($msg) . ''); + } + } + + /** + * Output a warning message + * + * @param string $msg Message + * + * @return void + */ + protected function warning(string $msg): void + { + if ($this->output) { + $this->output->writeln('' . OutputFormatter::escape($msg) . ''); + } + } + + /** + * Check if file name points to a readable file + * + * @param string $filename File name + * + * @return bool + */ + protected function isReadableFile(string $filename): bool + { + return file_exists($filename) && (is_file($filename) || is_link($filename)); + } +} diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php new file mode 100644 index 00000000000..8b1579e27c3 --- /dev/null +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php @@ -0,0 +1,70 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:developer_manual Wiki + */ + +namespace FinnaConsole\Command\Util; + +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface as ContainerException; +use Psr\Container\ContainerInterface; + +/** + * Factory for the "less to sass" task. + * + * @category VuFind + * @package Service + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:developer_manual Wiki + */ +class LessToSassCommandFactory implements FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke( + ContainerInterface $container, + $requestedName, + array $options = null + ) { + return new $requestedName($container->get(\VuFind\Config\PathResolver::class)); + } +} From 0c0d580fa59a8bee751599cea998705cd73823f1 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 22 Oct 2024 22:48:32 +0300 Subject: [PATCH 05/30] lessToSass functional --- .../FinnaConsole/config/lessToSass.config.php | 4 +- .../Command/Util/LessToSassCommand.php | 126 ++++++++---------- themes/finna2/less/finna.less | 25 +++- 3 files changed, 79 insertions(+), 76 deletions(-) diff --git a/module/FinnaConsole/config/lessToSass.config.php b/module/FinnaConsole/config/lessToSass.config.php index 22b9653774b..8667eb1e17a 100644 --- a/module/FinnaConsole/config/lessToSass.config.php +++ b/module/FinnaConsole/config/lessToSass.config.php @@ -3,7 +3,7 @@ // The first few rules up to 'literal' are based on the rules in the grunt-less-to-sass library return [ [ // functions - 'pattern' => '/(?!@debug|@import|@media|@keyframes|@font-face|@include|@extend|@mixin|@supports|@container|@if|@use|@page|@-\w)@/i', + 'pattern' => '/(?!@debug|@import|@media|@keyframes|@font-face|@include|@extend|@mixin|@supports|@container |@if |@use |@page |@-\w)@/i', 'replacement' => '$', ], [ // when => if @@ -41,7 +41,7 @@ 'replacement' => function ($matches) { [, $method, $color2, $weight] = $matches; $color1 = $method === 'shade' ? '#000000' : '#ffffff'; - return "mix($color1, $color2, $weight);"; + return "mix($color1, $color2, $weight)"; }, ], [ // fade diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php index f66d915bac3..b77f6ad64c5 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php @@ -149,24 +149,10 @@ protected function configure() 'Files not to be touched in the target directory (in addition to the ones outside of the starting' . ' directory)' ) - ->addOption( - 'less_file', - null, - InputOption::VALUE_REQUIRED, - 'Name of main LESS file to use as an entry point (relative to source_dir).', - 'compiled.less' - ) - ->addOption( - 'scss_file', - null, - InputOption::VALUE_REQUIRED, - 'Name of existing main SCSS file to use as an entry point (relative to target_dir).', - 'compiled.less' - ) ->addArgument( - 'theme_dir', + 'main_file', InputArgument::REQUIRED, - 'Theme directory' + 'Main LESS file to use as entry point' ); } @@ -182,8 +168,9 @@ protected function execute(InputInterface $input, OutputInterface $output) { $this->output = $output; $this->includePaths = $input->getOption('include_path'); - $this->sourceDir = $input->getArgument('theme_dir') . '/less'; - $this->targetDir = $input->getArgument('theme_dir') . '/scss'; + $mainFile = $input->getArgument('main_file'); + $this->sourceDir = dirname($mainFile); + $this->targetDir = preg_replace('/\/less\b/', '/scss', $this->sourceDir); if ($this->excludedFiles = $input->getOption('exclude')) { $this->excludedFiles = array_map( function ($s) { @@ -193,16 +180,12 @@ function ($s) { ); } if ($variablesFile = $input->getOption('variables_file')) { - $this->variablesFile = $this->targetDir . '/' . $variablesFile; + $this->variablesFile = $this->sourceDir . '/' . preg_replace('/\.scss$/', '', $variablesFile); } - $mainFile = $this->sourceDir . '/' . $input->getOption('less_file'); // First read all vars: if (!$this->discoverLess($mainFile, $this->allLessVars)) { return Command::FAILURE; } - if ($scssFile = $input->getOption('scss_file')) { - $mainFile = $this->targetDir . '/' . $scssFile; - } // Now do changes: $currentVars = []; if (!$this->processFile($mainFile, $currentVars)) { @@ -298,7 +281,7 @@ protected function processFile(string $filename, array &$vars): bool // Process string substitutions if (!str_ends_with($filename, '.scss')) { - $this->processSubstitutions($filename, $lines); + $lines = explode(PHP_EOL, $this->processSubstitutions($filename, implode(PHP_EOL, $lines))); $this->updateFileCollection($filename, compact('lines', 'vars')); } @@ -331,7 +314,7 @@ protected function processFile(string $filename, array &$vars): bool } // Collect variables that need to be defined: - if (str_starts_with($fileDir, $this->targetDir) && !str_contains($fileDir, '..')) { + if (!str_contains($fileDir, '..') && !str_starts_with($fileDir, '/')) { if ($newVars = $this->checkVariables($lineId, $line, $vars)) { $requiredVars = [ ...$requiredVars, @@ -371,9 +354,8 @@ protected function processLessVariables(string $lineId, string $line, array &$va } else { $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); } - $tmp = [$value]; - $this->processSubstitutions('', $tmp); - $vars[$var] = $tmp[0]; + + $vars[$var] = $value; } /** @@ -397,7 +379,7 @@ protected function processScssVariables(string $lineId, string $line, array &$va if ($existing) { if ($existing['default'] && !$default) { $this->debug( - "$lineId: '$var: $value' overrides default value '" . $vars[$var] . "'", + "$lineId: '$var: $value' overrides default value '" . $vars[$var]['value'] . "'", OutputInterface::VERBOSITY_DEBUG ); } else { @@ -459,6 +441,43 @@ protected function processImports(string $lineId, string $fileDir, string $line, return true; } + /** + * Find import file + * + * @param string $filename Relative file name + * @param string $baseDir Base directory + * + * @return ?array + */ + protected function resolveImportFileName(string $filename, string $baseDir): ?array + { + $allDirs = [ + $baseDir, + ...$this->includePaths + ]; + foreach (['less', 'scss'] as $extension) { + foreach ($allDirs as $dir) { + // full path + if (str_ends_with($filename, '.scss') || str_ends_with($filename, '.less')) { + $fullPath = "$dir/$filename"; + } else { + $fullPath = "$dir/$filename.$extension"; + } + if (!$this->isReadableFile($fullPath)) { + // reference import + $fullPath = dirname($fullPath) . '/_' . basename($fullPath); + } + if ($this->isReadableFile($fullPath)) { + return [ + 'fullPath' => $fullPath, + 'inBaseDir' => str_starts_with(realpath($fullPath), $this->sourceDir . '/'), + ]; + } + } + } + return null; + } + /** * Replace variables that are defined later with their last values * @@ -484,6 +503,7 @@ protected function checkVariables(string $lineId, string $line, array $vars): ?a continue; } // Use last defined value: + $this->debug("$lineId: Need $lessVal for $var"); $required[] = [ 'var' => $var, @@ -559,40 +579,6 @@ protected function getBlockLevelChange(string $line): int return $level; } - /** - * Find import file - * - * @param string $filename Relative file name - * @param string $baseDir Base directory - * - * @return ?array - */ - protected function resolveImportFileName(string $filename, string $baseDir): ?array - { - $allDirs = [ - $baseDir, - ...$this->includePaths - ]; - $filename = preg_replace('/\.(less|scss)$/', '', $filename); - foreach (['less', 'scss'] as $extension) { - foreach ($allDirs as $dir) { - // full path - $fullPath = "$dir/$filename.$extension"; - if (!$this->isReadableFile($fullPath)) { - // reference import - $fullPath = dirname($fullPath) . '/_' . basename($fullPath); - } - if ($this->isReadableFile($fullPath)) { - return [ - 'fullPath' => $fullPath, - 'inBaseDir' => str_starts_with(realpath($fullPath), $this->sourceDir . '/'), - ]; - } - } - } - return null; - } - /** * Update a file in the all files collection * @@ -644,6 +630,7 @@ protected function writeTargetFiles(): bool $fileSpec['requiredVars'] = []; } unset($fileSpec); + $this->updateFileCollection( $this->variablesFile, [ @@ -674,8 +661,7 @@ protected function writeTargetFiles(): bool foreach (array_reverse($requiredVars) as $current) { $var = $current['var']; if (!in_array($var, $addedVars)) { - $value = $current['value']; - $linesToAdd[] = "\$$var: $value;"; + $linesToAdd[] = $this->processSubstitutions('', "@$var: $current[value];"); $addedVars[] = $var; } } @@ -696,14 +682,15 @@ protected function writeTargetFiles(): bool * Process string substitutions * * @param string $filename File name - * @param array $lines File contents + * @param string $contents File contents + * + * @return string */ - protected function processSubstitutions(string $filename, array &$lines): void + protected function processSubstitutions(string $filename, string $contents): string { if ($filename) { $this->debug("$filename: start processing substitutions", OutputInterface::VERBOSITY_DEBUG); } - $contents = implode(PHP_EOL, $lines); foreach ($this->getSubstitutions() as $i => $substitution) { if ($filename) { $this->debug("$filename: processing substitution $i", OutputInterface::VERBOSITY_DEBUG); @@ -731,10 +718,11 @@ protected function processSubstitutions(string $filename, array &$lines): void } } - $lines = explode(PHP_EOL, $contents); if ($filename) { $this->debug("$filename: done processing substitutions", OutputInterface::VERBOSITY_DEBUG); } + + return $contents; } /** diff --git a/themes/finna2/less/finna.less b/themes/finna2/less/finna.less index 3573b34e977..ed631e6964e 100644 --- a/themes/finna2/less/finna.less +++ b/themes/finna2/less/finna.less @@ -1,20 +1,35 @@ /* #SCSS> -@import "scss-functions"; +@import "../../finna2/scss/scss-functions"; -@import "global/bootstrap-variable-overrides"; +// Custom theme variable overrides +@import "variables-custom"; + +// Variable overrides +@import "variables"; + +// Finna Bootstrap variable overrides +@import "../../finna2/scss/global/bootstrap-variable-overrides"; + +// Custom scss +@import "custom.scss"; +// Font Awesome (loaded via include path to silence warnings from dependencies) @import "font-awesome/font-awesome"; -// Bootstrap v3.4.1 -@import "bootstrap/bootstrap"; +// Bootstrap v3.4.1 (loaded via include path to silence warnings from dependencies) +@import "vendor/bootstrap/bootstrap"; // Finna variables -@import "global/variables"; +@import "../../finna2/scss/global/variables"; // Bootstrap3 theme components @import "../../bootstrap3/scss/components/cookie-consent/index"; @import "../../bootstrap3/scss/components/keyboard"; @import "../../bootstrap3/scss/components/trees"; + +@import "../../finna2/scss/global"; +@import "../../finna2/scss/finna-other"; +@import "../../finna2/scss/components"; <#SCSS */ /* #LESS> */ From f9c3e8c6dfa3e8e4df9a4dc0c6d1de2e0c8b8689 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 11:36:53 +0300 Subject: [PATCH 06/30] Fix issues, rename command. --- ...oSass.config.php => lessToScss.config.php} | 47 ++++++++ module/FinnaConsole/config/module.config.php | 4 +- ...oSassCommand.php => LessToScssCommand.php} | 114 ++++++++++++++---- ...ctory.php => LessToScssCommandFactory.php} | 2 +- 4 files changed, 139 insertions(+), 28 deletions(-) rename module/FinnaConsole/config/{lessToSass.config.php => lessToScss.config.php} (87%) rename module/FinnaConsole/src/FinnaConsole/Command/Util/{LessToSassCommand.php => LessToScssCommand.php} (90%) rename module/FinnaConsole/src/FinnaConsole/Command/Util/{LessToSassCommandFactory.php => LessToScssCommandFactory.php} (97%) diff --git a/module/FinnaConsole/config/lessToSass.config.php b/module/FinnaConsole/config/lessToScss.config.php similarity index 87% rename from module/FinnaConsole/config/lessToSass.config.php rename to module/FinnaConsole/config/lessToScss.config.php index 8667eb1e17a..fb2f96a5e75 100644 --- a/module/FinnaConsole/config/lessToSass.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -52,6 +52,53 @@ 'pattern' => '/~"(.*)"/i', 'replacement' => 'unquote("$1")', ], + // end of basic less-to-sass rules --------------- + + [ // Activate SCSS + 'pattern' => '/\/\* #SCSS>/i', + 'replacement' => '/* #SCSS> */', + ], + [ + 'pattern' => '/<#SCSS \*\//i', + 'replacement' => '/* <#SCSS */', + ], + [ + 'pattern' => '/\/\* #LESS> \*\//i', + 'replacement' => '/* #LESS>', + ], + [ + 'pattern' => '/\/\* <#LESS \*\//i', + 'replacement' => '<#LESS */', + ], + [ // Fix include parameter separator + 'pattern' => '/@include ([^\(]+)\(([^\)]+)\);/i', + 'replacement' => function ($matches) { + [, $m1, $m2] = $matches; + return '@include ' . $m1 . '(' . str_replace(';', ',', $m2) . ');'; + }, + ], + [ // Fix tilde literals + 'pattern' => "/~'(.*?)'/i", + 'replacement' => '$1', + ], + [ // Convert inline &:extends + 'pattern' => '/&:extend\(([^\)]+?)( all)?\)/i', + 'replacement' => '@extend $1', + ], + [ // Wrap variables in calcs with #{} + 'pattern' => '/calc\([^;]+/i', + 'replacement' => function ($matches) { + return preg_replace('/(\$[\w\-]+)/i', '#{$1}', $matches[0]); + }, + ], + [ // Wrap variables set to css variables with #{} + 'pattern' => '/(--[\w:-]+:\s*)((\$|darken\(|lighten\()[^;]+)/i', + 'replacement' => '$1#{$2}', + ], + [ // Remove !default from extends (icons.scss) + 'pattern' => '/@extend ([^;}]+) !default;/i', + 'replacement' => '@extend $1;', + ], [ // Fix comparison: 'pattern' => '/ ==< /i', diff --git a/module/FinnaConsole/config/module.config.php b/module/FinnaConsole/config/module.config.php index f633b79b3fc..d1eb4f9a58a 100644 --- a/module/FinnaConsole/config/module.config.php +++ b/module/FinnaConsole/config/module.config.php @@ -20,7 +20,7 @@ 'FinnaConsole\Command\Util\ExpireFinnaCacheCommand' => 'FinnaConsole\Command\Util\ExpireFinnaCacheCommandFactory', 'FinnaConsole\Command\Util\ExpireUsers' => 'FinnaConsole\Command\Util\ExpireUsersFactory', 'FinnaConsole\Command\Util\ImportComments' => 'FinnaConsole\Command\Util\ImportCommentsFactory', - 'FinnaConsole\Command\Util\LessToSassCommand' => 'FinnaConsole\Command\Util\LessToSassCommandFactory', + 'FinnaConsole\Command\Util\LessToScssCommand' => 'FinnaConsole\Command\Util\LessToScssCommandFactory', 'FinnaConsole\Command\Util\OnlinePaymentMonitor' => 'FinnaConsole\Command\Util\OnlinePaymentMonitorFactory', 'FinnaConsole\Command\Util\ProcessRecordStatsLog' => 'FinnaConsole\Command\Util\ProcessRecordStatsLogFactory', 'FinnaConsole\Command\Util\ProcessStatsQueue' => 'FinnaConsole\Command\Util\ProcessStatsQueueFactory', @@ -41,7 +41,7 @@ 'util/expire_finna_cache' => 'FinnaConsole\Command\Util\ExpireFinnaCacheCommand', 'util/expire_users' => 'FinnaConsole\Command\Util\ExpireUsers', 'util/import_comments' => 'FinnaConsole\Command\Util\ImportComments', - 'util/less_to_sass' => 'FinnaConsole\Command\Util\LessToSassCommand', + 'util/less_to_scss' => 'FinnaConsole\Command\Util\LessToScssCommand', 'util/online_payment_monitor' => 'FinnaConsole\Command\Util\OnlinePaymentMonitor', 'util/process_record_stats' => 'FinnaConsole\Command\Util\ProcessRecordStatsLog', 'util/scss_fixer' => 'FinnaConsole\Command\Util\ScssFixerCommand', diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php similarity index 90% rename from module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php rename to module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index b77f6ad64c5..86852040cd2 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -1,7 +1,7 @@ isInSourceDir($fileDir); $lineNo = 0; $this->debug("Start processing $filename (conversion)", OutputInterface::VERBOSITY_DEBUG); $lines = file($filename, FILE_IGNORE_NEW_LINES); @@ -286,9 +290,13 @@ protected function processFile(string $filename, array &$vars): bool } $inMixin = 0; + $inComment = false; $requiredVars = []; foreach ($lines as $idx => $line) { ++$lineNo; + if (trim($line) === '') { + continue; + } $lineId = "$filename:$lineNo"; $parts = explode('//', $line, 2); $line = $parts[0]; @@ -301,11 +309,21 @@ protected function processFile(string $filename, array &$vars): bool if ($inMixin) { $inMixin += $this->getBlockLevelChange($line); } - if ($inMixin) { continue; } + $cStart = strpos($line, '/*'); + $cEnd = strrpos($line, '*/'); + if (false !== $cStart && (false === $cEnd || $cEnd < $cStart)) { + $inComment = true; + } elseif (false !== $cEnd) { + $inComment = false; + } + if ($inComment) { + continue; + } + // Process variable declarations: $this->processScssVariables($lineId, $line, $vars); // Process imports: @@ -314,7 +332,7 @@ protected function processFile(string $filename, array &$vars): bool } // Collect variables that need to be defined: - if (!str_contains($fileDir, '..') && !str_starts_with($fileDir, '/')) { + if ($inSourceDir) { if ($newVars = $this->checkVariables($lineId, $line, $vars)) { $requiredVars = [ ...$requiredVars, @@ -330,6 +348,30 @@ protected function processFile(string $filename, array &$vars): bool return true; } + /** + * Check if path is in source directory + * + * @param string $path Path + * + * @return bool + */ + protected function isInSourceDir(string $path): bool + { + return str_starts_with($path, $this->sourceDir) && !str_contains($path, '..'); + } + + /** + * Check if path is in target directory + * + * @param string $path Path + * + * @return bool + */ + protected function isInTargetDir(string $path): bool + { + return str_starts_with($path, $this->targetDir) && !str_contains($path, '..'); + } + /** * Find variables in LESS * @@ -341,7 +383,7 @@ protected function processFile(string $filename, array &$vars): bool */ protected function processLessVariables(string $lineId, string $line, array &$vars): void { - if (!preg_match('/^\s*\@(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { + if (!preg_match('/^\s*\@(' . static::VARIABLE_CHARS . '+):\s*(.*?);?\s*$/', $line, $matches)) { return; } [, $var, $value] = $matches; @@ -455,14 +497,11 @@ protected function resolveImportFileName(string $filename, string $baseDir): ?ar $baseDir, ...$this->includePaths ]; + $filename = preg_replace('/\.(less|scss)$/', '', $filename); foreach (['less', 'scss'] as $extension) { foreach ($allDirs as $dir) { // full path - if (str_ends_with($filename, '.scss') || str_ends_with($filename, '.less')) { - $fullPath = "$dir/$filename"; - } else { - $fullPath = "$dir/$filename.$extension"; - } + $fullPath = "$dir/$filename.$extension"; if (!$this->isReadableFile($fullPath)) { // reference import $fullPath = dirname($fullPath) . '/_' . basename($fullPath); @@ -493,7 +532,7 @@ protected function checkVariables(string $lineId, string $line, array $vars): ?a preg_match_all('/\$(' . static::VARIABLE_CHARS . '+)(?!.*:)\\b/', $line, $allMatches); foreach ($allMatches[1] ?? [] as $var) { $lessVal = $this->allLessVars[$var] ?? null; - if (isset($vars[$var]) && $vars[$var]['value'] === $lessVal) { + if (isset($vars[$var]) && $vars[$var]['value'] === $this->processSubstitutions('', $lessVal)) { // Previous definition contains the correct value: $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERY_VERBOSE); continue; @@ -528,7 +567,7 @@ protected function resolveVariableDependencies(array $vars, array $knownVars): a $var = $current['var']; $varDefinition = $current['value']; $loop = 0; - while (preg_match('/\$(' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { + while (preg_match('/[@\$](' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { $requiredVar = $matches[1]; if (in_array($requiredVar, $knownVars)) { $this->debug( @@ -626,6 +665,7 @@ protected function writeTargetFiles(): bool if ($fileSpec['index'] < $variablesFileIndex) { continue; } + array_push($allRequiredVars, ...$fileSpec['requiredVars']); $fileSpec['requiredVars'] = []; } @@ -641,14 +681,8 @@ protected function writeTargetFiles(): bool } foreach ($this->allFiles as $filename => $fileSpec) { - if (str_starts_with($filename, '/')) { - continue; - } - $fullPath = str_replace($this->sourceDir, $this->targetDir, $filename); - if (!str_ends_with($fullPath, '.scss')) { - $fullPath .= '.scss'; - } - if (str_contains($filename, '..') || in_array($fullPath, $this->excludedFiles)) { + $fullPath = $this->getTargetFilename($filename); + if (!$this->isInTargetDir($fullPath)) { continue; } $lines = $fileSpec['lines'] ?? []; @@ -665,6 +699,20 @@ protected function writeTargetFiles(): bool $addedVars[] = $var; } } + + // Remove later definitions for the required variables: + foreach ($addedVars as $var) { + foreach ($lines as &$line) { + $line = preg_replace( + '/^(\s*\$' . preg_quote($var) . ':.*)$/', + '/* $1 // Commented out in SCSS conversion */', + $line + ); + } + unset($line); + } + + // Prepend new definitions: $linesToAdd[] = ''; array_unshift($lines, ...$linesToAdd); } @@ -678,6 +726,22 @@ protected function writeTargetFiles(): bool return true; } + /** + * Get target file name + * + * @param string $filename File name + * + * @return string + */ + protected function getTargetFilename(string $filename): string + { + $fullPath = str_replace($this->sourceDir, $this->targetDir, $filename); + if (!str_ends_with($fullPath, '.scss')) { + $fullPath .= '.scss'; + } + return $fullPath; + } + /** * Process string substitutions * @@ -732,11 +796,11 @@ protected function processSubstitutions(string $filename, string $contents): str */ protected function getSubstitutions(): array { - if ($localConfigFile = $this->pathResolver->getLocalConfigPath('lessToSass.config.php')) { + if ($localConfigFile = $this->pathResolver->getLocalConfigPath('lessToScss.config.php')) { $this->debug("Using local config file $localConfigFile", OutputInterface::VERBOSITY_DEBUG); $config = include $localConfigFile; } else { - $configFile = dirname(__FILE__) . '/../../../../config/lessToSass.config.php'; + $configFile = dirname(__FILE__) . '/../../../../config/lessToScss.config.php'; $this->debug("Using shared config file $configFile", OutputInterface::VERBOSITY_DEBUG); $config = include $configFile; } diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommandFactory.php similarity index 97% rename from module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php rename to module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommandFactory.php index 8b1579e27c3..ece405b248c 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToSassCommandFactory.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommandFactory.php @@ -44,7 +44,7 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link http://vufind.org/wiki/vufind2:developer_manual Wiki */ -class LessToSassCommandFactory implements FactoryInterface +class LessToScssCommandFactory implements FactoryInterface { /** * Create an object From 7229aa728f98d7147e219f32ac138d623c068de6 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 11:49:12 +0300 Subject: [PATCH 07/30] Revert finna2/less/finna.less --- themes/finna2/less/finna.less | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/themes/finna2/less/finna.less b/themes/finna2/less/finna.less index ed631e6964e..3573b34e977 100644 --- a/themes/finna2/less/finna.less +++ b/themes/finna2/less/finna.less @@ -1,35 +1,20 @@ /* #SCSS> -@import "../../finna2/scss/scss-functions"; +@import "scss-functions"; -// Custom theme variable overrides -@import "variables-custom"; - -// Variable overrides -@import "variables"; - -// Finna Bootstrap variable overrides -@import "../../finna2/scss/global/bootstrap-variable-overrides"; - -// Custom scss -@import "custom.scss"; +@import "global/bootstrap-variable-overrides"; -// Font Awesome (loaded via include path to silence warnings from dependencies) @import "font-awesome/font-awesome"; -// Bootstrap v3.4.1 (loaded via include path to silence warnings from dependencies) -@import "vendor/bootstrap/bootstrap"; +// Bootstrap v3.4.1 +@import "bootstrap/bootstrap"; // Finna variables -@import "../../finna2/scss/global/variables"; +@import "global/variables"; // Bootstrap3 theme components @import "../../bootstrap3/scss/components/cookie-consent/index"; @import "../../bootstrap3/scss/components/keyboard"; @import "../../bootstrap3/scss/components/trees"; - -@import "../../finna2/scss/global"; -@import "../../finna2/scss/finna-other"; -@import "../../finna2/scss/components"; <#SCSS */ /* #LESS> */ From cb2fd4619763c0314f9d8adac2ce68bcf1457ffb Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 11:50:40 +0300 Subject: [PATCH 08/30] Update custom theme import order. --- themes/custom/less/finna.less | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/themes/custom/less/finna.less b/themes/custom/less/finna.less index 8caa6e8060f..2d262c7082b 100644 --- a/themes/custom/less/finna.less +++ b/themes/custom/less/finna.less @@ -1,18 +1,15 @@ /* #SCSS> @import "../../finna2/scss/scss-functions"; -// Custom theme variable overrides -@import "variables-custom"; - // Variable overrides @import "variables"; +// Custom theme variable overrides +@import "variables-custom"; + // Finna Bootstrap variable overrides @import "../../finna2/scss/global/bootstrap-variable-overrides"; -// Custom scss -@import "custom.scss"; - // Font Awesome (loaded via include path to silence warnings from dependencies) @import "font-awesome/font-awesome"; @@ -31,6 +28,9 @@ @import "../../finna2/scss/finna-other"; @import "../../finna2/scss/components"; +// Custom scss +@import "custom.scss"; + <#SCSS */ /* #LESS> */ @import "../../finna2/less/finna.less"; From dc42531dc10601399df5394c20fffd362c23381b Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 11:50:59 +0300 Subject: [PATCH 09/30] Fix null as string --- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 86852040cd2..57e91f643bd 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -532,7 +532,11 @@ protected function checkVariables(string $lineId, string $line, array $vars): ?a preg_match_all('/\$(' . static::VARIABLE_CHARS . '+)(?!.*:)\\b/', $line, $allMatches); foreach ($allMatches[1] ?? [] as $var) { $lessVal = $this->allLessVars[$var] ?? null; - if (isset($vars[$var]) && $vars[$var]['value'] === $this->processSubstitutions('', $lessVal)) { + if ( + isset($vars[$var]) + && null !== $lessVal + && $vars[$var]['value'] === $this->processSubstitutions('', $lessVal) + ) { // Previous definition contains the correct value: $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERY_VERBOSE); continue; From 86b80b888f314371bb44b6726e00f90b8c1414a0 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 12:43:03 +0300 Subject: [PATCH 10/30] Fix code style. --- .../FinnaConsole/config/lessToScss.config.php | 8 ++-- .../Command/Util/LessToScssCommand.php | 34 +++++++------- .../Command/Util/ScssFixerCommand.php | 44 +++++++++++-------- .../Command/Util/ScssFixerCommandFactory.php | 1 - 4 files changed, 47 insertions(+), 40 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index fb2f96a5e75..5f095e4288a 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -277,15 +277,15 @@ ], [ // another typo in home column styles 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', - 'replacement' => "\$1\$2", + 'replacement' => '$1$2', ], [ // missing semicolon: display: none 'pattern' => '/display: none\n/', 'replacement' => 'display: none;', ], [ // missing semicolon in variable definitions - 'pattern' => '/(\n\s*\$' . static::VARIABLE_CHARS . '+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', - 'replacement' => "\$1;\$2", + 'pattern' => '/(\n\s*\$[a-zA-Z_-]+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', + 'replacement' => '$1;$2', ], [ // missing semicolon: $header-text-color: #000000 'pattern' => '/$header-text-color: #000000\n/', @@ -297,7 +297,7 @@ ], [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', - 'replacement' => "\$finna-feedback-background: darken(#d80073, 10%);\$1", + 'replacement' => '$finna-feedback-background: darken(#d80073, 10%);$1', ], [ // invalid (and obsolete) rule 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 57e91f643bd..883d64e3dd3 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -38,6 +38,12 @@ use Symfony\Component\Console\Output\OutputInterface; use VuFind\Config\PathResolver; +use function array_key_exists; +use function count; +use function dirname; +use function in_array; +use function is_string; + /** * Console command: convert style files from LESS to SCSS (Sass). * @@ -53,7 +59,7 @@ )] class LessToScssCommand extends Command { - const VARIABLE_CHARS = '[a-zA-Z_-]'; + public const VARIABLE_CHARS = '[a-zA-Z_-]'; /** * Include paths @@ -336,7 +342,7 @@ protected function processFile(string $filename, array &$vars): bool if ($newVars = $this->checkVariables($lineId, $line, $vars)) { $requiredVars = [ ...$requiredVars, - ...$newVars + ...$newVars, ]; } } @@ -457,25 +463,22 @@ protected function processImports(string $lineId, string $fileDir, string $line, $this->debug("$lineId: skipping .css import"); return true; } - if (!($pathInfo = $this->resolveImportFileName($import, $fileDir))) { + if (!($fullPath = $this->resolveImportFileName($import, $fileDir))) { $targetFileDir = str_replace($this->sourceDir, $this->targetDir, $fileDir); $targetFileDir = str_replace('/less', '/scss', $targetFileDir); $targetImport = str_replace('/less/', '/scss/', $import); - if (!($pathInfo = $this->resolveImportFileName($targetImport, $targetFileDir))) { + if (!($fullPath = $this->resolveImportFileName($targetImport, $targetFileDir))) { $this->error("$lineId: import file $import not found"); return false; } } else { - $this->debug( - "$lineId: import $pathInfo[fullPath] as $import" . ($pathInfo['inBaseDir'] ? ' (IN BASE)' : ''), - OutputInterface::VERBOSITY_DEBUG - ); + $this->debug("$lineId: import $fullPath as $import", OutputInterface::VERBOSITY_DEBUG); if ($discover) { - if (!$this->discoverLess($pathInfo['fullPath'], $vars)) { + if (!$this->discoverLess($fullPath, $vars)) { return false; } } else { - if (!$this->processFile($pathInfo['fullPath'], $vars, $discover, $pathInfo['inBaseDir'])) { + if (!$this->processFile($fullPath, $vars)) { return false; } } @@ -489,13 +492,13 @@ protected function processImports(string $lineId, string $fileDir, string $line, * @param string $filename Relative file name * @param string $baseDir Base directory * - * @return ?array + * @return ?string */ - protected function resolveImportFileName(string $filename, string $baseDir): ?array + protected function resolveImportFileName(string $filename, string $baseDir): ?string { $allDirs = [ $baseDir, - ...$this->includePaths + ...$this->includePaths, ]; $filename = preg_replace('/\.(less|scss)$/', '', $filename); foreach (['less', 'scss'] as $extension) { @@ -507,10 +510,7 @@ protected function resolveImportFileName(string $filename, string $baseDir): ?ar $fullPath = dirname($fullPath) . '/_' . basename($fullPath); } if ($this->isReadableFile($fullPath)) { - return [ - 'fullPath' => $fullPath, - 'inBaseDir' => str_starts_with(realpath($fullPath), $this->sourceDir . '/'), - ]; + return $fullPath; } } } diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php index a5be7f1479f..b09852d13f9 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php @@ -38,6 +38,12 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function array_key_exists; +use function count; +use function dirname; +use function in_array; +use function is_string; + /** * Console command: fix SCSS variable declarations. * @@ -53,7 +59,7 @@ )] class ScssFixerCommand extends Command { - const VARIABLE_CHARS = '[a-zA-Z_-]'; + public const VARIABLE_CHARS = '[a-zA-Z_-]'; /** * Include paths @@ -169,11 +175,10 @@ protected function execute(InputInterface $input, OutputInterface $output) /** * Process a file * - * @param string $filename File name - * @param array $vars Currently defined variables - * @param OutputInterface $output Output object - * @param bool $discover Whether to just discover files and their content - * @param bool $change Whether to do changes to the file + * @param string $filename File name + * @param array $vars Currently defined variables + * @param bool $discover Whether to just discover files and their content + * @param bool $change Whether to do changes to the file * * @return bool */ @@ -239,7 +244,7 @@ protected function processFile( if ($newVars = $this->checkVariables($lineId, $line, $vars)) { $requiredVars = [ ...$requiredVars, - ...$newVars + ...$newVars, ]; } $lines[$idx] = $line . ($comments ? "//$comments" : ''); @@ -252,7 +257,6 @@ protected function processFile( return true; } - /** * Find variables * @@ -335,7 +339,7 @@ protected function checkVariables(string $lineId, string $line, array $vars): ?a $lastLine = $line; $line = preg_replace_callback( '/\$(' . static::VARIABLE_CHARS . '+)(?!.*:)\\b/', - function ($matches) use ($vars, $lineId, &$ok, &$required) { + function ($matches) use ($vars, $lineId, &$required) { $var = $matches[1]; $lastVal = $this->allVars[$var] ?? null; if (isset($vars[$var]) && $vars[$var] === $lastVal) { @@ -396,7 +400,7 @@ protected function resolveImportFileName(string $filename, string $baseDir): ?ar } $allDirs = [ $baseDir, - ...$this->includePaths + ...$this->includePaths, ]; foreach ($allDirs as $dir) { // full import @@ -564,6 +568,8 @@ protected function resolveVariableDependencies(array $vars, array $knownVars): a * * @param string $filename File name * @param array $lines File contents + * + * @return void */ protected function processSubstitutions(string $filename, array &$lines): void { @@ -624,7 +630,7 @@ protected function getSubstitutions(): array ], [ // Revert @page => $page change: 'pattern' => '$page ', - 'replacement' => "@page ", + 'replacement' => '@page ', ], [ // Fix comparison: 'pattern' => '/ ==< /i', @@ -793,18 +799,20 @@ protected function getSubstitutions(): array 'replacement' => '// Not supported in SCSS: @extend .finna-panel-default .panel-heading;', ], - [ // gradient mixin call - 'pattern' => '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', - 'replacement' => 'gradient-vertical($background-start-color, $background-end-color, $background-start-percent, $background-end-percent);', + 'pattern' => '#gradient.vertical($background-start-color; $background-end-color;' + . ' $background-start-percent; $background-end-percent);', + 'replacement' => 'gradient-vertical($background-start-color, $background-end-color' + . ', $background-start-percent, $background-end-percent);', ], [ // common typo in home column styles - 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?\& .right-column-content \{.*?\}.*?\})/s', + 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?' + . '\& .right-column-content \{.*?\}.*?\})/s', 'replacement' => "\$1\$2\n}", ], [ // another typo in home column styles 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', - 'replacement' => "\$1\$2", + 'replacement' => '$1$2', ], [ // missing semicolon: display: none 'pattern' => '/display: none\n/', @@ -812,7 +820,7 @@ protected function getSubstitutions(): array ], [ // missing semicolon in variable definitions 'pattern' => '/(\n\s*\$' . static::VARIABLE_CHARS . '+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', - 'replacement' => "\$1;\$2", + 'replacement' => '$1;$2', ], [ // missing semicolon: $header-text-color: #000000 'pattern' => '/$header-text-color: #000000\n/', @@ -824,7 +832,7 @@ protected function getSubstitutions(): array ], [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', - 'replacement' => "\$finna-feedback-background: darken(#d80073, 10%);\$1", + 'replacement' => '$finna-feedback-background: darken(#d80073, 10%);$1', ], [ // invalid (and obsolete) rule 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php index ddd4f5d47f6..23001e2aec4 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php @@ -29,7 +29,6 @@ namespace FinnaConsole\Command\Util; -use Finna\Db\Service\FinnaRecordServiceInterface; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Laminas\ServiceManager\Factory\FactoryInterface; From 54a3b7cce6b675d2f54a9768a6d0d2b8f2deafb0 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 13:37:56 +0300 Subject: [PATCH 11/30] Handle nested extends. --- .../FinnaConsole/config/lessToScss.config.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index 5f095e4288a..0eb29d7a90c 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -262,11 +262,26 @@ 'pattern' => '$home-2_fi {', 'replacement' => '.home-2_fi {', ], - [ // disable unsupported extend + [ // Convert unsupported nested extend 'pattern' => '@extend .finna-panel-default .panel-heading;', - 'replacement' => '// Not supported in SCSS: @extend .finna-panel-default .panel-heading;', + 'replacement' => << '@extend .finna-panel-default .finna-panel-heading-inner;', + 'replacement' => << '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', 'replacement' => 'gradient-vertical($background-start-color, $background-end-color, $background-start-percent, $background-end-percent);', @@ -312,7 +327,6 @@ 'pattern' => 'calc(100vh - "#{$navbar-height}~")', 'replacement' => 'calc(100vh - #{$navbar-height})', ], - [ // math without calc 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', 'replacement' => function ($matches) { From 34a79d3020facb0b48b88311b463fd0aca08e97d Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 13:57:11 +0300 Subject: [PATCH 12/30] lessToSass --- themes/custom/scss/finna.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/themes/custom/scss/finna.scss b/themes/custom/scss/finna.scss index 4a3805470b4..a51edc74cf1 100644 --- a/themes/custom/scss/finna.scss +++ b/themes/custom/scss/finna.scss @@ -1,18 +1,15 @@ /* #SCSS> */ @import "../../finna2/scss/scss-functions"; -// Custom theme variable overrides -@import "variables-custom"; - // Variable overrides @import "variables"; +// Custom theme variable overrides +@import "variables-custom"; + // Finna Bootstrap variable overrides @import "../../finna2/scss/global/bootstrap-variable-overrides"; -// Custom scss -@import "custom.scss"; - // Font Awesome (loaded via include path to silence warnings from dependencies) @import "font-awesome/font-awesome"; @@ -31,5 +28,8 @@ @import "../../finna2/scss/finna-other"; @import "../../finna2/scss/components"; +// Custom scss +@import "custom.scss"; + /* <#SCSS */ From 47afe20f54221f6a02c9b86322a0f605e7e170f8 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 14:04:34 +0300 Subject: [PATCH 13/30] Tweaks. --- module/FinnaConsole/config/lessToScss.config.php | 4 ++-- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index 0eb29d7a90c..b952ef0073d 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -269,7 +269,7 @@ outline: 1px solid \$gray-lighter; box-shadow: 1px 1px 1px 1px \$gray-lighter; background-color: \$gray-ultralight; - EOT, + EOT, ], [ // Convert unsupported nested extend 'pattern' => '@extend .finna-panel-default .finna-panel-heading-inner;', @@ -278,7 +278,7 @@ display: inline-block; width: 100%; padding: 10px; - EOT, + EOT, ], diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 883d64e3dd3..4d0d92b4ae5 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -724,7 +724,7 @@ protected function writeTargetFiles(): bool if (false === file_put_contents($fullPath, implode(PHP_EOL, $lines)) . PHP_EOL) { $this->error("Could not write file $fullPath"); } - $this->debug("$fullPath updated"); + $this->debug("Created $fullPath"); } return true; From e4fd4559d92f9bff958988a9c961e60385a7dab0 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 14:25:11 +0300 Subject: [PATCH 14/30] Handle multiple imports --- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 4d0d92b4ae5..cb606eee697 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -648,6 +648,11 @@ protected function updateFileCollection(string $filename, array $values): void $values['lines'] = file($filename, FILE_IGNORE_NEW_LINES); } } + // Merge requiredVars in case the file is imported multiple times + $values['requiredVars'] = array_merge( + $oldValues['requiredVars'] ?? [], + $values['requiredVars'] ?? [] + ); $this->allFiles[$barename] = array_merge($oldValues, $values); } From 5de8efd4ec2a8356ae541cf9d245383cb8ad41bb Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 14:36:49 +0300 Subject: [PATCH 15/30] Use standard vertical gradient. --- module/FinnaConsole/config/lessToScss.config.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index b952ef0073d..c98fd6f8c61 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -281,10 +281,9 @@ EOT, ], - [ // gradient mixin call 'pattern' => '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', - 'replacement' => 'gradient-vertical($background-start-color, $background-end-color, $background-start-percent, $background-end-percent);', + 'replacement' => 'background-image: linear-gradient(to bottom, $background-start-color $background-start-percent, $background-end-color $background-end-percent);', ], [ // common typo in home column styles 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?\& .right-column-content \{.*?\}.*?\})/s', From 4a2e4622a037dca603c6b26932be0a9ce9acc39d Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 15:23:03 +0300 Subject: [PATCH 16/30] Avoid useless capture. --- module/FinnaConsole/config/lessToScss.config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index c98fd6f8c61..4ca344fc962 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -151,7 +151,7 @@ 'replacement' => '@media #{$1}', ], [ // missing semicolon: - 'pattern' => '/(.+:.*auto)\n/', + 'pattern' => '/(:.*auto)\n/', 'replacement' => "\$1;\n", ], [ // lost space in mixin declarations: From 963f3d3258f63c949a4fabd7c8d3552b62490b37 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 15:35:28 +0300 Subject: [PATCH 17/30] Include numbers in variables. --- module/FinnaConsole/config/lessToScss.config.php | 6 +++--- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 4 ++-- .../src/FinnaConsole/Command/Util/ScssFixerCommand.php | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index 4ca344fc962..cc66b2a89fa 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -11,7 +11,7 @@ 'replacement' => '@if $3==$4 {$5@mixin $1($2){$5$6}}', ], [ // .class => @extend .class - 'pattern' => '/\.([[a-zA-Z-_]*)\s*;/i', + 'pattern' => '/\.([[a-zA-Z0-9-_]*)\s*;/i', 'replacement' => '@extend .$1;', ], [ // Remove .less extension from imports @@ -131,7 +131,7 @@ }, ], [ // remove invalid &: - 'pattern' => '/([a-zA-Z])&:/', + 'pattern' => '/([a-zA-Z0-9])&:/', 'replacement' => '$1:', ], [ // remove (reference) from import): @@ -298,7 +298,7 @@ 'replacement' => 'display: none;', ], [ // missing semicolon in variable definitions - 'pattern' => '/(\n\s*\$[a-zA-Z_-]+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', + 'pattern' => '/(\n\s*\$[a-zA-Z0-9_-]+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', 'replacement' => '$1;$2', ], [ // missing semicolon: $header-text-color: #000000 diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index cb606eee697..631ca6ea2b4 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -59,7 +59,7 @@ )] class LessToScssCommand extends Command { - public const VARIABLE_CHARS = '[a-zA-Z_-]'; + public const VARIABLE_CHARS = '[a-zA-Z0-9_-]'; /** * Include paths @@ -542,7 +542,7 @@ protected function checkVariables(string $lineId, string $line, array $vars): ?a continue; } if (null === $lessVal) { - $this->warning("$lineId: Value for variable '$var' not found"); + $this->warning("$lineId: Value for variable '$var' not found (line: $line)"); continue; } // Use last defined value: diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php index b09852d13f9..661e894bce8 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php @@ -59,7 +59,7 @@ )] class ScssFixerCommand extends Command { - public const VARIABLE_CHARS = '[a-zA-Z_-]'; + public const VARIABLE_CHARS = '[a-zA-Z0-9_-]'; /** * Include paths @@ -663,7 +663,7 @@ protected function getSubstitutions(): array }, ], [ // remove invalid &: - 'pattern' => '/([a-zA-Z])&:/', + 'pattern' => '/([a-zA-Z0-9])&:/', 'replacement' => '$1:', ], [ // remove (reference) from import): From 274c9a71a6901c563a96fc23e93e8ee488db6910 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Wed, 23 Oct 2024 15:51:27 +0300 Subject: [PATCH 18/30] Revert bad change. --- module/FinnaConsole/config/lessToScss.config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index cc66b2a89fa..038a5988003 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -11,7 +11,7 @@ 'replacement' => '@if $3==$4 {$5@mixin $1($2){$5$6}}', ], [ // .class => @extend .class - 'pattern' => '/\.([[a-zA-Z0-9-_]*)\s*;/i', + 'pattern' => '/\.([[a-zA-Z-_]*)\s*;/i', 'replacement' => '@extend .$1;', ], [ // Remove .less extension from imports From 83dc055309cb8c7f2cb21a3f297f2ef5f716c2f4 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 09:14:39 +0300 Subject: [PATCH 19/30] Fix media queries and interpolation, improve debug --- .../FinnaConsole/config/lessToScss.config.php | 8 +++---- .../Command/Util/LessToScssCommand.php | 23 +++++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index 038a5988003..99156442604 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -158,10 +158,6 @@ 'pattern' => '/(\@mixin.+){/', 'replacement' => '$1 {', ], - [ // special cases: media query variables - 'pattern' => '/(\$(mobile-portrait|mobile|tablet|desktop):\s*)(.*?);/s', - 'replacement' => '$1"$2";', - ], [ // special cases: mobile mixin 'pattern' => '/\.mobile\(\{(.*?)\}\);/s', 'replacement' => '@media #{$mobile} { & { $1 } }', @@ -336,4 +332,8 @@ return $pre . "calc($math)"; }, ], + [ // variable interpolation + 'pattern' => '/\$\{([A-Za-z0-9_-]+)\}/', + 'replacement' => '#{\$$1}', + ], ]; diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 631ca6ea2b4..040320d9cac 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -117,6 +117,13 @@ class LessToScssCommand extends Command */ protected $excludedFiles = []; + /** + * Substitutions (regexp and string replace) + * + * @var array + */ + protected $substitutions = []; + /** * Constructor * @@ -173,6 +180,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $this->output = $output; + $this->substitutions = $this->getSubstitutions(); $this->includePaths = $input->getOption('include_path'); $mainFile = $input->getArgument('main_file'); $this->sourceDir = dirname($mainFile); @@ -674,7 +682,6 @@ protected function writeTargetFiles(): bool if ($fileSpec['index'] < $variablesFileIndex) { continue; } - array_push($allRequiredVars, ...$fileSpec['requiredVars']); $fileSpec['requiredVars'] = []; } @@ -710,8 +717,8 @@ protected function writeTargetFiles(): bool } // Remove later definitions for the required variables: - foreach ($addedVars as $var) { - foreach ($lines as &$line) { + foreach ($lines as &$line) { + foreach ($addedVars as $var) { $line = preg_replace( '/^(\s*\$' . preg_quote($var) . ':.*)$/', '/* $1 // Commented out in SCSS conversion */', @@ -754,7 +761,7 @@ protected function getTargetFilename(string $filename): string /** * Process string substitutions * - * @param string $filename File name + * @param string $filename File name (or empty string when converting variables) * @param string $contents File contents * * @return string @@ -763,10 +770,14 @@ protected function processSubstitutions(string $filename, string $contents): str { if ($filename) { $this->debug("$filename: start processing substitutions", OutputInterface::VERBOSITY_DEBUG); + } else { + $this->debug("Start processing substitutions for '$contents'", OutputInterface::VERBOSITY_DEBUG); } - foreach ($this->getSubstitutions() as $i => $substitution) { + foreach ($this->substitutions as $i => $substitution) { if ($filename) { $this->debug("$filename: processing substitution $i", OutputInterface::VERBOSITY_DEBUG); + } else { + $this->debug("Processing substitution $i for '$contents'", OutputInterface::VERBOSITY_DEBUG); } if (str_starts_with($substitution['pattern'], '/')) { // Regexp @@ -793,6 +804,8 @@ protected function processSubstitutions(string $filename, string $contents): str if ($filename) { $this->debug("$filename: done processing substitutions", OutputInterface::VERBOSITY_DEBUG); + } else { + $this->debug("Done processing substitutions for '$contents'", OutputInterface::VERBOSITY_DEBUG); } return $contents; From caa77840d044b424d8e4cecb5cfb3eb28a18145d Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 09:31:55 +0300 Subject: [PATCH 20/30] Add glob support etc. --- .../Command/Util/LessToScssCommand.php | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 040320d9cac..ea81a84e319 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -32,6 +32,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -111,7 +112,7 @@ class LessToScssCommand extends Command protected $variablesFile = null; /** - * Files excluded from output + * Files excluded from processing * * @var array */ @@ -147,7 +148,7 @@ protected function configure() 'variables_file', null, InputOption::VALUE_REQUIRED, - 'File to use for added SCSS variables (may be relative to ' + 'File to use for added SCSS variables (may be relative to the target directory)' ) ->addOption( 'include_path', @@ -159,13 +160,12 @@ protected function configure() 'exclude', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Files not to be touched in the target directory (in addition to the ones outside of the starting' - . ' directory)' + 'Files to skip as main LESS files' ) ->addArgument( 'main_file', - InputArgument::REQUIRED, - 'Main LESS file to use as entry point' + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'Main LESS file to use as entry point. Can also be a glob pattern.' ); } @@ -182,36 +182,41 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->output = $output; $this->substitutions = $this->getSubstitutions(); $this->includePaths = $input->getOption('include_path'); - $mainFile = $input->getArgument('main_file'); - $this->sourceDir = dirname($mainFile); - $this->targetDir = preg_replace('/\/less\b/', '/scss', $this->sourceDir); - if ($this->excludedFiles = $input->getOption('exclude')) { - $this->excludedFiles = array_map( - function ($s) { - return $this->targetDir . '/' . $s; - }, - $this->excludedFiles - ); - } - if ($variablesFile = $input->getOption('variables_file')) { - $this->variablesFile = $this->sourceDir . '/' . preg_replace('/\.scss$/', '', $variablesFile); - } - // First read all vars: - if (!$this->discoverLess($mainFile, $this->allLessVars)) { - return Command::FAILURE; - } - // Now do changes: - $currentVars = []; - if (!$this->processFile($mainFile, $currentVars)) { - $this->error('Stop on failure'); - return Command::FAILURE; - } + $this->excludedFiles = $input->getOption('exclude'); + $variablesFile = $input->getOption('variables_file'); + $patterns = $input->getArgument('main_file'); - // Write out the target files: - if (!$this->writeTargetFiles()) { - return Command::FAILURE; - } + foreach ($patterns as $pattern) { + foreach (glob($pattern) as $mainFile) { + if (in_array($mainFile, $this->excludedFiles)) { + continue; + } + $this->output->writeln("Processing $mainFile"); + $this->allLessVars = []; + + $this->sourceDir = dirname($mainFile); + $this->targetDir = preg_replace('/\/less\b/', '/scss', $this->sourceDir); + $this->variablesFile = $variablesFile + ? $this->sourceDir . '/' . preg_replace('/\.scss$/', '', $variablesFile) + : null; + // First read all vars: + if (!$this->discoverLess($mainFile, $this->allLessVars)) { + return Command::FAILURE; + } + // Now do changes: + $currentVars = []; + if (!$this->processFile($mainFile, $currentVars)) { + $this->error('Stop on failure'); + return Command::FAILURE; + } + // Write out the target files: + if (!$this->writeTargetFiles()) { + return Command::FAILURE; + } + $this->output->writeln("Done processing $mainFile"); + } + } return Command::SUCCESS; } @@ -671,6 +676,12 @@ protected function updateFileCollection(string $filename, array $values): void */ protected function writeTargetFiles(): bool { + if (!file_exists($this->targetDir)) { + if (!mkdir($this->targetDir, 0777, true)) { + $this->error("Could not create target directory $this->targetDir"); + return false; + } + } // If we have a variables file, collect all variables needed by later files and add them: if ($this->variablesFile) { $variablesFileIndex = $this->allFiles[$this->variablesFile]['index'] ?? PHP_INT_MAX; From 40a3f2cb7ff9c8ed92a3538dd3d3b867e2aba03a Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 09:37:52 +0300 Subject: [PATCH 21/30] Reset file list on start. --- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index ea81a84e319..b086b8ca068 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -192,6 +192,7 @@ protected function execute(InputInterface $input, OutputInterface $output) continue; } $this->output->writeln("Processing $mainFile"); + $this->allFiles = []; $this->allLessVars = []; $this->sourceDir = dirname($mainFile); @@ -214,7 +215,6 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$this->writeTargetFiles()) { return Command::FAILURE; } - $this->output->writeln("Done processing $mainFile"); } } return Command::SUCCESS; From 97f4b48c362cb8f62e9af37c99a54ef8469cdde1 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 09:40:12 +0300 Subject: [PATCH 22/30] Fix indentation. --- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index b086b8ca068..8046deaee1b 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -729,7 +729,7 @@ protected function writeTargetFiles(): bool // Remove later definitions for the required variables: foreach ($lines as &$line) { - foreach ($addedVars as $var) { + foreach ($addedVars as $var) { $line = preg_replace( '/^(\s*\$' . preg_quote($var) . ':.*)$/', '/* $1 // Commented out in SCSS conversion */', From dc49aa52951e9bd0a5f492956d08692837128914 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 10:17:03 +0300 Subject: [PATCH 23/30] Allow fnmatch patterns in exclude --- .../src/FinnaConsole/Command/Util/LessToScssCommand.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 8046deaee1b..b92b151a940 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -160,7 +160,7 @@ protected function configure() 'exclude', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Files to skip as main LESS files' + 'Files to skip as main LESS files (fnmatch patterns)' ) ->addArgument( 'main_file', @@ -188,8 +188,10 @@ protected function execute(InputInterface $input, OutputInterface $output) foreach ($patterns as $pattern) { foreach (glob($pattern) as $mainFile) { - if (in_array($mainFile, $this->excludedFiles)) { - continue; + foreach ($this->excludedFiles as $exclude) { + if (fnmatch($exclude, $mainFile)) { + continue 2; + } } $this->output->writeln("Processing $mainFile"); $this->allFiles = []; From 03bd1fe80e5e918a61f5a1c274e099af22fb79e6 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 10:37:31 +0300 Subject: [PATCH 24/30] Drop ScssFixer --- module/FinnaConsole/config/module.config.php | 2 - .../Command/Util/ScssFixerCommand.php | 916 ------------------ .../Command/Util/ScssFixerCommandFactory.php | 70 -- 3 files changed, 988 deletions(-) delete mode 100644 module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php delete mode 100644 module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php diff --git a/module/FinnaConsole/config/module.config.php b/module/FinnaConsole/config/module.config.php index d1eb4f9a58a..2563c44f661 100644 --- a/module/FinnaConsole/config/module.config.php +++ b/module/FinnaConsole/config/module.config.php @@ -25,7 +25,6 @@ 'FinnaConsole\Command\Util\ProcessRecordStatsLog' => 'FinnaConsole\Command\Util\ProcessRecordStatsLogFactory', 'FinnaConsole\Command\Util\ProcessStatsQueue' => 'FinnaConsole\Command\Util\ProcessStatsQueueFactory', 'FinnaConsole\Command\Util\ScheduledAlerts' => 'VuFindConsole\Command\ScheduledSearch\NotifyCommandFactory', - 'FinnaConsole\Command\Util\ScssFixerCommand' => 'FinnaConsole\Command\Util\ScssFixerCommandFactory', 'FinnaConsole\Command\Util\VerifyRecordLinks' => 'FinnaConsole\Command\Util\VerifyRecordLinksFactory', 'FinnaConsole\Command\Util\VerifyResourceMetadata' => 'FinnaConsole\Command\Util\VerifyResourceMetadataFactory', ], @@ -44,7 +43,6 @@ 'util/less_to_scss' => 'FinnaConsole\Command\Util\LessToScssCommand', 'util/online_payment_monitor' => 'FinnaConsole\Command\Util\OnlinePaymentMonitor', 'util/process_record_stats' => 'FinnaConsole\Command\Util\ProcessRecordStatsLog', - 'util/scss_fixer' => 'FinnaConsole\Command\Util\ScssFixerCommand', 'util/verify_record_links' => 'FinnaConsole\Command\Util\VerifyRecordLinks', 'util/verify_resource_metadata' => 'FinnaConsole\Command\Util\VerifyResourceMetadata', diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php deleted file mode 100644 index 661e894bce8..00000000000 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommand.php +++ /dev/null @@ -1,916 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace FinnaConsole\Command\Util; - -use PHPMD\Console\Output; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Formatter\OutputFormatter; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -use function array_key_exists; -use function count; -use function dirname; -use function in_array; -use function is_string; - -/** - * Console command: fix SCSS variable declarations. - * - * @category VuFind - * @package Console - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -#[AsCommand( - name: 'util/scssFixer', - description: 'SCSS fixer' -)] -class ScssFixerCommand extends Command -{ - public const VARIABLE_CHARS = '[a-zA-Z0-9_-]'; - - /** - * Include paths - * - * @var array - */ - protected $includePaths = []; - - /** - * Console output - * - * @var OutputInterface - */ - protected $output = null; - - /** - * All variables with the last occurrence taking precedence (like in lesscss) - * - * @var array - */ - protected $allVars = []; - - /** - * Base dir for the main SCSS file - * - * @var string - */ - protected $scssBaseDir = ''; - - /** - * An array tracking all processed files - * - * @var array - */ - protected $allFiles = []; - - /** - * File to use for all added variables - * - * @var ?string - */ - protected $variablesFile = null; - - /** - * Configure the command. - * - * @return void - */ - protected function configure() - { - $this - ->setHelp('Fixes variable declarations in SCSS files.') - ->addOption( - 'variables_file', - null, - InputOption::VALUE_REQUIRED, - 'File to use for added SCSS variables' - ) - ->addOption( - 'include_path', - 'I', - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Include directories' - ) - ->addOption( - 'exclude', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Files not to be touched (in addition to the ones outside of the starting directory)' - ) - ->addArgument( - 'scss_file', - InputArgument::REQUIRED, - 'Name of main SCSS file to use as an entry point' - ); - } - - /** - * Run the command. - * - * @param InputInterface $input Input object - * @param OutputInterface $output Output object - * - * @return int 0 for success - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->variablesFile = $input->getOption('variables_file'); - $this->includePaths = $input->getOption('include_path'); - $this->output = $output; - $mainFile = $input->getArgument('scss_file'); - $this->allVars = []; - $this->scssBaseDir = realpath(dirname($mainFile)); - // First read all vars: - if (!$this->processFile($mainFile, $this->allVars, true, false)) { - return Command::FAILURE; - } - // Now do changes: - $currentVars = []; - if (!$this->processFile($mainFile, $currentVars, false, true)) { - $this->error('Stop on failure'); - return Command::FAILURE; - } - - // Write out the modified files: - if (!$this->updateModifiedFiles()) { - return Command::FAILURE; - } - - return Command::SUCCESS; - } - - /** - * Process a file - * - * @param string $filename File name - * @param array $vars Currently defined variables - * @param bool $discover Whether to just discover files and their content - * @param bool $change Whether to do changes to the file - * - * @return bool - */ - protected function processFile( - string $filename, - array &$vars, - bool $discover, - bool $change - ): bool { - if (!$this->isReadableFile($filename)) { - $this->error("File $filename does not exist or is not a readable file"); - return false; - } - $filename = str_starts_with($filename, '/') ? $filename : realpath($filename); - $fileDir = dirname($filename); - $lineNo = 0; - $this->debug( - "Start processing $filename" . ($discover ? ' (discovery)' : ($change ? '' : ' (read only)')), - $change ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_DEBUG - ); - $lines = file($filename, FILE_IGNORE_NEW_LINES); - $this->updateFileCollection($filename, compact('lines', 'vars')); - - // Process string substitutions - if ($change) { - $this->processSubstitutions($filename, $lines); - $this->updateFileCollection($filename, compact('lines')); - } - - $inMixin = 0; - $requiredVars = []; - foreach ($lines as $idx => $line) { - ++$lineNo; - $lineId = "$filename:$lineNo"; - $parts = explode('//', $line, 2); - $line = $parts[0]; - $comments = $parts[1] ?? null; - - if (str_starts_with(trim($line), '@mixin ')) { - $inMixin = $this->getBlockLevelChange($line); - continue; - } - if ($inMixin) { - $inMixin += $this->getBlockLevelChange($line); - } - - if ($inMixin) { - continue; - } - - // Process variable declarations: - $this->processVariables($lineId, $line, $vars); - // Process import: - if (!$this->processImport($lineId, $fileDir, $line, $vars, $discover)) { - return false; - } - - if ($discover || !$change) { - continue; - } - - // Collect variables that need to be defined: - if ($newVars = $this->checkVariables($lineId, $line, $vars)) { - $requiredVars = [ - ...$requiredVars, - ...$newVars, - ]; - } - $lines[$idx] = $line . ($comments ? "//$comments" : ''); - } - - if (!$discover && $change && $requiredVars || $this->allFiles[$filename]['lines'] !== $lines) { - $this->updateFileCollection($filename, compact('lines', 'requiredVars') + ['modified' => true]); - } - - return true; - } - - /** - * Find variables - * - * @param string $lineId Line identifier for logging - * @param string $line Line - * @param array $vars Currently defined variables - * - * @return ?array Array of required variables and their valuesm, or null on error - */ - protected function processVariables(string $lineId, string $line, array &$vars): void - { - if (!preg_match('/^\s*\$(' . static::VARIABLE_CHARS . '+):\s*(.*?);?$/', $line, $matches)) { - return; - } - [, $var, $value] = $matches; - $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); - if (array_key_exists($var, $vars)) { - $this->debug( - "$lineId: '$var: $value' overrides existing value '" . $vars[$var] . "'", - OutputInterface::VERBOSITY_DEBUG - ); - } else { - $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); - } - $vars[$var] = $value; - } - - /** - * Process @import - * - * @param string $lineId Line identifier for logging - * @param string $fileDir Current file directory - * @param string $line Line - * @param array $vars Currently defined variables - * @param bool $discover Whether to just discover files and their content - * - * @return bool - */ - protected function processImport(string $lineId, string $fileDir, string $line, array &$vars, bool $discover): bool - { - if (!preg_match("/^\s*@import\s+['\"]([^'\"]+)['\"]\s*;/", $line, $matches)) { - // Check for LESS import reference: - if (!preg_match("/^\s*@import \/\*\(reference\)\*\/ ['\"]([^'\"]+)['\"]\s*;/", $line, $matches)) { - return true; - } - } - $import = $matches[1]; - if (str_ends_with($import, '.css')) { - $this->debug("$lineId: skipping .css import"); - return true; - } - if (!($pathInfo = $this->resolveImportFileName($import, $fileDir))) { - $this->error("$lineId: import file $import not found"); - return false; - } else { - $this->debug( - "$lineId: import $pathInfo[fullPath] as $import" . ($pathInfo['inBaseDir'] ? ' (IN BASE)' : ''), - OutputInterface::VERBOSITY_DEBUG - ); - if (!$this->processFile($pathInfo['fullPath'], $vars, $discover, $pathInfo['inBaseDir'])) { - return false; - } - } - return true; - } - - /** - * Replace variables that are defined later with their last values - * - * @param string $lineId Line identifier for logging - * @param string $line Line - * @param array $vars Currently defined variables - * - * @return ?array Array of required variables and their values, or null on error - */ - protected function checkVariables(string $lineId, string $line, array $vars): ?array - { - $required = []; - do { - $lastLine = $line; - $line = preg_replace_callback( - '/\$(' . static::VARIABLE_CHARS . '+)(?!.*:)\\b/', - function ($matches) use ($vars, $lineId, &$required) { - $var = $matches[1]; - $lastVal = $this->allVars[$var] ?? null; - if (isset($vars[$var]) && $vars[$var] === $lastVal) { - // Previous definition contains the correct value, return as is: - $this->debug("$lineId: $var ok", OutputInterface::VERBOSITY_VERY_VERBOSE); - return $matches[0]; - } - if (null === $lastVal) { - $this->warning("$lineId: Value for variable '$var' not found"); - return $matches[0]; - } - // Use last defined value: - $this->debug("$lineId: Need $lastVal for $var"); - $required[] = [ - 'var' => $var, - 'value' => $lastVal, - ]; - return $lastVal; - }, - $line - ); - } while ($lastLine !== $line); - return $required; - } - - /** - * Get block level (depth) change - * - * @param string $line Line - * - * @return int - */ - protected function getBlockLevelChange(string $line): int - { - $level = 0; - foreach (str_split($line) as $ch) { - if ('{' === $ch) { - ++$level; - } elseif ('}' === $ch) { - --$level; - } - } - return $level; - } - - /** - * Find import file - * - * @param string $filename Relative file name - * @param string $baseDir Base directory - * - * @return ?array - */ - protected function resolveImportFileName(string $filename, string $baseDir): ?array - { - if (!str_ends_with($filename, '.scss')) { - $filename .= '.scss'; - } - $allDirs = [ - $baseDir, - ...$this->includePaths, - ]; - foreach ($allDirs as $dir) { - // full import - $fullPath = "$dir/$filename"; - if (!$this->isReadableFile($fullPath)) { - // reference import - $fullPath = dirname($fullPath) . '/_' . basename($fullPath); - } - if ($this->isReadableFile($fullPath)) { - return [ - 'fullPath' => $fullPath, - 'inBaseDir' => str_starts_with(realpath($fullPath), $this->scssBaseDir . '/'), - ]; - } - } - return null; - } - - /** - * Update a file in the all files collection - * - * @param string $filename File name - * @param array $values Values to set - * - * @return void; - */ - protected function updateFileCollection(string $filename, array $values): void - { - if (null === ($oldValues = $this->allFiles[$filename] ?? null)) { - $oldValues = [ - 'modified' => false, - 'requiredVars' => [], - ]; - $values['index'] = count($this->allFiles); - } - if (!isset($oldValues['lines']) && !isset($values['lines'])) { - // Read in any existing file: - if (file_exists($filename)) { - if (!$this->isReadableFile($filename)) { - throw new \Exception("$filename is not readable"); - } - $values['lines'] = file($filename, FILE_IGNORE_NEW_LINES); - } - } - // Set modified flag if needed: - if (isset($oldValues['lines']) && isset($values['lines']) && $oldValues['lines'] !== $values['lines']) { - $values['modified'] = true; - } - $this->allFiles[$filename] = array_merge($oldValues, $values); - } - - /** - * Update any modified files - * - * @return bool - */ - protected function updateModifiedFiles(): bool - { - // If we have a variables file, collect all variables needed by later files and add them: - if ($this->variablesFile) { - $variablesFile = realpath($this->variablesFile) ?: $this->variablesFile; - $variablesFileIndex = $this->allFiles[$variablesFile]['index'] ?? PHP_INT_MAX; - - $allRequiredVars = []; - foreach ($this->allFiles as $filename => &$fileSpec) { - // Check if the file is included before the variables file (if so, we must add the variables in - // that file): - if ($fileSpec['index'] < $variablesFileIndex) { - continue; - } - array_push($allRequiredVars, ...$fileSpec['requiredVars']); - $fileSpec['requiredVars'] = []; - } - unset($fileSpec); - $this->updateFileCollection( - $variablesFile, - [ - 'requiredVars' => $allRequiredVars, - 'modified' => true, - ] - ); - $this->debug(count($allRequiredVars) . " variables added to $variablesFile"); - } - - foreach ($this->allFiles as $filename => $fileSpec) { - if (!$fileSpec['modified'] && !$fileSpec['requiredVars']) { - continue; - } - $lines = $fileSpec['lines']; - - // Prepend required variables: - if ($fileSpec['requiredVars']) { - $requiredVars = $this->resolveVariableDependencies($fileSpec['requiredVars'], $fileSpec['vars']); - $linesToAdd = ['// The following variables were automatically added in SCSS conversion']; - $addedVars = []; - foreach (array_reverse($requiredVars) as $current) { - $var = $current['var']; - if (!in_array($var, $addedVars)) { - $value = $current['value']; - $linesToAdd[] = "\$$var: $value;"; - $addedVars[] = $var; - } - } - $linesToAdd[] = ''; - array_unshift($lines, ...$linesToAdd); - } - // Write the updated file: - if (false === file_put_contents($filename, implode(PHP_EOL, $lines)) . PHP_EOL) { - $this->error("Could not write file $filename"); - } - $this->debug("$filename updated"); - } - - return true; - } - - /** - * Resolve requirements for variables that depend on other variables - * - * @param array $vars Variables to resolve - * @param array $knownVars Vars that are already available - * - * @return array - */ - protected function resolveVariableDependencies(array $vars, array $knownVars): array - { - $result = $vars; - foreach ($vars as $current) { - $var = $current['var']; - $varDefinition = $current['value']; - $loop = 0; - while (preg_match('/\$(' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { - $requiredVar = $matches[1]; - if (in_array($requiredVar, $knownVars)) { - $this->debug( - "Existing definition found for '$requiredVar' required by '$var: $varDefinition'", - OutputInterface::VERBOSITY_DEBUG - ); - continue; - } - if ($requiredVarValue = $this->allVars[$requiredVar] ?? null) { - $this->debug("'$var: $varDefinition' requires '$requiredVar: $requiredVarValue'"); - $result[] = [ - 'var' => $requiredVar, - 'value' => $requiredVarValue, - ]; - $varDefinition = $requiredVarValue; - } else { - $this->warning( - "Could not resolve dependency for variable '$var'; definition missing for '$requiredVar'" - ); - break; - } - if (++$loop >= 10) { - $this->warning("Value definition loop detected ($var -> $requiredVar)"); - break; - } - } - } - return $result; - } - - /** - * Process string substitutions - * - * @param string $filename File name - * @param array $lines File contents - * - * @return void - */ - protected function processSubstitutions(string $filename, array &$lines): void - { - $this->debug("$filename: start processing substitutions", OutputInterface::VERBOSITY_DEBUG); - $contents = implode(PHP_EOL, $lines); - foreach ($this->getSubstitutions() as $i => $substitution) { - $this->debug("$filename: processing substitution $i", OutputInterface::VERBOSITY_DEBUG); - if (str_starts_with($substitution['pattern'], '/')) { - // Regexp - if (is_string($substitution['replacement'])) { - $contents = preg_replace($substitution['pattern'], $substitution['replacement'], $contents); - } else { - $contents = preg_replace_callback( - $substitution['pattern'], - $substitution['replacement'], - $contents - ); - } - if (null === $contents) { - throw new \Exception( - "Failed to process regexp substitution $i: " . $substitution['pattern'] - . ': ' . preg_last_error_msg() - ); - } - } else { - // String - $contents = str_replace($substitution['pattern'], $substitution['replacement'], $contents); - } - } - - $lines = explode(PHP_EOL, $contents); - $this->debug("$filename: done processing substitutions", OutputInterface::VERBOSITY_DEBUG); - } - - /** - * Get substitutions - * - * @return array; - */ - protected function getSubstitutions(): array - { - return [ - [ // Revert invalid @ => $ changes for css rules: - 'pattern' => '/\$(supports|container) \(/i', - 'replacement' => '@$1 (', - ], - [ // Revert @if => $if change: - 'pattern' => '/\$if \(/i', - 'replacement' => '@if (', - ], - [ // Revert @use => $use change: - 'pattern' => "/\$use '/i", - 'replacement' => "@use '", - ], - [ // Revert @supports => $supports change: - 'pattern' => "/\$supports '/i", - 'replacement' => "@supports '", - ], - [ // Revert @page => $page change: - 'pattern' => '$page ', - 'replacement' => '@page ', - ], - [ // Fix comparison: - 'pattern' => '/ ==< /i', - 'replacement' => ' <= ', - ], - [ // Remove !important from variables: - 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*;/m', - 'replacement' => '$1:$2;', - ], - [ // Remove !important from functions: - 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*\)/m', - 'replacement' => '$1:$2;', - ], - [ // fadein => fade-in: - 'pattern' => '/fadein\((\S+),\s*(\S+)\)/', - 'replacement' => function ($matches) { - return 'fade-in(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; - }, - ], - [ // fadeout => fade-out: - 'pattern' => '/fadeout\((\S+),\s*(\S+)\)/', - 'replacement' => function ($matches) { - return 'fade-out(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; - }, - ], - [ // replace invalid characters in variable names: - 'pattern' => '/\$([^: };\/]+)/', - 'replacement' => function ($matches) { - return '$' . str_replace('.', '__', $matches[1]); - }, - ], - [ // remove invalid &: - 'pattern' => '/([a-zA-Z0-9])&:/', - 'replacement' => '$1:', - ], - [ // remove (reference) from import): - 'pattern' => '/@import\s+\(reference\)\s*/', - 'replacement' => '@import /*(reference)*/ ', - ], - [ // fix missing semicolon from background-image rule: - 'pattern' => '/(\$background-image:([^;]+?))\n/', - 'replacement' => "\$1;\n", - ], - [ // remove broken (and useless) rule: - 'pattern' => '/\.feed-container \.list-feed \@include feed-header\(\);/', - 'replacement' => '', - ], - [ // interpolate variables in media queries: - 'pattern' => '/\@media (\$[^ ]+)/', - 'replacement' => '@media #{$1}', - ], - [ // missing semicolon: - 'pattern' => '/(.+:.*auto)\n/', - 'replacement' => "\$1;\n", - ], - [ // lost space in mixin declarations: - 'pattern' => '/(\@mixin.+){/', - 'replacement' => '$1 {', - ], - [ // special cases: media query variables - 'pattern' => '/(\$(mobile-portrait|mobile|tablet|desktop):\s*)(.*?);/s', - 'replacement' => '$1"$2";', - ], - [ // special cases: mobile mixin - 'pattern' => '/\.mobile\(\{(.*?)\}\);/s', - 'replacement' => '@media #{$mobile} { & { $1 } }', - ], - [ // special cases: mobile mixin 2 - 'pattern' => '@mixin mobile($rules){', - 'replacement' => '@mixin mobile {', - ], - [ // special cases: mobile mixin 3 - 'pattern' => '$rules();', - 'replacement' => '@content;', - ], - [ // invalid mixin name - 'pattern' => 'text(uppercase)', - 'replacement' => 'text-uppercase', - ], - [ // when isnumber - 'pattern' => '& when (isnumber($z-index))', - 'replacement' => '@if $z-index != null', - ], - [ // blocks extending container - 'pattern' => '@include container();', - 'replacement' => '@extend .container;', - ], - [ // blocks extending more-link - 'pattern' => '@include more-link();', - 'replacement' => '@extend .more-link;', - ], - [ // fix math operations - 'pattern' => '/(\s+)(\(.+\/.+\))/', - 'replacement' => '$1calc$2', - ], - [ // typo - 'pattern' => '$carousel-header-color none;', - 'replacement' => '$carousel-header-color: none;', - ], - [ // typo - 'pattern' => '$brand-primary // $link-color;', - 'replacement' => '$brand-primary; // $link-color', - ], - [ // typo - 'pattern' => '- aukioloaikojen otsikko', - 'replacement' => '{ /* aukioloaikojen otsikko */ }', - ], - [ // typo - 'pattern' => '$link-hover-color: $tut-a-hover,', - 'replacement' => '$link-hover-color: $tut-a-hover;', - ], - [ // typo - 'pattern' => 'rgba(43,65,98,0,9)', - 'replacement' => 'rgba(43,65,98,0.9)', - ], - [ // typo $input-bg: ##ff8d0f; - 'pattern' => '/:\s*##+/', - 'replacement' => ': #', - ], - [ // typo - 'pattern' => '!importanti', - 'replacement' => '!important', - ], - [ // typo - 'pattern' => '$brand-secondary: #;', - 'replacement' => '', - ], - [ // typo - 'pattern' => '$brand-secondary: ###;', - 'replacement' => '', - ], - [ // typo - 'pattern' => '#00000;', - 'replacement' => '#000000;', - ], - [ // typo - 'pattern' => 'background-color: ;', - 'replacement' => '', - ], - [ // typo - 'pattern' => '$header-background-color #fff;', - 'replacement' => '$header-background-color: #fff;', - ], - [ // typo - 'pattern' => '$action-link-color #FFF;', - 'replacement' => '$action-link-color: #FFF;', - ], - [ // typo - 'pattern' => '$finna-browsebar-background (selaa palkin taustaväri)', - 'replacement' => '//$finna-browsebar-background (selaa palkin taustaväri)', - ], - [ // typo - 'pattern' => '$finna-browsebar-link-color(selaa palkin linkin)', - 'replacement' => '//$finna-browsebar-link-color(selaa palkin linkin)', - ], - [ // typo - 'pattern' => '$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', - 'replacement' => '//$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', - ], - [ // typo - 'pattern' => '$home-2_fi {', - 'replacement' => '.home-2_fi {', - ], - [ // disable unsupported extend - 'pattern' => '@extend .finna-panel-default .panel-heading;', - 'replacement' => '// Not supported in SCSS: @extend .finna-panel-default .panel-heading;', - ], - - [ // gradient mixin call - 'pattern' => '#gradient.vertical($background-start-color; $background-end-color;' - . ' $background-start-percent; $background-end-percent);', - 'replacement' => 'gradient-vertical($background-start-color, $background-end-color' - . ', $background-start-percent, $background-end-percent);', - ], - [ // common typo in home column styles - 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?' - . '\& .right-column-content \{.*?\}.*?\})/s', - 'replacement' => "\$1\$2\n}", - ], - [ // another typo in home column styles - 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', - 'replacement' => '$1$2', - ], - [ // missing semicolon: display: none - 'pattern' => '/display: none\n/', - 'replacement' => 'display: none;', - ], - [ // missing semicolon in variable definitions - 'pattern' => '/(\n\s*\$' . static::VARIABLE_CHARS . '+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', - 'replacement' => '$1;$2', - ], - [ // missing semicolon: $header-text-color: #000000 - 'pattern' => '/$header-text-color: #000000\n/', - 'replacement' => '$header-text-color: #000000;', - ], - [ // missing semicolon: clip: rect(0px,1200px,1000px,0px) - 'pattern' => '/clip: rect\(0px,1200px,1000px,0px\)\n/', - 'replacement' => "clip: rect(0px,1200px,1000px,0px);\n", - ], - [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // - 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', - 'replacement' => '$finna-feedback-background: darken(#d80073, 10%);$1', - ], - [ // invalid (and obsolete) rule - 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', - 'replacement' => "// Invalid rule commented out by SCSS conversion\n/*\n\$1\n*/", - ], - - [ // literal fix - 'pattern' => "~ ')'", - 'replacement' => ')', - ], - [ // literal fix - 'pattern' => 'calc(100vh - "#{$navbar-height}~")', - 'replacement' => 'calc(100vh - #{$navbar-height})', - ], - - [ // math without calc - 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', - 'replacement' => function ($matches) { - [$full, $pre, $math] = $matches; - if (str_contains($matches[1], '(')) { - return $full; - } - return $pre . "calc($math)"; - }, - ], - ]; - } - - /** - * Output a debug message - * - * @param string $msg Message - * @param int $verbosity Verbosity level - * - * @return void - */ - protected function debug(string $msg, int $verbosity = OutputInterface::VERBOSITY_VERBOSE): void - { - $this->output->writeln($msg, $verbosity); - } - - /** - * Output an error message - * - * @param string $msg Message - * - * @return void - */ - protected function error(string $msg): void - { - if ($this->output) { - $this->output->writeln('' . OutputFormatter::escape($msg) . ''); - } - } - - /** - * Output a warning message - * - * @param string $msg Message - * - * @return void - */ - protected function warning(string $msg): void - { - if ($this->output) { - $this->output->writeln('' . OutputFormatter::escape($msg) . ''); - } - } - - /** - * Check if file name points to a readable file - * - * @param string $filename File name - * - * @return bool - */ - protected function isReadableFile(string $filename): bool - { - return file_exists($filename) && (is_file($filename) || is_link($filename)); - } -} diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php deleted file mode 100644 index 23001e2aec4..00000000000 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/ScssFixerCommandFactory.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link http://vufind.org/wiki/vufind2:developer_manual Wiki - */ - -namespace FinnaConsole\Command\Util; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Laminas\ServiceManager\Factory\FactoryInterface; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * Factory for the "scss fixer" task. - * - * @category VuFind - * @package Service - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link http://vufind.org/wiki/vufind2:developer_manual Wiki - */ -class ScssFixerCommandFactory implements FactoryInterface -{ - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - array $options = null - ) { - return new $requestedName(); - } -} From 97eef7861a9ed86a94eddbc59e247a5724ebe8b2 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 11:37:26 +0300 Subject: [PATCH 25/30] Add --enable_scss --- .../Command/Util/LessToScssCommand.php | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index b92b151a940..92d1e3ede93 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -125,6 +125,13 @@ class LessToScssCommand extends Command */ protected $substitutions = []; + /** + * Whether to enable SCSS in target theme(s) + * + * @var bool + */ + protected $enableScss = false; + /** * Constructor * @@ -162,6 +169,12 @@ protected function configure() InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Files to skip as main LESS files (fnmatch patterns)' ) + ->addOption( + 'enable_scss', + null, + InputOption::VALUE_NONE, + 'If specified, enables SCSS in the target theme(s)', + ) ->addArgument( 'main_file', InputArgument::REQUIRED | InputArgument::IS_ARRAY, @@ -183,6 +196,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->substitutions = $this->getSubstitutions(); $this->includePaths = $input->getOption('include_path'); $this->excludedFiles = $input->getOption('exclude'); + $this->enableScss = $input->getOption('enable_scss'); $variablesFile = $input->getOption('variables_file'); $patterns = $input->getArgument('main_file'); @@ -679,7 +693,7 @@ protected function updateFileCollection(string $filename, array $values): void protected function writeTargetFiles(): bool { if (!file_exists($this->targetDir)) { - if (!mkdir($this->targetDir, 0777, true)) { + if (!mkdir($this->targetDir, 0o777, true)) { $this->error("Could not create target directory $this->targetDir"); return false; } @@ -752,6 +766,18 @@ protected function writeTargetFiles(): bool $this->debug("Created $fullPath"); } + if ($this->enableScss) { + $styleIni = <<targetDir . '/style.ini'; + if (false === file_put_contents($iniFile, $styleIni)) { + $this->error("Could not write file $iniFile"); + } + } + return true; } From 56d369712c81638db7cc10c5ceda8d91577d3944 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 11:38:17 +0300 Subject: [PATCH 26/30] Improve media query variable support --- module/FinnaConsole/config/lessToScss.config.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index 99156442604..74052708e1b 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -1,6 +1,6 @@ '/(?!@debug|@import|@media|@keyframes|@font-face|@include|@extend|@mixin|@supports|@container |@if |@use |@page |@-\w)@/i', @@ -50,7 +50,14 @@ ], [ // literal 'pattern' => '/~"(.*)"/i', - 'replacement' => 'unquote("$1")', + 'replacement' => function ($matches) { + [, $match] = $matches; + // Keep media queries quoted + if (str_starts_with($match, 'screen and')) { + return '"' . $match . '"'; + } + return 'unquote("' . $match . '")'; + }, ], // end of basic less-to-sass rules --------------- From 2d6f62fadbab33006c4fcb37a4c685f0e733c0bd Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 11:40:59 +0300 Subject: [PATCH 27/30] Use !default for variables when appropriate --- .../Command/Util/LessToScssCommand.php | 67 ++++++++++++------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index 92d1e3ede93..bf1cc910f6b 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -32,7 +32,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -132,6 +131,13 @@ class LessToScssCommand extends Command */ protected $enableScss = false; + /** + * Patterns for files that must not use the !default flag for variables + * + * @var array + */ + protected $noDefaultFiles = []; + /** * Constructor * @@ -169,6 +175,13 @@ protected function configure() InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Files to skip as main LESS files (fnmatch patterns)' ) + ->addOption( + 'no_default', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Files where variable definitions must not include the !default flag (fnmatch patterns relative to' + . ' the target directory)' + ) ->addOption( 'enable_scss', null, @@ -196,6 +209,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->substitutions = $this->getSubstitutions(); $this->includePaths = $input->getOption('include_path'); $this->excludedFiles = $input->getOption('exclude'); + $this->noDefaultFiles = $input->getOption('no_default'); $this->enableScss = $input->getOption('enable_scss'); $variablesFile = $input->getOption('variables_file'); $patterns = $input->getArgument('main_file'); @@ -422,14 +436,14 @@ protected function processLessVariables(string $lineId, string $line, array &$va return; } [, $var, $value] = $matches; - $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value); + $value = trim(preg_replace('/\s*!default\s*;?\s*$/', '', $value)); if (array_key_exists($var, $vars)) { $this->debug( - "$lineId: '$var: $value' overrides existing value '" . $vars[$var] . "'", + "$lineId: `$var: $value` overrides existing value `" . $vars[$var] . '`', OutputInterface::VERBOSITY_DEBUG ); } else { - $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); + $this->debug("$lineId: found `$var: $value`", OutputInterface::VERBOSITY_DEBUG); } $vars[$var] = $value; @@ -450,20 +464,20 @@ protected function processScssVariables(string $lineId, string $line, array &$va return; } [, $var, $value] = $matches; - $value = preg_replace('/\s*!default\s*;?\s*$/', '', $value, -1, $count); + $value = trim(preg_replace('/\s*!default\s*;?\s*$/', '', $value, -1, $count)); $default = $count > 0; $existing = $vars[$var] ?? null; if ($existing) { if ($existing['default'] && !$default) { $this->debug( - "$lineId: '$var: $value' overrides default value '" . $vars[$var]['value'] . "'", + "$lineId: `$var: $value` overrides default value `" . $vars[$var]['value'] . '`', OutputInterface::VERBOSITY_DEBUG ); } else { return; } } else { - $this->debug("$lineId: found '$var: $value'", OutputInterface::VERBOSITY_DEBUG); + $this->debug("$lineId: found `$var: $value`", OutputInterface::VERBOSITY_DEBUG); } $vars[$var] = compact('value', 'default'); } @@ -571,12 +585,12 @@ protected function checkVariables(string $lineId, string $line, array $vars): ?a continue; } if (null === $lessVal) { - $this->warning("$lineId: Value for variable '$var' not found (line: $line)"); + $this->warning("$lineId: Value for variable `$var` not found (line: $line)"); continue; } // Use last defined value: - $this->debug("$lineId: Need $lessVal for $var"); + $this->debug("$lineId: Need `$lessVal` for $var (have `" . ($vars[$var]['value'] ?? '[nothing]') . '`)'); $required[] = [ 'var' => $var, 'value' => $lessVal, @@ -600,17 +614,17 @@ protected function resolveVariableDependencies(array $vars, array $knownVars): a $var = $current['var']; $varDefinition = $current['value']; $loop = 0; - while (preg_match('/[@\$](' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { + while (preg_match('/[@\$]\{?(' . static::VARIABLE_CHARS . '+)/', $varDefinition, $matches)) { $requiredVar = $matches[1]; if (in_array($requiredVar, $knownVars)) { $this->debug( - "Existing definition found for '$requiredVar' required by '$var: $varDefinition'", + "Existing definition found for '$requiredVar' required by `$var: $varDefinition`", OutputInterface::VERBOSITY_DEBUG ); continue; } if ($requiredVarValue = $this->allLessVars[$requiredVar] ?? null) { - $this->debug("'$var: $varDefinition' requires '$requiredVar: $requiredVarValue'"); + $this->debug("`$var: $varDefinition` requires `$requiredVar: $requiredVarValue`"); $result[] = [ 'var' => $requiredVar, 'value' => $requiredVarValue, @@ -618,7 +632,7 @@ protected function resolveVariableDependencies(array $vars, array $knownVars): a $varDefinition = $requiredVarValue; } else { $this->warning( - "Could not resolve dependency for variable '$var'; definition missing for '$requiredVar'" + "Could not resolve dependency for variable `$var`; definition missing for `$requiredVar`" ); break; } @@ -678,6 +692,7 @@ protected function updateFileCollection(string $filename, array $values): void } } // Merge requiredVars in case the file is imported multiple times + // (we may end up with duplicates, but it's difficult to dedup without affecting order, so keep it that way): $values['requiredVars'] = array_merge( $oldValues['requiredVars'] ?? [], $values['requiredVars'] ?? [] @@ -730,6 +745,20 @@ protected function writeTargetFiles(): bool } $lines = $fileSpec['lines'] ?? []; + // Add !default to existing variables (unless excluded): + $addDefault = true; + foreach ($this->noDefaultFiles as $noDefaultPattern) { + if (fnmatch($noDefaultPattern, $fullPath)) { + $addDefault = false; + } + } + if ($addDefault) { + foreach ($lines as &$line) { + $line = preg_replace('/(\$.+):(.+);/', '$1:$2 !default;', $line); + } + unset($line); + } + // Prepend required variables: if ($fileSpec['requiredVars']) { $requiredVars = $this->resolveVariableDependencies($fileSpec['requiredVars'], $fileSpec['vars'] ?? []); @@ -743,18 +772,6 @@ protected function writeTargetFiles(): bool } } - // Remove later definitions for the required variables: - foreach ($lines as &$line) { - foreach ($addedVars as $var) { - $line = preg_replace( - '/^(\s*\$' . preg_quote($var) . ':.*)$/', - '/* $1 // Commented out in SCSS conversion */', - $line - ); - } - unset($line); - } - // Prepend new definitions: $linesToAdd[] = ''; array_unshift($lines, ...$linesToAdd); From aa0ab4689247e11bf0c24daf4a94849c688dafdc Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 12:28:47 +0300 Subject: [PATCH 28/30] Make configuration more future-proof. --- .../FinnaConsole/config/lessToScss.config.php | 674 +++++++++--------- .../Command/Util/LessToScssCommand.php | 2 +- 2 files changed, 339 insertions(+), 337 deletions(-) diff --git a/module/FinnaConsole/config/lessToScss.config.php b/module/FinnaConsole/config/lessToScss.config.php index 74052708e1b..831640eb855 100644 --- a/module/FinnaConsole/config/lessToScss.config.php +++ b/module/FinnaConsole/config/lessToScss.config.php @@ -2,345 +2,347 @@ // The first few rules are based on the rules in the grunt-less-to-sass library return [ - [ // functions - 'pattern' => '/(?!@debug|@import|@media|@keyframes|@font-face|@include|@extend|@mixin|@supports|@container |@if |@use |@page |@-\w)@/i', - 'replacement' => '$', - ], - [ // when => if - 'pattern' => '/\.([\w_-]*)\s*\((.*)\)\s*when\s*\((.*)=(.*)\)\s*\{(\s*)([^}]+)}[;]?/i', - 'replacement' => '@if $3==$4 {$5@mixin $1($2){$5$6}}', - ], - [ // .class => @extend .class - 'pattern' => '/\.([[a-zA-Z-_]*)\s*;/i', - 'replacement' => '@extend .$1;', - ], - [ // Remove .less extension from imports - 'pattern' => "/\@import\s*[\"'](.*).less[\"']/i", - 'replacement' => function ($matches) { - return '@import \'' . str_replace('/less/', '/scss/', $matches[1]) . '\''; - }, - ], - [ // Nested include - 'pattern' => '/(\s*)\#([\w\-]*)\s*>\s*\@include\s+(.*);/i', - 'replacement' => '$1@include $2-$3;', - ], - [ // Include mixin - 'pattern' => '/(\s+)\.([\w\-]*)\s*\((.*)\);/i', - 'replacement' => '$1@include $2($3);', - ], - [ // Mixin declaration - 'pattern' => '/\.([\w\-]*)\s*\((.*)\)\s*\{/i', - 'replacement' => '@mixin $1($2){', - ], - [ - 'pattern' => '/spin\((.+),(.+)\)/i', - 'replacement' => 'adjust-hue($1,$2)', - ], - [ // shade/tint - 'pattern' => '/(shade|tint)\(([^,]+),\s?([\d%]+)\)/i', - 'replacement' => function ($matches) { - [, $method, $color2, $weight] = $matches; - $color1 = $method === 'shade' ? '#000000' : '#ffffff'; - return "mix($color1, $color2, $weight)"; - }, - ], - [ // fade - 'pattern' => '/fade\((.*),\s?([\d]+)\%\)/mi', - 'replacement' => 'rgba($1, ($2/100))', - ], - [ // literal - 'pattern' => '/~"(.*)"/i', - 'replacement' => function ($matches) { - [, $match] = $matches; - // Keep media queries quoted - if (str_starts_with($match, 'screen and')) { - return '"' . $match . '"'; - } - return 'unquote("' . $match . '")'; - }, - ], - // end of basic less-to-sass rules --------------- + 'substitutions' => [ + [ // functions + 'pattern' => '/(?!@debug|@import|@media|@keyframes|@font-face|@include|@extend|@mixin|@supports|@container |@if |@use |@page |@-\w)@/i', + 'replacement' => '$', + ], + [ // when => if + 'pattern' => '/\.([\w_-]*)\s*\((.*)\)\s*when\s*\((.*)=(.*)\)\s*\{(\s*)([^}]+)}[;]?/i', + 'replacement' => '@if $3==$4 {$5@mixin $1($2){$5$6}}', + ], + [ // .class => @extend .class + 'pattern' => '/\.([[a-zA-Z-_]*)\s*;/i', + 'replacement' => '@extend .$1;', + ], + [ // Remove .less extension from imports + 'pattern' => "/\@import\s*[\"'](.*).less[\"']/i", + 'replacement' => function ($matches) { + return '@import \'' . str_replace('/less/', '/scss/', $matches[1]) . '\''; + }, + ], + [ // Nested include + 'pattern' => '/(\s*)\#([\w\-]*)\s*>\s*\@include\s+(.*);/i', + 'replacement' => '$1@include $2-$3;', + ], + [ // Include mixin + 'pattern' => '/(\s+)\.([\w\-]*)\s*\((.*)\);/i', + 'replacement' => '$1@include $2($3);', + ], + [ // Mixin declaration + 'pattern' => '/\.([\w\-]*)\s*\((.*)\)\s*\{/i', + 'replacement' => '@mixin $1($2){', + ], + [ + 'pattern' => '/spin\((.+),(.+)\)/i', + 'replacement' => 'adjust-hue($1,$2)', + ], + [ // shade/tint + 'pattern' => '/(shade|tint)\(([^,]+),\s?([\d%]+)\)/i', + 'replacement' => function ($matches) { + [, $method, $color2, $weight] = $matches; + $color1 = $method === 'shade' ? '#000000' : '#ffffff'; + return "mix($color1, $color2, $weight)"; + }, + ], + [ // fade + 'pattern' => '/fade\((.*),\s?([\d]+)\%\)/mi', + 'replacement' => 'rgba($1, ($2/100))', + ], + [ // literal + 'pattern' => '/~"(.*)"/i', + 'replacement' => function ($matches) { + [, $match] = $matches; + // Keep media queries quoted + if (str_starts_with($match, 'screen and')) { + return '"' . $match . '"'; + } + return 'unquote("' . $match . '")'; + }, + ], + // end of basic less-to-sass rules --------------- - [ // Activate SCSS - 'pattern' => '/\/\* #SCSS>/i', - 'replacement' => '/* #SCSS> */', - ], - [ - 'pattern' => '/<#SCSS \*\//i', - 'replacement' => '/* <#SCSS */', - ], - [ - 'pattern' => '/\/\* #LESS> \*\//i', - 'replacement' => '/* #LESS>', - ], - [ - 'pattern' => '/\/\* <#LESS \*\//i', - 'replacement' => '<#LESS */', - ], - [ // Fix include parameter separator - 'pattern' => '/@include ([^\(]+)\(([^\)]+)\);/i', - 'replacement' => function ($matches) { - [, $m1, $m2] = $matches; - return '@include ' . $m1 . '(' . str_replace(';', ',', $m2) . ');'; - }, - ], - [ // Fix tilde literals - 'pattern' => "/~'(.*?)'/i", - 'replacement' => '$1', - ], - [ // Convert inline &:extends - 'pattern' => '/&:extend\(([^\)]+?)( all)?\)/i', - 'replacement' => '@extend $1', - ], - [ // Wrap variables in calcs with #{} - 'pattern' => '/calc\([^;]+/i', - 'replacement' => function ($matches) { - return preg_replace('/(\$[\w\-]+)/i', '#{$1}', $matches[0]); - }, - ], - [ // Wrap variables set to css variables with #{} - 'pattern' => '/(--[\w:-]+:\s*)((\$|darken\(|lighten\()[^;]+)/i', - 'replacement' => '$1#{$2}', - ], - [ // Remove !default from extends (icons.scss) - 'pattern' => '/@extend ([^;}]+) !default;/i', - 'replacement' => '@extend $1;', - ], + [ // Activate SCSS + 'pattern' => '/\/\* #SCSS>/i', + 'replacement' => '/* #SCSS> */', + ], + [ + 'pattern' => '/<#SCSS \*\//i', + 'replacement' => '/* <#SCSS */', + ], + [ + 'pattern' => '/\/\* #LESS> \*\//i', + 'replacement' => '/* #LESS>', + ], + [ + 'pattern' => '/\/\* <#LESS \*\//i', + 'replacement' => '<#LESS */', + ], + [ // Fix include parameter separator + 'pattern' => '/@include ([^\(]+)\(([^\)]+)\);/i', + 'replacement' => function ($matches) { + [, $m1, $m2] = $matches; + return '@include ' . $m1 . '(' . str_replace(';', ',', $m2) . ');'; + }, + ], + [ // Fix tilde literals + 'pattern' => "/~'(.*?)'/i", + 'replacement' => '$1', + ], + [ // Convert inline &:extends + 'pattern' => '/&:extend\(([^\)]+?)( all)?\)/i', + 'replacement' => '@extend $1', + ], + [ // Wrap variables in calcs with #{} + 'pattern' => '/calc\([^;]+/i', + 'replacement' => function ($matches) { + return preg_replace('/(\$[\w\-]+)/i', '#{$1}', $matches[0]); + }, + ], + [ // Wrap variables set to css variables with #{} + 'pattern' => '/(--[\w:-]+:\s*)((\$|darken\(|lighten\()[^;]+)/i', + 'replacement' => '$1#{$2}', + ], + [ // Remove !default from extends (icons.scss) + 'pattern' => '/@extend ([^;}]+) !default;/i', + 'replacement' => '@extend $1;', + ], - [ // Fix comparison: - 'pattern' => '/ ==< /i', - 'replacement' => ' <= ', - ], - [ // Remove !important from variables: - 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*;/m', - 'replacement' => '$1:$2;', - ], -/* [ // Remove !important from functions: - 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*\)/m', - 'replacement' => '$1:$2;', - ],*/ - [ // fadein => fade-in: - 'pattern' => '/fadein\((\S+),\s*(\S+)\)/', - 'replacement' => function ($matches) { - return 'fade-in(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; - }, - ], - [ // fadeout => fade-out: - 'pattern' => '/fadeout\((\S+),\s*(\S+)\)/', - 'replacement' => function ($matches) { - return 'fade-out(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; - }, - ], - [ // replace invalid characters in variable names: - 'pattern' => '/\$([^: };\/]+)/', - 'replacement' => function ($matches) { - return '$' . str_replace('.', '__', $matches[1]); - }, - ], - [ // remove invalid &: - 'pattern' => '/([a-zA-Z0-9])&:/', - 'replacement' => '$1:', - ], - [ // remove (reference) from import): - 'pattern' => '/@import\s+\(reference\)\s*/', - 'replacement' => '@import /*(reference)*/ ', - ], - [ // fix missing semicolon from background-image rule: - 'pattern' => '/(\$background-image:([^;]+?))\n/', - 'replacement' => "\$1;\n", - ], - [ // remove broken (and useless) rule: - 'pattern' => '/\.feed-container \.list-feed \@include feed-header\(\);/', - 'replacement' => '', - ], - [ // interpolate variables in media queries: - 'pattern' => '/\@media (\$[^ ]+)/', - 'replacement' => '@media #{$1}', - ], - [ // missing semicolon: - 'pattern' => '/(:.*auto)\n/', - 'replacement' => "\$1;\n", - ], - [ // lost space in mixin declarations: - 'pattern' => '/(\@mixin.+){/', - 'replacement' => '$1 {', - ], - [ // special cases: mobile mixin - 'pattern' => '/\.mobile\(\{(.*?)\}\);/s', - 'replacement' => '@media #{$mobile} { & { $1 } }', - ], - [ // special cases: mobile mixin 2 - 'pattern' => '@mixin mobile($rules){', - 'replacement' => '@mixin mobile {', - ], - [ // special cases: mobile mixin 3 - 'pattern' => '$rules();', - 'replacement' => '@content;', - ], - [ // invalid mixin name - 'pattern' => 'text(uppercase)', - 'replacement' => 'text-uppercase', - ], - [ // when isnumber - 'pattern' => '& when (isnumber($z-index))', - 'replacement' => '@if $z-index != null', - ], - [ // blocks extending container - 'pattern' => '@include container();', - 'replacement' => '@extend .container;', - ], - [ // blocks extending more-link - 'pattern' => '@include more-link();', - 'replacement' => '@extend .more-link;', - ], - [ // fix math operations - 'pattern' => '/(\s+)(\(.+\/.+\))/', - 'replacement' => '$1calc$2', - ], - [ // typo - 'pattern' => '$carousel-header-color none;', - 'replacement' => '$carousel-header-color: none;', - ], - [ // typo - 'pattern' => '$brand-primary // $link-color;', - 'replacement' => '$brand-primary; // $link-color', - ], - [ // typo - 'pattern' => '- aukioloaikojen otsikko', - 'replacement' => '{ /* aukioloaikojen otsikko */ }', - ], - [ // typo - 'pattern' => '$link-hover-color: $tut-a-hover,', - 'replacement' => '$link-hover-color: $tut-a-hover;', - ], - [ // typo - 'pattern' => 'rgba(43,65,98,0,9)', - 'replacement' => 'rgba(43,65,98,0.9)', - ], - [ // typo $input-bg: ##ff8d0f; - 'pattern' => '/:\s*##+/', - 'replacement' => ': #', - ], - [ // typo - 'pattern' => '!importanti', - 'replacement' => '!important', - ], - [ // typo - 'pattern' => '$brand-secondary: #;', - 'replacement' => '', - ], - [ // typo - 'pattern' => '$brand-secondary: ###;', - 'replacement' => '', - ], - [ // typo - 'pattern' => '#00000;', - 'replacement' => '#000000;', - ], - [ // typo - 'pattern' => 'background-color: ;', - 'replacement' => '', - ], - [ // typo - 'pattern' => '$header-background-color #fff;', - 'replacement' => '$header-background-color: #fff;', - ], - [ // typo - 'pattern' => '$action-link-color #FFF;', - 'replacement' => '$action-link-color: #FFF;', - ], - [ // typo - 'pattern' => '$finna-browsebar-background (selaa palkin taustaväri)', - 'replacement' => '//$finna-browsebar-background (selaa palkin taustaväri)', - ], - [ // typo - 'pattern' => '$finna-browsebar-link-color(selaa palkin linkin)', - 'replacement' => '//$finna-browsebar-link-color(selaa palkin linkin)', - ], - [ // typo - 'pattern' => '$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', - 'replacement' => '//$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', - ], - [ // typo - 'pattern' => '$home-2_fi {', - 'replacement' => '.home-2_fi {', - ], - [ // Convert unsupported nested extend - 'pattern' => '@extend .finna-panel-default .panel-heading;', - 'replacement' => << '@extend .finna-panel-default .finna-panel-heading-inner;', - 'replacement' => << '/ ==< /i', + 'replacement' => ' <= ', + ], + [ // Remove !important from variables: + 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*;/m', + 'replacement' => '$1:$2;', + ], + /* [ // Remove !important from functions: + 'pattern' => '/^[^(]*(\$.+?):(.+?)\s*!important\s*\)/m', + 'replacement' => '$1:$2;', + ],*/ + [ // fadein => fade-in: + 'pattern' => '/fadein\((\S+),\s*(\S+)\)/', + 'replacement' => function ($matches) { + return 'fade-in(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; + }, + ], + [ // fadeout => fade-out: + 'pattern' => '/fadeout\((\S+),\s*(\S+)\)/', + 'replacement' => function ($matches) { + return 'fade-out(' . $matches[1] . ', ' . (str_replace('%', '', $matches[2]) / 100) . ')'; + }, + ], + [ // replace invalid characters in variable names: + 'pattern' => '/\$([^: };\/]+)/', + 'replacement' => function ($matches) { + return '$' . str_replace('.', '__', $matches[1]); + }, + ], + [ // remove invalid &: + 'pattern' => '/([a-zA-Z0-9])&:/', + 'replacement' => '$1:', + ], + [ // remove (reference) from import): + 'pattern' => '/@import\s+\(reference\)\s*/', + 'replacement' => '@import /*(reference)*/ ', + ], + [ // fix missing semicolon from background-image rule: + 'pattern' => '/(\$background-image:([^;]+?))\n/', + 'replacement' => "\$1;\n", + ], + [ // remove broken (and useless) rule: + 'pattern' => '/\.feed-container \.list-feed \@include feed-header\(\);/', + 'replacement' => '', + ], + [ // interpolate variables in media queries: + 'pattern' => '/\@media (\$[^ ]+)/', + 'replacement' => '@media #{$1}', + ], + [ // missing semicolon: + 'pattern' => '/(:.*auto)\n/', + 'replacement' => "\$1;\n", + ], + [ // lost space in mixin declarations: + 'pattern' => '/(\@mixin.+){/', + 'replacement' => '$1 {', + ], + [ // special cases: mobile mixin + 'pattern' => '/\.mobile\(\{(.*?)\}\);/s', + 'replacement' => '@media #{$mobile} { & { $1 } }', + ], + [ // special cases: mobile mixin 2 + 'pattern' => '@mixin mobile($rules){', + 'replacement' => '@mixin mobile {', + ], + [ // special cases: mobile mixin 3 + 'pattern' => '$rules();', + 'replacement' => '@content;', + ], + [ // invalid mixin name + 'pattern' => 'text(uppercase)', + 'replacement' => 'text-uppercase', + ], + [ // when isnumber + 'pattern' => '& when (isnumber($z-index))', + 'replacement' => '@if $z-index != null', + ], + [ // blocks extending container + 'pattern' => '@include container();', + 'replacement' => '@extend .container;', + ], + [ // blocks extending more-link + 'pattern' => '@include more-link();', + 'replacement' => '@extend .more-link;', + ], + [ // fix math operations + 'pattern' => '/(\s+)(\(.+\/.+\))/', + 'replacement' => '$1calc$2', + ], + [ // typo + 'pattern' => '$carousel-header-color none;', + 'replacement' => '$carousel-header-color: none;', + ], + [ // typo + 'pattern' => '$brand-primary // $link-color;', + 'replacement' => '$brand-primary; // $link-color', + ], + [ // typo + 'pattern' => '- aukioloaikojen otsikko', + 'replacement' => '{ /* aukioloaikojen otsikko */ }', + ], + [ // typo + 'pattern' => '$link-hover-color: $tut-a-hover,', + 'replacement' => '$link-hover-color: $tut-a-hover;', + ], + [ // typo + 'pattern' => 'rgba(43,65,98,0,9)', + 'replacement' => 'rgba(43,65,98,0.9)', + ], + [ // typo $input-bg: ##ff8d0f; + 'pattern' => '/:\s*##+/', + 'replacement' => ': #', + ], + [ // typo + 'pattern' => '!importanti', + 'replacement' => '!important', + ], + [ // typo + 'pattern' => '$brand-secondary: #;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '$brand-secondary: ###;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '#00000;', + 'replacement' => '#000000;', + ], + [ // typo + 'pattern' => 'background-color: ;', + 'replacement' => '', + ], + [ // typo + 'pattern' => '$header-background-color #fff;', + 'replacement' => '$header-background-color: #fff;', + ], + [ // typo + 'pattern' => '$action-link-color #FFF;', + 'replacement' => '$action-link-color: #FFF;', + ], + [ // typo + 'pattern' => '$finna-browsebar-background (selaa palkin taustaväri)', + 'replacement' => '//$finna-browsebar-background (selaa palkin taustaväri)', + ], + [ // typo + 'pattern' => '$finna-browsebar-link-color(selaa palkin linkin)', + 'replacement' => '//$finna-browsebar-link-color(selaa palkin linkin)', + ], + [ // typo + 'pattern' => '$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', + 'replacement' => '//$finna-browsebar-highlight-background (selaa palkin korotuksen taustaväri)', + ], + [ // typo + 'pattern' => '$home-2_fi {', + 'replacement' => '.home-2_fi {', + ], + [ // Convert unsupported nested extend + 'pattern' => '@extend .finna-panel-default .panel-heading;', + 'replacement' => << '@extend .finna-panel-default .finna-panel-heading-inner;', + 'replacement' => << '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', - 'replacement' => 'background-image: linear-gradient(to bottom, $background-start-color $background-start-percent, $background-end-color $background-end-percent);', - ], - [ // common typo in home column styles - 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?\& .right-column-content \{.*?\}.*?\})/s', - 'replacement' => "\$1\$2\n}", - ], - [ // another typo in home column styles - 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', - 'replacement' => '$1$2', - ], - [ // missing semicolon: display: none - 'pattern' => '/display: none\n/', - 'replacement' => 'display: none;', - ], - [ // missing semicolon in variable definitions - 'pattern' => '/(\n\s*\$[a-zA-Z0-9_-]+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', - 'replacement' => '$1;$2', - ], - [ // missing semicolon: $header-text-color: #000000 - 'pattern' => '/$header-text-color: #000000\n/', - 'replacement' => '$header-text-color: #000000;', - ], - [ // missing semicolon: clip: rect(0px,1200px,1000px,0px) - 'pattern' => '/clip: rect\(0px,1200px,1000px,0px\)\n/', - 'replacement' => "clip: rect(0px,1200px,1000px,0px);\n", - ], - [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // - 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', - 'replacement' => '$finna-feedback-background: darken(#d80073, 10%);$1', - ], - [ // invalid (and obsolete) rule - 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', - 'replacement' => "// Invalid rule commented out by SCSS conversion\n/*\n\$1\n*/", - ], + [ // gradient mixin call + 'pattern' => '#gradient.vertical($background-start-color; $background-end-color; $background-start-percent; $background-end-percent);', + 'replacement' => 'background-image: linear-gradient(to bottom, $background-start-color $background-start-percent, $background-end-color $background-end-percent);', + ], + [ // common typo in home column styles + 'pattern' => '/(\.home-1, \.home-3 \{[^}]+)}(\s*\n\s*\& \.left-column-content.*?\& .right-column-content \{.*?\}.*?\})/s', + 'replacement' => "\$1\$2\n}", + ], + [ // another typo in home column styles + 'pattern' => '/(\n\s+\.left-column-content.*?\n\s+)& (.right-column-content)/s', + 'replacement' => '$1$2', + ], + [ // missing semicolon: display: none + 'pattern' => '/display: none\n/', + 'replacement' => 'display: none;', + ], + [ // missing semicolon in variable definitions + 'pattern' => '/(\n\s*\$[a-zA-Z0-9_-]+\s*:\s*?[^;\s]+)((\n|\s*\/\/))/', + 'replacement' => '$1;$2', + ], + [ // missing semicolon: $header-text-color: #000000 + 'pattern' => '/$header-text-color: #000000\n/', + 'replacement' => '$header-text-color: #000000;', + ], + [ // missing semicolon: clip: rect(0px,1200px,1000px,0px) + 'pattern' => '/clip: rect\(0px,1200px,1000px,0px\)\n/', + 'replacement' => "clip: rect(0px,1200px,1000px,0px);\n", + ], + [ // missing semicolon: $finna-feedback-background: darken(#d80073, 10%) // + 'pattern' => '/\$finna-feedback-background: darken\(#d80073, 10%\)\s*?(\n|\s*\/\/)/', + 'replacement' => '$finna-feedback-background: darken(#d80073, 10%);$1', + ], + [ // invalid (and obsolete) rule + 'pattern' => '/(\@supports\s*\(-ms-ime-align:\s*auto\)\s*\{\s*\n\s*clip-path.*?\})/s', + 'replacement' => "// Invalid rule commented out by SCSS conversion\n/*\n\$1\n*/", + ], - [ // literal fix - 'pattern' => "~ ')'", - 'replacement' => ')', - ], - [ // literal fix - 'pattern' => 'calc(100vh - "#{$navbar-height}~")', - 'replacement' => 'calc(100vh - #{$navbar-height})', - ], - [ // math without calc - 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', - 'replacement' => function ($matches) { - [$full, $pre, $math] = $matches; - if (str_contains($matches[1], '(')) { - return $full; - } - return $pre . "calc($math)"; - }, - ], - [ // variable interpolation - 'pattern' => '/\$\{([A-Za-z0-9_-]+)\}/', - 'replacement' => '#{\$$1}', + [ // literal fix + 'pattern' => "~ ')'", + 'replacement' => ')', + ], + [ // literal fix + 'pattern' => 'calc(100vh - "#{$navbar-height}~")', + 'replacement' => 'calc(100vh - #{$navbar-height})', + ], + [ // math without calc + 'pattern' => '/(.*\s)(\S+ \/ (\$|\d)[^\s;]*)/', + 'replacement' => function ($matches) { + [$full, $pre, $math] = $matches; + if (str_contains($matches[1], '(')) { + return $full; + } + return $pre . "calc($math)"; + }, + ], + [ // variable interpolation + 'pattern' => '/\$\{([A-Za-z0-9_-]+)\}/', + 'replacement' => '#{\$$1}', + ], ], ]; diff --git a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php index bf1cc910f6b..f71c908b0fd 100644 --- a/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php +++ b/module/FinnaConsole/src/FinnaConsole/Command/Util/LessToScssCommand.php @@ -882,7 +882,7 @@ protected function getSubstitutions(): array $this->debug("Using shared config file $configFile", OutputInterface::VERBOSITY_DEBUG); $config = include $configFile; } - return $config; + return $config['substitutions']; } /** From 571196cda99aad99e63400dff22a301f6bf173ff Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 12:30:31 +0300 Subject: [PATCH 29/30] Revert order change in finna.less. --- themes/custom/less/finna.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/themes/custom/less/finna.less b/themes/custom/less/finna.less index 2d262c7082b..9b123afc96c 100644 --- a/themes/custom/less/finna.less +++ b/themes/custom/less/finna.less @@ -1,12 +1,12 @@ /* #SCSS> @import "../../finna2/scss/scss-functions"; -// Variable overrides -@import "variables"; - // Custom theme variable overrides @import "variables-custom"; +// Variable overrides +@import "variables"; + // Finna Bootstrap variable overrides @import "../../finna2/scss/global/bootstrap-variable-overrides"; From 570f93207d9bd4acba029730eacd0dd6b38a63e9 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Thu, 24 Oct 2024 12:38:58 +0300 Subject: [PATCH 30/30] Update finna.scss --- themes/custom/scss/finna.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/themes/custom/scss/finna.scss b/themes/custom/scss/finna.scss index a51edc74cf1..8a192e4f1ef 100644 --- a/themes/custom/scss/finna.scss +++ b/themes/custom/scss/finna.scss @@ -1,12 +1,12 @@ /* #SCSS> */ @import "../../finna2/scss/scss-functions"; -// Variable overrides -@import "variables"; - // Custom theme variable overrides @import "variables-custom"; +// Variable overrides +@import "variables"; + // Finna Bootstrap variable overrides @import "../../finna2/scss/global/bootstrap-variable-overrides";