]> git.bts.cx Git - garden.git/commitdiff
Initial release
authorBen Sherratt <redacted>
Sat, 27 Dec 2025 10:54:05 +0000 (10:54 +0000)
committerBen Sherratt <redacted>
Sun, 28 Dec 2025 02:19:26 +0000 (02:19 +0000)
13 files changed:
.gitignore [new file with mode: 0644]
.gitmodules
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
config.php
garden.php [new file with mode: 0644]
garden/garden.php [deleted file]
garden/third_party/parsedown [deleted submodule]
templates/article.php
templates/atom.php [new file with mode: 0644]
templates/base.php
templates/style/screen.css [new file with mode: 0644]
third_party/parsedown [new submodule]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..bc300b5
--- /dev/null
@@ -0,0 +1,2 @@
+content
+public
index 841e07af33882c7167e110cbd39785c1ca2042bd..61f93306f31598594b257904d64827ef73280edf 100644 (file)
@@ -1,3 +1,3 @@
-[submodule "garden/third_party/parsedown"]
-       path = garden/third_party/parsedown
+[submodule "third_party/parsedown"]
+       path = third_party/parsedown
        url = https://github.com/erusev/parsedown.git
        url = https://github.com/erusev/parsedown.git
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..ae51da0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+site:
+       php garden.php config.php
+
+serve:
+       php -S localhost:8080 -t public
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..597a29b
--- /dev/null
+++ b/README
@@ -0,0 +1,6 @@
+Garden site generator
+
+To use this...
+ - Copy your content into this folder (or somewher you can access it)
+ - Refer to config.php to set up your configuration
+ - Refer to Makefile as to how to run the tool
index 36a3931a64eac845acafb6a1821329aeadfc4c13..9ea6b10cc370b84fd5b57a4291a69531a1bfb0a5 100644 (file)
@@ -1,14 +1,27 @@
 <?php
 
 <?php
 
-define('GARDEN_SITE_BASE_URL', 'https://bts.cx');
+// What is the URL of this website? (No trailing slash...)
+define('GARDEN_SITE_BASE_URL', 'http://localhost:8080');
 
 
+// Where is the markdown and other files that contain the content?
 define('GARDEN_CONTENT_DIR', garden_path(__DIR__, 'content'));
 define('GARDEN_CONTENT_DIR', garden_path(__DIR__, 'content'));
+
+// Where are the templates?
 define('GARDEN_TEMPLATE_DIR', garden_path(__DIR__, 'templates'));
 
 define('GARDEN_TEMPLATE_DIR', garden_path(__DIR__, 'templates'));
 
+// Where do you want to write the website to?
 define('GARDEN_OUTPUT_DIR', garden_path(__DIR__, 'public'));
 
 define('GARDEN_OUTPUT_DIR', garden_path(__DIR__, 'public'));
 
-
-
 // Put anything here you want the templates to be able to see...
 // Put anything here you want the templates to be able to see...
+define('GARDEN_TEMPLATE_CONSTANTS', [
+       'site_name' => 'garden',
+       'site_language' => 'en',
+       'site_copyline' => 'Content &copy; Mr Fox',
+       'site_author' => 'Mr Fox',
+
+       'mastodon_link' => 'https://mastodon.website/@me',
+       'mastodon_handle' => '@me',
 
 
-define('site_name', 'bts.cx');
+       'itch_link' => 'https://itch.io',
+       'itch_handle' => '@???.itch.io',
+]);
diff --git a/garden.php b/garden.php
new file mode 100644 (file)
index 0000000..e4391e4
--- /dev/null
@@ -0,0 +1,574 @@
+<?php
+
+/*
+   _____               _               _____ _ _          _____                           _             
+  / ____|             | |             / ____(_) |        / ____|                         | |            
+ | |  __  __ _ _ __ __| | ___ _ __   | (___  _| |_ ___  | |  __  ___ _ __   ___ _ __ __ _| |_ ___  _ __ 
+ | | |_ |/ _` | '__/ _` |/ _ \ '_ \   \___ \| | __/ _ \ | | |_ |/ _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__|
+ | |__| | (_| | | | (_| |  __/ | | |  ____) | | ||  __/ | |__| |  __/ | | |  __/ | | (_| | || (_) | |   
+  \_____|\__,_|_|  \__,_|\___|_| |_| |_____/|_|\__\___|  \_____|\___|_| |_|\___|_|  \__,_|\__\___/|_|
+
+ */
+
+///////////////////////////////////////////////////////////////////////////////
+// Logging
+///////////////////////////////////////////////////////////////////////////////
+
+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 $category;
+       public $url;
+       public $date;
+       public $source_filename;
+       public $target_filename;
+
+       public function __construct($path, $type, $id, $title, $category, $url, $date, $source_filename, $target_filename) {
+               $this->path = $path;
+               $this->type = $type;
+               $this->id = $id;
+               $this->title = $title;
+               $this->category = $category;
+               $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;
+               }
+
+               $category_components = explode(DIRECTORY_SEPARATOR, $item->path);
+               $category = count($category_components) >= 2 ? $category_components[1] : "";
+
+               $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, $category, $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...
+               }
+       }
+
+       return $content_items;
+}
+
+function garden_move_raw($content_items) {
+       foreach ($content_items as $item) {
+               if ($item->type != GardenItemType::Raw && $item->type != GardenItemType::Image) { // FIXME, do we need to copy images?
+                       continue;
+               }
+
+               $success = copy($item->source_filename, $item->target_filename);
+               if ($success != true) {
+                       error('Failed to copy file: filename="', $item->source_filename, '"');
+               }
+       }
+
+       return $content_items;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// 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();
+}
+
+function garden_site_url(...$url_segments) {
+       return GARDEN_SITE_BASE_URL . garden_url(...$url_segments);
+}
+
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Images
+///////////////////////////////////////////////////////////////////////////////
+
+class GardenImage {
+       public $content_item;
+       public $width;
+       public $height;
+       public $url;
+       public $target_filename;
+       
+       public function __construct($content_item, $width, $height, $url, $target_filename) {
+               $this->content_item = $content_item;
+               $this->width = $width;
+               $this->height = $height;
+               $this->url = $url;
+               $this->target_filename = $target_filename;
+       }
+}
+
+function garden_make_images($post_process_items) {
+       foreach ($post_process_items as $item) {
+               $image = new Imagick($item->content_item->source_filename);
+               $image->thumbnailImage($item->width, $item->height);
+               $image->writeImage($item->target_filename);
+       }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// HTML
+///////////////////////////////////////////////////////////////////////////////
+
+require_once(garden_path(__DIR__, 'third_party', 'parsedown', 'Parsedown.php'));
+
+class GardenExtendedParsedown extends Parsedown {
+       public $post_process_items;
+       private $output_directory;
+       private $content_items;
+
+       public function __construct($output_directory, $content_items) {
+               $this->post_process_items = [];
+               $this->output_directory = $output_directory;
+               $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;
+                       }
+
+                       $image_content_item = null;
+                       foreach ($this->content_items as $content_item) {
+                               if ($content_item->path->basename == $image_name) {
+                                       $image_content_item = $content_item;
+                                       break;
+                               }
+                       }
+
+                       if ($image_content_item == null) {
+                               return;
+                       }
+
+                       $target_url = $image_content_item->url;
+                       if ($image_width != null && $image_height != null) {
+                               $original_path = $image_content_item->path;
+
+                               $target_basename = $original_path->filename . '_' . $image_width . 'x' . $image_height . '.png';
+
+                               $target_url = GARDEN_SITE_BASE_URL . garden_url($original_path->path, $target_basename);
+                               $target_path = garden_path($this->output_directory, garden_url($original_path->path, $target_basename));
+
+                               $post_process_item = new GardenImage($image_content_item, $image_width, $image_height, $target_url, $target_path);
+                               $this->post_process_items[] = $post_process_item;
+                       }
+
+                       return array(
+                               'extent' => strlen($matches[0]), 
+                               'element' => array(
+                                       'name' => 'img',
+                                       '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($output_directory, $content_items) {
+       $output_items = [];
+
+       $categorised_items = [];
+       foreach ($content_items as $item) {
+               if ($item->type != GardenItemType::Article) {
+                       continue;
+               }
+
+               if (isset($categorised_items[$item->category]) == false) {
+                       $categorised_items[$item->category] = [];
+               }
+               $categorised_items[$item->category][] = $item;
+       }
+
+       foreach ($content_items as $item) {
+               if ($item->type != GardenItemType::Article) {
+                       continue;
+               }
+
+               $markdown_data = garden_read_file($item->source_filename);
+
+               $parsedown = new GardenExtendedParsedown($output_directory, $content_items);
+               $markdown_html = $parsedown->text($markdown_data);
+
+               if (count($parsedown->post_process_items) > 0) {
+                       array_push($output_items, ...$parsedown->post_process_items);
+               }
+
+               $variables = GARDEN_TEMPLATE_CONSTANTS; // PHP will copy by default!
+               $variables['article'] =  $item;
+               $variables['article_content'] =  $markdown_html;
+               $variables['categories'] =  $categorised_items;
+
+               $html_data = garden_template_render('article', $variables);
+
+               garden_write_file($item->target_filename, $html_data);
+       }
+
+       return $output_items;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Feed
+///////////////////////////////////////////////////////////////////////////////
+
+function garden_generate_atom($output_directory, $content_items) {
+       $output_items = [];
+
+       $recent_items = [];
+       foreach ($content_items as $item) {
+               if ($item->type != GardenItemType::Article) {
+                       continue;
+               }
+
+               $recent_items[$item->date] = $item;
+       }
+
+       // Sort by date...
+       ksort($recent_items);
+       $recent_items = array_reverse($recent_items);
+
+       $variables = GARDEN_TEMPLATE_CONSTANTS; // PHP will copy by default!
+       $variables['recent_items'] =  $recent_items;
+       $html_data = garden_template_render('atom', $variables);
+
+       garden_write_file(garden_path($output_directory, 'feed.atom'), $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);
+       $post_process_items = garden_generate_html(GARDEN_OUTPUT_DIR, $process_items);
+       garden_make_images($post_process_items);
+       garden_generate_atom(GARDEN_OUTPUT_DIR, $process_items);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Main code!
+///////////////////////////////////////////////////////////////////////////////
+
+assert(extension_loaded('imagick'), 'Needs Imagick');
+assert($argc >= 2, 'Please provide configuration file');
+
+// 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/garden.php b/garden/garden.php
deleted file mode 100644 (file)
index e73804a..0000000
+++ /dev/null
@@ -1,455 +0,0 @@
-<?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
deleted file mode 160000 (submodule)
index 0b274ac..0000000
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0b274ac959624e6c6d647e9c9b6c2d20da242004
index 9c5d0988dbbdedcf580355bea9c858e25ac91496..2b10babd02dec2766a04892336ca6fa68a62e59d 100644 (file)
@@ -3,7 +3,7 @@
 <main>
        <article>
                <h1><a href="<?= $article->url ?>"><?= $article->title ?></a></h1>
 <main>
        <article>
                <h1><a href="<?= $article->url ?>"><?= $article->title ?></a></h1>
-               <p class="updated"><?= date('l, F jS, Y', $article->date) ?></p>
+               <p class="updated">Last updated: <?= date('l, F jS, Y', $article->date) ?></p>
                <?= $article_content ?>
        </article>
 </main>
                <?= $article_content ?>
        </article>
 </main>
diff --git a/templates/atom.php b/templates/atom.php
new file mode 100644 (file)
index 0000000..5335752
--- /dev/null
@@ -0,0 +1,31 @@
+<?= '<?xml version="1.0" encoding="utf-8"?>'."\n" /* So some installs of PHP don't get confused... */ ?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+       <id><?= garden_site_url('/'); ?>/</id>
+
+       <title><?= $site_name; ?></title>
+
+       <updated><?= date(DATE_RFC3339, $recent_items[0]->date) ?></updated>
+
+       <link href="<?= garden_site_url('feed.atom'); ?>" rel="self" type="application/atom+xml" />
+       <link href="<?= garden_site_url('/'); ?>" rel="alternate" type="text/html" />
+
+       <author>
+               <name><?= $site_author ?></name>
+       </author>
+
+<?php foreach ($recent_items as $update): ?>
+       <entry>
+               <id><?= $update->url ?></id>
+               <title><?= $update->title ?></title>
+               <updated><?= date(DATE_RFC3339, $update->date) ?></updated>
+               <link href="<?= $update->url ?>" rel="alternate" type="text/html" />
+               <summary>An update was published.</summary>
+               <content type="xhtml">
+                       <div xmlns="http://www.w3.org/1999/xhtml">
+                               An update was published.
+                       </div>
+               </content>
+       </entry>
+<?php endforeach; ?>
+
+</feed>
index 83217f2769b29a87d540d9b9cd576f041c0394d7..76d6d0081e24b154860f8903484802b6521c65e8 100644 (file)
@@ -1,24 +1,52 @@
 <!DOCTYPE html>
 
 <!DOCTYPE html>
 
-<html lang="en">
+<html lang="<?= $site_language ?>">
        <head>
        <head>
+               <?php if (isset($article)): ?>
+               <title><?= $site_name ?> - <?= $article->title ?></title>
+               <?php else: ?>
+               <title><?= $site_name ?></title>
+               <?php endif ?>
+
                <meta charset="UTF-8">
                <meta http-equiv="X-UA-Compatible" content="IE=edge">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
                <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>
+               <link rel="stylesheet" href="<?= garden_site_url('style', 'screen.css') ?>">
+               <link rel="alternate" type="application/atom+xml" href="<?= garden_site_url('feed.atom') ?>">
        </head>
 
        <body>
        </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>
+               <div class="container">
+                       <header>
+                               <h1><a href="<?= garden_site_url('/') ?>"><?= $site_name ?></a></h1>
+                               <p>Find me <a href="<?= $mastodon_link ?>" rel="me"><?= $mastodon_handle ?></a> or <a href="<?= $itch_link ?>"><?= $itch_handle ?></a></p>
+                       </header>
+
+                       <nav>
+                               <ul>
+                               <?php foreach ($categories as $category_name => $category_items): ?>
+                                       <?php if ($category_name != ''): ?>
+                                       <li>
+                                               <?= $category_name ?>
+                                               <ul>
+                                                       <?php foreach ($category_items as $category_item): ?>
+                                                               <li><a href="<?= $category_item->url ?>"><?= $category_item->title ?></a></li>
+                                                       <?php endforeach; ?>
+                                               </ul>
+                                       </li>
+                                       <?php endif ?>
+                               <?php endforeach ?>
+                               </ul>
+                       </nav>
 
 
-               <?= garden_template_content(); ?>
+                       <?= garden_template_content(); ?>
 
 
-               <footer>
-                       <div>
-                       </div>
-               </footer>
+                       <footer>
+                               <div>
+                                       <?= $site_copyline ?>
+                               </div>
+                       </footer>
+               </div>
        </body>
 </html>
        </body>
 </html>
diff --git a/templates/style/screen.css b/templates/style/screen.css
new file mode 100644 (file)
index 0000000..cb1176c
--- /dev/null
@@ -0,0 +1,132 @@
+html {
+       line-height: 1.15;
+       -webkit-text-size-adjust:100%
+}
+
+body {
+       background-color: rgba(255, 255, 255);
+       font-family: -apple-system, "BlinkMacSystemFont", "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+       margin: 0;
+       padding: 0;
+}
+
+.container {
+       display: grid;
+       grid-template:
+               "head head" 7em
+               "nav main" auto
+               "nav foot" 2em / 15em 1fr;
+       margin: 0 auto;
+       min-height: 100vh;
+       padding: 0;
+       width: 60em;
+}
+
+header {
+       grid-area: head;
+}
+
+header h1 {
+       font-size: 2em;
+}
+
+nav {
+       background-color: rgb(250, 250, 250);
+       grid-area: nav;
+       padding: 1em;
+}
+
+nav > ul {
+       list-style-type: none;
+       padding: 0;
+}
+
+nav > ul > li > ul {
+       list-style-type: square;
+}
+
+main {
+       grid-area: main;
+       padding: 1em;
+}
+
+footer {
+       font-size: 0.6em;
+       grid-area: foot;
+       padding: 1em;
+       text-align: right;
+}
+
+img {
+       max-width: 100%;
+       margin: 0 auto;
+       max-height: 75vh;
+       border-radius:4px
+}
+
+a {
+       border-bottom: 1px solid rgb(217, 217, 217);
+       color: rgb(26, 26, 26);
+       text-decoration: none;
+       padding: 0 0.1em;
+}
+
+a:hover {
+       background: rgb(255, 250, 241);
+       color:  #000 !important;
+}
+
+h1, h2, h3, h4, h5, h6 {
+       line-height: 1.3;
+       margin-bottom: 0;
+       padding-bottom: 0;
+}
+
+:is(h1, h2, h3) {
+       line-height: 1.2;
+}
+
+:is(h1, h2) {
+       max-width: 40ch;
+}
+
+:is(h2, h3):not(:first-child) {
+       margin-top: 2em;
+}
+
+hr {
+       margin: 4em auto;
+       border: 0;
+       height: 1px;
+       background: #ccc;
+}
+
+
+code:not([class*="language"]) {
+       font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
+       font-size: 1.75ex;
+       color: #444;
+       background-color: rgba(0, 0, 0, 0.1);
+       padding-right: 0.15em;
+       padding-left: 0.15em;
+}
+
+pre code {
+       margin: 2rem 0;
+       padding: 0.5em 1rem;
+       display: block;
+       border-left: 3px solid rgba(0, 0, 0, 0.35);
+       background-color: rgba(0, 0, 0, 0.05);
+       border-radius: 0 0.25rem 0.25rem 0;
+       overflow-x: auto;
+       font-size: 0.8em !important;
+}
+
+blockquote {
+       margin: 2rem 0;
+       padding: 0.5em 1rem;
+       border-left: 3px solid rgba(0, 0, 0, 0.35);
+       background-color: rgba(0, 0, 0, 0.05);
+       border-radius: 0 0.25rem 0.25rem 0;
+}
+
diff --git a/third_party/parsedown b/third_party/parsedown
new file mode 160000 (submodule)
index 0000000..0b274ac
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 0b274ac959624e6c6d647e9c9b6c2d20da242004