diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 5ff28e28..2f20b5b4 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -8,8 +8,8 @@ jobs:
matrix:
# operating-systems: ubuntu-latest, windows-latest, macos-latest
operating-system: [ubuntu-latest]
- # php-versions: 7.4, 8.0, 8.1
- php-versions: ['7.4', '8.0', '8.1']
+ # php-versions: 8.0, 8.1, 8.2
+ php-versions: ['8.0', '8.1', '8.2']
runs-on: ${{ matrix.operating-system }}
steps:
- name: Checkout
@@ -61,7 +61,7 @@ jobs:
- name: Setup PHP and Composer
uses: shivammathur/setup-php@v2
with:
- php-version: 7.4
+ php-version: 8.0
tools: composer:v2
- name: Install PHP dependencies
diff --git a/README.md b/README.md
index 462c5f6d..7005725e 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ TBD
The easiest way to install Herbie is through Composer.
Run the following commands in your terminal to create a new project and install all dependent libraries.
-For the upcoming 2.x version (PHP 7.4, 8.0, 8.1):
+For the upcoming 2.x version (PHP 8.x):
composer create-project getherbie/start-website:dev-master mywebsite
diff --git a/composer.json b/composer.json
index c478d249..b5b3b203 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,7 @@
}
},
"require": {
- "php": ">=7.4",
+ "php": ">=8.0",
"ext-json": "*",
"composer-runtime-api": "^2.0",
"ausi/slug-generator": "^1.1",
diff --git a/plugins/imagine/ImagineSysPlugin.php b/plugins/imagine/ImagineSysPlugin.php
index bf1bd170..728ab080 100755
--- a/plugins/imagine/ImagineSysPlugin.php
+++ b/plugins/imagine/ImagineSysPlugin.php
@@ -50,13 +50,13 @@ public function twigFunctions(): array
];
}
- public function imagineFunction(string $path, string $filterSet = 'default', array $attribs = []): string
+ public function imagineFunction(string $path, string $collection = 'default', array $attribs = []): string
{
- $abspath = $this->alias->get('@media/' . $path);
+ $absolutePath = $this->alias->get('@media/' . $path);
$attribs['class'] = $attribs['class'] ?? 'imagine';
- if (!is_file($abspath)) {
+ if (!is_file($absolutePath)) {
$attribs['class'] = trim($attribs['class'] . ' imagine--file-not-found');
return sprintf(
'',
@@ -65,7 +65,7 @@ public function imagineFunction(string $path, string $filterSet = 'default', arr
);
}
- $sanitizedFilter = $this->sanitizeFilterName($filterSet);
+ $sanitizedFilter = $this->sanitizeFilterName($collection);
$attribs['class'] = trim($attribs['class'] . ' imagine--filter-' . $sanitizedFilter);
@@ -98,16 +98,16 @@ private function getTransparentOnePixelSrc(): string
/**
* Gets the browser path for the image and filter to apply.
*/
- public function imagineFilter(string $path, string $filterSet = 'default'): Markup
+ public function imagineFilter(string $path, string $collection = 'default'): Markup
{
- $abspath = $this->alias->get('@media/' . $path);
+ $absolutePath = $this->alias->get('@media/' . $path);
- if (!is_file($abspath)) {
+ if (!is_file($absolutePath)) {
$dataSrc = $this->getTransparentOnePixelSrc();
return new Markup($dataSrc, 'utf8');
}
- $sanitizedFilterSet = $this->sanitizeFilterName($filterSet);
+ $sanitizedFilterSet = $this->sanitizeFilterName($collection);
return new Markup(
$this->basePath . $this->applyFilterSet($path, $sanitizedFilterSet),
@@ -118,7 +118,7 @@ public function imagineFilter(string $path, string $filterSet = 'default'): Mark
private function sanitizeFilterName(string $filterSet): string
{
if ($filterSet !== 'default') {
- if ($this->config->check("plugins.imagine.filterSets.{$filterSet}") === false) {
+ if ($this->config->check("plugins.imagine.collections.{$filterSet}") === false) {
$filterSet = 'default';
}
}
@@ -141,7 +141,7 @@ protected function applyFilterSet(string $relpath, string $filterSet): string
{
$path = $this->alias->get('@media/' . $relpath);
- $filterConfig = $this->config->getAsArray("plugins.imagine.filterSets.{$filterSet}");
+ $filterConfig = $this->config->getAsArray("plugins.imagine.collections.{$filterSet}");
$cachePath = $this->resolveCachePath($relpath, $filterSet);
if (!empty($filterConfig['test'])) {
diff --git a/plugins/imagine/README.md b/plugins/imagine/README.md
index b87155a4..1f1fae22 100644
--- a/plugins/imagine/README.md
+++ b/plugins/imagine/README.md
@@ -27,7 +27,7 @@ return [
'plugins' => [
'imagine' => [
'cachePath' => 'cache/imagine',
- 'filterSets' => []
+ 'collections' => []
]
]
];
@@ -35,14 +35,14 @@ return [
## Filter sets
-To use Imagine in Herbie, Filter sets must be defined, each containing one or more filters.
+To use Imagine in Herbie, one or more filter collections must be defined, each containing one or more filters.
-The following default filter set is always enabled.
+The following default collection is always enabled.
~~~php
return [
// ...
- 'filterSets' => [
+ 'collections' => [
'default' => [
'test' => true,
'filters' => [
@@ -57,11 +57,11 @@ return [
];
~~~
-In the following configuration example, we see two simple filter sets for scaling and cropping an image.
+In the following configuration example, we see two simple collections for scaling and cropping an image.
~~~php
'imagine'
- 'filterSets' => [
+ 'collections' => [
'resize' => [
'filters' => [
'thumbnail' => [
@@ -82,10 +82,10 @@ In the following configuration example, we see two simple filter sets for scalin
],
~~~
-With the above configuration you set two Imagine filters `resize` and `crop` that can be applied to images in your project.
+With the above configuration you set two Imagine collections `resize` and `crop` that can be applied to images in your project.
-- A resize filterSet to resize an image to a size of 280 x 280 pixels
-- A crop filterSet to crop an image to a size of 560 x 560 pixels
+- A resize collection to resize an image to a size of 280 x 280 pixels
+- A crop collection to crop an image to a size of 560 x 560 pixels
## Usage
@@ -107,9 +107,9 @@ With the activation of the system plugin, one Twig filter and one Twig function
|
- filterSet |
+ collection |
string |
- The filter set to be applied. |
+ The filter collection to be applied. |
default |
diff --git a/plugins/imagine/config.php b/plugins/imagine/config.php
index 7e9761ed..d25f0fce 100644
--- a/plugins/imagine/config.php
+++ b/plugins/imagine/config.php
@@ -8,7 +8,7 @@
'pluginClass' => ImagineSysPlugin::class,
'pluginPath' => __DIR__,
'cachePath' => 'cache/imagine',
- 'filterSets' => [
+ 'collections' => [
'default' => [
'test' => true,
'filters' => [
diff --git a/plugins/twig/README.md b/plugins/twig/README.md
new file mode 100644
index 00000000..11e1b89f
--- /dev/null
+++ b/plugins/twig/README.md
@@ -0,0 +1,19 @@
+# Twig System Plugin
+
+`Twig` is a [Herbie](http://github.com/getherbie) system plugin that brings support for several Twig filters, functions and tests.
+
+## Installation
+
+The plugin is installed already.
+
+To activate it, add `twig` to the `enabledSysPlugins` configuration option.
+
+~~~php
+return [
+ 'enabledSysPlugins' => 'twig'
+];
+~~~
+
+## More Information
+
+For more information, see .
diff --git a/plugins/twig/TwigExtension.php b/plugins/twig/TwigExtension.php
new file mode 100644
index 00000000..57576999
--- /dev/null
+++ b/plugins/twig/TwigExtension.php
@@ -0,0 +1,665 @@
+alias = $alias;
+ $this->assets = $assets;
+ $this->config = $config;
+ $this->environment = $environment;
+ $this->pageRepository = $pageRepository;
+ $this->slugGenerator = $slugGenerator;
+ $this->translator = $translator;
+ $this->urlManager = $urlManager;
+ }
+
+ /**
+ * @return TwigFilter[]
+ */
+ public function getFilters(): array
+ {
+ return [
+ new TwigFilter('format_size', [$this, 'filterFilesize']),
+ new TwigFilter('slugify', [$this, 'filterSlugify']),
+ new TwigFilter('visible', [$this, 'filterVisible'], ['deprecated' => true]) // doesn't work properly
+ ];
+ }
+
+ /**
+ * @return TwigFunction[]
+ */
+ public function getFunctions(): array
+ {
+ return [
+ new TwigFunction('css_add', [$this, 'cssAdd']),
+ new TwigFunction('css_classes', [$this, 'cssClasses'], ['needs_context' => true]),
+ new TwigFunction('css_out', [$this, 'cssOut'], ['is_safe' => ['html']]),
+ new TwigFunction('file', [$this, 'file'], ['is_safe' => ['html']]),
+ new TwigFunction('image', [$this, 'image'], ['is_safe' => ['html']]),
+ new TwigFunction('js_add', [$this, 'jsAdd']),
+ new TwigFunction('js_out', [$this, 'jsOut'], ['is_safe' => ['html']]),
+ new TwigFunction('link_file', [$this, 'linkFile'], ['is_safe' => ['html']]),
+ new TwigFunction('link_mail', [$this, 'linkMail'], ['is_safe' => ['html']]),
+ new TwigFunction('link_page', [$this, 'linkPage'], ['is_safe' => ['html']]),
+ new TwigFunction('menu_ascii_tree', [$this, 'menuAsciiTree'], ['is_safe' => ['html']]),
+ new TwigFunction('menu_breadcrumb', [$this, 'menuBreadcrumb'], ['is_safe' => ['html']]),
+ new TwigFunction('menu_list', [$this, 'menuList'], ['is_safe' => ['html']]),
+ new TwigFunction('menu_pager', [$this, 'menuPager'], ['is_safe' => ['html']]),
+ new TwigFunction('menu_sitemap', [$this, 'menuSitemap'], ['is_safe' => ['html']]),
+ new TwigFunction('menu_tree', [$this, 'menuTree'], ['is_safe' => ['html']]),
+ new TwigFunction('page_title', [$this, 'pageTitle'], ['needs_context' => true]),
+ new TwigFunction('query', [$this, 'query']),
+ new TwigFunction('snippet', [$this, 'snippet'], ['is_safe' => ['all']]),
+ new TwigFunction('translate', [$this, 'translate']),
+ new TwigFunction('url_rel', [$this, 'urlRelative']),
+ new TwigFunction('url_abs', [$this, 'urlAbsolute']),
+ ];
+ }
+
+ /**
+ * @return TwigTest[]
+ */
+ public function getTests(): array
+ {
+ return [
+ new TwigTest('file_readable', [$this, 'testIsReadable']),
+ new TwigTest('file_writable', [$this, 'testIsWritable'])
+ ];
+ }
+
+ /**
+ * @param array $attribs
+ */
+ private function buildHtmlAttributes(array $attribs = []): string
+ {
+ $attribsAsString = '';
+ foreach ($attribs as $key => $value) {
+ $attribsAsString .= $key . '="' . $value . '" ';
+ }
+ return trim($attribsAsString);
+ }
+
+ public function filterFilesize(int $size): string
+ {
+ if ($size <= 0) {
+ return '0';
+ }
+ if ($size === 1) {
+ return '1 Byte';
+ }
+ $mod = 1024;
+ $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+ for ($i = 0; $size > $mod && $i < count($units) - 1; ++$i) {
+ $size /= $mod;
+ }
+ return str_replace(',', '.', (string)round($size, 1)) . ' ' . $units[$i];
+ }
+
+ /**
+ * Creates a web friendly URL (slug) from a string.
+ */
+ public function filterSlugify(string $url): string
+ {
+ return $this->slugGenerator->generate($url);
+ }
+
+ public function filterVisible(PageTree $tree): PageTreeFilterIterator
+ {
+ $treeIterator = new PageTreeIterator($tree);
+ return new PageTreeFilterIterator($treeIterator);
+ }
+
+ public function cssAdd(
+ array|string $paths,
+ array $attr = [],
+ ?string $group = null,
+ bool $raw = false,
+ int $pos = 1
+ ): void {
+ $this->assets->addCss($paths, $attr, $group, $raw, $pos);
+ }
+
+ public function jsAdd(
+ array|string $paths,
+ array $attr = [],
+ ?string $group = null,
+ bool $raw = false,
+ int $pos = 1
+ ): void {
+ $this->assets->addJs($paths, $attr, $group, $raw, $pos);
+ }
+
+ /**
+ * @param array $context
+ * @return string
+ */
+ public function cssClasses(array $context): string
+ {
+ $page = 'error';
+ if (isset($context['page'])) {
+ $route = $context['page']->getRoute();
+ $page = !empty($route) ? $route : 'index';
+ }
+
+ $layout = 'default';
+ if (isset($context['page'])) {
+ $layout = $context['page']->getLayout();
+ }
+
+ $theme = 'default';
+ if (!empty($context['theme'])) {
+ $theme = $context['theme'];
+ }
+
+ $language = 'en';
+ if (isset($context['site'])) {
+ $language = $context['site']->getLanguage();
+ }
+
+ $class = sprintf('page-%s theme-%s layout-%s language-%s', $page, $theme, $layout, $language);
+ return str_replace(['/', '.'], '-', $class);
+ }
+
+ public function linkFile(
+ string $path,
+ string $label = '',
+ bool $info = false,
+ array $attribs = []
+ ): string {
+ $attribs['alt'] = $attribs['alt'] ?? '';
+ $attribs['class'] = $attribs['class'] ?? 'link__label';
+
+ $baseUrl = str_trailing_slash($this->config->getAsString('components.downloadMiddleware.route'));
+ $storagePath = str_trailing_slash($this->config->getAsString('components.downloadMiddleware.storagePath'));
+
+ // combine url and path
+ $href = $this->urlManager->createUrl($baseUrl . $path);
+ $path = $this->alias->get($storagePath . $path);
+
+ if (!empty($info)) {
+ $fileInfo = $this->getFileInfo($path);
+ }
+
+ $replace = [
+ '{href}' => $href,
+ '{attribs}' => $this->buildHtmlAttributes($attribs),
+ '{label}' => empty($label) ? basename($path) : $label,
+ '{info}' => empty($fileInfo) ? '' : sprintf('%s', $fileInfo)
+ ];
+ return strtr('{label}{info}', $replace);
+ }
+
+ public function linkMail(
+ string $email,
+ ?string $label = null,
+ array $attribs = [],
+ string $template = '@snippet/link_mail.twig'
+ ): string {
+ $attribs['href'] = 'mailto:' . $email;
+ $attribs['class'] = $attribs['class'] ?? 'link__label';
+
+ ksort($attribs);
+
+ $context = [
+ 'attribs' => $attribs,
+ 'label' => $label ?? $email,
+ ];
+
+ try {
+ return $this->environment->render($template, $context);
+ } catch (Error $e) {
+ return $email;
+ }
+ }
+
+ public function menuAsciiTree(
+ string $route = '',
+ int $depth = -1,
+ bool $hidden = false
+ ): string {
+ // TODO use $class parameter
+ $branch = $this->pageRepository->findAll()->getPageTree()->findByRoute($route);
+ if ($branch === null) {
+ return '';
+ }
+
+ $treeIterator = new PageTreeIterator($branch);
+ $filterIterator = new PageTreeFilterIterator($treeIterator, !$hidden);
+
+ $asciiTree = new PageTreeTextRenderer($filterIterator);
+ $asciiTree->setMaxDepth($depth);
+ return $asciiTree->render();
+ }
+
+ /**
+ * @param array{0: string, 1?: string}|string $home
+ */
+ public function menuBreadcrumb(
+ string $delim = '',
+ array|string $home = '',
+ bool $reverse = false
+ ): string {
+ // TODO use string type for param $homeLink (like "route|label")
+
+ $links = [];
+
+ if (!empty($home)) {
+ if (is_array($home)) {
+ $route = reset($home);
+ $label = isset($home[1]) ? $home[1] : 'Home';
+ } else {
+ $route = $home;
+ $label = 'Home';
+ }
+ $links[] = $this->createLink($route, $label);
+ }
+
+ [$route] = $this->urlManager->parseRequest();
+ $pageTrail = $this->pageRepository->findAll()->getPageTrail($route);
+ foreach ($pageTrail as $item) {
+ $links[] = $this->createLink($item->getRoute(), $item->getMenuTitle());
+ }
+
+ if (!empty($reverse)) {
+ $links = array_reverse($links);
+ }
+
+ $html = '';
+ foreach ($links as $i => $link) {
+ if ($i > 0 && !empty($delim)) {
+ $html .= '- ' . $delim . '
';
+ }
+ $html .= '- ' . $link . '
';
+ }
+ $html .= '
';
+
+ return $html;
+ }
+
+ /**
+ * @throws LoaderError
+ * @throws RuntimeError
+ * @throws SyntaxError
+ */
+ public function menuList(
+ ?PageList $page_list = null,
+ string $filter = '',
+ string $sort = '',
+ bool $shuffle = false,
+ int $limit = 10,
+ string $template = '@snippet/listing.twig'
+ ): string {
+ if ($page_list === null) {
+ $page_list = $this->pageRepository->findAll();
+ }
+
+ if (!empty($filter)) {
+ [$field, $value] = explode('|', $filter);
+ $page_list = $page_list->filter($field, $value);
+ }
+
+ if (!empty($sort)) {
+ [$field, $direction] = explode('|', $sort);
+ $page_list = $page_list->sort($field, $direction);
+ }
+
+ if ($shuffle) {
+ $page_list = $page_list->shuffle();
+ }
+
+ // filter pages with empty title
+ $page_list = $page_list->filter(function (Page $page) {
+ return !empty($page->getTitle());
+ });
+
+ $pagination = new Pagination($page_list);
+ $pagination->setLimit($limit);
+
+ return $this->environment->render($template, ['pagination' => $pagination]);
+ }
+
+ public function menuTree(
+ string $route = '',
+ int $depth = -1,
+ bool $hidden = false,
+ string $class = 'menu'
+ ): string {
+ // NOTE duplicated code, see function sitemap
+ $branch = $this->pageRepository->findAll()->getPageTree()->findByRoute($route);
+ if ($branch === null) {
+ return '';
+ }
+
+ $treeIterator = new PageTreeIterator($branch);
+ $filterIterator = new PageTreeFilterIterator($treeIterator, !$hidden);
+
+ $htmlTree = new PageTreeHtmlRenderer($filterIterator);
+ $htmlTree->setMaxDepth($depth);
+ $htmlTree->setClass($class);
+ $htmlTree->setItemCallback(function (PageTree $node) {
+ $menuItem = $node->getMenuItem();
+ $href = $this->urlManager->createUrl($menuItem->getRoute());
+ return sprintf('%s', $href, $menuItem->getMenuTitle());
+ });
+
+ [$currenRoute] = $this->urlManager->parseRequest();
+ return $htmlTree->render($currenRoute);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function menuPager(
+ string $limit = '',
+ string $prev_label = '',
+ string $next_label = '',
+ string $prev_icon = '',
+ string $next_icon = '',
+ string $class = 'pager',
+ string $template = '{prev}{next}
'
+ ): string {
+ [$route] = $this->urlManager->parseRequest();
+ $pageList = $this->pageRepository->findAll();
+
+ if ($limit !== '') {
+ $pageList = $pageList->filter(function ($page) use ($limit) {
+ return strpos($page->getRoute(), $limit) === 0;
+ });
+ }
+
+ $prevPage = null;
+ $currentPage = null;
+ $nextPage = null;
+ $lastPage = null;
+ foreach ($pageList as $key => $page) {
+ if ($currentPage) {
+ $nextPage = $page;
+ break;
+ }
+ if ($key === $route) {
+ $prevPage = $lastPage;
+ $currentPage = $page;
+ continue;
+ }
+ $lastPage = $page;
+ }
+
+ $replacements = [
+ '{class}' => $class,
+ '{prev}' => '',
+ '{next}' => ''
+ ];
+
+ if (isset($prevPage)) {
+ $label = empty($prev_label) ? $prevPage->getMenuTitle() : $prev_label;
+ $label = sprintf('%s', $class, $label);
+ if ($prev_icon) {
+ $label = sprintf('%s%s', $class, $prev_icon, $label);
+ }
+ $attribs = ['class' => $class . '-link-prev'];
+ $replacements['{prev}'] = $this->createLink($prevPage->getRoute(), $label, $attribs);
+ }
+
+ if (isset($nextPage)) {
+ $label = empty($next_label) ? $nextPage->getMenuTitle() : $next_label;
+ $label = sprintf('%s', $class, $label);
+ if ($next_icon) {
+ $label = sprintf('%s%s', $label, $class, $next_icon);
+ }
+ $attribs = ['class' => $class . '-link-next'];
+ $replacements['{next}'] = $this->createLink($nextPage->getRoute(), $label, $attribs);
+ }
+
+ return strtr($template, $replacements);
+ }
+
+ public function menuSitemap(
+ string $route = '',
+ int $depth = -1,
+ bool $hidden = false,
+ string $class = 'sitemap'
+ ): string {
+ return $this->menuTree($route, $depth, $hidden, $class);
+ }
+
+ public function cssOut(?string $group = null, bool $timestamp = false): string
+ {
+ return $this->assets->outputCss($group, $timestamp);
+ }
+
+ public function jsOut(?string $group = null, bool $timestamp = false): string
+ {
+ return $this->assets->outputJs($group, $timestamp);
+ }
+
+ /**
+ * @param array $context
+ * @throws LoaderError
+ * @throws RuntimeError
+ * @throws SyntaxError
+ */
+ public function snippet(string $path, array $context = []): string
+ {
+ return $this->environment->render($path, $context);
+ }
+
+ public function image(
+ string $src,
+ int $width = 0,
+ int $height = 0,
+ string $alt = '',
+ string $class = ''
+ ): string {
+ $attribs = [];
+ $attribs['src'] = $this->urlManager->createUrl('/') . $src;
+ $attribs['alt'] = $alt;
+ if (!empty($width)) {
+ $attribs['width'] = (string)$width;
+ }
+ if (!empty($height)) {
+ $attribs['height'] = (string)$height;
+ }
+ if (!empty($class)) {
+ $attribs['class'] = $class;
+ }
+ return sprintf('', $this->buildHtmlAttributes($attribs));
+ }
+
+ public function linkPage(string $route, string $label, array $attribs = []): string
+ {
+ $scheme = parse_url($route, PHP_URL_SCHEME);
+ if ($scheme === null) {
+ $class = 'link--internal';
+ $href = $this->urlManager->createUrl($route);
+ } else {
+ $class = 'link--external';
+ $href = $route;
+ }
+
+ $attribs['class'] = $attribs['class'] ?? '';
+ $attribs['class'] = trim($attribs['class'] . ' link__label');
+
+ $replace = [
+ '{class}' => $class,
+ '{href}' => $href,
+ '{attribs}' => $this->buildHtmlAttributes($attribs),
+ '{label}' => $label,
+ ];
+
+ $template = '{label}';
+ return strtr($template, $replace);
+ }
+
+ /**
+ * @param array{site: Site} $context
+ */
+ public function pageTitle(
+ array $context,
+ string $delim = ' / ',
+ string $site_title = '',
+ string $root_title = '',
+ bool $reverse = false
+ ): string {
+ $pageTrail = $context['site']->getPageTrail();
+ $count = count($pageTrail);
+
+ $titles = [];
+
+ if (!empty($site_title)) {
+ $titles[] = $site_title;
+ }
+
+ foreach ($pageTrail as $item) {
+ if ((1 === $count) && $item->isStartPage() && !empty($root_title)) {
+ return $root_title;
+ }
+ $titles[] = $item->getTitle();
+ }
+
+ if (!empty($reverse)) {
+ $titles = array_reverse($titles);
+ }
+
+ return implode($delim, $titles);
+ }
+
+ public function query(iterable $iterator): QueryBuilder
+ {
+ if ($iterator instanceof Traversable) {
+ $data = iterator_to_array($iterator);
+ } else {
+ $data = (array)$iterator;
+ }
+ return (new QueryBuilder())->from($data);
+ }
+
+ public function translate(string $category = '', string $message = '', array $params = []): string
+ {
+ return $this->translator->translate($category, $message, $params);
+ }
+
+ public function urlRelative(string $route = ''): string
+ {
+ return $this->urlManager->createUrl($route);
+ }
+
+ public function urlAbsolute(string $route = ''): string
+ {
+ return $this->urlManager->createAbsoluteUrl($route);
+ }
+
+ public function file(string $path, string $label = '', bool $info = false, array $attribs = []): string
+ {
+ $attribs['class'] = $attribs['class'] ?? 'link__label';
+
+ if (!empty($info)) {
+ $fileInfo = $this->getFileInfo($path);
+ }
+
+ $replace = [
+ '{href}' => $path,
+ '{attribs}' => $this->buildHtmlAttributes($attribs),
+ '{label}' => empty($label) ? basename($path) : $label,
+ '{info}' => empty($fileInfo) ? '' : sprintf('%s', $fileInfo)
+ ];
+ return strtr('{label}{info}', $replace);
+ }
+
+ private function getFileInfo(string $path): string
+ {
+ if (!is_readable($path)) {
+ return '';
+ }
+ $replace = [
+ '{size}' => $this->filterFilesize(file_size($path)),
+ '{extension}' => strtoupper(pathinfo($path, PATHINFO_EXTENSION))
+ ];
+ return strtr(' ({extension}, {size})', $replace);
+ }
+
+ public function testIsReadable(string $alias): bool
+ {
+ if ($alias === '') {
+ return false;
+ }
+ $filename = $this->alias->get($alias);
+ return is_readable($filename);
+ }
+
+ public function testIsWritable(string $alias): bool
+ {
+ if ($alias === '') {
+ return false;
+ }
+ $filename = $this->alias->get($alias);
+ return is_writable($filename);
+ }
+
+ /**
+ * @param array $attribs
+ */
+ protected function createLink(string $route, string $label, array $attribs = []): string
+ {
+ $url = $this->urlManager->createUrl($route);
+ $attribsAsString = $this->buildHtmlAttributes($attribs);
+ return sprintf('%s', $url, $attribsAsString, $label);
+ }
+}
diff --git a/plugins/twig_core/TwigCorePlugin.php b/plugins/twig/TwigPlugin.php
similarity index 71%
rename from plugins/twig_core/TwigCorePlugin.php
rename to plugins/twig/TwigPlugin.php
index 981c2374..8fdcd61f 100644
--- a/plugins/twig_core/TwigCorePlugin.php
+++ b/plugins/twig/TwigPlugin.php
@@ -2,20 +2,24 @@
declare(strict_types=1);
-namespace herbie\sysplugin\twig_core;
+namespace herbie\sysplugin\twig;
use Ausi\SlugGenerator\SlugGenerator;
use herbie\Alias;
use herbie\Assets;
+use herbie\Config;
use herbie\event\TwigInitializedEvent;
+use herbie\PageRepositoryInterface;
use herbie\Plugin;
use herbie\Translator;
use herbie\UrlManager;
-final class TwigCorePlugin extends Plugin
+final class TwigPlugin extends Plugin
{
private Alias $alias;
private Assets $assets;
+ private Config $config;
+ private PageRepositoryInterface $pageRepository;
private SlugGenerator $slugGenerator;
private Translator $translator;
private UrlManager $urlManager;
@@ -23,12 +27,16 @@ final class TwigCorePlugin extends Plugin
public function __construct(
Alias $alias,
Assets $assets,
+ Config $config,
+ PageRepositoryInterface $pageRepository,
SlugGenerator $slugGenerator,
Translator $translator,
UrlManager $urlManager
) {
$this->alias = $alias;
$this->assets = $assets;
+ $this->config = $config;
+ $this->pageRepository = $pageRepository;
$this->slugGenerator = $slugGenerator;
$this->translator = $translator;
$this->urlManager = $urlManager;
@@ -43,10 +51,12 @@ public function eventListeners(): array
public function onTwigInitialized(TwigInitializedEvent $event): void
{
- $event->getEnvironment()->addExtension(new TwigCoreExtension(
+ $event->getEnvironment()->addExtension(new TwigExtension(
$this->alias,
$this->assets,
+ $this->config,
$event->getEnvironment(),
+ $this->pageRepository,
$this->slugGenerator,
$this->translator,
$this->urlManager
diff --git a/plugins/twig/config.php b/plugins/twig/config.php
new file mode 100755
index 00000000..2744e481
--- /dev/null
+++ b/plugins/twig/config.php
@@ -0,0 +1,10 @@
+ 2,
+ 'pluginName' => 'twig',
+ 'pluginClass' => TwigPlugin::class,
+ 'pluginPath' => __DIR__,
+];
diff --git a/plugins/twig_core/README.md b/plugins/twig_core/README.md
deleted file mode 100644
index 01dc5e70..00000000
--- a/plugins/twig_core/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Twig Core System Plugin
-
-`Twig Core` is a [Herbie](http://github.com/getherbie) system plugin that brings support for several Twig filters, functions and tests.
-
-## Installation
-
-The plugin is installed already.
-
-To activate it, add `twig_core` to the `enabledSysPlugins` configuration option.
-
-~~~php
-return [
- 'enabledSysPlugins' => 'twig_core'
-];
-~~~
-
-## More Information
-
-For more information, see .
diff --git a/plugins/twig_core/TwigCoreExtension.php b/plugins/twig_core/TwigCoreExtension.php
deleted file mode 100644
index 76510806..00000000
--- a/plugins/twig_core/TwigCoreExtension.php
+++ /dev/null
@@ -1,466 +0,0 @@
-alias = $alias;
- $this->assets = $assets;
- $this->environment = $environment;
- $this->slugGenerator = $slugGenerator;
- $this->translator = $translator;
- $this->urlManager = $urlManager;
- }
-
- /**
- * @return TwigFilter[]
- */
- public function getFilters(): array
- {
- return [
- new TwigFilter('filesize', [$this, 'filterFilesize']),
- new TwigFilter('find', [$this, 'filterFind'], ['is_variadic' => true]),
- new TwigFilter('slugify', [$this, 'filterSlugify']),
- new TwigFilter('strftime', [$this, 'filterStrftime']),
- new TwigFilter('visible', [$this, 'filterVisible'], ['deprecated' => true]) // doesn't work properly
- ];
- }
-
- /**
- * @return TwigFunction[]
- */
- public function getFunctions(): array
- {
- return [
- new TwigFunction('add_css', [$this, 'functionAddCss']),
- new TwigFunction('add_js', [$this, 'functionAddJs']),
- new TwigFunction('css_classes', [$this, 'functionCssClasses'], ['needs_context' => true]),
- new TwigFunction('file_link', [$this, 'functionFileLink'], [
- 'is_safe' => ['html'],
- 'needs_context' => true
- ]),
- new TwigFunction('file', [$this, 'functionFile'], ['is_safe' => ['html']]),
- new TwigFunction('image', [$this, 'functionImage'], ['is_safe' => ['html']]),
- new TwigFunction('page_link', [$this, 'functionPageLink'], ['is_safe' => ['html']]),
- new TwigFunction('page_title', [$this, 'functionPageTitle'], ['needs_context' => true]),
- new TwigFunction('output_css', [$this, 'functionOutputCss'], ['is_safe' => ['html']]),
- new TwigFunction('output_js', [$this, 'functionOutputJs'], ['is_safe' => ['html']]),
- new TwigFunction('snippet', [$this, 'functionSnippet'], ['is_safe' => ['all']]),
- new TwigFunction('translate', [$this, 'functionTranslate']),
- new TwigFunction('url', [$this, 'functionUrl']),
- new TwigFunction('abs_url', [$this, 'functionAbsUrl']),
- new TwigFunction('mail_link', [$this, 'functionMailLink'], ['is_safe' => ['html']])
- ];
- }
-
- /**
- * @return TwigTest[]
- */
- public function getTests(): array
- {
- return [
- new TwigTest('readable', [$this, 'testIsReadable']),
- new TwigTest('writable', [$this, 'testIsWritable'])
- ];
- }
-
- /**
- * @param array $htmlOptions
- */
- private function buildHtmlAttributes(array $htmlOptions = []): string
- {
- $attributes = '';
- foreach ($htmlOptions as $key => $value) {
- $attributes .= $key . '="' . $value . '" ';
- }
- return trim($attributes);
- }
-
- public function filterFilesize(int $size): string
- {
- if ($size <= 0) {
- return '0';
- }
- if ($size === 1) {
- return '1 Byte';
- }
- $mod = 1024;
- $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
- for ($i = 0; $size > $mod && $i < count($units) - 1; ++$i) {
- $size /= $mod;
- }
- return str_replace(',', '.', (string)round($size, 1)) . ' ' . $units[$i];
- }
-
- /**
- * @param string[] $selectors
- * @throws \Exception
- */
- public function filterFind(iterable $iterator, array $selectors = []): iterable
- {
- if ($iterator instanceof \Traversable) {
- $data = iterator_to_array($iterator);
- } else {
- $data = (array)$iterator;
- }
- $selector = new Selector();
- return $selector->find($selectors, $data);
- }
-
- /**
- * Creates a web friendly URL (slug) from a string.
- */
- public function filterSlugify(string $url): string
- {
- return $this->slugGenerator->generate($url);
- }
-
- /**
- * @throws \Exception
- */
- public function filterStrftime(string $date, string $format = '%x'): string
- {
- // timestamp?
- if (is_numeric($date)) {
- $date = date_format('Y-m-d H:i:s', (int)$date);
- }
- try {
- $dateTime = new \DateTime($date);
- } catch (\Exception $e) {
- return $date;
- }
- return time_format($format, $dateTime->getTimestamp());
- }
-
- public function filterVisible(PageTree $tree): PageTreeFilterIterator
- {
- $treeIterator = new PageTreeIterator($tree);
- return new PageTreeFilterIterator($treeIterator);
- }
-
- /**
- * @param array|string $paths
- */
- public function functionAddCss(
- $paths,
- array $attr = [],
- ?string $group = null,
- bool $raw = false,
- int $pos = 1
- ): void {
- $this->assets->addCss($paths, $attr, $group, $raw, $pos);
- }
-
- /**
- * @param array|string $paths
- */
- public function functionAddJs(
- $paths,
- array $attr = [],
- ?string $group = null,
- bool $raw = false,
- int $pos = 1
- ): void {
- $this->assets->addJs($paths, $attr, $group, $raw, $pos);
- }
-
- /**
- * @param array $context
- * @return string
- */
- public function functionCssClasses(array $context): string
- {
- $page = 'error';
- if (isset($context['page'])) {
- $route = $context['page']->getRoute();
- $page = !empty($route) ? $route : 'index';
- }
-
- $layout = 'default';
- if (isset($context['page'])) {
- $layout = $context['page']->getLayout();
- }
-
- $theme = 'default';
- if (!empty($context['theme'])) {
- $theme = $context['theme'];
- }
-
- $language = 'en';
- if (isset($context['site'])) {
- $language = $context['site']->getLanguage();
- }
-
- $class = sprintf('page-%s theme-%s layout-%s language-%s', $page, $theme, $layout, $language);
- return str_replace(['/', '.'], '-', $class);
- }
-
- public function functionFileLink(
- array $context,
- string $path,
- string $label = '',
- bool $info = false,
- array $attribs = []
- ): string {
- $attribs['alt'] = $attribs['alt'] ?? '';
- $attribs['class'] = $attribs['class'] ?? 'link__label';
-
- /** @var Config $config from download middleware */
- $config = $context['config'];
- $baseUrl = str_trailing_slash($config->getAsString('components.downloadMiddleware.route'));
- $storagePath = str_trailing_slash($config->getAsString('components.downloadMiddleware.storagePath'));
-
- // combine url and path
- $href = $this->urlManager->createUrl($baseUrl . $path);
- $path = $this->alias->get($storagePath . $path);
-
- if (!empty($info)) {
- $fileInfo = $this->getFileInfo($path);
- }
-
- $replace = [
- '{href}' => $href,
- '{attribs}' => $this->buildHtmlAttributes($attribs),
- '{label}' => empty($label) ? basename($path) : $label,
- '{info}' => empty($fileInfo) ? '' : sprintf('%s', $fileInfo)
- ];
- return strtr('{label}{info}', $replace);
- }
-
- public function functionMailLink(
- string $email,
- ?string $label = null,
- array $attribs = [],
- string $template = '@snippet/mail_link.twig'
- ): string {
- $attribs['href'] = 'mailto:' . $email;
- $attribs['class'] = $attribs['class'] ?? 'link__label';
-
- ksort($attribs);
-
- $context = [
- 'attribs' => $attribs,
- 'label' => $label ?? $email,
- ];
-
- try {
- return $this->environment->render($template, $context);
- } catch (Error $e) {
- return $email;
- }
- }
-
- public function functionOutputCss(?string $group = null, bool $addTimestamp = false): string
- {
- return $this->assets->outputCss($group, $addTimestamp);
- }
-
- public function functionOutputJs(?string $group = null, bool $addTimestamp = false): string
- {
- return $this->assets->outputJs($group, $addTimestamp);
- }
-
- /**
- * @param array $context
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionSnippet(string $path, array $context = []): string
- {
- return $this->environment->render($path, $context);
- }
-
- public function functionImage(
- string $src,
- int $width = 0,
- int $height = 0,
- string $alt = '',
- string $class = ''
- ): string {
- $attribs = [];
- $attribs['src'] = $this->urlManager->createUrl('/') . $src;
- $attribs['alt'] = $alt;
- if (!empty($width)) {
- $attribs['width'] = (string)$width;
- }
- if (!empty($height)) {
- $attribs['height'] = (string)$height;
- }
- if (!empty($class)) {
- $attribs['class'] = $class;
- }
- return sprintf('', $this->buildHtmlAttributes($attribs));
- }
-
- public function functionPageLink(string $route, string $label, array $attribs = []): string
- {
- $scheme = parse_url($route, PHP_URL_SCHEME);
- if ($scheme === null) {
- $class = 'link--internal';
- $href = $this->urlManager->createUrl($route);
- } else {
- $class = 'link--external';
- $href = $route;
- }
-
- $attribs['class'] = $attribs['class'] ?? '';
- $attribs['class'] = trim($attribs['class'] . ' link__label');
-
- $replace = [
- '{class}' => $class,
- '{href}' => $href,
- '{attribs}' => $this->buildHtmlAttributes($attribs),
- '{label}' => $label,
- ];
-
- $template = '{label}';
- return strtr($template, $replace);
- }
-
- /**
- * @param array{site: Site} $context
- */
- public function functionPageTitle(
- array $context,
- string $delim = ' / ',
- string $siteTitle = '',
- string $rootTitle = '',
- bool $reverse = false
- ): string {
- $pageTrail = $context['site']->getPageTrail();
- $count = count($pageTrail);
-
- $titles = [];
-
- if (!empty($siteTitle)) {
- $titles[] = $siteTitle;
- }
-
- foreach ($pageTrail as $item) {
- if ((1 === $count) && $item->isStartPage() && !empty($rootTitle)) {
- return $rootTitle;
- }
- $titles[] = $item->getTitle();
- }
-
- if (!empty($reverse)) {
- $titles = array_reverse($titles);
- }
-
- return implode($delim, $titles);
- }
-
- public function functionTranslate(string $category = '', string $message = '', array $params = []): string
- {
- return $this->translator->translate($category, $message, $params);
- }
-
- public function functionUrl(string $route = ''): string
- {
- return $this->urlManager->createUrl($route);
- }
-
- public function functionAbsUrl(string $route = ''): string
- {
- return $this->urlManager->createAbsoluteUrl($route);
- }
-
- public function functionFile(string $path, string $label = '', bool $info = false, array $attribs = []): string
- {
- $attribs['class'] = $attribs['class'] ?? 'link__label';
-
- if (!empty($info)) {
- $fileInfo = $this->getFileInfo($path);
- }
-
- $replace = [
- '{href}' => $path,
- '{attribs}' => $this->buildHtmlAttributes($attribs),
- '{label}' => empty($label) ? basename($path) : $label,
- '{info}' => empty($fileInfo) ? '' : sprintf('%s', $fileInfo)
- ];
- return strtr('{label}{info}', $replace);
- }
-
- private function getFileInfo(string $path): string
- {
- if (!is_readable($path)) {
- return '';
- }
- $replace = [
- '{size}' => $this->filterFilesize(file_size($path)),
- '{extension}' => strtoupper(pathinfo($path, PATHINFO_EXTENSION))
- ];
- return strtr(' ({extension}, {size})', $replace);
- }
-
- public function testIsReadable(string $alias): bool
- {
- if (!is_string($alias) || empty($alias)) {
- return false;
- }
- $filename = $this->alias->get($alias);
- return is_readable($filename);
- }
-
- public function testIsWritable(string $alias): bool
- {
- if (!is_string($alias) || empty($alias)) {
- return false;
- }
- $filename = $this->alias->get($alias);
- return is_writable($filename);
- }
-}
diff --git a/plugins/twig_core/config.php b/plugins/twig_core/config.php
deleted file mode 100755
index 6aefc3d5..00000000
--- a/plugins/twig_core/config.php
+++ /dev/null
@@ -1,10 +0,0 @@
- 2,
- 'pluginName' => 'twig_core',
- 'pluginClass' => TwigCorePlugin::class,
- 'pluginPath' => __DIR__,
-];
diff --git a/plugins/twig_plus/README.md b/plugins/twig_plus/README.md
deleted file mode 100644
index 80dedc14..00000000
--- a/plugins/twig_plus/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Twig Plus System Plugin
-
-`Twig Plus` is a [Herbie](http://github.com/getherbie) system plugin that brings support for several Twig functions.
-
-## Installation
-
-The plugin is installed already.
-
-To activate it, add `twig_plus` to the `enabledSysPlugins` configuration option.
-
-~~~php
-return [
- 'enabledSysPlugins' => 'twig_plus'
-];
-~~~
-
-## More Information
-
-For more information, see .
diff --git a/plugins/twig_plus/TwigPlusExtension.php b/plugins/twig_plus/TwigPlusExtension.php
deleted file mode 100644
index c09dafec..00000000
--- a/plugins/twig_plus/TwigPlusExtension.php
+++ /dev/null
@@ -1,466 +0,0 @@
-environment = $environment;
- $this->pageRepository = $pageRepository;
- $this->urlManager = $urlManager;
- }
-
- /**
- * @return TwigFunction[]
- */
- public function getFunctions(): array
- {
- $options = ['is_safe' => ['html']];
- return [
- new TwigFunction('menu_ascii_tree', [$this, 'functionAsciiTree'], $options),
- new TwigFunction('menu_breadcrumb', [$this, 'functionBreadcrumb'], $options),
- new TwigFunction('menu_list', [$this, 'functionListing'], $options),
- new TwigFunction('menu_pager', [$this, 'functionPager'], $options),
- new TwigFunction('menu_sitemap', [$this, 'functionSitemap'], $options),
- new TwigFunction('menu_tree', [$this, 'functionMenu'], $options),
- new TwigFunction('page_taxonomies', [$this, 'functionPageTaxonomies'], $options),
- new TwigFunction('pages_filtered', [$this, 'functionPagesFiltered'], $options),
- new TwigFunction('pages_recent', [$this, 'functionPagesRecent'], $options),
- new TwigFunction('taxonomy_archive', [$this, 'functionTaxonomyArchive'], $options),
- new TwigFunction('taxonomy_authors', [$this, 'functionTaxonomyAuthors'], $options),
- new TwigFunction('taxonomy_categories', [$this, 'functionTaxonomyCategories'], $options),
- new TwigFunction('taxonomy_tags', [$this, 'functionTaxonomyTags'], $options)
- ];
- }
-
- public function functionAsciiTree(
- string $route = '',
- int $maxDepth = -1,
- bool $showHidden = false
- ): string {
- // TODO use $class parameter
- $branch = $this->pageRepository->findAll()->getPageTree()->findByRoute($route);
- if ($branch === null) {
- return '';
- }
-
- $treeIterator = new PageTreeIterator($branch);
- $filterIterator = new PageTreeFilterIterator($treeIterator, !$showHidden);
-
- $asciiTree = new PageTreeTextRenderer($filterIterator);
- $asciiTree->setMaxDepth($maxDepth);
- return $asciiTree->render();
- }
-
- /**
- * @param array{0: string, 1?: string}|string $homeLink
- */
- public function functionBreadcrumb(
- string $delim = '',
- $homeLink = '',
- bool $reverse = false
- ): string {
- // TODO use string type for param $homeLink (like "route|label")
-
- $links = [];
-
- if (!empty($homeLink)) {
- if (is_array($homeLink)) {
- $route = reset($homeLink);
- $label = isset($homeLink[1]) ? $homeLink[1] : 'Home';
- } else {
- $route = $homeLink;
- $label = 'Home';
- }
- $links[] = $this->createLink($route, $label);
- }
-
- [$route] = $this->urlManager->parseRequest();
- $pageTrail = $this->pageRepository->findAll()->getPageTrail($route);
- foreach ($pageTrail as $item) {
- $links[] = $this->createLink($item->getRoute(), $item->getMenuTitle());
- }
-
- if (!empty($reverse)) {
- $links = array_reverse($links);
- }
-
- $html = '';
- foreach ($links as $i => $link) {
- if ($i > 0 && !empty($delim)) {
- $html .= '- ' . $delim . '
';
- }
- $html .= '- ' . $link . '
';
- }
- $html .= '
';
-
- return $html;
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionListing(
- ?PageList $pageList = null,
- string $filter = '',
- string $sort = '',
- bool $shuffle = false,
- int $limit = 10,
- string $template = '@snippet/listing.twig'
- ): string {
- if ($pageList === null) {
- $pageList = $this->pageRepository->findAll();
- }
-
- if (!empty($filter)) {
- [$field, $value] = explode('|', $filter);
- $pageList = $pageList->filter($field, $value);
- }
-
- if (!empty($sort)) {
- [$field, $direction] = explode('|', $sort);
- $pageList = $pageList->sort($field, $direction);
- }
-
- if ($shuffle) {
- $pageList = $pageList->shuffle();
- }
-
- // filter pages with empty title
- $pageList = $pageList->filter(function (Page $page) {
- return !empty($page->getTitle());
- });
-
- $pagination = new Pagination($pageList);
- $pagination->setLimit($limit);
-
- return $this->environment->render($template, ['pagination' => $pagination]);
- }
-
- public function functionMenu(
- string $route = '',
- int $maxDepth = -1,
- bool $showHidden = false,
- string $class = 'menu'
- ): string {
- // NOTE duplicated code, see function sitemap
- $branch = $this->pageRepository->findAll()->getPageTree()->findByRoute($route);
- if ($branch === null) {
- return '';
- }
-
- $treeIterator = new PageTreeIterator($branch);
- $filterIterator = new PageTreeFilterIterator($treeIterator, !$showHidden);
-
- $htmlTree = new PageTreeHtmlRenderer($filterIterator);
- $htmlTree->setMaxDepth($maxDepth);
- $htmlTree->setClass($class);
- $htmlTree->setItemCallback(function (PageTree $node) {
- $menuItem = $node->getMenuItem();
- $href = $this->urlManager->createUrl($menuItem->getRoute());
- return sprintf('%s', $href, $menuItem->getMenuTitle());
- });
-
- [$currenRoute] = $this->urlManager->parseRequest();
- return $htmlTree->render($currenRoute);
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionPageTaxonomies(
- ?Page $page = null,
- string $pageRoute = '',
- bool $renderAuthors = true,
- bool $renderCategories = true,
- bool $renderTags = true,
- string $template = '@template/page/taxonomies.twig'
- ): string {
- return $this->environment->render($template, [
- 'page' => $page,
- 'pageRoute' => $pageRoute,
- 'renderAuthors' => $renderAuthors,
- 'renderCategories' => $renderCategories,
- 'renderTags' => $renderTags
- ]);
- }
-
- /**
- * @throws \Exception
- */
- public function functionPager(
- string $limit = '',
- string $prevPageLabel = '',
- string $nextPageLabel = '',
- string $prevPageIcon = '',
- string $nextPageIcon = '',
- string $cssClass = 'pager',
- string $template = '{prev}{next}
'
- ): string {
- [$route] = $this->urlManager->parseRequest();
- $pageList = $this->pageRepository->findAll();
-
- if ($limit !== '') {
- $pageList = $pageList->filter(function ($page) use ($limit) {
- return strpos($page->getRoute(), $limit) === 0;
- });
- }
-
- $prevPage = null;
- $currentPage = null;
- $nextPage = null;
- $lastPage = null;
- foreach ($pageList as $key => $page) {
- if ($currentPage) {
- $nextPage = $page;
- break;
- }
- if ($key === $route) {
- $prevPage = $lastPage;
- $currentPage = $page;
- continue;
- }
- $lastPage = $page;
- }
-
- $replacements = [
- '{class}' => $cssClass,
- '{prev}' => '',
- '{next}' => ''
- ];
-
- if (isset($prevPage)) {
- $label = empty($prevPageLabel) ? $prevPage->getMenuTitle() : $prevPageLabel;
- $label = sprintf('%s', $cssClass, $label);
- if ($prevPageIcon) {
- $label = sprintf('%s%s', $cssClass, $prevPageIcon, $label);
- }
- $attribs = ['class' => $cssClass . '-link-prev'];
- $replacements['{prev}'] = $this->createLink($prevPage->getRoute(), $label, $attribs);
- }
-
- if (isset($nextPage)) {
- $label = empty($nextPageLabel) ? $nextPage->getMenuTitle() : $nextPageLabel;
- $label = sprintf('%s', $cssClass, $label);
- if ($nextPageIcon) {
- $label = sprintf('%s%s', $label, $cssClass, $nextPageIcon);
- }
- $attribs = ['class' => $cssClass . '-link-next'];
- $replacements['{next}'] = $this->createLink($nextPage->getRoute(), $label, $attribs);
- }
-
- return strtr($template, $replacements);
- }
-
- /**
- * @param array $routeParams
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionPagesFiltered(
- array $routeParams,
- string $template = '@template/pages/filtered.twig'
- ): string {
- return $this->environment->render($template, [
- 'routeParams' => $routeParams
- ]);
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionPagesRecent(
- ?PageList $pageList = null,
- string $dateFormat = '%e. %B %Y',
- int $limit = 5,
- ?string $pageType = null,
- bool $showDate = false,
- string $title = 'Recent posts',
- string $template = '@template/pages/recent.twig'
- ): string {
- if ($pageList === null) {
- $pageList = $this->pageRepository->findAll();
- }
- $recentPages = $pageList->getRecent($limit, $pageType);
- return $this->environment->render($template, [
- 'recentPages' => $recentPages,
- 'dateFormat' => $dateFormat,
- 'pageType' => $pageType,
- 'showDate' => $showDate,
- 'title' => $title
- ]);
- }
-
- public function functionSitemap(
- string $route = '',
- int $maxDepth = -1,
- bool $showHidden = false,
- string $class = 'sitemap'
- ): string {
- return $this->functionMenu($route, $maxDepth, $showHidden, $class);
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionTaxonomyArchive(
- ?PageList $pageList = null,
- string $pageRoute = '',
- string $pageType = '',
- bool $showCount = false,
- string $title = 'Archive',
- string $template = '@template/taxonomy/archive.twig'
- ): string {
- if ($pageList === null) {
- $pageList = $this->pageRepository->findAll();
- }
- $months = $pageList->getMonths($pageType);
- return $this->environment->render($template, [
- 'months' => $months,
- 'pageRoute' => $pageRoute,
- 'pageType' => $pageType,
- 'showCount' => $showCount,
- 'title' => $title
- ]);
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionTaxonomyAuthors(
- ?PageList $pageList = null,
- string $pageRoute = '',
- string $pageType = '',
- bool $showCount = false,
- string $title = 'Authors',
- string $template = '@template/taxonomy/authors.twig'
- ): string {
- if ($pageList === null) {
- $pageList = $this->pageRepository->findAll();
- }
- $authors = $pageList->getAuthors($pageType);
- return $this->environment->render($template, [
- 'authors' => $authors,
- 'pageRoute' => $pageRoute,
- 'pageType' => $pageType,
- 'showCount' => $showCount,
- 'title' => $title
- ]);
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionTaxonomyCategories(
- ?PageList $pageList = null,
- string $pageRoute = '',
- string $pageType = '',
- bool $showCount = false,
- string $title = 'Categories',
- string $template = '@template/taxonomy/categories.twig'
- ): string {
- if ($pageList === null) {
- $pageList = $this->pageRepository->findAll();
- }
- $categories = $pageList->getCategories($pageType);
- return $this->environment->render($template, [
- 'categories' => $categories,
- 'pageRoute' => $pageRoute,
- 'pageType' => $pageType,
- 'showCount' => $showCount,
- 'title' => $title
- ]);
- }
-
- /**
- * @throws LoaderError
- * @throws RuntimeError
- * @throws SyntaxError
- */
- public function functionTaxonomyTags(
- ?PageList $pageList = null,
- string $pageRoute = '',
- string $pageType = '',
- bool $showCount = false,
- string $title = 'Tags',
- string $template = '@template/taxonomy/tags.twig'
- ): string {
- if ($pageList === null) {
- $pageList = $this->pageRepository->findAll();
- }
- $tags = $pageList->getTags($pageType);
- return $this->environment->render($template, [
- 'pageRoute' => $pageRoute,
- 'pageType' => $pageType,
- 'showCount' => $showCount,
- 'tags' => $tags,
- 'title' => $title
- ]);
- }
-
- /**
- * @param array $htmlOptions
- */
- protected function buildHtmlAttributes(array $htmlOptions = []): string
- {
- $attributes = '';
- foreach ($htmlOptions as $key => $value) {
- $attributes .= $key . '="' . $value . '" ';
- }
- return trim($attributes);
- }
-
- /**
- * @param array $htmlAttributes
- */
- protected function createLink(string $route, string $label, array $htmlAttributes = []): string
- {
- $url = $this->urlManager->createUrl($route);
- $attributesAsString = $this->buildHtmlAttributes($htmlAttributes);
- return sprintf('%s', $url, $attributesAsString, $label);
- }
-}
diff --git a/plugins/twig_plus/TwigPlusPlugin.php b/plugins/twig_plus/TwigPlusPlugin.php
deleted file mode 100644
index 762b465c..00000000
--- a/plugins/twig_plus/TwigPlusPlugin.php
+++ /dev/null
@@ -1,40 +0,0 @@
-pageRepository = $pageRepository;
- $this->urlManager = $urlManager;
- }
-
- public function eventListeners(): array
- {
- return [
- [TwigInitializedEvent::class, [$this, 'onTwigInitialized']],
- ];
- }
-
- public function onTwigInitialized(TwigInitializedEvent $event): void
- {
- $event->getEnvironment()->addExtension(new TwigPlusExtension(
- $event->getEnvironment(),
- $this->pageRepository,
- $this->urlManager
- ));
- }
-}
diff --git a/plugins/twig_plus/config.php b/plugins/twig_plus/config.php
deleted file mode 100755
index 0c2ea3d8..00000000
--- a/plugins/twig_plus/config.php
+++ /dev/null
@@ -1,10 +0,0 @@
- 2,
- 'pluginName' => 'twig_plus',
- 'pluginClass' => TwigPlusPlugin::class,
- 'pluginPath' => __DIR__,
-];
diff --git a/system/Config.php b/system/Config.php
index 8f3e7bde..596d7f0a 100644
--- a/system/Config.php
+++ b/system/Config.php
@@ -133,4 +133,17 @@ public function flatten(): array
ksort($flatten);
return $flatten;
}
+
+ /**
+ * This is for snake_case in twig only.
+ * @return null|mixed
+ */
+ public function __call(string $name, array $arguments)
+ {
+ $getter = 'get' . str_replace('_', '', $name);
+ if (method_exists($this, $getter)) {
+ return $this->$getter(...$arguments);
+ }
+ return null;
+ }
}
diff --git a/system/ContainerBuilder.php b/system/ContainerBuilder.php
index a250b3a6..8b43de89 100644
--- a/system/ContainerBuilder.php
+++ b/system/ContainerBuilder.php
@@ -271,8 +271,6 @@ public function build(): ContainerInterface
$c->get(EventManager::class),
$c->get(LoggerInterface::class),
$c->get(Site::class),
- $c->get(UrlManager::class),
- $this->app->getBaseUrl()
);
});
diff --git a/system/CorePlugin.php b/system/CorePlugin.php
index 6a91cfea..896bfa32 100644
--- a/system/CorePlugin.php
+++ b/system/CorePlugin.php
@@ -60,8 +60,6 @@ public function onRenderPage(RenderPageEvent $event): void
{
$twig = $this->twigRenderer->getTwigEnvironment();
$twig->addGlobal('page', $event->getPage());
- $twig->addGlobal('route', $event->getRoute());
- $twig->addGlobal('routeParams', $event->getRouteParams());
}
/**
diff --git a/system/PageList.php b/system/PageList.php
index 16cef103..f373ca33 100644
--- a/system/PageList.php
+++ b/system/PageList.php
@@ -76,6 +76,11 @@ public function find(string $value, string $key): ?Page
return null;
}
+ public function query(): QueryBuilder
+ {
+ return (new QueryBuilder())->from($this);
+ }
+
/**
* Run a filter over each of the items.
*
diff --git a/system/QueryBuilder.php b/system/QueryBuilder.php
new file mode 100644
index 00000000..0e284d13
--- /dev/null
+++ b/system/QueryBuilder.php
@@ -0,0 +1,423 @@
+ 'matchNotEqual',
+ ">=" => 'matchGreaterThanEqual',
+ "<=" => 'matchLessThanEqual',
+ "*=" => 'matchContains',
+ "^=" => 'matchStarts',
+ "~=" => 'matchContainsWords',
+ "$=" => 'matchEnds',
+ "?=" => 'matchRegex',
+ "=" => 'matchEqual',
+ ">" => 'matchGreaterThan',
+ "<" => 'matchLessThan',
+ "&" => 'matchBitwiseAnd',
+ ];
+ private array $where;
+ private int $limit;
+ private int $offset;
+ /** @var callable|string $order */
+ private $order;
+ private array $data;
+ private array $processed;
+
+ public function __construct()
+ {
+ $this->where = [];
+ $this->limit = 0;
+ $this->offset = 0;
+ $this->order = '';
+ $this->data = [];
+ $this->processed = [];
+ }
+
+ public function from(iterable $iterator): self
+ {
+ if ($iterator instanceof Traversable) {
+ $this->data = iterator_to_array($iterator);
+ } else {
+ $this->data = (array)$iterator;
+ }
+ return $this;
+ }
+
+ /**
+ * @param array|string ...$conditions
+ */
+ public function where(...$conditions): self
+ {
+ if (empty($conditions)) {
+ throw new \InvalidArgumentException('Empty where conditions');
+ }
+ foreach ($conditions as $condition) {
+ if (is_string($condition)) {
+ $parsedCondition = $this->parseCondition($condition);
+ $this->where[] = array_merge(['AND'], [$parsedCondition]);
+ continue;
+ }
+ if (is_array($condition)) {
+ if (isset($condition[0]) && in_array(strtoupper($condition[0]), self::WHERE_CLAUSE_OPERATORS)) {
+ $this->where[] = $this->parseConditionsInOperatorFormat($condition);
+ continue;
+ } elseif (array_is_assoc($condition)) {
+ $this->where[] = $this->parseConditionsInHashFormat($condition);
+ continue;
+ }
+ }
+ throw new \InvalidArgumentException('Unsupported where conditions');
+ }
+ return $this;
+ }
+
+ private function parseCondition(string $condition): array
+ {
+ foreach (self::OPERATORS as $syntax => $name) {
+ $position = stripos($condition, $syntax);
+ if ($position !== false) {
+ $syntaxLength = strlen($syntax);
+ $value1 = substr($condition, 0, $position);
+ $value2 = substr($condition, $position + $syntaxLength);
+ if (str_contains($value1, '|')) {
+ $values1 = str_explode_filtered($value1, '|');
+ $conditions = ['OR'];
+ foreach ($values1 as $value1) {
+ $conditions[] = [$name, $value1, $value2];
+ }
+ return $conditions;
+ }
+ if (str_contains($value2, '|')) {
+ $values2 = str_explode_filtered($value2, '|');
+ $conditions = ['OR'];
+ foreach ($values2 as $value2) {
+ $conditions[] = [$name, $value1, $value2];
+ }
+ return $conditions;
+ }
+ return [$name, $value1, $value2];
+ }
+ }
+ throw new \InvalidArgumentException('Unsupported operator');
+ }
+
+ private function convertType(mixed $value1, mixed $value2): mixed
+ {
+ if (is_bool($value1) && is_string($value2)) {
+ $lowered = strtolower($value2);
+ if ($lowered === 'true') {
+ return true;
+ }
+ if ($lowered === 'false') {
+ return false;
+ }
+ return $value2;
+ }
+ return $value2;
+ }
+
+ private function parseConditionsInOperatorFormat(array $conditions): array
+ {
+ if (!isset($conditions[0]) || !in_array(strtoupper($conditions[0]), self::WHERE_CLAUSE_OPERATORS)) {
+ throw new \InvalidArgumentException('Missing where clause operator');
+ }
+ $whereClauseOperator = [strtoupper(array_shift($conditions))];
+ $items = [];
+ foreach ($conditions as $condition) {
+ if (is_array($condition)) {
+ $items[] = $this->parseConditionsInOperatorFormat($condition);
+ } else {
+ $items[] = $this->parseCondition($condition);
+ }
+ }
+ return array_merge($whereClauseOperator, $items);
+ }
+
+ private function parseConditionsInHashFormat(array $conditions): array
+ {
+ $items = [];
+ foreach ($conditions as $key => $value) {
+ if (is_scalar($value)) {
+ $type = \herbie\get_type($value);
+ $items[] = ['match' . ucfirst($type), $key, $value];
+ }
+ }
+ return array_merge(['AND'], $items);
+ }
+
+ public function limit(int $limit): self
+ {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ public function offset(int $offset): self
+ {
+ $this->offset = $offset;
+ return $this;
+ }
+
+ public function order(callable|string $order): self
+ {
+ $this->order = $order;
+ return $this;
+ }
+
+ public function count(): int
+ {
+ return count($this->processed);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function paginate(int $size): Pagination
+ {
+ $this->limit = $size;
+ $this->processData();
+ return new Pagination($this->processed, $this->limit);
+ }
+
+ public function all(): iterable
+ {
+ $this->processData();
+ return $this->processed;
+ }
+
+ public function one(): array|object|null
+ {
+ $this->limit = 1;
+ $this->processData();
+ $item = reset($this->processed);
+ if ($item === false) {
+ return null;
+ }
+ return $item;
+ }
+
+ public function getIterator(): Traversable
+ {
+ $this->processData();
+ return new ArrayIterator($this->processed);
+ }
+
+ private function processData(): void
+ {
+ $i = 0;
+ $j = 0;
+ $this->sort();
+ foreach ($this->data as $item) {
+ if (($this->offset > 0) && ($j < ($this->offset))) {
+ $j++;
+ continue;
+ }
+ $status = $this->processItem($item, array_merge(['AND'], $this->where));
+ if ($status === true) {
+ $this->processed[] = $item;
+ $i++;
+ if (($this->limit > 0) && ($i >= $this->limit)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private function processItem(ArrayAccess|array|int|float|string|bool $item, array $conditions): bool
+ {
+ $whereClauseOperator = array_shift($conditions);
+
+ if (empty($conditions)) {
+ return true;
+ }
+
+ $status = [];
+ foreach ($conditions as $condition) {
+ if (isset($condition[0]) && in_array(strtoupper($condition[0]), self::WHERE_CLAUSE_OPERATORS)) {
+ $status[] = $this->processItem($item, $condition);
+ continue;
+ }
+
+ $itemIsScalar = is_scalar($item);
+ $itemIsArrayable = ($item instanceof ArrayAccess) || is_array($item);
+
+ [$operator, $field, $value2] = $condition;
+
+ if (!$itemIsScalar && !isset($item[$field])) {
+ $status[] = false;
+ continue;
+ }
+
+ /** @var callable $callable */
+ $callable = [$this, $operator];
+ if ($itemIsScalar && ($field === 'value')) {
+ $value2 = $this->convertType($item, $value2);
+ $status[] = call_user_func_array($callable, [$item, $value2]);
+ } elseif ($itemIsArrayable && isset($item[$field]) && is_array($item[$field])) {
+ $arrStatus = [];
+ foreach ($item[$field] as $value1) {
+ $value2 = $this->convertType($value1, $value2);
+ $arrStatus[] = call_user_func_array($callable, [$value1, $value2]);
+ }
+ $status[] = in_array(true, $arrStatus, true);
+ } elseif ($itemIsArrayable && isset($item[$field])) {
+ $value1 = $item[$field];
+ $value2 = $this->convertType($value1, $value2);
+ $status[] = call_user_func_array($callable, [$value1, $value2]);
+ }
+ }
+
+ if ($whereClauseOperator === 'OR') {
+ return in_array(true, $status, true);
+ }
+
+ $uniqueStatus = array_unique($status);
+ $uniqueStatusCount = count($uniqueStatus);
+ return $uniqueStatusCount === 1 && in_array(true, $uniqueStatus, true);
+ }
+
+ private function sort(): bool
+ {
+ if (is_callable($this->order)) {
+ return uasort($this->data, $this->order);
+ }
+
+ if (trim($this->order, '-+') === '') {
+ return false;
+ }
+
+ $field = '';
+ if (!empty($this->order)) {
+ $field = trim($this->order, '+');
+ }
+
+ $direction = 'asc';
+ if (str_starts_with($field, '-')) {
+ $field = substr($field, 1);
+ $direction = 'desc';
+ }
+
+ return uasort($this->data, function ($value1, $value2) use ($field, $direction) {
+ if (!isset($value1[$field]) || !isset($value2[$field])) {
+ return 0;
+ }
+ if ($value1[$field] === $value2[$field]) {
+ return 0;
+ }
+ if ($direction === 'asc') {
+ return ($value1[$field] < $value2[$field]) ? -1 : 1;
+ } else {
+ return ($value2[$field] < $value1[$field]) ? -1 : 1;
+ }
+ });
+ }
+
+ protected function matchString(string $value1, string $value2): bool
+ {
+ return $value1 === $value2;
+ }
+
+ protected function matchBoolean(bool $value1, bool $value2): bool
+ {
+ return $value1 === $value2;
+ }
+
+ protected function matchInteger(int $value1, int $value2): bool
+ {
+ return $value1 === $value2;
+ }
+
+ protected function matchFloat(float $value1, float $value2): bool
+ {
+ return $value1 === $value2;
+ }
+
+ protected function matchEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool
+ {
+ return $value1 === $value2;
+ }
+
+ protected function matchNotEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool
+ {
+ return $value1 !== $value2;
+ }
+
+ protected function matchGreaterThan(string|float|int|bool $value1, string|float|int|bool $value2): bool
+ {
+ return $value1 > $value2;
+ }
+
+ protected function matchLessThan(string|float|int|bool $value1, string|float|int|bool $value2): bool
+ {
+ return $value1 < $value2;
+ }
+
+ protected function matchGreaterThanEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool
+ {
+ return $value1 >= $value2;
+ }
+
+ protected function matchLessThanEqual(string|float|int|bool $value1, string|float|int|bool $value2): bool
+ {
+ return $value1 <= $value2;
+ }
+
+ protected function matchBitwiseAnd(int $value1, int $value2): bool
+ {
+ return ($value1 & $value2) > 0;
+ }
+
+ protected function matchContains(string $value1, string $value2): bool
+ {
+ return stripos($value1, $value2) !== false;
+ }
+
+ protected function matchContainsWords(string $value1, string $value2): bool
+ {
+ $words = preg_split('/[-\s]/', $value2, -1, PREG_SPLIT_NO_EMPTY);
+ if (!is_array($words) || count($words) === 0) {
+ return false;
+ }
+ foreach ($words as $word) {
+ if (!preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ protected function matchStarts(string $value1, string $value2): bool
+ {
+ return stripos(trim($value1), $value2) === 0;
+ }
+
+ protected function matchEnds(string $value1, string $value2): bool
+ {
+ $value2 = trim($value2);
+ $value1 = substr($value1, -1 * strlen($value2));
+ return strcasecmp($value1, $value2) === 0;
+ }
+
+ protected function matchRegex(string $value1, string $value2): bool
+ {
+ if (preg_match($value2, $value1, $matches)) {
+ return count($matches) > 0;
+ }
+ return false;
+ }
+}
diff --git a/system/Selector.php b/system/Selector.php
deleted file mode 100644
index 0359b738..00000000
--- a/system/Selector.php
+++ /dev/null
@@ -1,283 +0,0 @@
- 'matchNotEqual',
- ">=" => 'matchGreaterThanEqual',
- "<=" => 'matchLessThanEqual',
- "*=" => 'matchContains',
- "^=" => 'matchStarts',
- "~=" => 'matchContainsWords',
- "$=" => 'matchEnds',
- "&" => 'matchBitwiseAnd',
- ">" => 'matchGreaterThan',
- "<" => 'matchLessThan',
- "=" => 'matchEqual',
- ];
-
- protected array $selectors = [];
-
- /**
- * Find and return all items matching the given selector string.
- *
- * = Equal to
- * != Not equal to
- * < Less than
- * > Greater than
- * <= Less than or equal to
- * >= Greater than or equal to
- * *= Contains the exact word or phrase
- * ~= Contains all the words
- * ^= Contains the exact word or phrase at the beginning of the field
- * $= Contains the exact word or phrase at the end of the field
- * & Bitwise and
- *
- * @param array|string $selector
- * @return mixed
- * @throws \Exception
- */
- public function find($selector, array $data)
- {
- $selectors = $this->getSelector($selector);
- $sort = $this->extractSort($selectors);
- $limit = $this->extractLimit($selectors);
-
- unset($selector);
-
- if (!empty($sort)) {
- $this->sort($sort, $data);
- }
-
- if (empty($selectors)) {
- return $data;
- }
-
- $return = [];
- $i = 1;
- foreach ($data as $item) {
- if (($limit > 0) && ($i > $limit)) {
- break;
- }
-
- $bool = true;
- foreach ($selectors as $selector) {
- [$field, $value, $function] = $selector;
- if (!isset($item[$field])) {
- $bool = false;
- break;
- }
- /** @var callable $callable */
- $callable = [$this, $function];
- $bool &= call_user_func_array($callable, [$item[$field], $value]);
- }
-
- if ($bool) {
- $return[] = $item;
- $i++;
- }
- }
-
- return $return;
- }
-
- protected function extractSort(array &$selectors): string
- {
- $sort = "";
- foreach ($selectors as $index => $selector) {
- if ($selector[0] === "sort") {
- $sort = $selector[1];
- unset($selectors[$index]);
- break;
- }
- }
- return $sort;
- }
-
- protected function extractLimit(array &$selectors): int
- {
- $limit = 0;
- foreach ($selectors as $index => $selector) {
- if ($selector[0] === "limit") {
- $limit = abs((int)$selector[1]);
- unset($selectors[$index]);
- break;
- }
- }
- return $limit;
- }
-
- /**
- * @param string $selector
- * @param array &$data
- * @return mixed
- * @throws \Exception
- */
- public function get($selector, &$data)
- {
- return $this->find($selector, $data)->first();
- }
-
- protected function matchEqual(string $value1, string $value2): bool
- {
- return $value1 === $value2;
- }
-
- protected function matchNotEqual(string $value1, string $value2): bool
- {
- return $value1 !== $value2;
- }
-
- protected function matchGreaterThan(string $value1, string $value2): bool
- {
- return $value1 > $value2;
- }
-
- protected function matchLessThan(string $value1, string $value2): bool
- {
- return $value1 < $value2;
- }
-
- protected function matchGreaterThanEqual(string $value1, string $value2): bool
- {
- return $value1 >= $value2;
- }
-
- protected function matchLessThanEqual(string $value1, string $value2): bool
- {
- return $value1 <= $value2;
- }
-
- protected function matchBitwiseAnd(string $value1, string $value2): bool
- {
- return ((int)$value1 & (int)$value2) > 0;
- }
-
- protected function matchContains(string $value1, string $value2): bool
- {
- return stripos($value1, $value2) !== false;
- }
-
- protected function matchContainsWords(string $value1, string $value2): bool
- {
- $words = preg_split('/[-\s]/', $value2, -1, PREG_SPLIT_NO_EMPTY);
- if (!is_array($words) || count($words) === 0) {
- return false;
- }
- foreach ($words as $word) {
- if (!preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) {
- return false;
- }
- }
- return true;
- }
-
- protected function matchStarts(string $value1, string $value2): bool
- {
- return stripos(trim($value1), $value2) === 0;
- }
-
- protected function matchEnds(string $value1, string $value2): bool
- {
- $value2 = trim($value2);
- $value1 = substr($value1, -1 * strlen($value2));
- return strcasecmp($value1, $value2) === 0;
- }
-
- /**
- * @param string|array $selector
- * @return array
- */
- protected function getSelector($selector): array
- {
- if (is_array($selector)) {
- $selectors = $selector;
- } elseif (is_string($selector)) {
- $selectors = [trim($selector)];
- } else {
- throw new \InvalidArgumentException("Selector has to be a string or an array.");
- }
- unset($selector);
-
- $return = [];
- foreach ($selectors as $selector) {
- foreach ($this->operators as $op => $methodName) {
- $pos = stripos($selector, $op);
- if ($pos !== false) {
- $return[] = [
- substr($selector, 0, $pos),
- substr($selector, $pos + strlen($op)),
- $methodName
- ];
- break;
- }
- }
- }
-
- return $return;
- }
-
- /**
- * @param callable|string $sort
- */
- public function sort($sort, array &$items): bool
- {
- if (is_numeric($sort)) {
- return false;
- }
-
- if (is_callable($sort)) {
- $bool = uasort($items, $sort);
- return $bool;
- }
-
- $field = "title";
- if (!empty($sort)) {
- $field = trim($sort, "+");
- }
-
- $direction = "asc";
- if (substr($field, 0, 1) === "-") {
- $field = substr($field, 1);
- $direction = "desc";
- }
-
- return uasort($items, function ($value1, $value2) use ($field, $direction) {
- if (!isset($value1[$field]) || !isset($value2[$field])) {
- return 0;
- }
- if ($value1[$field] === $value2[$field]) {
- return 0;
- }
- if ($direction === 'asc') {
- return ($value1[$field] < $value2[$field]) ? -1 : 1;
- } else {
- return ($value2[$field] < $value1[$field]) ? -1 : 1;
- }
- });
- }
-
- /**
- * @param array|string $selector1
- * @param array|string $selector2
- */
- public static function mergeSelectors($selector1, $selector2): array
- {
- $selectors = [];
- if (is_array($selector1)) {
- $selectors = $selector1;
- } else {
- $selectors[] = $selector1;
- }
- if (is_array($selector2)) {
- $selectors = array_merge($selectors, $selector2);
- } else {
- $selectors[] = $selector2;
- }
- return array_filter($selectors); // filter empty
- }
-}
diff --git a/system/Site.php b/system/Site.php
index 5a501e04..a96ae8c9 100644
--- a/system/Site.php
+++ b/system/Site.php
@@ -4,8 +4,6 @@
namespace herbie;
-use Psr\Http\Message\ServerRequestInterface;
-
/**
* Stores the site.
*/
@@ -91,4 +89,47 @@ public function getCharset(): string
{
return $this->config->getAsString('charset');
}
+
+ public function getBaseUrl(): string
+ {
+ return $this->urlManager->createUrl('/');
+ }
+
+ public function getRoute(): string
+ {
+ return $this->urlManager->parseRequest()[0];
+ }
+
+ public function getRouteParams(): array
+ {
+ return $this->urlManager->parseRequest()[1];
+ }
+
+ public function getTheme(): string
+ {
+ return $this->config->getAsString('theme');
+ }
+
+ /**
+ * @return mixed
+ */
+ public function __get(string $name)
+ {
+ $getter = 'get' . str_replace('_', '', $name);
+ if (method_exists($this, $getter)) {
+ return $this->$getter();
+ } else {
+ throw new \InvalidArgumentException("Field {$name} does not exist.");
+ }
+ }
+
+ public function __isset(string $name): bool
+ {
+ $getter = 'get' . str_replace('_', '', $name);
+ if (method_exists($this, $getter)) {
+ return $this->$getter() !== null;
+ } else {
+ return false;
+ }
+ }
}
diff --git a/system/SystemInfoPlugin.php b/system/SystemInfoPlugin.php
index 0b14dfb6..dfb69b72 100644
--- a/system/SystemInfoPlugin.php
+++ b/system/SystemInfoPlugin.php
@@ -97,7 +97,7 @@ private function getConfig(): array
foreach ($this->config->flatten() as $key => $value) {
$configs[] = [
$key,
- gettype($value),
+ \herbie\get_type($value),
$this->filterValue($value)
];
}
@@ -187,13 +187,13 @@ private function getTwigGlobalsFromContext(array $context): array
foreach ($context as $string => $mixed) {
if (is_scalar($mixed)) {
$value = $mixed;
- $type = gettype($mixed);
+ $type = \herbie\get_type($mixed);
} elseif (is_object($mixed)) {
$value = get_class($mixed);
$type = 'class';
} else {
$value = json_encode($mixed);
- $type = gettype($mixed);
+ $type = \herbie\get_type($mixed);
}
$globals[] = [$string, $value, $type];
}
diff --git a/system/Translator.php b/system/Translator.php
index 55fa57ac..fd2959cb 100644
--- a/system/Translator.php
+++ b/system/Translator.php
@@ -96,7 +96,7 @@ public function addPath(string $category, $path): void
if (is_string($path)) {
$path = [$path];
} elseif (!is_array($path)) {
- $message = sprintf('Argument $path has to be an array or a string, %s given.', gettype($path));
+ $message = sprintf('Argument $path has to be an array or a string, %s given.', \herbie\get_type($path));
throw new \InvalidArgumentException($message);
}
$this->paths[$category] = array_merge($this->paths[$category], $path);
diff --git a/system/TwigRenderer.php b/system/TwigRenderer.php
index 7f5fd95e..4fbc72af 100644
--- a/system/TwigRenderer.php
+++ b/system/TwigRenderer.php
@@ -25,8 +25,6 @@ final class TwigRenderer
private TwigEnvironment $twig;
private LoggerInterface $logger;
private Site $site;
- private UrlManager $urlManager;
- private string $baseUrl;
/**
* TwigRenderer constructor.
@@ -35,17 +33,13 @@ public function __construct(
Config $config,
EventManager $eventManager,
LoggerInterface $logger,
- Site $site,
- UrlManager $urlManager,
- string $baseUrl
+ Site $site
) {
$this->initialized = false;
$this->config = $config;
$this->eventManager = $eventManager;
$this->logger = $logger;
$this->site = $site;
- $this->urlManager = $urlManager;
- $this->baseUrl = $baseUrl;
}
/**
@@ -84,9 +78,8 @@ public function init(): void
$this->twig->addExtension(new DebugExtension());
}
- foreach ($this->getContext() as $key => $value) {
- $this->twig->addGlobal($key, $value);
- }
+ $this->twig->addGlobal('page', null); // will be set by page renderer middleware
+ $this->twig->addGlobal('site', $this->site);
$this->initialized = true;
@@ -120,31 +113,6 @@ public function renderTemplate(string $name, array $context = []): string
return $this->twig->render($name, $context);
}
- /**
- * @return array{
- * route: string,
- * routeParams: array,
- * baseUrl: string,
- * theme: string,
- * site: Site,
- * page: Page|null,
- * config: Config
- * }
- */
- private function getContext(): array
- {
- [$route, $routeParams] = $this->urlManager->parseRequest();
- return [
- 'route' => $route,
- 'routeParams' => $routeParams,
- 'baseUrl' => $this->baseUrl,
- 'theme' => $this->config->getAsString('theme'),
- 'site' => $this->site,
- 'page' => null, // will be set by page renderer middleware
- 'config' => $this->config
- ];
- }
-
public function addFunction(TwigFunction $function): void
{
$this->twig->addFunction($function);
diff --git a/system/functions.php b/system/functions.php
index 4a70c245..af41e57e 100644
--- a/system/functions.php
+++ b/system/functions.php
@@ -469,3 +469,20 @@ function is_natural($value, bool $includingZero = false): bool
}
return false;
}
+
+function array_is_assoc(array $array): bool
+{
+ $keys = array_keys($array);
+ return array_keys($keys) !== $keys;
+}
+
+function get_type(mixed $value): string
+{
+ $type = \gettype($value);
+ // for historical reasons "double" is returned in case of a float, and not simply "float"
+ // see https://www.php.net/manual/en/function.gettype
+ if ($type === 'double') {
+ $type = 'float';
+ }
+ return $type;
+}
diff --git a/templates/macros/page.twig b/templates/macros/page.twig
index bbca12fa..33afbbde 100644
--- a/templates/macros/page.twig
+++ b/templates/macros/page.twig
@@ -1,30 +1,30 @@
-{% macro filters(routeParams) %}
+{% macro filters(route_params) %}
- {% if routeParams %}
+ {% if route_params %}
- {% if routeParams.author %}
+ {% if route_params.author %}
- Filtered by Author "{{ routeParams.author }}"
+ Filtered by Author "{{ route_params.author }}"
- {% elseif routeParams.category %}
+ {% elseif route_params.category %}
- Filtered by Category "{{ routeParams.category }}"
+ Filtered by Category "{{ route_params.category }}"
- {% elseif routeParams.tag %}
+ {% elseif route_params.tag %}
- Filtered by Tag "{{ routeParams.tag }}"
+ Filtered by Tag "{{ route_params.tag }}"
- {% elseif routeParams.year and routeParams.month and routeParams.day %}
+ {% elseif route_params.year and route_params.month and route_params.day %}
- Filtered by Year/Month/Day "{{ routeParams.year }}-{{ routeParams.month }}-{{ routeParams.day }}"
+ Filtered by Year/Month/Day "{{ route_params.year }}-{{ route_params.month }}-{{ route_params.day }}"
- {% elseif routeParams.year and routeParams.month %}
+ {% elseif route_params.year and route_params.month %}
- Filtered by Year/Month "{{ routeParams.year }}-{{ routeParams.month }}"
+ Filtered by Year/Month "{{ route_params.year }}-{{ route_params.month }}"
- {% elseif routeParams.year %}
+ {% elseif route_params.year %}
- Filtered by Year "{{ routeParams.year }}"
+ Filtered by Year "{{ route_params.year }}"
{% endif %}
@@ -58,7 +58,7 @@
{% set delim = '' %}
{% for category in categories %}
{%- set route = options.pageRoute ~ '/category/' ~ category|slugify -%}
- {{ delim|raw }}{{ page_link(route, category) }}
+ {{ delim|raw }}{{ link_page(route, category) }}
{%- set delim = ', ' -%}
{% endfor %}
@@ -75,7 +75,7 @@
{% set delim = '' %}
{% for tag in tags %}
{%- set route = options.pageRoute ~ '/tag/' ~ tag|slugify -%}
- {{ delim|raw }}{{ page_link(route, tag) }}
+ {{ delim|raw }}{{ link_page(route, tag) }}
{%- set delim = ', ' -%}
{% endfor %}
@@ -92,7 +92,7 @@
{% set delim = '' %}
{% for author in authors %}
{%- set route = options.pageRoute ~ '/author/' ~ author|slugify -%}
- {{ delim|raw }}{{ page_link(route, author) }}
+ {{ delim|raw }}{{ link_page(route, author) }}
{%- set delim = ', ' -%}
{% endfor %}
diff --git a/templates/macros/pages.twig b/templates/macros/pages.twig
index 0d93a02b..7cddf41b 100644
--- a/templates/macros/pages.twig
+++ b/templates/macros/pages.twig
@@ -22,8 +22,8 @@
{% for pageItem in recentPages %}
-
- {{ page_link(pageItem.route, pageItem.title) }}
- {% if options.showDate %}
{{ pageItem.date|strftime(options.dateFormat) }}{% endif %}
+ {{ link_page(pageItem.route, pageItem.title) }}
+ {% if options.showDate %}
{{ pageItem.date|date(options.dateFormat) }}{% endif %}
{% endfor %}
diff --git a/templates/macros/taxonomy.twig b/templates/macros/taxonomy.twig
index f4e903f0..675c4bfb 100644
--- a/templates/macros/taxonomy.twig
+++ b/templates/macros/taxonomy.twig
@@ -17,8 +17,8 @@
{% for item in months %}
{% set route = pageRoute ~ '/' ~ item.year ~ '/' ~ item.month %}
- {% set label = item.date|strftime('%B %Y') %}
- - {{ page_link(route, label) }}{% if showCount %} ({{ item.count }}){% endif %}
+ {% set label = item.date|date('%B %Y') %}
+ - {{ link_page(route, label) }}{% if showCount %} ({{ item.count }}){% endif %}
{% endfor %}
@@ -47,7 +47,7 @@
{% for author, count in authors %}
{% set route = pageRoute ~ '/author/' ~ author|slugify %}
- - {{ page_link(route, author) }}{% if showCount %} ({{ count }}){% endif %}
+ - {{ link_page(route, author) }}{% if showCount %} ({{ count }}){% endif %}
{% endfor %}
@@ -76,7 +76,7 @@
{% for category, count in categories %}
{% set route = pageRoute ~ '/category/' ~ category|slugify %}
- - {{ page_link(route, category) }}{% if showCount %} ({{ count }}){% endif %}
+ - {{ link_page(route, category) }}{% if showCount %} ({{ count }}){% endif %}
{% endfor %}
@@ -105,7 +105,7 @@
{% for tag, count in tags %}
{% set route = pageRoute ~ '/tag/' ~ tag|slugify %}
- - {{ page_link(route, tag) }}{% if showCount %} ({{ count }}){% endif %}
+ - {{ link_page(route, tag) }}{% if showCount %} ({{ count }}){% endif %}
{% endfor %}
diff --git a/templates/page/taxonomies.twig b/templates/page/taxonomies.twig
deleted file mode 100644
index 4b6efc1a..00000000
--- a/templates/page/taxonomies.twig
+++ /dev/null
@@ -1,58 +0,0 @@
-{% apply spaceless %}
- {% if renderCategories and renderTags and renderAuthors %}
-
-
- {# render markup for categories #}
- {% if renderCategories %}
- {% set categories = page.getCategories() %}
- {% if categories %}
-
- Categories:
-
- {% set delim = '' %}
- {% for category in categories %}
- {%- set route = pageRoute ~ '/category/' ~ category|slugify -%}
- {{ delim|raw }}{{ page_link(route, category) }}
- {%- set delim = ', ' -%}
- {% endfor %}
-
- {% endif %}
- {% endif %}
-
- {# render markup for tags #}
- {% if renderTags %}
- {% set tags = page.getTags() %}
- {% if tags %}
-
- Tags:
-
- {% set delim = '' %}
- {% for tag in tags %}
- {%- set route = pageRoute ~ '/tag/' ~ tag|slugify -%}
- {{ delim|raw }}{{ page_link(route, tag) }}
- {%- set delim = ', ' -%}
- {% endfor %}
-
- {% endif %}
- {% endif %}
-
- {# render markup for authors #}
- {% if renderAuthors %}
- {% set authors = page.getAuthors() %}
- {% if authors %}
-
- Authors:
-
- {% set delim = '' %}
- {% for author in authors %}
- {%- set route = pageRoute ~ '/author/' ~ author|slugify -%}
- {{ delim|raw }}{{ page_link(route, author) }}
- {%- set delim = ', ' -%}
- {% endfor %}
-
- {% endif %}
- {% endif %}
-
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/templates/pages/filtered.twig b/templates/pages/filtered.twig
deleted file mode 100644
index 8ca8bd60..00000000
--- a/templates/pages/filtered.twig
+++ /dev/null
@@ -1,31 +0,0 @@
-{% apply spaceless %}
- {% if routeParams %}
-
- {% if routeParams.author %}
-
- Filtered by Author "{{ routeParams.author }}"
-
- {% elseif routeParams.category %}
-
- Filtered by Category "{{ routeParams.category }}"
-
- {% elseif routeParams.tag %}
-
- Filtered by Tag "{{ routeParams.tag }}"
-
- {% elseif routeParams.year and routeParams.month and routeParams.day %}
-
- Filtered by Year/Month/Day "{{ routeParams.year }}-{{ routeParams.month }}-{{ routeParams.day }}"
-
- {% elseif routeParams.year and routeParams.month %}
-
- Filtered by Year/Month "{{ routeParams.year }}-{{ routeParams.month }}"
-
- {% elseif routeParams.year %}
-
- Filtered by Year "{{ routeParams.year }}"
-
- {% endif %}
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/templates/pages/recent.twig b/templates/pages/recent.twig
deleted file mode 100644
index 0fa87f0c..00000000
--- a/templates/pages/recent.twig
+++ /dev/null
@@ -1,15 +0,0 @@
-{% apply spaceless %}
- {% if recentPages|length > 0 %}
-
-
{{ title }}
-
- {% for pageItem in recentPages %}
- -
- {{ page_link(pageItem.route, pageItem.title) }}
- {% if showDate %}
{{ pageItem.date|strftime(dateFormat) }}{% endif %}
-
- {% endfor %}
-
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/templates/snippets/herbie_info.twig b/templates/snippets/herbie_info.twig
index e090daa1..3df749e8 100644
--- a/templates/snippets/herbie_info.twig
+++ b/templates/snippets/herbie_info.twig
@@ -1,19 +1,19 @@
System Info
{#
Constants ({{ constants|length }})
diff --git a/templates/snippets/mail_link.twig b/templates/snippets/link_mail.twig
similarity index 100%
rename from templates/snippets/mail_link.twig
rename to templates/snippets/link_mail.twig
diff --git a/templates/snippets/listing.twig b/templates/snippets/listing.twig
index 30df443e..5c36b4f2 100644
--- a/templates/snippets/listing.twig
+++ b/templates/snippets/listing.twig
@@ -3,10 +3,10 @@
{% for menuitem in pagination %}
- {{ page_link(menuitem.route, menuitem.getMenuTitle()) }}
+ {{ link_page(menuitem.route, menuitem.getMenuTitle()) }}
{% if menuitem.image %}
-
+
{% endif %}
{{ menuitem.excerpt }}
@@ -17,10 +17,10 @@
diff --git a/templates/taxonomy/archive.twig b/templates/taxonomy/archive.twig
deleted file mode 100644
index 78a6abfe..00000000
--- a/templates/taxonomy/archive.twig
+++ /dev/null
@@ -1,14 +0,0 @@
-{% apply spaceless %}
- {% if months|length > 0 %}
-
-
{{ title }}
-
- {% for item in months %}
- {% set route = pageRoute ~ '/' ~ item.year ~ '/' ~ item.month %}
- {% set label = item.date|strftime('%B %Y') %}
- - {{ page_link(route, label) }}{% if showCount %} ({{ item.count }}){% endif %}
- {% endfor %}
-
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/templates/taxonomy/authors.twig b/templates/taxonomy/authors.twig
deleted file mode 100644
index 58183ad7..00000000
--- a/templates/taxonomy/authors.twig
+++ /dev/null
@@ -1,13 +0,0 @@
-{% apply spaceless %}
- {% if authors|length > 0 %}
-
-
{{ title }}
-
- {% for author, count in authors %}
- {% set route = pageRoute ~ '/author/' ~ author|slugify %}
- - {{ page_link(route, author) }}{% if showCount %} ({{ count }}){% endif %}
- {% endfor %}
-
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/templates/taxonomy/categories.twig b/templates/taxonomy/categories.twig
deleted file mode 100644
index 703b567e..00000000
--- a/templates/taxonomy/categories.twig
+++ /dev/null
@@ -1,13 +0,0 @@
-{% apply spaceless %}
- {% if categories|length > 0 %}
-
-
{{ title }}
-
- {% for category, count in categories %}
- {% set route = pageRoute ~ '/category/' ~ category|slugify %}
- - {{ page_link(route, category) }}{% if showCount %} ({{ count }}){% endif %}
- {% endfor %}
-
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/templates/taxonomy/tags.twig b/templates/taxonomy/tags.twig
deleted file mode 100644
index 08a4a5c5..00000000
--- a/templates/taxonomy/tags.twig
+++ /dev/null
@@ -1,13 +0,0 @@
-{% apply spaceless %}
- {% if tags|length > 0 %}
-
-
{{ title }}
-
- {% for tag, count in tags %}
- {% set route = pageRoute ~ '/tag/' ~ tag|slugify %}
- - {{ page_link(route, tag) }}{% if showCount %} ({{ count }}){% endif %}
- {% endfor %}
-
-
- {% endif %}
-{% endapply %}
\ No newline at end of file
diff --git a/tests/_data/site/config/main.php b/tests/_data/site/config/main.php
index 5ec2dca7..125b9e90 100644
--- a/tests/_data/site/config/main.php
+++ b/tests/_data/site/config/main.php
@@ -18,11 +18,11 @@
]
],
'enabledPlugins' => '',
- 'enabledSysPlugins' => 'twig_core,twig_plus,markdown,rest,textile,imagine,dummy',
+ 'enabledSysPlugins' => 'twig,markdown,rest,textile,imagine,dummy',
'plugins' => [
'imagine' => [
'test' => true,
- 'filterSets' => [
+ 'collections' => [
'bsp1' => [
'filters' => [
'thumbnail' => [
diff --git a/tests/_data/site/pages/index.html b/tests/_data/site/pages/index.html
index 832c938e..94e41200 100644
--- a/tests/_data/site/pages/index.html
+++ b/tests/_data/site/pages/index.html
@@ -4,21 +4,21 @@
Herbie Tests
-{{ 100000|filesize }}
+{{ 100000|format_size }}
-{{ sitemap() }}
+{{ menu_sitemap() }}
Downloads
-{{ file_link('favicon.ico', 'File', true) }}
+{{ link_file('favicon.ico', 'File', true) }}
-Download
+Download
- - {{ file_link('dummy.pdf') }}
- - {{ file_link('dummy.pdf', 'Dummy', false) }}
- - {{ file_link('dummy.pdf', 'Dummy', true) }}
- - {{ file_link('dummy.pdf', 'Dummy', true, { target:"_blank"}) }}
+ - {{ link_file('dummy.pdf') }}
+ - {{ link_file('dummy.pdf', 'Dummy', false) }}
+ - {{ link_file('dummy.pdf', 'Dummy', true) }}
+ - {{ link_file('dummy.pdf', 'Dummy', true, { target:"_blank"}) }}
Imagine
@@ -36,13 +36,13 @@ Imagine
Links
- - {{ page_link('adfsafsdf', 'About') }}
- - {{ page_link('about', 'About') }}
- - {{ page_link('https://example.com/', 'About') }}
+ - {{ link_page('adfsafsdf', 'About') }}
+ - {{ link_page('about', 'About') }}
+ - {{ link_page('https://example.com/', 'About') }}
Mailto-Links
- - {{ mail_link('me@example.com', 'John Doe') }}
+ - {{ link_mail('me@example.com', 'John Doe') }}
diff --git a/tests/_data/site/pages/plugins/dummy.html b/tests/_data/site/pages/plugins/dummy.html
index 22dd0130..1cc9e53f 100644
--- a/tests/_data/site/pages/plugins/dummy.html
+++ b/tests/_data/site/pages/plugins/dummy.html
@@ -9,4 +9,4 @@ Dummy Plugin
{{ dummy("This is from") }}.
{% if "dummy" is dummy %}This is from Dummy Test.{% endif %}
{% apply dummy_dynamic %}This is from {% endapply %}.
-{{ file_link('dummy.pdf', 'Dummy', true) }}
+{{ link_file('dummy.pdf', 'Dummy', true) }}
diff --git a/tests/_data/site/pages/plugins/twig-plus.html b/tests/_data/site/pages/plugins/twig-plus.html
deleted file mode 100644
index 53e1d20a..00000000
--- a/tests/_data/site/pages/plugins/twig-plus.html
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Twig Plus Plugin
-hidden: false
----
diff --git a/tests/_data/site/themes/default/main.twig b/tests/_data/site/themes/default/main.twig
index 166581fa..e0475242 100644
--- a/tests/_data/site/themes/default/main.twig
+++ b/tests/_data/site/themes/default/main.twig
@@ -4,7 +4,7 @@
- {{ page_title(delim=' / ', siteTitle='Herbie Tests') }}
+ {{ page_title(delim=' / ', site_title='Herbie Tests') }}