Convert resource loading code into a method of Template class

This commit is contained in:
Kijin Sung 2023-10-17 23:29:55 +09:00
parent 55cafc5c33
commit 012dbb9ab7
6 changed files with 227 additions and 225 deletions

View file

@ -25,7 +25,8 @@ class Template
public $relative_path;
public $cache_path;
public $cache_enabled = true;
public $ob_level = 0;
public $source_type;
public $ob_level;
public $vars;
protected $_fragments = [];
protected static $_loopvars = [];
@ -146,6 +147,7 @@ class Template
$this->config->version = 2;
$this->config->autoescape = true;
}
$this->source_type = preg_match('!^((?:m\.)?[a-z]+)/!', $this->relative_dirname, $match) ? $match[1] : null;
$this->_setCachePath();
}
@ -409,10 +411,183 @@ class Template
}
}
/**
* Check if a path should be treated as relative to the path of the current template.
*
* @param string $path
* @return bool
*/
public function isRelativePath(string $path): bool
{
return !preg_match('#^((?:https?|file|data):|[\/\{<])#i', $path);
}
/**
* Convert a relative path using the given basepath.
*
* @param string $path
* @param string $basepath
* @return string
*/
public function convertPath(string $path, string $basepath): string
{
// Path relative to the Rhymix installation directory?
if (preg_match('#^\^/?(\w.+)$#s', $path, $match))
{
$path = \RX_BASEURL . $match[1];
}
// Other paths will be relative to the given basepath.
else
{
$path = preg_replace('#/\./#', '/', $basepath . $path);
}
// Remove extra slashes and parent directory references.
$path = preg_replace('#\\\\#', '/', $path);
$path = preg_replace('#//#', '/', $path);
while (($tmp = preg_replace('#/[^/]+/\.\./#', '/', $path)) !== $path)
{
$path = $tmp;
}
return $path;
}
/**
* =================== HELPER FUNCTIONS FOR TEMPLATE v2 ===================
*/
/**
* Load a resource from v2 @load directive.
*
* The Blade-style syntax does not have named arguments, so we must rely
* on the position and format of each argument to guess what it is for.
* Fortunately, there are only a handful of valid options for the type,
* media, and index attributes.
*
* @param ...$args
* @return void
*/
protected function _v2_loadResource(...$args): void
{
// Assign the path.
$path = null;
if (count($args))
{
$path = array_shift($args);
}
if (empty($path))
{
trigger_error('Resource loading directive used with no path', \E_USER_WARNING);
return;
}
// Assign the remaining arguments to respective array keys.
$info = [];
while ($value = array_shift($args))
{
if (preg_match('#^([\'"])(head|body)\1$#', $value, $match))
{
$info['type'] = $match[2];
}
elseif (preg_match('#^([\'"])((?:screen|print)[^\'"]*)\1$#', $value, $match))
{
$info['media'] = $match[2];
}
elseif (preg_match('#^([\'"])([0-9]+)\1$#', $value, $match))
{
$info['index'] = $match[2];
}
elseif (ctype_digit($value))
{
$info['index'] = $value;
}
else
{
$info['vars'] = $value;
}
}
// Check whether the path is an internal or external link.
$external = false;
if (preg_match('#^\^#', $path))
{
$path = './' . ltrim($path, '^/');
}
elseif ($this->isRelativePath($path))
{
$path = $this->convertPath($path, './' . $this->relative_dirname);
}
else
{
$external = true;
}
// Determine the type of resource.
if (!$external && str_starts_with($path, './common/js/plugins/'))
{
$type = 'jsplugin';
}
elseif (!$external && preg_match('#/lang(\.xml)?$#', $path))
{
$type = 'lang';
}
elseif (preg_match('#\.(css|js|scss|less)($|\?|/)#', $path, $match))
{
$type = $match[1];
}
elseif (preg_match('#/css\d?\?.+#', $path))
{
$type = 'css';
}
else
{
$type = 'unknown';
}
// Load the resource.
if ($type === 'jsplugin')
{
if (preg_match('#/common/js/plugins/([^/]+)#', $path, $match))
{
$plugin_name = $match[1];
\Context::loadJavascriptPlugin($plugin_name);
}
else
{
trigger_error("Unable to find JS plugin at $path", \E_USER_WARNING);
}
}
elseif ($type === 'lang')
{
$lang_dir = preg_replace('#/lang\.xml$#', '', $path);
\Context::loadLang($lang_dir);
}
elseif ($type === 'js')
{
\Context::loadFile([
$path,
$info['type'] ?? '',
$external ? $this->source_type : '',
isset($info['index']) ? intval($info['index']) : '',
]);
}
elseif ($type === 'css' || $type === 'scss' || $type === 'less')
{
\Context::loadFile([
$path,
$info['media'] ?? '',
$external ? $this->source_type : '',
isset($info['index']) ? intval($info['index']) : '',
$info['vars'] ?? [],
]);
}
else
{
trigger_error("Unable to determine type of resource at $path", \E_USER_WARNING);
}
}
/**
* Initialize v2 loop variable.
*

View file

@ -24,7 +24,6 @@ class TemplateParser_v2
* Cache template path info here.
*/
public $template;
public $source_type;
/**
* Properties for internal bookkeeping.
@ -91,7 +90,6 @@ class TemplateParser_v2
{
// Store template info in instance property.
$this->template = $template;
$this->source_type = preg_match('!^((?:m\.)?[a-z]+)/!', $template->relative_dirname, $match) ? $match[1] : null;
// Preprocessing.
$content = $this->_preprocess($content);
@ -105,7 +103,7 @@ class TemplateParser_v2
$content = $this->_convertFragments($content);
$content = $this->_convertClassAliases($content);
$content = $this->_convertIncludes($content);
$content = $this->_convertAssets($content);
$content = $this->_convertResource($content);
$content = $this->_convertLoopDirectives($content);
$content = $this->_convertInlineDirectives($content);
$content = $this->_convertMiscDirectives($content);
@ -185,13 +183,13 @@ class TemplateParser_v2
if ($match[2] !== 'srcset')
{
$src = trim($match[3]);
return $match[1] . sprintf('%s="%s"', $match[2], self::_isRelativePath($src) ? self::_convertRelativePath($src, $basepath) : $src);
return $match[1] . sprintf('%s="%s"', $match[2], $this->template->isRelativePath($src) ? $this->template->convertPath($src, $basepath) : $src);
}
else
{
$srcset = array_map('trim', explode(',', $match[3]));
$result = array_map(function($src) use($basepath) {
return self::_isRelativePath($src) ? self::_convertRelativePath($src, $basepath) : $src;
return $this->template->isRelativePath($src) ? $this->template->convertPath($src, $basepath) : $src;
}, array_filter($srcset, function($src) {
return !empty($src);
}));
@ -461,7 +459,7 @@ class TemplateParser_v2
}
/**
* Convert asset loading directives.
* Convert resource loading directives.
*
* This can be used to load nearly every kind of asset, from scripts
* and stylesheets to lang files to Rhymix core Javascript plugins.
@ -479,171 +477,31 @@ class TemplateParser_v2
* @param string $content
* @return string
*/
protected function _convertAssets(string $content): string
protected function _convertResource(string $content): string
{
// Convert XE-style load directives.
$regexp = '#(<load(?:\s+(?:target|src|type|media|index|vars)="(?:[^"]+)")+\s*/?>)#';
$content = preg_replace_callback($regexp, function($match) {
$attrs = self::_getTagAttributes($match[1]);
$attrs['src'] = $attrs['src'] ?? ($attrs['target'] ?? null);
if (!$attrs['src']) return $match[0];
return self::_escapeVars(self::_generateCodeForAsset($attrs));
return vsprintf('<?php \$this->_v2_loadResource(%s, %s, %s, %s); ?>', [
var_export($attrs['src'] ?? ($attrs['target'] ?? ''), true),
var_export($attrs['type'] ?? ($attrs['media'] ?? ''), true),
var_export($attrs['index'] ?? '', true),
self::_convertVariableScope($attrs['vars'] ?? '') ?: '[]',
]);
}, $content);
// Convert Blade-style load directives.
$parentheses = self::_getRegexpForParentheses(1);
$regexp = '#(?<!@)@load\x20?(' . $parentheses . ')#';
$content = preg_replace_callback($regexp, function($match) {
$args = array_map('trim', explode(',', substr($match[1], 1, strlen($match[1]) - 2)));
$attrs = self::_arrangeArgumentsForAsset($args);
if (!$attrs['src']) return $match[0];
return self::_escapeVars(self::_generateCodeForAsset($attrs));
$args = self::_convertVariableScope(substr($match[1], 1, strlen($match[1]) - 2));
return sprintf('<?php \$this->_v2_loadResource(%s); ?>', $args);
}, $content);
return $content;
}
/**
* Subroutine for determining the role of each argument to @load.
*
* The Blade-style syntax does not have named arguments, so we must rely
* on the position and format of each argument to guess what it is for.
* Fortunately, there are only a handful of valid options for the type,
* media, and index attributes.
*
* @param array $args
* @return array
*/
protected function _arrangeArgumentsForAsset(array $args): array
{
// Assign the path.
$info = [];
if (preg_match('#^([\'"])([^\'"]+)\1$#', array_shift($args) ?? '', $match))
{
$info['src'] = $match[2];
}
if (!$info['src'])
{
return [];
}
// Assign the remaining arguments to respective array keys.
while ($value = array_shift($args))
{
if (preg_match('#^([\'"])(head|body)\1$#', $value, $match))
{
$info['type'] = $match[2];
}
elseif (preg_match('#^([\'"])((?:screen|print)[^\'"]*)\1$#', $value, $match))
{
$info['media'] = $match[2];
}
elseif (preg_match('#^([\'"])([0-9]+)\1$#', $value, $match))
{
$info['index'] = $match[2];
}
elseif (ctype_digit($value))
{
$info['index'] = $value;
}
else
{
$info['vars'] = $value;
}
}
return $info;
}
/**
* Subroutine to generate code for asset loading.
*
* @param array $info
* @return string
*/
protected function _generateCodeForAsset(array $info): string
{
// Determine whether the path is an internal or external link.
$path = $info['src'];
$external = false;
if (preg_match('#^\^#', $path))
{
$path = './' . ltrim($path, '^/');
}
elseif (self::_isRelativePath($path))
{
$path = self::_convertRelativePath($path, './' . $this->template->relative_dirname);
}
else
{
$external = true;
}
// Determine the type of resource.
if (!$external && str_starts_with($path, './common/js/plugins/'))
{
$restype = 'jsplugin';
}
elseif (!$external && preg_match('#/lang(\.xml)?$#', $path))
{
$restype = 'lang';
}
elseif (preg_match('#\.(css|js|scss|less)($|\?|/)#', $path, $match))
{
$restype = $match[1];
}
elseif (preg_match('#/css\d?\?.+#', $path))
{
$restype = 'css';
}
else
{
$restype = 'unknown';
}
// Generate code for each type of asset.
if ($restype === 'jsplugin')
{
if (preg_match('#/common/js/plugins/([^/]+)#', $path, $match))
{
$plugin_name = $match[1];
return sprintf('<?php \Context::loadJavascriptPlugin(%s); ?>', var_export($plugin_name, true));
}
else
{
return sprintf('<?php trigger_error("Unable to find JS plugin at " . %s, \E_USER_WARNING); ?>', var_export($path, true));
}
}
elseif ($restype === 'lang')
{
$lang_dir = preg_replace('#/lang\.xml$#', '', $path);
return sprintf('<?php \Context::loadLang(%s); ?>', var_export($lang_dir, true));
}
elseif ($restype === 'js')
{
return vsprintf('<?php \Context::loadFile([%s, %s, %s, %s]); ?>', [
var_export($path, true),
var_export($info['type'] ?? '', true),
var_export($external ? $this->source_type : '', true),
var_export(isset($info['index']) ? intval($info['index']) : '', true),
]);
}
elseif ($restype === 'unknown')
{
return sprintf('<?php trigger_error("Unable to determine type of resource at " . %s, \E_USER_WARNING); ?>', var_export($path, true));
}
else
{
return vsprintf('<?php \Context::loadFile([%s, %s, %s, %s, %s]); ?>', [
var_export($path, true),
var_export($info['media'] ?? '', true),
var_export($external ? $this->source_type : '', true),
var_export(isset($info['index']) ? intval($info['index']) : '', true),
empty($info['vars']) ? '[]' : self::_convertVariableScope($info['vars'])
]);
}
}
/**
* Convert loop and condition directives.
*
@ -1072,48 +930,6 @@ class TemplateParser_v2
return $content;
}
/**
* Check if a path should be treated as relative to the path of the current template.
*
* @param string $path
* @return bool
*/
protected static function _isRelativePath(string $path): bool
{
return !preg_match('#^((?:https?|file|data):|[\/\{<])#i', $path);
}
/**
* Check if a path should be treated as relative to the path of the current template.
*
* @param string $path
* @param string $basepath
* @return string
*/
protected static function _convertRelativePath(string $path, string $basepath): string
{
// Path relative to the Rhymix installation directory?
if (preg_match('#^\^/?(\w.+)$#s', $path, $match))
{
$path = \RX_BASEURL . $match[1];
}
// Other paths will be relative to the given basepath.
else
{
$path = preg_replace('#/\./#', '/', $basepath . $path);
}
// Remove extra slashes and parent directory references.
$path = preg_replace('#\\\\#', '/', $path);
$path = preg_replace('#//#', '/', $path);
while (($tmp = preg_replace('#/[^/]+/\.\./#', '/', $path)) !== $path)
{
$path = $tmp;
}
return $path;
}
/**
* Convert variable scope.
*

View file

@ -0,0 +1,5 @@
.foo {
.bar {
width: 100%;
}
}

View file

@ -2,8 +2,8 @@
<div><?php (function($__dir, $__path, $__vars = null) { $__tpl = new \Rhymix\Framework\Template($__dir, $__path, "html"); if ($__vars) $__tpl->setVars($__vars); echo $__tpl->compile(); })("common/tpl", 'refresh.html'); ?></div>
<div><?php \Context::loadJavascriptPlugin('ckeditor'); ?></div>
<?php \Context::loadFile(['./tests/_data/template/css/style.css', 'print', '', '', []]); ?>
<div><?php $this->_v2_loadResource('^/common/js/plugins/ckeditor/'); ?></div>
<?php $this->_v2_loadResource('css/style.scss', 'print', '', []); ?>
<?php
$__Context->foo = 'FOOFOOFOO';

View file

@ -3,7 +3,7 @@
@use('Rhymix\Framework\Push', 'Push')
<div>@include('^/common/tpl/refresh.html')</div>
<div>@load('^/common/js/plugins/ckeditor/')</div>
<load src="css/style.css" media="print" />
<load src="css/style.scss" media="print" />
<?php
$foo = 'FOOFOOFOO';

View file

@ -164,104 +164,104 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$this->assertStringContainsString($target, $this->_parse($source));
}
public function testAssetLoading()
public function testResourceLoading()
{
// CSS, SCSS, LESS with media and variables
$source = '<load src="assets/hello.scss" media="print" vars="$foo" />';
$target = "<?php \Context::loadFile(['./tests/_data/template/assets/hello.scss', 'print', '', '', \$__Context->foo]); ?>";
$target = "<?php \$this->_v2_loadResource('assets/hello.scss', 'print', '', \$__Context->foo); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = '<load target="../hello.css" media="screen and (max-width: 800px)" />';
$target = "<?php \Context::loadFile(['./tests/_data/hello.css', 'screen and (max-width: 800px)', '', '', []]); ?>";
$target = "<?php \$this->_v2_loadResource('../hello.css', 'screen and (max-width: 800px)', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// JS with type and index
$source = '<load src="assets/hello.js" type="head" />';
$target = "<?php \Context::loadFile(['./tests/_data/template/assets/hello.js', 'head', '', '']); ?>";
$target = "<?php \$this->_v2_loadResource('assets/hello.js', 'head', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = '<load target="assets/../otherdir/hello.js" type="body" index="20" />';
$target = "<?php \Context::loadFile(['./tests/_data/template/otherdir/hello.js', 'body', '', 20]); ?>";
$target = "<?php \$this->_v2_loadResource('assets/../otherdir/hello.js', 'body', '20', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// External script
$source = '<load src="//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js" />';
$target = "<?php \Context::loadFile(['//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js', '', 'tests', '']); ?>";
$target = "<?php \$this->_v2_loadResource('//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js', '', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// External webfont
$source = '<load src="https://fonts.googleapis.com/css2?family=Roboto&display=swap" />';
$target = "<?php \Context::loadFile(['https://fonts.googleapis.com/css2?family=Roboto&display=swap', '', 'tests', '', []]); ?>";
$target = "<?php \$this->_v2_loadResource('https://fonts.googleapis.com/css2?family=Roboto&display=swap', '', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// Path relative to Rhymix installation directory
$source = '<load src="^/common/js/foobar.js" />';
$target = "<?php \Context::loadFile(['./common/js/foobar.js', '', '', '']); ?>";
$target = "<?php \$this->_v2_loadResource('^/common/js/foobar.js', '', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// JS plugin
$source = '<load src="^/common/js/plugins/ckeditor/" />';
$target = "<?php \Context::loadJavascriptPlugin('ckeditor'); ?>";
$target = "<?php \$this->_v2_loadResource('^/common/js/plugins/ckeditor/', '', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// Lang file
$source = '<load src="^/modules/member/lang" />';
$target = "<?php \Context::loadLang('./modules/member/lang'); ?>";
$target = "<?php \$this->_v2_loadResource('^/modules/member/lang', '', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = '<load src="^/modules/legacy_module/lang/lang.xml" />';
$target = "<?php \Context::loadLang('./modules/legacy_module/lang'); ?>";
$target = "<?php \$this->_v2_loadResource('^/modules/legacy_module/lang/lang.xml', '', '', []); ?>";
$this->assertEquals($target, $this->_parse($source));
// Blade-style SCSS with media and variables
$source = "@load('assets/hello.scss', 'print', \$vars)";
$target = "<?php \Context::loadFile(['./tests/_data/template/assets/hello.scss', 'print', '', '', \$__Context->vars]); ?>";
$target = "<?php \$this->_v2_loadResource('assets/hello.scss', 'print', \$__Context->vars); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = "@load ('../hello.css', 'screen')";
$target = "<?php \Context::loadFile(['./tests/_data/hello.css', 'screen', '', '', []]); ?>";
$target = "<?php \$this->_v2_loadResource('../hello.css', 'screen'); ?>";
$this->assertEquals($target, $this->_parse($source));
// Blade-style JS with type and index
$source = "@load('assets/hello.js', 'body', 10)";
$target = "<?php \Context::loadFile(['./tests/_data/template/assets/hello.js', 'body', '', 10]); ?>";
$target = "<?php \$this->_v2_loadResource('assets/hello.js', 'body', 10); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = "@load ('assets/hello.js', 'head')";
$target = "<?php \Context::loadFile(['./tests/_data/template/assets/hello.js', 'head', '', '']); ?>";
$target = "<?php \$this->_v2_loadResource('assets/hello.js', 'head'); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = "@load ('assets/hello.js')";
$target = "<?php \Context::loadFile(['./tests/_data/template/assets/hello.js', '', '', '']); ?>";
$target = "<?php \$this->_v2_loadResource('assets/hello.js'); ?>";
$this->assertEquals($target, $this->_parse($source));
// Blade-style external script
$source = "@load ('//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js')";
$target = "<?php \Context::loadFile(['//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js', '', 'tests', '']); ?>";
$target = "<?php \$this->_v2_loadResource('//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js'); ?>";
$this->assertEquals($target, $this->_parse($source));
// Blade-style external webfont
$source = "@load('https://fonts.googleapis.com/css2?family=Roboto&display=swap')";
$target = "<?php \Context::loadFile(['https://fonts.googleapis.com/css2?family=Roboto&display=swap', '', 'tests', '', []]); ?>";
$target = "<?php \$this->_v2_loadResource('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); ?>";
$this->assertEquals($target, $this->_parse($source));
// Blade-style path relative to Rhymix installation directory
$source = '@load ("^/common/js/foobar.js")';
$target = "<?php \Context::loadFile(['./common/js/foobar.js', '', '', '']); ?>";
$target = '<?php $this->_v2_loadResource("^/common/js/foobar.js"); ?>';
$this->assertEquals($target, $this->_parse($source));
// Blade-style JS plugin
$source = "@load('^/common/js/plugins/ckeditor/')";
$target = "<?php \Context::loadJavascriptPlugin('ckeditor'); ?>";
$target = "<?php \$this->_v2_loadResource('^/common/js/plugins/ckeditor/'); ?>";
$this->assertEquals($target, $this->_parse($source));
// Blade-style lang file
$source = "@load('^/modules/member/lang')";
$target = "<?php \Context::loadLang('./modules/member/lang'); ?>";
$target = "<?php \$this->_v2_loadResource('^/modules/member/lang'); ?>";
$this->assertEquals($target, $this->_parse($source));
$source = '@load("^/modules/legacy_module/lang/lang.xml")';
$target = "<?php \Context::loadLang('./modules/legacy_module/lang'); ?>";
$target = '<?php $this->_v2_loadResource("^/modules/legacy_module/lang/lang.xml"); ?>';
$this->assertEquals($target, $this->_parse($source));
}
@ -1014,7 +1014,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Get compiled code
$compiled_output = $tmpl->compileDirect('./tests/_data/template', 'v2example.html');
$tmpvar = preg_match('/\$__tmp_([0-9a-f]{14})/', $compiled_output, $m) ? $m[1] : '';
Rhymix\Framework\Storage::write(\RX_BASEDIR . 'tests/_data/template/v2example.compiled.html', $compiled_output);
//Rhymix\Framework\Storage::write(\RX_BASEDIR . 'tests/_data/template/v2example.compiled.html', $compiled_output);
$expected = file_get_contents(\RX_BASEDIR . 'tests/_data/template/v2example.compiled.html');
$expected = preg_replace('/RANDOM_LOOP_ID/', $tmpvar, $expected);
$this->assertEquals(
@ -1024,7 +1024,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Get final output
$executed_output = $tmpl->compile();
Rhymix\Framework\Storage::write(\RX_BASEDIR . 'tests/_data/template/v2example.executed.html', $executed_output);
//Rhymix\Framework\Storage::write(\RX_BASEDIR . 'tests/_data/template/v2example.executed.html', $executed_output);
$expected = file_get_contents(\RX_BASEDIR . 'tests/_data/template/v2example.executed.html');
$expected = preg_replace('/RANDOM_LOOP_ID/', $tmpvar, $expected);
$this->assertEquals(
@ -1042,6 +1042,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$this->_normalizeWhitespace($fragment_output)
);
// Check that resource is loaded
$list = \Context::getJsFile('body');
$this->assertStringContainsString('/rhymix/common/js/plugins/ckeditor/', array_first($list)['file']);
$list = \Context::getCssFile();
$this->assertStringContainsString('/rhymix/tests/_data/template/css/style.scss', array_first($list)['file']);
// Loop variable
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'v2loops.html');
$tmpl->disableCache();