From: Ben Sherratt Date: Sat, 27 Dec 2025 10:54:05 +0000 (+0000) Subject: Initial release X-Git-Url: https://git.bts.cx/garden.git/commitdiff_plain/1ff3a665663a5095c770cc63c3d09572944b30f1?hp=9b85fe10f66386c9af92a9a2d2bd79838ff8f883 Initial release --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc300b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +content +public diff --git a/.gitmodules b/.gitmodules index 841e07a..61f9330 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 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 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 diff --git a/config.php b/config.php index 36a3931..9ea6b10 100644 --- a/config.php +++ b/config.php @@ -1,14 +1,27 @@ 'garden', + 'site_language' => 'en', + 'site_copyline' => 'Content © 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 index 0000000..e4391e4 --- /dev/null +++ b/garden.php @@ -0,0 +1,574 @@ +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 index e73804a..0000000 --- a/garden/garden.php +++ /dev/null @@ -1,455 +0,0 @@ -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 index 0b274ac..0000000 --- a/garden/third_party/parsedown +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0b274ac959624e6c6d647e9c9b6c2d20da242004 diff --git a/templates/article.php b/templates/article.php index 9c5d098..2b10bab 100644 --- a/templates/article.php +++ b/templates/article.php @@ -3,7 +3,7 @@

title ?>

-

date) ?>

+

Last updated: date) ?>

diff --git a/templates/atom.php b/templates/atom.php new file mode 100644 index 0000000..5335752 --- /dev/null +++ b/templates/atom.php @@ -0,0 +1,31 @@ +'."\n" /* So some installs of PHP don't get confused... */ ?> + + / + + <?= $site_name; ?> + + date) ?> + + + + + + + + + + + url ?> + <?= $update->title ?> + date) ?> + + An update was published. + +
+ An update was published. +
+
+
+ + +
diff --git a/templates/base.php b/templates/base.php index 83217f2..76d6d00 100644 --- a/templates/base.php +++ b/templates/base.php @@ -1,24 +1,52 @@ - + + + <?= $site_name ?> - <?= $article->title ?> + + <?= $site_name ?> + + - <?= site_name ?> - <?= $article->title; ?> + + -
-

Find me @btsherratt or on itch.io

-
+
+
+

+

Find me or

+
+ + - + -
-
-
-
+ +
diff --git a/templates/style/screen.css b/templates/style/screen.css new file mode 100644 index 0000000..cb1176c --- /dev/null +++ b/templates/style/screen.css @@ -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 index 0000000..0b274ac --- /dev/null +++ b/third_party/parsedown @@ -0,0 +1 @@ +Subproject commit 0b274ac959624e6c6d647e9c9b6c2d20da242004