Skip to content

Instantly share code, notes, and snippets.

@radist2s
Last active May 4, 2019 22:52
Show Gist options
  • Save radist2s/21509a7d366ec98aad978bc2fde2beef to your computer and use it in GitHub Desktop.
Save radist2s/21509a7d366ec98aad978bc2fde2beef to your computer and use it in GitHub Desktop.
Tool for renaming BEM Blocks
<?php
/**
* Usage
* bem-block-rename.php ./templates/ .renaming-block-name .destination-block-name # will change every `.blade.php` in directory
* bem-block-rename.php .renaming-block--with-mod .dest-block--with-renamed-mode # will change every `.blade.php` in CWD directory
* bem-block-rename.php specified-file.php .renaming-block-name__child .destination-block-name__renamed-child # will change only `specified-file.php`
*/
namespace BemClassesTools;
const FILE_FORMAT = '.blade.php';
const ELEMENTS_SEPARATOR = '__';
const MODIFIER_SEPARATOR = '--';
/**
* @param $path
* @param string $template_ext
* @return array
* @throws \Exception
*/
function find_templates($path, $template_ext = FILE_FORMAT): array {
if (is_file($path)) {
return [$path];
}
if (!is_dir($path)) {
throw new \Exception("Specified path is not and directory: `$path`");
}
$files = array_diff(scandir($path), ['.', '..']);
if (!$files) {
return [];
}
$templates = [];
$template_ext_escaped = preg_quote($template_ext);
$template_ext_regexp = "~$template_ext_escaped$~iu";
$base_dir = rtrim($path, DIRECTORY_SEPARATOR);
foreach ($files as $file) {
$file_path = $base_dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($file_path)) {
$templates = $templates + find_templates($file_path);
}
elseif (is_file($file_path) and preg_match($template_ext_regexp, $file_path)) {
$templates[] = $file_path;
}
}
return $templates;
}
/**
* @param $name
* @throws \Exception
*/
function validate_block_name($name) {
if (!preg_match('~^\.~', $name)) {
throw new \Exception("Block name must begin with dot. Example: `.block-name`, provided name: `$name`");
}
if (!preg_match('~[^\W_]$~iu', $name)) {
throw new \Exception("Block name must end with letter or number. Example: `.block-name`, provided name: `$name`");
}
}
/**
* @param string $code
* @param string $block_source
* @param string $block_dest
* @param string $elements_separator
* @param string $modifier_separator
* @return array
*/
function replace_block_name(string $code, string $block_source, string $block_dest, $elements_separator = ELEMENTS_SEPARATOR, $modifier_separator = MODIFIER_SEPARATOR) {
$block_source = trim($block_source);
$block_dest = trim($block_dest);
validate_block_name($block_source);
validate_block_name($block_dest);
$block_name_dest = trim($block_dest, '.');
$block_name_escaped = preg_quote(trim($block_source, '.'));
$sep_el = preg_quote($elements_separator);
$sep_mod = preg_quote($modifier_separator);
$block_find_regex = "(?<=^|\s|['\"`])(?P<Block>{$block_name_escaped})(?P<BlockAfter>$|{$sep_el}|{$sep_mod}|[^\w{$sep_el}{$sep_mod}])";
$code_highlighted = '';
$code_to_highlight = $code;
$lines_count = 0;
$replace_callback = function ($matches) use ($block_name_dest, &$code_highlighted, &$code_to_highlight, &$lines_count) {
['0' => $matched_string, 'BlockAfter' => $block_after] = $matches;
$matched_position_start = mb_strpos($code_to_highlight, $matched_string);
$matched_before_code = mb_substr($code_to_highlight, 0, $matched_position_start);
$matched_before_code_lines = explode("\n", $matched_before_code);
// $lines_count = $lines_count + count($matched_before_code_lines); // todo::fixme
$matched_line_beginning = end($matched_before_code_lines);
$matched_after_code = mb_substr($code_to_highlight, $matched_position_start + mb_strlen($matched_string));
$matched_line_ending = explode("\n", $matched_after_code, 2)[0] ?? '';
$code_highlighted .=
// "\e[36mLine {$lines_count}: \e[0m" . // echo colors: https://misc.flogisoft.com/bash/tip_colors_and_formatting
$matched_line_beginning .
"\e[42m{$block_name_dest}\e[0m" .
$block_after .
$matched_line_ending .
PHP_EOL;
$code_to_highlight = mb_substr($code_to_highlight, $matched_position_start + mb_strlen($matched_string));
return $block_name_dest . $block_after;
};
$code_refactored = preg_replace_callback("~$block_find_regex~mu", $replace_callback, $code);
return [
$code_refactored,
$code_highlighted
];
}
/**
* @param string $path
* @param string $block_source
* @param string $block_dest
* @param string $elements_separator
* @param string $modifier_separator
* @return array
* @throws \Exception
*/
function replace_block_name_by_path(string $path, string $block_source, string $block_dest, $elements_separator = ELEMENTS_SEPARATOR, $modifier_separator = MODIFIER_SEPARATOR) {
if (!is_writable($path)) {
throw new \Exception("Specified file is not writable: `$path`");
}
$code = file_get_contents($path);
return replace_block_name($code, $block_source, $block_dest, $elements_separator, $modifier_separator);
}
function get_relative_path($from, $to) {
// some compatibility fixes for Windows paths
$from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
$to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
$from = str_replace('\\', '/', $from);
$to = str_replace('\\', '/', $to);
$from = explode('/', $from);
$to = explode('/', $to);
$relPath = $to;
foreach ($from as $depth => $dir) {
// find first non-matching dir
if ($dir === $to[$depth]) {
// ignore this directory
array_shift($relPath);
}
else {
// get number of remaining dirs to $from
$remaining = count($from) - $depth;
if ($remaining > 1) {
// add traversals up to first matching dir
$padLength = (count($relPath) + $remaining - 1) * -1;
$relPath = array_pad($relPath, $padLength, '..');
break;
}
else {
$relPath[0] = './' . $relPath[0];
}
}
}
return implode('/', $relPath);
}
/**
* @param array $args
* @throws \Exception
*/
function main(...$args) {
$option_args = array_filter($args, function ($arg) {
return mb_strpos($arg, '-') === 0;
});
$command_args = array_diff($args, $option_args);
$options = array_map(
function ($arg) {
return ltrim($arg, '-');
},
$option_args
);
$command_args_length = count($command_args);
if ($command_args_length === 3) {
[$path, $block_source, $block_dest] = $command_args;
}
else if ($command_args_length == 2) {
[$block_source, $block_dest] = $command_args;
if (is_file($block_source)) {
throw new \Exception("Too ambiguous, found file `$block_source`, but this is proved as block name. Provide `path` as first argument.");
}
$path = getcwd();
}
else {
throw new \Exception("Wrong args count provided.");
}
$file_list = find_templates($path);
$is_dry_run = !array_intersect($options, ['f', 'force', 'write']);
$files_refactored_list = [];
foreach ($file_list as $path) {
[$code_refactored, $code_highlighted] = replace_block_name_by_path($path, $block_source, $block_dest);
if (!$code_refactored or !$code_highlighted) {
continue;
}
$files_refactored_list[] = $path;
$file_relative_path = get_relative_path(getcwd(), $path);
echo
"\e[32m{$file_relative_path}\e[0m\n",
$code_highlighted,
PHP_EOL;
if (!$is_dry_run) {
file_put_contents($path, $code_refactored);
}
}
if ($files_refactored_list) {
if ($is_dry_run) {
$message = "Dry run is complete. To replace block names in destination files provide `-f` argument.";
echo "\e[31m{$message}\e[0m\n", PHP_EOL;
}
}
else {
$message = "Block not found, provided: `$block_source`";
echo "\e[31m{$message}\e[0m\n", PHP_EOL;
}
}
call_user_func_array('\BemClassesTools\main', array_slice($argv, 1));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment