First commit with actual files.
parent
1f82cfa839
commit
c1b83f8775
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
];
|
|
@ -0,0 +1,5 @@
|
|||
@charset "utf-8";
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body { font-family: sans-serif; }
|
|
@ -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>
|
|
@ -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";}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
spl_autoload_register(
|
||||
function ($class_name) {
|
||||
require_once "lib/$class_name.php";
|
||||
}
|
||||
);
|
||||
|
||||
$blog = new Blog();
|
||||
$blog->run();
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
final class ArticleDate
|
||||
{
|
||||
|
||||
public string $str;
|
||||
public array $articles = [];
|
||||
|
||||
public function __construct(string $str)
|
||||
{
|
||||
$this->str = $str;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
<aside>
|
||||
<?php
|
||||
include "views/bio.phtml";
|
||||
include "views/nav-by-tag.phtml";
|
||||
include "views/nav-by-latest.phtml";
|
||||
?>
|
||||
</aside>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
<header>
|
||||
<h1><?=$title?></h1>
|
||||
<?php if ($has_sub_title): ?>
|
||||
<h2><?=$sub_title?></h2>
|
||||
<?php endif ?>
|
||||
</header>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<main>
|
||||
<?php include "views/$view.phtml"; ?>
|
||||
</main>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<nav class="tags">
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<a href="?view=tags&tag=<?=$tag?>"><?=$tag?></a>
|
||||
<?php endforeach ?>
|
||||
</nav>
|
|
@ -0,0 +1,3 @@
|
|||
<article>
|
||||
<?php $article->render(); ?>
|
||||
</article>
|
|
@ -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>
|
Loading…
Reference in New Issue