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 = ''; + + 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 = ''; - - 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') }}