First commit!
commit
92a272c03d
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,16 @@
|
|||
<?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("BLOG_LINK", "https://example.org/");
|
||||
define("AUTHOR_NICKNAME", "blog author's nickname");
|
||||
define("AUTHOR_REALNAME", "blog author's real name - optional, leave blank if none");
|
||||
define("AUTHOR_EMAIL", "blog author's email address - optional, leave blank if none");
|
||||
define("AUTHOR_BIOGRAPHY", "blog author's 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,119 @@
|
|||
<?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) 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;
|
||||
}
|
||||
}
|
|
@ -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,117 @@
|
|||
<?php
|
||||
|
||||
final class Blog
|
||||
{
|
||||
|
||||
private array $links = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class Entry
|
||||
{
|
||||
|
||||
public string $title;
|
||||
public string $content;
|
||||
public string $date;
|
||||
public string $url;
|
||||
public string $id;
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
final class EntryParser
|
||||
{
|
||||
|
||||
const BASE_PATH = "./data/cache/uuids/";
|
||||
|
||||
public static function parse(Post $post, string $link): Entry
|
||||
{
|
||||
$entry = new Entry();
|
||||
$entry->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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
final class Feed
|
||||
{
|
||||
|
||||
const BASE_PATH = "./data/cache/";
|
||||
const POST_COUNT = 10;
|
||||
|
||||
private string $type;
|
||||
|
||||
public function __construct(string $type = "rss")
|
||||
{
|
||||
$this->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];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
final class FeedManager
|
||||
{
|
||||
|
||||
const BASE_PATH = "./data/articles";
|
||||
|
||||
public static function getLatestPosts(int $count): array
|
||||
{
|
||||
$dot_dirs = [".", ".."];
|
||||
$posts = [];
|
||||
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;
|
||||
}
|
||||
$posts[] = FeedParser::parse($file_path);
|
||||
if (count($posts) === $count) {
|
||||
return $posts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $posts;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
final class FeedParser
|
||||
{
|
||||
|
||||
public static function parse(string $file_path): Post
|
||||
{
|
||||
$post = new Post();
|
||||
$post->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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class Item
|
||||
{
|
||||
|
||||
public string $title;
|
||||
public string $description;
|
||||
public string $pub_date;
|
||||
public string $url;
|
||||
public array $categories = [];
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
final class ItemParser
|
||||
{
|
||||
|
||||
public static function parse(Post $post, string $link): Item
|
||||
{
|
||||
$item = new Item();
|
||||
$item->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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,12 @@
|
|||
<?php
|
||||
|
||||
final class Post
|
||||
{
|
||||
|
||||
public string $title;
|
||||
public string $content;
|
||||
public int $date;
|
||||
public string $id;
|
||||
public array $tags;
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?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) {
|
||||
$tagfile = self::BASE_PATH . "/$tag";
|
||||
if (is_file($tagfile) and strpos(file_get_contents($tagfile), $fullref) !== false) {
|
||||
continue;
|
||||
}
|
||||
file_put_contents($tagfile, "$fullref\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
final class UUIDGenerator
|
||||
{
|
||||
|
||||
public static function generate(): string
|
||||
{
|
||||
return "urn:uuid:" . sprintf(
|
||||
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff) | 0x4000,
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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,8 @@
|
|||
<aside>
|
||||
<?php
|
||||
include "views/bio.phtml";
|
||||
include "views/links.phtml";
|
||||
include "views/nav-by-tag.phtml";
|
||||
include "views/nav-by-latest.phtml";
|
||||
?>
|
||||
</aside>
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title><?=$title?></title>
|
||||
<?php if ($has_sub_title): ?>
|
||||
<subtitle><?=$sub_title?></subtitle>
|
||||
<?php endif; ?>
|
||||
<link href="<?=$url?>" rel="alternate" type="text/html" />
|
||||
<link href="<?=$self?>" rel="self" type="application/atom+xml" />
|
||||
<updated><?=$updated?></updated>
|
||||
<author>
|
||||
<name><?=$author_name?></name>
|
||||
<?php if ($has_author_email): ?>
|
||||
<email><?=$author_email?></email>
|
||||
<?php endif; ?>
|
||||
</author>
|
||||
<id><?=$id?></id>
|
||||
<?php foreach ($entries as $entry): ?>
|
||||
<entry>
|
||||
<title><?=$entry->title?></title>
|
||||
<link href="<?=$entry->url?>" />
|
||||
<id><?=$entry->id?></id>
|
||||
<updated><?=$entry->date?></updated>
|
||||
<content type="html"><![CDATA[<?=$entry->content?>]]></content>
|
||||
</entry>
|
||||
<?php endforeach; ?>
|
||||
</feed>
|
|
@ -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,3 @@
|
|||
<footer>
|
||||
<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,18 @@
|
|||
<!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">
|
||||
<link href="rss.php" rel="alternate" title="<?=$title?> RSS feed" type="application/rss+xml">
|
||||
<link href="atom.php" rel="alternate" title="<?=$title?> Atom feed" type="application/atom+xml">
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
include "views/header.phtml";
|
||||
include "views/main.phtml";
|
||||
include "views/aside.phtml";
|
||||
include "views/footer.phtml";
|
||||
?>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,5 @@
|
|||
<ul class="links">
|
||||
<?php foreach ($links as $link): ?>
|
||||
<li class="icon"><a rel="me" href="<?=$link->url?>" class="icon <?=$link->title?>"><?=$link->title?></a></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
|
@ -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,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<atom:link href="<?=$self?>" rel="self" type="application/rss+xml" />
|
||||
<title><?=$title?></title>
|
||||
<?php if ($has_sub_title): ?>
|
||||
<description><?=$sub_title?></description>
|
||||
<?php endif; ?>
|
||||
<lastBuildDate><?=$last_build_date?></lastBuildDate>
|
||||
<link><?=$link?></link>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<item>
|
||||
<title><?=$item->title?></title>
|
||||
<description><![CDATA[<?=$item->description?>]]></description>
|
||||
<pubDate><?=$item->pub_date?></pubDate>
|
||||
<link><?=$item->url?></link>
|
||||
<?php if ($has_author_email): ?>
|
||||
<author><?="$author_email ($author_name)"?></author>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($item->categories as $category): ?>
|
||||
<category><?=$category?></category>
|
||||
<?php endforeach; ?>
|
||||
</item>
|
||||
<?php endforeach; ?>
|
||||
</channel>
|
||||
</rss>
|
|
@ -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