commit 92a272c03de3fba2888de8644a448eed0fd2a6b2 Author: Nicolas Ong Date: Thu Feb 13 10:37:29 2025 +0100 First commit! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7a34b7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 kholo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e218ce --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# fibable + +A little PHP file-based blog engine. + +This is a lightweight and not yet finished blog engine, written in PHP, working without database, only with directory structure and files and a sort of Markdown format. diff --git a/config.php b/config.php new file mode 100644 index 0000000..9a253c1 --- /dev/null +++ b/config.php @@ -0,0 +1,16 @@ + "https://example.com" +]; diff --git a/css/blog.css b/css/blog.css new file mode 100644 index 0000000..87925bd --- /dev/null +++ b/css/blog.css @@ -0,0 +1,5 @@ +@charset "utf-8"; + +* { box-sizing: border-box; } + +body { font-family: sans-serif; } diff --git a/data/articles/.gitignore b/data/articles/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/data/cache/default.phtml b/data/cache/default.phtml new file mode 100644 index 0000000..7947b73 --- /dev/null +++ b/data/cache/default.phtml @@ -0,0 +1,8 @@ +
+

L'article que vous cherchez n'existe pas

+ +
+

+ Il n'y a rien à voir ici ! Vous avez du vous tromper de chemin ! +

+ diff --git a/data/images/.gitignore b/data/images/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/data/serial/default.pobj b/data/serial/default.pobj new file mode 100644 index 0000000..d1efe76 --- /dev/null +++ b/data/serial/default.pobj @@ -0,0 +1 @@ +O:7:"Article":5:{s:5:"title";s:40:"L'article que vous cherchez n'existe pas";s:4:"date";s:10:"2023-08-01";s:3:"ref";s:2:"01";s:4:"tags";a:2:{i:0;s:4:"blog";i:1;s:5:"essai";}s:4:"view";s:26:"./data/cache/default.phtml";} diff --git a/data/tags/.gitignore b/data/tags/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/index.php b/index.php new file mode 100644 index 0000000..59f28e7 --- /dev/null +++ b/index.php @@ -0,0 +1,10 @@ +run(); diff --git a/lib/Article.php b/lib/Article.php new file mode 100644 index 0000000..8b3e3de --- /dev/null +++ b/lib/Article.php @@ -0,0 +1,17 @@ +view; + } + +} diff --git a/lib/ArticleDate.php b/lib/ArticleDate.php new file mode 100644 index 0000000..e9aecff --- /dev/null +++ b/lib/ArticleDate.php @@ -0,0 +1,14 @@ +str = $str; + } + +} diff --git a/lib/ArticleManager.php b/lib/ArticleManager.php new file mode 100644 index 0000000..e5e7917 --- /dev/null +++ b/lib/ArticleManager.php @@ -0,0 +1,119 @@ +articles[] = self::getArticle($file_path); + } + } + } + } + return $dates; + } + + private static function getArticle(string $file_path): Article + { + $pattern = "|(./data)/articles/([0-9]+)/([0-9]+)/([0-9]+)/([0-9]+)\.txt|"; + $replacement = "$1/serial/$2$3$4$5.pobj"; + $serial_path = preg_replace($pattern, $replacement, $file_path); + return (file_exists($serial_path) and filemtime($serial_path) > filemtime($file_path)) ? + unserialize(file_get_contents($serial_path)) : + self::createArticle($file_path, $serial_path); + } + + private static function createArticle(string $file_path, string $serial_path): Article + { + $parser = new ArticleParser(); + $article = $parser->parse($file_path); + file_put_contents($serial_path, serialize($article)); + file_put_contents("./data/cache/latest", "", LOCK_EX); + TagManager::add($article->tags, substr(basename($serial_path), 0, -5)); + return $article; + } +} diff --git a/lib/ArticleParser.php b/lib/ArticleParser.php new file mode 100644 index 0000000..66532ed --- /dev/null +++ b/lib/ArticleParser.php @@ -0,0 +1,122 @@ +$1"; + const REPLACE_ITALIC = "$1"; + const REPLACE_UNDERLINE = "$1"; + + private bool $in_p = false; + private array $content = []; + private array $p = []; + + public function parse(string $file_path): Article + { + $article = new Article(); + $article->date = preg_replace(self::PATTERN_DATE, self::REPLACE_DATE, $file_path); + $article->ref = preg_replace(self::PATTERN_DATE, self::REPLACE_REF, $file_path); + $lines = file($file_path, FILE_IGNORE_NEW_LINES); + foreach ($lines as $line) { + if (substr($line, 0, 2) === "# ") { + $article->title = substr($line, 2); + continue; + } + if (substr($line, 0, 3) === "## ") { + $this->closeParagraph(); + $this->content[] = "

" . substr($line, 3) . "

"; + continue; + } + if (substr($line, 0, 4) === "### ") { + $this->closeParagraph(); + $this->content[] = "

" . substr($line, 4) . "

"; + continue; + } + if (substr($line, 0, 5) === "#### ") { + $this->closeParagraph(); + $this->content[] = "
" . substr($line, 5) . "
"; + continue; + } + if (preg_match(self::PATTERN_TAGS, $line, $matches)) { + $article->tags = explode(",", $matches[1]); + continue; + } + if (preg_match(self::PATTERN_IMG, $line, $matches)) { + $this->closeParagraph(); + $this->content[] = "
"; + $this->content[] = " \"$matches[3]\""; + $this->content[] = "
$matches[3]
"; + $this->content[] = "
"; + continue; + } + if ($line === "") { + $this->closeParagraph(); + continue; + } else { + if (!$this->in_p) { + $this->content[] = "

"; + $this->in_p = true; + } + $this->p[] = $this->parseLine($line); + } + } + $this->closeParagraph(); + array_unshift($this->content, + "

", + "

" . $article->title . "

", + " ", + "
" + ); + $this->content[] = ""; + $view_path = "./data/cache/{$article->date}_{$article->ref}.phtml"; + file_put_contents($view_path, " " . implode("\n ", $this->content) . "\n"); + $article->view = $view_path; + $this->content = []; + return $article; + } + + private function closeParagraph() + { + if ($this->in_p) { + $this->content[] = " " . implode("
", $this->p); + $this->content[] = "

"; + $this->in_p = false; + $this->p = []; + } + } + + private function parseLine(string $line): string + { + $patterns = [ + self::PATTERN_BOLD, + self::PATTERN_ITALIC, + self::PATTERN_UNDERLINE + ]; + $replacements = [ + self::REPLACE_BOLD, + self::REPLACE_ITALIC, + self::REPLACE_UNDERLINE + ]; + return preg_replace($patterns, $replacements, htmlspecialchars($line, ENT_HTML5, "UTF-8")); + } + + private function getHtmlTags(array $tags): string + { + $html = ""; + foreach ($tags as $tag) { + $html .= "$tag"; + } + return $html; + } + +} diff --git a/lib/Blog.php b/lib/Blog.php new file mode 100644 index 0000000..d189495 --- /dev/null +++ b/lib/Blog.php @@ -0,0 +1,117 @@ +loadConfig(); + } + + public function run() + { + $this->render(); + } + + private function loadConfig() + { + require_once "config.php"; + foreach ($links as $title => $url) { + $this->links[] = new Link($title, $url); + } + } + + private function render() + { + extract($this->populateViews()); + include "views/index.phtml"; + } + + private function populateViews(): array + { + return array_merge( + ["lang" => BLOG_LANGUAGE], + $this->populateHeader(), + $this->populateMainView(), + $this->populateBio(), + $this->populateNavByTag(), + $this->populateNavByLatest(), + $this->populateFooter() + ); + } + + private function populateHeader(): array + { + return [ + "title" => BLOG_TITLE, + "has_sub_title" => BLOG_SUB_TITLE !== "", + "sub_title" => BLOG_SUB_TITLE + ]; + } + + private function populateMainView(): array + { + $variables = []; + $view = filter_input(INPUT_GET, "view", FILTER_SANITIZE_STRING) ?: "single"; + $variables["view"] = $view; + switch ($view) { + case "single": + $date = filter_input(INPUT_GET, "date", FILTER_SANITIZE_STRING); + $ref = filter_input(INPUT_GET, "ref", FILTER_SANITIZE_STRING); + $variables["article"] = ($date !== null and $ref !== null) ? + ArticleManager::get(str_replace("-", "", $date) . $ref) : + ArticleManager::getNewest(); + break; + case "tags": + $tag = filter_input(INPUT_GET, "tag", FILTER_SANITIZE_STRING) ?: ""; + $variables["articles"] = ArticleManager::getMultiple(TagManager::getFromTag($tag)); + break; + case "archives": + $variables["dates"] = ArticleManager::getAll();; + break; + } + return $variables; + } + + private function populateBio(): array + { + return [ + "author" => AUTHOR_NICKNAME, + "bio" => AUTHOR_BIOGRAPHY, + ]; + } + + private function populateNavByTag(): array + { + return ["tags" => TagManager::getTags()]; + } + + private function populateNavByLatest(): array + { + return [ + "latest" => ArticleManager::getLatestArticles(ARTICLE_COUNT_IN_NAV) + ]; + } + + private function populateFooter(): array + { + return [ + "has_links" => count($this->links) > 0, + "links" => $this->links, + "footer" => $this->getFooterString() + ]; + } + + private function getFooterString(): string + { + $currentYear = getdate()["year"]; + $footerYear = BLOG_CREATION_YEAR . + (BLOG_CREATION_YEAR !== $currentYear ? " - $currentYear" : ""); + $footerAuthor = AUTHOR_NICKNAME . + (AUTHOR_REALNAME !== "" ? " (" . AUTHOR_REALNAME . ")" : ""); + return "$footerYear. $footerAuthor"; + } + +} diff --git a/lib/Entry.php b/lib/Entry.php new file mode 100644 index 0000000..95192ba --- /dev/null +++ b/lib/Entry.php @@ -0,0 +1,12 @@ +title = $post->title; + $entry->content = $post->content; + $entry->date = date("c", $post->date); + $entry->url = $link . + "/?view=single&date=" . date("Y-m-d", $post->date) . + "&ref=" . $post->id; + $entry->id = self::getEntryUUID($post); + return $entry; + } + + private static function getEntryUUID(Post $post): string + { + $ref = date("Ymd", $post->date) . $post->id; + $file = self::BASE_PATH . $ref; + if (!is_file($file)) { + file_put_contents($file, UUIDGenerator::generate(), LOCK_EX); + } + return file_get_contents($file); + } + +} diff --git a/lib/Feed.php b/lib/Feed.php new file mode 100644 index 0000000..b31de98 --- /dev/null +++ b/lib/Feed.php @@ -0,0 +1,125 @@ +type = $type; + $this->loadConfig(); + } + + public function run() + { + $this->render(); + } + + private function loadConfig() + { + require_once "config.php"; + } + + private function render() + { + header("Content-Type: application/" . $this->type . "+xml"); + $file = self::BASE_PATH . $this->type . ".xml"; + $latest = self::BASE_PATH . "latest"; + if (!is_file($file) or filemtime($file) < filemtime($latest)) { + extract($this->populateViews(FeedManager::getLatestPosts(self::POST_COUNT))); + ob_start(); + include "views/" . $this->type . ".phtml"; + $output = ob_get_clean(); + file_put_contents($file, $output, LOCK_EX); + } + include $file; + } + + private function populateViews(array $posts): array + { + return $this->type === "rss" ? + $this->populateRss($posts) : + $this->populateAtom($posts); + } + + private function populateRss(array $posts): array + { + return array_merge( + $this->populateCommon(), + $this->populateRssRoot(), + $this->populateItems($posts) + ); + } + + private function populateAtom(array $posts): array + { + return array_merge( + $this->populateCommon(), + $this->populateAtomRoot(), + $this->populateEntries($posts) + ); + } + + private function populateCommon(): array + { + return [ + "title" => BLOG_TITLE, + "has_sub_title" => BLOG_SUB_TITLE !== "", + "sub_title" => BLOG_SUB_TITLE, + "author_name" => AUTHOR_NICKNAME, + "has_author_email" => AUTHOR_EMAIL !== "", + "author_email" => AUTHOR_EMAIL + ]; + } + + private function populateRssRoot(): array + { + return [ + "last_build_date" => date("r"), + "link" => BLOG_LINK, + "self" => BLOG_LINK . "/rss.php" + ]; + } + + private function populateItems(array $posts): array + { + $items = []; + foreach ($posts as $post) { + $items[] = ItemParser::parse($post, BLOG_LINK); + } + return ["items" => $items]; + } + + private function populateAtomRoot(): array + { + return [ + "url" => BLOG_LINK, + "updated" => date("c"), + "id" => $this->getAtomUUID(), + "self" => BLOG_LINK . "/atom.php" + ]; + } + + private function getAtomUUID(): string + { + $file = self::BASE_PATH . "uuid"; + if (!is_file($file)) { + file_put_contents($file, UUIDGenerator::generate(), LOCK_EX); + } + return file_get_contents($file); + } + + private function populateEntries(array $posts): array + { + $entries = []; + foreach ($posts as $post) { + $entries[] = EntryParser::parse($post, BLOG_LINK); + } + return ["entries" => $entries]; + } + +} diff --git a/lib/FeedManager.php b/lib/FeedManager.php new file mode 100644 index 0000000..7784610 --- /dev/null +++ b/lib/FeedManager.php @@ -0,0 +1,34 @@ +date = self::getDate($file_path); + $post->id = preg_replace(ArticleParser::PATTERN_DATE, ArticleParser::REPLACE_REF, $file_path); + $lines = file($file_path, FILE_IGNORE_NEW_LINES); + foreach ($lines as $line) { + if (substr($line, 0, 2) === "# ") { + $post->title = substr($line, 2); + continue; + } + if (preg_match(ArticleParser::PATTERN_TAGS, $line, $matches)) { + $post->tags = explode(",", $matches[1]); + break; + } + } + $post->content = self::getContent($post->date, $post->id); + return $post; + } + + private static function getDate(string $file_path): int + { + $strdate = preg_replace(ArticleParser::PATTERN_DATE, ArticleParser::REPLACE_DATE, $file_path); + list($year, $month, $day) = explode("-", $strdate, 3); + $strtime = date("H:i:s", filemtime($file_path)); + list($hour, $minute, $second) = explode(":", $strtime); + return mktime($hour, $minute, $second, $month, $day, $year); + } + + private static function getContent(int $date, string $id): string + { + $file = "./data/cache/" . date("Y-m-d", $date) . "_${id}.phtml"; + ob_start(); + include $file; + return ob_get_clean(); + } + +} diff --git a/lib/Item.php b/lib/Item.php new file mode 100644 index 0000000..f0caacf --- /dev/null +++ b/lib/Item.php @@ -0,0 +1,12 @@ +title = $post->title; + $item->description = $post->content; + $item->pub_date = date("r", $post->date); + $item->url = $link . + "/?view=single&date=" . date("Y-m-d", $post->date) . + "&ref=" . $post->id; + foreach ($post->tags as $tag) { + $item->categories[] = $tag; + } + return $item; + } + +} diff --git a/lib/Link.php b/lib/Link.php new file mode 100644 index 0000000..ce914c2 --- /dev/null +++ b/lib/Link.php @@ -0,0 +1,15 @@ +title = $title; + $this->url = $url; + } + +} diff --git a/lib/Post.php b/lib/Post.php new file mode 100644 index 0000000..5aae7cd --- /dev/null +++ b/lib/Post.php @@ -0,0 +1,12 @@ + + +
  • +

    str?>

    + +
  • + + diff --git a/views/aside.phtml b/views/aside.phtml new file mode 100644 index 0000000..0ca97b2 --- /dev/null +++ b/views/aside.phtml @@ -0,0 +1,8 @@ + diff --git a/views/atom.phtml b/views/atom.phtml new file mode 100644 index 0000000..771621a --- /dev/null +++ b/views/atom.phtml @@ -0,0 +1,26 @@ + + + <?=$title?> + + + + + + + + + + + + + + + + <?=$entry->title?> + + id?> + date?> + content?>]]> + + + diff --git a/views/bio.phtml b/views/bio.phtml new file mode 100644 index 0000000..3117918 --- /dev/null +++ b/views/bio.phtml @@ -0,0 +1,7 @@ +
    +
    + <?=$author?> +
    +
    +

    +
    diff --git a/views/footer.phtml b/views/footer.phtml new file mode 100644 index 0000000..2e8b741 --- /dev/null +++ b/views/footer.phtml @@ -0,0 +1,3 @@ + diff --git a/views/header.phtml b/views/header.phtml new file mode 100644 index 0000000..1f9cc0e --- /dev/null +++ b/views/header.phtml @@ -0,0 +1,6 @@ +
    +

    + +

    + +
    diff --git a/views/index.phtml b/views/index.phtml new file mode 100644 index 0000000..762fb31 --- /dev/null +++ b/views/index.phtml @@ -0,0 +1,18 @@ + + + + + <?=$title?> + + + + + + + + diff --git a/views/links.phtml b/views/links.phtml new file mode 100644 index 0000000..735ea23 --- /dev/null +++ b/views/links.phtml @@ -0,0 +1,5 @@ + diff --git a/views/main.phtml b/views/main.phtml new file mode 100644 index 0000000..27b8e9e --- /dev/null +++ b/views/main.phtml @@ -0,0 +1,3 @@ +
    + +
    diff --git a/views/nav-by-latest.phtml b/views/nav-by-latest.phtml new file mode 100644 index 0000000..8ea3d9e --- /dev/null +++ b/views/nav-by-latest.phtml @@ -0,0 +1,6 @@ + diff --git a/views/nav-by-tag.phtml b/views/nav-by-tag.phtml new file mode 100644 index 0000000..d4ac6ac --- /dev/null +++ b/views/nav-by-tag.phtml @@ -0,0 +1,5 @@ + diff --git a/views/rss.phtml b/views/rss.phtml new file mode 100644 index 0000000..50779f7 --- /dev/null +++ b/views/rss.phtml @@ -0,0 +1,26 @@ + + + + + <?=$title?> + + + + + + + + <?=$item->title?> + description?>]]> + pub_date?> + url?> + + + +categories as $category): ?> + + + + + + diff --git a/views/single.phtml b/views/single.phtml new file mode 100644 index 0000000..f5c588f --- /dev/null +++ b/views/single.phtml @@ -0,0 +1,3 @@ +
    +render(); ?> +
    diff --git a/views/tags.phtml b/views/tags.phtml new file mode 100644 index 0000000..67f02b1 --- /dev/null +++ b/views/tags.phtml @@ -0,0 +1,5 @@ +