Automatische Markdown-Preview via PHP-Skript für Apache
Unter f.malte70.de habe ich eine öffentliche Ablage für alles Mögliche, z.B. Kochrezepte oder Notizen in Form von Markdown-Dateien. Und für ebendiese gibt es eine automatische Preview, wenn an den Dateinamen ein ?preview
angehängt wird.
Mit der PHP-Bibliothek Parsedown lässt sich dies relativ einfach implementieren. Zusätzlich zum parsen des Markdown-Content extrahieren wir die Überschrift (um diese außerhalb des <main>
-Elements anzuzeigen), und ermitteln die mtime
der Datei (Last Modification Time), welche in einer Fußzeile angezeigt wird.
Markdown-Parser mit Parsedown
Das Github-Repo von Parsedown wurde oberhalb des DocumentRoot geklont. Die Verwendung von Parsedown ist einfach und der folgende Code sollte selbsterklärend sein:
require_once("../parsedown/Parsedown.php");
$filename = @$_GET["file"]
$Parsedown = new Parsedown();
$html = $Parsedown->text(file_get_contents($filename));
Überschrift extrahieren
Nun extrahieren wir die erste Level 1-Überschrift aus dem generierten HTML-Code. Falls der reguläre Ausdruck keine Übereinstimmung liefert, wird stattdessen der Dateiname ohne Suffix als Überschrift verwendet:
// RegEx auf $html anwenden
preg_match(
"'<h1>(.*?)</h1>'si",
$html,
$preg_match_h1
);
if ($preg_match_h1) {
// Übereinstimmung gefunden
$headline = $preg_match_h1[1];
} else {
// Keine Übereinstimmung. Dateiname ohne Suffix als Überschrift nutzen
$headline = str_replace(".md", "", basename($filename));
}
mtime
der Markdown-Datei ermitteln
Mit der PHP-Funktion filemtime()
ermitteln wir die UNIX-Zeit der letzten Dateiänderung. Bevor diese mit strftime()
in ein menschenlesbares Format umgewandelt wird, setzen wir die Locale für Datumsformate, LC_TIME
kurzfristig auf deutsch. Achtung: Die Locale-Einstellungen gelten pro Prozess, nicht pro Thread. Um andere PHP-Skripte auf dem selben Server nicht zu behindern, muss die Locale sofort nach dem Aufruf von strftime()
wieder auf den vorher gespeicherten Wert zurückgesetzt werden.
// Aktuellen Wert von LC_TIME ermitteln
$original_locale = setlocale(LC_TIME, 0);
// Deutsche Locale
setlocale(LC_TIME, "de_DE.utf8");
// Formattierung wie folgt: Do, 26 Okt 2023 13:37:42 +0100
$mtime = strftime(
"%a, %d %b %Y %T %z",
filemtime($filename)
);
// Gespeicherte Locale wieder anwenden
setlocale(LC_TIME, $original_locale);
Fehlerseiten für 404 und 403
Für den Fall, dass die Markdown-Datei nicht existiert oder nicht lesbar ist, müssen wir uns selbst um die Darstellung einer 404er bzw. 403er-Fehlerseite kümmern. Die Pfade in $error_pages
zeigen auf eine unter /srv/http/example.com/error-document/
geklonte Kopie meines Github-Repos malte70/error-document; du kannst natürlich auch deine eigenen Fehlerseiten verwenden.
Wichtig ist, dass wir über die Funktion header()
den HTTP-Status-Code auf 404 bzw. 403 ändern. Folgender Code sollte am Anfang des Skripts stehen, bevor das Parsedown
-Objekt erstellt wird.
$error_pages = Array(
"403" => "/srv/http/example.com/error-document/403.html",
"404" => "/srv/http/example.com/error-document/404.html"
);
if (empty($filename) || !file_exists($filename)) {
// Markdown file not found → Error 404
header("HTTP/1.0 404 Not Found");
die(file_get_contents($error_pages["404"]));
} elseif (!is_readable($filename)) {
// File not readable → Error 403
header("HTTP/1.0 403 Forbidden");
die(file_get_contents($error_pages["403"]));
}
HTML
Es folgt ein einfaches HTML-Grundgerüst, welches auch auf f.malte70.de verwendet wird:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title><?=$headline?></title>
<link rel="shortcut icon" type="image/png" sizes="512x512" href="/assets/img/tango-floppy-512.png">
<link rel="stylesheet" href="/assets/css/markdown.css">
</head>
<body>
<header>
<h1>
<?=$headline?>
</h1>
</header>
<main>
<section id="markdown">
<?=$html?>
</section>
</main>
<footer>
<p>
<a href="<?=$filename?>" download><?=$filename?></a> · Letzte Änderung: <?=$mtime?>
</p>
</footer>
</body>
</html>
Als Favicon wird eine auf 512×512px skalierte PNG-Version des gemeinfreien Floppy-Icons aus dem Tango Icon Theme. Das Stylesheet kann unter xyz.malte70.de/css/markdown.css heruntergeladen und unter den Bedingungen der MIT-Lizenz genutzt werden; es wäre zu lang, um es hier in diesem Beitrag anzugeben.
Das komplette Skript
Hier nocheinmal das komplette Skript markdown_preview.php
:
1<?php
2require_once("../parsedown/Parsedown.php");
3
4$filename = @$_GET["file"]
5
6$error_pages = Array(
7 "403" => "/srv/http/example.com/error-document/403.html",
8 "404" => "/srv/http/example.com/error-document/404.html"
9);
10
11if (empty($filename) || !file_exists($filename)) {
12 // Markdown file not found → Error 404
13 header("HTTP/1.0 404 Not Found");
14 die(file_get_contents($error_pages["404"]));
15
16} elseif (!is_readable($filename)) {
17 // File not readable → Error 403
18 header("HTTP/1.0 403 Forbidden");
19 die(file_get_contents($error_pages["403"]));
20}
21
22$Parsedown = new Parsedown();
23$html = $Parsedown->text(file_get_contents($filename));
24
25// RegEx auf $html anwenden
26preg_match(
27 "'<h1>(.*?)</h1>'si",
28 $html,
29 $preg_match_h1
30);
31if ($preg_match_h1) {
32 // Übereinstimmung gefunden
33 $headline = $preg_match_h1[1];
34} else {
35 // Keine Übereinstimmung. Dateiname ohne Suffix als Überschrift nutzen
36 $headline = str_replace(".md", "", basename($filename));
37}
38
39// Aktuellen Wert von LC_TIME ermitteln
40$original_locale = setlocale(LC_TIME, 0);
41// Deutsche Locale
42setlocale(LC_TIME, "de_DE.utf8");
43// Formattierung wie folgt: Do, 26 Okt 2023 13:37:42 +0100
44$mtime = strftime(
45 "%a, %d %b %Y %T %z",
46 filemtime($filename)
47);
48// Gespeicherte Locale wieder anwenden
49setlocale(LC_TIME, $original_locale);
50
51?><!DOCTYPE html>
52<html lang="en">
53 <head>
54 <meta charset="utf-8">
55 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
56 <title><?=$headline?></title>
57
58 <link rel="shortcut icon" type="image/png" sizes="512x512" href="/assets/img/tango-floppy-512.png">
59 <link rel="stylesheet" href="/assets/css/markdown.css">
60 </head>
61 <body>
62 <header>
63 <h1>
64 <?=$headline?>
65 </h1>
66 </header>
67
68 <main>
69 <section id="markdown">
70<?=$html?>
71 </section>
72 </main>
73
74 <footer>
75 <p>
76 <a href="<?=$filename?>" download><?=$filename?></a> · Letzte Änderung: <?=$mtime?>
77 </p>
78 </footer>
79 </body>
80</html>
mod_rewrite-Regeln
Es fehlt noch eine RewriteRule in der .htaccess
, die filename.md?preview
zu markdown_preview.php?file=filename.md
umleitet:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} ^preview$
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.+)\.md$ markdown_preview.php?file=$1.md [L]
</IfModule>