First commit with actual files.

main
Nicolas Ong 2023-09-04 21:13:29 +02:00
parent 1f82cfa839
commit c1b83f8775
24 changed files with 549 additions and 1 deletions

View File

@ -1,3 +1,5 @@
# fibable
A little PHP file-based blog engine.
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.

14
config.php Normal file
View File

@ -0,0 +1,14 @@
<?php
define("BLOG_TITLE", "blog title");
define("BLOG_SUB_TITLE", "blog sub title - optional, leave blank if none");
define("BLOG_LANGUAGE", "country code");
define("BLOG_CREATION_YEAR", 2023);
define("AUTHOR_NICKNAME", "blog author nickname");
define("AUTHOR_REALNAME", "blog author real name - optional, leave blank if none");
define("AUTHOR_BIOGRAPHY", "blog author biography");
define("ARTICLE_COUNT_IN_NAV", 10);
$links = [
"link label" => "https://example.com"
];

5
css/blog.css Normal file
View File

@ -0,0 +1,5 @@
@charset "utf-8";
* { box-sizing: border-box; }
body { font-family: sans-serif; }

8
data/cache/default.phtml vendored Normal file
View File

@ -0,0 +1,8 @@
<header>
<h2>L'article que vous cherchez n'existe pas</h2>
<nav></nav>
</header>
<p>
Il n'y a rien à voir ici ! Vous avez du vous tromper de chemin !
</p>
<footer>01/08/2023</footer>

1
data/serial/default.pobj Normal file
View File

@ -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";}

10
index.php Normal file
View File

@ -0,0 +1,10 @@
<?php
spl_autoload_register(
function ($class_name) {
require_once "lib/$class_name.php";
}
);
$blog = new Blog();
$blog->run();

17
lib/Article.php Normal file
View File

@ -0,0 +1,17 @@
<?php
final class Article
{
public string $title;
public string $date;
public string $ref;
public array $tags;
public string $view;
public function render()
{
include $this->view;
}
}

14
lib/ArticleDate.php Normal file
View File

@ -0,0 +1,14 @@
<?php
final class ArticleDate
{
public string $str;
public array $articles = [];
public function __construct(string $str)
{
$this->str = $str;
}
}

118
lib/ArticleManager.php Normal file
View File

@ -0,0 +1,118 @@
<?php
final class ArticleManager
{
const BASE_PATH = "./data/articles";
public static function get(string $fullref): Article
{
$serial_path = "./data/serial/$fullref.pobj";
return is_file($serial_path) ?
unserialize(file_get_contents($serial_path)) :
unserialize(file_get_contents("./data/serial/default.pobj"));
}
public static function getMultiple(array $fullrefs): array
{
$articles = [];
foreach ($fullrefs as $fullref) {
$articles[] = self::get($fullref);
}
return $articles;
}
public static function getNewest(): Article
{
$dot_dirs = [".", ".."];
$years = array_diff(scandir(self::BASE_PATH, SCANDIR_SORT_DESCENDING), $dot_dirs);
$year = $years[0];
$path = self::BASE_PATH . "/$year";
$months = array_diff(scandir($path, SCANDIR_SORT_DESCENDING), $dot_dirs);
$month = $months[0];
$path = "$path/$month";
$days = array_diff(scandir($path, SCANDIR_SORT_DESCENDING), $dot_dirs);
$day = $days[0];
$path = "$path/$day";
foreach (array_diff(scandir($path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $file) {
$file_path = "$path/$file";
if (pathinfo($file_path, PATHINFO_EXTENSION) !== "txt") {
continue;
}
$ref = substr($file, 0, -4);
break;
}
return self::get("$year$month$day$ref");
}
public static function getLatestArticles(int $count): array
{
$dot_dirs = [".", ".."];
$articles = [];
foreach (array_diff(scandir(self::BASE_PATH, SCANDIR_SORT_DESCENDING), $dot_dirs) as $year) {
$year_path = self::BASE_PATH . "/$year";
foreach (array_diff(scandir($year_path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $month) {
$month_path = "$year_path/$month";
foreach (array_diff(scandir($month_path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $day) {
$day_path = "$month_path/$day";
foreach (array_diff(scandir($day_path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $file) {
$file_path = "$day_path/$file";
if (pathinfo($file_path, PATHINFO_EXTENSION) !== "txt") {
continue;
}
$articles[] = self::getArticle($file_path);
if (count($articles) === $count) {
return $articles;
}
}
}
}
}
return $articles;
}
public static function getAll(): array
{
$dot_dirs = [".", ".."];
$dates = [];
foreach (array_diff(scandir(self::BASE_PATH, SCANDIR_SORT_DESCENDING), $dot_dirs) as $year) {
$year_path = self::BASE_PATH . "/$year";
foreach (array_diff(scandir($year_path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $month) {
$month_path = "$year_path/$month";
foreach (array_diff(scandir($month_path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $day) {
$day_path = "$month_path/$day";
$date = "$year$month$day";
foreach (array_diff(scandir($day_path, SCANDIR_SORT_DESCENDING), $dot_dirs) as $file) {
$file_path = "$day_path/$file";
if (pathinfo($file_path, PATHINFO_EXTENSION) !== "txt") {
continue;
}
if (!isset($dates["$date"])) {
$dates["$date"] = new ArticleDate("$day/$month/$year");
}
$dates["$date"]->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) ?
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));
TagManager::add($article->tags, substr(basename($serial_path), 0, -5));
return $article;
}
}

122
lib/ArticleParser.php Normal file
View File

@ -0,0 +1,122 @@
<?php
final class ArticleParser
{
const PATTERN_DATE = "|\./data/articles/([0-9]+)/([0-9]+)/([0-9]+)/([0-9]+).txt|";
const PATTERN_TAGS = "/^\{(([a-zA-Z0-9]+,*)+)\}$/";
const PATTERN_IMG = "/^([a-zA-Z0-9\-_\.]+\.(jpg|png)) \((.*)\)$/";
const PATTERN_BOLD = "/\*([^*]*)\*/";
const PATTERN_ITALIC = "|/([^/]*)/|";
const PATTERN_UNDERLINE = "/_([^_]*)_/";
const REPLACE_DATE = "$1-$2-$3";
const REPLACE_REF = "$4";
const REPLACE_BOLD = "<strong>$1</strong>";
const REPLACE_ITALIC = "<em>$1</em>";
const REPLACE_UNDERLINE = "<u>$1</u>";
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[] = "<h3>" . substr($line, 3) . "</h3>";
continue;
}
if (substr($line, 0, 4) === "### ") {
$this->closeParagraph();
$this->content[] = "<h4>" . substr($line, 4) . "</h4>";
continue;
}
if (substr($line, 0, 5) === "#### ") {
$this->closeParagraph();
$this->content[] = "<h5>" . substr($line, 5) . "</h5>";
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[] = "<figure>";
$this->content[] = " <img src=\"" . dirname($file_path) . "/$matches[1]\" alt=\"$matches[3]\">";
$this->content[] = " <figcaption>$matches[3]</figcaption>";
$this->content[] = "</figure>";
continue;
}
if ($line === "") {
$this->closeParagraph();
continue;
} else {
if (!$this->in_p) {
$this->content[] = "<p>";
$this->in_p = true;
}
$this->p[] = $this->parseLine($line);
}
}
$this->closeParagraph();
array_unshift($this->content,
"<header>",
" <h2>" . $article->title . "</h2>",
" <nav>" . $this->getHtmlTags($article->tags) . "</nav>",
"</header>"
);
$this->content[] = "<footer>" .
preg_replace("/([0-9]+)\-([0-9]+)-([0-9]+)/", "$3/$2/$1", $article->date) .
"</footer>";
$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("<br>", $this->p);
$this->content[] = "</p>";
$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 .= "<a href=\"?view=tags&tag=$tag\" class=\"tag\">$tag</a>";
}
return $html;
}
}

113
lib/Blog.php Normal file
View File

@ -0,0 +1,113 @@
<?php
final class Blog
{
private array $links = [];
public function run()
{
$this->loadConfig();
$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";
}
}

15
lib/Link.php Normal file
View File

@ -0,0 +1,15 @@
<?php
final class Link
{
public string $title;
public string $url;
public function __construct(string $title, string $url)
{
$this->title = $title;
$this->url = $url;
}
}

29
lib/TagManager.php Normal file
View File

@ -0,0 +1,29 @@
<?php
final class TagManager
{
const BASE_PATH = "./data/tags";
public static function getTags(): array
{
$tags = [];
foreach (array_diff(scandir(self::BASE_PATH), [".", ".."]) as $file) {
$tags[] = $file;
}
return $tags;
}
public static function getFromTag(string $tag): array
{
return file(self::BASE_PATH . "/$tag", FILE_IGNORE_NEW_LINES);
}
public static function add(array $tags, string $fullref)
{
foreach ($tags as $tag) {
file_put_contents(self::BASE_PATH . "/$tag", "$fullref\n", FILE_APPEND);
}
}
}

12
views/archives.phtml Normal file
View File

@ -0,0 +1,12 @@
<ul class="archives">
<?php foreach ($dates as $date): ?>
<li class="date">
<h3><?=$date->str?></h3>
<ul class="articles">
<?php foreach ($date->articles as $article): ?>
<li class="article"><a href="?view=single&date=<?=$article->date?>"><?=$article->title?></a></li>
<?php endforeach ?>
</ul>
</li>
<?php endforeach ?>
</ul>

7
views/aside.phtml Normal file
View File

@ -0,0 +1,7 @@
<aside>
<?php
include "views/bio.phtml";
include "views/nav-by-tag.phtml";
include "views/nav-by-latest.phtml";
?>
</aside>

7
views/bio.phtml Normal file
View File

@ -0,0 +1,7 @@
<div class="bio">
<figure>
<img src="./data/images/avatar.jpg" alt="<?=$author?>" class="portrait">
<figcaption><?=$author?></figcaption>
</figure>
<p><?=$bio?></p>
</div>

10
views/footer.phtml Normal file
View File

@ -0,0 +1,10 @@
<footer>
<?php if ($has_links): ?>
<ul class="links">
<?php foreach ($links as $link): ?>
<li><a rel="me" href="<?=$link->url?>" class="<?=$link->title?>"><?=$link->title?></a></li>
<?php endforeach ?>
</ul>
<?php endif ?>
<p><?=$footer?></p>
</footer>

6
views/header.phtml Normal file
View File

@ -0,0 +1,6 @@
<header>
<h1><?=$title?></h1>
<?php if ($has_sub_title): ?>
<h2><?=$sub_title?></h2>
<?php endif ?>
</header>

16
views/index.phtml Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="<?=$lang?>">
<head>
<meta charset="utf-8">
<title><?=$title?></title>
<link href="css/blog.css" rel="stylesheet" title="Style par défaut" type="text/css">
</head>
<body>
<?php
include "views/header.phtml";
include "views/main.phtml";
include "views/aside.phtml";
include "views/footer.phtml";
?>
</body>
</html>

3
views/main.phtml Normal file
View File

@ -0,0 +1,3 @@
<main>
<?php include "views/$view.phtml"; ?>
</main>

View File

@ -0,0 +1,6 @@
<nav class="latest">
<?php foreach ($latest as $prev): ?>
<a href="?view=single&date=<?=$prev->date?>&ref=<?=$prev->ref?>"><?=$prev->title?></a>
<?php endforeach ?>
<a href="?view=archives">Archives</a>
</nav>

5
views/nav-by-tag.phtml Normal file
View File

@ -0,0 +1,5 @@
<nav class="tags">
<?php foreach ($tags as $tag): ?>
<a href="?view=tags&tag=<?=$tag?>"><?=$tag?></a>
<?php endforeach ?>
</nav>

3
views/single.phtml Normal file
View File

@ -0,0 +1,3 @@
<article>
<?php $article->render(); ?>
</article>

5
views/tags.phtml Normal file
View File

@ -0,0 +1,5 @@
<ul class="articles">
<?php foreach ($articles as $article): ?>
<li><a href="?view=single&date=<?=$article->date?>&ref=<?=$article->ref?>"><?=$article->title?></a></li>
<?php endforeach ?>
</ul>