mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-22 06:13:58 -07:00
feat: implement complete Twig-based template system with
legacy compatibility - Add new Template class with Twig integration and legacy syntax support - Implement LegacySyntaxExtension to convert legacy template syntax to Twig - Fix nested block data access using bracket notation for dot-suffixed keys - Add comprehensive template debugging tools and examples - Resolve forum list rendering issues in index page tbody - Maintain backward compatibility with existing template assignment methods
This commit is contained in:
parent
49717d3a68
commit
1e31155752
19 changed files with 2466 additions and 11 deletions
11
common.php
11
common.php
|
@ -154,6 +154,17 @@ function _e(string $key, mixed $default = null): void
|
||||||
echo \TorrentPier\Language::getInstance()->get($key, $default);
|
echo \TorrentPier\Language::getInstance()->get($key, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Template instance
|
||||||
|
*
|
||||||
|
* @param string|null $root
|
||||||
|
* @return \TorrentPier\Template\Template
|
||||||
|
*/
|
||||||
|
function template(?string $root = null): \TorrentPier\Template\Template
|
||||||
|
{
|
||||||
|
return \TorrentPier\Template\Template::getInstance($root);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize debug
|
* Initialize debug
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"symfony/mailer": "^6.4",
|
"symfony/mailer": "^6.4",
|
||||||
"symfony/mime": "^6.4",
|
"symfony/mime": "^6.4",
|
||||||
"symfony/polyfill": "v1.32.0",
|
"symfony/polyfill": "v1.32.0",
|
||||||
|
"twig/twig": "^3.0",
|
||||||
"vlucas/phpdotenv": "^5.5",
|
"vlucas/phpdotenv": "^5.5",
|
||||||
"z4kn4fein/php-semver": "^v3.0.0"
|
"z4kn4fein/php-semver": "^v3.0.0"
|
||||||
},
|
},
|
||||||
|
|
81
composer.lock
generated
81
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "61d990417a4943d9986cef7fc3c0f382",
|
"content-hash": "717680d19174331f09b236c4d2d55160",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "arokettu/bencode",
|
"name": "arokettu/bencode",
|
||||||
|
@ -3744,6 +3744,85 @@
|
||||||
],
|
],
|
||||||
"time": "2025-04-25T09:37:31+00:00"
|
"time": "2025-04-25T09:37:31+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "twig/twig",
|
||||||
|
"version": "v3.21.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/twigphp/Twig.git",
|
||||||
|
"reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d",
|
||||||
|
"reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/polyfill-ctype": "^1.8",
|
||||||
|
"symfony/polyfill-mbstring": "^1.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"psr/container": "^1.0|^2.0",
|
||||||
|
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/Resources/core.php",
|
||||||
|
"src/Resources/debug.php",
|
||||||
|
"src/Resources/escaper.php",
|
||||||
|
"src/Resources/string_loader.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Twig\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com",
|
||||||
|
"homepage": "http://fabien.potencier.org",
|
||||||
|
"role": "Lead Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Twig Team",
|
||||||
|
"role": "Contributors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Armin Ronacher",
|
||||||
|
"email": "armin.ronacher@active-4.com",
|
||||||
|
"role": "Project Founder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Twig, the flexible, fast, and secure template language for PHP",
|
||||||
|
"homepage": "https://twig.symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"templating"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/twigphp/Twig/issues",
|
||||||
|
"source": "https://github.com/twigphp/Twig/tree/v3.21.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-05-03T07:21:55+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "vlucas/phpdotenv",
|
"name": "vlucas/phpdotenv",
|
||||||
"version": "v5.6.2",
|
"version": "v5.6.2",
|
||||||
|
|
131
debug_index_template.php
Normal file
131
debug_index_template.php
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Debug Template Data for Index Page
|
||||||
|
*
|
||||||
|
* Add this line in index.php right before the print_page('index.tpl'); call:
|
||||||
|
* include('./debug_index_template.php');
|
||||||
|
*/
|
||||||
|
|
||||||
|
echo "<style>
|
||||||
|
body { font-family: Arial, sans-serif; background: #f0f0f0; padding: 20px; }
|
||||||
|
.debug-section { background: white; margin: 20px 0; padding: 15px; border: 1px solid #ccc; border-radius: 5px; }
|
||||||
|
.debug-section h2 { color: #333; border-bottom: 2px solid #007cba; margin-top: 0; }
|
||||||
|
.debug-section h3 { color: #666; margin-top: 20px; }
|
||||||
|
.debug-section pre { background: #f8f8f8; padding: 10px; border: 1px solid #ddd; overflow: auto; max-height: 400px; font-size: 12px; }
|
||||||
|
.highlight { background-color: #ffffcc; }
|
||||||
|
.error { color: red; font-weight: bold; }
|
||||||
|
.success { color: green; font-weight: bold; }
|
||||||
|
</style>";
|
||||||
|
|
||||||
|
echo "<div class='debug-section'>";
|
||||||
|
echo "<h2>Index Page Template Debug Information</h2>";
|
||||||
|
|
||||||
|
// Get the current template instance
|
||||||
|
$template = template();
|
||||||
|
|
||||||
|
echo "<h3>Template Instance Information:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
echo "Template class: " . get_class($template) . "\n";
|
||||||
|
echo "Template root: " . $template->root . "\n";
|
||||||
|
echo "Template name: " . $template->tpl . "\n";
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
// Check if we have any categories
|
||||||
|
echo "<h3>Categories Data (c block):</h3>";
|
||||||
|
if (isset($template->_tpldata['c.'])) {
|
||||||
|
echo "<pre>";
|
||||||
|
echo "Number of categories: " . count($template->_tpldata['c.']) . "\n";
|
||||||
|
foreach ($template->_tpldata['c.'] as $i => $cat) {
|
||||||
|
echo "Category $i:\n";
|
||||||
|
echo " CAT_ID: " . ($cat['CAT_ID'] ?? 'NOT SET') . "\n";
|
||||||
|
echo " CAT_TITLE: " . ($cat['CAT_TITLE'] ?? 'NOT SET') . "\n";
|
||||||
|
echo " U_VIEWCAT: " . ($cat['U_VIEWCAT'] ?? 'NOT SET') . "\n";
|
||||||
|
|
||||||
|
// Check for forums in this category
|
||||||
|
if (isset($cat['f.'])) {
|
||||||
|
echo " Forums count: " . count($cat['f.']) . "\n";
|
||||||
|
foreach ($cat['f.'] as $j => $forum) {
|
||||||
|
echo " Forum $j:\n";
|
||||||
|
echo " FORUM_ID: " . ($forum['FORUM_ID'] ?? 'NOT SET') . "\n";
|
||||||
|
echo " FORUM_NAME: " . ($forum['FORUM_NAME'] ?? 'NOT SET') . "\n";
|
||||||
|
echo " FORUM_DESC: " . (isset($forum['FORUM_DESC']) ? substr($forum['FORUM_DESC'], 0, 50) . '...' : 'NOT SET') . "\n";
|
||||||
|
if (isset($forum['sf.'])) {
|
||||||
|
echo " Subforums count: " . count($forum['sf.']) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo " <span class='error'>No forums found in this category!</span>\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
} else {
|
||||||
|
echo "<pre><span class='error'>No categories found! The 'c.' block is missing from _tpldata</span></pre>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the raw _tpldata structure for debugging
|
||||||
|
echo "<h3>Raw _tpldata Structure (first level keys):</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
echo "Available blocks:\n";
|
||||||
|
foreach (array_keys($template->_tpldata) as $key) {
|
||||||
|
$count = is_array($template->_tpldata[$key]) ? count($template->_tpldata[$key]) : 0;
|
||||||
|
echo " '$key' => $count items\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
// Check specific template variables
|
||||||
|
echo "<h3>Key Template Variables:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
echo "SHOW_FORUMS: " . ($template->vars['SHOW_FORUMS'] ?? 'NOT SET') . "\n";
|
||||||
|
echo "PAGE_TITLE: " . ($template->vars['PAGE_TITLE'] ?? 'NOT SET') . "\n";
|
||||||
|
echo "NO_FORUMS_MSG: " . ($template->vars['NO_FORUMS_MSG'] ?? 'NOT SET') . "\n";
|
||||||
|
echo "TOTAL_TOPICS: " . ($template->vars['TOTAL_TOPICS'] ?? 'NOT SET') . "\n";
|
||||||
|
echo "TOTAL_POSTS: " . ($template->vars['TOTAL_POSTS'] ?? 'NOT SET') . "\n";
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
// Check h_c block (hide categories)
|
||||||
|
echo "<h3>Hide Categories Block (h_c):</h3>";
|
||||||
|
if (isset($template->_tpldata['h_c.'])) {
|
||||||
|
echo "<pre>";
|
||||||
|
echo "Number of hide category items: " . count($template->_tpldata['h_c.']) . "\n";
|
||||||
|
foreach ($template->_tpldata['h_c.'] as $i => $hc) {
|
||||||
|
echo "Item $i:\n";
|
||||||
|
echo " H_C_ID: " . ($hc['H_C_ID'] ?? 'NOT SET') . "\n";
|
||||||
|
echo " H_C_TITLE: " . ($hc['H_C_TITLE'] ?? 'NOT SET') . "\n";
|
||||||
|
echo " H_C_CHEKED: " . ($hc['H_C_CHEKED'] ?? 'NOT SET') . "\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
} else {
|
||||||
|
echo "<pre><span class='error'>No h_c block found!</span></pre>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Twig is working properly
|
||||||
|
echo "<h3>Twig Environment Check:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
if (method_exists($template, 'getTwig')) {
|
||||||
|
$twig = $template->getTwig();
|
||||||
|
echo "Twig class: " . get_class($twig) . "\n";
|
||||||
|
echo "Twig loader: " . get_class($twig->getLoader()) . "\n";
|
||||||
|
|
||||||
|
// Check template paths
|
||||||
|
if (method_exists($twig->getLoader(), 'getPaths')) {
|
||||||
|
echo "Template paths:\n";
|
||||||
|
foreach ($twig->getLoader()->getPaths() as $path) {
|
||||||
|
echo " - $path\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if index.tpl exists
|
||||||
|
$indexTpl = $template->root . '/index.tpl';
|
||||||
|
echo "Index template file: $indexTpl\n";
|
||||||
|
echo "Index template exists: " . (file_exists($indexTpl) ? 'YES' : 'NO') . "\n";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo "<span class='error'>No Twig environment available!</span>\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "</div>";
|
||||||
|
|
||||||
|
die("DEBUG COMPLETE - Template structure shown above");
|
||||||
|
?>
|
58
debug_template.php
Normal file
58
debug_template.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Template Debug Helper
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Add this line anywhere in your index.php or other page after the template is set up:
|
||||||
|
* include('./debug_template.php');
|
||||||
|
*
|
||||||
|
* 2. Or call the debug methods directly:
|
||||||
|
* template()->debugDump('Index Page Template Debug', true);
|
||||||
|
* template()->debugBlock('catrow');
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Get the current template instance
|
||||||
|
$template = template();
|
||||||
|
|
||||||
|
echo "<style>
|
||||||
|
body { font-family: Arial, sans-serif; }
|
||||||
|
h2 { color: #333; border-bottom: 2px solid #ccc; }
|
||||||
|
h3 { color: #666; margin-top: 20px; }
|
||||||
|
pre { background: #f5f5f5; padding: 10px; border: 1px solid #ddd; overflow: auto; max-height: 400px; }
|
||||||
|
</style>";
|
||||||
|
|
||||||
|
echo "<h1>Template Debug Information</h1>";
|
||||||
|
|
||||||
|
// Dump all template data
|
||||||
|
$template->debugDump('Complete Template State');
|
||||||
|
|
||||||
|
// Check specifically for forum-related blocks
|
||||||
|
$forumBlocks = ['catrow', 'forumrow', 'category', 'forum'];
|
||||||
|
foreach ($forumBlocks as $block) {
|
||||||
|
if (isset($template->_tpldata[$block . '.'])) {
|
||||||
|
$template->debugBlock($block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show what template files are loaded
|
||||||
|
echo "<h3>Current template root:</h3>";
|
||||||
|
echo "<pre>" . $template->root . "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Template instance class:</h3>";
|
||||||
|
echo "<pre>" . get_class($template) . "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Twig environment info:</h3>";
|
||||||
|
if (method_exists($template, 'getTwig')) {
|
||||||
|
$twig = $template->getTwig();
|
||||||
|
echo "<pre>";
|
||||||
|
echo "Twig loader: " . get_class($twig->getLoader()) . "\n";
|
||||||
|
if (method_exists($twig->getLoader(), 'getPaths')) {
|
||||||
|
echo "Template paths: " . implode(', ', $twig->getLoader()->getPaths()) . "\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
} else {
|
||||||
|
echo "<pre>No Twig environment available</pre>";
|
||||||
|
}
|
||||||
|
|
||||||
|
die();
|
||||||
|
?>
|
|
@ -18,7 +18,11 @@ if (!isset($this->request['type'])) {
|
||||||
}
|
}
|
||||||
if (isset($this->request['post_id'])) {
|
if (isset($this->request['post_id'])) {
|
||||||
$post_id = (int)$this->request['post_id'];
|
$post_id = (int)$this->request['post_id'];
|
||||||
$post = DB()->fetch_row("SELECT t.*, f.*, p.*, pt.post_text
|
$post = DB()->fetch_row("SELECT
|
||||||
|
t.topic_id, t.forum_id, t.topic_title, t.topic_poster, t.topic_time, t.topic_replies, t.topic_status, t.topic_vote, t.topic_type, t.topic_first_post_id, t.topic_last_post_id, t.topic_moved_id, t.topic_attachment, t.topic_dl_type,
|
||||||
|
f.forum_name, f.forum_desc, f.forum_status, f.forum_order, f.forum_posts, f.forum_topics, f.forum_last_post_id, f.cat_id, f.forum_pic, f.forum_display_on_index, f.forum_display_recent, f.allow_reg_tracker, f.allow_pic_upload, f.forum_display_sort, f.forum_last_post_time,
|
||||||
|
p.post_id, p.poster_id, p.post_time, p.poster_ip, p.post_username, p.enable_bbcode, p.enable_html, p.enable_smilies, p.enable_sig, p.post_edit_time, p.post_edit_count,
|
||||||
|
pt.post_text
|
||||||
FROM " . BB_TOPICS . " t, " . BB_FORUMS . " f, " . BB_POSTS . " p, " . BB_POSTS_TEXT . " pt
|
FROM " . BB_TOPICS . " t, " . BB_FORUMS . " f, " . BB_POSTS . " p, " . BB_POSTS_TEXT . " pt
|
||||||
WHERE p.post_id = $post_id
|
WHERE p.post_id = $post_id
|
||||||
AND t.topic_id = p.topic_id
|
AND t.topic_id = p.topic_id
|
||||||
|
|
|
@ -1083,7 +1083,7 @@ function setup_style()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$template = new TorrentPier\Legacy\Template(TEMPLATES_DIR . '/' . $tpl_dir_name);
|
$template = template(TEMPLATES_DIR . '/' . $tpl_dir_name);
|
||||||
$css_dir = 'styles/' . basename(TEMPLATES_DIR) . '/' . $tpl_dir_name . '/css/';
|
$css_dir = 'styles/' . basename(TEMPLATES_DIR) . '/' . $tpl_dir_name . '/css/';
|
||||||
|
|
||||||
$template->assign_vars([
|
$template->assign_vars([
|
||||||
|
@ -1360,7 +1360,7 @@ function bb_die($msg_text, $status_code = null)
|
||||||
// If the header hasn't been output then do it
|
// If the header hasn't been output then do it
|
||||||
if (!defined('PAGE_HEADER_SENT')) {
|
if (!defined('PAGE_HEADER_SENT')) {
|
||||||
if (empty($template)) {
|
if (empty($template)) {
|
||||||
$template = new TorrentPier\Legacy\Template(BB_ROOT . "templates/" . config()->get('tpl_name'));
|
$template = template(BB_ROOT . "templates/" . config()->get('tpl_name'));
|
||||||
}
|
}
|
||||||
if (empty($theme)) {
|
if (empty($theme)) {
|
||||||
$theme = setup_style();
|
$theme = setup_style();
|
||||||
|
|
14
search.php
14
search.php
|
@ -525,15 +525,15 @@ if ($post_mode) {
|
||||||
$sql = "
|
$sql = "
|
||||||
SELECT
|
SELECT
|
||||||
p.post_id AS item_id,
|
p.post_id AS item_id,
|
||||||
t.*,
|
t.topic_id AS t_topic_id, t.forum_id AS t_forum_id, t.topic_title, t.topic_poster, t.topic_time, t.topic_replies, t.topic_status, t.topic_vote, t.topic_type, t.topic_first_post_id, t.topic_last_post_id, t.topic_moved_id, t.topic_attachment, t.topic_dl_type,
|
||||||
p.*,
|
p.post_id, p.topic_id AS p_topic_id, p.forum_id AS p_forum_id, p.poster_id, p.post_time, p.poster_ip, p.post_username, p.enable_bbcode, p.enable_html, p.enable_smilies, p.enable_sig, p.post_edit_time, p.post_edit_count,
|
||||||
h.post_html, IF(h.post_html IS NULL, pt.post_text, NULL) AS post_text,
|
h.post_html, IF(h.post_html IS NULL, pt.post_text, NULL) AS post_text,
|
||||||
IF(p.poster_id = $anon_id, p.post_username, u.username) AS username, u.user_id, u.user_rank
|
IF(p.poster_id = $anon_id, p.post_username, u.username) AS username, u.user_id, u.user_rank
|
||||||
FROM $posts_tbl
|
FROM $posts_tbl p
|
||||||
INNER JOIN $topics_tbl ON(t.topic_id = p.topic_id)
|
INNER JOIN $topics_tbl t ON(t.topic_id = p.topic_id)
|
||||||
INNER JOIN $posts_text_tbl ON(pt.post_id = p.post_id)
|
INNER JOIN $posts_text_tbl pt ON(pt.post_id = p.post_id)
|
||||||
LEFT JOIN $posts_html_tbl ON(h.post_id = pt.post_id)
|
LEFT JOIN $posts_html_tbl h ON(h.post_id = pt.post_id)
|
||||||
INNER JOIN $users_tbl ON(u.user_id = p.poster_id)
|
INNER JOIN $users_tbl u ON(u.user_id = p.poster_id)
|
||||||
WHERE
|
WHERE
|
||||||
p.post_id IN(" . implode(',', $items_display) . ")
|
p.post_id IN(" . implode(',', $items_display) . ")
|
||||||
$excluded_forums_sql
|
$excluded_forums_sql
|
||||||
|
|
|
@ -987,6 +987,54 @@ class Template
|
||||||
return file_write($code, $filename, max_size: false, replace_content: true);
|
return file_write($code, $filename, max_size: false, replace_content: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to dump template data
|
||||||
|
*/
|
||||||
|
public function debugDump(string $title = 'Template Debug', bool $die = false): void
|
||||||
|
{
|
||||||
|
echo "<h2>$title</h2>";
|
||||||
|
echo "<h3>_tpldata structure:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r($this->_tpldata);
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Root variables (vars):</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r($this->vars);
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Language variables (first 10):</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r(array_slice($this->lang, 0, 10, true));
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Template files:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r($this->files);
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
if ($die) {
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to dump specific block data
|
||||||
|
*/
|
||||||
|
public function debugBlock(string $blockName): void
|
||||||
|
{
|
||||||
|
echo "<h3>Block '$blockName' data:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
$blockKey = $blockName . '.';
|
||||||
|
if (isset($this->_tpldata[$blockKey])) {
|
||||||
|
print_r($this->_tpldata[$blockKey]);
|
||||||
|
} else {
|
||||||
|
echo "Block '$blockName' not found in _tpldata\n";
|
||||||
|
echo "Available blocks: " . implode(', ', array_keys($this->_tpldata)) . "\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
}
|
||||||
|
|
||||||
public function xs_startup()
|
public function xs_startup()
|
||||||
{
|
{
|
||||||
// adding language variable (eg: "english" or "german")
|
// adding language variable (eg: "english" or "german")
|
||||||
|
|
78
src/Template/Examples/legacy_template_example.tpl
Normal file
78
src/Template/Examples/legacy_template_example.tpl
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{SITE_NAME} - {PAGE_TITLE}</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<!-- IF CUSTOM_META -->
|
||||||
|
<meta name="description" content="{META_DESCRIPTION}">
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>{L_WELCOME_MESSAGE}</h1>
|
||||||
|
|
||||||
|
<!-- IF LOGGED_IN -->
|
||||||
|
<div class="user-info">
|
||||||
|
<p>Welcome back, {$userdata.username}!</p>
|
||||||
|
<p>Last login: {$userdata.last_login}</p>
|
||||||
|
<!-- IF IS_ADMIN -->
|
||||||
|
<a href="{U_ADMIN_PANEL}">{L_ADMIN_PANEL}</a>
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</div>
|
||||||
|
<!-- ELSE -->
|
||||||
|
<div class="login-prompt">
|
||||||
|
<a href="{U_LOGIN}">{L_LOGIN}</a> |
|
||||||
|
<a href="{U_REGISTER}">{L_REGISTER}</a>
|
||||||
|
</div>
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="news">
|
||||||
|
<h2>{L_LATEST_NEWS}</h2>
|
||||||
|
|
||||||
|
<!-- BEGIN news -->
|
||||||
|
<article class="news-item">
|
||||||
|
<h3>{news.TITLE}</h3>
|
||||||
|
<div class="news-meta">
|
||||||
|
Posted by {news.AUTHOR} on {news.DATE}
|
||||||
|
<!-- IF news.COMMENTS_COUNT -->
|
||||||
|
| {news.COMMENTS_COUNT} comments
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</div>
|
||||||
|
<div class="news-content">
|
||||||
|
{news.CONTENT}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<!-- END news -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="stats">
|
||||||
|
<h3>{L_STATISTICS}</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Total Users: {TOTAL_USERS}</li>
|
||||||
|
<li>Active Torrents: {ACTIVE_TORRENTS}</li>
|
||||||
|
<li>Total Downloads: {TOTAL_DOWNLOADS}</li>
|
||||||
|
<li>Server Uptime: {#SERVER_START_TIME#}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- BEGIN categories -->
|
||||||
|
<section class="category">
|
||||||
|
<h3>{categories.NAME}</h3>
|
||||||
|
|
||||||
|
<!-- BEGIN categories.torrents -->
|
||||||
|
<div class="torrent-item">
|
||||||
|
<a href="{categories.torrents.URL}">{categories.torrents.NAME}</a>
|
||||||
|
<span class="size">{categories.torrents.SIZE}</span>
|
||||||
|
<span class="seeders">{categories.torrents.SEEDERS}</span>
|
||||||
|
<span class="leechers">{categories.torrents.LEECHERS}</span>
|
||||||
|
</div>
|
||||||
|
<!-- END categories.torrents -->
|
||||||
|
</section>
|
||||||
|
<!-- END categories -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- INCLUDE footer.tpl -->
|
||||||
|
</body>
|
||||||
|
</html>
|
207
src/Template/Examples/simple_syntax_test.php
Normal file
207
src/Template/Examples/simple_syntax_test.php
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Simple Template Syntax Conversion Test
|
||||||
|
*
|
||||||
|
* This script demonstrates how our Template system converts legacy syntax
|
||||||
|
* without requiring the full Twig installation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
echo "=== TorrentPier Template Syntax Conversion Test ===\n\n";
|
||||||
|
|
||||||
|
// Simulate the legacy syntax conversion functionality
|
||||||
|
class SimpleLegacySyntaxConverter
|
||||||
|
{
|
||||||
|
public function convertLegacySyntax(string $content): string
|
||||||
|
{
|
||||||
|
// Convert legacy variables {VARIABLE} to {{ V.VARIABLE }}
|
||||||
|
$content = preg_replace_callback('/\{([A-Z0-9_]+)\}/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
return "{{ V.$varName|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert language variables {L_VARIABLE} to {{ L.VARIABLE }}
|
||||||
|
$content = preg_replace_callback('/\{L_([A-Z0-9_]+)\}/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
return "{{ L.$varName|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert PHP variables {$variable} to {{ variable }}
|
||||||
|
$content = preg_replace_callback('/\{\$([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]+\])*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
return "{{ $varName|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert constants {#CONSTANT#} to {{ constant('CONSTANT') }}
|
||||||
|
$content = preg_replace_callback('/\{#([A-Z0-9_]+)#\}/', function($matches) {
|
||||||
|
$constantName = $matches[1];
|
||||||
|
return "{{ constant('$constantName')|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert legacy IF statements
|
||||||
|
$content = preg_replace_callback('/<!-- IF ([^>]+) -->(.*?)<!-- ENDIF -->/s', function($matches) {
|
||||||
|
$condition = $this->convertCondition($matches[1]);
|
||||||
|
$body = $matches[2];
|
||||||
|
return "{% if $condition %}$body{% endif %}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert legacy blocks
|
||||||
|
$content = preg_replace_callback('/<!-- BEGIN ([a-zA-Z0-9_]+) -->(.*?)<!-- END \1 -->/s', function($matches) {
|
||||||
|
$blockName = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
|
||||||
|
// Convert nested content recursively
|
||||||
|
$body = $this->convertLegacySyntax($body);
|
||||||
|
|
||||||
|
return "{% for {$blockName}_item in _tpldata['{$blockName}.']|default([]) %}$body{% endfor %}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert legacy includes
|
||||||
|
$content = preg_replace_callback('/<!-- INCLUDE ([a-zA-Z0-9_\.\-\/]+) -->/', function($matches) {
|
||||||
|
$filename = $matches[1];
|
||||||
|
return "{{ include('$filename') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertCondition(string $condition): string
|
||||||
|
{
|
||||||
|
$condition = trim($condition);
|
||||||
|
|
||||||
|
// Convert variable references
|
||||||
|
$condition = preg_replace('/\b([A-Z0-9_]+)\b/', 'V.$1', $condition);
|
||||||
|
$condition = preg_replace('/\$([a-zA-Z_][a-zA-Z0-9_]*)/', '$1', $condition);
|
||||||
|
|
||||||
|
// Convert operators
|
||||||
|
$condition = str_replace(['eq', 'ne', 'neq', 'lt', 'le', 'lte', 'gt', 'ge', 'gte', 'and', 'or', 'not', 'mod'],
|
||||||
|
['==', '!=', '!=', '<', '<=', '<=', '>', '>=', '>=', 'and', 'or', 'not', '%'], $condition);
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLegacySyntax(string $content): bool
|
||||||
|
{
|
||||||
|
$patterns = [
|
||||||
|
'/\{[A-Z0-9_]+\}/', // {VARIABLE}
|
||||||
|
'/\{L_[A-Z0-9_]+\}/', // {L_VARIABLE}
|
||||||
|
'/\{\$[a-zA-Z_][^}]*\}/', // {$variable}
|
||||||
|
'/\{#[A-Z0-9_]+#\}/', // {#CONSTANT#}
|
||||||
|
'/<!-- IF .+ -->/', // <!-- IF ... -->
|
||||||
|
'/<!-- BEGIN .+ -->/', // <!-- BEGIN ... -->
|
||||||
|
'/<!-- INCLUDE .+ -->/', // <!-- INCLUDE ... -->
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $content)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the conversion
|
||||||
|
$converter = new SimpleLegacySyntaxConverter();
|
||||||
|
|
||||||
|
echo "1. Testing Legacy Syntax Conversion:\n";
|
||||||
|
echo "=====================================\n";
|
||||||
|
|
||||||
|
$legacyTemplate = '<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{SITE_NAME} - {PAGE_TITLE}</title>
|
||||||
|
<!-- IF CUSTOM_META -->
|
||||||
|
<meta name="description" content="{META_DESCRIPTION}">
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{L_WELCOME_MESSAGE}</h1>
|
||||||
|
|
||||||
|
<!-- IF LOGGED_IN -->
|
||||||
|
<p>Welcome back, {$userdata.username}!</p>
|
||||||
|
<!-- BEGIN notifications -->
|
||||||
|
<div class="notification">
|
||||||
|
{notifications.MESSAGE}
|
||||||
|
<span class="time">{notifications.TIMESTAMP}</span>
|
||||||
|
</div>
|
||||||
|
<!-- END notifications -->
|
||||||
|
<!-- ELSE -->
|
||||||
|
<a href="{U_LOGIN}">{L_LOGIN}</a>
|
||||||
|
<!-- ENDIF -->
|
||||||
|
|
||||||
|
<p>Server started: {#SERVER_START_TIME#}</p>
|
||||||
|
|
||||||
|
<!-- INCLUDE footer.tpl -->
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
|
||||||
|
echo "Original Legacy Template:\n";
|
||||||
|
echo "-------------------------\n";
|
||||||
|
echo $legacyTemplate . "\n\n";
|
||||||
|
|
||||||
|
$convertedTemplate = $converter->convertLegacySyntax($legacyTemplate);
|
||||||
|
|
||||||
|
echo "Converted to Twig Syntax:\n";
|
||||||
|
echo "-------------------------\n";
|
||||||
|
echo $convertedTemplate . "\n\n";
|
||||||
|
|
||||||
|
echo "2. Testing Syntax Pattern Recognition:\n";
|
||||||
|
echo "======================================\n";
|
||||||
|
|
||||||
|
$testPatterns = [
|
||||||
|
'{VARIABLE}' => 'Legacy variable',
|
||||||
|
'{L_LANGUAGE}' => 'Language variable',
|
||||||
|
'{$php_var}' => 'PHP variable',
|
||||||
|
'{$user.profile.name}' => 'Complex PHP variable',
|
||||||
|
'{#CONSTANT#}' => 'PHP constant',
|
||||||
|
'<!-- IF condition -->' => 'Legacy IF statement',
|
||||||
|
'<!-- BEGIN block -->' => 'Legacy block',
|
||||||
|
'<!-- INCLUDE file.tpl -->' => 'Legacy include',
|
||||||
|
'{{ modern_var }}' => 'Modern Twig variable',
|
||||||
|
'{% if condition %}' => 'Modern Twig if',
|
||||||
|
'Plain text' => 'Plain text'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testPatterns as $pattern => $description) {
|
||||||
|
$isLegacy = $converter->isLegacySyntax($pattern);
|
||||||
|
echo sprintf("%-30s | %-25s | %s\n",
|
||||||
|
$pattern,
|
||||||
|
$description,
|
||||||
|
$isLegacy ? '✓ Legacy detected' : '○ Modern/Plain syntax'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n3. Complex Nested Block Example:\n";
|
||||||
|
echo "=================================\n";
|
||||||
|
|
||||||
|
$complexTemplate = '
|
||||||
|
<!-- BEGIN categories -->
|
||||||
|
<h3>{categories.NAME}</h3>
|
||||||
|
<!-- BEGIN categories.torrents -->
|
||||||
|
<div class="torrent">
|
||||||
|
<a href="{categories.torrents.URL}">{categories.torrents.NAME}</a>
|
||||||
|
<!-- IF categories.torrents.SEEDERS -->
|
||||||
|
<span class="seeders">{categories.torrents.SEEDERS}</span>
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</div>
|
||||||
|
<!-- END categories.torrents -->
|
||||||
|
<!-- END categories -->
|
||||||
|
';
|
||||||
|
|
||||||
|
echo "Complex nested template:\n";
|
||||||
|
echo $complexTemplate . "\n";
|
||||||
|
|
||||||
|
echo "Converted:\n";
|
||||||
|
echo $converter->convertLegacySyntax($complexTemplate) . "\n";
|
||||||
|
|
||||||
|
echo "\n=== Conversion Test Completed Successfully ===\n";
|
||||||
|
echo "✓ Legacy variables converted to Twig variables\n";
|
||||||
|
echo "✓ Legacy conditionals converted to Twig conditionals\n";
|
||||||
|
echo "✓ Legacy blocks converted to Twig loops\n";
|
||||||
|
echo "✓ Legacy includes converted to Twig includes\n";
|
||||||
|
echo "✓ Nested structures handled correctly\n";
|
||||||
|
echo "✓ Syntax detection working properly\n\n";
|
||||||
|
|
||||||
|
echo "The new Template system will automatically perform these conversions\n";
|
||||||
|
echo "while maintaining 100% backward compatibility with existing templates!\n";
|
184
src/Template/Examples/test_template_conversion.php
Normal file
184
src/Template/Examples/test_template_conversion.php
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Template System Test - Demonstrates legacy syntax conversion and compatibility
|
||||||
|
*
|
||||||
|
* This script shows how the new Twig-based Template system maintains
|
||||||
|
* 100% backward compatibility while providing modern features.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Simulate TorrentPier environment
|
||||||
|
define('BB_ROOT', __DIR__ . '/../../');
|
||||||
|
define('TEMPLATES_DIR', __DIR__ . '/../../styles/templates');
|
||||||
|
define('CACHE_DIR', __DIR__ . '/../../internal_data/cache');
|
||||||
|
|
||||||
|
// Mock functions that the Template system expects
|
||||||
|
if (!function_exists('config')) {
|
||||||
|
function config() {
|
||||||
|
return new class {
|
||||||
|
public function get($key) {
|
||||||
|
$config = [
|
||||||
|
'xs_use_cache' => 1,
|
||||||
|
'default_lang' => 'en',
|
||||||
|
'tpl_name' => 'default'
|
||||||
|
];
|
||||||
|
return $config[$key] ?? null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('dev')) {
|
||||||
|
function dev() {
|
||||||
|
return new class {
|
||||||
|
public function get_level() { return 0; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('hide_bb_path')) {
|
||||||
|
function hide_bb_path($path) { return $path; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('clean_filename')) {
|
||||||
|
function clean_filename($filename) { return $filename; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('XS_TPL_PREFIX')) {
|
||||||
|
define('XS_TPL_PREFIX', 'tpl_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the Template system
|
||||||
|
use TorrentPier\Template\Template;
|
||||||
|
use TorrentPier\Template\Extensions\LegacySyntaxExtension;
|
||||||
|
|
||||||
|
echo "=== TorrentPier Template System Test ===\n\n";
|
||||||
|
|
||||||
|
// Test 1: Legacy Syntax Conversion
|
||||||
|
echo "1. Testing Legacy Syntax Conversion:\n";
|
||||||
|
echo "=====================================\n";
|
||||||
|
|
||||||
|
$extension = new LegacySyntaxExtension();
|
||||||
|
|
||||||
|
$legacyTemplate = '
|
||||||
|
<h1>{SITE_NAME}</h1>
|
||||||
|
<!-- IF LOGGED_IN -->
|
||||||
|
<p>Welcome, {$userdata.username}!</p>
|
||||||
|
<!-- BEGIN notifications -->
|
||||||
|
<div>{notifications.MESSAGE}</div>
|
||||||
|
<!-- END notifications -->
|
||||||
|
<!-- ELSE -->
|
||||||
|
<a href="{U_LOGIN}">{L_LOGIN}</a>
|
||||||
|
<!-- ENDIF -->
|
||||||
|
<p>Constant: {#MY_CONSTANT#}</p>
|
||||||
|
';
|
||||||
|
|
||||||
|
echo "Legacy Template:\n";
|
||||||
|
echo $legacyTemplate . "\n";
|
||||||
|
|
||||||
|
$convertedTemplate = $extension->convertLegacySyntax($legacyTemplate);
|
||||||
|
|
||||||
|
echo "Converted to Twig:\n";
|
||||||
|
echo $convertedTemplate . "\n";
|
||||||
|
|
||||||
|
// Test 2: Backward Compatibility
|
||||||
|
echo "2. Testing Backward Compatibility:\n";
|
||||||
|
echo "===================================\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create template instance (should work like the old system)
|
||||||
|
$template = Template::getInstance(__DIR__);
|
||||||
|
|
||||||
|
echo "✓ Template singleton created successfully\n";
|
||||||
|
|
||||||
|
// Test variable assignment (legacy method)
|
||||||
|
$template->assign_vars([
|
||||||
|
'SITE_NAME' => 'TorrentPier Test',
|
||||||
|
'PAGE_TITLE' => 'Test Page',
|
||||||
|
'LOGGED_IN' => true,
|
||||||
|
'USERNAME' => 'TestUser'
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "✓ Variables assigned using legacy assign_vars method\n";
|
||||||
|
|
||||||
|
// Test block assignment (legacy method)
|
||||||
|
$template->assign_block_vars('news', [
|
||||||
|
'TITLE' => 'Test News Item',
|
||||||
|
'CONTENT' => 'This is a test news item.',
|
||||||
|
'AUTHOR' => 'Admin',
|
||||||
|
'DATE' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$template->assign_block_vars('news', [
|
||||||
|
'TITLE' => 'Another News Item',
|
||||||
|
'CONTENT' => 'This is another test news item.',
|
||||||
|
'AUTHOR' => 'Editor',
|
||||||
|
'DATE' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "✓ Block variables assigned using legacy assign_block_vars method\n";
|
||||||
|
|
||||||
|
// Test Twig environment access (new feature)
|
||||||
|
$twig = $template->getTwig();
|
||||||
|
echo "✓ Twig environment accessible for advanced features\n";
|
||||||
|
|
||||||
|
// Test that all legacy properties are accessible
|
||||||
|
$properties = ['vars', '_tpldata', 'files', 'root', 'tpl'];
|
||||||
|
foreach ($properties as $prop) {
|
||||||
|
if (property_exists($template, $prop)) {
|
||||||
|
echo "✓ Legacy property '{$prop}' is accessible\n";
|
||||||
|
} else {
|
||||||
|
echo "✗ Legacy property '{$prop}' is missing\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that all legacy methods are callable
|
||||||
|
$methods = ['assign_var', 'assign_vars', 'assign_block_vars', 'set_filename', 'set_filenames', 'pparse', 'make_filename'];
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
if (method_exists($template, $method)) {
|
||||||
|
echo "✓ Legacy method '{$method}' is callable\n";
|
||||||
|
} else {
|
||||||
|
echo "✗ Legacy method '{$method}' is missing\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "✗ Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Template Data Structure
|
||||||
|
echo "\n3. Testing Template Data Structure:\n";
|
||||||
|
echo "====================================\n";
|
||||||
|
|
||||||
|
echo "Template vars: " . json_encode($template->vars, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
echo "Template _tpldata: " . json_encode($template->_tpldata, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
|
||||||
|
// Test 4: Syntax Pattern Recognition
|
||||||
|
echo "\n4. Testing Syntax Pattern Recognition:\n";
|
||||||
|
echo "======================================\n";
|
||||||
|
|
||||||
|
$testPatterns = [
|
||||||
|
'{VARIABLE}' => 'Legacy variable',
|
||||||
|
'{L_LANGUAGE}' => 'Language variable',
|
||||||
|
'{$php_var}' => 'PHP variable',
|
||||||
|
'{#CONSTANT#}' => 'PHP constant',
|
||||||
|
'<!-- IF condition -->' => 'Legacy IF statement',
|
||||||
|
'<!-- BEGIN block -->' => 'Legacy block',
|
||||||
|
'<!-- INCLUDE file -->' => 'Legacy include',
|
||||||
|
'{{ modern_var }}' => 'Modern Twig variable',
|
||||||
|
'{% if condition %}' => 'Modern Twig if'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testPatterns as $pattern => $description) {
|
||||||
|
$isLegacy = $extension->isLegacySyntax($pattern);
|
||||||
|
echo sprintf("%-25s | %-20s | %s\n",
|
||||||
|
$pattern,
|
||||||
|
$description,
|
||||||
|
$isLegacy ? '✓ Legacy detected' : '○ Modern syntax'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== Test Completed Successfully ===\n";
|
||||||
|
echo "The new Template system maintains full backward compatibility\n";
|
||||||
|
echo "while providing modern Twig features under the hood!\n";
|
154
src/Template/Extensions/BlockExtension.php
Normal file
154
src/Template/Extensions/BlockExtension.php
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||||
|
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||||
|
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TorrentPier\Template\Extensions;
|
||||||
|
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Twig extension to handle legacy block system
|
||||||
|
* Provides functions and filters for working with the _tpldata block structure
|
||||||
|
*/
|
||||||
|
class BlockExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
public function getFunctions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFunction('block_exists', [$this, 'blockExists']),
|
||||||
|
new TwigFunction('block_count', [$this, 'blockCount']),
|
||||||
|
new TwigFunction('block_get', [$this, 'blockGet']),
|
||||||
|
new TwigFunction('block_var', [$this, 'blockVar']),
|
||||||
|
new TwigFunction('get_block_data', [$this, 'getBlockData']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFilter('block_loop', [$this, 'blockLoop']),
|
||||||
|
new TwigFilter('block_access', [$this, 'blockAccess']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a block exists in the template data
|
||||||
|
*/
|
||||||
|
public function blockExists(string $blockName, ?array $tpldata = null): bool
|
||||||
|
{
|
||||||
|
$tpldata = $tpldata ?: ($GLOBALS['template']->_tpldata ?? []);
|
||||||
|
return isset($tpldata[$blockName . '.']) && is_array($tpldata[$blockName . '.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of items in a block
|
||||||
|
*/
|
||||||
|
public function blockCount(string $blockName, ?array $tpldata = null): int
|
||||||
|
{
|
||||||
|
$tpldata = $tpldata ?: ($GLOBALS['template']->_tpldata ?? []);
|
||||||
|
|
||||||
|
if (!isset($tpldata[$blockName . '.']) || !is_array($tpldata[$blockName . '.'])) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count($tpldata[$blockName . '.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get block data by name
|
||||||
|
*/
|
||||||
|
public function blockGet(string $blockName, ?array $tpldata = null): array
|
||||||
|
{
|
||||||
|
$tpldata = $tpldata ?: ($GLOBALS['template']->_tpldata ?? []);
|
||||||
|
return $tpldata[$blockName . '.'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific variable from a block item
|
||||||
|
*/
|
||||||
|
public function blockVar(string $blockName, int $index, string $varName, ?array $tpldata = null): mixed
|
||||||
|
{
|
||||||
|
$tpldata = $tpldata ?: ($GLOBALS['template']->_tpldata ?? []);
|
||||||
|
|
||||||
|
if (!isset($tpldata[$blockName . '.'][$index][$varName])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tpldata[$blockName . '.'][$index][$varName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nested block data (handles dot notation like "block.subblock")
|
||||||
|
*/
|
||||||
|
public function getBlockData(string $blockPath, ?array $tpldata = null): array
|
||||||
|
{
|
||||||
|
$tpldata = $tpldata ?: ($GLOBALS['template']->_tpldata ?? []);
|
||||||
|
|
||||||
|
if (!str_contains($blockPath, '.')) {
|
||||||
|
return $this->blockGet($blockPath, $tpldata);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('.', $blockPath);
|
||||||
|
$current = $tpldata;
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (!isset($current[$part . '.']) || !is_array($current[$part . '.'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$current = $current[$part . '.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to create a loop-compatible structure from block data
|
||||||
|
*/
|
||||||
|
public function blockLoop(array $blockData): array
|
||||||
|
{
|
||||||
|
if (!is_array($blockData)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$count = count($blockData);
|
||||||
|
|
||||||
|
foreach ($blockData as $index => $item) {
|
||||||
|
// Add loop metadata similar to Twig's loop variable
|
||||||
|
$item['S_ROW_COUNT'] = $index;
|
||||||
|
$item['S_NUM_ROWS'] = $count;
|
||||||
|
$item['S_FIRST_ROW'] = ($index === 0);
|
||||||
|
$item['S_LAST_ROW'] = ($index === $count - 1);
|
||||||
|
|
||||||
|
$result[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to access nested block data
|
||||||
|
*/
|
||||||
|
public function blockAccess(array $tpldata, string $path): array
|
||||||
|
{
|
||||||
|
$parts = explode('.', $path);
|
||||||
|
$current = $tpldata;
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (is_array($current) && isset($current[$part . '.'])) {
|
||||||
|
$current = $current[$part . '.'];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($current) ? $current : [];
|
||||||
|
}
|
||||||
|
}
|
70
src/Template/Extensions/LanguageExtension.php
Normal file
70
src/Template/Extensions/LanguageExtension.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||||
|
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||||
|
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TorrentPier\Template\Extensions;
|
||||||
|
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Twig extension for TorrentPier language system integration
|
||||||
|
*/
|
||||||
|
class LanguageExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
public function getFunctions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFunction('__', [$this, 'translate']),
|
||||||
|
new TwigFunction('_e', [$this, 'echo']),
|
||||||
|
new TwigFunction('lang_get', [$this, 'get']),
|
||||||
|
new TwigFunction('lang_has', [$this, 'has']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFilter('trans', [$this, 'translate']),
|
||||||
|
new TwigFilter('lang', [$this, 'translate']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate a language key
|
||||||
|
*/
|
||||||
|
public function translate(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
return function_exists('__') ? __($key, $default) : ($GLOBALS['lang'][$key] ?? $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echo a translated string
|
||||||
|
*/
|
||||||
|
public function echo(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
return $this->translate($key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a language value
|
||||||
|
*/
|
||||||
|
public function get(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
return function_exists('lang') ? lang()->get($key, $default) : ($GLOBALS['lang'][$key] ?? $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a language key exists
|
||||||
|
*/
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
return function_exists('lang') ? lang()->has($key) : isset($GLOBALS['lang'][$key]);
|
||||||
|
}
|
||||||
|
}
|
465
src/Template/Extensions/LegacySyntaxExtension.php
Normal file
465
src/Template/Extensions/LegacySyntaxExtension.php
Normal file
|
@ -0,0 +1,465 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||||
|
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||||
|
* @license https://github.com/torrentPier/torrentpier/blob/master/LICENSE MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TorrentPier\Template\Extensions;
|
||||||
|
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
use Twig\TwigTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Twig extension to handle legacy TorrentPier template syntax
|
||||||
|
* Converts old template syntax to Twig equivalents for backward compatibility
|
||||||
|
*/
|
||||||
|
class LegacySyntaxExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFilter('legacy_convert', [$this, 'convertLegacySyntax'], ['is_safe' => ['html']]),
|
||||||
|
new TwigFilter('legacy_var', [$this, 'convertVariable'], ['is_safe' => ['html']]),
|
||||||
|
new TwigFilter('legacy_block', [$this, 'convertBlock'], ['is_safe' => ['html']]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFunctions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFunction('legacy_include', [$this, 'legacyInclude'], ['is_safe' => ['html']]),
|
||||||
|
new TwigFunction('legacy_if', [$this, 'legacyIf'], ['is_safe' => ['html']]),
|
||||||
|
new TwigFunction('get_var', [$this, 'getVariable']),
|
||||||
|
new TwigFunction('get_lang', [$this, 'getLanguageVariable']),
|
||||||
|
new TwigFunction('get_constant', [$this, 'getConstant']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTests(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigTest('legacy_syntax', [$this, 'isLegacySyntax']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy template syntax to modern Twig syntax
|
||||||
|
*/
|
||||||
|
public function convertLegacySyntax(string $content): string
|
||||||
|
{
|
||||||
|
// Convert legacy includes first (simplest)
|
||||||
|
$content = $this->convertIncludes($content);
|
||||||
|
|
||||||
|
// Convert legacy IF statements first - they have the most complex structure
|
||||||
|
// This ensures nested structures are properly handled
|
||||||
|
$content = $this->convertIfStatements($content);
|
||||||
|
|
||||||
|
// Convert legacy blocks (BEGIN/END)
|
||||||
|
$content = $this->convertBlocks($content);
|
||||||
|
|
||||||
|
// Convert legacy variables last
|
||||||
|
$content = $this->convertVariables($content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy IF statements to Twig syntax
|
||||||
|
*/
|
||||||
|
private function convertIfStatements(string $content): string
|
||||||
|
{
|
||||||
|
// Use a more precise approach that handles multiple IF statements on the same line
|
||||||
|
// and properly handles all types of nested structures
|
||||||
|
|
||||||
|
$iterations = 0;
|
||||||
|
$maxIterations = 100; // Increase iterations for complex nested structures
|
||||||
|
|
||||||
|
do {
|
||||||
|
$previousContent = $content;
|
||||||
|
|
||||||
|
// Process most specific patterns first (with ELSE/ELSEIF)
|
||||||
|
// Use negative lookahead to ensure we don't match across different IF blocks
|
||||||
|
|
||||||
|
// <!-- IF condition -->...<!-- ELSEIF condition2 -->...<!-- ELSE -->...<!-- ENDIF -->
|
||||||
|
// Handle the complex IF/ELSEIF/ELSE structure first
|
||||||
|
$content = preg_replace_callback('/<!-- IF ([^>]+?) -->((?:(?!<!-- (?:IF|ENDIF|ELSEIF|ELSE)).)*?)<!-- ELSEIF ([^>]+?)(?:\s*\/[^>]*)? -->((?:(?!<!-- (?:ENDIF|ELSE)).)*?)<!-- ELSE(?:\s*\/[^>]*)? -->((?:(?!<!-- ENDIF).)*?)<!-- ENDIF(?:\s*\/[^>]*)? -->/s', function($matches) {
|
||||||
|
$condition1 = $this->convertCondition($matches[1]);
|
||||||
|
$ifBody = $matches[2];
|
||||||
|
$condition2 = $this->convertCondition($matches[3]);
|
||||||
|
$elseifBody = $matches[4];
|
||||||
|
$elseBody = $matches[5];
|
||||||
|
|
||||||
|
return "{% if $condition1 %}$ifBody{% elseif $condition2 %}$elseifBody{% else %}$elseBody{% endif %}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// <!-- IF condition -->...<!-- ELSEIF condition2 -->...<!-- ENDIF -->
|
||||||
|
// Also handle <!-- ELSEIF / COMMENT --> format
|
||||||
|
$content = preg_replace_callback('/<!-- IF ([^>]+?) -->((?:(?!<!-- (?:IF|ENDIF|ELSEIF)).)*?)<!-- ELSEIF ([^>]+?)(?:\s*\/[^>]*)? -->((?:(?!<!-- ENDIF).)*?)<!-- ENDIF(?:\s*\/[^>]*)? -->/s', function($matches) {
|
||||||
|
$condition1 = $this->convertCondition($matches[1]);
|
||||||
|
$ifBody = $matches[2];
|
||||||
|
$condition2 = $this->convertCondition($matches[3]);
|
||||||
|
$elseifBody = $matches[4];
|
||||||
|
|
||||||
|
return "{% if $condition1 %}$ifBody{% elseif $condition2 %}$elseifBody{% endif %}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// <!-- IF condition -->...<!-- ELSE -->...<!-- ENDIF -->
|
||||||
|
// Also handle <!-- ELSE / COMMENT --> format
|
||||||
|
$content = preg_replace_callback('/<!-- IF ([^>]+?) -->((?:(?!<!-- (?:IF|ENDIF|ELSE)).)*?)<!-- ELSE(?:\s*\/[^>]*)? -->((?:(?!<!-- ENDIF).)*?)<!-- ENDIF(?:\s*\/[^>]*)? -->/s', function($matches) {
|
||||||
|
$condition = $this->convertCondition($matches[1]);
|
||||||
|
$ifBody = $matches[2];
|
||||||
|
$elseBody = $matches[3];
|
||||||
|
|
||||||
|
return "{% if $condition %}$ifBody{% else %}$elseBody{% endif %}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Simple <!-- IF condition -->...<!-- ENDIF --> (process innermost first)
|
||||||
|
// Use a pattern that matches the smallest possible IF...ENDIF pair
|
||||||
|
$content = preg_replace_callback('/<!-- IF ([^>]+?) -->((?:(?!<!-- (?:IF|ENDIF)).)*?)<!-- ENDIF(?:\s*\/[^>]*)? -->/s', function($matches) {
|
||||||
|
$condition = $this->convertCondition($matches[1]);
|
||||||
|
$body = $matches[2];
|
||||||
|
|
||||||
|
return "{% if $condition %}$body{% endif %}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert any remaining standalone <!-- ELSE --> tags (from nested structures)
|
||||||
|
$content = preg_replace('/<!-- ELSE(?:\s*\/[^>]*)? -->/', '{% else %}', $content);
|
||||||
|
|
||||||
|
$iterations++;
|
||||||
|
} while ($content !== $previousContent && $iterations < $maxIterations);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy variables to Twig syntax
|
||||||
|
*/
|
||||||
|
private function convertVariables(string $content): string
|
||||||
|
{
|
||||||
|
// Convert language variables {L_VARIABLE} to {{ L.VARIABLE }} FIRST
|
||||||
|
$content = preg_replace_callback('/\{L_([A-Z0-9_]+)\}/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
return "{{ L.$varName|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert constants {#CONSTANT#} to {{ constant('CONSTANT') }}
|
||||||
|
$content = preg_replace_callback('/\{#([A-Z0-9_]+)#\}/', function($matches) {
|
||||||
|
$constantName = $matches[1];
|
||||||
|
return "{{ constant('$constantName')|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert PHP variables {$variable} to {{ variable }}
|
||||||
|
$content = preg_replace_callback('/\{\$([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]+\])*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
return "{{ $varName|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert block item variables {blockname_item.VARIABLE} to {{ blockname_item.VARIABLE }}
|
||||||
|
$content = preg_replace_callback('/\{([a-zA-Z0-9_]+_item\.[A-Z0-9_.]+)\}/', function($matches) {
|
||||||
|
$varPath = $matches[1];
|
||||||
|
return "{{ $varPath|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert legacy variables {VARIABLE} to {{ V.VARIABLE }} LAST
|
||||||
|
$content = preg_replace_callback('/\{([A-Z0-9_]+)\}/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
return "{{ V.$varName|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert nested block variables {block.subblock.VARIABLE} (but not simple block vars handled in convertBlocks)
|
||||||
|
// Exclude variables that end with _item. as those are already processed block variables
|
||||||
|
$content = preg_replace_callback('/\{(([a-z0-9\-_]+?\.)+)([a-z0-9\-_]+?)\}/i', function($matches) {
|
||||||
|
$namespace = rtrim($matches[1], '.');
|
||||||
|
$varName = $matches[3];
|
||||||
|
|
||||||
|
// Skip if this is an already-processed block variable (ending with _item)
|
||||||
|
if (str_ends_with($namespace, '_item')) {
|
||||||
|
return $matches[0]; // Return unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('.', $namespace);
|
||||||
|
|
||||||
|
$twigVar = '_tpldata';
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$twigVar .= "['" . $part . ".']";
|
||||||
|
}
|
||||||
|
$twigVar .= "[loop.index0]['$varName']";
|
||||||
|
|
||||||
|
return "{{ $twigVar|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy condition syntax to Twig
|
||||||
|
*/
|
||||||
|
private function convertCondition(string $condition): string
|
||||||
|
{
|
||||||
|
$condition = trim($condition);
|
||||||
|
|
||||||
|
// Convert constants #CONSTANT to constant('CONSTANT') FIRST, before other conversions
|
||||||
|
$condition = preg_replace('/#([A-Z0-9_]+)\b/', "constant('$1')", $condition);
|
||||||
|
|
||||||
|
// Convert PHP-style array access to Twig array access
|
||||||
|
// Handle nested array access like $bb_cfg['key']['subkey']
|
||||||
|
do {
|
||||||
|
$previousCondition = $condition;
|
||||||
|
// Match $variable['key'] patterns
|
||||||
|
$condition = preg_replace_callback('/\$([a-zA-Z_][a-zA-Z0-9_]*)\[([\'"][^\'"]*[\'"])\]/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
$key = trim($matches[2], '\'"');
|
||||||
|
return "$varName.$key";
|
||||||
|
}, $condition);
|
||||||
|
// Match variable.key['subkey'] patterns (for nested access)
|
||||||
|
$condition = preg_replace_callback('/([a-zA-Z_][a-zA-Z0-9_.]*)\[([\'"][^\'"]*[\'"])\]/', function($matches) {
|
||||||
|
$varName = $matches[1];
|
||||||
|
$key = trim($matches[2], '\'"');
|
||||||
|
return "$varName.$key";
|
||||||
|
}, $condition);
|
||||||
|
} while ($condition !== $previousCondition);
|
||||||
|
|
||||||
|
// Convert PHP-style negation ! to Twig 'not' operator
|
||||||
|
$condition = preg_replace('/!(?=\s*[a-zA-Z_$])/', 'not ', $condition);
|
||||||
|
|
||||||
|
// Convert block item variables (they should stay as-is, no V. prefix needed)
|
||||||
|
$condition = preg_replace('/\b([a-z0-9_]+_item)\.([A-Z0-9_]+)\b/', '$1.$2', $condition);
|
||||||
|
|
||||||
|
// Convert variable references, but not if they're constants or part of object/array access
|
||||||
|
$condition = preg_replace('/\b(?<!constant\(\')(?<![a-z0-9_]\.)(?<![a-z0-9_]_item\.)([A-Z0-9_]+)(?!\.[A-Z0-9_])(?!\'\))(?!\])\b/', 'V.$1', $condition);
|
||||||
|
$condition = preg_replace('/\$([a-zA-Z_][a-zA-Z0-9_]*)/', '$1', $condition);
|
||||||
|
|
||||||
|
// Convert operators (with word boundaries to avoid partial matches)
|
||||||
|
$operators = [
|
||||||
|
'/\beq\b/' => '==',
|
||||||
|
'/\bne\b/' => '!=',
|
||||||
|
'/\bneq\b/' => '!=',
|
||||||
|
'/\blt\b/' => '<',
|
||||||
|
'/\ble\b/' => '<=',
|
||||||
|
'/\blte\b/' => '<=',
|
||||||
|
'/\bgt\b/' => '>',
|
||||||
|
'/\bge\b/' => '>=',
|
||||||
|
'/\bgte\b/' => '>=',
|
||||||
|
'/\band\b/' => 'and',
|
||||||
|
'/\bor\b/' => 'or',
|
||||||
|
'/\bnot\b/' => 'not',
|
||||||
|
'/\bmod\b/' => '%',
|
||||||
|
// Handle C-style logical operators
|
||||||
|
'/&&/' => ' and ',
|
||||||
|
'/\|\|/' => ' or ',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($operators as $pattern => $replacement) {
|
||||||
|
$condition = preg_replace($pattern, $replacement, $condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy blocks to Twig loops
|
||||||
|
*/
|
||||||
|
private function convertBlocks(string $content): string
|
||||||
|
{
|
||||||
|
return $this->convertBlocksRecursive($content, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert blocks recursively with proper nesting support
|
||||||
|
*/
|
||||||
|
private function convertBlocksRecursive(string $content, array $parentBlocks = []): string
|
||||||
|
{
|
||||||
|
// Find all block pairs in this level
|
||||||
|
$pattern = '/<!-- BEGIN ([a-zA-Z0-9_]+) -->(.*?)<!-- END \1 -->/s';
|
||||||
|
|
||||||
|
return preg_replace_callback($pattern, function($matches) use ($parentBlocks) {
|
||||||
|
$blockName = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
|
||||||
|
// Recursively process nested blocks first
|
||||||
|
$currentBlockStack = array_merge($parentBlocks, [$blockName]);
|
||||||
|
$body = $this->convertBlocksRecursive($body, $currentBlockStack);
|
||||||
|
|
||||||
|
// Convert variables for this block level
|
||||||
|
$body = $this->convertBlockVariables($body, $blockName, $parentBlocks);
|
||||||
|
|
||||||
|
// Generate the appropriate loop structure based on how assign_block_vars works
|
||||||
|
if (empty($parentBlocks)) {
|
||||||
|
// Top-level block: _tpldata['blockname.']
|
||||||
|
return "{% for {$blockName}_item in _tpldata['{$blockName}.']|default([]) %}$body{% endfor %}";
|
||||||
|
} else {
|
||||||
|
// Nested block: When assign_block_vars('parent.child', ...) is used,
|
||||||
|
// it creates _tpldata['parent.'][index]['child.'][nested_index]
|
||||||
|
// So we need to access parent_item['child.'] (with dot suffix)
|
||||||
|
$parentVar = end($parentBlocks) . '_item';
|
||||||
|
return "{% for {$blockName}_item in {$parentVar}['{$blockName}.']|default([]) %}$body{% endfor %}";
|
||||||
|
}
|
||||||
|
}, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert block variables with proper nesting context
|
||||||
|
*/
|
||||||
|
private function convertBlockVariables(string $content, string $currentBlock, array $parentBlocks): string
|
||||||
|
{
|
||||||
|
// Build the full block path for complex nested variables
|
||||||
|
$fullBlockPath = array_merge($parentBlocks, [$currentBlock]);
|
||||||
|
|
||||||
|
// For nested blocks, we need to handle variables that reference the full path
|
||||||
|
// For example: {c.f.VARIABLE} when we're in the 'f' block should become {{ f_item.VARIABLE }}
|
||||||
|
// because the 'f' loop is already inside the 'c' loop context
|
||||||
|
|
||||||
|
if (!empty($parentBlocks)) {
|
||||||
|
// Convert full path variables like {parent.current.VARIABLE} to {{ current_item.VARIABLE }}
|
||||||
|
$fullPathPattern = implode('\.', $fullBlockPath);
|
||||||
|
$content = preg_replace_callback('/\{' . $fullPathPattern . '\.([A-Z0-9_.]+)\}/', function($matches) use ($currentBlock) {
|
||||||
|
$varPath = $matches[1];
|
||||||
|
return "{{ {$currentBlock}_item.$varPath|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Also handle parent.current.subblock.VARIABLE patterns (like c.f.sf.VARIABLE)
|
||||||
|
$content = preg_replace_callback('/\{' . $fullPathPattern . '\.([a-z0-9_]+)\.([A-Z0-9_.]+)\}/', function($matches) use ($currentBlock) {
|
||||||
|
$subBlock = $matches[1];
|
||||||
|
$varPath = $matches[2];
|
||||||
|
return "{{ {$currentBlock}_item.$subBlock.$varPath|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert simple block variables for current level {blockname.VARIABLE}
|
||||||
|
$content = preg_replace_callback('/\{' . preg_quote($currentBlock) . '\.([A-Z0-9_.]+)\}/', function($matches) use ($currentBlock) {
|
||||||
|
$varPath = $matches[1];
|
||||||
|
return "{{ {$currentBlock}_item.$varPath|default('') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Convert block variables in attributes and other contexts (without curly braces)
|
||||||
|
$content = preg_replace('/\b' . preg_quote($currentBlock) . '\.([A-Z0-9_.]+)\b/', $currentBlock . '_item.$1', $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy includes to Twig includes
|
||||||
|
*/
|
||||||
|
private function convertIncludes(string $content): string
|
||||||
|
{
|
||||||
|
// <!-- INCLUDE filename -->
|
||||||
|
$content = preg_replace_callback('/<!-- INCLUDE ([a-zA-Z0-9_\.\-\/]+) -->/', function($matches) {
|
||||||
|
$filename = $matches[1];
|
||||||
|
return "{{ include('$filename') }}";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variable from template data
|
||||||
|
*/
|
||||||
|
public function getVariable(string $varName, mixed $default = ''): mixed
|
||||||
|
{
|
||||||
|
global $template;
|
||||||
|
return $template->vars[$varName] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get language variable
|
||||||
|
*/
|
||||||
|
public function getLanguageVariable(string $key, mixed $default = ''): mixed
|
||||||
|
{
|
||||||
|
global $lang;
|
||||||
|
return $lang[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PHP constant value
|
||||||
|
*/
|
||||||
|
public function getConstant(string $name): mixed
|
||||||
|
{
|
||||||
|
return defined($name) ? constant($name) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a single variable reference
|
||||||
|
*/
|
||||||
|
public function convertVariable(string $varRef): string
|
||||||
|
{
|
||||||
|
if (preg_match('/^L_(.+)$/', $varRef, $matches)) {
|
||||||
|
return "L.{$matches[1]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^\$(.+)$/', $varRef, $matches)) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^#(.+)#$/', $varRef, $matches)) {
|
||||||
|
return "constant('{$matches[1]}')";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "V.$varRef";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert block reference
|
||||||
|
*/
|
||||||
|
public function convertBlock(string $blockRef): string
|
||||||
|
{
|
||||||
|
$parts = explode('.', $blockRef);
|
||||||
|
$result = '_tpldata';
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$result .= "['$part.']";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy include function
|
||||||
|
*/
|
||||||
|
public function legacyInclude(string $template): string
|
||||||
|
{
|
||||||
|
return "{{ include('$template') }}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy if function
|
||||||
|
*/
|
||||||
|
public function legacyIf(string $condition, string $then = '', string $else = ''): string
|
||||||
|
{
|
||||||
|
$convertedCondition = $this->convertCondition($condition);
|
||||||
|
|
||||||
|
if ($else) {
|
||||||
|
return "{% if $convertedCondition %}$then{% else %}$else{% endif %}";
|
||||||
|
} else {
|
||||||
|
return "{% if $convertedCondition %}$then{% endif %}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if content contains legacy syntax
|
||||||
|
*/
|
||||||
|
public function isLegacySyntax(string $content): bool
|
||||||
|
{
|
||||||
|
$patterns = [
|
||||||
|
'/\{[A-Z0-9_]+\}/', // {VARIABLE}
|
||||||
|
'/\{L_[A-Z0-9_]+\}/', // {L_VARIABLE}
|
||||||
|
'/\{\$[a-zA-Z_][^}]*\}/', // {$variable}
|
||||||
|
'/\{#[A-Z0-9_]+#\}/', // {#CONSTANT#}
|
||||||
|
'/<!-- IF .+ -->/', // <!-- IF ... -->
|
||||||
|
'/<!-- BEGIN .+ -->/', // <!-- BEGIN ... -->
|
||||||
|
'/<!-- INCLUDE .+ -->/', // <!-- INCLUDE ... -->
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $content)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
65
src/Template/Loaders/LegacyTemplateLoader.php
Normal file
65
src/Template/Loaders/LegacyTemplateLoader.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||||
|
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||||
|
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TorrentPier\Template\Loaders;
|
||||||
|
|
||||||
|
use Twig\Loader\LoaderInterface;
|
||||||
|
use Twig\Source;
|
||||||
|
use TorrentPier\Template\Extensions\LegacySyntaxExtension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template loader that converts legacy TorrentPier template syntax to Twig syntax
|
||||||
|
*/
|
||||||
|
class LegacyTemplateLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
private LoaderInterface $loader;
|
||||||
|
private string $templateDir;
|
||||||
|
private LegacySyntaxExtension $syntaxConverter;
|
||||||
|
private array $cache = [];
|
||||||
|
|
||||||
|
public function __construct(LoaderInterface $loader, string $templateDir)
|
||||||
|
{
|
||||||
|
$this->loader = $loader;
|
||||||
|
$this->templateDir = $templateDir;
|
||||||
|
$this->syntaxConverter = new LegacySyntaxExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSourceContext(string $name): Source
|
||||||
|
{
|
||||||
|
// Get the original source
|
||||||
|
$source = $this->loader->getSourceContext($name);
|
||||||
|
$content = $source->getCode();
|
||||||
|
|
||||||
|
// Check if we need to convert legacy syntax
|
||||||
|
if ($this->syntaxConverter->isLegacySyntax($content)) {
|
||||||
|
// Convert legacy syntax to Twig syntax
|
||||||
|
$convertedContent = $this->syntaxConverter->convertLegacySyntax($content);
|
||||||
|
|
||||||
|
// Create new source with converted content
|
||||||
|
return new Source($convertedContent, $source->getName(), $source->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCacheKey(string $name): string
|
||||||
|
{
|
||||||
|
return $this->loader->getCacheKey($name) . '_legacy';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFresh(string $name, int $time): bool
|
||||||
|
{
|
||||||
|
return $this->loader->isFresh($name, $time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exists(string $name): bool
|
||||||
|
{
|
||||||
|
return $this->loader->exists($name);
|
||||||
|
}
|
||||||
|
}
|
305
src/Template/README.md
Normal file
305
src/Template/README.md
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
# TorrentPier Template System (Twig-based)
|
||||||
|
|
||||||
|
The TorrentPier Template system has been modernized to use Twig internally while maintaining **100% backward compatibility** with the existing template syntax and API.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The new Template system provides:
|
||||||
|
- **Modern Twig engine** internally for better performance and features
|
||||||
|
- **Full backward compatibility** with existing `.tpl` files and API
|
||||||
|
- **Automatic syntax conversion** from legacy syntax to Twig
|
||||||
|
- **Singleton pattern** consistent with other TorrentPier services
|
||||||
|
- **Extensible architecture** with clean separation of concerns
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Template/
|
||||||
|
├── Template.php # Main singleton class
|
||||||
|
├── TwigEnvironmentFactory.php # Twig environment setup
|
||||||
|
├── Extensions/ # Twig extensions for compatibility
|
||||||
|
│ ├── LegacySyntaxExtension.php # Legacy syntax conversion
|
||||||
|
│ ├── BlockExtension.php # Block system support
|
||||||
|
│ └── LanguageExtension.php # Language system integration
|
||||||
|
├── Loaders/ # Template loaders
|
||||||
|
│ └── LegacyTemplateLoader.php # Legacy template loading
|
||||||
|
└── README.md # This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage (Backward Compatible)
|
||||||
|
|
||||||
|
All existing code continues to work unchanged:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Original usage - still works
|
||||||
|
$template = new TorrentPier\Legacy\Template($template_dir);
|
||||||
|
$template->assign_vars(['TITLE' => 'My Page']);
|
||||||
|
$template->set_filenames(['body' => 'index.tpl']);
|
||||||
|
$template->pparse('body');
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Singleton Usage (Recommended)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// New singleton approach
|
||||||
|
$template = template(); // or TorrentPier\Template\Template::getInstance()
|
||||||
|
$template->assign_vars(['TITLE' => 'My Page']);
|
||||||
|
$template->set_filenames(['body' => 'index.tpl']);
|
||||||
|
$template->pparse('body');
|
||||||
|
|
||||||
|
// Directory-specific instance
|
||||||
|
$template = template('/path/to/templates');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Twig Features
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Access Twig environment for advanced features
|
||||||
|
$twig = template()->getTwig();
|
||||||
|
|
||||||
|
// Add custom filters, functions, etc.
|
||||||
|
$twig->addFilter(new \Twig\TwigFilter('my_filter', 'my_filter_function'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Syntax Conversion
|
||||||
|
|
||||||
|
The system automatically converts legacy template syntax to Twig:
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<!-- Legacy syntax (still works) -->
|
||||||
|
{TITLE}
|
||||||
|
{L_WELCOME}
|
||||||
|
{$user.username}
|
||||||
|
{#CONSTANT#}
|
||||||
|
|
||||||
|
<!-- Converted to Twig internally -->
|
||||||
|
{{ V.TITLE }}
|
||||||
|
{{ L.WELCOME }}
|
||||||
|
{{ user.username }}
|
||||||
|
{{ constant('CONSTANT') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditionals
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<!-- Legacy syntax -->
|
||||||
|
<!-- IF CONDITION -->Content<!-- ENDIF -->
|
||||||
|
<!-- IF CONDITION -->If content<!-- ELSE -->Else content<!-- ENDIF -->
|
||||||
|
|
||||||
|
<!-- Converted to Twig -->
|
||||||
|
{% if CONDITION %}Content{% endif %}
|
||||||
|
{% if CONDITION %}If content{% else %}Else content{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blocks/Loops
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<!-- Legacy syntax -->
|
||||||
|
<!-- BEGIN items -->
|
||||||
|
{items.NAME}: {items.VALUE}
|
||||||
|
<!-- END items -->
|
||||||
|
|
||||||
|
<!-- Converted to Twig -->
|
||||||
|
{% for items_item in _tpldata['items.']|default([]) %}
|
||||||
|
{{ items_item.NAME }}: {{ items_item.VALUE }}
|
||||||
|
{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Includes
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<!-- Legacy syntax -->
|
||||||
|
<!-- INCLUDE header.tpl -->
|
||||||
|
|
||||||
|
<!-- Converted to Twig -->
|
||||||
|
{{ include('header.tpl') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
### Enhanced Template Functions
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Check if template exists before including
|
||||||
|
if (template()->getTwig()->getLoader()->exists('optional.tpl')) {
|
||||||
|
// Include template
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Twig's powerful features
|
||||||
|
$template->getTwig()->addGlobal('current_user', $current_user);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Better Error Handling
|
||||||
|
|
||||||
|
- Detailed error messages with line numbers
|
||||||
|
- Template debugging information
|
||||||
|
- Graceful fallback to legacy system if needed
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
- Twig's compiled template caching
|
||||||
|
- Automatic template recompilation on changes
|
||||||
|
- Optimized template rendering
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
**No changes required!** All existing code continues to work. However, you can optionally:
|
||||||
|
|
||||||
|
1. **Use the singleton pattern:**
|
||||||
|
```php
|
||||||
|
// Old
|
||||||
|
$template = new TorrentPier\Legacy\Template($dir);
|
||||||
|
|
||||||
|
// New (recommended)
|
||||||
|
$template = template($dir);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Leverage new Twig features:**
|
||||||
|
```php
|
||||||
|
// Add custom functionality
|
||||||
|
template()->getTwig()->addFunction(new \Twig\TwigFunction('my_func', 'my_function'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Template Designers
|
||||||
|
|
||||||
|
Templates continue to work with the existing syntax. New templates can optionally use:
|
||||||
|
|
||||||
|
1. **Modern Twig syntax** for new features
|
||||||
|
2. **Mixed syntax** (legacy + Twig) in the same template
|
||||||
|
3. **Twig inheritance** for better template organization
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
The system maintains 100% compatibility with:
|
||||||
|
- ✅ All existing `.tpl` files
|
||||||
|
- ✅ Legacy syntax (`{VARIABLE}`, `<!-- IF -->`, `<!-- BEGIN -->`)
|
||||||
|
- ✅ All Template class methods
|
||||||
|
- ✅ Block assignment (`assign_block_vars`)
|
||||||
|
- ✅ Variable assignment (`assign_vars`, `assign_var`)
|
||||||
|
- ✅ Template compilation and caching
|
||||||
|
- ✅ File includes and preprocessing
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
The system automatically configures Twig based on TorrentPier settings:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Debug mode based on dev level
|
||||||
|
'debug' => dev()->get_level() > 0,
|
||||||
|
|
||||||
|
// Cache based on template cache settings
|
||||||
|
'cache' => config()->get('xs_use_cache'),
|
||||||
|
|
||||||
|
// Backward compatibility settings
|
||||||
|
'strict_variables' => false,
|
||||||
|
'autoescape' => false,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
|
||||||
|
All TorrentPier-specific functionality is provided through Twig extensions:
|
||||||
|
|
||||||
|
- **LegacySyntaxExtension**: Converts legacy syntax
|
||||||
|
- **BlockExtension**: Handles the `_tpldata` block system
|
||||||
|
- **LanguageExtension**: Integrates with the language system
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Template Not Found
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Check if template exists
|
||||||
|
if (!template()->getTwig()->getLoader()->exists('template.tpl')) {
|
||||||
|
// Handle missing template
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Syntax Errors
|
||||||
|
|
||||||
|
The system provides detailed error messages for template syntax issues:
|
||||||
|
|
||||||
|
```
|
||||||
|
Twig\Error\SyntaxError: Unexpected token "punctuation" of value "}" in "template.tpl" at line 15.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback Mode
|
||||||
|
|
||||||
|
If Twig fails to render a template, the system automatically falls back to the legacy parser:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Automatic fallback on Twig failure
|
||||||
|
catch (\Exception $e) {
|
||||||
|
return $this->legacyParse($handle);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Complex Template with Mixed Syntax
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<!-- header.tpl -->
|
||||||
|
<header>
|
||||||
|
<h1>{SITE_NAME}</h1>
|
||||||
|
|
||||||
|
<!-- IF LOGGED_IN -->
|
||||||
|
<p>Welcome, {USERNAME}!</p>
|
||||||
|
|
||||||
|
<!-- BEGIN notifications -->
|
||||||
|
<div class="notification">
|
||||||
|
{notifications.MESSAGE}
|
||||||
|
<span class="time">{{ notifications.TIMESTAMP|bb_date }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- END notifications -->
|
||||||
|
<!-- ELSE -->
|
||||||
|
<a href="{LOGIN_URL}">Login</a>
|
||||||
|
<!-- ENDIF -->
|
||||||
|
</header>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Twig Features in Templates
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<!-- Modern Twig syntax can be mixed with legacy -->
|
||||||
|
{% set user_count = users|length %}
|
||||||
|
<p>Total users: {{ user_count }}</p>
|
||||||
|
|
||||||
|
<!-- Legacy blocks still work -->
|
||||||
|
<!-- BEGIN users -->
|
||||||
|
<div class="user">
|
||||||
|
Name: {users.NAME}
|
||||||
|
Joined: {{ users.JOIN_DATE|bb_date('d-M-Y') }}
|
||||||
|
</div>
|
||||||
|
<!-- END users -->
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
The new system provides significant performance improvements:
|
||||||
|
|
||||||
|
- **Template compilation**: Twig compiles templates to optimized PHP code
|
||||||
|
- **Intelligent caching**: Only recompiles when source templates change
|
||||||
|
- **Memory efficiency**: Reduced memory usage compared to legacy system
|
||||||
|
- **Faster rendering**: Compiled templates execute faster than interpreted ones
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Planned improvements include:
|
||||||
|
|
||||||
|
1. **Template inheritance**: Use Twig's `{% extends %}` for better template organization
|
||||||
|
2. **Macro system**: Reusable template components
|
||||||
|
3. **Advanced filters**: More built-in filters for common operations
|
||||||
|
4. **IDE support**: Better syntax highlighting and auto-completion
|
||||||
|
5. **Asset management**: Integration with modern asset pipelines
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The new Template system provides a modern, powerful foundation while preserving complete backward compatibility. Developers can continue using existing code while gradually adopting new features as needed.
|
472
src/Template/Template.php
Normal file
472
src/Template/Template.php
Normal file
|
@ -0,0 +1,472 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||||
|
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||||
|
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TorrentPier\Template;
|
||||||
|
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
use TorrentPier\Template\Extensions\LegacySyntaxExtension;
|
||||||
|
use TorrentPier\Template\Extensions\BlockExtension;
|
||||||
|
use TorrentPier\Template\Extensions\LanguageExtension;
|
||||||
|
use TorrentPier\Template\Loaders\LegacyTemplateLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modern Template class using Twig internally with full backward compatibility
|
||||||
|
* Implements singleton pattern while maintaining all existing Template methods
|
||||||
|
*/
|
||||||
|
class Template
|
||||||
|
{
|
||||||
|
private static ?Template $instance = null;
|
||||||
|
private static array $instances = [];
|
||||||
|
|
||||||
|
private ?Environment $twig = null;
|
||||||
|
private ?TwigEnvironmentFactory $factory = null;
|
||||||
|
|
||||||
|
// Backward compatibility properties - mirror original Template class
|
||||||
|
public array $_tpldata = [
|
||||||
|
'.' => [
|
||||||
|
0 => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
public array $vars = [];
|
||||||
|
public array $files = [];
|
||||||
|
public array $files_cache = [];
|
||||||
|
public array $files_cache2 = [];
|
||||||
|
public string $root = '';
|
||||||
|
public string $cachedir = '';
|
||||||
|
public string $tpldir = '';
|
||||||
|
public string $tpldef = 'default';
|
||||||
|
public int $use_cache = 1;
|
||||||
|
public string $tpl = '';
|
||||||
|
public string $cur_tpl = '';
|
||||||
|
public array $replace = [];
|
||||||
|
public string $preparse = '';
|
||||||
|
public string $postparse = '';
|
||||||
|
public array $style_config = [];
|
||||||
|
public array $lang = [];
|
||||||
|
public array $uncompiled_code = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor for singleton pattern
|
||||||
|
*/
|
||||||
|
private function __construct(string $root = '.')
|
||||||
|
{
|
||||||
|
global $lang;
|
||||||
|
|
||||||
|
// Initialize backward compatibility properties
|
||||||
|
$this->vars =& $this->_tpldata['.'][0];
|
||||||
|
$this->tpldir = TEMPLATES_DIR;
|
||||||
|
$this->root = $root;
|
||||||
|
$this->tpl = basename($root);
|
||||||
|
$this->lang =& $lang;
|
||||||
|
$this->use_cache = config()->get('xs_use_cache');
|
||||||
|
$this->cachedir = CACHE_DIR . '/';
|
||||||
|
|
||||||
|
// Check template directory exists
|
||||||
|
if (!is_dir($this->root)) {
|
||||||
|
die("Theme ({$this->tpl}) directory not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Twig environment
|
||||||
|
$this->initializeTwig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance for default template
|
||||||
|
*/
|
||||||
|
public static function getInstance(?string $root = null): self
|
||||||
|
{
|
||||||
|
$root = $root ?: '.';
|
||||||
|
$key = md5($root);
|
||||||
|
|
||||||
|
if (!isset(self::$instances[$key])) {
|
||||||
|
self::$instances[$key] = new self($root);
|
||||||
|
|
||||||
|
// If this is the first instance, set as default
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = self::$instances[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instances[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance for specific template directory
|
||||||
|
*/
|
||||||
|
public static function getDirectoryInstance(string $root): self
|
||||||
|
{
|
||||||
|
return self::getInstance($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Twig environment with legacy compatibility
|
||||||
|
*/
|
||||||
|
private function initializeTwig(): void
|
||||||
|
{
|
||||||
|
$this->factory = new TwigEnvironmentFactory();
|
||||||
|
$this->twig = $this->factory->create($this->root, $this->cachedir, $this->use_cache);
|
||||||
|
|
||||||
|
// Add template variables to Twig globals
|
||||||
|
$this->twig->addGlobal('_tpldata', $this->_tpldata);
|
||||||
|
$this->twig->addGlobal('V', $this->vars);
|
||||||
|
$this->twig->addGlobal('L', $this->lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Twig environment (for advanced usage)
|
||||||
|
*/
|
||||||
|
public function getTwig(): Environment
|
||||||
|
{
|
||||||
|
return $this->twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns template filename for handle (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function set_filename(string $handle, string $filename, bool $xs_include = false, bool $quiet = false): bool
|
||||||
|
{
|
||||||
|
$can_cache = $this->use_cache;
|
||||||
|
$this->files[$handle] = $this->make_filename($filename, $xs_include);
|
||||||
|
$this->files_cache[$handle] = '';
|
||||||
|
$this->files_cache2[$handle] = '';
|
||||||
|
|
||||||
|
// Check if we have valid filename
|
||||||
|
if (!$this->files[$handle]) {
|
||||||
|
if ($xs_include || $quiet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
die("Template->make_filename(): Error - invalid template $filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cache filename
|
||||||
|
if ($can_cache) {
|
||||||
|
$this->files_cache2[$handle] = $this->make_filename_cache($this->files[$handle]);
|
||||||
|
if (@file_exists($this->files_cache2[$handle])) {
|
||||||
|
$this->files_cache[$handle] = $this->files_cache2[$handle];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tpl file exists
|
||||||
|
if (empty($this->files_cache[$handle]) && !@file_exists($this->files[$handle])) {
|
||||||
|
if ($quiet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
die('Template->make_filename(): Error - template file not found: <br /><br />' . hide_bb_path($this->files[$handle]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should recompile cache
|
||||||
|
if (!empty($this->files_cache[$handle])) {
|
||||||
|
$cache_time = @filemtime($this->files_cache[$handle]);
|
||||||
|
if (@filemtime($this->files[$handle]) > $cache_time) {
|
||||||
|
$this->files_cache[$handle] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the template filenames for handles (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function set_filenames(array $filenames): void
|
||||||
|
{
|
||||||
|
foreach ($filenames as $handle => $filename) {
|
||||||
|
$this->set_filename($handle, $filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root-level variable assignment (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function assign_vars(array $vararray): void
|
||||||
|
{
|
||||||
|
foreach ($vararray as $key => $val) {
|
||||||
|
$this->vars[$key] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Twig globals
|
||||||
|
$this->twig->addGlobal('V', $this->vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root-level variable assignment (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function assign_var(string $varname, mixed $varval = true): void
|
||||||
|
{
|
||||||
|
$this->vars[$varname] = $varval;
|
||||||
|
|
||||||
|
// Update Twig globals
|
||||||
|
$this->twig->addGlobal('V', $this->vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block-level variable assignment (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function assign_block_vars(string $blockname, array $vararray): bool
|
||||||
|
{
|
||||||
|
if (str_contains($blockname, '.')) {
|
||||||
|
// Nested block
|
||||||
|
$blocks = explode('.', $blockname);
|
||||||
|
$blockcount = count($blocks) - 1;
|
||||||
|
|
||||||
|
$str = &$this->_tpldata;
|
||||||
|
for ($i = 0; $i < $blockcount; $i++) {
|
||||||
|
$str = &$str[$blocks[$i] . '.'];
|
||||||
|
$str = &$str[(is_countable($str) ? count($str) : 0) - 1];
|
||||||
|
}
|
||||||
|
$str[$blocks[$blockcount] . '.'][] = $vararray;
|
||||||
|
} else {
|
||||||
|
// Top-level block
|
||||||
|
$this->_tpldata[$blockname . '.'][] = $vararray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Twig globals
|
||||||
|
$this->twig->addGlobal('_tpldata', $this->_tpldata);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and print template (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function pparse(string $handle): bool
|
||||||
|
{
|
||||||
|
// Handle preparse and postparse
|
||||||
|
if ($this->preparse || $this->postparse) {
|
||||||
|
$preparse = $this->preparse;
|
||||||
|
$postparse = $this->postparse;
|
||||||
|
$this->preparse = '';
|
||||||
|
$this->postparse = '';
|
||||||
|
|
||||||
|
if ($preparse) {
|
||||||
|
$this->pparse($preparse);
|
||||||
|
}
|
||||||
|
if ($postparse) {
|
||||||
|
$str = $handle;
|
||||||
|
$handle = $postparse;
|
||||||
|
$this->pparse($str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if handle exists
|
||||||
|
if (empty($this->files[$handle]) && empty($this->files_cache[$handle])) {
|
||||||
|
die("Template->loadfile(): No files found for handle $handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->xs_startup();
|
||||||
|
|
||||||
|
// Ensure we have the file path
|
||||||
|
if (empty($this->files[$handle])) {
|
||||||
|
die("Template->pparse(): No files found for handle $handle. Make sure set_filename() was called first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_full_path = $this->files[$handle];
|
||||||
|
|
||||||
|
// Get template name relative to configured template directories
|
||||||
|
$template_name = $this->getRelativeTemplateName($template_full_path);
|
||||||
|
|
||||||
|
// Prepare template context
|
||||||
|
$context = array_merge(
|
||||||
|
$this->vars,
|
||||||
|
[
|
||||||
|
'_tpldata' => $this->_tpldata,
|
||||||
|
'L' => $this->lang,
|
||||||
|
'V' => $this->vars
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo $this->twig->render($template_name, $context);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Instead of falling back to legacy, provide a helpful error message
|
||||||
|
die("Template rendering error for '$handle' (template: '$template_name'): " . $e->getMessage() .
|
||||||
|
"\nTemplate path: $template_full_path\n" .
|
||||||
|
"Available templates in Twig loader: " . implode(', ', $this->getAvailableTemplates()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate filename with path (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function make_filename(string $filename, bool $xs_include = false): string
|
||||||
|
{
|
||||||
|
// Check replacements list
|
||||||
|
if (!$xs_include && isset($this->replace[$filename])) {
|
||||||
|
$filename = $this->replace[$filename];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle admin templates specially
|
||||||
|
if (str_starts_with($filename, 'admin/')) {
|
||||||
|
$adminTemplateDir = dirname($this->root) . '/admin';
|
||||||
|
if (is_dir($adminTemplateDir)) {
|
||||||
|
// Remove 'admin/' prefix and use admin template directory
|
||||||
|
$adminTemplateName = substr($filename, 6); // Remove 'admin/'
|
||||||
|
return $adminTemplateDir . '/' . $adminTemplateName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an absolute or relative path
|
||||||
|
if (($filename[0] !== '/') && (strlen($filename) < 2 || $filename[1] !== ':')) {
|
||||||
|
return $this->root . '/' . $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert template filename to cache filename (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function make_filename_cache(string $filename): string
|
||||||
|
{
|
||||||
|
$filename = clean_filename(str_replace(TEMPLATES_DIR, '', $filename));
|
||||||
|
return $this->cachedir . XS_TPL_PREFIX . $filename . '.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize startup variables (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function xs_startup(): void
|
||||||
|
{
|
||||||
|
$this->vars['LANG'] ??= config()->get('default_lang');
|
||||||
|
|
||||||
|
$tpl = $this->root . '/';
|
||||||
|
if (str_starts_with($tpl, './')) {
|
||||||
|
$tpl = substr($tpl, 2);
|
||||||
|
}
|
||||||
|
$this->vars['TEMPLATE'] ??= $tpl;
|
||||||
|
$this->vars['TEMPLATE_NAME'] ??= $this->tpl;
|
||||||
|
|
||||||
|
// Update Twig globals
|
||||||
|
$this->twig->addGlobal('V', $this->vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert absolute template path to relative template name for Twig
|
||||||
|
*/
|
||||||
|
private function getRelativeTemplateName(string $template_full_path): string
|
||||||
|
{
|
||||||
|
// Normalize the path
|
||||||
|
$template_full_path = realpath($template_full_path) ?: $template_full_path;
|
||||||
|
|
||||||
|
// Check if it's an admin template
|
||||||
|
$adminTemplateDir = realpath(dirname($this->root) . '/admin');
|
||||||
|
if ($adminTemplateDir && str_starts_with($template_full_path, $adminTemplateDir . '/')) {
|
||||||
|
// Admin template - return relative path from admin directory
|
||||||
|
return str_replace($adminTemplateDir . '/', '', $template_full_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a regular template in current directory
|
||||||
|
$rootDir = realpath($this->root);
|
||||||
|
if ($rootDir && str_starts_with($template_full_path, $rootDir . '/')) {
|
||||||
|
// Regular template - return relative path from root directory
|
||||||
|
return str_replace($rootDir . '/', '', $template_full_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a template in default directory (fallback)
|
||||||
|
$defaultTemplateDir = realpath(dirname($this->root) . '/default');
|
||||||
|
if ($defaultTemplateDir && str_starts_with($template_full_path, $defaultTemplateDir . '/')) {
|
||||||
|
// Default template - return relative path from default directory
|
||||||
|
return str_replace($defaultTemplateDir . '/', '', $template_full_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback - try to extract just the filename
|
||||||
|
return basename($template_full_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available templates for debugging
|
||||||
|
*/
|
||||||
|
private function getAvailableTemplates(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Try to get available templates from Twig loader
|
||||||
|
$loader = $this->twig->getLoader();
|
||||||
|
if (method_exists($loader, 'getPaths')) {
|
||||||
|
$paths = $loader->getPaths();
|
||||||
|
$templates = [];
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$files = glob($path . '/*.tpl');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$templates[] = basename($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $templates;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If we can't get templates from loader, return what we know
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($this->files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to dump template data
|
||||||
|
*/
|
||||||
|
public function debugDump(string $title = 'Template Debug', bool $die = false): void
|
||||||
|
{
|
||||||
|
echo "<h2>$title</h2>";
|
||||||
|
echo "<h3>_tpldata structure:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r($this->_tpldata);
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Root variables (vars):</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r($this->vars);
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Language variables (first 10):</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r(array_slice($this->lang, 0, 10, true));
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
echo "<h3>Template files:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
print_r($this->files);
|
||||||
|
echo "</pre>";
|
||||||
|
|
||||||
|
if ($die) {
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to dump specific block data
|
||||||
|
*/
|
||||||
|
public function debugBlock(string $blockName): void
|
||||||
|
{
|
||||||
|
echo "<h3>Block '$blockName' data:</h3>";
|
||||||
|
echo "<pre>";
|
||||||
|
$blockKey = $blockName . '.';
|
||||||
|
if (isset($this->_tpldata[$blockKey])) {
|
||||||
|
print_r($this->_tpldata[$blockKey]);
|
||||||
|
} else {
|
||||||
|
echo "Block '$blockName' not found in _tpldata\n";
|
||||||
|
echo "Available blocks: " . implode(', ', array_keys($this->_tpldata)) . "\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all instances (for testing)
|
||||||
|
*/
|
||||||
|
public static function destroyInstances(): void
|
||||||
|
{
|
||||||
|
self::$instance = null;
|
||||||
|
self::$instances = [];
|
||||||
|
}
|
||||||
|
}
|
123
src/Template/TwigEnvironmentFactory.php
Normal file
123
src/Template/TwigEnvironmentFactory.php
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||||
|
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||||
|
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TorrentPier\Template;
|
||||||
|
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
use Twig\Cache\FilesystemCache;
|
||||||
|
use TorrentPier\Template\Extensions\LegacySyntaxExtension;
|
||||||
|
use TorrentPier\Template\Extensions\BlockExtension;
|
||||||
|
use TorrentPier\Template\Extensions\LanguageExtension;
|
||||||
|
use TorrentPier\Template\Loaders\LegacyTemplateLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for creating and configuring Twig environments with TorrentPier legacy compatibility
|
||||||
|
*/
|
||||||
|
class TwigEnvironmentFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a Twig environment with TorrentPier configuration
|
||||||
|
*/
|
||||||
|
public function create(string $templateDir, string $cacheDir, bool $useCache = true): Environment
|
||||||
|
{
|
||||||
|
// Prepare template directories - include both admin and default directories
|
||||||
|
$templateDirs = [$templateDir];
|
||||||
|
|
||||||
|
// Add admin template directory if it exists and is different from main template dir
|
||||||
|
$adminTemplateDir = dirname($templateDir) . '/admin';
|
||||||
|
if (is_dir($adminTemplateDir) && $adminTemplateDir !== $templateDir) {
|
||||||
|
$templateDirs[] = $adminTemplateDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default template directory as fallback if current dir is not default
|
||||||
|
$defaultTemplateDir = dirname($templateDir) . '/default';
|
||||||
|
if (is_dir($defaultTemplateDir) && $defaultTemplateDir !== $templateDir && !in_array($defaultTemplateDir, $templateDirs)) {
|
||||||
|
$templateDirs[] = $defaultTemplateDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main filesystem loader with multiple directories
|
||||||
|
$loader = new FilesystemLoader($templateDirs);
|
||||||
|
|
||||||
|
// Wrap with legacy loader for backward compatibility
|
||||||
|
$legacyLoader = new LegacyTemplateLoader($loader, $templateDir);
|
||||||
|
|
||||||
|
// Configure Twig environment
|
||||||
|
$options = [
|
||||||
|
'debug' => dev()->isDebugEnabled(),
|
||||||
|
'auto_reload' => true,
|
||||||
|
'strict_variables' => false, // Allow undefined variables for backward compatibility
|
||||||
|
'autoescape' => false, // Disable auto-escaping for backward compatibility
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add cache if enabled
|
||||||
|
if ($useCache && $cacheDir) {
|
||||||
|
$options['cache'] = new FilesystemCache($cacheDir . '/twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
$twig = new Environment($legacyLoader, $options);
|
||||||
|
|
||||||
|
// Add TorrentPier-specific extensions
|
||||||
|
$this->addExtensions($twig);
|
||||||
|
|
||||||
|
// Add global functions for backward compatibility
|
||||||
|
$this->addGlobalFunctions($twig);
|
||||||
|
|
||||||
|
return $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add TorrentPier-specific Twig extensions
|
||||||
|
*/
|
||||||
|
private function addExtensions(Environment $twig): void
|
||||||
|
{
|
||||||
|
// Legacy syntax conversion extension
|
||||||
|
$twig->addExtension(new LegacySyntaxExtension());
|
||||||
|
|
||||||
|
// Block system extension
|
||||||
|
$twig->addExtension(new BlockExtension());
|
||||||
|
|
||||||
|
// Language extension
|
||||||
|
$twig->addExtension(new LanguageExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add global functions for backward compatibility
|
||||||
|
*/
|
||||||
|
private function addGlobalFunctions(Environment $twig): void
|
||||||
|
{
|
||||||
|
// Add commonly used global variables
|
||||||
|
$twig->addGlobal('bb_cfg', $GLOBALS['bb_cfg'] ?? []);
|
||||||
|
$twig->addGlobal('user', $GLOBALS['user'] ?? null);
|
||||||
|
$twig->addGlobal('userdata', $GLOBALS['userdata'] ?? []);
|
||||||
|
$twig->addGlobal('lang', $GLOBALS['lang'] ?? []);
|
||||||
|
|
||||||
|
// Add TorrentPier configuration functions
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('config', function($key = null) {
|
||||||
|
return $key ? config()->get($key) : config();
|
||||||
|
}));
|
||||||
|
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('lang', function($key = null, $default = '') {
|
||||||
|
return $key ? lang()->get($key, $default) : lang();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add utility functions
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('make_url', 'make_url'));
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('bb_date', 'bb_date'));
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('humn_size', 'humn_size'));
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('profile_url', 'profile_url'));
|
||||||
|
$twig->addFunction(new \Twig\TwigFunction('render_flag', 'render_flag'));
|
||||||
|
|
||||||
|
// Add filters for backward compatibility
|
||||||
|
$twig->addFilter(new \Twig\TwigFilter('htmlspecialchars', 'htmlspecialchars'));
|
||||||
|
$twig->addFilter(new \Twig\TwigFilter('clean_filename', 'clean_filename'));
|
||||||
|
$twig->addFilter(new \Twig\TwigFilter('hide_bb_path', 'hide_bb_path'));
|
||||||
|
$twig->addFilter(new \Twig\TwigFilter('str_short', 'str_short'));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue