Skip to content

Commit 46a58d5

Browse files
committedMar 12, 2021
Initial upload
1 parent 02834d0 commit 46a58d5

38 files changed

+7619
-0
lines changed
 

‎.github/workflows/php.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: PHP
2+
on:
3+
push:
4+
branches: [ master ]
5+
pull_request:
6+
branches: [ master ]
7+
8+
jobs:
9+
ci-tests:
10+
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v2
15+
16+
- name: Validate composer.json and composer.lock
17+
run: composer validate
18+
19+
- name: Cache Composer packages
20+
id: composer-cache
21+
uses: actions/cache@v2
22+
with:
23+
path: vendor
24+
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
25+
restore-keys: |
26+
${{ runner.os }}-php-
27+
28+
- name: Install dependencies
29+
if: steps.composer-cache.outputs.cache-hit != 'true'
30+
run: composer install --prefer-dist --no-progress --no-suggest --ignore-platform-reqs
31+
32+
# Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
33+
# Docs: https://getcomposer.org/doc/articles/scripts.md
34+
- name: Run PHPStan
35+
run: composer run-script phpstan
36+
37+
- name: Run PHP Coding Standards Fixer
38+
run: composer run-script cscheck

‎.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.sass-cache/
2+
/.idea/
3+
/.php_cs.cache
4+
/vendor/

‎.php_cs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/** @noinspection PhpUndefinedNamespaceInspection */
3+
/** @noinspection PhpUndefinedClassInspection */
4+
5+
$finder = PhpCsFixer\Finder::create()
6+
->notPath('cache')
7+
->notPath('vendor')
8+
->in(__DIR__)
9+
->name('*.php')
10+
->notName('*.blade.php')
11+
->ignoreDotFiles(true)
12+
->ignoreVCS(true)
13+
;
14+
15+
return PhpCsFixer\Config::create()
16+
->setRules([
17+
'@PSR2' => true,
18+
'align_multiline_comment' => ['comment_type' => 'phpdocs_like'],
19+
'array_syntax' => ['syntax' => 'short'],
20+
'braces' => [
21+
'allow_single_line_closure' => true,
22+
'position_after_control_structures' => 'next',
23+
'position_after_functions_and_oop_constructs' => 'next',
24+
'position_after_anonymous_constructs' => 'next',
25+
],
26+
'constant_case' => ['case' => 'lower'],
27+
'function_declaration' => ['closure_function_spacing' => 'none'],
28+
'linebreak_after_opening_tag' => true,
29+
'phpdoc_order' => true,
30+
'strict_param' => true,
31+
'string_line_ending' => true,
32+
'native_function_invocation' => [
33+
'include' => ['@all'],
34+
],
35+
'global_namespace_import' => [
36+
'import_classes' => null,
37+
'import_constants' => true,
38+
'import_functions' => true,
39+
]
40+
])
41+
->setFinder($finder);

‎composer.json

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"name": "cyndaron/view",
3+
"description": "View and template components Cyndaron",
4+
"license": "MIT",
5+
"autoload": {
6+
"psr-4": {
7+
"Cyndaron\\View\\": "src/"
8+
}
9+
},
10+
"repositories": [
11+
{
12+
"type": "vcs",
13+
"url": "https://github.com/Cyndaron/module"
14+
},
15+
{
16+
"type": "vcs",
17+
"url": "https://github.com/Cyndaron/dbal"
18+
},
19+
{
20+
"type": "vcs",
21+
"url": "https://github.com/Cyndaron/util"
22+
},
23+
{
24+
"type": "vcs",
25+
"url": "https://github.com/Cyndaron/Cyndaron"
26+
},
27+
{
28+
"type": "vcs",
29+
"url": "https://github.com/Cyndaron/module-geelhoed"
30+
},
31+
{
32+
"type": "vcs",
33+
"url": "https://github.com/Cyndaron/module-minecraft"
34+
},
35+
{
36+
"type": "vcs",
37+
"url": "https://github.com/Cyndaron/module-ticketsale"
38+
}
39+
],
40+
"minimum-stability": "dev",
41+
"prefer-stable": true,
42+
"require": {
43+
"php": "^7.4",
44+
"thecodingmachine/safe": "^1.1",
45+
"cyndaron/module": "^1.0",
46+
"cyndaron/dbal": "^1.0",
47+
"cyndaron/util": "9999999-dev"
48+
},
49+
"require-dev": {
50+
"cyndaron/cyndaron": "dev-master",
51+
"roave/security-advisories": "dev-master",
52+
"phan/phan": "^3.0",
53+
"thecodingmachine/phpstan-safe-rule": "^1.0",
54+
"phpstan/phpstan": "^0.12.42",
55+
"friendsofphp/php-cs-fixer": "^2.16"
56+
},
57+
"scripts": {
58+
"phpstan": [
59+
"vendor/bin/phpstan analyse src --level 8"
60+
],
61+
"cscheck": [
62+
"vendor/bin/php-cs-fixer fix src/ --allow-risky=yes --dry-run --diff"
63+
]
64+
}
65+
}

‎composer.lock

+6,382
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎phpstan.neon

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
checkMissingIterableValueType: false
3+
includes:
4+
- vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon

‎src/Page.php

+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<?php
2+
/**
3+
* Copyright © 2009-2020 Michael Steenbeek
4+
*
5+
* Cyndaron is licensed under the MIT License. See the LICENSE file for more details.
6+
*/
7+
/** @noinspection PhpIncludeInspection */
8+
9+
namespace Cyndaron\View;
10+
11+
use Cyndaron\Category\ModelWithCategory;
12+
use Cyndaron\CyndaronInfo;
13+
use Cyndaron\Menu\MenuItem;
14+
use Cyndaron\Util\Setting;
15+
use Cyndaron\View\Template\ViewHelpers;
16+
use Cyndaron\User\User;
17+
use Cyndaron\DBAL\Model;
18+
19+
use function Safe\sprintf;
20+
use function Safe\substr;
21+
use function assert;
22+
use function dirname;
23+
use function str_replace;
24+
use function basename;
25+
use function strrchr;
26+
use function file_exists;
27+
use function ob_start;
28+
use function ob_get_clean;
29+
use function count;
30+
use function array_merge;
31+
32+
class Page
33+
{
34+
public const DEFAULT_SCRIPTS = [
35+
'/vendor/components/jquery/jquery.min.js',
36+
'/vendor/twbs/bootstrap/dist/js/bootstrap.min.js',
37+
'/js/cyndaron.js',
38+
];
39+
40+
protected string $extraMeta = '';
41+
protected string $title = '';
42+
protected array $extraScripts = [];
43+
protected array $extraCss = [];
44+
protected string $websiteName = '';
45+
protected string $body = '';
46+
47+
protected ?Model $model = null;
48+
49+
protected string $template = '';
50+
protected array $templateVars = [];
51+
52+
public function __construct(string $title, string $body = '')
53+
{
54+
$this->title = $title;
55+
$this->body = $body;
56+
57+
$this->updateTemplate();
58+
}
59+
60+
protected function updateTemplate(): void
61+
{
62+
if (empty($this->template))
63+
{
64+
$rc = new \ReflectionClass(static::class);
65+
$filename = $rc->getFileName();
66+
assert($filename !== false);
67+
$dir = dirname($filename) . '/templates';
68+
69+
$file = str_replace('.php', '.blade.php', basename($filename));
70+
$testPath = "$dir/$file";
71+
72+
if (file_exists($testPath))
73+
{
74+
$this->template = $testPath;
75+
}
76+
else
77+
{
78+
$this->template = 'Index';
79+
}
80+
}
81+
}
82+
83+
protected function renderSkeleton(): void
84+
{
85+
$this->websiteName = Setting::get('siteName');
86+
$this->templateVars['isAdmin'] = User::isAdmin();
87+
$this->templateVars['websiteName'] = $this->websiteName;
88+
$this->templateVars['title'] = $this->title;
89+
$this->templateVars['referrer'] = $_SESSION['referrer'] ?? '';
90+
$this->templateVars['previewImage'] = '';
91+
if ($this->model instanceof ModelWithCategory)
92+
{
93+
$this->templateVars['previewImage'] = $this->model->getPreviewImage();
94+
}
95+
96+
$this->templateVars['version'] = CyndaronInfo::ENGINE_VERSION;
97+
$favicon = Setting::get('favicon');
98+
$this->templateVars['favicon'] = $favicon;
99+
if ($favicon !== '')
100+
{
101+
$dotPosition = strrchr($favicon, '.');
102+
$extension = $dotPosition !== false ? substr($dotPosition, 1) : '';
103+
/** @todo Replace with actual mime type check */
104+
$this->templateVars['faviconType'] = "image/$extension";
105+
}
106+
107+
foreach (['backgroundColor', 'menuColor', 'menuBackground', 'articleColor', 'accentColor'] as $setting)
108+
{
109+
$this->templateVars[$setting] = Setting::get($setting);
110+
}
111+
112+
$this->templateVars['menu'] = $this->renderMenu();
113+
114+
$jumboContents = Setting::get('jumboContents');
115+
$this->templateVars['showJumbo'] = $this->isFrontPage() && Setting::get('frontPageIsJumbo') && $jumboContents;
116+
$this->templateVars['jumboContents'] = ViewHelpers::parseText($jumboContents);
117+
118+
$this->templateVars['pageCaptionClasses'] = '';
119+
if ($this->isFrontPage())
120+
{
121+
$this->templateVars['pageCaptionClasses'] = 'voorpagina';
122+
}
123+
124+
$this->templateVars['pageCaption'] = $this->generateBreadcrumbs();
125+
126+
$this->templateVars['scripts'] = array_merge(self::DEFAULT_SCRIPTS, $this->extraScripts);
127+
$this->templateVars['extraCss'] = $this->extraCss;
128+
129+
static $includes = [
130+
'extraHead' => 'extra-head',
131+
'extraBodyStart' => 'extra-body-start',
132+
'extraBodyEnd' => 'extra-body-end'
133+
];
134+
135+
foreach ($includes as $varName => $filename)
136+
{
137+
$this->templateVars[$varName] = '';
138+
if (file_exists(__DIR__ . "/../$filename.php"))
139+
{
140+
ob_start();
141+
include __DIR__ . "/../$filename.php";
142+
$this->templateVars[$varName] = ViewHelpers::parseText(ob_get_clean() ?: '');
143+
}
144+
}
145+
}
146+
147+
public function setExtraMeta(string $extraMeta): void
148+
{
149+
$this->extraMeta = $extraMeta;
150+
}
151+
152+
public function isFrontPage(): bool
153+
{
154+
return $_SERVER['REQUEST_URI'] === '/';
155+
}
156+
157+
protected function renderMenu(): string
158+
{
159+
$logo = Setting::get('logo');
160+
$vars = [
161+
'isLoggedIn' => User::isLoggedIn(),
162+
'isAdmin' => User::isAdmin(),
163+
'inverseClass' => (Setting::get('menuTheme') === 'dark') ? 'navbar-dark' : 'navbar-light',
164+
'navbar' => $logo !== '' ? sprintf('<img alt="" src="%s"> ', $logo) : $this->websiteName,
165+
];
166+
167+
$vars['menuItems'] = $this->getMenu();
168+
$vars['configMenuItems'] = [
169+
['link' => '/system', 'title' => 'Systeembeheer', 'icon' => 'cog'],
170+
['link' => '/pagemanager', 'title' => 'Pagina-overzicht', 'icon' => 'th-list'],
171+
['link' => '/menu-editor', 'title' => 'Menu bewerken', 'icon' => 'menu-hamburger'],
172+
['link' => '/user/manager', 'title' => 'Gebruikersbeheer', 'icon' => 'user'],
173+
];
174+
$userMenuItems = [
175+
['link' => '', 'title' => $_SESSION['username'] ?? ''],
176+
];
177+
foreach (User::getUserMenuFiltered() as $extraItem)
178+
{
179+
$userMenuItems[] = ['link' => $extraItem['link'], 'title' => $extraItem['label'], 'icon' => $extraItem['icon'] ?? ''];
180+
}
181+
$userMenuItems[] = ['link' => '/user/changePassword', 'title' => 'Wachtwoord wijzigen', 'icon' => 'lock'];
182+
$userMenuItems[] = ['link' => '/user/logout', 'title' => 'Uitloggen', 'icon' => 'log-out'];
183+
184+
$vars['userMenuItems'] = $userMenuItems;
185+
186+
$vars['notifications'] = User::getNotifications();
187+
188+
$template = new \Cyndaron\View\Template\Template();
189+
return $template->render('Menu', $vars);
190+
}
191+
192+
public function render(array $vars = []): string
193+
{
194+
$this->addTemplateVars($vars);
195+
196+
$this->templateVars['contents'] = $this->body;
197+
198+
$this->renderSkeleton();
199+
200+
$template = new \Cyndaron\View\Template\Template();
201+
return $template->render($this->template, $this->templateVars);
202+
}
203+
204+
public function addScript(string $filename): void
205+
{
206+
$this->extraScripts[] = $filename;
207+
}
208+
209+
public function addCss(string $filename): void
210+
{
211+
$this->extraCss[] = $filename;
212+
}
213+
214+
public function getMenu(): array
215+
{
216+
if (!User::hasSufficientReadLevel())
217+
{
218+
return [];
219+
}
220+
return MenuItem::fetchAll([], [], 'ORDER BY priority, id');
221+
}
222+
223+
protected function generateBreadcrumbs(): string
224+
{
225+
$title = '';
226+
$titleParts = [$this->title];
227+
if ($this->model instanceof ModelWithCategory)
228+
{
229+
$titleParts = [];
230+
if ($this->model->showBreadcrumbs)
231+
{
232+
$category = $this->model->getFirstCategory();
233+
if ($category !== null)
234+
{
235+
$titleParts[] = $category->name;
236+
}
237+
}
238+
$titleParts[] = $this->model->name;
239+
}
240+
241+
$count = count($titleParts);
242+
if ($count === 1)
243+
{
244+
$title = $titleParts[0];
245+
}
246+
else
247+
{
248+
for ($i = 0; $i < $count; $i++)
249+
{
250+
$class = ($i === 0) ? 'breadcrumb-main-item' : 'breadcrumb-item';
251+
$title .= sprintf('<span class="%s">%s</span>', $class, $titleParts[$i]);
252+
if ($i !== $count - 1)
253+
{
254+
$title .= '<span class="breadcrumb-separator"> // </span>';
255+
}
256+
}
257+
}
258+
259+
return $title;
260+
}
261+
262+
/**
263+
* @param string $varName
264+
* @param mixed $var
265+
*/
266+
public function addTemplateVar(string $varName, $var): void
267+
{
268+
$this->templateVars[$varName] = $var;
269+
}
270+
271+
public function addTemplateVars(array $vars): void
272+
{
273+
$this->templateVars = array_merge($this->templateVars, $vars);
274+
}
275+
}

‎src/Template/Blade.php

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
namespace Cyndaron\View\Template;
4+
5+
use Illuminate\Container\Container;
6+
use Illuminate\Contracts\View\Factory as FactoryContract;
7+
use Illuminate\Contracts\View\View;
8+
use Illuminate\Events\Dispatcher;
9+
use Illuminate\Filesystem\Filesystem;
10+
use Illuminate\View\Compilers\BladeCompiler;
11+
use Illuminate\View\Factory;
12+
use Pine\BladeFilters\BladeFiltersCompiler;
13+
use function assert;
14+
use function is_string;
15+
use function call_user_func_array;
16+
17+
/**
18+
* Based on the original at https://github.com/jenssegers/blade
19+
* which is stated to be released under the terms of the MIT License, but does not contain a copy of that license.
20+
*
21+
* Any changes to this file are © Michael Steenbeek and available under the same license.
22+
*
23+
* Class Blade
24+
* @package Cyndaron\Template
25+
*/
26+
final class Blade implements FactoryContract
27+
{
28+
protected Container $container;
29+
30+
private Factory $factory;
31+
32+
private BladeCompiler $compiler;
33+
34+
/**
35+
* Blade constructor.
36+
* @param string[] $viewPaths
37+
* @param string $cachePath
38+
*/
39+
public function __construct(array $viewPaths, string $cachePath)
40+
{
41+
$this->container = new Container();
42+
43+
$this->setupContainer($viewPaths, $cachePath);
44+
/** @noinspection PhpParamsInspection @phpstan-ignore-next-line */
45+
(new ViewServiceProvider($this->container))->register();
46+
47+
$this->factory = $this->container->get('view');
48+
$this->compiler = $this->container->get('blade.compiler');
49+
$this->compiler->extend(function($view)
50+
{
51+
return $this->container[BladeFiltersCompiler::class]->compile($view);
52+
});
53+
}
54+
55+
public function render(string $view, array $data = [], array $mergeData = []): string
56+
{
57+
return $this->make($view, $data, $mergeData)->render();
58+
}
59+
60+
public function make($view, $data = [], $mergeData = []): View
61+
{
62+
return $this->factory->make($view, $data, $mergeData);
63+
}
64+
65+
public function compiler(): BladeCompiler
66+
{
67+
return $this->compiler;
68+
}
69+
70+
public function directive(string $name, callable $handler): void
71+
{
72+
$this->compiler->directive($name, $handler);
73+
}
74+
75+
public function exists($view): bool
76+
{
77+
return $this->factory->exists($view);
78+
}
79+
80+
public function file($path, $data = [], $mergeData = []): View
81+
{
82+
return $this->factory->file($path, $data, $mergeData);
83+
}
84+
85+
public function share($key, $value = null)
86+
{
87+
assert(is_string($key));
88+
return $this->factory->shared($key, $value);
89+
}
90+
91+
public function composer($views, $callback): array
92+
{
93+
return $this->factory->composer($views, $callback);
94+
}
95+
96+
public function creator($views, $callback): array
97+
{
98+
return $this->factory->creator($views, $callback);
99+
}
100+
101+
public function addNamespace($namespace, $hints): self
102+
{
103+
$this->factory->addNamespace($namespace, $hints);
104+
105+
return $this;
106+
}
107+
108+
public function replaceNamespace($namespace, $hints): self
109+
{
110+
$this->factory->replaceNamespace($namespace, $hints);
111+
112+
return $this;
113+
}
114+
115+
/**
116+
* @param string $method
117+
* @param array $params
118+
* @return mixed
119+
*/
120+
public function __call(string $method, array $params)
121+
{
122+
/** @phpstan-ignore-next-line (false positive) */
123+
return call_user_func_array([$this->factory, $method], $params);
124+
}
125+
126+
protected function setupContainer(array $viewPaths, string $cachePath): void
127+
{
128+
$this->container->bindIf('files', static function()
129+
{
130+
return new Filesystem();
131+
}, true);
132+
133+
$this->container->bindIf('events', static function()
134+
{
135+
return new Dispatcher();
136+
}, true);
137+
138+
$this->container->bindIf('config', static function() use ($viewPaths, $cachePath)
139+
{
140+
return [
141+
'view.paths' => $viewPaths,
142+
'view.compiled' => $cachePath,
143+
];
144+
}, true);
145+
}
146+
}

‎src/Template/Template.php

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Cyndaron\View\Template;
4+
5+
use Cyndaron\Util\Util;
6+
use Safe\Exceptions\FilesystemException;
7+
8+
final class Template
9+
{
10+
private const COMPILED_DIR = 'cache/template';
11+
private TemplateFinder $templateFinder;
12+
13+
public function __construct()
14+
{
15+
$this->templateFinder = new TemplateFinder();
16+
}
17+
18+
/**
19+
* @param string $engine
20+
* @throws FilesystemException
21+
* @return string
22+
*/
23+
public function createCacheDir(string $engine): string
24+
{
25+
$cacheDir = self::COMPILED_DIR . '/' . $engine;
26+
Util::createDir($cacheDir);
27+
28+
return $cacheDir;
29+
}
30+
31+
/**
32+
* @param string $template
33+
* @param array $data
34+
* @throws FilesystemException
35+
* @return string
36+
*/
37+
public function render(string $template, array $data = []): string
38+
{
39+
$blade = new Blade([], $this->createCacheDir('blade'));
40+
$result = $blade->make($template, $data);
41+
42+
return $result->render();
43+
}
44+
45+
public function templateExists(string $name): bool
46+
{
47+
return $this->templateFinder->path($name) !== null;
48+
}
49+
}

‎src/Template/TemplateFinder.php

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace Cyndaron\View\Template;
4+
5+
use Cyndaron\Module\TemplateRoot;
6+
use function array_key_exists;
7+
use function array_slice;
8+
use function file_exists;
9+
use function explode;
10+
use function count;
11+
use function array_pop;
12+
use function implode;
13+
use function rtrim;
14+
15+
final class TemplateFinder
16+
{
17+
private static array $templateRoots = [
18+
];
19+
20+
/**
21+
* Locate actual path to template file (based on current SmartyTools logic)
22+
*
23+
* @param string $name
24+
* @return string|null
25+
*/
26+
public function path(string $name): ?string
27+
{
28+
// Full path?
29+
if (file_exists($name))
30+
{
31+
return $name;
32+
}
33+
34+
// First, look in the global folder.
35+
$template = $this->searchPath('src/templates/', $name);
36+
37+
// If the template is not present in the global folder, look in the module templates.
38+
if ($template === null)
39+
{
40+
$template = $this->searchSrcAndVendor($name);
41+
}
42+
43+
return $template;
44+
}
45+
46+
/**
47+
* @param string $path
48+
* @param string $name
49+
* @return string|null
50+
*/
51+
private function searchPath(string $path, string $name): ?string
52+
{
53+
$baseName = $path . $name;
54+
$files = [
55+
$baseName . '.blade.php',
56+
$baseName . '.html',
57+
$baseName,
58+
];
59+
60+
foreach ($files as $file)
61+
{
62+
if (file_exists($file))
63+
{
64+
return $file;
65+
}
66+
}
67+
68+
return null;
69+
}
70+
71+
/**
72+
* @param string $fullName
73+
* @return string|null
74+
*/
75+
private function searchSrcAndVendor(string $fullName): ?string
76+
{
77+
$template = null;
78+
$parts = explode('/', $fullName);
79+
if (count($parts) > 1)
80+
{
81+
$name = array_pop($parts);
82+
$module = $parts[0];
83+
$parts = array_slice($parts, 1);
84+
85+
$pathInModule = implode('/', $parts);
86+
87+
$template = $this->searchPath("src/$module/$pathInModule/templates/", $name);
88+
if ($template === null && array_key_exists($module, static::$templateRoots))
89+
{
90+
$root = static::$templateRoots[$module];
91+
$template = $this->searchPath("$root/$pathInModule/templates/", $name);
92+
}
93+
}
94+
95+
return $template;
96+
}
97+
98+
public static function addTemplateRoot(TemplateRoot $templateRoot): void
99+
{
100+
static::$templateRoots[$templateRoot->name] = rtrim($templateRoot->root, '/');
101+
}
102+
}

‎src/Template/ViewFinder.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
namespace Cyndaron\View\Template;
3+
4+
use Illuminate\View\FileViewFinder;
5+
use InvalidArgumentException;
6+
use function strtr;
7+
8+
final class ViewFinder extends FileViewFinder
9+
{
10+
/**
11+
* Find the given view in the list of paths.
12+
*
13+
* @param string $name
14+
* @param array $paths
15+
* @throws InvalidArgumentException
16+
* @return string
17+
*
18+
*/
19+
protected function findInPaths($name, $paths): string
20+
{
21+
$name = strtr($name, [
22+
'.' => '/',
23+
'.blade.php' => '.blade.php',
24+
]);
25+
$templateFinder = new TemplateFinder();
26+
$path = $templateFinder->path($name);
27+
28+
if ($path !== null)
29+
{
30+
return $path;
31+
}
32+
33+
throw new InvalidArgumentException("View [$name] not found.");
34+
}
35+
}

‎src/Template/ViewHelpers.php

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
namespace Cyndaron\View\Template;
3+
4+
use Cyndaron\Photoalbum\Photoalbum;
5+
use Cyndaron\Photoalbum\PhotoalbumPage;
6+
use Cyndaron\User\User;
7+
use PhpOffice\PhpSpreadsheet\IOFactory;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
10+
use Safe\Exceptions\ArrayException;
11+
use Safe\Exceptions\DatetimeException;
12+
use function Safe\date;
13+
use function Safe\natsort;
14+
use function Safe\preg_replace;
15+
use function Safe\sprintf;
16+
use function Safe\strtotime;
17+
use function strip_tags;
18+
use function count;
19+
use function implode;
20+
use function array_slice;
21+
use function number_format;
22+
use function explode;
23+
use function array_key_exists;
24+
use function ob_start;
25+
use function ob_get_clean;
26+
use function preg_replace_callback;
27+
use function array_unique;
28+
29+
final class ViewHelpers
30+
{
31+
protected const DUTCH_MONTHS = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'];
32+
protected const DUTCH_WEEKDAYS = ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'];
33+
34+
protected const BUTTON_TYPE_TO_ICON = [
35+
'new' => 'plus',
36+
'edit' => 'pencil',
37+
'delete' => 'trash',
38+
'lastversion' => 'lastversion',
39+
'addtomenu' => 'bookmark',
40+
];
41+
42+
protected const BUTTON_TYPE_TO_CLASS = [
43+
'new' => 'btn-success',
44+
'delete' => 'btn-danger',
45+
];
46+
47+
private const YOUTUBE = '<div class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item" src="https://www.youtube.com/embed/$1" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation" allowfullscreen></iframe></div>';
48+
49+
/**
50+
* Zet een maandnummer om in de naam.
51+
* Bijvoorbeeld: 1 -> januari.
52+
*
53+
* @param int $number Het maandnummer, waarbij 1 januari is en 12 december.
54+
* @return string De naam van de maand, bijvoorbeeld "januari".
55+
*/
56+
public static function getDutchMonth(int $number): string
57+
{
58+
return self::DUTCH_MONTHS[$number];
59+
}
60+
61+
/**
62+
* Zet een dagnummer om in de naam.
63+
* Bijvoorbeeld: 0 -> zondag.
64+
*
65+
* @param int $number Het dagnummer, waarbij 0 zondag is en 6 zaterdag.
66+
* @return string De naam van de dag, bijvoorbeeld "zondag".
67+
*/
68+
public static function getDutchWeekday(int $number): string
69+
{
70+
return self::DUTCH_WEEKDAYS[$number % 7];
71+
}
72+
73+
/**
74+
* Limit a string to the specified word count.
75+
*
76+
* @param string $text The input string
77+
* @param int $length The maximum word count.
78+
* @param string $ellipsis What to use as postfix if the string is shortened.
79+
* @return string The shortened string, or the input string if it was short enough.
80+
*/
81+
public static function wordlimit(string $text, int $length = 50, string $ellipsis = ''): string
82+
{
83+
$text = strip_tags($text);
84+
$words = explode(' ', $text);
85+
if (count($words) > $length)
86+
{
87+
return implode(' ', array_slice($words, 0, $length)) . $ellipsis;
88+
}
89+
90+
return $text;
91+
}
92+
93+
public static function formatCurrency(float $amount): string
94+
{
95+
return number_format($amount, 2, ',', '.');
96+
}
97+
98+
public static function formatEuro(float $amount): string
99+
{
100+
return '€ ' . static::formatCurrency($amount);
101+
}
102+
103+
public static function boolToText(?bool $bool): string
104+
{
105+
return $bool ? 'Ja' : 'Nee';
106+
}
107+
108+
public static function filterHm(string $hms): string
109+
{
110+
$parts = explode(':', $hms);
111+
return "$parts[0]:$parts[1]";
112+
}
113+
114+
public static function filterDutchDate(string $date): string
115+
{
116+
try
117+
{
118+
$timestamp = strtotime($date);
119+
}
120+
catch (DatetimeException $e)
121+
{
122+
return 'Ongeldige datum';
123+
}
124+
$day = date('j', $timestamp);
125+
$month = self::getDutchMonth((int)date('m', $timestamp));
126+
$year = date('Y', $timestamp);
127+
return sprintf('%s %s %s', $day, $month, $year);
128+
}
129+
130+
public static function filterDutchDateTime(string $date): string
131+
{
132+
try
133+
{
134+
$timestamp = strtotime($date);
135+
}
136+
catch (DatetimeException $e)
137+
{
138+
return 'Ongeldige datum en tijd';
139+
}
140+
141+
return sprintf('%s om %s', self::filterDutchDate($date), date('H:i', $timestamp));
142+
}
143+
144+
/**
145+
* @param string $type
146+
* @return string[]
147+
*/
148+
public static function getButtonIconAndClass(string $type): array
149+
{
150+
$icon = $type;
151+
if (array_key_exists($type, self::BUTTON_TYPE_TO_ICON))
152+
{
153+
$icon = self::BUTTON_TYPE_TO_ICON[$type];
154+
}
155+
156+
$btnClass = 'btn-outline-cyndaron';
157+
if (array_key_exists($type, self::BUTTON_TYPE_TO_CLASS))
158+
{
159+
$btnClass = self::BUTTON_TYPE_TO_CLASS[$type];
160+
}
161+
162+
return [$icon, $btnClass];
163+
}
164+
165+
public static function spreadsheetToString(Spreadsheet $spreadsheet): string
166+
{
167+
ob_start();
168+
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
169+
$writer->save('php://output');
170+
return ob_get_clean() ?: '';
171+
}
172+
173+
public static function parseText(string $text): string
174+
{
175+
$text = preg_replace_callback('/%slider\|(\d+)%/', static function($matches)
176+
{
177+
$album = Photoalbum::loadFromDatabase($matches[1]);
178+
if ($album !== null)
179+
{
180+
$page = new PhotoalbumPage($album, 1);
181+
return $page->drawSlider($album);
182+
}
183+
return '';
184+
}, $text);
185+
186+
$text = preg_replace('/%youtube\|([A-Za-z0-9_\-]+)%/', self::YOUTUBE, $text ?? '');
187+
188+
$text = preg_replace_callback('/%csrfToken\|([A-Za-z0-9_\-]+)\|([A-Za-z0-9_\-]+)%/', static function($matches)
189+
{
190+
return User::getCSRFToken($matches[1], $matches[2]);
191+
}, $text);
192+
193+
return $text ?? '';
194+
}
195+
196+
/**
197+
* @param int $numPages
198+
* @param int $currentPage
199+
* @throws ArrayException
200+
* @return int[]
201+
*
202+
* todo: Filter out impossible page numbers
203+
*/
204+
public static function determinePages(int $numPages, int $currentPage): array
205+
{
206+
$pagesToShow = [
207+
1, 2, 3,
208+
$numPages, $numPages - 1, $numPages - 2,
209+
$currentPage - 2, $currentPage - 1, $currentPage, $currentPage + 1, $currentPage + 2,
210+
];
211+
212+
if ($currentPage === 7)
213+
{
214+
$pagesToShow[] = 4;
215+
}
216+
if ($numPages - $currentPage === 6)
217+
{
218+
$pagesToShow[] = $numPages - 3;
219+
}
220+
221+
$pagesToShow = array_unique($pagesToShow);
222+
natsort($pagesToShow);
223+
return $pagesToShow;
224+
}
225+
}

‎src/Template/ViewServiceProvider.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Cyndaron\View\Template;
4+
5+
use Pine\BladeFilters\BladeFilters;
6+
7+
final class ViewServiceProvider extends \Illuminate\View\ViewServiceProvider
8+
{
9+
public const FILTERS = [
10+
'euro' => ViewHelpers::class . '::formatEuro',
11+
'hm' => ViewHelpers::class . '::filterHm',
12+
'dmy' => ViewHelpers::class . '::filterDutchDate',
13+
'dmyHm' => ViewHelpers::class . '::filterDutchDateTime',
14+
'boolToText' => ViewHelpers::class . '::boolToText',
15+
];
16+
17+
public function register(): void
18+
{
19+
parent::register();
20+
21+
foreach (self::FILTERS as $filterName => $function)
22+
{
23+
BladeFilters::macro($filterName, $function);
24+
}
25+
}
26+
27+
/**
28+
* Register the view finder implementation.
29+
*
30+
* @return void
31+
*/
32+
public function registerViewFinder(): void
33+
{
34+
$this->app->bind('view.finder', static function($app)
35+
{
36+
return new ViewFinder($app['files'], $app['config']['view.paths']);
37+
});
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@component('View/Widget/Form/FormWrapper', ['id' => $id, 'label' => $label])
2+
@slot('right')
3+
<input
4+
type="{{ $inputType ?? $type ?? 'text' }}"
5+
class="form-control"
6+
id="{{ $id }}"
7+
name="{{ $id }}"
8+
value="{{ $value ?? '' }}"
9+
@if (isset($placeholder)) placeholder="{{ $placeholder }}" @endif
10+
@if (isset($min)) min="{{ $min }}" @endif
11+
@if (isset($max)) max="{{ $max }}" @endif
12+
@if (isset($step)) step="{{ $step }}" @endif
13+
@if (isset($pattern)) pattern="{{ $pattern }}" @endif
14+
@if (isset($datalist)) list="{{ $datalist }}" @endif
15+
@if (!empty($required)) required @endif>
16+
@endslot
17+
@endcomponent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="form-group form-check">
2+
<input type="checkbox" class="form-check-input" id="{{ $id }}" name="{{ $id }}" @if ($checked ?? false) checked @endif value="1">
3+
<label class="form-check-label" for="{{ $id }}">{{ $description ?? $label }}</label>
4+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="form-group row">
2+
<label for="{{ $id }}" class="col-sm-2 col-form-label">{{ $label }}:</label>
3+
<div class="input-group col-sm-3">
4+
<div class="input-group-prepend">
5+
<span class="input-group-text">€</span>
6+
</div>
7+
<input type="text" class="form-control" id="{{ $id }}" name="{{ $id }}" value="{{ $value }}">
8+
</div>
9+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="form-group row">
2+
<label class="col-sm-2 col-form-label" for="{{ $id }}">{{ $label }}: </label>
3+
<div class="col-sm-5">
4+
<select id="{{ $id }}" name="{{ $id }}" class="form-control custom-select">
5+
@foreach ($options as $value => $description)
6+
<option value="{{ $value }}" @if ($value === $selected) selected @endif>{{ $description }}</option>
7+
@endforeach
8+
</select>
9+
</div>
10+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@include('View/Widget/Form/BasicInput', ['inputType' => 'email', 'id' => $id, 'label' => $label, 'value' => $value])
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@include('View/Widget/Form/BasicInput', ['inputType' => 'file', 'id' => $id, 'label' => $label, 'value' => $value ?? ''])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div class="form-group row">
2+
<label @if (isset($id)) for="{{ $id }}" @endif class="col-md-3 col-form-label">@if (isset($label)){!! $label !!}:@endif</label>
3+
<div class="col-md-6">
4+
{!! $right ?? '' !!}
5+
</div>
6+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@include('View/Widget/Form/BasicInput', ['inputType' => 'text', 'id' => $id, 'label' => $label, 'value' => $value ?? '', 'required' => $required ?? false])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@include('View/Widget/Form/BasicInput', ['inputType' => 'number', 'id' => $id, 'label' => $label, 'value' => $value ?? '', 'min' => $min ?? 0, 'max' => $max ?? '', 'step' => $step ?? 1])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@include('View/Widget/Form/BasicInput', ['inputType' => 'password', 'id' => $id, 'label' => $label, 'value' => $value ?? '', 'required' => $required ?? false])
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@php $required ??= false @endphp
2+
@php $selected ??= 0 @endphp
3+
<div class="form-group row">
4+
<label for="{{ $id }}" class="col-md-3 col-form-label">{{ $label }}:</label>
5+
<div class="col-md-6">
6+
<select id="{{ $id }}" name="{{ $id }}" class="form-control custom-select" @if ($required) required @endif>
7+
@foreach ($options as $key => $description)
8+
<option value="{{ $key }}" @if ($key === $selected) selected @endif>{{ $description }}</option>
9+
@endforeach
10+
</select>
11+
</div>
12+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@component('View/Widget/Form/FormWrapper', ['id' => $id, 'label' => $label])
2+
@slot('right')
3+
<textarea class="form-control" id="{{ $id }}" name="{{ $id }}" rows="4" @if (isset($placeholder)) placeholder="{{ $placeholder }}" @endif>{{ $value ?? '' }}</textarea>
4+
@endslot
5+
@endcomponent

‎src/Widget/templates/Button.blade.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@php
2+
if (!isset($description))
3+
$description = '';
4+
if (!isset($text))
5+
$text = null;
6+
if (!isset($size))
7+
$size = 20;
8+
9+
[$icon, $btnClass] = \Cyndaron\View\Template\ViewHelpers::getButtonIconAndClass($kind);
10+
11+
if ($size === 16)
12+
{
13+
$btnClass .= ' btn-sm';
14+
}
15+
16+
$title = $description ? sprintf('title="%s"', $description) : '';
17+
$textAfterIcon = $text ? " $text" : '';
18+
@endphp
19+
20+
<a class="btn {{ $btnClass }}" href="{{ $link }}" {{ $title }}>
21+
<span class="glyphicon glyphicon-{{ $icon }}"></span>{{ $textAfterIcon }}
22+
</a>
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@if (!empty($showEmpty))
2+
<option value="">&nbsp;</option>
3+
@endif
4+
5+
@for ($i = 1; $i <= 31; $i++)
6+
@php
7+
$i = (strlen($i) === 1) ? $i = '0' . $i : $i;
8+
$sel = ($i === $selectedDay) ? 'selected' : '';
9+
@endphp
10+
<option value="{{ $i }}" {{ $sel }}>{{ $i }}</option>
11+
@endfor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="alert alert-warning">{{ $text }}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<li class="nav-item dropdown">
2+
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
3+
@if (!empty($icon)) <span class="glyphicon glyphicon-{{ $icon }}"></span> @endif {{ $title }}
4+
</a>
5+
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
6+
@foreach ($items as $item)
7+
@if ($item['link'])
8+
<a class="dropdown-item" href="{{ $item['link'] }}">
9+
@if (!empty($item['icon']))<span class="glyphicon glyphicon-{{ $item['icon'] }}"></span>&nbsp; @endif{{ $item['title'] }}
10+
</a>
11+
@else
12+
<span class="dropdown-item">
13+
@if (!empty($item['icon']))<span class="glyphicon glyphicon-{{ $item['icon'] }}"></span>&nbsp; @endif<i>{{ $item['title'] }}</i>
14+
</span>
15+
@endif
16+
@endforeach
17+
</div>
18+
</li>

‎src/Widget/templates/Modal.blade.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<div id="{{ $id }}" class="modal" tabindex="-1" role="dialog">
2+
<div class="modal-dialog {{ $sizeClass ?? '' }}" role="document">
3+
<div class="modal-content">
4+
<div class="modal-header">
5+
<h5 class="modal-title">{{ $title }}</h5>
6+
<button type="button" class="close" data-dismiss="modal" aria-label="Sluiten">
7+
<span aria-hidden="true">&times;</span>
8+
</button>
9+
</div>
10+
<div class="modal-body">
11+
{{ $body }}
12+
</div>
13+
<div class="modal-footer">
14+
{{ $footer ?? '' }}
15+
</div>
16+
</div>
17+
</div>
18+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@if (!empty($showEmpty))
2+
<option value="">&nbsp;</option>
3+
@endif
4+
5+
@for ($i = 1; $i <= 31; $i++)
6+
@php
7+
$i = (strlen($i) === 1) ? $i = '0' . $i : $i;
8+
$sel = ($i === $selectedMonth) ? 'selected' : '';
9+
@endphp
10+
<option value="{{ $i }}" {{ $sel }}>{{ \Cyndaron\View\Template\ViewHelpers::getDutchMonth($i) }}</option>
11+
@endfor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="alert alert-info">{{ $text }}</div>
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<ul class="nav nav-tabs">
2+
@foreach ($subPages as $link => $title)
3+
<li role="presentation" class="nav-item">
4+
<a class="nav-link @if ($link === $currentPage) active @endif" href="{{ $urlPrefix }}{{ rtrim($link, '/') }}">{{ $title }}</a>
5+
</li>
6+
@endforeach
7+
</ul>
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@if ((int)$numPages > 1)
2+
<div class="lettermenu">
3+
<ul class="pagination">
4+
@php $lastPageNum = 0 @endphp
5+
@foreach (\Cyndaron\View\Template\ViewHelpers::determinePages($numPages, $currentPage) as $i)
6+
@if ($i > $numPages)
7+
@break;
8+
@endif
9+
10+
@if ($i < 1)
11+
@continue;
12+
@endif
13+
14+
@if ($lastPageNum !== $i - 1)
15+
<li><span>...</span></li>
16+
@endif
17+
18+
@php $class = $i === $currentPage ? 'class="active"' : '' @endphp
19+
<li {{ $class }}><a href="{{ $link }}{{ $i + $offset }}">{{ $i }}</a></li>
20+
21+
@php $lastPageNum = $i @endphp
22+
@endforeach
23+
</ul>
24+
</div>
25+
@endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="alert alert-success">{{ $text }}</div>
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<nav class="navbar toolbar">
2+
<form class="form-inline">
3+
{{ $left ?? '' }}
4+
</form>
5+
<form class="form-inline">
6+
{{ $middle ?? '' }}
7+
</form>
8+
<form class="form-inline">
9+
{{ $right ?? '' }}
10+
</form>
11+
</nav>
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<nav class="navbar toolbar">
2+
<div>
3+
{{ $left ?? '' }}
4+
</div>
5+
<div>
6+
{{ $middle ?? '' }}
7+
</div>
8+
<div>
9+
{{ $right ?? '' }}
10+
</div>
11+
</nav>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@if (!empty($showEmpty))
2+
<option value="">&nbsp;</option>
3+
@endif
4+
@php $laatsteWaarde = ($selectedYear >= 1900) ? min($selectedYear, date('Y') - 36) : date("Y") - 36; @endphp
5+
6+
@for ($i = date('Y') - 5; $i >= $laatsteWaarde; $i--)
7+
@php $sel = ($i === $selectedYear) ? "selected" : "" @endphp
8+
<option value="{{ $i }}" {{ $sel }}>{{ $i }}</option>
9+
@endfor

0 commit comments

Comments
 (0)
Please sign in to comment.