= $shaarliCache; ?>
+= $shaarliRegenCache; ?>
+ += $nanogalCache; ?>
+= $nanogalRegenCache; ?>
+ += $repoCache; ?>
+= $giteaRegenCache; ?>
+ += $postCache; ?>
+= $postRegenCache; ?>
+commit 6bafa8f0e688bfc8d3faf78a81548af224f0b105
Author: Knah-Tsaeb = $shaarliCache; ?> = $shaarliRegenCache; ?> = $nanogalCache; ?> = $nanogalRegenCache; ?> = $repoCache; ?> = $giteaRegenCache; ?> = $postCache; ?> = $postRegenCache; ?>
+ C'est ma page d'accueil, mon petit coin de web à moi, ça renvoie vers d'autres trucs, n'hésitez pas à fouiller.
+
+ J'ai réellement découvert internet en 1999 avec un Pentium 166mhz et un modem 33k sur un port ISA. J'ai tout de suite adoré ça, même un peu trop au vu de mes factures téléphonique de l'époque :-( .
+ Depuis je n'ai jamais quitté internet.
+
+ Je n'ai qu'un seul pseudonyme (parfois écrit Knah-Tsaeb, quand les sites n'autorisent pas les espaces) et un seul avatar. Donc si vous croisez un Knah Tsaeb quelque part sur le web, il y a de forte chance que ce soit moi.
+
+ Si vous voulez plus d'info, la page /about ou à propos en français est faite pour ça.
+ ';
+ foreach ($this->postList as $key => $value) {
+ $htmlPost .=
+ '
+
';
+
+ $this->postsHash = sha1($htmlPost);
+ return $htmlPost;
+ }
+ $this->postsHash = sha1('empty');
+ return '';
+ }
+
+ /**
+ * Converts markdown content to HTML
+ *
+ * @param string $content The markdown content to convert to HTML
+ * @return string The converted HTML content
+ */
+ function markdownToHtml(string $content): string {
+ $config = [
+ 'disallowed_raw_html' => [
+ 'disallowed_tags' => ['title', 'textarea', 'style', 'xmp', 'iframe', 'noembed', 'noframes', 'script', 'plaintext'],
+ ],
+ 'table' => [
+ 'wrap' => [
+ 'enabled' => false,
+ 'tag' => 'div',
+ 'attributes' => [],
+ ],
+ 'alignment_attributes' => [
+ 'left' => ['align' => 'left'],
+ 'center' => ['align' => 'center'],
+ 'right' => ['align' => 'right'],
+ ],
+ ],
+ 'mentions' => [
+ 'tag_handle' => [
+ 'prefix' => '#',
+ 'pattern' => '[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)',
+ 'generator' => '/tag/%s',
+ ]
+ ],
+ ];
+
+ $environment = new Environment($config);
+ $environment->addExtension(new CommonMarkCoreExtension());
+ $environment->addExtension(new MentionExtension());
+ $environment->addExtension(new TableExtension());
+
+ $converter = new MarkdownConverter($environment);
+ return $converter->convert($content);
+ }
+
+ /**
+ * Extracts the lead paragraph from a post
+ *
+ * @param string $markdownContent The markdown content to extract the lead paragraph from
+ * @return string|null The extracted lead paragraph or null
+ */
+ static function extractLead(string $markdownContent): ?string {
+ $pattern = '/---\s*(.*?)\s*---/s';
+ if (preg_match($pattern, $markdownContent, $matches)) {
+ return trim($matches[1]);
+ }
+ return null;
+ }
+}
diff --git a/app/Cache.php b/app/Cache.php
new file mode 100644
index 0000000..a873b75
--- /dev/null
+++ b/app/Cache.php
@@ -0,0 +1,177 @@
+ __DIR__ . '/../cache/feed',
+ 'page' => __DIR__ . '/../cache/page',
+ 'home' => __DIR__ . '/../cache/page',
+ 'post' => __DIR__ . '/../cache/post',
+ 'posts' => __DIR__ . '/../cache/posts',
+ 'dev' => __DIR__ . '/../cache/page'
+ ];
+ static $fileDir = __DIR__ . '/../datas';
+ static $templateDir = '../template';
+
+ /**
+ * Checks if a cache file exists for the given request URL
+ *
+ * @param string $requestUrl The requested URL to check for a cached version
+ * @param string $cacheDir The directory where the cache is stored, defaults to 'page'
+ * @return bool Returns true if the cache file exists; otherwise, false
+ */
+ static function isCache(array $params): bool {
+ if (file_exists(self::$pathCacheDir[$params['type']] . '/' . $params['cacheName'])) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the page is valid based on the modification times of the markdown and cache files
+ *
+ * @param array $params An associative array
+ * @return bool Returns true if the cached page is still valid; otherwise, false
+ */
+ static function isValidPage(array $params): bool {
+ $mdModifiedTime = 0;
+ // @todo add test if file exist
+ if ($params['type'] === 'home') {
+ $mdModifiedTime = filemtime(self::$templateDir . '/home.php');
+ } elseif ($params['type'] === 'post') {
+ $postList = new Blogs($params);
+ $postAttr = $postList->returnPostInfo($params['slug']);
+ $mdModifiedTime = filemtime($postAttr['file']);
+ } elseif ($params['type'] === 'posts') {
+ $mdModifiedTime = 0;
+ } elseif(file_exists(self::$fileDir . '/' . $params['type'] . 's' . $params['requestUrl'] . '.md')) {
+ $mdModifiedTime = filemtime(self::$fileDir . '/' . $params['type'] . 's' . $params['requestUrl'] . '.md');
+ }
+ $cacheModifiedTime = filemtime(self::$pathCacheDir[$params['type']] . '/' . $params['cacheName']);
+
+ if ($mdModifiedTime > $cacheModifiedTime) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Checks if the feed is valid based on the feed's last modified timestamp
+ * and the cache file's modified time
+ *
+ * @param string $feedName The name of the feed
+ * @param int $feedTimestamp The last modified timestamp of the feed
+ * @return bool
+ */
+ static function isValidFeed(string $feedName, int $feedTimestamp): bool {
+ $feedName = $feedName;
+
+ if (file_exists(self::$pathCacheDir['feed'] . '/' . $feedName . '.html')) {
+ $cacheModifiedTime = filemtime(self::$pathCacheDir['feed'] . '/' . $feedName . '.html');
+ if ($cacheModifiedTime > $feedTimestamp) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Validates the API by checking if the cache file exists and if its hash
+ * matches the provided API hash
+ *
+ * @param string $apiName The name of the API
+ * @param string $apiHash Hash of the API data
+ * @return bool
+ */
+ static function isValidApi(string $apiName, string $apiHash): bool {
+ $apiName = $apiName;
+
+ if (file_exists(self::$pathCacheDir['feed'] . '/' . $apiName . '.html')) {
+ $cacheHashFile = sha1_file(self::$pathCacheDir['feed'] . '/' . $apiName . '.html');
+
+ if ($cacheHashFile === $apiHash) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Validates the API by checking if the cache file exists and if its hash
+ * matches the provided API hash
+ *
+ * @param string $apiName The name of the API
+ * @param string $apiHash Hash of the API data
+ * @return bool
+ */
+ static function isValidConfig(): bool {
+ if (file_exists(__DIR__ . '/../datas/config.json')) {
+ $configLastMod = filemtime(__DIR__ . '/../datas/config.json');
+ if (file_exists(self::$pathCacheDir['home'] . '/home.html') && filemtime(self::$pathCacheDir['home'] . '/home.html') > $configLastMod) {
+ return true;
+ }
+ }
+
+ Cache::clearCache('posts');
+ Cache::clearCache('post');
+ Cache::clearCache('page');
+ Cache::clearCache('feed');
+
+ return false;
+ }
+
+ /**
+ * Retrieves the contents of a cached file for the given request URL
+ *
+ * @param string $cacheName The requested URL to retrieve the cached content for
+ * @param string $cacheDir The directory where the cache is stored'
+ * @return string The contents of the cached file
+ */
+ static function getCache(string $cacheName, string $cacheDir = 'page'): ?string {
+ $cacheFilename = self::$pathCacheDir[$cacheDir] . '/' . $cacheName;
+ if (file_exists($cacheFilename)) {
+ return file_get_contents($cacheFilename);
+ }
+ return null;
+ }
+
+ /**
+ * Stores the given HTML content in a cache file for the specified request URL
+ *
+ * @param string $cacheName The requested URL
+ * @param string $htmlPage The HTML content to be cached
+ * @param string $cacheDir The directory where the cache will be stored
+ * @return void
+ */
+ static function setCache(string $cacheName, string $htmlPage, string $cacheDir = 'page'): void {
+ file_put_contents(self::$pathCacheDir[$cacheDir] . '/' . $cacheName, $htmlPage);
+ }
+
+ /**
+ * Clears all HTML cache files in the specified cache directory
+ *
+ * @return void
+ */
+ static function clearCache($type): void {
+ array_map('unlink', glob(self::$pathCacheDir[$type] . "/*.html"));
+ }
+
+ /**
+ * Deletes the cache file for the specified request URL
+ *
+ * @param string $cacheName The URL for unvalidate cache
+ * @param string $cacheDir The directory where the cache is stored
+ * @return void
+ */
+ static function unValidateCache(string $cacheName, string $cacheDir = 'page'): void {
+ if (file_exists(self::$pathCacheDir[$cacheDir] . '/' . $cacheName)) {
+ unlink(self::$pathCacheDir[$cacheDir] . '/' . $cacheName);
+ }
+ }
+}
diff --git a/app/Controllers/Dev.php b/app/Controllers/Dev.php
new file mode 100644
index 0000000..4717172
--- /dev/null
+++ b/app/Controllers/Dev.php
@@ -0,0 +1,93 @@
+ Config::loadConfig()];
+
+ $params['config']['cron'] = true;
+ //Debug::n_print($params);
+
+ //Cache::clearCache('feed');
+ //Cache::clearCache('page');
+ //Cache::clearCache('post');
+ Cache::clearCache('posts');
+
+ $shaarli = new Shaarli($params);
+ if (Cache::isValidFeed('shaarli', $shaarli->shaarliFeedUpdate)) {
+ $shaarliCache = Cache::getCache('shaarli', 'feed');
+ $shaarliRegenCache = 'Shaarli : cache';
+ } else {
+ Cache::unValidateCache('shaarli', 'feed');
+ Cache::unValidateCache('/', 'page');
+ $shaarli->getLastBookmark();
+ $shaarliCache = $shaarli->makeList();
+ Cache::setCache('shaarli', $shaarliCache, 'feed');
+ $shaarliRegenCache = 'Shaarli : regen';
+ }
+
+ $nanogal = new NanoGal($params);
+ if (Cache::isValidFeed('nanogal', $nanogal->nanogalFeedUpdate)) {
+ $nanogalCache = Cache::getCache('nanogal', 'feed');
+ $nanogalRegenCache = 'Nanogal : cache';
+ } else {
+ Cache::unValidateCache('nanogal', 'feed');
+ Cache::unValidateCache('/', 'page');
+ $nanogal->getLastBookmark();
+ $nanogalCache = $nanogal->makeList();
+ Cache::setCache('nanogal', $nanogalCache, 'feed');
+ $nanogalRegenCache = 'Nanogal : regen';
+ }
+
+ $gitea = new Gitea($params);
+ $gitea->getLastRepo();
+ $repoCache = $gitea->makeList();
+
+ if (Cache::isValidApi('gitea', $gitea->giteaHash)) {
+ $repoCache = Cache::getCache('gitea', 'feed');
+ $giteaRegenCache = 'Gitea : cache';
+ } else {
+ Cache::unValidateCache('gitea', 'feed');
+ Cache::unValidateCache('/', 'page');
+ Cache::setCache('gitea', $repoCache, 'feed');
+ $giteaRegenCache = 'Gitea : regen';
+ }
+
+ $post = new Blogs($params);
+
+ $post->getLastPost();
+ $postCache = $post->makeList();
+
+ if (Cache::isValidApi('posts', $post->postsHash)) {
+ $postCache = Cache::getCache('posts', 'feed');
+ $postRegenCache = 'Post : cache';
+ } else {
+ Cache::unValidateCache('posts', 'feed');
+ Cache::unValidateCache('/', 'page');
+ Cache::setCache('posts', $postCache, 'feed');
+ $postRegenCache = 'Post : regen';
+ }
+
+ require __DIR__ . '/../../template/dev.php';
+ $content = ob_get_contents();
+ ob_end_clean();
+ return $content;
+ }
+
+ function title() {
+ return $this->title;
+ }
+}
diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php
new file mode 100644
index 0000000..340fc2b
--- /dev/null
+++ b/app/Controllers/Home.php
@@ -0,0 +1,85 @@
+getLastBookmark();
+ $shaarliCache = $shaarli->makeList();
+ if ($params['config']['useCache'] === true) {
+ Cache::setCache('shaarli.html', $shaarliCache, 'feed');
+ }
+ $lastShaare = $shaarliCache;
+ }
+
+ $lastAppsUpdates = Cache::getCache('gitea.html', 'feed');
+ if ($lastAppsUpdates === null) {
+ $gitea = new Gitea($params);
+ $gitea->getLastRepo();
+ $repoCache = $gitea->makeList();
+ Cache::unValidateCache('gitea.html', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ if ($params['config']['useCache'] === true) {
+ Cache::setCache('gitea.html', $repoCache, 'feed');
+ }
+ $lastAppsUpdates = $repoCache;
+ }
+
+ $lastPost = Cache::getCache('posts.html', 'feed');
+ if ($lastPost === null) {
+ $post = new Blogs($params);
+ $post->getLastPost();
+ $postCache = $post->makeList();
+ Cache::unValidateCache('posts.html', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ if ($params['config']['useCache'] === true) {
+ Cache::setCache('posts.html', $postCache, 'feed');
+ }
+ $lastPost = $postCache;
+ }
+
+ $lastPics = Cache::getCache('nanogal.html', 'feed');
+ if ($lastPics === null) {
+ $nanogal = new NanoGal($params);
+ Cache::unValidateCache('nanogal.html', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ $nanogal->getLastBookmark();
+ $nanogalCache = $nanogal->makeList();
+ if ($params['config']['useCache'] === true) {
+ Cache::setCache('nanogal.html', $nanogalCache, 'feed');
+ }
+ $lastPics = $nanogalCache;
+ }
+
+ require __DIR__ . '/../../template/home.php';
+ $content = ob_get_contents();
+ ob_end_clean();
+ return $content;
+ }
+
+ function title() {
+ return $this->title;
+ }
+}
diff --git a/app/Controllers/Page.php b/app/Controllers/Page.php
new file mode 100644
index 0000000..d0685a0
--- /dev/null
+++ b/app/Controllers/Page.php
@@ -0,0 +1,47 @@
+title = $page = str_replace('/', '', $params['requestUrl']);
+ $author = $params['config']['author'];
+
+ if (file_exists('../datas/pages/' . $page . '.md')) {
+ $pageContent = file_get_contents('../datas/pages/' . $page . '.md');
+
+ $modifiedDate = \DateTime::createFromFormat("U", filemtime('../datas/pages/' . $page . '.md'));
+ $lastMod = $modifiedDate->format(\DateTime::ATOM);
+ } else {
+ $pageContent = file_get_contents('../datas/pages/404.md');
+ }
+
+ $pageContent = RenderHtml::markdownToHtml($pageContent);
+
+ require __DIR__ . '/../../template/page.php';
+
+ $content = ob_get_contents();
+ ob_end_clean();
+ return $content;
+ }
+
+ function title() {
+ return ucfirst($this->title);
+ }
+
+
+}
diff --git a/app/Controllers/Post.php b/app/Controllers/Post.php
new file mode 100644
index 0000000..23db9c4
--- /dev/null
+++ b/app/Controllers/Post.php
@@ -0,0 +1,38 @@
+returnPostInfo($params['slug']);
+ $postContent = $posts->findPostBySlug($params['slug']);
+ $postContent = RenderHtml::markdownToHtml($postContent);
+ $this->title = $postInfo['title'];
+ $author = $params['config']['author'];
+
+ require __DIR__ . '/../../template/post.php';
+ $content = ob_get_contents();
+ ob_end_clean();
+ return $content;
+ }
+
+ function title() {
+ return $this->title;
+ }
+}
diff --git a/app/Controllers/Posts.php b/app/Controllers/Posts.php
new file mode 100644
index 0000000..f3395e0
--- /dev/null
+++ b/app/Controllers/Posts.php
@@ -0,0 +1,51 @@
+page = $params['page'];
+
+ $posts = new Blogs($params);
+
+ $list = $posts->makeList();
+
+ if (!empty($params['extraParams'])) {
+ if (isset($params['extraParams']['year']) && (int)$params['extraParams']['year']) {
+ $posts->findPostByYear($params['extraParams']['year']);
+ }
+ }
+ $getPostList = $posts->getFilesForPage($params['page']);
+ $pagination = $posts->makePagination($params['page']);
+
+ require __DIR__ . '/../../template/posts.php';
+ $content = ob_get_contents();
+ ob_end_clean();
+ return $content;
+ }
+
+ function title() {
+ return $this->title;
+ }
+}
diff --git a/app/Fetching/ApiFeed.php b/app/Fetching/ApiFeed.php
new file mode 100644
index 0000000..eb53e92
--- /dev/null
+++ b/app/Fetching/ApiFeed.php
@@ -0,0 +1,26 @@
+request('GET', $url);
+ $this->json = json_decode($response->getBody(), true);
+ return $this;
+ } catch (\Exception $e) {
+ echo 'Catch in : ', $e->getMessage(), "\n";
+ }
+ }
+}
diff --git a/app/Fetching/Gitea.php b/app/Fetching/Gitea.php
new file mode 100644
index 0000000..0288a3b
--- /dev/null
+++ b/app/Fetching/Gitea.php
@@ -0,0 +1,77 @@
+giteaFeed = $feed->load($params['config']['fetching']['Gitea']);
+ $this->numberEntry = $params['config']['numberOfLastItem'];
+
+ return $this;
+ }
+
+ /**
+ * Retrieves a list of the most recently updated repositories that are not archived
+ *
+ * @return array List of repositories with their name, URL, and last update timestamp
+ */
+ public function getLastRepo(): array {
+ $repoList = [];
+ if (count($this->giteaFeed->json) > 0) {
+ foreach ($this->giteaFeed->json as $value) {
+ if ($value['archived'] === false) {
+ $timestamp = new \DateTimeImmutable($value['updated_at']);
+ $timestamp = (int)$timestamp->format("U");
+ $repoList[$timestamp] = [
+ 'name' => $value['name'],
+ 'url' => $value['html_url'],
+ 'update_at' => $value['updated_at']
+ ];
+ }
+ }
+ krsort($repoList);
+ $repoList = array_slice($repoList, 0, $this->numberEntry);
+ $this->repoList = $repoList;
+ }
+ return $repoList;
+ }
+
+ /**
+ * Generates an HTML list of repositories from the repo list
+ *
+ * @return string|null The HTML string of the repository list or null if the repo list is empty
+ */
+ public function makeList(): ?string {
+ if (!empty($this->repoList)) {
+ $htmlRepo = '';
+ foreach ($this->repoList as $value) {
+ $htmlRepo .=
+ '
+
';
+ $this->giteaHash = sha1($htmlRepo);
+
+ return $htmlRepo;
+ }
+ return null;
+ }
+}
diff --git a/app/Fetching/NanoGal.php b/app/Fetching/NanoGal.php
new file mode 100644
index 0000000..6287be5
--- /dev/null
+++ b/app/Fetching/NanoGal.php
@@ -0,0 +1,76 @@
+nanogalFeed = $feed->load($params['config']['fetching']['NanoGal']);
+ $this->nanogalFeedUpdate = $feed->lastUpdate();
+ $this->numberEntry = $params['config']['numberOfLastItem'];
+
+ return $this;
+ }
+
+ /**
+ * Retrieves the latest images from the NanoGal feed
+ *
+ * @return array Returns an array of bookmarks
+ */
+ public function getLastBookmark(): array {
+ $totalEntry = count($this->nanogalFeed->xml->entry);
+
+ if ($totalEntry <= $this->numberEntry) {
+ $needItem = $totalEntry;
+ } else {
+ $needItem = $this->numberEntry;
+ }
+
+ for ($i = 0; $i < $needItem; $i++) {
+ $bookmarkList[] = [
+ 'title' => (string)$this->nanogalFeed->xml->entry[$i]->title[0],
+ 'content' => (string)$this->nanogalFeed->xml->entry[$i]->content,
+ ];
+ }
+ $this->bookmarkList = $bookmarkList;
+ return $bookmarkList;
+ }
+
+ /**
+ * Generates an HTML list of bookmarks from the bookmark list
+ *
+ * @return string|null Returns the generated HTML string of bookmarks
+ */
+ public function makeList(): ?string {
+ if (!empty($this->bookmarkList)) {
+
+ $htmlBookmark = '';
+ foreach ($this->bookmarkList as $value) {
+ $htmlBookmark .='
+ ';
+ }
+ $htmlBookmark .= '
+ ';
+ return $htmlBookmark;
+ }
+ return null;
+ }
+}
diff --git a/app/Fetching/RssFeed.php b/app/Fetching/RssFeed.php
new file mode 100644
index 0000000..ffa8cb8
--- /dev/null
+++ b/app/Fetching/RssFeed.php
@@ -0,0 +1,47 @@
+ array(
+ 'verify_peer' => false,
+ 'verify_peer_name' => false
+ )));
+
+ libxml_set_streams_context($context);*/
+
+ $xml = simplexml_load_file($url, 'SimpleXMLElement', LIBXML_NOCDATA);
+ $this->xml = $xml;
+ return $this;
+ } catch (\Exception $e) {
+ echo 'Catch in : ', $e->getMessage(), "\n";
+ }
+ }
+
+ /**
+ * Retrieves the last update time from feed
+ *
+ * @return string The timestamp of the last update
+ */
+ public function lastUpdate(): string {
+ $feedUpdateTime = new DateTimeImmutable((string)$this->xml->updated);
+ return $feedUpdateTime->format("U");
+ }
+}
diff --git a/app/Fetching/Shaarli.php b/app/Fetching/Shaarli.php
new file mode 100644
index 0000000..37e08fb
--- /dev/null
+++ b/app/Fetching/Shaarli.php
@@ -0,0 +1,76 @@
+shaarliFeed = $feed->load($params['config']['fetching']['Shaarli']);
+ $this->shaarliFeedUpdate = $feed->lastUpdate();
+ $this->numberEntry = $params['config']['numberOfLastItem'];
+
+ return $this;
+ }
+
+ /**
+ * Retrieves the latest images from the Shaarli feed
+ *
+ * @return array $bookmakList Array of last bookmark
+ */
+ public function getLastBookmark(): array {
+ $totalEntry = count($this->shaarliFeed->xml->entry);
+
+ if ($totalEntry <= $this->numberEntry) {
+ $needItem = $totalEntry;
+ } else {
+ $needItem = $this->numberEntry;
+ }
+
+ for ($i = 0; $i < $needItem; $i++) {
+ $bookmarkList[] = [
+ 'title' => (string)$this->shaarliFeed->xml->entry[$i]->title[0],
+ 'link' => (string)$this->shaarliFeed->xml->entry[$i]->link[0]->attributes()[0],
+ 'permalink' => (string)$this->shaarliFeed->xml->entry[$i]->id[0],
+ ];
+ }
+ $this->bookmarkList = $bookmarkList;
+ return $bookmarkList;
+ }
+
+ /**
+ * Generates an HTML list of images from the images list
+ *
+ * @return string|null Returns the generated HTML string of images
+ */
+ public function makeList(): ?string {
+ if (!empty($this->bookmarkList)) {
+ $htmlBookmark = '';
+ foreach ($this->bookmarkList as $value) {
+ $htmlBookmark .=
+ '
+
';
+ return $htmlBookmark;
+ }
+ return null;
+ }
+}
diff --git a/app/Router.php b/app/Router.php
new file mode 100644
index 0000000..94d3964
--- /dev/null
+++ b/app/Router.php
@@ -0,0 +1,96 @@
+[a-zA-Z0-9_-]+)', $url);
+ $this->routes[] = array($method, '#^' . $url . '$#', $target);
+ }
+
+ /**
+ * Matches the current request against defined routes and returns the target and parameters.
+ *
+ * @param array $config The configuration array
+ * @return array|false Returns an array containing the target and parameters
+ */
+ public function match(array $config): array|false {
+ $requestMethod = $_SERVER['REQUEST_METHOD'];
+ $this->requestUrl = $requestUrl = strtok($_SERVER['REQUEST_URI'], '?');
+
+ parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $extraParams);
+
+ foreach ($this->routes as $route) {
+ list($method, $url, $target) = $route;
+
+ if ($requestMethod === $method) {
+ if (preg_match($url, $requestUrl, $params)) {
+ $slug = new Slugify();
+ if ($requestUrl === '/') {
+ $requestUrl = 'home';
+ }
+ $params = array_filter($params, 'is_string', ARRAY_FILTER_USE_KEY);
+ $params['requestUrl'] = $requestUrl;
+ $params['cacheName'] = $slug->slugify($requestUrl . '_' . implode('-', $extraParams)) . '.html';
+ $params['type'] = $this->getType($target);
+ $params['config'] = $config;
+ $params['extraParams'] = $extraParams;
+ $this->match['requestUrl'] = $requestUrl;
+ $this->match = array('target' => $target, 'params' => $params);
+ return $this->match;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieves the type of the target by extracting and normalizing its name
+ *
+ * @param string $target The fully qualified name of the target
+ * @return string The lowercase name of the target without its namespace
+ */
+ private function getType(string $target): string {
+ return strtolower(basename(str_replace('\\', '/', $target)));
+ }
+
+ /**
+ * Renders the content by invoking the matched target's index method
+ *
+ * @return string The rendered content as a string
+ */
+ public function render(): string {
+ $target = $this->match['target'];
+ $params = [$this->match['params']];
+ $author = $params[0]['config']['author'];
+ $siteName = $params[0]['config']['siteName'];
+ $navLinks = $params[0]['config']['navLinks'];
+ $className = '\\' . $target;
+ $controller_instance = new $className();
+ $content = call_user_func_array(array($controller_instance, 'index'), $params);
+ $title = $controller_instance->title();
+ ob_start();
+ require_once __DIR__ . '/../template/bone.php';
+
+ $contentFinal = ob_get_contents();
+ ob_end_clean();
+ return $contentFinal;
+ }
+}
diff --git a/app/Utils/Config.php b/app/Utils/Config.php
new file mode 100644
index 0000000..1b5f2ec
--- /dev/null
+++ b/app/Utils/Config.php
@@ -0,0 +1,28 @@
+ 'My super page',
+ 'author' => 'Me !!!',
+ 'useCache' => false,
+ 'postPerPage' => 5,
+ 'numberOfLastItem' => 5,
+ 'debug' => false,
+ 'fetching' => [],
+ 'navLinks' => []
+ ];
+
+ /**
+ * Loads the configuration from file
+ *
+ * @return array The configuration data
+ */
+ static function loadConfig(): array {
+ if (file_exists(__DIR__ . "/../../datas/config.json")) {
+ self::$config = json_decode(file_get_contents(__DIR__ . '/../../datas/config.json'), true);
+ }
+ return self::$config;
+ }
+}
\ No newline at end of file
diff --git a/app/Utils/Debug.php b/app/Utils/Debug.php
new file mode 100644
index 0000000..53eefe5
--- /dev/null
+++ b/app/Utils/Debug.php
@@ -0,0 +1,23 @@
+', $name, '';
+ echo '
';
+ }
+}
diff --git a/app/Utils/RenderHtml.php b/app/Utils/RenderHtml.php
new file mode 100644
index 0000000..6f4c67d
--- /dev/null
+++ b/app/Utils/RenderHtml.php
@@ -0,0 +1,92 @@
+ [
+ 'disallowed_tags' => [
+ 'title',
+ 'textarea',
+ 'style',
+ 'xmp',
+ 'iframe',
+ 'noembed',
+ 'noframes',
+ 'script',
+ 'plaintext'
+ ],
+ ],
+ 'table_of_contents' => [
+ 'html_class' => 'table-of-contents',
+ 'min_heading_level' => 3,
+ 'max_heading_level' => 6,
+ 'normalize' => 'relative',
+ 'placeholder' => null,
+ ],
+ 'heading_permalink' => [
+ 'html_class' => '',
+ 'apply_id_to_heading' => false,
+ 'min_heading_level' => 3,
+ 'max_heading_level' => 6,
+ 'symbol' => '',
+ ],
+ 'table' => [
+ 'wrap' => [
+ 'enabled' => false,
+ 'tag' => 'div',
+ 'attributes' => [],
+ ],
+ 'alignment_attributes' => [
+ 'left' => ['align' => 'left'],
+ 'center' => ['align' => 'center'],
+ 'right' => ['align' => 'right'],
+ ],
+ ],
+ 'mentions' => [
+ 'tag_handle' => [
+ 'prefix' => '#',
+ 'pattern' => '[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)',
+ 'generator' => '/tag/%s',
+ ]
+ ],
+ 'external_link' => [
+ 'internal_hosts' => 'site.knah-tsaeb.local', // TODO: Don't forget to set this!
+ 'open_in_new_window' => true,
+ 'html_class' => 'external-link',
+ 'nofollow' => '',
+ 'noopener' => 'external',
+ 'noreferrer' => 'external',
+ ],
+ ];
+
+ $environment = new Environment($config);
+ $environment->addExtension(new CommonMarkCoreExtension());
+ $environment->addExtension(new MentionExtension());
+ $environment->addExtension(new TableExtension());
+ $environment->addExtension(new TableOfContentsExtension());
+ $environment->addExtension(new ExternalLinkExtension());
+ $environment->addExtension(new HeadingPermalinkExtension());
+ $environment->addExtension(new DisallowedRawHtmlExtension());
+
+ $converter = new MarkdownConverter($environment);
+ return $converter->convert($content);
+ }
+}
diff --git a/app/Utils/Selected.php b/app/Utils/Selected.php
new file mode 100644
index 0000000..5eaae74
--- /dev/null
+++ b/app/Utils/Selected.php
@@ -0,0 +1,16 @@
+ Config::loadConfig()];
+
+$params['config']['cron'] = true;
+
+$shaarli = new Shaarli($params);
+if (Cache::isValidFeed('shaarli', $shaarli->shaarliFeedUpdate)) {
+ $shaarliCache = Cache::getCache('shaarli.html', 'feed');
+ $shaarliRegenCache = 'Shaarli : cache';
+} else {
+ Cache::unValidateCache('shaarli.html', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ $shaarli->getLastBookmark();
+ $shaarliCache = $shaarli->makeList();
+ Cache::setCache('shaarli.html', $shaarliCache, 'feed');
+ $shaarliRegenCache = 'Shaarli : regen';
+}
+
+$nanogal = new NanoGal($params);
+if (Cache::isValidFeed('nanogal', $nanogal->nanogalFeedUpdate)) {
+ $nanogalCache = Cache::getCache('nanogal.html', 'feed');
+ $nanogalRegenCache = 'Nanogal : cache';
+} else {
+ Cache::unValidateCache('nanogal.html', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ $nanogal->getLastBookmark();
+ $nanogalCache = $nanogal->makeList();
+ Cache::setCache('nanogal.html', $nanogalCache, 'feed');
+ $nanogalRegenCache = 'Nanogal : regen';
+}
+
+$gitea = new Gitea($params);
+$gitea->getLastRepo();
+$repoCache = $gitea->makeList();
+
+if (Cache::isValidApi('gitea', $gitea->giteaHash)) {
+ $repoCache = Cache::getCache('gitea.html', 'feed');
+ $giteaRegenCache = 'Gitea : cache';
+} else {
+ Cache::unValidateCache('gitea.html', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ Cache::setCache('gitea.html', $repoCache, 'feed');
+ $giteaRegenCache = 'Gitea : regen';
+}
+
+$post = new Blogs($params);
+$post->getLastPost();
+$postCache = $post->makeList();
+
+if (Cache::isValidApi('posts', $post->postsHash)) {
+ $postCache = Cache::getCache('posts.html', 'feed');
+ $postRegenCache = 'Post : cache';
+} else {
+ Cache::unValidateCache('posts', 'feed');
+ Cache::unValidateCache('home.html', 'page');
+ Cache::clearCache('posts');
+ Cache::setCache('posts.html', $postCache, 'feed');
+ $postRegenCache = 'Post : regen';
+}
+
+$client = new \GuzzleHttp\Client();
+$response = $client->request('GET', 'http://site.knah-tsaeb.local/');
+echo "\n".$response->getStatusCode()."\n";
\ No newline at end of file
diff --git a/cache/feed/.gitkeep b/cache/feed/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/cache/page/.gitkeep b/cache/page/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/cache/post/.gitkeep b/cache/post/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/cache/posts/.gitkeep b/cache/posts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..d3ace4b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,13 @@
+{
+ "require": {
+ "league/commonmark": "^2.5",
+ "guzzlehttp/guzzle": "^7.9",
+ "cocur/slugify": "^4.6"
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\" : "app/"
+ }
+ }
+
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..b8eb6a3
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1230 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "bcb10088b537a6ab55328a81fb06662f",
+ "packages": [
+ {
+ "name": "cocur/slugify",
+ "version": "v4.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cocur/slugify.git",
+ "reference": "1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cocur/slugify/zipball/1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb",
+ "reference": "1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "conflict": {
+ "symfony/config": "<3.4 || >=4,<4.3",
+ "symfony/dependency-injection": "<3.4 || >=4,<4.3",
+ "symfony/http-kernel": "<3.4 || >=4,<4.3",
+ "twig/twig": "<2.12.1"
+ },
+ "require-dev": {
+ "laravel/framework": "^5.0|^6.0|^7.0|^8.0",
+ "latte/latte": "~2.2",
+ "league/container": "^2.2.0",
+ "mikey179/vfsstream": "~1.6.8",
+ "mockery/mockery": "^1.3",
+ "nette/di": "~2.4",
+ "pimple/pimple": "~1.1",
+ "plumphp/plum": "~0.1",
+ "symfony/config": "^3.4 || ^4.3 || ^5.0 || ^6.0",
+ "symfony/dependency-injection": "^3.4 || ^4.3 || ^5.0 || ^6.0",
+ "symfony/http-kernel": "^3.4 || ^4.3 || ^5.0 || ^6.0",
+ "symfony/phpunit-bridge": "^5.4 || ^6.0",
+ "twig/twig": "^2.12.1 || ~3.0",
+ "zendframework/zend-modulemanager": "~2.2",
+ "zendframework/zend-servicemanager": "~2.2",
+ "zendframework/zend-view": "~2.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Cocur\\Slugify\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Florian Eckerstorfer",
+ "email": "florian@eckerstorfer.co",
+ "homepage": "https://florian.ec"
+ },
+ {
+ "name": "Ivo Bathke",
+ "email": "ivo.bathke@gmail.com"
+ }
+ ],
+ "description": "Converts a string into a slug.",
+ "keywords": [
+ "slug",
+ "slugify"
+ ],
+ "support": {
+ "issues": "https://github.com/cocur/slugify/issues",
+ "source": "https://github.com/cocur/slugify/tree/v4.6.0"
+ },
+ "time": "2024-09-10T14:09:25+00:00"
+ },
+ {
+ "name": "dflydev/dot-access-data",
+ "version": "v3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dflydev/dflydev-dot-access-data.git",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.42",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
+ "scrutinizer/ocular": "1.6.0",
+ "squizlabs/php_codesniffer": "^3.5",
+ "vimeo/psalm": "^4.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Dflydev\\DotAccessData\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dragonfly Development Inc.",
+ "email": "info@dflydev.com",
+ "homepage": "http://dflydev.com"
+ },
+ {
+ "name": "Beau Simensen",
+ "email": "beau@dflydev.com",
+ "homepage": "http://beausimensen.com"
+ },
+ {
+ "name": "Carlos Frutos",
+ "email": "carlos@kiwing.it",
+ "homepage": "https://github.com/cfrutos"
+ },
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com"
+ }
+ ],
+ "description": "Given a deep data structure, access data by dot notation.",
+ "homepage": "https://github.com/dflydev/dflydev-dot-access-data",
+ "keywords": [
+ "access",
+ "data",
+ "dot",
+ "notation"
+ ],
+ "support": {
+ "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
+ "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
+ },
+ "time": "2024-07-08T12:26:09+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+ "guzzlehttp/psr7": "^2.7.0",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.9.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-24T11:22:20+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8",
+ "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-18T10:29:17+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
+ "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-18T11:15:46+00:00"
+ },
+ {
+ "name": "league/commonmark",
+ "version": "2.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/commonmark.git",
+ "reference": "b650144166dfa7703e62a22e493b853b58d874b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0",
+ "reference": "b650144166dfa7703e62a22e493b853b58d874b0",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "league/config": "^1.1.1",
+ "php": "^7.4 || ^8.0",
+ "psr/event-dispatcher": "^1.0",
+ "symfony/deprecation-contracts": "^2.1 || ^3.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "cebe/markdown": "^1.0",
+ "commonmark/cmark": "0.31.1",
+ "commonmark/commonmark.js": "0.31.1",
+ "composer/package-versions-deprecated": "^1.8",
+ "embed/embed": "^4.4",
+ "erusev/parsedown": "^1.0",
+ "ext-json": "*",
+ "github/gfm": "0.29.0",
+ "michelf/php-markdown": "^1.4 || ^2.0",
+ "nyholm/psr7": "^1.5",
+ "phpstan/phpstan": "^1.8.2",
+ "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
+ "scrutinizer/ocular": "^1.8.1",
+ "symfony/finder": "^5.3 | ^6.0 || ^7.0",
+ "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0",
+ "unleashedtech/php-coding-standard": "^3.1.1",
+ "vimeo/psalm": "^4.24.0 || ^5.0.0"
+ },
+ "suggest": {
+ "symfony/yaml": "v2.3+ required if using the Front Matter extension"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\CommonMark\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
+ "homepage": "https://commonmark.thephpleague.com",
+ "keywords": [
+ "commonmark",
+ "flavored",
+ "gfm",
+ "github",
+ "github-flavored",
+ "markdown",
+ "md",
+ "parser"
+ ],
+ "support": {
+ "docs": "https://commonmark.thephpleague.com/",
+ "forum": "https://github.com/thephpleague/commonmark/discussions",
+ "issues": "https://github.com/thephpleague/commonmark/issues",
+ "rss": "https://github.com/thephpleague/commonmark/releases.atom",
+ "source": "https://github.com/thephpleague/commonmark"
+ },
+ "funding": [
+ {
+ "url": "https://www.colinodell.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.paypal.me/colinpodell/10.00",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/colinodell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/commonmark",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-08-16T11:46:16+00:00"
+ },
+ {
+ "name": "league/config",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/config.git",
+ "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
+ "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
+ "shasum": ""
+ },
+ "require": {
+ "dflydev/dot-access-data": "^3.0.1",
+ "nette/schema": "^1.2",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.8.2",
+ "phpunit/phpunit": "^9.5.5",
+ "scrutinizer/ocular": "^1.8.1",
+ "unleashedtech/php-coding-standard": "^3.1",
+ "vimeo/psalm": "^4.7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Config\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Define configuration arrays with strict schemas and access values with dot notation",
+ "homepage": "https://config.thephpleague.com",
+ "keywords": [
+ "array",
+ "config",
+ "configuration",
+ "dot",
+ "dot-access",
+ "nested",
+ "schema"
+ ],
+ "support": {
+ "docs": "https://config.thephpleague.com/",
+ "issues": "https://github.com/thephpleague/config/issues",
+ "rss": "https://github.com/thephpleague/config/releases.atom",
+ "source": "https://github.com/thephpleague/config"
+ },
+ "funding": [
+ {
+ "url": "https://www.colinodell.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.paypal.me/colinpodell/10.00",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/colinodell",
+ "type": "github"
+ }
+ ],
+ "time": "2022-12-11T20:36:23+00:00"
+ },
+ {
+ "name": "nette/schema",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nette/schema.git",
+ "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
+ "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
+ "shasum": ""
+ },
+ "require": {
+ "nette/utils": "^4.0",
+ "php": "8.1 - 8.3"
+ },
+ "require-dev": {
+ "nette/tester": "^2.4",
+ "phpstan/phpstan-nette": "^1.0",
+ "tracy/tracy": "^2.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause",
+ "GPL-2.0-only",
+ "GPL-3.0-only"
+ ],
+ "authors": [
+ {
+ "name": "David Grudl",
+ "homepage": "https://davidgrudl.com"
+ },
+ {
+ "name": "Nette Community",
+ "homepage": "https://nette.org/contributors"
+ }
+ ],
+ "description": "📐 Nette Schema: validating data structures against a given Schema.",
+ "homepage": "https://nette.org",
+ "keywords": [
+ "config",
+ "nette"
+ ],
+ "support": {
+ "issues": "https://github.com/nette/schema/issues",
+ "source": "https://github.com/nette/schema/tree/v1.3.0"
+ },
+ "time": "2023-12-11T11:54:22+00:00"
+ },
+ {
+ "name": "nette/utils",
+ "version": "v4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nette/utils.git",
+ "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
+ "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
+ "shasum": ""
+ },
+ "require": {
+ "php": "8.0 - 8.4"
+ },
+ "conflict": {
+ "nette/finder": "<3",
+ "nette/schema": "<1.2.2"
+ },
+ "require-dev": {
+ "jetbrains/phpstorm-attributes": "dev-master",
+ "nette/tester": "^2.5",
+ "phpstan/phpstan": "^1.0",
+ "tracy/tracy": "^2.9"
+ },
+ "suggest": {
+ "ext-gd": "to use Image",
+ "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
+ "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
+ "ext-json": "to use Nette\\Utils\\Json",
+ "ext-mbstring": "to use Strings::lower() etc...",
+ "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause",
+ "GPL-2.0-only",
+ "GPL-3.0-only"
+ ],
+ "authors": [
+ {
+ "name": "David Grudl",
+ "homepage": "https://davidgrudl.com"
+ },
+ {
+ "name": "Nette Community",
+ "homepage": "https://nette.org/contributors"
+ }
+ ],
+ "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
+ "homepage": "https://nette.org",
+ "keywords": [
+ "array",
+ "core",
+ "datetime",
+ "images",
+ "json",
+ "nette",
+ "paginator",
+ "password",
+ "slugify",
+ "string",
+ "unicode",
+ "utf-8",
+ "utility",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/nette/utils/issues",
+ "source": "https://github.com/nette/utils/tree/v4.0.5"
+ },
+ "time": "2024-08-07T15:39:19+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+ "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-04-18T09:32:20+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.6.0"
+}
diff --git a/datas/drafts/.gitkeep b/datas/drafts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/datas/pages/.gitkeep b/datas/pages/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/datas/posts/.gitkeep b/datas/posts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..b9c18cf
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,10 @@
+RewriteEngine on
+
+Redirect 301 /bookmarks https://book.knah-tsaeb.org
+Redirect 301 /gallery https://img.knah-tsaeb.org
+
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+
+RewriteRule ^(.*)$ index.php?id=$1 [QSA,L]
\ No newline at end of file
diff --git a/public/assets/css/app-min.css b/public/assets/css/app-min.css
new file mode 100644
index 0000000..ada76eb
--- /dev/null
+++ b/public/assets/css/app-min.css
@@ -0,0 +1 @@
+:root{color-scheme:dark light;--primary:#cc2027;--primary-darken:#8E161B;--primary-lighten:#D64C52;--primary-text-contrast:#FFF;--secondary:#20ccc5;--secondary-darken:#168E89;--secondary-lighten:#4CD6D0;--secondary-text-contrast:#000;--error:#c43933;--error-darken:#892723;--error-lighten:#CF605B;--error-text-contrast:#FFF;--info:#206ccc;--info-darken:#164B8E;--info-lighten:#4C89D6;--info-text-contrast:#FFF;--success:#7dcc20;--success-darken:#578E16;--success-lighten:#97D64C;--success-text-contrast:#000;--warning:#cc5e20;--warning-darken:#8E4116;--warning-lighten:#D67E4C;--warning-text-contrast:#FFF;--background-color:light-dark(#fffbfb,#171414);--background-color-darken:light-dark(#B2AFAF,#100E0E);--background-color-lighten:light-dark(#FFFBFB,#454343);--light-background-color:#fffbfb;--dark-background-color:#171414;--header-background-color:light-dark(#171414,#fffbfb);--header-background-color-darken:light-dark(#100E0E,#B2AFAF);--header-background-color-lighten:light-dark(#454343,#FFFBFB);--header-text-color:light-dark(#fffbfb,#171414);--header-text-color-secondary:#ffffffb3;--header-text-color-disable:light-dark(#ffffff80,#454343);--text-color:light-dark(#171414,#fffbfb);--text-color-secondary:#ffffffb3;--text-color-disable:light-dark(#454343,#ffffff80);--light-text-color:#171414;--dark-text-color:#fffbfb;--text-color-inverse:light-dark(#fffbfb,#171414);--text-color-secondary-inverse:#ffffffb3;--text-color-disable-inverse:light-dark(#ffffff80,#454343);--box-shadow-light:.4rem .4rem 0 .1rem #B2AFAF;--box-shadow-dark:.4rem .4rem 0 .1rem #454343;--box-shadow-auto:.4rem .4rem 0 .1rem light-dark(#B2AFAF,#454343);--h1-color:var(--primary);--h2-color:#c33d35;--h3-color:#b94f44;--h4-color:#ae5e52;--h5-color:#a16a61;--h6-color:#927671;--font-size:1.1em;--default-space:.2em;}[data-theme=dark]{color-scheme:dark;}[data-theme="light"]{color-scheme:light;}*,*::before,*::after{box-sizing:border-box;}html{-moz-text-size-adjust:none;-webkit-text-size-adjust:none;text-size-adjust:none;}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin-block-end:0;}ul[role='list'],ol[role='list']{list-style:none;}body{min-height:100vh;line-height:1.5;}h1,h2,h3,h4,h4,h5,h6,button,input,label{line-height:1.1;}h1,h2,h3,h4,h5,h6{text-wrap:balance;margin:calc(var(--default-space)*2);}input,button,textarea,select{font-family:inherit;font-size:inherit;}textarea:not([rows]){min-height:10em;}:target{scroll-margin-block:5ex;}a{color:var(--primary);text-decoration:none;}a:hover{color:var(--primary-lighten);text-decoration:underline var(--primary-darken);}a[target="_blank"]::after,a.external::after{content:'&nsp;';display:inline-block;width:1em;height:1em;--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -6 24 28'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 6H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-5m-6 0l7.5-7.5M15 3h6v6'/%3E%3C/svg%3E");background-color:currentColor;mask-image:var(--svg);mask-repeat:no-repeat;mask-size:100% 100%;}h1{color:var(--h1-color);font-size:calc(var(--font-size) * 1.6);}h2{color:var(--h2-color);font-size:calc(var(--font-size) * 1.5);}h3{color:var(--h3-color);font-size:calc(var(--font-size) * 1.4);}h4{color:var(--h4-color);font-size:calc(var(--font-size) * 1.3);}h5{color:var(--h5-color);font-size:calc(var(--font-size) * 1.2);}h6{color:var(--h6-color);font-size:calc(var(--font-size) * 1.1);}table{border-collapse:collapse;}table,th,td{border:1px solid var(--primary-darken);}th{padding:calc(var(--default-space)*2);background-color:var(--primary-lighten);color:var(--primary-text-contrast);}td{padding:var(--default-space)}html{background-color:var(--background-color);scroll-behavior:smooth;}body{width:80vw;margin:0 auto;min-height:100vh;min-height:100dvh;text-rendering:optimizeSpeed;font-size:var(--font-size);}header{background-color:transparent;}header h1 img{display:inline;}header h1{color:var(--primary);}header svg{fill:var(--primary-lighten);vertical-align:middle;}nav{width:100%;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;}nav h1,nav ul{margin:0;}nav h1 img{margin-right:var(--default-space);vertical-align:middle;}nav ul{display:flex;gap:calc(var(--default-space) * 2);flex-wrap:wrap;padding:0;}nav li{margin:var(--default-space);list-style:none;font-size:calc(var(--font-size) * 1.2);}nav li a{color:var(--primary-lighten);}nav li a:hover,nav .active{color:var(--primary-lighten);text-decoration:underline var(--primary-darken);text-decoration-thickness:var(--default-space);}section h2{font-size:calc(var(--font-size) * 1.6);color:var(--h2-color);margin:0;}section,article{align-self:center;color:var(--light-text-color);background-color:var(--light-background-color);padding:calc(var(--default-space) * 2);border:1px solid var(--background-color-darken);border-radius:calc(var(--default-space) * 1.2);margin:calc(var(--default-space) *5) 0;box-shadow:var(--box-shadow-light);}.last-pics article{box-shadow:none;}.home{display:grid;grid-template-columns:repeat(7,1fr);gap:calc(var(--default-space) *5);width:100%;height:100%;article{margin:var(--default-space) 0;padding:0}.last-pics article{display:flex;flex-wrap:wrap;justify-content:center;box-shadow:none;border:none}figure{width:min-content;display:grid}figcaption{overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap}}.blog{section{display:flex;flex-wrap:wrap;justify-content:center;justify-content:space-around;gap:calc(var(--default-space)*3)}.article-card article{width:24vw;padding:var(--default-space);border-radius:calc(var(--default-space) *2)}.breadcrumb{ul{gap:0}li{margin:auto 0}li:not(:last-child)::after{content:">"}}.blog-page-number{text-align:left;display:inherit}.card{box-shadow:none;width:25vw}}.pagination{gap:0;}.pagination li{border:1px solid var(--primary-darken);margin:0;}.pagination li a{padding:var(--default-space) calc(var(--default-space)*2);display:block;}.pagination li a:hover,.pagination li a.current{text-decoration:none;background-color:var(--primary-lighten);color:var(--primary-text-contrast);}.table-of-contents{float:right;width:25vw;background-color:rgb(178,175,175);border:1px solid var(--primary-lighten);border-radius:calc(var(--default-space) * 1.2);position:sticky;top:calc(var(--default-space) * 2);}.table-of-contents a{text-decoration:none;}.table-of-contents a:hover,.table-of-contents a:active{text-decoration:underline;}.article-footer{border-top:1px solid var(--light-text-color);margin-top:calc(var(--default-space)*3);font-size:1rem;}footer{text-align:center;margin:calc(var(--default-space) *2) auto 0 auto;background-color:transparent;width:fit-content;padding:var(--default-space);}footer .a{color:var(--primary-lighten);}footer p{margin:var(--default-space);font-size:.9rem;}pre{background-color:var(--dark-background-color);color:var(--dark-text-color);padding:calc(var(--default-space) * 4);}@media (max-width:992px){.table-of-contents{float:none;width:auto}.home{grid-template-columns:1fr}.what-is-this{grid-area:1 / 1 / 2 / 2}.social-networks{grid-area:2 / 1 / 3 / 2}.last-posts{grid-area:3 / 1 / 4 / 2}.last-shaares{grid-area:4 / 1 / 5 / 2}.last-apps-updates{grid-area:5 / 1 / 6 / 2}.last-pics{grid-area:6 / 1 / 7 / 2}}@media (min-width:992px){.home{grid-template-columns:repeat(7,1fr)}.what-is-this{grid-area:1 / 1 / 2 / 6}.social-networks{grid-area:1 / 6 / 2 / 8}.last-posts{grid-area:2 / 1 / 4 / 4}.last-shaares{grid-area:2 / 5 / 4 / 8}.last-apps-updates{grid-area:4 / 1 / 5 / 2}.last-pics{grid-area:4 / 2 / 5 / 8}}
\ No newline at end of file
diff --git a/public/assets/css/app.css b/public/assets/css/app.css
new file mode 100644
index 0000000..d1c5769
--- /dev/null
+++ b/public/assets/css/app.css
@@ -0,0 +1,555 @@
+:root {
+ color-scheme: dark light;
+
+ --primary: #cc2027;
+ --primary-darken: #8E161B;
+ --primary-lighten: #D64C52;
+ --primary-text-contrast: #FFF;
+
+ --secondary: #20ccc5;
+ --secondary-darken: #168E89;
+ --secondary-lighten: #4CD6D0;
+ --secondary-text-contrast: #000;
+
+ --error: #c43933;
+ --error-darken: #892723;
+ --error-lighten: #CF605B;
+ --error-text-contrast: #FFF;
+
+ --info: #206ccc;
+ --info-darken: #164B8E;
+ --info-lighten: #4C89D6;
+ --info-text-contrast: #FFF;
+
+ --success: #7dcc20;
+ --success-darken: #578E16;
+ --success-lighten: #97D64C;
+ --success-text-contrast: #000;
+
+ --warning: #cc5e20;
+ --warning-darken: #8E4116;
+ --warning-lighten: #D67E4C;
+ --warning-text-contrast: #FFF;
+
+ --background-color: light-dark(#fffbfb, #171414);
+ --background-color-darken: light-dark(#B2AFAF, #100E0E);
+ --background-color-lighten: light-dark(#FFFBFB, #454343);
+
+ --light-background-color: #fffbfb;
+ --dark-background-color: #171414;
+
+ --header-background-color: light-dark(#171414, #fffbfb);
+ --header-background-color-darken: light-dark(#100E0E, #B2AFAF);
+ --header-background-color-lighten: light-dark(#454343, #FFFBFB);
+
+ --header-text-color: light-dark(#fffbfb, #171414);
+ --header-text-color-secondary: #ffffffb3;
+ --header-text-color-disable: light-dark(#ffffff80, #454343);
+
+ --text-color: light-dark(#171414, #fffbfb);
+ --text-color-secondary: #ffffffb3;
+ --text-color-disable: light-dark(#454343, #ffffff80);
+
+ --light-text-color: #171414;
+ --dark-text-color: #fffbfb;
+
+ --text-color-inverse: light-dark(#fffbfb, #171414);
+ --text-color-secondary-inverse: #ffffffb3;
+ --text-color-disable-inverse: light-dark(#ffffff80, #454343);
+
+ --box-shadow-light: .4rem .4rem 0px .1rem #B2AFAF;
+ --box-shadow-dark: .4rem .4rem 0px .1rem #454343;
+ --box-shadow-auto: .4rem .4rem 0px .1rem light-dark(#B2AFAF, #454343);
+
+ --h1-color: var(--primary);
+ --h2-color: #c33d35;
+ --h3-color: #b94f44;
+ --h4-color: #ae5e52;
+ --h5-color: #a16a61;
+ --h6-color: #927671;
+
+ --font-size: 1.1em;
+ --default-space: .2em;
+}
+
+[data-theme=dark] {
+ color-scheme: dark;
+}
+
+[data-theme="light"] {
+ color-scheme: light;
+}
+
+
+/* RESET CSS BY Piccalil.li */
+/* https://piccalil.li/blog/a-more-modern-css-reset/ */
+/* License https://creativecommons.org/licenses/by/3.0/ */
+
+
+/* Box sizing rules */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+/* Prevent font size inflation */
+html {
+ -moz-text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+ text-size-adjust: none;
+}
+
+/* Remove default margin in favour of better control in authored CSS */
+body,
+h1,
+h2,
+h3,
+h4,
+p,
+figure,
+blockquote,
+dl,
+dd {
+ margin-block-end: 0;
+}
+
+/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
+ul[role='list'],
+ol[role='list'] {
+ list-style: none;
+}
+
+/* Set core body defaults */
+body {
+ min-height: 100vh;
+ line-height: 1.5;
+}
+
+/* Set shorter line heights on headings and interactive elements */
+h1,
+h2,
+h3,
+h4,
+h4,
+h5,
+h6,
+button,
+input,
+label {
+ line-height: 1.1;
+}
+
+/* Balance text wrapping on headings */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ text-wrap: balance;
+ margin: calc(var(--default-space)*2);
+}
+
+/* Make images easier to work with */
+/*img,
+picture {
+ max-width: 100%;
+ display: block;
+}*/
+
+/* Inherit fonts for inputs and buttons */
+input,
+button,
+textarea,
+select {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+/* Make sure textareas without a rows attribute are not tiny */
+textarea:not([rows]) {
+ min-height: 10em;
+}
+
+/* Anything that has been anchored to should have extra scroll margin */
+:target {
+ scroll-margin-block: 5ex;
+}
+
+a {
+ color: var(--primary);
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--primary-lighten);
+ text-decoration: underline var(--primary-darken);
+}
+
+a[target="_blank"]::after,
+a.external::after {
+ content: '&nsp;';
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -6 24 28'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 6H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-5m-6 0l7.5-7.5M15 3h6v6'/%3E%3C/svg%3E");
+ background-color: currentColor;
+ mask-image: var(--svg);
+ mask-repeat: no-repeat;
+ mask-size: 100% 100%;
+}
+
+h1 {
+ color: var(--h1-color);
+ font-size: calc(var(--font-size) * 1.6);
+}
+
+h2 {
+ color: var(--h2-color);
+ font-size: calc(var(--font-size) * 1.5);
+}
+
+h3 {
+ color: var(--h3-color);
+ font-size: calc(var(--font-size) * 1.4);
+}
+
+h4 {
+ color: var(--h4-color);
+ font-size: calc(var(--font-size) * 1.3);
+}
+
+h5 {
+ color: var(--h5-color);
+ font-size: calc(var(--font-size) * 1.2);
+}
+
+h6 {
+ color: var(--h6-color);
+ font-size: calc(var(--font-size) * 1.1);
+}
+
+table {
+ border-collapse: collapse;
+}
+
+table,
+th,
+td {
+ border: 1px solid var(--primary-darken);
+}
+
+th {
+ padding: calc(var(--default-space)*2);
+ background-color: var(--primary-lighten);
+ color: var(--primary-text-contrast);
+}
+
+td {
+ padding: var(--default-space)
+}
+
+html {
+ background-color: var(--background-color);
+ scroll-behavior: smooth;
+}
+
+body {
+ width: 80vw;
+ margin: 0 auto;
+ min-height: 100vh;
+ min-height: 100dvh;
+ text-rendering: optimizeSpeed;
+ font-size: var(--font-size);
+}
+
+header {
+ background-color: transparent;
+}
+
+header h1 img {
+ display: inline;
+}
+
+header h1 {
+ color: var(--primary);
+}
+
+header svg {
+ fill: var(--primary-lighten);
+ vertical-align: middle;
+}
+
+nav {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+}
+
+nav h1,
+nav ul {
+ margin: 0;
+}
+
+nav h1 img {
+ margin-right: var(--default-space);
+ vertical-align: middle;
+}
+
+nav ul {
+ display: flex;
+ gap: calc(var(--default-space) * 2);
+ flex-wrap: wrap;
+ padding: 0;
+}
+
+nav li {
+ margin: var(--default-space);
+ list-style: none;
+ font-size: calc(var(--font-size) * 1.2);
+}
+
+nav li a {
+ color: var(--primary-lighten);
+}
+
+nav li a:hover, nav .active {
+ color: var(--primary-lighten);
+ text-decoration: underline var(--primary-darken);
+ text-decoration-thickness: var(--default-space);
+}
+
+section h2 {
+ font-size: calc(var(--font-size) * 1.6);
+ color: var(--h2-color);
+ margin: 0;
+}
+
+section,
+article {
+ align-self: center;
+ color: var(--light-text-color);
+ background-color: var(--light-background-color);
+ padding: calc(var(--default-space) * 2);
+ border: 1px solid var(--background-color-darken);
+ border-radius: calc(var(--default-space) * 1.2);
+ margin: calc(var(--default-space) *5) 0;
+ box-shadow: var(--box-shadow-light);
+}
+
+.last-pics article {
+ box-shadow: none;
+}
+
+.home {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: calc(var(--default-space) *5);
+ width: 100%;
+ height: 100%;
+
+ article {
+ margin: var(--default-space) 0;
+ padding: 0;
+ }
+
+ .last-pics article {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ box-shadow: none;
+ border: none;
+ }
+
+ figure {
+ width: min-content;
+ display: grid;
+ }
+
+ figcaption {
+ overflow: hidden;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+.blog {
+ section {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ justify-content: space-around;
+ gap: calc(var(--default-space)*3);
+ }
+
+ .article-card article {
+ width: 24vw;
+ padding: var(--default-space);
+ border-radius: calc(var(--default-space) *2);
+ }
+
+ .breadcrumb {
+ ul {
+ gap: 0;
+ }
+
+ li {
+ margin: auto 0;
+ }
+
+ li:not(:last-child)::after {
+ content: " > ";
+ }
+ }
+
+ .blog-page-number {
+ text-align: left;
+ display: inherit;
+ }
+
+ .card {
+ box-shadow: none;
+ width: 25vw;
+ }
+}
+
+.pagination {
+ gap: 0;
+}
+
+.pagination li {
+ border: 1px solid var(--primary-darken);
+ margin: 0;
+}
+
+.pagination li a {
+ padding: var(--default-space) calc(var(--default-space)*2);
+ display: block;
+}
+
+.pagination li a:hover,
+.pagination li a.current {
+ text-decoration: none;
+ background-color: var(--primary-lighten);
+ color: var(--primary-text-contrast);
+}
+
+.table-of-contents {
+ float: right;
+ width: 25vw;
+ background-color: rgb(178, 175, 175);
+ border: 1px solid var(--primary-lighten);
+ border-radius: calc(var(--default-space) * 1.2);
+ position: sticky;
+ top: calc(var(--default-space) * 2);
+}
+
+.table-of-contents a {
+ text-decoration: none;
+}
+
+.table-of-contents a:hover,
+.table-of-contents a:active {
+ text-decoration: underline;
+}
+
+.article-footer {
+ border-top: 1px solid var(--light-text-color);
+ margin-top: calc(var(--default-space)*3);
+ font-size: 1rem;
+}
+
+footer {
+ text-align: center;
+ margin: calc(var(--default-space) *2) auto 0 auto;
+ background-color: transparent;
+ width: fit-content;
+ padding: var(--default-space);
+}
+
+footer .a {
+ color: var(--primary-lighten);
+}
+
+footer p {
+ margin: var(--default-space);
+ font-size: .9rem;
+}
+
+pre {
+ background-color: var(--dark-background-color);
+ color: var(--dark-text-color);
+ padding: calc(var(--default-space) * 4);
+}
+
+/* Smaller than 992px */
+
+@media (max-width: 992px) {
+ .table-of-contents {
+ float: none;
+ width: auto;
+ }
+
+ .home {
+ grid-template-columns: 1fr;
+ }
+
+ .what-is-this {
+ grid-area: 1 / 1 / 2 / 2;
+ }
+
+ .social-networks {
+ grid-area: 2 / 1 / 3 / 2;
+ }
+
+ .last-posts {
+ grid-area: 3 / 1 / 4 / 2;
+ }
+
+ .last-shaares {
+ grid-area: 4 / 1 / 5 / 2;
+ }
+
+ .last-apps-updates {
+ grid-area: 5 / 1 / 6 / 2;
+ }
+
+ .last-pics {
+ grid-area: 6 / 1 / 7 / 2;
+ }
+}
+
+/* Larger than 992px */
+@media (min-width: 992px) {
+ .home {
+ grid-template-columns: repeat(7, 1fr);
+ }
+
+ .what-is-this {
+ grid-area: 1 / 1 / 2 / 6;
+ }
+
+ .social-networks {
+ grid-area: 1 / 6 / 2 / 8;
+ }
+
+ .last-posts {
+ grid-area: 2 / 1 / 4 / 4;
+ }
+
+ .last-shaares {
+ grid-area: 2 / 5 / 4 / 8;
+ }
+
+ .last-apps-updates {
+ grid-area: 4 / 1 / 5 / 2;
+ }
+
+ .last-pics {
+ grid-area: 4 / 2 / 5 / 8;
+ }
+}
\ No newline at end of file
diff --git a/public/assets/favicons/android-chrome-192x192.png b/public/assets/favicons/android-chrome-192x192.png
new file mode 100644
index 0000000..fc945e8
Binary files /dev/null and b/public/assets/favicons/android-chrome-192x192.png differ
diff --git a/public/assets/favicons/android-chrome-256x256.png b/public/assets/favicons/android-chrome-256x256.png
new file mode 100644
index 0000000..18828fb
Binary files /dev/null and b/public/assets/favicons/android-chrome-256x256.png differ
diff --git a/public/assets/favicons/apple-touch-icon.png b/public/assets/favicons/apple-touch-icon.png
new file mode 100644
index 0000000..b9ae21e
Binary files /dev/null and b/public/assets/favicons/apple-touch-icon.png differ
diff --git a/public/assets/favicons/browserconfig.xml b/public/assets/favicons/browserconfig.xml
new file mode 100644
index 0000000..b8270a1
--- /dev/null
+++ b/public/assets/favicons/browserconfig.xml
@@ -0,0 +1,9 @@
+
+C'est quoi ici ?!
+
+ Quelques articles écrits plus ou moins récemment. +
+ = $lastPost; ?> ++ Tous mes posts +
++ Ce sont mes derniers marque-pages partagés, l'ensemble est disponible sur mon Shaarli. +
+ = $lastShaare; ?> + Tous mes partages ++ Les dernières mise à jours de mes apps. +
+ = $lastAppsUpdates; ?> + Toutes les mises à jours ++ Ma galerie d'image et de photos. +
+