]> git.bts.cx Git - garden.git/commitdiff
Initial commit main
authorBen Sherratt <redacted>
Sat, 27 Dec 2025 01:38:48 +0000 (01:38 +0000)
committerBen Sherratt <redacted>
Sat, 27 Dec 2025 01:50:19 +0000 (01:50 +0000)
.gitmodules [new file with mode: 0644]
config.php [new file with mode: 0644]
garden/garden.php [new file with mode: 0644]
garden/third_party/parsedown [new submodule]
templates/article.php [new file with mode: 0644]
templates/base.php [new file with mode: 0644]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..841e07a
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "garden/third_party/parsedown"]
+       path = garden/third_party/parsedown
+       url = https://github.com/erusev/parsedown.git
diff --git a/config.php b/config.php
new file mode 100644 (file)
index 0000000..36a3931
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+define('GARDEN_SITE_BASE_URL', 'https://bts.cx');
+
+define('GARDEN_CONTENT_DIR', garden_path(__DIR__, 'content'));
+define('GARDEN_TEMPLATE_DIR', garden_path(__DIR__, 'templates'));
+
+define('GARDEN_OUTPUT_DIR', garden_path(__DIR__, 'public'));
+
+
+
+// Put anything here you want the templates to be able to see...
+
+define('site_name', 'bts.cx');
diff --git a/garden/garden.php b/garden/garden.php
new file mode 100644 (file)
index 0000000..e73804a
--- /dev/null
@@ -0,0 +1,455 @@
+<?php
+
+/*
+   _____               _               _____ _ _          _____                           _             
+  / ____|             | |             / ____(_) |        / ____|                         | |            
+ | |  __  __ _ _ __ __| | ___ _ __   | (___  _| |_ ___  | |  __  ___ _ __   ___ _ __ __ _| |_ ___  _ __ 
+ | | |_ |/ _` | '__/ _` |/ _ \ '_ \   \___ \| | __/ _ \ | | |_ |/ _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__|
+ | |__| | (_| | | | (_| |  __/ | | |  ____) | | ||  __/ | |__| |  __/ | | |  __/ | | (_| | || (_) | |   
+  \_____|\__,_|_|  \__,_|\___|_| |_| |_____/|_|\__\___|  \_____|\___|_| |_|\___|_|  \__,_|\__\___/|_|
+
+ */
+
+function output_debug(...$str_segments) {
+       $str = join('', $str_segments);
+       echo($str . PHP_EOL);
+}
+
+function output_error(...$str_segments) {
+       $str = join('', $str_segments);
+       die($str);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Paths
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_path(...$path_segments) {
+       $segments = array();
+       foreach ($path_segments as $path_segment) {
+               $inner_segments = explode(DIRECTORY_SEPARATOR, $path_segment);
+               foreach ($inner_segments as $inner_segment) {
+                       if ($inner_segment != '') {
+                               $segments[] = $inner_segment;
+                       }
+               }
+       }
+       array_unshift($segments, '');
+       return join(DIRECTORY_SEPARATOR, $segments);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// String
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_slug($str) {
+       $str = preg_replace('/\s+/', '-', $str);
+       $str = preg_replace('/\-+/', '-', $str);
+       $str = preg_replace('/[^\w\s\-\.]+/', '', $str);
+       $str = strtolower($str);
+       return $str;
+}
+
+function garden_url(...$url_segments) {
+       $segments = array();
+       foreach ($url_segments as $url_segment) {
+               $inner_segments = explode(DIRECTORY_SEPARATOR, $url_segment);
+               foreach ($inner_segments as $inner_segment) {
+                       if ($inner_segment != '') {
+                               $segments[] = garden_slug($inner_segment);
+                       }
+               }
+       }
+       array_unshift($segments, '');
+       return join(DIRECTORY_SEPARATOR, $segments);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// I/O
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_read_file($filename) {
+       return file_get_contents($filename);
+}
+
+function garden_write_file($filename, $content) {
+       $utf8_bom = "\xEF\xBB\xBF";
+       file_put_contents($filename, $utf8_bom . $content);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Items to process
+///////////////////////////////////////////////////////////////////////////////
+
+enum GardenItemType {
+       case Article;
+       case Image;
+       case Raw;
+}
+
+class GardenItem {
+       public $path;
+       public $type;
+       public $id;
+       public $title;
+       public $url;
+       public $date;
+       public $source_filename;
+       public $target_filename;
+
+       public function __construct($path, $type, $id, $title, $url, $date, $source_filename, $target_filename) {
+               $this->path = $path;
+               $this->type = $type;
+               $this->id = $id;
+               $this->title = $title;
+               $this->url = $url;
+               $this->date = $date;
+               $this->source_filename = $source_filename;
+               $this->target_filename = $target_filename;
+       }
+}
+
+function garden_make_process_items($output_directory, $content_paths) {
+       $output_items = [];
+
+       foreach ($content_paths as $item) {
+               $id = garden_slug($item->filename);
+
+               $target_extension = $item->extension;
+
+               $ignore_file = false;
+               $type = GardenItemType::Raw;
+               switch ($item->extension) {
+                       case 'php':
+                               $ignore_file = true;
+                               break;
+
+                       case 'md':
+                               $type = GardenItemType::Article;
+                               $target_extension = 'html';
+                               break;
+
+                       case 'png':
+                       case 'jpg':
+                       case 'jpeg':
+                       case 'gif':
+                               $type = GardenItemType::Image;
+                               break;
+               }
+
+               if ($ignore_file == true) {
+                       continue;
+               }
+
+               $target_basename = $item->filename;
+               if ($target_extension != '') {
+                       $target_basename .= '.' . $target_extension;
+               }
+
+               $url = GARDEN_SITE_BASE_URL . garden_url($item->path, $target_basename);
+               $target_path = garden_path($output_directory, garden_url($item->path, $target_basename));
+
+               $date = filemtime($item->full_path);
+
+               $output_items[] = new GardenItem($item, $type, $id, $item->filename, $url, $date, $item->full_path, $target_path);
+       }
+
+       return $output_items;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Paths
+///////////////////////////////////////////////////////////////////////////////
+
+class GardenContentPath {
+       public $full_path;
+       public $root;
+       public $path;
+       public $basename;
+       public $filename;
+       public $extension;
+
+       public function __construct($full_path, $root, $path, $path_parts) {
+               $this->full_path = $full_path;
+               $this->root = $root;
+               $this->path = $path;
+               $this->basename = $path_parts['basename'];
+               $this->filename = $path_parts['filename'];
+               $this->extension = $path_parts['extension'];
+       }
+}
+
+function garden_find_content_files($content_dir) {
+       $content_dir = realpath($content_dir);
+
+       $scan_paths = [''];
+       $output_paths = [];
+
+       while (count($scan_paths) > 0) {
+               $scan_path = array_shift($scan_paths);
+               $path_contents = scandir(garden_path($content_dir, $scan_path));
+               foreach ($path_contents as $item) {
+                       if (str_starts_with($item, '.')) {
+                               continue;
+                       }
+
+                       $full_path = garden_path($content_dir, $scan_path, $item);
+                       if (is_dir($full_path)) {
+                               $scan_paths[] = garden_path($scan_path, $item);
+                               continue;
+                       }
+
+                       $path_parts = pathinfo($full_path);
+                       $output_paths[] = new GardenContentPath($full_path, $content_dir, $scan_path, $path_parts);
+               }
+       }
+
+       return $output_paths;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Directories
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_make_directories($content_items) {
+       foreach ($content_items as $item) {
+               $path_parts = pathinfo($item->target_filename);
+
+               $directory = $path_parts['dirname'];
+               if (is_dir($directory ) == false) {
+                       mkdir($directory , 0777, true); // FIXME, permissions...
+               }
+       }
+}
+
+function garden_move_raw($content_items) {
+       foreach ($content_items as $item) {
+               if ($item->type != GardenItemType::Raw && $item->type != GardenItemType::Image) {
+                       continue;
+               }
+
+               $success = copy($item->source_filename, $item->target_filename);
+               if ($success != true) {
+                       error('Failed to copy file: filename="', $item->source_filename, '"');
+               }
+       }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Template
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_template_render($name, $variables = null) {
+       global $garden_template_base, $garden_template_content;
+       
+       $base_template = null;
+
+       $output = '';
+       while ($name != null) {
+               $path = garden_path(GARDEN_TEMPLATE_DIR, $name . '.php');
+               
+               $base_template = null;
+               $base_template_variables = null;
+
+               $garden_template_base_previous = $garden_template_base;
+               $garden_template_base = function($name, $base_variables) use (&$base_template, &$base_template_variables) {
+                       $base_template = $name;
+                       $base_template_variables = $base_variables;
+               };
+
+               $garden_template_content_previous = $garden_template_content;
+               $garden_template_content = function() use ($output) {
+                       return $output;
+               };
+
+               if ($variables != null) {
+                       extract($variables);
+               }
+
+               ob_start();
+               include($path);
+               $output = ob_get_contents();
+               ob_end_clean();
+
+               $garden_template_base = $garden_template_base_previous;
+               $garden_template_content = $garden_template_content_previous;
+
+               $name = $base_template;
+               $variables = $base_template_variables;
+       }
+
+       return $output;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Template helpers
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_template_base($name, $variables = null) {
+       global $garden_template_base;
+       $garden_template_base($name, $variables);
+}
+
+function garden_template_content() {
+       global $garden_template_content;
+       return $garden_template_content();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// HTML
+///////////////////////////////////////////////////////////////////////////////
+
+require_once(garden_path(__DIR__, 'third_party', 'parsedown', 'Parsedown.php'));
+
+class GardenExtendedParsedown extends Parsedown {
+       private $content_items;
+
+       public function __construct($content_items) {
+               $this->content_items = $content_items;
+               $this->InlineTypes['!'][] = 'Youtube';
+               $this->InlineTypes['!'][] = 'Image';
+               $this->InlineTypes['['][] = 'WikiLinks';
+               $this->inlineMarkerList .= '!';
+               $this->inlineMarkerList .= '[';
+       }
+
+       protected function inlineImage($excerpt) {
+               if (preg_match('/^!\[\[([^\|\]]+)(\|([0-9]+)x([0-9]+))?\]\]/', $excerpt['text'], $matches)) {
+                       $image_name = $matches[1];
+                       $image_width = count($matches) == 5 ? $matches[3] : null;
+                       $image_height = count($matches) == 5 ? $matches[4] : null;
+
+                       if ($image_name == null) {
+                               return;
+                       }
+
+                       $target_url = null;
+                       foreach ($this->content_items as $content_item) {
+                               if ($content_item->path->basename == $image_name) {
+                                       $target_url = $content_item->url;
+                                       break;
+                               }
+                       }
+
+                       if ($target_url == null) {
+                               return;
+                       }
+
+                       return array(
+                               'extent' => strlen($matches[0]), 
+                               'element' => array(
+                                       'name' => 'img',
+                                       'text' => '',
+                                       'attributes' => array(
+                                               'src' => $target_url,
+                                       ),
+                               ),
+                       );
+               }
+       }
+
+       protected function inlineYoutube($excerpt) {
+               if (preg_match('/^!\[\[yt:([^\]]+)\]\]/', $excerpt['text'], $matches)) {
+                       $video_id = $matches[1];
+
+                       if ($video_id == null) {
+                               return;
+                       }
+
+                       return array(
+                               'extent' => strlen($matches[0]), 
+                               'element' => array(
+                                       'name' => 'iframe',
+                                       'text' => '',
+                                       'attributes' => array(
+                                               'class' => "video",
+                                               'type' => "text/html",
+                                               //'width' => "640",
+                                               //'height' => "360",
+                                               'src' => "https://www.youtube.com/embed/" . $video_id,
+                                               'frameborder' => "0",
+                                               'loading' => "lazy",
+                                               'referrerpolicy' => "no-referrer",
+                                               'sandbox' => "allow-same-origin allow-scripts",
+                                       ),
+                               ),
+                       );
+               }
+       }
+
+       protected function inlineWikiLinks($excerpt) {
+               if (preg_match('/^\[\[(.+)\]\]/', $excerpt['text'], $matches)) {
+                       $target_title = $matches[1];
+
+                       if ($target_title == null) {
+                               return;
+                       }
+
+                       $target_url = null;
+                       foreach ($this->content_items as $content_item) {
+                               if ($content_item->title == $target_title) {
+                                       $target_url = $content_item->url;
+                                       break;
+                               }
+                       }
+
+                       if ($target_url == null) {
+                               return;
+                       }
+
+                       return array(
+                               'extent' => strlen($matches[0]), 
+                               'element' => array(
+                                       'name' => 'a',
+                                       'text' => $target_title,
+                                       'attributes' => array(
+                                               'href' => $target_url,
+                                       ),
+                               ),
+                       );
+               }
+       }
+}
+
+function garden_generate_html($content_items) {
+       $parsedown = new GardenExtendedParsedown($content_items);
+
+       foreach ($content_items as $item) {
+               if ($item->type != GardenItemType::Article) {
+                       continue;
+               }
+
+               $markdown_data = garden_read_file($item->source_filename);
+               $markdown_html = $parsedown->text($markdown_data);
+               
+               $html_data = garden_template_render('article', [ 'article' => $item, 'article_content' => $markdown_html ]);
+
+               garden_write_file($item->target_filename, $html_data);
+       }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Main
+///////////////////////////////////////////////////////////////////////////////
+
+function garden() {
+       $content_files = garden_find_content_files(GARDEN_CONTENT_DIR);
+       array_push($content_files, ...garden_find_content_files(GARDEN_TEMPLATE_DIR));
+       $process_items = garden_make_process_items(GARDEN_OUTPUT_DIR, $content_files);
+       garden_make_directories($process_items);
+       garden_move_raw($process_items);
+       garden_generate_html($process_items);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Main code!
+///////////////////////////////////////////////////////////////////////////////
+
+assert($argc >= 2);
+
+// First parameter needs to be the configuration php
+$config_file = $argv[1];
+output_debug("Will use configuration: file='", $config_file, "'");
+require_once($config_file);
+
+garden();
diff --git a/garden/third_party/parsedown b/garden/third_party/parsedown
new file mode 160000 (submodule)
index 0000000..0b274ac
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 0b274ac959624e6c6d647e9c9b6c2d20da242004
diff --git a/templates/article.php b/templates/article.php
new file mode 100644 (file)
index 0000000..9c5d098
--- /dev/null
@@ -0,0 +1,9 @@
+<?php garden_template_base('base'); ?>
+
+<main>
+       <article>
+               <h1><a href="<?= $article->url ?>"><?= $article->title ?></a></h1>
+               <p class="updated"><?= date('l, F jS, Y', $article->date) ?></p>
+               <?= $article_content ?>
+       </article>
+</main>
diff --git a/templates/base.php b/templates/base.php
new file mode 100644 (file)
index 0000000..83217f2
--- /dev/null
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+
+<html lang="en">
+       <head>
+               <meta charset="UTF-8">
+               <meta http-equiv="X-UA-Compatible" content="IE=edge">
+               <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+               <title><?= site_name ?> - <?= $article->title; ?></title>
+       </head>
+
+       <body>
+               <header>
+                       <p>Find me <a href="https://mastodon.gamedev.place/@btsherratt" rel="me">@btsherratt</a> or on <a href="https://bts.itch.io/">itch.io</a></p>
+               </header>
+
+               <?= garden_template_content(); ?>
+
+               <footer>
+                       <div>
+                       </div>
+               </footer>
+       </body>
+</html>