Refactor: Replace sidebar with horizontal navigation bar

- Remove sidebar and toggle functionality
- Add Bootstrap navbar with dropdown menus
- Move navigation to top between header and content
- Update menu rendering for Bootstrap dropdowns
- Clean up unused files (header.mustache, sidebar.mustache, sidebar.js)
- Add guide link with book icon in footer
- Simplify layout structure
- Remove duplicate code and fix syntax errors
- Add .gitignore for node_modules and other temp files
This commit is contained in:
Edwin Noorlander 2025-11-21 14:23:41 +01:00
parent 0f1c7234b8
commit a86809c243
87 changed files with 8369 additions and 1353 deletions

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# Dependencies
node_modules/
package-lock.json
# Build outputs
*.log
.DS_Store
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
# Cache
.cache/
.sass-cache/
# Temporary files
*.tmp
*.temp

5
composer.json Normal file
View File

@ -0,0 +1,5 @@
{
"require": {
"mustache/mustache": "^3.0"
}
}

72
composer.lock generated Normal file
View File

@ -0,0 +1,72 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "44b7b2c58a30151ae57314c84e2abdd5",
"packages": [
{
"name": "mustache/mustache",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
},
"time": "2025-06-28T18:28:20+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

BIN
composer.phar Executable file

Binary file not shown.

View File

@ -1,16 +0,0 @@
<?php
return [
'site_title' => 'CodePress',
'site_description' => 'A simple PHP CMS',
'base_url' => '/',
'content_dir' => __DIR__ . '/public/content',
'templates_dir' => __DIR__ . '/templates',
'cache_dir' => __DIR__ . '/cache',
'default_page' => 'home',
'error_404' => '404',
'markdown_enabled' => true,
'php_enabled' => true,
'bootstrap_version' => '5.3.0',
'jquery_version' => '3.7.1'
];

View File

@ -0,0 +1,246 @@
// SCSS variables
$primary: #0d6efd;
$secondary: #6c757d;
$success: #198754;
$info: #0dcaf0;
$warning: #ffc107;
$danger: #dc3545;
$light: #f8f9fa;
$dark: #212529;
// SCSS mixins
@mixin transition($properties...) {
transition: $properties;
}
// Custom styles
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: calc(100vh - 70px);
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
flex-shrink: 0;
@include transition(transform 0.3s ease);
position: fixed;
top: 70px;
left: 0;
height: calc(100vh - 140px);
z-index: 999;
transform: translateX(0);
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: absolute;
top: 15px;
right: 15px;
z-index: 1001;
background: none;
border: none;
cursor: pointer;
@include transition(all 0.3s ease);
font-size: 20px;
color: #6c757d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
&:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
}
.sidebar-toggle-outer {
position: relative;
z-index: 1001;
background-color: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
margin-right: 10px;
color: white !important;
&:hover {
background-color: rgba(255, 255, 255, 0.3);
color: white !important;
}
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content {
margin-left: 0;
}
.sidebar.collapsed .sidebar-toggle-outer {
display: block !important;
}
.sidebar:not(.collapsed) .sidebar-toggle-outer {
display: none !important;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
@include transition(margin-left 0.3s ease);
margin-left: 250px;
}
.folder-toggle {
font-weight: bold;
color: #212529 !important;
cursor: pointer;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
@include transition(background-color 0.2s ease);
&:hover {
background-color: #e9ecef !important;
}
&[aria-expanded=true] {
background-color: #dee2e6 !important;
color: #212529 !important;
font-weight: 600;
}
.arrow {
margin-right: 8px;
@include transition(transform 0.2s);
font-size: 0.8em;
}
&[aria-expanded=true] .arrow {
transform: rotate(90deg);
}
}
.page-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 0.75rem;
padding-left: 2rem;
background-color: #ffffff !important;
margin: 0.125rem 0;
@include transition(background-color 0.2s ease);
&:hover {
color: #212529 !important;
background-color: #f8f9fa !important;
}
&.active {
background-color: #0d6efd !important;
color: #212529 !important;
font-weight: 600;
}
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
@include transition(all 0.2s ease);
&:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
}
.search-form {
max-width: 300px;
}
.card-title a {
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
}
.folder-disabled {
font-weight: 500;
color: #6c757d !important;
cursor: not-allowed;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
opacity: 0.7;
.arrow {
margin-right: 8px;
font-size: 0.8em;
opacity: 0.6;
}
}
@media (max-width: 768px) {
.sidebar {
width: 200px;
}
}

View File

@ -1,16 +1,8 @@
<?php
return [
'site_title' => 'CodePress',
'site_description' => 'A simple PHP CMS',
'base_url' => '/',
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates',
'cache_dir' => __DIR__ . '/../../cache',
'default_page' => 'home',
'error_404' => '404',
'markdown_enabled' => true,
'php_enabled' => true,
'bootstrap_version' => '5.3.0',
'jquery_version' => '3.7.1'
'default_page' => 'home'
];

View File

@ -2,6 +2,52 @@
require_once 'config.php';
// Simple template rendering without Mustache for now
class SimpleTemplate {
public static function render($template, $data) {
// Handle conditional blocks first
foreach ($data as $key => $value) {
if (is_array($value) || (is_string($value) && !empty($value))) {
// Handle {{#key}}...{{/key}} blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match($pattern, $template, $matches)) {
$replacement = $matches[1];
$template = preg_replace($pattern, $replacement, $template);
}
// Handle {{^key}}...{{/key}} blocks (negative condition)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
} else {
// Handle empty blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
// Handle {{^key}}...{{/key}} blocks (show when empty)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match_all($pattern, $template, $matches)) {
foreach ($matches[1] as $match) {
$template = preg_replace('/{{\^' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s', $match, $template, 1);
}
}
}
}
// Handle variable replacements
foreach ($data as $key => $value) {
// Handle triple braces for unescaped HTML content
if (strpos($template, '{{{' . $key . '}}}') !== false) {
$template = str_replace('{{{' . $key . '}}}', $value, $template);
}
// Handle double braces for escaped content
elseif (strpos($template, '{{' . $key . '}}') !== false) {
$template = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $template);
}
}
return $template;
}
}
$config = include 'config.php';
class CodePressCMS {
@ -104,6 +150,16 @@ class CodePressCMS {
return $this->getSearchResults();
}
// Check if guide is requested
if (isset($_GET['guide'])) {
return $this->getGuidePage();
}
// Check if content directory is empty
if ($this->isContentDirEmpty()) {
return $this->getGuidePage();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
@ -348,6 +404,46 @@ private function autoLinkPageTitles($content) {
];
}
private function isContentDirEmpty() {
$contentDir = $this->config['content_dir'];
if (!is_dir($contentDir)) {
return true;
}
$files = scandir($contentDir);
$files = array_diff($files, ['.', '..']);
return empty($files);
}
private function getGuidePage() {
$lang = $this->detectLanguage();
$guideFile = __DIR__ . '/../../guide/' . $lang . '.md';
if (!file_exists($guideFile)) {
$guideFile = __DIR__ . '/../../guide/en.md'; // Fallback to English
}
$content = file_get_contents($guideFile);
$result = $this->parseMarkdown($content);
// Set special title for guide
$result['title'] = 'Handleiding - CodePress CMS';
return $result;
}
private function detectLanguage() {
// Simple language detection based on browser Accept-Language header
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if (strpos($acceptLanguage, 'nl') !== false) {
return 'nl';
}
return 'en'; // Default to English
}
private function getError404() {
return [
'title' => 'Page Not Found',
@ -364,27 +460,59 @@ private function autoLinkPageTitles($content) {
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// Prepare template data
$templateData = [
'site_title' => $this->config['site_title'],
'page_title' => htmlspecialchars($page['title']),
'content' => $page['content'],
'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '',
'menu' => $this->renderMenu($menu),
'breadcrumb' => $breadcrumb,
'default_page' => $this->config['default_page']
];
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
} else {
$templateData['file_info'] = '';
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
// Check if content exists for guide link
$hasContent = !$this->isContentDirEmpty();
$templateData['has_content'] = $hasContent;
$template = str_replace('{{menu}}', $menuHtml, $template);
// Don't show site title link on guide page
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
echo $template;
// Load partials manually
$hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']);
$headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
if (!$hasContent) {
// Remove the link from header when no content
$headerContent = preg_replace('/<a href="[^"]*" class="site-title-link">\s*<h1[^>]*>(.*?)<\/h1>\s*<\/a>/', '<h1 class="h3 mb-0">$1</h1>', $headerContent);
}
$footerContent = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
if (!$hasContent) {
// Remove guide link from footer when no content
$footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent);
}
$partials = [
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache')
];
// Replace partials in template
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$template = str_replace('{{>navigation}}', $partials['navigation'], $template);
$template = str_replace('{{>footer}}', $partials['footer'], $template);
// Render template with data
echo SimpleTemplate::render($template, $templateData);
}
private function getBreadcrumb() {
@ -423,33 +551,27 @@ private function autoLinkPageTitles($content) {
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
// Check if this folder contains the active page
$containsActive = $this->folderContainsActivePage($item['children']);
$ariaExpanded = $containsActive ? 'true' : 'false';
$collapseClass = $containsActive ? 'collapse show' : 'collapse';
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="' . $ariaExpanded . '">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 ' . $collapseClass . '" id="' . $folderId . '">';
$isExpanded = $this->folderContainsActivePage($item['children']);
$html .= '<li class="nav-item dropdown">';
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
$html .= htmlspecialchars($item['title']);
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
$html .= '</li>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<li class="nav-item">';
$html .= '<span class="nav-link text-muted">' . htmlspecialchars($item['title']) . '</span>';
$html .= '</li>';
}
$html .= '</li>';
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
@ -470,7 +592,4 @@ private function autoLinkPageTitles($content) {
}
return false;
}
}
$cms = new CodePressCMS($config);
$cms->render();
}

View File

@ -2,6 +2,52 @@
require_once 'config.php';
// Simple template rendering without Mustache for now
class SimpleTemplate {
public static function render($template, $data) {
// Handle conditional blocks first
foreach ($data as $key => $value) {
if (is_array($value) || (is_string($value) && !empty($value))) {
// Handle {{#key}}...{{/key}} blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match($pattern, $template, $matches)) {
$replacement = $matches[1];
$template = preg_replace($pattern, $replacement, $template);
}
// Handle {{^key}}...{{/key}} blocks (negative condition)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
} else {
// Handle empty blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
// Handle {{^key}}...{{/key}} blocks (show when empty)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match_all($pattern, $template, $matches)) {
foreach ($matches[1] as $match) {
$template = preg_replace('/{{\^' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s', $match, $template, 1);
}
}
}
}
// Handle variable replacements
foreach ($data as $key => $value) {
// Handle triple braces for unescaped HTML content
if (strpos($template, '{{{' . $key . '}}}') !== false) {
$template = str_replace('{{{' . $key . '}}}', $value, $template);
}
// Handle double braces for escaped content
elseif (strpos($template, '{{' . $key . '}}') !== false) {
$template = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $template);
}
}
return $template;
}
}
$config = include 'config.php';
class CodePressCMS {
@ -104,6 +150,16 @@ class CodePressCMS {
return $this->getSearchResults();
}
// Check if guide is requested
if (isset($_GET['guide'])) {
return $this->getGuidePage();
}
// Check if content directory is empty
if ($this->isContentDirEmpty()) {
return $this->getGuidePage();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
@ -261,15 +317,6 @@ private function autoLinkPageTitles($content) {
$content = preg_replace_callback($pattern, $replacement, $content);
}
return $content;
}
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $matches[0] . '</a>';
};
$content = preg_replace_callback($pattern, $replacement, $content);
}
return $content;
}
@ -357,6 +404,46 @@ private function autoLinkPageTitles($content) {
];
}
private function isContentDirEmpty() {
$contentDir = $this->config['content_dir'];
if (!is_dir($contentDir)) {
return true;
}
$files = scandir($contentDir);
$files = array_diff($files, ['.', '..']);
return empty($files);
}
private function getGuidePage() {
$lang = $this->detectLanguage();
$guideFile = __DIR__ . '/../../guide/' . $lang . '.md';
if (!file_exists($guideFile)) {
$guideFile = __DIR__ . '/../../guide/en.md'; // Fallback to English
}
$content = file_get_contents($guideFile);
$result = $this->parseMarkdown($content);
// Set special title for guide
$result['title'] = 'Handleiding - CodePress CMS';
return $result;
}
private function detectLanguage() {
// Simple language detection based on browser Accept-Language header
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if (strpos($acceptLanguage, 'nl') !== false) {
return 'nl';
}
return 'en'; // Default to English
}
private function getError404() {
return [
'title' => 'Page Not Found',
@ -373,27 +460,59 @@ private function autoLinkPageTitles($content) {
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// Prepare template data
$templateData = [
'site_title' => $this->config['site_title'],
'page_title' => htmlspecialchars($page['title']),
'content' => $page['content'],
'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '',
'menu' => $this->renderMenu($menu),
'breadcrumb' => $breadcrumb,
'default_page' => $this->config['default_page']
];
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
} else {
$templateData['file_info'] = '';
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
// Check if content exists for guide link
$hasContent = !$this->isContentDirEmpty();
$templateData['has_content'] = $hasContent;
$template = str_replace('{{menu}}', $menuHtml, $template);
// Don't show site title link on guide page
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
echo $template;
// Load partials manually
$hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']);
$headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
if (!$hasContent) {
// Remove the link from header when no content
$headerContent = preg_replace('/<a href="[^"]*" class="site-title-link">\s*<h1[^>]*>(.*?)<\/h1>\s*<\/a>/', '<h1 class="h3 mb-0">$1</h1>', $headerContent);
}
$footerContent = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
if (!$hasContent) {
// Remove guide link from footer when no content
$footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent);
}
$partials = [
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache')
];
// Replace partials in template
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$template = str_replace('{{>navigation}}', $partials['navigation'], $template);
$template = str_replace('{{>footer}}', $partials['footer'], $template);
// Render template with data
echo SimpleTemplate::render($template, $templateData);
}
private function getBreadcrumb() {
@ -432,21 +551,41 @@ private function autoLinkPageTitles($content) {
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
$isExpanded = $this->folderContainsActivePage($item['children']);
$html .= '<li class="nav-item dropdown">';
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
$html .= htmlspecialchars($item['title']);
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
$html .= '</li>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<li class="nav-item">';
$html .= '<span class="nav-link text-muted">' . htmlspecialchars($item['title']) . '</span>';
$html .= '</li>';
}
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
return $html;
}
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
return $html;
}
$html .= '</li>';
} else {
@ -458,7 +597,19 @@ private function autoLinkPageTitles($content) {
}
return $html;
}
}
$cms = new CodePressCMS($config);
$cms->render();
private function folderContainsActivePage($children) {
foreach ($children as $child) {
if ($child['type'] === 'folder') {
if (!empty($child['children']) && $this->folderContainsActivePage($child['children'])) {
return true;
}
} else {
if (isset($_GET['page']) && $_GET['page'] === $child['path']) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,26 @@
<footer class="bg-light border-top py-3">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<div class="file-info">
<i class="bi bi-file-text"></i>
<span class="page-title" title="{{page_title}}">{{page_title}}</span>
{{#file_info}}
<span class="file-details"> | {{{file_info}}}</span>
{{/file_info}}
</div>
<div class="site-info">
<small class="text-muted">
<a href="?guide" class="guide-link" title="Handleiding">
<i class="bi bi-book"></i>
</a>
<span class="ms-2">|</span>
Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a>
</small>
</div>
</div>
</div>
</div>
</div>
</footer>

View File

@ -0,0 +1,23 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="?page={{default_page}}">
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
{{site_title}}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{{{menu}}}
</ul>
<form class="d-flex" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</nav>

View File

@ -1,440 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="/engine/assets/favicon.svg">
<link href="/engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: calc(100vh - 70px); /* Minus header height */
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
flex-shrink: 0;
transition: transform 0.3s ease;
position: fixed;
top: 70px;
left: 0;
height: calc(100vh - 140px); /* 70px header + 70px footer */
z-index: 999;
transform: translateX(0);
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: absolute;
top: 15px;
right: 15px;
z-index: 1001;
background: none;
border: none;
cursor: pointer;
transition: all 0.3s ease;
font-size: 20px;
color: #6c757d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.sidebar-toggle:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
.sidebar-toggle-inner {
/* Toggle inside sidebar */
}
.sidebar-toggle-outer {
position: fixed;
top: 90px;
left: 20px;
z-index: 1001;
background-color: white;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content .sidebar-toggle-outer {
display: block !important;
}
.sidebar:not(.collapsed) ~ .main-content .sidebar-toggle-outer {
display: none !important;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: all 0.3s ease;
margin-left: 250px;
}
.sidebar.collapsed ~ .main-content {
margin-left: 0;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: margin-left 0.3s ease;
}
.folder-toggle {
font-weight: bold;
color: #212529 !important;
cursor: pointer;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
.folder-toggle:hover {
background-color: #e9ecef !important;
}
.folder-toggle[aria-expanded="true"] {
background-color: #dee2e6 !important;
color: #212529 !important;
font-weight: 600;
}
/* Progressive background colors for nested folders */
.nav .nav .folder-toggle {
background-color: #f1f3f4 !important;
}
.nav .nav .nav .folder-toggle {
background-color: #eaedee !important;
}
.nav .nav .nav .nav .folder-toggle {
background-color: #e3e7e8 !important;
}
.nav .nav .nav .nav .nav .folder-toggle {
background-color: #dce1e2 !important;
}
.nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #d5dbdd !important;
}
.nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #ced5d8 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c7cfd3 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c0c9ce !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #b9c3c9 !important;
}
.folder-toggle .arrow {
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.folder-toggle[aria-expanded="true"] .arrow {
transform: rotate(90deg);
}
.page-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 0.75rem;
padding-left: 2rem;
background-color: #ffffff !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
/* Progressive background colors for nested pages */
.nav .nav .page-link {
background-color: #fafbfc !important;
}
.nav .nav .nav .page-link {
background-color: #f5f7f8 !important;
}
.nav .nav .nav .nav .page-link {
background-color: #f0f3f4 !important;
}
.nav .nav .nav .nav .nav .page-link {
background-color: #ebefef !important;
}
.nav .nav .nav .nav .nav .nav .page-link {
background-color: #e6eaea !important;
}
.nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #e1e5e5 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #dce0e0 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d7dbdb !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d2d6d6 !important;
}
.page-link:hover {
color: #212529 !important;
background-color: #f8f9fa !important;
}
.nav-link.active {
background-color: #0d6efd !important;
color: #212529 !important;
font-weight: 600;
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
.search-form {
max-width: 300px;
}
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
.folder-disabled {
font-weight: 500;
color: #6c757d !important;
cursor: not-allowed;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
opacity: 0.7;
}
.folder-disabled .arrow {
margin-right: 8px;
font-size: 0.8em;
opacity: 0.6;
}
@media (max-width: 768px) {
.sidebar {
width: 200px;
}
}
</style>
</head>
<body>
<header class="bg-primary text-white py-3">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<div class="d-flex align-items-center">
<img src="/engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1>
</div>
</div>
<div class="col-auto">
<form class="d-flex" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</div>
</header>
<div class="main-wrapper">
<div class="content-wrapper">
<nav class="sidebar" id="sidebar">
<div class="sidebar-toggle sidebar-toggle-inner" id="sidebarToggleInner">
<i class="bi bi-list"></i>
</div>
<div class="pt-3">
<ul class="nav flex-column">
{{menu}}
</ul>
</div>
</nav>
<main class="main-content">
<div class="sidebar-toggle sidebar-toggle-outer" id="sidebarToggleOuter" style="display: none;">
<i class="bi bi-list"></i>
</div>
<div>
{{breadcrumb}}
</div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{{page_title}}</h2>
</div>
<div class="content">
{{content}}
</div>
</main>
</div>
</div>
<footer class="bg-light border-top py-3" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 998;">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<div class="file-info">
{{file_info}}
</div>
<div class="site-info">
<small class="text-muted">Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a></small>
</div>
</div>
</div>
</div>
</div>
</footer>
<script src="/engine/assets/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggleInner = document.getElementById('sidebarToggleInner');
const sidebarToggleOuter = document.getElementById('sidebarToggleOuter');
const sidebar = document.getElementById('sidebar');
// Initialize sidebar state (open by default)
sidebar.classList.remove('collapsed');
const innerIcon = sidebarToggleInner.querySelector('i');
const outerIcon = sidebarToggleOuter.querySelector('i');
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
// Change icons
if (sidebar.classList.contains('collapsed')) {
innerIcon.classList.remove('bi-x');
innerIcon.classList.add('bi-list');
outerIcon.classList.remove('bi-x');
outerIcon.classList.add('bi-list');
} else {
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
}
}
sidebarToggleInner.addEventListener('click', toggleSidebar);
sidebarToggleOuter.addEventListener('click', toggleSidebar);
// Folders are now automatically expanded by PHP if they contain the active page
// Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target');
const targetCollapse = document.querySelector(targetId);
const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded && targetCollapse) {
// Close all other folders first
folderToggles.forEach(otherToggle => {
if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) {
otherCollapse.classList.remove('show');
otherToggle.setAttribute('aria-expanded', 'false');
// Reset arrow
const otherArrow = otherToggle.querySelector('.arrow');
if (otherArrow) {
otherArrow.style.transform = '';
}
}
}
}
});
// Open this folder
targetCollapse.classList.add('show');
this.setAttribute('aria-expanded', 'true');
// Rotate arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = 'rotate(90deg)';
}
} else if (isExpanded && targetCollapse) {
// Close this folder
targetCollapse.classList.remove('show');
this.setAttribute('aria-expanded', 'false');
// Reset arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = '';
}
}
});
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
<link href="/assets/css/style.css" rel="stylesheet">
</head>
<body>
{{>header}}
<div class="main-wrapper">
{{>navigation}}
<main class="main-content">
<div class="content-inner">
<div>
{{{breadcrumb}}}
</div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{{page_title}}</h2>
</div>
<div class="content">
{{{content}}}
</div>
</div>
</main>
</div>
{{>footer}}
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/sidebar.js"></script>
<script src="/assets/js/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,3 @@
{{#content}}
<p>{{{.}}}</p>
{{/content}}

View File

@ -0,0 +1 @@
{{{content}}}

171
guide/en.md Normal file
View File

@ -0,0 +1,171 @@
# CodePress CMS Guide
## Welcome to CodePress CMS
CodePress is a lightweight, file-based Content Management System built with PHP and Bootstrap.
### Table of Contents
1. [Getting Started](#getting-started)
2. [Content Management](#content-management)
3. [Templates](#templates)
4. [Configuration](#configuration)
---
## Getting Started
### Requirements
- PHP 8.4+
- Web server (Apache/Nginx)
- Modern web browser
### Installation
1. Clone or download the CodePress files
2. Upload to your web server
3. Make sure the `content/` directory is writable
4. Navigate to your website in the browser
### Basic Configuration
The most important settings are in `engine/core/config.php`:
```php
$config = [
'site_title' => 'My Website',
'default_page' => 'home',
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates'
];
```
---
## Content Management
### File Structure
```
content/
├── home.md # Home page
├── blog/
│ ├── index.md # Blog overview
│ ├── article-1.md # Blog article
│ └── category/
│ └── article.md # Article in category
└── about-us/
└── info.md # About us page
```
### Content Types
CodePress supports three content types:
#### Markdown (`.md`)
```markdown
# Page Title
This is the page content in **Markdown** format.
## Subsection
- List item 1
- List item 2
```
#### PHP (`.php`)
```php
<?php
$title = "Dynamic Page";
?>
<h1><?php echo $title; ?></h1>
<p>This is dynamic content with PHP.</p>
```
#### HTML (`.html`)
```html
<h1>HTML Page</h1>
<p>This is static HTML content.</p>
```
### Automatic Linking
CodePress automatically creates links to other pages when you mention page names in your content.
---
## Templates
### Template Structure
CodePress uses Mustache-compatible templates:
- `layout.mustache` - Main template
- `assets/header.mustache` - Header component
- `assets/sidebar.mustache` - Sidebar navigation
- `assets/footer.mustache` - Footer component
### Template Variables
Available variables in templates:
- `{{site_title}}` - Website title
- `{{page_title}}` - Current page title
- `{{content}}` - Page content
- `{{menu}}` - Navigation menu
- `{{breadcrumb}}` - Breadcrumb navigation
---
## Configuration
### Basic Settings
Edit `engine/core/config.php` for your website:
```php
$config = [
'site_title' => 'Your Website Name',
'default_page' => 'home', // Default start page
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates'
];
```
### SEO Friendly URLs
CodePress automatically generates clean URLs:
- `home.md``/home`
- `blog/article.md``/blog/article`
### Search Functionality
The built-in search function searches through:
- File names
- Content of Markdown/PHP/HTML files
---
## Tips and Tricks
### Page Organization
- Use subdirectories for categories
- Give each directory an `index.md` for an overview page
- Keep file names short and descriptive
### Content Optimization
- Use clear headings (H1, H2, H3)
- Add descriptive meta information
- Use internal links for better navigation
### Security
- Keep your CodePress installation updated
- Restrict write permissions on the `content/` directory
- Use HTTPS when possible
---
## Support
### Troubleshooting
- **Empty pages**: Check file permissions
- **Template errors**: Verify template syntax
- **404 errors**: Check file names and paths
### More Information
- Documentation: [CodePress GitHub](https://git.noorlander.info/E.Noorlander/CodePress.git)
- Issues and feature requests: GitHub Issues
---
*This guide is part of CodePress CMS and is automatically displayed when no content is available.*

171
guide/nl.md Normal file
View File

@ -0,0 +1,171 @@
# CodePress CMS Handleiding
## Welkom bij CodePress CMS
CodePress is een lichtgewicht, op bestanden gebaseerd Content Management Systeem gebouwd met PHP en Bootstrap.
### Inhoudsopgave
1. [Getting Started](#getting-started)
2. [Content Management](#content-management)
3. [Templates](#templates)
4. [Configuration](#configuration)
---
## Getting Started
### Vereisten
- PHP 8.4+
- Webserver (Apache/Nginx)
- Modern web browser
### Installatie
1. Clone of download de CodePress bestanden
2. Upload naar je webserver
3. Zorg dat de `content/` map schrijfbaar is
4. Navigeer naar je website in de browser
### Basis Configuratie
De belangrijkste instellingen vind je in `engine/core/config.php`:
```php
$config = [
'site_title' => 'Mijn Website',
'default_page' => 'home',
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates'
];
```
---
## Content Management
### Bestandsstructuur
```
content/
├── home.md # Home pagina
├── blog/
│ ├── index.md # Blog overzicht
│ ├── artikel-1.md # Blog artikel
│ └── categorie/
│ └── artikel.md # Artikel in categorie
└── over-ons/
└── info.md # Over ons pagina
```
### Content Types
CodePress ondersteunt drie content types:
#### Markdown (`.md`)
```markdown
# Pagina Titel
Dit is de inhoud van de pagina in **Markdown** formaat.
## Subsectie
- Lijst item 1
- Lijst item 2
```
#### PHP (`.php`)
```php
<?php
$title = "Dynamische Pagina";
?>
<h1><?php echo $title; ?></h1>
<p>Dit is dynamische content met PHP.</p>
```
#### HTML (`.html`)
```html
<h1>HTML Pagina</h1>
<p>Dit is statische HTML content.</p>
```
### Automatische Linking
CodePress maakt automatisch links naar andere pagina's wanneer je paginanamen in je content noemt.
---
## Templates
### Template Structuur
CodePress gebruikt Mustache-compatible templates:
- `layout.mustache` - Hoofdtemplate
- `assets/header.mustache` - Header component
- `assets/sidebar.mustache` - Sidebar navigatie
- `assets/footer.mustache` - Footer component
### Template Variabelen
Beschikbare variabelen in templates:
- `{{site_title}}` - Website titel
- `{{page_title}}` - Huidige pagina titel
- `{{content}}` - Pagina inhoud
- `{{menu}}` - Navigatie menu
- `{{breadcrumb}}` - Broodkruimel navigatie
---
## Configuration
### Basis Instellingen
Pas `engine/core/config.php` aan voor jouw website:
```php
$config = [
'site_title' => 'Jouw Website Naam',
'default_page' => 'home', // Standaard startpagina
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates'
];
```
### SEO Vriendelijke URLs
CodePress genereert automatisch schone URLs:
- `home.md``/home`
- `blog/artikel.md``/blog/artikel`
### Zoekfunctionaliteit
De ingebouwde zoekfunctie doorzoekt:
- Bestandsnamen
- Content van Markdown/PHP/HTML bestanden
---
## Tips en Tricks
### Pagina Organisatie
- Gebruik submappen voor categoriën
- Geef elke map een `index.md` voor een overzichtspagina
- Houd bestandsnamen kort en beschrijvend
### Content Optimalisatie
- Gebruik duidelijke koppen (H1, H2, H3)
- Voeg beschrijvende meta-informatie toe
- Gebruik interne links voor betere navigatie
### Veiligheid
- Houd je CodePress installatie bijgewerkt
- Beperk schrijfrechten op de `content/` map
- Gebruik HTTPS indien mogelijk
---
## Ondersteuning
### Problemen Oplossen
- **Lege pagina's**: Controleer bestandsrechten
- **Template fouten**: Verifieer template syntax
- **404 fouten**: Controleer bestandsnamen en paden
### Meer Informatie
- Documentatie: [CodePress GitHub](https://git.noorlander.info/E.Noorlander/CodePress.git)
- Issues en feature requests: GitHub Issues
---
*Deze handleiding is onderdeel van CodePress CMS en wordt automatisch getoond wanneer er geen content beschikbaar is.*

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "codepress",
"version": "1.0.0",
"description": "A lightweight, file-based Content Management System built with PHP and Bootstrap.",
"main": "index.js",
"scripts": {
"build:css": "npx sass engine/assets/css/style.scss public/assets/css/style.css --no-source-map",
"watch:css": "npx sass engine/assets/css/style.scss public/assets/css/style.css --no-source-map --watch",
"build": "npm run build:css",
"clean": "rm -rf node_modules package-lock.json",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.noorlander.info/E.Noorlander/CodePress.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"sass": "^1.94.2"
}
}

316
public/assets/css/style.css Normal file
View File

@ -0,0 +1,316 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-wrapper {
flex: 1;
display: flex;
position: relative;
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto; /* Only sidebar scrolls when needed */
flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
height: 100%;
z-index: 999;
transform: translateX(0);
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: absolute;
top: 15px;
right: 15px;
z-index: 1001;
background: none;
border: none;
cursor: pointer;
transition: all 0.3s ease;
font-size: 20px;
color: #6c757d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.sidebar-toggle:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
.sidebar-toggle-outer {
position: relative;
z-index: 1001;
background-color: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
margin-right: 10px;
color: white !important;
display: none; /* Hidden by default */
}
.sidebar-toggle-outer:hover {
background-color: rgba(255, 255, 255, 0.3);
color: white !important;
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content {
margin-left: 0;
}
body.sidebar-collapsed .sidebar-toggle-outer {
display: block !important;
}
body:not(.sidebar-collapsed) .sidebar-toggle-outer {
display: none !important;
}
/* Override inline styles */
.sidebar-toggle-outer[style*="display: none"] {
display: none !important;
}
body.sidebar-collapsed .sidebar-toggle-outer[style*="display: none"] {
display: block !important;
}
.main-content {
flex: 1;
overflow: hidden; /* Main content container doesn't scroll */
transition: margin-left 0.3s ease;
margin-left: 250px;
height: 100%;
display: flex;
flex-direction: column;
}
.content-inner {
flex: 1;
overflow-y: auto; /* Only content-inner scrolls when needed */
padding: 20px;
}
.folder-toggle {
font-weight: bold;
color: #212529 !important;
cursor: pointer;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
.folder-toggle:hover {
background-color: #e9ecef !important;
}
.folder-toggle[aria-expanded=true] {
background-color: #dee2e6 !important;
color: #212529 !important;
font-weight: 600;
}
.folder-toggle .arrow {
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.folder-toggle[aria-expanded=true] .arrow {
transform: rotate(90deg);
}
.page-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 0.75rem;
padding-left: 2rem;
background-color: #ffffff !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
.page-link:hover {
color: #212529 !important;
background-color: #f8f9fa !important;
}
.page-link.active {
background-color: rgba(13, 110, 253, 0.1) !important;
color: #0d6efd !important;
font-weight: 600;
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
flex: 1;
min-width: 0; /* Important for text truncation in flexbox */
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
flex: 1;
min-width: 0; /* Important for text truncation in flexbox */
}
.file-info i {
margin-right: 5px;
}
.page-title {
color: #6c757d;
max-width: 250px !important;
display: inline-block !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
vertical-align: middle;
margin-right: 10px;
}
.page-title:hover {
color: #495057;
cursor: default;
}
.file-details {
color: #6c757d;
font-size: 0.85rem;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.guide-link {
color: white !important;
text-decoration: none;
font-size: 0.9rem;
&:hover {
color: rgba(255, 255, 255, 0.8) !important;
text-decoration: none;
}
i {
font-size: 1rem;
}
}
.site-title-link {
color: white !important;
text-decoration: none;
&:hover {
color: rgba(255, 255, 255, 0.8) !important;
text-decoration: none;
}
h1 {
margin: 0;
transition: color 0.2s ease;
}
}
footer {
margin-top: auto; /* Push footer to bottom */
flex-shrink: 0;
}
.site-info a:hover {
text-decoration: underline;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
.search-form {
max-width: 300px;
}
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
.folder-disabled {
font-weight: 500;
color: #6c757d !important;
cursor: not-allowed;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
opacity: 0.7;
}
.folder-disabled .arrow {
margin-right: 8px;
font-size: 0.8em;
opacity: 0.6;
}
@media (max-width: 768px) {
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto; /* Only sidebar scrolls when needed */
flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
height: 100%;
z-index: 999;
transform: translateX(0);
}
}

View File

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAOJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKJ;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;;;AAOJ;EACI;;;AAKZ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;;;AAQI;EACI;;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AAMhB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;;;AAQI;EACI;;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AAMhB;EACI;EACA;;;AAIA;EACI;EACA;;AAEA;EACI;;;AAKZ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;;AAIR;EACI;;;AAIA;EACI;EACA;;AAEA;EACI;;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;;AAIR;EACI;IACI","file":"style.css"}

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

7
public/assets/js/app.js Normal file
View File

@ -0,0 +1,7 @@
// Main application JavaScript
// This file contains general application functionality
// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
console.log('CodePress CMS initialized');
});

View File

@ -1 +0,0 @@
../engine

View File

@ -1,442 +1,10 @@
<?php
require_once __DIR__ . '/../engine/core/config.php';
require_once __DIR__ . '/../engine/core/index.php';
$config = include __DIR__ . '/../engine/core/config.php';
class CodePressCMS {
private $config;
private $menu = [];
private $searchResults = [];
public function __construct($config) {
$this->config = $config;
$this->buildMenu();
if (isset($_GET['search'])) {
$this->performSearch($_GET['search']);
}
}
private function buildMenu() {
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
}
private function scanDirectory($dir, $prefix) {
if (!is_dir($dir)) return [];
$items = scandir($dir);
sort($items);
$result = [];
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$result[] = [
'type' => 'folder',
'title' => ucfirst($item),
'path' => $relativePath,
'children' => $this->scanDirectory($path, $relativePath)
];
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$result[] = [
'type' => 'file',
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath
];
}
}
return $result;
}
private function performSearch($query) {
$this->searchResults = [];
$this->searchInDirectory($this->config['content_dir'], '', $query);
}
private function searchInDirectory($dir, $prefix, $query) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->searchInDirectory($path, $relativePath, $query);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$content = file_get_contents($path);
if (stripos($content, $query) !== false || stripos($item, $query) !== false) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$this->searchResults[] = [
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath,
'snippet' => $this->createSnippet($content, $query)
];
}
}
}
}
private function createSnippet($content, $query) {
$content = strip_tags($content);
$pos = stripos($content, $query);
if ($pos === false) return substr($content, 0, 100) . '...';
$start = max(0, $pos - 50);
$snippet = substr($content, $start, 150);
return '...' . $snippet . '...';
}
public function getPage() {
if (isset($_GET['search'])) {
return $this->getSearchResults();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
$filePath = $this->config['content_dir'] . '/' . $page;
$actualFilePath = null;
if (file_exists($filePath . '.md')) {
$actualFilePath = $filePath . '.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '.php')) {
$actualFilePath = $filePath . '.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '.html')) {
$actualFilePath = $filePath . '.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} elseif (is_dir($filePath)) {
// Check for index files in directory
if (file_exists($filePath . '/index.md')) {
$actualFilePath = $filePath . '/index.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '/index.php')) {
$actualFilePath = $filePath . '/index.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '/index.html')) {
$actualFilePath = $filePath . '/index.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} else {
// Generate directory listing
return $this->generateDirectoryListing($filePath, $page);
}
} elseif (file_exists($filePath)) {
$actualFilePath = $filePath;
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if ($extension === 'md') {
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif ($extension === 'php') {
$result = $this->parsePHP($actualFilePath);
} elseif ($extension === 'html') {
$result = $this->parseHTML(file_get_contents($actualFilePath));
}
}
if (isset($result) && $actualFilePath) {
$result['file_info'] = $this->getFileInfo($actualFilePath);
return $result;
}
return $this->getError404();
}
private function getFileInfo($filePath) {
if (!file_exists($filePath)) {
return null;
}
$stats = stat($filePath);
$created = date('d-m-Y H:i', $stats['ctime']);
$modified = date('d-m-Y H:i', $stats['mtime']);
return [
'created' => $created,
'modified' => $modified,
'size' => $this->formatFileSize($stats['size'])
];
}
private function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
private function generateDirectoryListing($dirPath, $urlPath) {
$title = ucfirst(basename($dirPath));
$content = '<div class="row">';
$items = scandir($dirPath);
sort($items);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dirPath . '/' . $item;
$relativePath = $urlPath . '/' . $item;
$itemName = ucfirst(pathinfo($item, PATHINFO_FILENAME));
if (is_dir($path)) {
$content .= '<div class="col-md-6 mb-4">';
$content .= '<div class="card h-100 border-0 rounded-0 bg-light">';
$content .= '<div class="card-body">';
$content .= '<h3 class="h5 card-title"><a href="?page=' . $relativePath . '" class="text-decoration-none text-dark"><i class="bi bi-folder me-2"></i> ' . $itemName . '</a></h3>';
$content .= '</div></div></div>';
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
// Remove extension from URL for cleaner links
$cleanPath = preg_replace('/\.[^.]+$/', '', $relativePath);
// Get preview content
$preview = '';
$fileContent = file_get_contents($path);
// Extract title if possible
$fileTitle = $itemName;
if (preg_match('/^#\s+(.+)$/m', $fileContent, $matches)) {
$fileTitle = trim($matches[1]);
}
// Extract preview text (first paragraph)
$fileContent = strip_tags($this->parseMarkdown($fileContent)['content']);
$preview = substr($fileContent, 0, 150) . '...';
$content .= '<div class="col-md-6 mb-4">';
$content .= '<div class="card h-100 border rounded-0">';
$content .= '<div class="card-body">';
$content .= '<h3 class="h5 card-title"><a href="?page=' . $cleanPath . '" class="text-decoration-none text-primary">' . $fileTitle . '</a></h3>';
$content .= '<p class="card-text text-muted small">' . $preview . '</p>';
$content .= '<a href="?page=' . $cleanPath . '" class="btn btn-sm btn-outline-primary rounded-0">Lees meer</a>';
$content .= '</div></div></div>';
}
}
$content .= '</div>';
return [
'title' => $title,
'content' => $content
];
}
private function getSearchResults() {
$query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
if (empty($this->searchResults)) {
$content .= '<p>No results found.</p>';
} else {
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
foreach ($this->searchResults as $result) {
$content .= '<div class="card mb-3">';
$content .= '<div class="card-body">';
$content .= '<h5 class="card-title"><a href="' . htmlspecialchars($result['url']) . '">' . htmlspecialchars($result['title']) . '</a></h5>';
$content .= '<p class="card-text text-muted">' . htmlspecialchars($result['path']) . '</p>';
$content .= '<p class="card-text">' . htmlspecialchars($result['snippet']) . '</p>';
$content .= '</div></div>';
}
}
return [
'title' => 'Search Results',
'content' => $content
];
}
private function parseMarkdown($content) {
$lines = explode("\n", $content);
$title = '';
$body = '';
$inBody = false;
foreach ($lines as $line) {
if (!$inBody && preg_match('/^#\s+(.+)$/', $line, $matches)) {
$title = $matches[1];
$inBody = true;
} elseif ($inBody || trim($line) !== '') {
$body .= $line . "\n";
$inBody = true;
}
}
$body = preg_replace('/### (.+)/', '<h3>$1</h3>', $body);
$body = preg_replace('/## (.+)/', '<h2>$1</h2>', $body);
$body = preg_replace('/# (.+)/', '<h1>$1</h1>', $body);
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $body);
// Convert Markdown links to HTML links
$body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body);
// Convert relative internal links to CMS format
$body = preg_replace('/href="\/blog\/([^"]+)"/', 'href="?page=blog/$1"', $body);
$body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body);
$body = preg_replace('/\n\n/', '</p><p>', $body);
$body = '<p>' . $body . '</p>';
$body = preg_replace('/<p><\/p>/', '', $body);
$body = preg_replace('/<p>(<h[1-6]>)/', '$1', $body);
$body = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $body);
return [
'title' => $title ?: 'Untitled',
'content' => $body
];
}
private function parsePHP($filePath) {
ob_start();
$title = 'Untitled';
include $filePath;
$content = ob_get_clean();
return [
'title' => $title,
'content' => $content
];
}
private function parseHTML($content) {
$title = 'Untitled';
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
} elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
}
return [
'title' => $title,
'content' => $content
];
}
private function getError404() {
return [
'title' => 'Page Not Found',
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
];
}
public function getMenu() {
return $this->menu;
}
public function render() {
$page = $this->getPage();
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
$template = str_replace('{{menu}}', $menuHtml, $template);
echo $template;
}
private function getBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
}
$parts = explode('/', $page);
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
$path = '';
foreach ($parts as $i => $part) {
$path .= ($path ? '/' : '') . $part;
$title = ucfirst($part);
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
} else {
// Check if directory has index file
$dirPath = $this->config['content_dir'] . '/' . $path;
$hasIndex = file_exists($dirPath . '/index.md') || file_exists($dirPath . '/index.php') || file_exists($dirPath . '/index.html');
// Always make breadcrumb items clickable, CMS will generate index if missing
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
}
}
$breadcrumb .= '</ol></nav>';
return $breadcrumb;
}
private function renderMenu($items, $level = 0) {
$html = '';
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
}
$html .= '</li>';
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
return $html;
}
}
// Block direct access to content files
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($requestUri, '/content/') !== false) {

View File

@ -1 +0,0 @@
[Wed Nov 19 17:58:28 2025] Failed to listen on localhost:8080 (reason: Address already in use)

View File

@ -1,381 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
z-index: 1000;
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: fixed;
top: 80px;
left: 10px;
z-index: 1001;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 5px;
padding: 8px 12px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.sidebar-toggle.shifted {
left: 270px;
}
.main-content.shifted {
margin-left: 0;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: margin-left 0.3s ease;
}
.folder-toggle {
font-weight: bold;
color: #212529 !important;
cursor: pointer;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
.folder-toggle:hover {
background-color: #e9ecef !important;
}
.folder-toggle[aria-expanded="true"] {
background-color: #dee2e6 !important;
color: #212529 !important;
font-weight: 600;
}
/* Progressive background colors for nested folders */
.nav .nav .folder-toggle {
background-color: #f1f3f4 !important;
}
.nav .nav .nav .folder-toggle {
background-color: #eaedee !important;
}
.nav .nav .nav .nav .folder-toggle {
background-color: #e3e7e8 !important;
}
.nav .nav .nav .nav .nav .folder-toggle {
background-color: #dce1e2 !important;
}
.nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #d5dbdd !important;
}
.nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #ced5d8 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c7cfd3 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c0c9ce !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #b9c3c9 !important;
}
.folder-toggle .arrow {
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.folder-toggle[aria-expanded="true"] .arrow {
transform: rotate(90deg);
}
.page-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 0.75rem;
padding-left: 2rem;
background-color: #ffffff !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
/* Progressive background colors for nested pages */
.nav .nav .page-link {
background-color: #fafbfc !important;
}
.nav .nav .nav .page-link {
background-color: #f5f7f8 !important;
}
.nav .nav .nav .nav .page-link {
background-color: #f0f3f4 !important;
}
.nav .nav .nav .nav .nav .page-link {
background-color: #ebefef !important;
}
.nav .nav .nav .nav .nav .nav .page-link {
background-color: #e6eaea !important;
}
.nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #e1e5e5 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #dce0e0 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d7dbdb !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d2d6d6 !important;
}
.page-link:hover {
color: #212529 !important;
background-color: #f8f9fa !important;
}
.nav-link.active {
background-color: #0d6efd !important;
color: #212529 !important;
font-weight: 600;
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
.search-form {
max-width: 300px;
}
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
.folder-disabled {
font-weight: 500;
color: #6c757d !important;
cursor: not-allowed;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
opacity: 0.7;
}
.folder-disabled .arrow {
margin-right: 8px;
font-size: 0.8em;
opacity: 0.6;
}
@media (max-width: 768px) {
.sidebar {
width: 200px;
}
}
</style>
</head>
<body>
<header class="bg-primary text-white py-3">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<div class="d-flex align-items-center">
<img src="assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1>
</div>
</div>
<div class="col-auto">
<form class="d-flex" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</div>
</header>
<div class="main-wrapper">
<div class="content-wrapper">
<button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-list"></i>
</button>
<nav class="sidebar" id="sidebar">
<div class="pt-3">
<ul class="nav flex-column">
{{menu}}
</ul>
</div>
</nav>
<main class="main-content">
<div>
{{breadcrumb}}
</div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{{page_title}}</h2>
</div>
<div class="content">
{{content}}
</div>
</main>
</div>
</div>
<footer class="bg-light border-top py-3">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<div class="file-info">
{{file_info}}
</div>
<div class="site-info">
<small class="text-muted">Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a></small>
</div>
</div>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const mainContent = document.querySelector('.main-content');
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('shifted');
sidebarToggle.classList.toggle('shifted');
// Change icon
const icon = this.querySelector('i');
if (sidebar.classList.contains('collapsed')) {
icon.classList.remove('bi-list');
icon.classList.add('bi-chevron-right');
} else {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-list');
}
});
// Open folders that contain the current active page
const activeLink = document.querySelector('.nav-link.active');
if (activeLink) {
let parent = activeLink.closest('.collapse');
while (parent) {
const toggle = document.querySelector('[data-bs-target="#' + parent.id + '"]');
if (toggle) {
const collapse = new bootstrap.Collapse(parent, {
show: true
});
toggle.setAttribute('aria-expanded', 'true');
}
parent = parent.parentElement.closest('.collapse');
}
}
// Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target');
const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
// Close all other folders
folderToggles.forEach(otherToggle => {
if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) {
const bsCollapse = bootstrap.Collapse.getInstance(otherCollapse);
if (bsCollapse) {
bsCollapse.hide();
} else {
new bootstrap.Collapse(otherCollapse, {
hide: true
});
}
otherToggle.setAttribute('aria-expanded', 'false');
}
}
}
});
}
});
});
});
</script>
</body>
</html>

22
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c::getLoader();

579
vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

396
vendor/composer/InstalledVersions.php vendored Normal file
View File

@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

43
vendor/composer/autoload_classmap.php vendored Normal file
View File

@ -0,0 +1,43 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Mustache_Cache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_AbstractCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_FilesystemCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_NoopCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Compiler' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Context' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Engine' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_InvalidArgumentException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_LogicException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_RuntimeException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_SyntaxException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownFilterException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownHelperException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownTemplateException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_HelperCollection' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_LambdaHelper' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ArrayLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_CascadingLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_FilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_InlineLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_MutableLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ProductionFilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_StringLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger_AbstractLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger_StreamLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Parser' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Source' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Source_FilesystemSource' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Template' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Tokenizer' => $vendorDir . '/mustache/mustache/src/compat.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

10
vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Mustache\\' => array($vendorDir . '/mustache/mustache/src'),
);

38
vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit071586d19f5409de22b3235d85d8476c::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

69
vendor/composer/autoload_static.php vendored Normal file
View File

@ -0,0 +1,69 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit071586d19f5409de22b3235d85d8476c
{
public static $prefixLengthsPsr4 = array (
'M' =>
array (
'Mustache\\' => 9,
),
);
public static $prefixDirsPsr4 = array (
'Mustache\\' =>
array (
0 => __DIR__ . '/..' . '/mustache/mustache/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Mustache_Cache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_AbstractCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_FilesystemCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_NoopCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Compiler' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Context' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Engine' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_InvalidArgumentException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_LogicException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_RuntimeException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_SyntaxException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownFilterException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownHelperException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownTemplateException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_HelperCollection' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_LambdaHelper' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ArrayLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_CascadingLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_FilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_InlineLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_MutableLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ProductionFilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_StringLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger_AbstractLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger_StreamLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Parser' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Source' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Source_FilesystemSource' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Template' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Tokenizer' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit071586d19f5409de22b3235d85d8476c::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit071586d19f5409de22b3235d85d8476c::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit071586d19f5409de22b3235d85d8476c::$classMap;
}, null, ClassLoader::class);
}
}

62
vendor/composer/installed.json vendored Normal file
View File

@ -0,0 +1,62 @@
{
"packages": [
{
"name": "mustache/mustache",
"version": "v3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"time": "2025-06-28T18:28:20+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
},
"install-path": "../mustache/mustache"
}
],
"dev": true,
"dev-package-names": []
}

32
vendor/composer/installed.php vendored Normal file
View File

@ -0,0 +1,32 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '0f1c7234b8e213130e58252a3aa58a58290c959e',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '0f1c7234b8e213130e58252a3aa58a58290c959e',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'mustache/mustache' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
'reference' => '176b6b21d68516dd5107a63ab71b0050e518b7a4',
'type' => 'library',
'install_path' => __DIR__ . '/../mustache/mustache',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

25
vendor/composer/platform_check.php vendored Normal file
View File

@ -0,0 +1,25 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 50600)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 5.6.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}

View File

@ -0,0 +1,47 @@
name: Tests
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php:
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
- 7.4
- 8.0
- 8.1
- 8.2
- 8.3
- 8.4
name: PHP ${{ matrix.php }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run tests
run: vendor/bin/phpunit

View File

@ -0,0 +1,20 @@
<?php
use PhpCsFixer\Config;
$config = new Config();
$config->setRules([
'@Symfony' => true,
'binary_operator_spaces' => false,
'concat_space' => ['spacing' => 'one'],
'increment_style' => false,
'single_line_throw' => false,
'yoda_style' => false,
]);
$finder = $config->getFinder()
->in('src')
->in('test');
return $config;

21
vendor/mustache/mustache/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2010-2025 Justin Hileman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

94
vendor/mustache/mustache/README.md vendored Normal file
View File

@ -0,0 +1,94 @@
# Mustache.php
A [Mustache][mustache] implementation in PHP.
[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg?style=flat-square)][packagist]
[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg?style=flat-square)][packagist]
## Installation
```
composer require mustache/mustache
```
## Usage
A quick example:
```php
<?php
$m = new \Mustache\Engine(['entity_flags' => ENT_QUOTES]);
echo $m->render('Hello {{planet}}', ['planet' => 'World!']); // "Hello World!"
```
And a more in-depth example -- this is the canonical Mustache template:
```html+jinja
Hello {{name}}
You have just won {{value}} dollars!
{{#in_ca}}
Well, {{taxed_value}} dollars, after taxes.
{{/in_ca}}
```
Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well:
```php
<?php
class Chris {
public $name = "Chris";
public $value = 10000;
public function taxed_value() {
return $this->value - ($this->value * 0.4);
}
public $in_ca = true;
}
```
And render it:
```php
<?php
$m = new \Mustache\Engine(['entity_flags' => ENT_QUOTES]);
$chris = new \Chris;
echo $m->render($template, $chris);
```
*Note:* we recommend using `ENT_QUOTES` as a default of [entity_flags][entity_flags] to decrease the chance of Cross-site scripting vulnerability.
## And That's Not All!
Read [the Mustache.php documentation][docs] for more information.
## Upgrading from v2.x
_Mustache.php v3.x drops support for PHP 5.25.5_, but is otherwise backwards compatible with v2.x.
To ease the transition, previous behavior can be preserved via configuration:
- The `strict_callables` config option now defaults to `true`. Lambda sections should use closures or callable objects. To continue supporting array-style callables for lambda sections (e.g. `[$this, 'foo']`), set `strict_callables` to `false`.
- [A context shadowing bug from v2.x has been fixed](https://github.com/bobthecow/mustache.php/commit/66ecb327ce15b9efa0cfcb7026fdc62c6659b27f), but if you depend on the previous buggy behavior you can preserve it via the `buggy_property_shadowing` config option.
- By default the return value of higher-order sections that are rendered via the lambda helper will no longer be double-rendered. To preserve the previous behavior, set `double_render_lambdas` to `true`. _This is not recommended._
In order to maintain a wide PHP version support range, there are minor changes to a few interfaces, which you might need to handle if you extend Mustache (see [c0453be](https://github.com/bobthecow/mustache.php/commit/c0453be5c09e7d988b396982e29218fcb25b7304)).
## See Also
- [mustache(5)][manpage] man page.
- [Readme for the Ruby Mustache implementation][ruby].
[mustache]: https://mustache.github.io/
[packagist]: https://packagist.org/packages/mustache/mustache
[entity_flags]: https://github.com/bobthecow/mustache.php/wiki#entity_flags
[docs]: https://github.com/bobthecow/mustache.php/wiki/Home
[manpage]: https://mustache.github.io/mustache.5.html
[ruby]: https://github.com/mustache/mustache/blob/master/README.md

38
vendor/mustache/mustache/composer.json vendored Normal file
View File

@ -0,0 +1,38 @@
{
"name": "mustache/mustache",
"description": "A Mustache implementation in PHP.",
"keywords": [
"templating",
"mustache"
],
"homepage": "https://github.com/bobthecow/mustache.php",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"autoload-dev": {
"psr-4": {
"Mustache\\Test\\": "test/"
}
}
}

46
vendor/mustache/mustache/src/Cache.php vendored Normal file
View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Psr\Log\LoggerInterface;
/**
* Mustache Cache interface.
*
* Interface for caching and loading Template classes generated by the Compiler.
*/
interface Cache
{
/**
* Load a compiled Template class from cache.
*
* @param string $key
*
* @return bool indicates successfully class load
*/
public function load($key);
/**
* Cache and load a compiled Template class.
*
* @param string $key
* @param string $value
*/
public function cache($key, $value);
/**
* Set a logger instance.
*
* @param Logger|LoggerInterface $logger
*/
public function setLogger($logger = null);
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Cache;
use Mustache\Cache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Logger;
use Psr\Log\LoggerInterface;
/**
* Abstract Mustache Cache class.
*
* Provides logging support to child implementations.
*
* @abstract
*/
abstract class AbstractCache implements Cache
{
private $logger = null;
/**
* Get the current logger instance.
*
* @return Logger|LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* Set a logger instance.
*
* @param Logger|LoggerInterface $logger
*/
public function setLogger($logger = null)
{
// n.b. this uses `is_a` to prevent a dependency on Psr\Log
if ($logger !== null && !$logger instanceof Logger && !is_a($logger, 'Psr\\Log\\LoggerInterface')) {
throw new InvalidArgumentException('Expected an instance of Mustache\\Logger or Psr\\Log\\LoggerInterface.');
}
$this->logger = $logger;
}
/**
* Add a log record if logging is enabled.
*
* @param string $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
protected function log($level, $message, array $context = [])
{
if (isset($this->logger)) {
$this->logger->log($level, $message, $context);
}
}
}

View File

@ -0,0 +1,166 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Cache;
use Mustache\Exception\RuntimeException;
use Mustache\Logger;
/**
* Mustache Cache filesystem implementation.
*
* A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
*
* $cache = new FilesystemCache(__DIR__.'/cache');
* $cache->cache($className, $compiledSource);
*
* The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
*/
class FilesystemCache extends AbstractCache
{
private $baseDir;
private $fileMode;
/**
* Filesystem cache constructor.
*
* @param string $baseDir Directory for compiled templates
* @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask
*/
public function __construct($baseDir, $fileMode = null)
{
$this->baseDir = $baseDir;
$this->fileMode = $fileMode;
}
/**
* Load the class from cache using `require_once`.
*
* @param string $key
*
* @return bool
*/
public function load($key)
{
$fileName = $this->getCacheFilename($key);
if (!is_file($fileName)) {
return false;
}
require_once $fileName;
return true;
}
/**
* Cache and load the compiled class.
*
* @param string $key
* @param string $value
*/
public function cache($key, $value)
{
$fileName = $this->getCacheFilename($key);
$this->log(
Logger::DEBUG,
'Writing to template cache: "{fileName}"',
['fileName' => $fileName]
);
$this->writeFile($fileName, $value);
$this->load($key);
}
/**
* Build the cache filename.
* Subclasses should override for custom cache directory structures.
*
* @param string $name
*
* @return string
*/
protected function getCacheFilename($name)
{
return sprintf('%s/%s.php', $this->baseDir, $name);
}
/**
* Create cache directory.
*
* @throws RuntimeException If unable to create directory
*
* @param string $fileName
*
* @return string
*/
private function buildDirectoryForFilename($fileName)
{
$dirName = dirname($fileName);
if (!is_dir($dirName)) {
$this->log(
Logger::INFO,
'Creating Mustache template cache directory: "{dirName}"',
['dirName' => $dirName]
);
@mkdir($dirName, 0777, true);
// @codeCoverageIgnoreStart
if (!is_dir($dirName)) {
throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
}
// @codeCoverageIgnoreEnd
}
return $dirName;
}
/**
* Write cache file.
*
* @throws RuntimeException If unable to write file
*
* @param string $fileName
* @param string $value
*/
private function writeFile($fileName, $value)
{
$dirName = $this->buildDirectoryForFilename($fileName);
$this->log(
Logger::DEBUG,
'Caching compiled template to "{fileName}"',
['fileName' => $fileName]
);
$tempFile = tempnam($dirName, basename($fileName));
if (false !== @file_put_contents($tempFile, $value)) {
if (@rename($tempFile, $fileName)) {
$mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask());
@chmod($fileName, $mode);
return;
}
// @codeCoverageIgnoreStart
$this->log(
Logger::ERROR,
'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
['tempName' => $tempFile, 'fileName' => $fileName]
);
// @codeCoverageIgnoreEnd
}
// @codeCoverageIgnoreStart
throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
// @codeCoverageIgnoreEnd
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Cache;
use Mustache\Logger;
/**
* Mustache Cache in-memory implementation.
*
* The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
* recommended for production use.
*/
class NoopCache extends AbstractCache
{
/**
* Loads nothing. Move along.
*
* @param string $key
*
* @return bool
*/
public function load($key)
{
return false;
}
/**
* Loads the compiled Mustache Template class without caching.
*
* @param string $key
* @param string $value
*/
public function cache($key, $value)
{
$this->log(
Logger::WARNING,
'Template cache disabled, evaluating "{className}" class at runtime',
['className' => $key]
);
eval('?>' . $value);
}
}

View File

@ -0,0 +1,807 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\SyntaxException;
/**
* Mustache Compiler class.
*
* This class is responsible for turning a Mustache token parse tree into normal PHP source code.
*/
class Compiler
{
private $pragmas;
private $defaultPragmas = [];
private $sections;
private $blocks;
private $source;
private $indentNextLine;
private $customEscape;
private $entityFlags;
private $charset;
private $strictCallables;
// Optional Mustache specs
private $lambdas = true;
/**
* Compile a Mustache token parse tree into PHP source code.
*
* @throws InvalidArgumentException if the FILTERS pragma is set but lambdas are not enabled
*
* @param string $source Mustache Template source code
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
* @param bool $customEscape (default: false)
* @param string $charset (default: 'UTF-8')
* @param bool $strictCallables (default: false)
* @param int $entityFlags (default: ENT_COMPAT)
*
* @return string Generated PHP source code
*/
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
{
$this->pragmas = $this->defaultPragmas;
$this->sections = [];
$this->blocks = [];
$this->source = $source;
$this->indentNextLine = true;
$this->customEscape = $customEscape;
$this->entityFlags = $entityFlags;
$this->charset = $charset;
$this->strictCallables = $strictCallables;
$code = $this->writeCode($tree, $name);
if (isset($this->pragmas[Engine::PRAGMA_FILTERS]) && !$this->lambdas) {
throw new InvalidArgumentException('The FILTERS pragma requires lambda support');
}
return $code;
}
/**
* Disable optional Mustache specs.
*
* @internal Users should set options in Mustache\Engine, not here :)
*
* @param bool[] $options
*/
public function setOptions(array $options)
{
if (isset($options['lambdas'])) {
$this->lambdas = $options['lambdas'] !== false;
}
}
/**
* Enable pragmas across all templates, regardless of the presence of pragma
* tags in the individual templates.
*
* @internal Users should set global pragmas in \Mustache\Engine, not here :)
*
* @param string[] $pragmas
*/
public function setPragmas(array $pragmas)
{
$this->pragmas = [];
foreach ($pragmas as $pragma) {
$this->pragmas[$pragma] = true;
}
$this->defaultPragmas = $this->pragmas;
}
/**
* Helper function for walking the Mustache token parse tree.
*
* @throws SyntaxException upon encountering unknown token types
*
* @param array $tree Parse tree of Mustache tokens
* @param int $level (default: 0)
*
* @return string Generated PHP source code
*/
private function walk(array $tree, $level = 0)
{
$code = '';
$level++;
foreach ($tree as $node) {
switch ($node[Tokenizer::TYPE]) {
case Tokenizer::T_PRAGMA:
$this->pragmas[$node[Tokenizer::NAME]] = true;
break;
case Tokenizer::T_SECTION:
$code .= $this->section(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
isset($node[Tokenizer::FILTERS]) ? $node[Tokenizer::FILTERS] : [],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case Tokenizer::T_INVERTED:
$code .= $this->invertedSection(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
isset($node[Tokenizer::FILTERS]) ? $node[Tokenizer::FILTERS] : [],
$level
);
break;
case Tokenizer::T_PARTIAL:
$code .= $this->partial(
$node[Tokenizer::NAME],
isset($node[Tokenizer::DYNAMIC]) ? $node[Tokenizer::DYNAMIC] : false,
isset($node[Tokenizer::INDENT]) ? $node[Tokenizer::INDENT] : '',
$level
);
break;
case Tokenizer::T_PARENT:
$code .= $this->parent(
$node[Tokenizer::NAME],
isset($node[Tokenizer::DYNAMIC]) ? $node[Tokenizer::DYNAMIC] : false,
isset($node[Tokenizer::INDENT]) ? $node[Tokenizer::INDENT] : '',
$node[Tokenizer::NODES],
$level
);
break;
case Tokenizer::T_BLOCK_ARG:
$code .= $this->blockArg(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case Tokenizer::T_BLOCK_VAR:
$code .= $this->blockVar(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case Tokenizer::T_COMMENT:
break;
case Tokenizer::T_ESCAPED:
case Tokenizer::T_UNESCAPED:
case Tokenizer::T_UNESCAPED_2:
$code .= $this->variable(
$node[Tokenizer::NAME],
isset($node[Tokenizer::FILTERS]) ? $node[Tokenizer::FILTERS] : [],
$node[Tokenizer::TYPE] === Tokenizer::T_ESCAPED,
$level
);
break;
case Tokenizer::T_TEXT:
$code .= $this->text($node[Tokenizer::VALUE], $level);
break;
default:
throw new SyntaxException(sprintf('Unknown token type: %s', $node[Tokenizer::TYPE]), $node);
}
}
return $code;
}
const KLASS = '<?php
class %s extends \\Mustache\\Template
{
private $lambdaHelper;%s%s
public function renderInternal(\\Mustache\\Context $context, $indent = \'\')
{
$this->lambdaHelper = new \\Mustache\\LambdaHelper($this->mustache, $context);
$buffer = \'\';
%s
return $buffer;
}
%s
%s
}';
const KLASS_NO_LAMBDAS = '<?php
class %s extends \\Mustache\\Template
{%s%s
public function renderInternal(\\Mustache\\Context $context, $indent = \'\')
{
$buffer = \'\';
%s
return $buffer;
}
}';
const STRICT_CALLABLE = 'protected $strictCallables = true;';
const NO_LAMBDAS = 'protected $lambdas = false;';
/**
* Generate Mustache Template class PHP source.
*
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
*
* @return string Generated PHP source code
*/
private function writeCode(array $tree, $name)
{
$code = $this->walk($tree);
$sections = implode("\n", $this->sections);
$blocks = implode("\n", $this->blocks);
$klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS;
$callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
$lambda = $this->lambdas ? '' : $this->prepare(self::NO_LAMBDAS);
return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $lambda, $code, $sections, $blocks);
}
const BLOCK_VAR = '
$blockFunction = $context->findInBlock(%s);
if (is_callable($blockFunction)) {
$buffer .= call_user_func($blockFunction, $context);
%s}
';
const BLOCK_VAR_ELSE = '} else {%s';
/**
* Generate Mustache Template inheritance block variable PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated PHP source code
*/
private function blockVar(array $nodes, $id, $start, $end, $otag, $ctag, $level)
{
$id = var_export($id, true);
$else = $this->walk($nodes, $level);
if ($else !== '') {
$else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else);
}
return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else);
}
const BLOCK_ARG = '%s => [$this, \'block%s\'],';
/**
* Generate Mustache Template inheritance block argument PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated PHP source code
*/
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
{
$key = $this->block($nodes);
$id = var_export($id, true);
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
}
const BLOCK_FUNCTION = '
public function block%s($context)
{
$indent = $buffer = \'\';%s
return $buffer;
}
';
/**
* Generate Mustache Template inheritance block function PHP source.
*
* @param array $nodes Array of child tokens
*
* @return string key of new block function
*/
private function block(array $nodes)
{
$code = $this->walk($nodes, 0);
$key = ucfirst(md5($code));
if (!isset($this->blocks[$key])) {
$this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code);
}
return $key;
}
const SECTION_CALL = '
$value = $context->%s(%s%s);%s
$buffer .= $this->section%s($context, $indent, $value);
';
const SECTION = '
private function section%s(\\Mustache\\Context $context, $indent, $value)
{
$buffer = \'\';
if (%s) {
$source = %s;
$value = call_user_func($value, $source, %s);
if ($value instanceof \\Mustache\\RenderedString) {
return $value->getValue();
}
if (is_string($value)) {
if (strpos($value, \'{{\') === false) {
return $value;
}
return $this->mustache
->loadLambda($value%s)
->renderInternal($context);
}
}
if (!empty($value)) {
$values = $this->isIterable($value) ? $value : [$value];
foreach ($values as $value) {
$context->push($value);
%s
$context->pop();
}
}
return $buffer;
}
';
const SECTION_NO_LAMBDAS = '
private function section%s(\\Mustache\\Context $context, $indent, $value)
{
$buffer = \'\';
if (!empty($value)) {
$values = $this->isIterable($value) ? $value : [$value];
foreach ($values as $value) {
$context->push($value);
%s
$context->pop();
}
}
return $buffer;
}
';
/**
* Generate Mustache Template section PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param string[] $filters Array of filters
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated section PHP source code
*/
private function section(array $nodes, $id, $filters, $start, $end, $otag, $ctag, $level)
{
$source = var_export(substr($this->source, $start, $end - $start), true);
$callable = $this->getCallable();
if ($otag !== '{{' || $ctag !== '}}') {
$delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
$helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag);
$delims = ', ' . $delimTag;
} else {
$helper = '$this->lambdaHelper';
$delims = '';
}
$key = ucfirst(md5($delims . "\n" . $source));
if (!isset($this->sections[$key])) {
if ($this->lambdas) {
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
} else {
$this->sections[$key] = sprintf($this->prepare(self::SECTION_NO_LAMBDAS), $key, $this->walk($nodes, 2));
}
}
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $findArg, $filters, $key);
}
const INVERTED_SECTION = '
$value = $context->%s(%s%s);%s
if (empty($value)) {
%s
}
';
/**
* Generate Mustache Template inverted section PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param string[] $filters Array of filters
* @param int $level
*
* @return string Generated inverted section PHP source code
*/
private function invertedSection(array $nodes, $id, $filters, $level)
{
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $findArg, $filters, $this->walk($nodes, $level));
}
const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s%s), $context)';
/**
* Generate Mustache Template dynamic name resolution PHP source.
*
* @param string $id Tag name
* @param bool $dynamic True if the name is dynamic
*
* @return string Dynamic name resolution PHP source code
*/
private function resolveDynamicName($id, $dynamic)
{
if (!$dynamic) {
return var_export($id, true);
}
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$findArg = $this->getFindMethodArgs($method);
// TODO: filters?
return sprintf(self::DYNAMIC_NAME, $method, $id, $findArg);
}
const PARTIAL_INDENT = ', $indent . %s';
const PARTIAL = '
if ($partial = $this->mustache->loadPartial(%s)) {
$buffer .= $partial->renderInternal($context%s);
}
';
/**
* Generate Mustache Template partial call PHP source.
*
* @param string $id Partial name
* @param bool $dynamic Partial name is dynamic
* @param string $indent Whitespace indent to apply to partial
* @param int $level
*
* @return string Generated partial call PHP source code
*/
private function partial($id, $dynamic, $indent, $level)
{
if ($indent !== '') {
$indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
} else {
$indentParam = '';
}
return sprintf(
$this->prepare(self::PARTIAL, $level),
$this->resolveDynamicName($id, $dynamic),
$indentParam
);
}
const PARENT = '
if ($parent = $this->mustache->loadPartial(%s)) {
$context->pushBlockContext([%s
]);
$buffer .= $parent->renderInternal($context, $indent);
$context->popBlockContext();
}
';
const PARENT_NO_CONTEXT = '
if ($parent = $this->mustache->loadPartial(%s)) {
$buffer .= $parent->renderInternal($context, $indent);
}
';
/**
* Generate Mustache Template inheritance parent call PHP source.
*
* @param string $id Parent tag name
* @param bool $dynamic Tag name is dynamic
* @param string $indent Whitespace indent to apply to parent
* @param array $children Child nodes
* @param int $level
*
* @return string Generated PHP source code
*/
private function parent($id, $dynamic, $indent, array $children, $level)
{
$realChildren = array_filter($children, [self::class, 'onlyBlockArgs']);
$partialName = $this->resolveDynamicName($id, $dynamic);
if (empty($realChildren)) {
return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), $partialName);
}
return sprintf(
$this->prepare(self::PARENT, $level),
$partialName,
$this->walk($realChildren, $level + 1)
);
}
/**
* Helper method for filtering out non-block-arg tokens.
*
* @return bool True if $node is a block arg token
*/
private static function onlyBlockArgs(array $node)
{
return $node[Tokenizer::TYPE] === Tokenizer::T_BLOCK_ARG;
}
const VARIABLE = '
$value = $this->resolveValue($context->%s(%s%s), $context);%s
$buffer .= %s($value === null ? \'\' : %s);
';
/**
* Generate Mustache Template variable interpolation PHP source.
*
* @param string $id Variable name
* @param string[] $filters Array of filters
* @param bool $escape Escape the variable value for output?
* @param int $level
*
* @return string Generated variable interpolation PHP source
*/
private function variable($id, $filters, $escape, $level)
{
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
$value = $escape ? $this->getEscape() : '$value';
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $findArg, $filters, $this->flushIndent(), $value);
}
const FILTER = '
$filter = $context->%s(%s%s);
if (!(%s)) {
throw new \\Mustache\\Exception\\UnknownFilterException(%s);
}
$value = call_user_func($filter, %s);%s
';
const FILTER_FIRST_VALUE = '$this->resolveValue($value, $context)';
const FILTER_VALUE = '$value';
/**
* Generate Mustache Template variable filtering PHP source.
*
* If the initial $value is a lambda it will be resolved before starting the filter chain.
*
* @param string[] $filters Array of filters
* @param int $level
* @param bool $first (default: false)
*
* @return string Generated filter PHP source
*/
private function getFilters(array $filters, $level, $first = true)
{
if (empty($filters)) {
return '';
}
$name = array_shift($filters);
$method = $this->getFindMethod($name);
$filter = ($method !== 'last') ? var_export($name, true) : '';
$findArg = $this->getFindMethodArgs($method);
$callable = $this->getCallable('$filter');
$msg = var_export($name, true);
$value = $first ? self::FILTER_FIRST_VALUE : self::FILTER_VALUE;
return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $findArg, $callable, $msg, $value, $this->getFilters($filters, $level, false));
}
const LINE = '$buffer .= "\n";';
const TEXT = '$buffer .= %s%s;';
/**
* Generate Mustache Template output Buffer call PHP source.
*
* @param string $text
* @param int $level
*
* @return string Generated output Buffer call PHP source
*/
private function text($text, $level)
{
$indentNextLine = (substr($text, -1) === "\n");
$code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
$this->indentNextLine = $indentNextLine;
return $code;
}
/**
* Prepare PHP source code snippet for output.
*
* @param string $text
* @param int $bonus Additional indent level (default: 0)
* @param bool $prependNewline Prepend a newline to the snippet? (default: true)
* @param bool $appendNewline Append a newline to the snippet? (default: false)
*
* @return string PHP source code snippet
*/
private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
{
$text = ($prependNewline ? "\n" : '') . trim($text);
if ($prependNewline) {
$bonus++;
}
if ($appendNewline) {
$text .= "\n";
}
return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text);
}
const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
/**
* Get the current escaper.
*
* @param string $value (default: '$value')
*
* @return string Either a custom callback, or an inline call to `htmlspecialchars`
*/
private function getEscape($value = '$value')
{
if ($this->customEscape) {
return sprintf(self::CUSTOM_ESCAPE, $value);
}
return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
}
/**
* Select the appropriate Context `find` method for a given $id.
*
* The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`.
*
* @see \Mustache\Context::find
* @see \Mustache\Context::findDot
* @see \Mustache\Context::last
*
* @param string $id Variable name
*
* @return string `find` method name
*/
private function getFindMethod($id)
{
if ($id === '.') {
return 'last';
}
if (isset($this->pragmas[Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Engine::PRAGMA_ANCHORED_DOT]) {
if (substr($id, 0, 1) === '.') {
return 'findAnchoredDot';
}
}
if (strpos($id, '.') === false) {
return 'find';
}
return 'findDot';
}
/**
* Get the args needed for a given find method.
*
* In this case, it's "true" iff it's a "find dot" method and strict callables is enabled.
*
* @param string $method Find method name
*/
private function getFindMethodArgs($method)
{
if (($method === 'findDot' || $method === 'findAnchoredDot') && $this->strictCallables) {
return ', true';
}
return '';
}
const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
/**
* Helper function to compile strict vs lax "is callable" logic.
*
* @param string $variable (default: '$value')
*
* @return string "is callable" logic
*/
private function getCallable($variable = '$value')
{
$tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
return sprintf($tpl, $variable, $variable);
}
const LINE_INDENT = '$indent . ';
/**
* Get the current $indent prefix to write to the buffer.
*
* @return string "$indent . " or ""
*/
private function flushIndent()
{
if (!$this->indentNextLine) {
return '';
}
$this->indentNextLine = false;
return self::LINE_INDENT;
}
}

277
vendor/mustache/mustache/src/Context.php vendored Normal file
View File

@ -0,0 +1,277 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
/**
* Mustache Template rendering Context.
*/
class Context
{
private $stack = [];
private $blockStack = [];
private $buggyPropertyShadowing = false;
/**
* Mustache rendering Context constructor.
*
* @param mixed $context Default rendering context (default: null)
* @param bool $buggyPropertyShadowing See Engine::getBuggyPropertyShadowing (default: false)
*/
public function __construct($context = null, $buggyPropertyShadowing = false)
{
if ($context !== null) {
$this->stack = [$context];
}
$this->buggyPropertyShadowing = $buggyPropertyShadowing;
}
/**
* Push a new Context frame onto the stack.
*
* @param mixed $value Object or array to use for context
*/
public function push($value)
{
array_push($this->stack, $value);
}
/**
* Push a new Context frame onto the block context stack.
*
* @param mixed $value Object or array to use for block context
*/
public function pushBlockContext($value)
{
array_push($this->blockStack, $value);
}
/**
* Pop the last Context frame from the stack.
*
* @return mixed Last Context frame (object or array)
*/
public function pop()
{
return array_pop($this->stack);
}
/**
* Pop the last block Context frame from the stack.
*
* @return mixed Last block Context frame (object or array)
*/
public function popBlockContext()
{
return array_pop($this->blockStack);
}
/**
* Get the last Context frame.
*
* @return mixed Last Context frame (object or array)
*/
public function last()
{
return end($this->stack);
}
/**
* Find a variable in the Context stack.
*
* Starting with the last Context frame (the context of the innermost section), and working back to the top-level
* rendering context, look for a variable with the given name:
*
* * If the Context frame is an associative array which contains the key $id, returns the value of that element.
* * If the Context frame is an object, this will check first for a public method, then a public property named
* $id. Failing both of these, it will try `__isset` and `__get` magic methods.
* * If a value named $id is not found in any Context frame, returns an empty string.
*
* @param string $id Variable name
*
* @return mixed Variable value, or '' if not found
*/
public function find($id)
{
return $this->findVariableInStack($id, $this->stack);
}
/**
* Find a 'dot notation' variable in the Context stack.
*
* Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
* the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
* result. For example, given the following context stack:
*
* $data = [
* 'name' => 'Fred',
* 'child' => [
* 'name' => 'Bob'
* ],
* ];
*
* ... and the Mustache following template:
*
* {{ child.name }}
*
* ... the `name` value is only searched for within the `child` value of the global Context, not within parent
* Context frames.
*
* @param string $id Dotted variable selector
* @param bool $strictCallables (default: false)
*
* @return mixed Variable value, or '' if not found
*/
public function findDot($id, $strictCallables = false)
{
$chunks = explode('.', $id);
$first = array_shift($chunks);
$value = $this->findVariableInStack($first, $this->stack);
// This wasn't really a dotted name, so we can just return the value.
if (empty($chunks)) {
return $value;
}
foreach ($chunks as $chunk) {
$isCallable = $strictCallables ? (is_object($value) && is_callable($value)) : (!is_string($value) && is_callable($value));
if ($isCallable) {
$value = $value();
} elseif ($value === '') {
return $value;
}
$value = $this->findVariableInStack($chunk, [$value]);
}
return $value;
}
/**
* Find an 'anchored dot notation' variable in the Context stack.
*
* This is the same as findDot(), except it looks in the top of the context
* stack for the first value, rather than searching the whole context stack
* and starting from there.
*
* @see Mustache\Context::findDot
*
* @throws InvalidArgumentException if given an invalid anchored dot $id
*
* @param string $id Dotted variable selector
*
* @return mixed Variable value, or '' if not found
*/
public function findAnchoredDot($id)
{
$chunks = explode('.', $id);
$first = array_shift($chunks);
if ($first !== '') {
throw new InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id));
}
$value = $this->last();
foreach ($chunks as $chunk) {
if ($value === '') {
return $value;
}
$value = $this->findVariableInStack($chunk, [$value]);
}
return $value;
}
/**
* Find an argument in the block context stack.
*
* @param string $id
*
* @return mixed Variable value, or '' if not found
*/
public function findInBlock($id)
{
foreach ($this->blockStack as $context) {
if (array_key_exists($id, $context)) {
return $context[$id];
}
}
return '';
}
/**
* Helper function to find a variable in the Context stack.
*
* @see Mustache\Context::find
*
* @param string $id Variable name
* @param array $stack Context stack
*
* @return mixed Variable value, or '' if not found
*/
private function findVariableInStack($id, array $stack)
{
for ($i = count($stack) - 1; $i >= 0; $i--) {
$frame = &$stack[$i];
switch (gettype($frame)) {
case 'object':
if (!($frame instanceof \Closure)) {
// Note that is_callable() *will not work here*
// See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
if (method_exists($frame, $id)) {
return $frame->$id();
}
if (isset($frame->$id)) {
return $frame->$id;
}
// Preserve backwards compatibility with a property shadowing bug in
// Mustache.php <= 2.14.2
// See https://github.com/bobthecow/mustache.php/pull/410
if ($this->buggyPropertyShadowing) {
if ($frame instanceof \ArrayAccess && isset($frame[$id])) {
return $frame[$id];
}
} else {
if (property_exists($frame, $id)) {
$rp = new \ReflectionProperty($frame, $id);
if ($rp->isPublic()) {
return $frame->$id;
}
}
if ($frame instanceof \ArrayAccess && $frame->offsetExists($id)) {
return $frame[$id];
}
}
}
break;
case 'array':
if (array_key_exists($id, $frame)) {
return $frame[$id];
}
break;
}
}
return '';
}
}

963
vendor/mustache/mustache/src/Engine.php vendored Normal file
View File

@ -0,0 +1,963 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Cache\FilesystemCache;
use Mustache\Cache\NoopCache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\RuntimeException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader\ArrayLoader;
use Mustache\Loader\MutableLoader;
use Mustache\Loader\StringLoader;
use Psr\Log\LoggerInterface;
/**
* A Mustache implementation in PHP.
*
* {@link https://mustache.github.io}
*
* Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
* logic from template files. In fact, it is not even possible to embed logic in the template.
*
* This is very, very rad.
*
* @author Justin Hileman {@link http://justinhileman.com}
*/
class Engine
{
const VERSION = '3.0.0';
const SPEC_VERSION = '1.4.3';
const PRAGMA_FILTERS = 'FILTERS';
const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
/**
* @deprecated PRAGMA_BLOCKS is now part of the Mustache spec, and is enabled by default
*/
const PRAGMA_BLOCKS = 'BLOCKS';
// Known pragmas
private static $knownPragmas = [
self::PRAGMA_FILTERS => true,
self::PRAGMA_ANCHORED_DOT => true,
self::PRAGMA_BLOCKS => true,
];
// Template cache
private $templates = [];
// Environment
private $templateClassPrefix = '__Mustache_';
private $cache;
private $lambdaCache;
private $cacheLambdaTemplates = false;
private $doubleRenderLambdas = false;
private $loader;
private $partialsLoader;
private $helpers;
private $escape;
private $entityFlags = ENT_COMPAT;
private $charset = 'UTF-8';
private $logger;
private $strictCallables = true;
private $pragmas = [];
private $delimiters;
private $buggyPropertyShadowing = false;
// Optional Mustache specs
private $dynamicNames = true;
private $inheritance = true;
private $lambdas = true;
// Services
private $tokenizer;
private $parser;
private $compiler;
/**
* Mustache class constructor.
*
* Passing an $options array allows overriding certain Mustache options during instantiation:
*
* $options = [
* // The class prefix for compiled templates. Defaults to '__Mustache_'.
* 'template_class_prefix' => '__MyTemplates_',
*
* // A Mustache cache instance or a cache directory string for compiled templates.
* // Mustache will not cache templates unless this is set.
* 'cache' => __DIR__.'/tmp/cache/mustache',
*
* // Override default permissions for cache files. Defaults to using the system-defined umask. It is
* // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
* 'cache_file_mode' => 0666,
*
* // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
* // sections are often too dynamic to benefit from caching.
* 'cache_lambda_templates' => true,
*
* // Customize the tag delimiters used by this engine instance. Note that overriding here changes the
* // delimiters used to parse all templates and partials loaded by this instance. To override just for a
* // single template, use an inline "change delimiters" tag at the start of the template file:
* //
* // {{=<% %>=}}
* //
* 'delimiters' => '<% %>',
*
* // A Mustache template loader instance. Uses a StringLoader if not specified.
* 'loader' => new \Mustache\Loader\FilesystemLoader(__DIR__.'/views'),
*
* // A Mustache loader instance for partials.
* 'partials_loader' => new \Mustache\Loader\FilesystemLoader(__DIR__.'/views/partials'),
*
* // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
* // efficient or lazy as a Filesystem (or database) loader.
* 'partials' => ['foo' => file_get_contents(__DIR__.'/views/partials/foo.mustache')],
*
* // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
* // sections), or any other valid Mustache context value. They will be prepended to the context stack,
* // so they will be available in any template loaded by this Mustache instance.
* 'helpers' => ['i18n' => function ($text) {
* // do something translatey here...
* }],
*
* // An 'escape' callback, responsible for escaping double-mustache variables.
* 'escape' => function ($value) {
* return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
* },
*
* // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES.
* 'entity_flags' => ENT_QUOTES,
*
* // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
* 'charset' => 'ISO-8859-1',
*
* // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
* // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
* // available as well:
* 'logger' => new \Mustache\Logger\StreamLogger('php://stderr'),
*
*
* // OPTIONAL MUSTACHE FEATURES:
*
* // Enable dynamic names. By default, variables and sections like `{{*name}}` will be resolved dynamically.
* //
* // To disable dynamic name resolution, set this to false.
* 'dynamic_names' => true,
*
* // Enable template inheritance. By default, templates can extend other templates using the `{{< name}}` and
* // `{{$ block}}` tags.
* //
* // To disable inheritance, set this to false.
* 'inheritance' => true,
*
* // Enable lambda sections and values. By default, "lambdas" are enabled; if a variable resolves to a
* // callable value, that callable is called before interpolation. If a section name resolves to a callable
* // value, it is treated as a "higher order section", and the section content is passed to the callable
* // for processing prior to rendering.
* //
* // Note that the FILTERS pragma requires lambdas to function, so using FILTERS without lambdas enabled
* // will throw an invalid argument exception.
* //
* // To disable lambdas and higher order sections entirely, set this to false.
* 'lambdas' => true,
*
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
* // templates.
* 'pragmas' => [\Mustache\Engine::PRAGMA_FILTERS],
*
*
* // BACKWARDS COMPATIBILITY:
*
* // Only treat \Closure instances and invokable classes as callable. If true, values like
* // `['ClassName', 'methodName']` and `[$classInstance, 'methodName']`, which are traditionally
* // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
* // helps protect against arbitrary code execution when user input is passed directly into the template.
* //
* // Defaults to true, but can be set to false to preserve Mustache.php v2.x behavior.
* //
* // THIS IS NOT RECOMMENDED.
* 'strict_callables' => true,
*
* // Enable buggy property shadowing. Per the Mustache spec, keys of a value higher in the context stack
* // shadow similarly named keys lower in the stack. For example, in the template
* // `{{# foo }}{{ bar }}{{/ foo }}` if the value for `foo` has a method, property, or key named `bar`, it
* // will prevent looking lower in the context stack for a another value named `bar`.
* //
* // Setting the value of an array key to null prevents lookups higher in the context stack. The behavior
* // should have been identical for object properties (and ArrayAccess) as well, but a bug in the context
* // lookup logic meant that a property which exists but is set to null would not prevent further context
* // lookup.
* //
* // This bug was fixed in Mustache.php v3.x, but the previous buggy behavior can be preserved by setting this
* // option to true.
* //
* // THIS IS NOT RECOMMENDED.
* 'buggy_property_shadowing' => false,
*
* // Double-render lambda return values. By default, the return value of higher order sections that are
* // rendered via the lambda helper will *not* be re-rendered.
* //
* // To preserve the behavior of Mustache.php v2.x, set this to true.
* //
* // THIS IS NOT RECOMMENDED.
* 'double_render_lambdas' => false,
* ];
*
* @throws InvalidArgumentException If `escape` option is not callable
* @throws InvalidArgumentException If `lambdas` is disabled but the `FILTERS` pragma is enabled
*/
public function __construct(array $options = [])
{
if (isset($options['template_class_prefix'])) {
if ((string) $options['template_class_prefix'] === '') {
throw new InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty');
}
$this->templateClassPrefix = $options['template_class_prefix'];
}
if (isset($options['cache'])) {
$cache = $options['cache'];
if (is_string($cache)) {
$mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
$cache = new FilesystemCache($cache, $mode);
}
$this->setCache($cache);
}
if (isset($options['cache_lambda_templates'])) {
$this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
}
if (isset($options['loader'])) {
$this->setLoader($options['loader']);
}
if (isset($options['partials_loader'])) {
$this->setPartialsLoader($options['partials_loader']);
}
if (isset($options['partials'])) {
$this->setPartials($options['partials']);
}
if (isset($options['helpers'])) {
$this->setHelpers($options['helpers']);
}
if (isset($options['escape'])) {
if (!is_callable($options['escape'])) {
throw new InvalidArgumentException('Mustache Constructor "escape" option must be callable');
}
$this->escape = $options['escape'];
}
if (isset($options['entity_flags'])) {
$this->entityFlags = $options['entity_flags'];
}
if (isset($options['charset'])) {
$this->charset = $options['charset'];
}
if (isset($options['logger'])) {
$this->setLogger($options['logger']);
}
if (isset($options['delimiters'])) {
$this->delimiters = $options['delimiters'];
}
// Optional Mustache features
if (isset($options['dynamic_names'])) {
$this->dynamicNames = $options['dynamic_names'] !== false;
}
if (isset($options['inheritance'])) {
$this->inheritance = $options['inheritance'] !== false;
}
if (isset($options['lambdas'])) {
$this->lambdas = $options['lambdas'] !== false;
}
if (isset($options['pragmas'])) {
foreach ($options['pragmas'] as $pragma) {
if (!isset(self::$knownPragmas[$pragma])) {
throw new InvalidArgumentException(sprintf('Unknown pragma: "%s"', $pragma));
}
$this->pragmas[$pragma] = true;
}
}
if (!$this->lambdas && isset($this->pragmas[self::PRAGMA_FILTERS])) {
throw new InvalidArgumentException('The FILTERS pragma requires lambda support');
}
// Backwards compatibility
if (isset($options['strict_callables'])) {
$this->strictCallables = (bool) $options['strict_callables'];
}
if (isset($options['buggy_property_shadowing'])) {
$this->buggyPropertyShadowing = (bool) $options['buggy_property_shadowing'];
}
if (isset($options['double_render_lambdas'])) {
$this->doubleRenderLambdas = (bool) $options['double_render_lambdas'];
}
}
/**
* Shortcut 'render' invocation.
*
* Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
*
* @see Mustache\Engine::loadTemplate
* @see Mustache\Template::render
*
* @param string $template
*
* @return string Rendered template
*/
public function render($template, $context = [])
{
return $this->loadTemplate($template)->render($context);
}
/**
* Get the current Mustache escape callback.
*
* @return callable|null
*/
public function getEscape()
{
return $this->escape;
}
/**
* Get the current Mustache entity type to escape.
*
* @return int
*/
public function getEntityFlags()
{
return $this->entityFlags;
}
/**
* Get the current Mustache character set.
*
* @return string
*/
public function getCharset()
{
return $this->charset;
}
/**
* Check whether to double-render higher-order sections.
*
* By default, the return value of higher order sections that are rendered
* via the lambda helper will *not* be re-rendered. To preserve the
* behavior of Mustache.php v2.x, set this to true.
*
* THIS IS NOT RECOMMENDED.
*/
public function getDoubleRenderLambdas()
{
return $this->doubleRenderLambdas;
}
/**
* Check whether to use buggy property shadowing.
*
* THIS IS NOT RECOMMENDED.
*
* See https://github.com/bobthecow/mustache.php/pull/410
*/
public function getBuggyPropertyShadowing()
{
return $this->buggyPropertyShadowing;
}
/**
* Get currently enabled optional features.
*
* @return array
*/
public function getOptions()
{
return [
'dynamic_names' => $this->dynamicNames,
'inheritance' => $this->inheritance,
'lambdas' => $this->lambdas,
];
}
/**
* Get the current globally enabled pragmas.
*
* @return array
*/
public function getPragmas()
{
return array_keys($this->pragmas);
}
/**
* Set the Mustache template Loader instance.
*/
public function setLoader(Loader $loader)
{
$this->loader = $loader;
}
/**
* Get the current Mustache template Loader instance.
*
* If no Loader instance has been explicitly specified, this method will instantiate and return
* a StringLoader instance.
*
* @return Loader
*/
public function getLoader()
{
if (!isset($this->loader)) {
$this->loader = new StringLoader();
}
return $this->loader;
}
/**
* Set the Mustache partials Loader instance.
*/
public function setPartialsLoader(Loader $partialsLoader)
{
$this->partialsLoader = $partialsLoader;
}
/**
* Get the current Mustache partials Loader instance.
*
* If no Loader instance has been explicitly specified, this method will instantiate and return
* an ArrayLoader instance.
*
* @return Loader
*/
public function getPartialsLoader()
{
if (!isset($this->partialsLoader)) {
$this->partialsLoader = new ArrayLoader();
}
return $this->partialsLoader;
}
/**
* Set partials for the current partials Loader instance.
*
* @throws RuntimeException If the current Loader instance is immutable
*/
public function setPartials(array $partials = [])
{
if (!isset($this->partialsLoader)) {
$this->partialsLoader = new ArrayLoader();
}
if (!$this->partialsLoader instanceof MutableLoader) {
throw new RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
}
$this->partialsLoader->setTemplates($partials);
}
/**
* Set an array of Mustache helpers.
*
* An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
* any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
* any template loaded by this Mustache instance.
*
* @throws InvalidArgumentException if $helpers is not an array or \Traversable
*
* @param array|\Traversable $helpers
*/
public function setHelpers($helpers)
{
if (!is_array($helpers) && !$helpers instanceof \Traversable) {
throw new InvalidArgumentException('setHelpers expects an array of helpers');
}
$this->getHelpers()->clear();
foreach ($helpers as $name => $helper) {
$this->addHelper($name, $helper);
}
}
/**
* Get the current set of Mustache helpers.
*
* @see Mustache\Engine::setHelpers
*
* @return HelperCollection
*/
public function getHelpers()
{
if (!isset($this->helpers)) {
$this->helpers = new HelperCollection();
}
return $this->helpers;
}
/**
* Add a new Mustache helper.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
* @param mixed $helper
*/
public function addHelper($name, $helper)
{
$this->getHelpers()->add($name, $helper);
}
/**
* Get a Mustache helper by name.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
*
* @return mixed Helper
*/
public function getHelper($name)
{
return $this->getHelpers()->get($name);
}
/**
* Check whether this Mustache instance has a helper.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
*
* @return bool True if the helper is present
*/
public function hasHelper($name)
{
return $this->getHelpers()->has($name);
}
/**
* Remove a helper by name.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
*/
public function removeHelper($name)
{
$this->getHelpers()->remove($name);
}
/**
* Set the Mustache Logger instance.
*
* @throws InvalidArgumentException If logger is not an instance of Mustache\Logger or Psr\Log\LoggerInterface
*
* @param Logger|LoggerInterface $logger
*/
public function setLogger($logger = null)
{
// n.b. this uses `is_a` to prevent a dependency on Psr\Log
if ($logger !== null && !$logger instanceof Logger && !is_a($logger, 'Psr\\Log\\LoggerInterface')) {
throw new InvalidArgumentException('Expected an instance of Mustache\\Logger or Psr\\Log\\LoggerInterface.');
}
if ($this->getCache()->getLogger() === null) {
$this->getCache()->setLogger($logger);
}
$this->logger = $logger;
}
/**
* Get the current Mustache Logger instance.
*
* @return Logger|LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* Set the Mustache Tokenizer instance.
*/
public function setTokenizer(Tokenizer $tokenizer)
{
$this->tokenizer = $tokenizer;
}
/**
* Get the current Mustache Tokenizer instance.
*
* If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Tokenizer
*/
public function getTokenizer()
{
if (!isset($this->tokenizer)) {
$this->tokenizer = new Tokenizer();
}
return $this->tokenizer;
}
/**
* Set the Mustache Parser instance.
*/
public function setParser(Parser $parser)
{
$this->parser = $parser;
}
/**
* Get the current Mustache Parser instance.
*
* If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Parser
*/
public function getParser()
{
if (!isset($this->parser)) {
$this->parser = new Parser();
}
return $this->parser;
}
/**
* Set the Mustache Compiler instance.
*/
public function setCompiler(Compiler $compiler)
{
$this->compiler = $compiler;
}
/**
* Get the current Mustache Compiler instance.
*
* If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Compiler
*/
public function getCompiler()
{
if (!isset($this->compiler)) {
$this->compiler = new Compiler();
}
return $this->compiler;
}
/**
* Set the Mustache Cache instance.
*/
public function setCache(Cache $cache)
{
if (isset($this->logger) && $cache->getLogger() === null) {
$cache->setLogger($this->getLogger());
}
$this->cache = $cache;
}
/**
* Get the current Mustache Cache instance.
*
* If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Cache
*/
public function getCache()
{
if (!isset($this->cache)) {
$this->setCache(new NoopCache());
}
return $this->cache;
}
/**
* Get the current Lambda Cache instance.
*
* If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
*
* @see Mustache\Engine::getCache
*
* @return Cache
*/
protected function getLambdaCache()
{
if ($this->cacheLambdaTemplates) {
return $this->getCache();
}
if (!isset($this->lambdaCache)) {
$this->lambdaCache = new NoopCache();
}
return $this->lambdaCache;
}
/**
* Helper method to generate a Mustache template class.
*
* This method must be updated any time options are added which make it so
* the same template could be parsed and compiled multiple different ways.
*
* @param string|Source $source
*
* @return string Mustache Template class name
*/
public function getTemplateClassName($source)
{
// For the most part, adding a new option here should do the trick.
//
// Pick a value here which is unique for each possible way the template
// could be compiled... but not necessarily unique per option value. See
// escape below, which only needs to differentiate between 'custom' and
// 'default' escapes.
//
// Keep this list in alphabetical order :)
$chunks = [
'charset' => $this->charset,
'delimiters' => $this->delimiters ?: '{{ }}',
'entityFlags' => $this->entityFlags,
'escape' => isset($this->escape) ? 'custom' : 'default',
'key' => ($source instanceof Source) ? $source->getKey() : 'source',
'options' => $this->getOptions(),
'pragmas' => $this->getPragmas(),
'strictCallables' => $this->strictCallables,
'version' => self::VERSION,
];
$key = json_encode($chunks);
// Template Source instances have already provided their own source key. For strings, just include the whole
// source string in the md5 hash.
if (!$source instanceof Source) {
$key .= "\n" . $source;
}
return $this->templateClassPrefix . md5($key);
}
/**
* Load a Mustache Template by name.
*
* @param string $name
*
* @return Template
*/
public function loadTemplate($name)
{
return $this->loadSource($this->getLoader()->load($name));
}
/**
* Load a Mustache partial Template by name.
*
* This is a helper method used internally by Template instances for loading partial templates. You can most likely
* ignore it completely.
*
* @param string $name
*
* @return Template
*/
public function loadPartial($name)
{
try {
if (isset($this->partialsLoader)) {
$loader = $this->partialsLoader;
} elseif (isset($this->loader) && !$this->loader instanceof StringLoader) {
$loader = $this->loader;
} else {
throw new UnknownTemplateException($name);
}
return $this->loadSource($loader->load($name));
} catch (UnknownTemplateException $e) {
// If the named partial cannot be found, log then return null.
$this->log(
Logger::WARNING,
'Partial not found: "{name}"',
['name' => $e->getTemplateName()]
);
}
}
/**
* Load a Mustache lambda Template by source.
*
* This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
* likely ignore it completely.
*
* @param string $source
* @param string $delims (default: null)
*
* @return Template
*/
public function loadLambda($source, $delims = null)
{
if ($delims !== null) {
$source = $delims . "\n" . $source;
}
return $this->loadSource($source, $this->getLambdaCache());
}
/**
* Instantiate and return a Mustache Template instance by source.
*
* Optionally provide a Mustache\Cache instance. This is used internally by Mustache\Engine::loadLambda to respect
* the 'cache_lambda_templates' configuration option.
*
* @see Mustache\Engine::loadTemplate
* @see Mustache\Engine::loadPartial
* @see Mustache\Engine::loadLambda
*
* @param string|Source $source
* @param Cache $cache (default: null)
*
* @return Template
*/
private function loadSource($source, $cache = null)
{
$className = $this->getTemplateClassName($source);
if (!isset($this->templates[$className])) {
if ($cache === null || !$cache instanceof Cache) {
$cache = $this->getCache();
}
if (!class_exists($className, false)) {
if (!$cache->load($className)) {
$compiled = $this->compile($source);
$cache->cache($className, $compiled);
}
}
$this->log(
Logger::DEBUG,
'Instantiating template: "{className}"',
['className' => $className]
);
$this->templates[$className] = new $className($this);
}
return $this->templates[$className];
}
/**
* Helper method to tokenize a Mustache template.
*
* @see Mustache\Tokenizer::scan
*
* @param string $source
*
* @return array Tokens
*/
private function tokenize($source)
{
return $this->getTokenizer()->scan($source, $this->delimiters);
}
/**
* Helper method to parse a Mustache template.
*
* @see Mustache\Parser::parse
*
* @param string $source
*
* @return array Token tree
*/
private function parse($source)
{
$parser = $this->getParser();
$parser->setOptions($this->getOptions());
$parser->setPragmas($this->getPragmas());
return $parser->parse($this->tokenize($source));
}
/**
* Helper method to compile a Mustache template.
*
* @see Mustache\Compiler::compile
*
* @param string|Source $source
*
* @return string generated Mustache template class code
*/
private function compile($source)
{
$name = $this->getTemplateClassName($source);
$this->log(
Logger::INFO,
'Compiling template to "{className}" class',
['className' => $name]
);
if ($source instanceof Source) {
$source = $source->getSource();
}
$tree = $this->parse($source);
$compiler = $this->getCompiler();
$compiler->setOptions($this->getOptions());
$compiler->setPragmas($this->getPragmas());
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
}
/**
* Add a log record if logging is enabled.
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
private function log($level, $message, array $context = [])
{
if (isset($this->logger)) {
$this->logger->log($level, $message, $context);
}
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
interface Exception
{
// This space intentionally left blank.
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Invalid argument exception.
*/
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
// This space intentionally left blank.
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Logic exception.
*/
class LogicException extends \LogicException implements Exception
{
// This space intentionally left blank.
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Runtime exception.
*/
class RuntimeException extends \RuntimeException implements Exception
{
// This space intentionally left blank.
}

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Mustache syntax exception.
*/
class SyntaxException extends LogicException implements Exception
{
protected $token;
/**
* @param string $msg
* @param Exception $previous
*/
public function __construct($msg, array $token, $previous = null)
{
$this->token = $token;
parent::__construct($msg, 0, $previous);
}
/**
* @return array
*/
public function getToken()
{
return $this->token;
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Unknown filter exception.
*/
class UnknownFilterException extends \UnexpectedValueException implements Exception
{
protected $filterName;
/**
* @param string $filterName
* @param Exception $previous
*/
public function __construct($filterName, $previous = null)
{
$this->filterName = $filterName;
$message = sprintf('Unknown filter: %s', $filterName);
parent::__construct($message, 0, $previous);
}
public function getFilterName()
{
return $this->filterName;
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Unknown helper exception.
*/
class UnknownHelperException extends InvalidArgumentException implements Exception
{
protected $helperName;
/**
* @param string $helperName
* @param Exception $previous
*/
public function __construct($helperName, $previous = null)
{
$this->helperName = $helperName;
$message = sprintf('Unknown helper: %s', $helperName);
parent::__construct($message, 0, $previous);
}
public function getHelperName()
{
return $this->helperName;
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Unknown template exception.
*/
class UnknownTemplateException extends InvalidArgumentException implements Exception
{
protected $templateName;
/**
* @param string $templateName
* @param Exception $previous
*/
public function __construct($templateName, $previous = null)
{
$this->templateName = $templateName;
$message = sprintf('Unknown template: %s', $templateName);
parent::__construct($message, 0, $previous);
}
public function getTemplateName()
{
return $this->templateName;
}
}

View File

@ -0,0 +1,177 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\UnknownHelperException;
/**
* A collection of helpers for a Mustache instance.
*/
class HelperCollection
{
private $helpers = [];
/**
* Helper Collection constructor.
*
* Optionally accepts an array (or \Traversable) of `$name => $helper` pairs.
*
* @throws InvalidArgumentException if the $helpers argument isn't an array or \Traversable
*
* @param array|\Traversable $helpers (default: null)
*/
public function __construct($helpers = null)
{
if ($helpers === null) {
return;
}
if (!is_array($helpers) && !$helpers instanceof \Traversable) {
throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers');
}
foreach ($helpers as $name => $helper) {
$this->add($name, $helper);
}
}
/**
* Magic mutator.
*
* @see Mustache\HelperCollection::add
*
* @param string $name
* @param mixed $helper
*/
public function __set($name, $helper)
{
$this->add($name, $helper);
}
/**
* Add a helper to this collection.
*
* @param string $name
* @param mixed $helper
*/
public function add($name, $helper)
{
$this->helpers[$name] = $helper;
}
/**
* Magic accessor.
*
* @see Mustache\HelperCollection::get
*
* @param string $name
*
* @return mixed Helper
*/
public function __get($name)
{
return $this->get($name);
}
/**
* Get a helper by name.
*
* @throws UnknownHelperException If helper does not exist
*
* @param string $name
*
* @return mixed Helper
*/
public function get($name)
{
if (!$this->has($name)) {
throw new UnknownHelperException($name);
}
return $this->helpers[$name];
}
/**
* Magic isset().
*
* @see Mustache\HelperCollection::has
*
* @param string $name
*
* @return bool True if helper is present
*/
public function __isset($name)
{
return $this->has($name);
}
/**
* Check whether a given helper is present in the collection.
*
* @param string $name
*
* @return bool True if helper is present
*/
public function has($name)
{
return array_key_exists($name, $this->helpers);
}
/**
* Magic unset().
*
* @see Mustache\HelperCollection::remove
*
* @param string $name
*/
public function __unset($name)
{
$this->remove($name);
}
/**
* Check whether a given helper is present in the collection.
*
* @throws UnknownHelperException if the requested helper is not present
*
* @param string $name
*/
public function remove($name)
{
if (!$this->has($name)) {
throw new UnknownHelperException($name);
}
unset($this->helpers[$name]);
}
/**
* Clear the helper collection.
*
* Removes all helpers from this collection
*/
public function clear()
{
$this->helpers = [];
}
/**
* Check whether the helper collection is empty.
*
* @return bool True if the collection is empty
*/
public function isEmpty()
{
return empty($this->helpers);
}
}

View File

@ -0,0 +1,96 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
/**
* Mustache Lambda Helper.
*
* Passed as the second argument to section lambdas (higher order sections),
* giving them access to a `render` method for rendering a string with the
* current context.
*/
class LambdaHelper
{
private $mustache;
private $context;
private $delims;
/**
* Mustache Lambda Helper constructor.
*
* @param Engine $mustache Mustache engine instance
* @param Context $context Rendering context
* @param string $delims Optional custom delimiters, in the format `{{= <% %> =}}`. (default: null)
*/
public function __construct(Engine $mustache, Context $context, $delims = null)
{
$this->mustache = $mustache;
$this->context = $context;
$this->delims = $delims;
}
/**
* Render a string as a Mustache template with the current rendering context.
*
* @param string $string
*
* @return string Rendered template
*/
public function render($string)
{
$value = $this->mustache
->loadLambda((string) $string, $this->delims)
->renderInternal($this->context);
return $this->mustache->getDoubleRenderLambdas() ? $value : $this->preventRender($value);
}
/**
* Prevent rendering of a string as a Mustache template.
*
* This is useful for returning a raw string from a lambda without processing it as a Mustache template.
*
* @see RenderedString
*
* @param string $value The raw string value to return
*
* @return RenderedString A RenderedString instance containing the raw value
*/
public function preventRender($value)
{
return new RenderedString($value);
}
/**
* Render a string as a Mustache template with the current rendering context.
*
* @param string $string
*
* @return string Rendered template
*/
public function __invoke($string)
{
return $this->render($string);
}
/**
* Get a Lambda Helper with custom delimiters.
*
* @param string $delims Custom delimiters, in the format `{{= <% %> =}}`
*
* @return LambdaHelper
*/
public function withDelimiters($delims)
{
return new self($this->mustache, $this->context, $delims);
}
}

28
vendor/mustache/mustache/src/Loader.php vendored Normal file
View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\UnknownTemplateException;
interface Loader
{
/**
* Load a Template by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string|Source Mustache Template source
*/
public function load($name);
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* Mustache Template array Loader implementation.
*
* An ArrayLoader instance loads Mustache Template source by name from an initial array:
*
* $loader = new ArrayLoader(
* 'foo' => '{{ bar }}',
* 'baz' => 'Hey {{ qux }}!'
* );
*
* $tpl = $loader->load('foo'); // '{{ bar }}'
*
* The ArrayLoader is used internally as a partials loader by Mustache\Engine instance when an array of partials
* is set. It can also be used as a quick-and-dirty Template loader.
*/
class ArrayLoader implements Loader, MutableLoader
{
private $templates;
/**
* ArrayLoader constructor.
*
* @param array $templates Associative array of Template source (default: [])
*/
public function __construct(array $templates = [])
{
$this->templates = $templates;
}
/**
* Load a Template.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
if (!isset($this->templates[$name])) {
throw new UnknownTemplateException($name);
}
return $this->templates[$name];
}
/**
* Set an associative array of Template sources for this loader.
*/
public function setTemplates(array $templates)
{
$this->templates = $templates;
}
/**
* Set a Template source by name.
*
* @param string $name
* @param string $template Mustache Template source
*/
public function setTemplate($name, $template)
{
$this->templates[$name] = $template;
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* A Mustache Template cascading loader implementation, which delegates to other
* Loader instances.
*/
class CascadingLoader implements Loader
{
private $loaders;
/**
* Construct a CascadingLoader with an array of loaders.
*
* $loader = new CascadingLoader([
* new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
* new FilesystemLoader(__DIR__.'/templates')
* ]);
*
* @param Loader[] $loaders
*/
public function __construct(array $loaders = [])
{
$this->loaders = [];
foreach ($loaders as $loader) {
$this->addLoader($loader);
}
}
/**
* Add a Loader instance.
*/
public function addLoader(Loader $loader)
{
$this->loaders[] = $loader;
}
/**
* Load a Template by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
foreach ($this->loaders as $loader) {
try {
return $loader->load($name);
} catch (UnknownTemplateException $e) {
// do nothing, check the next loader.
}
}
throw new UnknownTemplateException($name);
}
}

View File

@ -0,0 +1,141 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\RuntimeException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* Mustache Template filesystem Loader implementation.
*
* A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
*
* $loader = new FilesystemLoader(__DIR__.'/views');
* $tpl = $loader->load('foo'); // equivalent to `file_get_contents(__DIR__.'/views/foo.mustache');
*
* This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
*
* $m = new \Mustache\Engine([
* 'loader' => new FilesystemLoader(__DIR__.'/views'),
* 'partials_loader' => new FilesystemLoader(__DIR__.'/views/partials'),
* ]);
*/
class FilesystemLoader implements Loader
{
private $baseDir;
private $extension = '.mustache';
private $templates = [];
/**
* Mustache filesystem Loader constructor.
*
* Passing an $options array allows overriding certain Loader options during instantiation:
*
* $options = [
* // The filename extension used for Mustache templates. Defaults to '.mustache'
* 'extension' => '.ms',
* ];
*
* @throws RuntimeException if $baseDir does not exist
*
* @param string $baseDir Base directory containing Mustache template files
* @param array $options Loader options (default: [])
*/
public function __construct($baseDir, array $options = [])
{
$this->baseDir = $baseDir;
if (strpos($this->baseDir, '://') === false) {
$this->baseDir = realpath($this->baseDir);
}
if ($this->shouldCheckPath() && !is_dir($this->baseDir)) {
throw new RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
}
if (array_key_exists('extension', $options)) {
if (empty($options['extension'])) {
$this->extension = '';
} else {
$this->extension = '.' . ltrim($options['extension'], '.');
}
}
}
/**
* Load a Template by name.
*
* $loader = new FilesystemLoader(__DIR__.'/views');
* $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
if (!isset($this->templates[$name])) {
$this->templates[$name] = $this->loadFile($name);
}
return $this->templates[$name];
}
/**
* Helper function for loading a Mustache file by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
protected function loadFile($name)
{
$fileName = $this->getFileName($name);
if ($this->shouldCheckPath() && !file_exists($fileName)) {
throw new UnknownTemplateException($name);
}
return file_get_contents($fileName);
}
/**
* Helper function for getting a Mustache template file name.
*
* @param string $name
*
* @return string Template file name
*/
protected function getFileName($name)
{
$fileName = $this->baseDir . '/' . $name;
if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
$fileName .= $this->extension;
}
return $fileName;
}
/**
* Only check if baseDir is a directory and requested templates are files if
* baseDir is using the filesystem stream wrapper.
*
* @return bool Whether to check `is_dir` and `file_exists`
*/
protected function shouldCheckPath()
{
return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0;
}
}

View File

@ -0,0 +1,129 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* A Mustache Template loader for inline templates.
*
* With the InlineLoader, templates can be defined at the end of any PHP source
* file:
*
* $loader = new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
* $hello = $loader->load('hello');
* $goodbye = $loader->load('goodbye');
*
* __halt_compiler();
*
* @@ hello
* Hello, {{ planet }}!
*
* @@ goodbye
* Goodbye, cruel {{ planet }}
*
* Templates are deliniated by lines containing only `@@ name`.
*
* The InlineLoader is well-suited to micro-frameworks such as Silex:
*
* $app->register(new MustacheServiceProvider, [
* 'mustache.loader' => new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
* ]);
*
* $app->get('/{name}', function ($name) use ($app) {
* return $app['mustache']->render('hello', compact('name'));
* })
* ->value('name', 'world');
*
* // ...
*
* __halt_compiler();
*
* @@ hello
* Hello, {{ name }}!
*/
class InlineLoader implements Loader
{
protected $fileName;
protected $offset;
protected $templates;
/**
* The InlineLoader requires a filename and offset to process templates.
*
* The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
* perfectly suited to the job:
*
* $loader = new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
*
* Note that this only works if the loader is instantiated inside the same
* file as the inline templates. If the templates are located in another
* file, it would be necessary to manually specify the filename and offset.
*
* @param string $fileName The file to parse for inline templates
* @param int $offset A string offset for the start of the templates.
* This usually coincides with the `__halt_compiler`
* call, and the `__COMPILER_HALT_OFFSET__`
*/
public function __construct($fileName, $offset)
{
if (!is_file($fileName)) {
throw new InvalidArgumentException('InlineLoader expects a valid filename.');
}
if (!is_int($offset) || $offset < 0) {
throw new InvalidArgumentException('InlineLoader expects a valid file offset.');
}
$this->fileName = $fileName;
$this->offset = $offset;
}
/**
* Load a Template by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
$this->loadTemplates();
if (!array_key_exists($name, $this->templates)) {
throw new UnknownTemplateException($name);
}
return $this->templates[$name];
}
/**
* Parse and load templates from the end of a source file.
*/
protected function loadTemplates()
{
if ($this->templates === null) {
$this->templates = [];
$data = file_get_contents($this->fileName, false, null, $this->offset);
foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
if (trim($chunk) !== '') {
list($name, $content) = explode("\n", $chunk, 2);
$this->templates[trim($name)] = trim($content);
}
}
}
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
interface MutableLoader
{
/**
* Set an associative array of Template sources for this loader.
*/
public function setTemplates(array $templates);
/**
* Set a Template source by name.
*
* @param string $name
* @param string $template Mustache Template source
*/
public function setTemplate($name, $template);
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\RuntimeException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Source;
use Mustache\Source\FilesystemSource;
/**
* Mustache Template production filesystem Loader implementation.
*
* A production-ready FilesystemLoader, which doesn't require reading a file if it already exists in the template cache.
*
* {@inheritdoc}
*/
class ProductionFilesystemLoader extends FilesystemLoader
{
private $statProps;
/**
* Mustache production filesystem Loader constructor.
*
* Passing an $options array allows overriding certain Loader options during instantiation:
*
* $options = [
* // The filename extension used for Mustache templates. Defaults to '.mustache'
* 'extension' => '.ms',
* 'stat_props' => ['size', 'mtime'],
* ];
*
* Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this
* uses 'mtime' and 'size', but this can be set to any of the properties supported by stat():
*
* http://php.net/manual/en/function.stat.php
*
* You can also disable filesystem stat entirely:
*
* $options = ['stat_props' => null];
*
* But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation,
* YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy
* process so you don't forget!
*
* @throws RuntimeException if $baseDir does not exist
*
* @param string $baseDir base directory containing Mustache template files
* @param array $options Loader options (default: [])
*/
public function __construct($baseDir, array $options = [])
{
parent::__construct($baseDir, $options);
if (array_key_exists('stat_props', $options)) {
if (empty($options['stat_props'])) {
$this->statProps = [];
} else {
$this->statProps = $options['stat_props'];
}
} else {
$this->statProps = ['size', 'mtime'];
}
}
/**
* Helper function for loading a Mustache file by name.
*
* @throws UnknownTemplateException if a template file is not found
*
* @param string $name
*
* @return Source Mustache Template source
*/
protected function loadFile($name)
{
$fileName = $this->getFileName($name);
if (!file_exists($fileName)) {
throw new UnknownTemplateException($name);
}
return new FilesystemSource($fileName, $this->statProps);
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Loader;
/**
* Mustache Template string Loader implementation.
*
* A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
*
* $loader = new StringLoader;
* $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
*
* This is the default Template Loader instance used by Mustache:
*
* $m = new \Mustache\Engine;
* $tpl = $m->loadTemplate('{{ foo }}');
* echo $tpl->render(['foo' => 'bar']); // "bar"
*/
class StringLoader implements Loader
{
/**
* Load a Template by source.
*
* @param string $name Mustache Template source
*
* @return string Mustache Template source
*/
public function load($name)
{
return $name;
}
}

102
vendor/mustache/mustache/src/Logger.php vendored Normal file
View File

@ -0,0 +1,102 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
interface Logger
{
/**
* Psr\Log compatible log levels.
*/
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
/**
* System is unusable.
*
* @param string $message
*/
public function emergency($message, array $context = []);
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
*/
public function alert($message, array $context = []);
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
*/
public function critical($message, array $context = []);
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
*/
public function error($message, array $context = []);
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
*/
public function warning($message, array $context = []);
/**
* Normal but significant events.
*
* @param string $message
*/
public function notice($message, array $context = []);
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
*/
public function info($message, array $context = []);
/**
* Detailed debug information.
*
* @param string $message
*/
public function debug($message, array $context = []);
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
*/
public function log($level, $message, array $context = []);
}

View File

@ -0,0 +1,117 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Logger;
use Mustache\Logger;
/**
* This is a simple Logger implementation that other Loggers can inherit from.
*
* This is identical to the Psr\Log\AbstractLogger.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
abstract class AbstractLogger implements Logger
{
/**
* System is unusable.
*
* @param string $message
*/
public function emergency($message, array $context = [])
{
$this->log(Logger::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
*/
public function alert($message, array $context = [])
{
$this->log(Logger::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
*/
public function critical($message, array $context = [])
{
$this->log(Logger::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
*/
public function error($message, array $context = [])
{
$this->log(Logger::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
*/
public function warning($message, array $context = [])
{
$this->log(Logger::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
*/
public function notice($message, array $context = [])
{
$this->log(Logger::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
*/
public function info($message, array $context = [])
{
$this->log(Logger::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
*/
public function debug($message, array $context = [])
{
$this->log(Logger::DEBUG, $message, $context);
}
}

View File

@ -0,0 +1,199 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Logger;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\LogicException;
use Mustache\Exception\RuntimeException;
use Mustache\Logger;
/**
* A Mustache Stream Logger.
*
* The Stream Logger wraps a file resource instance (such as a stream) or a
* stream URL. All log messages over the threshold level will be appended to
* this stream.
*
* Hint: Try `php://stderr` for your stream URL.
*/
class StreamLogger extends AbstractLogger
{
protected static $levels = [
self::DEBUG => 100,
self::INFO => 200,
self::NOTICE => 250,
self::WARNING => 300,
self::ERROR => 400,
self::CRITICAL => 500,
self::ALERT => 550,
self::EMERGENCY => 600,
];
protected $level;
protected $stream = null;
protected $url = null;
/**
* @throws InvalidArgumentException if the logging level is unknown
*
* @param resource|string $stream Resource instance or URL
* @param int $level The minimum logging level at which this handler will be triggered
*/
public function __construct($stream, $level = Logger::ERROR)
{
$this->setLevel($level);
if (is_resource($stream)) {
$this->stream = $stream;
} else {
$this->url = $stream;
}
}
/**
* Close stream resources.
*/
public function __destruct()
{
if (is_resource($this->stream)) {
fclose($this->stream);
}
}
/**
* Set the minimum logging level.
*
* @throws InvalidArgumentException if the logging level is unknown
*
* @param int $level The minimum logging level which will be written
*/
public function setLevel($level)
{
if (!array_key_exists($level, self::$levels)) {
throw new InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
}
$this->level = $level;
}
/**
* Get the current minimum logging level.
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* Logs with an arbitrary level.
*
* @throws InvalidArgumentException if the logging level is unknown
*
* @param mixed $level
* @param string $message
*/
public function log($level, $message, array $context = [])
{
if (!array_key_exists($level, self::$levels)) {
throw new InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
}
if (self::$levels[$level] >= self::$levels[$this->level]) {
$this->writeLog($level, $message, $context);
}
}
/**
* Write a record to the log.
*
* @throws LogicException If neither a stream resource nor url is present
* @throws RuntimeException If the stream url cannot be opened
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
protected function writeLog($level, $message, array $context = [])
{
if (!is_resource($this->stream)) {
if (!isset($this->url)) {
throw new LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
}
$this->stream = fopen($this->url, 'a');
if (!is_resource($this->stream)) {
// @codeCoverageIgnoreStart
throw new RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
// @codeCoverageIgnoreEnd
}
}
fwrite($this->stream, self::formatLine($level, $message, $context));
}
/**
* Gets the name of the logging level.
*
* @throws InvalidArgumentException if the logging level is unknown
*
* @param int $level
*
* @return string
*/
protected static function getLevelName($level)
{
return strtoupper($level);
}
/**
* Format a log line for output.
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
*
* @return string
*/
protected static function formatLine($level, $message, array $context = [])
{
return sprintf(
"%s: %s\n",
self::getLevelName($level),
self::interpolateMessage($message, $context)
);
}
/**
* Interpolate context values into the message placeholders.
*
* @param string $message
*
* @return string
*/
protected static function interpolateMessage($message, array $context = [])
{
if (strpos($message, '{') === false) {
return $message;
}
// build a replacement array with braces around the context keys
$replace = [];
foreach ($context as $key => $val) {
$replace['{' . $key . '}'] = $val;
}
// interpolate replacement values into the the message and return
return strtr($message, $replace);
}
}

392
vendor/mustache/mustache/src/Parser.php vendored Normal file
View File

@ -0,0 +1,392 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\SyntaxException;
/**
* Mustache Parser class.
*
* This class is responsible for turning a set of Mustache tokens into a parse tree.
*/
class Parser
{
private $lineNum;
private $lineTokens;
private $pragmas;
private $defaultPragmas = [];
// Optional Mustache specs
private $dynamicNames = true;
private $inheritance = true;
private $pragmaFilters;
/**
* Process an array of Mustache tokens and convert them into a parse tree.
*
* @param array $tokens Set of Mustache tokens
*
* @return array Mustache token parse tree
*/
public function parse(array $tokens = [])
{
$this->lineNum = -1;
$this->lineTokens = 0;
$this->pragmas = $this->defaultPragmas;
$this->pragmaFilters = isset($this->pragmas[Engine::PRAGMA_FILTERS]);
return $this->buildTree($tokens);
}
/**
* Disable optional Mustache specs.
*
* @internal Users should set options in Mustache\Engine, not here :)
*
* @param bool[] $options
*/
public function setOptions(array $options)
{
if (isset($options['dynamic_names'])) {
$this->dynamicNames = $options['dynamic_names'] !== false;
}
if (isset($options['inheritance'])) {
$this->inheritance = $options['inheritance'] !== false;
}
}
/**
* Enable pragmas across all templates, regardless of the presence of pragma
* tags in the individual templates.
*
* @internal Users should set global pragmas in Mustache\Engine, not here :)
*
* @param string[] $pragmas
*/
public function setPragmas(array $pragmas)
{
$this->pragmas = [];
foreach ($pragmas as $pragma) {
$this->enablePragma($pragma);
}
$this->defaultPragmas = $this->pragmas;
}
/**
* Helper method for recursively building a parse tree.
*
* @throws SyntaxException when nesting errors or mismatched section tags are encountered
*
* @param array &$tokens Set of Mustache tokens
* @param array $parent Parent token (default: null)
*
* @return array Mustache Token parse tree
*/
private function buildTree(array &$tokens, $parent = null)
{
$nodes = [];
while (!empty($tokens)) {
$token = array_shift($tokens);
if ($token[Tokenizer::LINE] === $this->lineNum) {
$this->lineTokens++;
} else {
$this->lineNum = $token[Tokenizer::LINE];
$this->lineTokens = 0;
}
if ($token[Tokenizer::TYPE] !== Tokenizer::T_COMMENT) {
if (isset($token[Tokenizer::NAME])) {
list($name, $isDynamic) = $this->getDynamicName($token);
if ($isDynamic) {
$token[Tokenizer::NAME] = $name;
$token[Tokenizer::DYNAMIC] = true;
}
}
if ($this->pragmaFilters && isset($token[Tokenizer::NAME])) {
list($name, $filters) = $this->getNameAndFilters($token[Tokenizer::NAME]);
if (!empty($filters)) {
$token[Tokenizer::NAME] = $name;
$token[Tokenizer::FILTERS] = $filters;
}
}
}
switch ($token[Tokenizer::TYPE]) {
case Tokenizer::T_DELIM_CHANGE:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens);
break;
case Tokenizer::T_SECTION:
case Tokenizer::T_INVERTED:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token);
break;
case Tokenizer::T_END_SECTION:
if (!isset($parent)) {
$msg = sprintf(
'Unexpected closing tag: /%s on line %d',
$token[Tokenizer::NAME],
$token[Tokenizer::LINE]
);
throw new SyntaxException($msg, $token);
}
$sameName = $token[Tokenizer::NAME] !== $parent[Tokenizer::NAME];
$tokenDynamic = isset($token[Tokenizer::DYNAMIC]) && $token[Tokenizer::DYNAMIC];
$parentDynamic = isset($parent[Tokenizer::DYNAMIC]) && $parent[Tokenizer::DYNAMIC];
if ($sameName || ($tokenDynamic !== $parentDynamic)) {
$msg = sprintf(
'Nesting error: %s%s (on line %d) vs. %s%s (on line %d)',
$parentDynamic ? '*' : '',
$parent[Tokenizer::NAME],
$parent[Tokenizer::LINE],
$tokenDynamic ? '*' : '',
$token[Tokenizer::NAME],
$token[Tokenizer::LINE]
);
throw new SyntaxException($msg, $token);
}
$this->clearStandaloneLines($nodes, $tokens);
$parent[Tokenizer::END] = $token[Tokenizer::INDEX];
$parent[Tokenizer::NODES] = $nodes;
return $parent;
case Tokenizer::T_PARTIAL:
$this->checkIfTokenIsAllowedInParent($parent, $token);
//store the whitespace prefix for laters!
if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
$token[Tokenizer::INDENT] = $indent[Tokenizer::VALUE];
}
$nodes[] = $token;
break;
case Tokenizer::T_PARENT:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$nodes[] = $this->buildTree($tokens, $token);
break;
case Tokenizer::T_BLOCK_VAR:
if ($this->inheritance) {
if (isset($parent) && $parent[Tokenizer::TYPE] === Tokenizer::T_PARENT) {
$token[Tokenizer::TYPE] = Tokenizer::T_BLOCK_ARG;
}
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token);
} else {
// pretend this was just a normal "escaped" token...
$token[Tokenizer::TYPE] = Tokenizer::T_ESCAPED;
// TODO: figure out how to figure out if there was a space after this dollar:
$token[Tokenizer::NAME] = '$' . $token[Tokenizer::NAME];
$nodes[] = $token;
}
break;
case Tokenizer::T_PRAGMA:
$this->enablePragma($token[Tokenizer::NAME]);
// no break
case Tokenizer::T_COMMENT:
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $token;
break;
default:
$nodes[] = $token;
break;
}
}
if (isset($parent)) {
$msg = sprintf(
'Missing closing tag: %s opened on line %d',
$parent[Tokenizer::NAME],
$parent[Tokenizer::LINE]
);
throw new SyntaxException($msg, $parent);
}
return $nodes;
}
/**
* Clear standalone line tokens.
*
* Returns a whitespace token for indenting partials, if applicable.
*
* @param array $nodes Parsed nodes
* @param array $tokens Tokens to be parsed
*
* @return array|null Resulting indent token, if any
*/
private function clearStandaloneLines(array &$nodes, array &$tokens)
{
if ($this->lineTokens > 1) {
// this is the third or later node on this line, so it can't be standalone
return;
}
$prev = null;
if ($this->lineTokens === 1) {
// this is the second node on this line, so it can't be standalone
// unless the previous node is whitespace.
if ($prev = end($nodes)) {
if (!$this->tokenIsWhitespace($prev)) {
return;
}
}
}
if ($next = reset($tokens)) {
// If we're on a new line, bail.
if ($next[Tokenizer::LINE] !== $this->lineNum) {
return;
}
// If the next token isn't whitespace, bail.
if (!$this->tokenIsWhitespace($next)) {
return;
}
if (count($tokens) !== 1) {
// Unless it's the last token in the template, the next token
// must end in newline for this to be standalone.
if (substr($next[Tokenizer::VALUE], -1) !== "\n") {
return;
}
}
// Discard the whitespace suffix
array_shift($tokens);
}
if ($prev) {
// Return the whitespace prefix, if any
return array_pop($nodes);
}
}
/**
* Check whether token is a whitespace token.
*
* True if token type is T_TEXT and value is all whitespace characters.
*
* @return bool True if token is a whitespace token
*/
private function tokenIsWhitespace(array $token)
{
if ($token[Tokenizer::TYPE] === Tokenizer::T_TEXT) {
return preg_match('/^\s*$/', $token[Tokenizer::VALUE]);
}
return false;
}
/**
* Check whether a token is allowed inside a parent tag.
*
* @throws SyntaxException if an invalid token is found inside a parent tag
*
* @param array|null $parent
*/
private function checkIfTokenIsAllowedInParent($parent, array $token)
{
if (isset($parent) && $parent[Tokenizer::TYPE] === Tokenizer::T_PARENT) {
throw new SyntaxException('Illegal content in < parent tag', $token);
}
}
/**
* Parse dynamic names.
*
* @throws SyntaxException when a tag does not allow *
* @throws SyntaxException on multiple *s, or dots or filters with *
*/
private function getDynamicName(array $token)
{
$name = $token[Tokenizer::NAME];
$isDynamic = false;
if ($this->dynamicNames && preg_match('/^\s*\*\s*/', $name)) {
$this->ensureTagAllowsDynamicNames($token);
$name = preg_replace('/^\s*\*\s*/', '', $name);
$isDynamic = true;
}
return [$name, $isDynamic];
}
/**
* Check whether the given token supports dynamic tag names.
*
* @throws SyntaxException when a tag does not allow *
*/
private function ensureTagAllowsDynamicNames(array $token)
{
switch ($token[Tokenizer::TYPE]) {
case Tokenizer::T_PARTIAL:
case Tokenizer::T_PARENT:
case Tokenizer::T_END_SECTION:
return;
}
$msg = sprintf(
'Invalid dynamic name: %s in %s tag',
$token[Tokenizer::NAME],
Tokenizer::getTagName($token[Tokenizer::TYPE])
);
throw new SyntaxException($msg, $token);
}
/**
* Split a tag name into name and filters.
*
* @param string $name
*
* @return array [Tag name, Array of filters]
*/
private function getNameAndFilters($name)
{
$filters = array_map('trim', explode('|', $name));
$name = array_shift($filters);
return [$name, $filters];
}
/**
* Enable a pragma.
*
* @param string $name
*/
private function enablePragma($name)
{
$this->pragmas[$name] = true;
switch ($name) {
case Engine::PRAGMA_FILTERS:
$this->pragmaFilters = true;
break;
}
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
/**
* A class representing a rendered string in Mustache.
*
* This is primarily used to prevent re-rendering of strings that have already
* been processed in higher-order sections.
*
* @see LambdaHelper::render()
* @see LambdaHelper::preventRender()
*/
class RenderedString
{
private $value;
/**
* RenderedString constructor.
*
* @param string $value The rendered string value
*/
public function __construct($value)
{
$this->value = (string) $value;
}
public function __toString()
{
return $this->value;
}
/**
* Get the rendered string value.
*
* @return string The rendered string value
*/
public function getValue()
{
return $this->value;
}
}

39
vendor/mustache/mustache/src/Source.php vendored Normal file
View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
interface Source
{
/**
* Get the Source key (used to generate the compiled class name).
*
* This must return a distinct key for each template source. For example, an
* MD5 hash of the template contents would probably do the trick. The
* ProductionFilesystemLoader uses mtime and file path. If your production
* source directory is under version control, you could use the current Git
* rev and the file path...
*
* @throws RuntimeException when a source file cannot be read
*
* @return string
*/
public function getKey();
/**
* Get the template Source.
*
* @throws RuntimeException when a source file cannot be read
*
* @return string
*/
public function getSource();
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Source;
use Mustache\Exception\RuntimeException;
use Mustache\Source;
/**
* Mustache template Filesystem Source.
*
* This template Source uses stat() to generate the Source key, so that using
* pre-compiled templates doesn't require hitting the disk to read the source.
* It is more suitable for production use, and is used by default in the
* ProductionFilesystemLoader.
*/
class FilesystemSource implements Source
{
private $fileName;
private $statProps;
private $stat;
/**
* Filesystem Source constructor.
*
* @param string $fileName
*/
public function __construct($fileName, array $statProps)
{
$this->fileName = $fileName;
$this->statProps = $statProps;
}
/**
* Get the Source key (used to generate the compiled class name).
*
* @throws RuntimeException when a source file cannot be read
*
* @return string
*/
public function getKey()
{
$chunks = [
'fileName' => $this->fileName,
];
if (!empty($this->statProps)) {
if (!isset($this->stat)) {
$this->stat = @stat($this->fileName);
}
if ($this->stat === false) {
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
}
foreach ($this->statProps as $prop) {
$chunks[$prop] = $this->stat[$prop];
}
}
return json_encode($chunks);
}
/**
* Get the template Source.
*
* @return string
*/
public function getSource()
{
return file_get_contents($this->fileName);
}
}

View File

@ -0,0 +1,193 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
/**
* Abstract Mustache Template class.
*
* @abstract
*/
abstract class Template
{
/**
* @var Engine
*/
protected $mustache;
/**
* @var bool
*/
protected $strictCallables = false;
/**
* @var bool
*/
protected $lambdas = true;
/**
* Mustache Template constructor.
*/
public function __construct(Engine $mustache)
{
$this->mustache = $mustache;
}
/**
* Mustache Template instances can be treated as a function and rendered by simply calling them.
*
* $m = new \Mustache\Engine;
* $tpl = $m->loadTemplate('Hello, {{ name }}!');
* echo $tpl(['name' => 'World']); // "Hello, World!"
*
* @see \Mustache\Template::render
*
* @param mixed $context Array or object rendering context (default: [])
*
* @return string Rendered template
*/
public function __invoke($context = [])
{
return $this->render($context);
}
/**
* Render this template given the rendering context.
*
* @param mixed $context Array or object rendering context (default: [])
*
* @return string Rendered template
*/
public function render($context = [])
{
return $this->renderInternal(
$this->prepareContextStack($context)
);
}
/**
* Internal rendering method implemented by Mustache Template concrete subclasses.
*
* This is where the magic happens :)
*
* NOTE: This method is not part of the Mustache.php public API.
*
* @param string $indent (default: '')
*
* @return string Rendered template
*/
abstract public function renderInternal(Context $context, $indent = '');
/**
* Tests whether a value should be iterated over (e.g. in a section context).
*
* In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
* should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
* Java, Python, etc.
*
* PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
* between between a list of things (numeric, normalized array) and a set of variables to be used as section context
* (associative array). In other words, this will be iterated over:
*
* $items = [
* ['name' => 'foo'],
* ['name' => 'bar'],
* ['name' => 'baz'],
* ];
*
* ... but this will be used as a section context block:
*
* $items = [
* 1 => ['name' => 'foo'],
* 'banana' => ['name' => 'bar'],
* 42 => ['name' => 'baz'],
* ];
*
* @param mixed $value
*
* @return bool True if the value is 'iterable'
*/
protected function isIterable($value)
{
switch (gettype($value)) {
case 'object':
return $value instanceof \Traversable;
case 'array':
$i = 0;
foreach ($value as $k => $v) {
if ($k !== $i++) {
return false;
}
}
return true;
default:
return false;
}
}
/**
* Helper method to prepare the Context stack.
*
* Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
*
* @param mixed $context Optional first context frame (default: null)
*
* @return Context
*/
protected function prepareContextStack($context = null)
{
$stack = new Context(null, $this->mustache->getBuggyPropertyShadowing());
$helpers = $this->mustache->getHelpers();
if (!$helpers->isEmpty()) {
$stack->push($helpers);
}
if (!empty($context)) {
$stack->push($context);
}
return $stack;
}
/**
* Resolve a context value.
*
* Invoke the value if it is callable, otherwise return the value.
*
* @param mixed $value
*
* @return string
*/
protected function resolveValue($value, Context $context)
{
if (!$this->lambdas) {
return $value;
}
if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
$result = call_user_func($value);
if (is_string($result)) {
return $this->mustache
->loadLambda($result)
->renderInternal($context);
}
return $result;
}
return $value;
}
}

View File

@ -0,0 +1,412 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\SyntaxException;
/**
* Mustache Tokenizer class.
*
* This class is responsible for turning raw template source into a set of Mustache tokens.
*/
class Tokenizer
{
// Finite state machine states
const IN_TEXT = 0;
const IN_TAG_TYPE = 1;
const IN_TAG = 2;
// Token types
const T_SECTION = '#';
const T_INVERTED = '^';
const T_END_SECTION = '/';
const T_COMMENT = '!';
const T_PARTIAL = '>';
const T_PARENT = '<';
const T_DELIM_CHANGE = '=';
const T_ESCAPED = '_v';
const T_UNESCAPED = '{';
const T_UNESCAPED_2 = '&';
const T_TEXT = '_t';
const T_PRAGMA = '%';
const T_BLOCK_VAR = '$';
const T_BLOCK_ARG = '$arg';
// Valid token types
private static $tagTypes = [
self::T_SECTION => true,
self::T_INVERTED => true,
self::T_END_SECTION => true,
self::T_COMMENT => true,
self::T_PARTIAL => true,
self::T_PARENT => true,
self::T_DELIM_CHANGE => true,
self::T_ESCAPED => true,
self::T_UNESCAPED => true,
self::T_UNESCAPED_2 => true,
self::T_PRAGMA => true,
self::T_BLOCK_VAR => true,
];
private static $tagNames = [
self::T_SECTION => 'section',
self::T_INVERTED => 'inverted section',
self::T_END_SECTION => 'section end',
self::T_COMMENT => 'comment',
self::T_PARTIAL => 'partial',
self::T_PARENT => 'parent',
self::T_DELIM_CHANGE => 'set delimiter',
self::T_ESCAPED => 'variable',
self::T_UNESCAPED => 'unescaped variable',
self::T_UNESCAPED_2 => 'unescaped variable',
self::T_PRAGMA => 'pragma',
self::T_BLOCK_VAR => 'block variable',
self::T_BLOCK_ARG => 'block variable',
];
// Token properties
const TYPE = 'type';
const NAME = 'name';
const DYNAMIC = 'dynamic';
const OTAG = 'otag';
const CTAG = 'ctag';
const LINE = 'line';
const INDEX = 'index';
const END = 'end';
const INDENT = 'indent';
const NODES = 'nodes';
const VALUE = 'value';
const FILTERS = 'filters';
private $state;
private $tagType;
private $buffer;
private $tokens;
private $seenTag;
private $line;
private $otag;
private $otagChar;
private $otagLen;
private $ctag;
private $ctagChar;
private $ctagLen;
/**
* Scan and tokenize template source.
*
* @throws SyntaxException when mismatched section tags are encountered
* @throws InvalidArgumentException when $delimiters string is invalid
*
* @param string $text Mustache template source to tokenize
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
*
* @return array Set of Mustache tokens
*/
public function scan($text, $delimiters = '')
{
// Setting mbstring.func_overload makes things *really* slow.
// Let's do everyone a favor and scan this string as ASCII instead.
//
// The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it
// when we remove support for older versions of PHP).
//
// @codeCoverageIgnoreStart
$encoding = null;
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) {
$encoding = mb_internal_encoding();
mb_internal_encoding('ASCII');
}
}
// @codeCoverageIgnoreEnd
$this->reset();
if (is_string($delimiters) && ($delimiters = trim($delimiters)) !== '') {
$this->setDelimiters($delimiters);
}
$len = strlen($text);
for ($i = 0; $i < $len; $i++) {
switch ($this->state) {
case self::IN_TEXT:
$char = $text[$i];
// Test whether it's time to change tags.
if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) {
$i--;
$this->flushBuffer();
$this->state = self::IN_TAG_TYPE;
} else {
$this->buffer .= $char;
if ($char === "\n") {
$this->flushBuffer();
$this->line++;
}
}
break;
case self::IN_TAG_TYPE:
$i += $this->otagLen - 1;
$char = $text[$i + 1];
if (isset(self::$tagTypes[$char])) {
$tag = $char;
$this->tagType = $tag;
} else {
$tag = null;
$this->tagType = self::T_ESCAPED;
}
if ($this->tagType === self::T_DELIM_CHANGE) {
$i = $this->changeDelimiters($text, $i);
$this->state = self::IN_TEXT;
} elseif ($this->tagType === self::T_PRAGMA) {
$i = $this->addPragma($text, $i);
$this->state = self::IN_TEXT;
} else {
if ($tag !== null) {
$i++;
}
$this->state = self::IN_TAG;
}
$this->seenTag = $i;
break;
default:
$char = $text[$i];
// Test whether it's time to change tags.
if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) {
$token = [
self::TYPE => $this->tagType,
self::NAME => trim($this->buffer),
self::OTAG => $this->otag,
self::CTAG => $this->ctag,
self::LINE => $this->line,
self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen,
];
if ($this->tagType === self::T_UNESCAPED) {
// Clean up `{{{ tripleStache }}}` style tokens.
if ($this->ctag === '}}') {
if (($i + 2 < $len) && $text[$i + 2] === '}') {
$i++;
} else {
$msg = sprintf(
'Mismatched tag delimiters: %s on line %d',
$token[self::NAME],
$token[self::LINE]
);
throw new SyntaxException($msg, $token);
}
} else {
$lastName = $token[self::NAME];
if (substr($lastName, -1) === '}') {
$token[self::NAME] = trim(substr($lastName, 0, -1));
} else {
$msg = sprintf(
'Mismatched tag delimiters: %s on line %d',
$token[self::NAME],
$token[self::LINE]
);
throw new SyntaxException($msg, $token);
}
}
}
$this->buffer = '';
$i += $this->ctagLen - 1;
$this->state = self::IN_TEXT;
$this->tokens[] = $token;
} else {
$this->buffer .= $char;
}
break;
}
}
if ($this->state !== self::IN_TEXT) {
$this->throwUnclosedTagException();
}
$this->flushBuffer();
// Restore the user's encoding...
// @codeCoverageIgnoreStart
if ($encoding) {
mb_internal_encoding($encoding);
}
// @codeCoverageIgnoreEnd
return $this->tokens;
}
/**
* Helper function to reset tokenizer internal state.
*/
private function reset()
{
$this->state = self::IN_TEXT;
$this->tagType = null;
$this->buffer = '';
$this->tokens = [];
$this->seenTag = false;
$this->line = 0;
$this->otag = '{{';
$this->otagChar = '{';
$this->otagLen = 2;
$this->ctag = '}}';
$this->ctagChar = '}';
$this->ctagLen = 2;
}
/**
* Flush the current buffer to a token.
*/
private function flushBuffer()
{
if (strlen($this->buffer) > 0) {
$this->tokens[] = [
self::TYPE => self::T_TEXT,
self::LINE => $this->line,
self::VALUE => $this->buffer,
];
$this->buffer = '';
}
}
/**
* Change the current Mustache delimiters. Set new `otag` and `ctag` values.
*
* @throws SyntaxException when delimiter string is invalid
*
* @param string $text Mustache template source
* @param int $index Current tokenizer index
*
* @return int New index value
*/
private function changeDelimiters($text, $index)
{
$startIndex = strpos($text, '=', $index) + 1;
$close = '=' . $this->ctag;
$closeIndex = strpos($text, $close, $index);
if ($closeIndex === false) {
$this->throwUnclosedTagException();
}
$token = [
self::TYPE => self::T_DELIM_CHANGE,
self::LINE => $this->line,
];
try {
$this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
} catch (InvalidArgumentException $e) {
throw new SyntaxException($e->getMessage(), $token);
}
$this->tokens[] = $token;
return $closeIndex + strlen($close) - 1;
}
/**
* Set the current Mustache `otag` and `ctag` delimiters.
*
* @throws InvalidArgumentException when delimiter string is invalid
*
* @param string $delimiters
*/
private function setDelimiters($delimiters)
{
if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) {
throw new InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters));
}
list($_, $otag, $ctag) = $matches;
$this->otag = $otag;
$this->otagChar = $otag[0];
$this->otagLen = strlen($otag);
$this->ctag = $ctag;
$this->ctagChar = $ctag[0];
$this->ctagLen = strlen($ctag);
}
/**
* Add pragma token.
*
* Pragmas are hoisted to the front of the template, so all pragma tokens
* will appear at the front of the token list.
*
* @param string $text
* @param int $index
*
* @return int New index value
*/
private function addPragma($text, $index)
{
$end = strpos($text, $this->ctag, $index);
if ($end === false) {
$this->throwUnclosedTagException();
}
$pragma = trim(substr($text, $index + 2, $end - $index - 2));
// Pragmas are hoisted to the front of the template.
array_unshift($this->tokens, [
self::TYPE => self::T_PRAGMA,
self::NAME => $pragma,
self::LINE => 0,
]);
return $end + $this->ctagLen - 1;
}
private function throwUnclosedTagException()
{
$name = trim($this->buffer);
if ($name !== '') {
$msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line);
} else {
$msg = sprintf('Unclosed tag on line %d', $this->line);
}
throw new SyntaxException($msg, [
self::TYPE => $this->tagType,
self::NAME => $name,
self::OTAG => $this->otag,
self::CTAG => $this->ctag,
self::LINE => $this->line,
self::INDEX => $this->seenTag - $this->otagLen,
]);
}
/**
* Get the human readable name for a tag type.
*
* @param string $tagType One of the tokenizer T_* constants
*
* @return string
*/
public static function getTagName($tagType)
{
return isset(self::$tagNames[$tagType]) ? self::$tagNames[$tagType] : 'unknown';
}
}

282
vendor/mustache/mustache/src/compat.php vendored Normal file
View File

@ -0,0 +1,282 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class_alias(\Mustache\Cache::class, \Mustache_Cache::class);
class_alias(\Mustache\Cache\AbstractCache::class, \Mustache_Cache_AbstractCache::class);
class_alias(\Mustache\Cache\FilesystemCache::class, \Mustache_Cache_FilesystemCache::class);
class_alias(\Mustache\Cache\NoopCache::class, \Mustache_Cache_NoopCache::class);
class_alias(\Mustache\Compiler::class, \Mustache_Compiler::class);
class_alias(\Mustache\Context::class, \Mustache_Context::class);
class_alias(\Mustache\Engine::class, \Mustache_Engine::class);
class_alias(\Mustache\Exception::class, \Mustache_Exception::class);
class_alias(\Mustache\Exception\InvalidArgumentException::class, \Mustache_Exception_InvalidArgumentException::class);
class_alias(\Mustache\Exception\LogicException::class, \Mustache_Exception_LogicException::class);
class_alias(\Mustache\Exception\RuntimeException::class, \Mustache_Exception_RuntimeException::class);
class_alias(\Mustache\Exception\SyntaxException::class, \Mustache_Exception_SyntaxException::class);
class_alias(\Mustache\Exception\UnknownFilterException::class, \Mustache_Exception_UnknownFilterException::class);
class_alias(\Mustache\Exception\UnknownHelperException::class, \Mustache_Exception_UnknownHelperException::class);
class_alias(\Mustache\Exception\UnknownTemplateException::class, \Mustache_Exception_UnknownTemplateException::class);
class_alias(\Mustache\HelperCollection::class, \Mustache_HelperCollection::class);
class_alias(\Mustache\LambdaHelper::class, \Mustache_LambdaHelper::class);
class_alias(\Mustache\Loader::class, \Mustache_Loader::class);
class_alias(\Mustache\Loader\ArrayLoader::class, \Mustache_Loader_ArrayLoader::class);
class_alias(\Mustache\Loader\CascadingLoader::class, \Mustache_Loader_CascadingLoader::class);
class_alias(\Mustache\Loader\FilesystemLoader::class, \Mustache_Loader_FilesystemLoader::class);
class_alias(\Mustache\Loader\InlineLoader::class, \Mustache_Loader_InlineLoader::class);
class_alias(\Mustache\Loader\MutableLoader::class, \Mustache_Loader_MutableLoader::class);
class_alias(\Mustache\Loader\ProductionFilesystemLoader::class, \Mustache_Loader_ProductionFilesystemLoader::class);
class_alias(\Mustache\Loader\StringLoader::class, \Mustache_Loader_StringLoader::class);
class_alias(\Mustache\Logger::class, \Mustache_Logger::class);
class_alias(\Mustache\Logger\AbstractLogger::class, \Mustache_Logger_AbstractLogger::class);
class_alias(\Mustache\Logger\StreamLogger::class, \Mustache_Logger_StreamLogger::class);
class_alias(\Mustache\Parser::class, \Mustache_Parser::class);
class_alias(\Mustache\Source::class, \Mustache_Source::class);
class_alias(\Mustache\Source\FilesystemSource::class, \Mustache_Source_FilesystemSource::class);
class_alias(\Mustache\Template::class, \Mustache_Template::class);
class_alias(\Mustache\Tokenizer::class, \Mustache_Tokenizer::class);
if (!class_exists(\Mustache_Engine::class)) {
/** @deprecated use Mustache\Engine */
class Mustache_Engine extends \Mustache\Engine
{
}
}
if (!interface_exists(\Mustache_Cache::class)) {
/** @deprecated use Mustache\Cache */
interface Mustache_Cache extends \Mustache\Cache
{
}
}
if (!class_exists(\Mustache_Cache_AbstractCache::class)) {
/** @deprecated use Mustache\Cache\AbstractCache */
abstract class Mustache_Cache_AbstractCache extends \Mustache\Cache\AbstractCache
{
}
}
if (!class_exists(\Mustache_Cache_FilesystemCache::class)) {
/** @deprecated use Mustache\Cache\FilesystemCache */
class Mustache_Cache_FilesystemCache extends \Mustache\Cache\FilesystemCache
{
}
}
if (!class_exists(\Mustache_Cache_NoopCache::class)) {
/** @deprecated use Mustache\Cache\NoopCache */
class Mustache_Cache_NoopCache extends \Mustache\Cache\NoopCache
{
}
}
if (!class_exists(\Mustache_Compiler::class)) {
/** @deprecated use Mustache\Compiler */
class Mustache_Compiler extends \Mustache\Compiler
{
}
}
if (!class_exists(\Mustache_Context::class)) {
/** @deprecated use Mustache\Context */
class Mustache_Context extends \Mustache\Context
{
}
}
if (!class_exists(\Mustache_Engine::class)) {
/** @deprecated use Mustache\Engine */
class Mustache_Engine extends \Mustache\Engine
{
}
}
if (!interface_exists(\Mustache_Exception::class)) {
/** @deprecated use Mustache\Exception */
interface Mustache_Exception extends \Mustache\Exception
{
}
}
if (!class_exists(\Mustache_Exception_InvalidArgumentException::class)) {
/** @deprecated use Mustache\Exception\InvalidArgumentException */
class Mustache_Exception_InvalidArgumentException extends \Mustache\Exception\InvalidArgumentException
{
}
}
if (!class_exists(\Mustache_Exception_LogicException::class)) {
/** @deprecated use Mustache\Exception\LogicException */
class Mustache_Exception_LogicException extends \Mustache\Exception\LogicException
{
}
}
if (!class_exists(\Mustache_Exception_RuntimeException::class)) {
/** @deprecated use Mustache\Exception\RuntimeException */
class Mustache_Exception_RuntimeException extends \Mustache\Exception\RuntimeException
{
}
}
if (!class_exists(\Mustache_Exception_SyntaxException::class)) {
/** @deprecated use Mustache\Exception\SyntaxException */
class Mustache_Exception_SyntaxException extends \Mustache\Exception\SyntaxException
{
}
}
if (!class_exists(\Mustache_Exception_UnknownFilterException::class)) {
/** @deprecated use Mustache\Exception\UnknownFilterException */
class Mustache_Exception_UnknownFilterException extends \Mustache\Exception\UnknownFilterException
{
}
}
if (!class_exists(\Mustache_Exception_UnknownHelperException::class)) {
/** @deprecated use Mustache\Exception\UnknownHelperException */
class Mustache_Exception_UnknownHelperException extends \Mustache\Exception\UnknownHelperException
{
}
}
if (!class_exists(\Mustache_Exception_UnknownTemplateException::class)) {
/** @deprecated use Mustache\Exception\UnknownTemplateException */
class Mustache_Exception_UnknownTemplateException extends \Mustache\Exception\UnknownTemplateException
{
}
}
if (!class_exists(\Mustache_HelperCollection::class)) {
/** @deprecated use Mustache\HelperCollection */
class Mustache_HelperCollection extends \Mustache\HelperCollection
{
}
}
if (!class_exists(\Mustache_LambdaHelper::class)) {
/** @deprecated use Mustache\LambdaHelper */
class Mustache_LambdaHelper extends \Mustache\LambdaHelper
{
}
}
if (!interface_exists(\Mustache_Loader::class)) {
/** @deprecated use Mustache\Loader */
interface Mustache_Loader extends \Mustache\Loader
{
}
}
if (!class_exists(\Mustache_Loader_ArrayLoader::class)) {
/** @deprecated use Mustache\Loader\ArrayLoader */
class Mustache_Loader_ArrayLoader extends \Mustache\Loader\ArrayLoader
{
}
}
if (!class_exists(\Mustache_Loader_CascadingLoader::class)) {
/** @deprecated use Mustache\Loader\CascadingLoader */
class Mustache_Loader_CascadingLoader extends \Mustache\Loader\CascadingLoader
{
}
}
if (!class_exists(\Mustache_Loader_FilesystemLoader::class)) {
/** @deprecated use Mustache\Loader\FilesystemLoader */
class Mustache_Loader_FilesystemLoader extends \Mustache\Loader\FilesystemLoader
{
}
}
if (!class_exists(\Mustache_Loader_InlineLoader::class)) {
/** @deprecated use Mustache\Loader\InlineLoader */
class Mustache_Loader_InlineLoader extends \Mustache\Loader\InlineLoader
{
}
}
if (!interface_exists(\Mustache_Loader_MutableLoader::class)) {
/** @deprecated use Mustache\Loader\MutableLoader */
interface Mustache_Loader_MutableLoader extends \Mustache\Loader\MutableLoader
{
}
}
if (!class_exists(\Mustache_Loader_ProductionFilesystemLoader::class)) {
/** @deprecated use Mustache\Loader\ProductionFilesystemLoader */
class Mustache_Loader_ProductionFilesystemLoader extends \Mustache\Loader\ProductionFilesystemLoader
{
}
}
if (!class_exists(\Mustache_Loader_StringLoader::class)) {
/** @deprecated use Mustache\Loader\StringLoader */
class Mustache_Loader_StringLoader extends \Mustache\Loader\StringLoader
{
}
}
if (!interface_exists(\Mustache_Logger::class)) {
/** @deprecated use Mustache\Logger */
interface Mustache_Logger extends \Mustache\Logger
{
}
}
if (!class_exists(\Mustache_Logger_AbstractLogger::class)) {
/** @deprecated use Mustache\Logger\AbstractLogger */
abstract class Mustache_Logger_AbstractLogger extends \Mustache\Logger\AbstractLogger
{
}
}
if (!class_exists(\Mustache_Logger_StreamLogger::class)) {
/** @deprecated use Mustache\Logger\StreamLogger */
class Mustache_Logger_StreamLogger extends \Mustache\Logger\StreamLogger
{
}
}
if (!class_exists(\Mustache_Parser::class)) {
/** @deprecated use Mustache\Parser */
class Mustache_Parser extends \Mustache\Parser
{
}
}
if (!interface_exists(\Mustache_Source::class)) {
/** @deprecated use Mustache\Source */
interface Mustache_Source extends \Mustache\Source
{
}
}
if (!class_exists(\Mustache_Source_FilesystemSource::class)) {
/** @deprecated use Mustache\Source\FilesystemSource */
class Mustache_Source_FilesystemSource extends \Mustache\Source\FilesystemSource
{
}
}
if (!class_exists(\Mustache_Template::class)) {
/** @deprecated use Mustache\Template */
abstract class Mustache_Template extends \Mustache\Template
{
}
}
if (!class_exists(\Mustache_Tokenizer::class)) {
/** @deprecated use Mustache\Tokenizer */
class Mustache_Tokenizer extends \Mustache\Tokenizer
{
}
}