4 _____ _ _____ _ _ _____ _
5 / ____| | | / ____(_) | / ____| | |
6 | | __ __ _ _ __ __| | ___ _ __ | (___ _| |_ ___ | | __ ___ _ __ ___ _ __ __ _| |_ ___ _ __
7 | | |_ |/ _` | '__/ _` |/ _ \ '_ \ \___ \| | __/ _ \ | | |_ |/ _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__|
8 | |__| | (_| | | | (_| | __/ | | | ____) | | || __/ | |__| | __/ | | | __/ | | (_| | || (_) | |
9 \_____|\__,_|_| \__,_|\___|_| |_| |_____/|_|\__\___| \_____|\___|_| |_|\___|_| \__,_|\__\___/|_|
13 ///////////////////////////////////////////////////////////////////////////////
15 ///////////////////////////////////////////////////////////////////////////////
17 function output_debug(...$str_segments) {
18 $str = join('', $str_segments);
22 function output_error(...$str_segments) {
23 $str = join('', $str_segments);
27 ///////////////////////////////////////////////////////////////////////////////
29 ///////////////////////////////////////////////////////////////////////////////
31 function garden_path(...$path_segments) {
33 foreach ($path_segments as $path_segment) {
34 $inner_segments = explode(DIRECTORY_SEPARATOR, $path_segment);
35 foreach ($inner_segments as $inner_segment) {
36 if ($inner_segment != '') {
37 $segments[] = $inner_segment;
41 array_unshift($segments, '');
42 return join(DIRECTORY_SEPARATOR, $segments);
45 ///////////////////////////////////////////////////////////////////////////////
47 ///////////////////////////////////////////////////////////////////////////////
49 function garden_slug($str) {
50 $str = preg_replace('/\s+/', '-', $str);
51 $str = preg_replace('/\-+/', '-', $str);
52 $str = preg_replace('/[^\w\s\-\.]+/', '', $str);
53 $str = strtolower($str);
57 function garden_url(...$url_segments) {
59 foreach ($url_segments as $url_segment) {
60 $inner_segments = explode(DIRECTORY_SEPARATOR, $url_segment);
61 foreach ($inner_segments as $inner_segment) {
62 if ($inner_segment != '') {
63 $segments[] = garden_slug($inner_segment);
67 array_unshift($segments, '');
68 return join(DIRECTORY_SEPARATOR, $segments);
71 ///////////////////////////////////////////////////////////////////////////////
73 ///////////////////////////////////////////////////////////////////////////////
75 function garden_read_file($filename) {
76 return file_get_contents($filename);
79 function garden_write_file($filename, $content) {
80 $utf8_bom = "\xEF\xBB\xBF";
81 file_put_contents($filename, $utf8_bom . $content);
84 ///////////////////////////////////////////////////////////////////////////////
86 ///////////////////////////////////////////////////////////////////////////////
102 public $source_filename;
103 public $target_filename;
105 public function __construct($path, $type, $id, $title, $category, $url, $date, $source_filename, $target_filename) {
109 $this->title = $title;
110 $this->category = $category;
113 $this->source_filename = $source_filename;
114 $this->target_filename = $target_filename;
118 function garden_make_process_items($output_directory, $content_paths) {
121 foreach ($content_paths as $item) {
122 $id = garden_slug($item->filename);
124 $target_extension = $item->extension;
126 $ignore_file = false;
127 $type = GardenItemType::Raw;
128 switch ($item->extension) {
134 $type = GardenItemType::Article;
135 $target_extension = 'html';
142 $type = GardenItemType::Image;
146 if ($ignore_file == true) {
150 $target_basename = $item->filename;
151 if ($target_extension != '') {
152 $target_basename .= '.' . $target_extension;
155 $category_components = explode(DIRECTORY_SEPARATOR, $item->path);
156 $category = count($category_components) >= 2 ? $category_components[1] : "";
158 $url = GARDEN_SITE_BASE_URL . garden_url($item->path, $target_basename);
159 $target_path = garden_path($output_directory, garden_url($item->path, $target_basename));
161 $date = filemtime($item->full_path);
163 $output_items[] = new GardenItem($item, $type, $id, $item->filename, $category, $url, $date, $item->full_path, $target_path);
166 return $output_items;
169 ///////////////////////////////////////////////////////////////////////////////
171 ///////////////////////////////////////////////////////////////////////////////
173 class GardenContentPath {
181 public function __construct($full_path, $root, $path, $path_parts) {
182 $this->full_path = $full_path;
185 $this->basename = $path_parts['basename'];
186 $this->filename = $path_parts['filename'];
187 $this->extension = $path_parts['extension'];
191 function garden_find_content_files($content_dir) {
192 $content_dir = realpath($content_dir);
197 while (count($scan_paths) > 0) {
198 $scan_path = array_shift($scan_paths);
199 $path_contents = scandir(garden_path($content_dir, $scan_path));
200 foreach ($path_contents as $item) {
201 if (str_starts_with($item, '.')) {
205 $full_path = garden_path($content_dir, $scan_path, $item);
206 if (is_dir($full_path)) {
207 $scan_paths[] = garden_path($scan_path, $item);
211 $path_parts = pathinfo($full_path);
212 $output_paths[] = new GardenContentPath($full_path, $content_dir, $scan_path, $path_parts);
216 return $output_paths;
219 ///////////////////////////////////////////////////////////////////////////////
221 ///////////////////////////////////////////////////////////////////////////////
223 function garden_make_directories($content_items) {
224 foreach ($content_items as $item) {
225 $path_parts = pathinfo($item->target_filename);
227 $directory = $path_parts['dirname'];
228 if (is_dir($directory ) == false) {
229 mkdir($directory , 0777, true); // FIXME, permissions...
233 return $content_items;
236 function garden_move_raw($content_items) {
237 foreach ($content_items as $item) {
238 if ($item->type != GardenItemType::Raw && $item->type != GardenItemType::Image) { // FIXME, do we need to copy images?
242 $success = copy($item->source_filename, $item->target_filename);
243 if ($success != true) {
244 error('Failed to copy file: filename="', $item->source_filename, '"');
248 return $content_items;
251 ///////////////////////////////////////////////////////////////////////////////
253 ///////////////////////////////////////////////////////////////////////////////
255 function garden_template_render($name, $variables = null) {
256 global $garden_template_base, $garden_template_content;
258 $base_template = null;
261 while ($name != null) {
262 $path = garden_path(GARDEN_TEMPLATE_DIR, $name . '.php');
264 if (file_exists($path) == false) {
265 $path = garden_path(GARDEN_FALLBACK_TEMPLATE_DIR, $name . '.php');
268 $base_template = null;
269 $base_template_variables = null;
271 $garden_template_base_previous = $garden_template_base;
272 $garden_template_base = function($name, $base_variables) use (&$base_template, &$base_template_variables) {
273 $base_template = $name;
274 $base_template_variables = $base_variables;
277 $garden_template_content_previous = $garden_template_content;
278 $garden_template_content = function() use ($output) {
282 if ($variables != null) {
288 $output = ob_get_contents();
291 $garden_template_base = $garden_template_base_previous;
292 $garden_template_content = $garden_template_content_previous;
294 $name = $base_template;
295 $variables = $base_template_variables;
301 ///////////////////////////////////////////////////////////////////////////////
303 ///////////////////////////////////////////////////////////////////////////////
305 function garden_template_base($name, $variables = null) {
306 global $garden_template_base;
307 $garden_template_base($name, $variables);
310 function garden_template_content() {
311 global $garden_template_content;
312 return $garden_template_content();
315 function garden_site_url(...$url_segments) {
316 return GARDEN_SITE_BASE_URL . garden_url(...$url_segments);
319 ///////////////////////////////////////////////////////////////////////////////
321 ///////////////////////////////////////////////////////////////////////////////
324 public $content_item;
328 public $target_filename;
330 public function __construct($content_item, $width, $height, $url, $target_filename) {
331 $this->content_item = $content_item;
332 $this->width = $width;
333 $this->height = $height;
335 $this->target_filename = $target_filename;
339 function garden_make_images($post_process_items) {
340 foreach ($post_process_items as $item) {
341 $image = new Imagick($item->content_item->source_filename);
342 $image->thumbnailImage($item->width, $item->height);
343 $image->writeImage($item->target_filename);
347 ///////////////////////////////////////////////////////////////////////////////
349 ///////////////////////////////////////////////////////////////////////////////
351 require_once(garden_path(__DIR__, 'third_party', 'parsedown', 'Parsedown.php'));
353 class GardenExtendedParsedown extends Parsedown {
354 public $post_process_items;
355 private $output_directory;
356 private $content_items;
358 public function __construct($output_directory, $content_items) {
359 $this->post_process_items = [];
360 $this->output_directory = $output_directory;
361 $this->content_items = $content_items;
362 $this->InlineTypes['!'][] = 'Youtube';
363 $this->InlineTypes['!'][] = 'Image';
364 $this->InlineTypes['['][] = 'WikiLinks';
365 $this->inlineMarkerList .= '!';
366 $this->inlineMarkerList .= '[';
369 protected function inlineImage($excerpt) {
370 if (preg_match('/^!\[\[([^\|\]]+)(\|([0-9]+)x([0-9]+))?\]\]/', $excerpt['text'], $matches)) {
371 $image_name = $matches[1];
372 $image_width = count($matches) == 5 ? $matches[3] : null;
373 $image_height = count($matches) == 5 ? $matches[4] : null;
375 if ($image_name == null) {
379 $image_content_item = null;
380 foreach ($this->content_items as $content_item) {
381 if ($content_item->path->basename == $image_name) {
382 $image_content_item = $content_item;
387 if ($image_content_item == null) {
391 $target_url = $image_content_item->url;
392 if ($image_width != null && $image_height != null) {
393 $original_path = $image_content_item->path;
395 $target_basename = $original_path->filename . '_' . $image_width . 'x' . $image_height . '.png';
397 $target_url = GARDEN_SITE_BASE_URL . garden_url($original_path->path, $target_basename);
398 $target_path = garden_path($this->output_directory, garden_url($original_path->path, $target_basename));
400 $post_process_item = new GardenImage($image_content_item, $image_width, $image_height, $target_url, $target_path);
401 $this->post_process_items[] = $post_process_item;
405 'extent' => strlen($matches[0]),
408 'attributes' => array(
409 'src' => $target_url,
416 protected function inlineYoutube($excerpt) {
417 if (preg_match('/^!\[\[yt:([^\]]+)\]\]/', $excerpt['text'], $matches)) {
418 $video_id = $matches[1];
420 if ($video_id == null) {
425 'extent' => strlen($matches[0]),
429 'attributes' => array(
431 'type' => "text/html",
434 'src' => "https://www.youtube.com/embed/" . $video_id,
435 'frameborder' => "0",
437 'referrerpolicy' => "no-referrer",
438 'sandbox' => "allow-same-origin allow-scripts",
445 protected function inlineWikiLinks($excerpt) {
446 if (preg_match('/^\[\[(.+)\]\]/', $excerpt['text'], $matches)) {
447 $target_title = $matches[1];
449 if ($target_title == null) {
454 foreach ($this->content_items as $content_item) {
455 if ($content_item->title == $target_title) {
456 $target_url = $content_item->url;
461 if ($target_url == null) {
466 'extent' => strlen($matches[0]),
469 'text' => $target_title,
470 'attributes' => array(
471 'href' => $target_url,
479 function garden_generate_html($output_directory, $content_items) {
482 $categorised_items = [];
483 foreach ($content_items as $item) {
484 if ($item->type != GardenItemType::Article) {
488 if (isset($categorised_items[$item->category]) == false) {
489 $categorised_items[$item->category] = [];
491 $categorised_items[$item->category][] = $item;
494 foreach ($content_items as $item) {
495 if ($item->type != GardenItemType::Article) {
499 $markdown_data = garden_read_file($item->source_filename);
501 $parsedown = new GardenExtendedParsedown($output_directory, $content_items);
502 $markdown_html = $parsedown->text($markdown_data);
504 if (count($parsedown->post_process_items) > 0) {
505 array_push($output_items, ...$parsedown->post_process_items);
508 $variables = GARDEN_TEMPLATE_CONSTANTS; // PHP will copy by default!
509 $variables['article'] = $item;
510 $variables['article_content'] = $markdown_html;
511 $variables['categories'] = $categorised_items;
513 $html_data = garden_template_render('article', $variables);
515 garden_write_file($item->target_filename, $html_data);
518 return $output_items;
521 ///////////////////////////////////////////////////////////////////////////////
523 ///////////////////////////////////////////////////////////////////////////////
525 function garden_generate_atom($output_directory, $output_filename, $content_items) {
527 foreach ($content_items as $item) {
528 if ($item->type != GardenItemType::Article) {
532 $recent_items[$item->date] = $item;
536 ksort($recent_items);
537 $recent_items = array_reverse($recent_items);
539 $variables = GARDEN_TEMPLATE_CONSTANTS; // PHP will copy by default!
540 $variables['recent_items'] = $recent_items;
541 $html_data = garden_template_render('atom', $variables);
543 garden_write_file(garden_path($output_directory, $output_filename), $html_data);
546 function garden_generate_json($output_directory, $output_filename, $content_items) {
548 foreach ($content_items as $item) {
549 if ($item->type != GardenItemType::Article) {
553 $recent_items[$item->date] = $item;
557 ksort($recent_items);
558 $recent_items = array_reverse($recent_items);
561 $feed['version'] = 'https://jsonfeed.org/version/1.1';
562 $feed['title'] = GARDEN_TEMPLATE_CONSTANTS['site_name'];
563 $feed['home_page_url'] = garden_site_url('/');
564 $feed['feed_url'] = garden_site_url($output_filename);
567 foreach ($recent_items as $item) {
569 $feed_item['id'] = $item->date + hexdec(hash('crc32', $item->title));
570 $feed_item['date_published'] = date(DATE_RFC3339, $item->date);
571 $feed_item['title'] = $item->title;
572 $feed_item['content_text'] = 'An update was published.';
573 $feed_item['url'] = $item->url;
574 $feed['items'][] = $feed_item;
577 $json_data = json_encode($feed);
578 garden_write_file(garden_path($output_directory, $output_filename), $json_data);
581 ///////////////////////////////////////////////////////////////////////////////
583 ///////////////////////////////////////////////////////////////////////////////
586 $content_files = garden_find_content_files(GARDEN_CONTENT_DIR);
587 array_push($content_files, ...garden_find_content_files(GARDEN_TEMPLATE_DIR));
588 $process_items = garden_make_process_items(GARDEN_OUTPUT_DIR, $content_files);
589 garden_make_directories($process_items);
590 garden_move_raw($process_items);
591 $post_process_items = garden_generate_html(GARDEN_OUTPUT_DIR, $process_items);
592 garden_make_images($post_process_items);
594 if (defined('GARDEN_ATOM_FEED_FILENAME')) {
595 garden_generate_atom(GARDEN_OUTPUT_DIR, GARDEN_ATOM_FEED_FILENAME, $process_items);
598 if (defined('GARDEN_JSON_FEED_FILENAME')) {
599 garden_generate_json(GARDEN_OUTPUT_DIR, GARDEN_JSON_FEED_FILENAME, $process_items);
603 ///////////////////////////////////////////////////////////////////////////////
605 ///////////////////////////////////////////////////////////////////////////////
607 assert(extension_loaded('imagick'), 'Needs Imagick');
608 assert($argc >= 2, 'Please provide configuration file');
610 define('GARDEN_FALLBACK_TEMPLATE_DIR', garden_path(__DIR__, 'templates'));
612 // First parameter needs to be the configuration php
613 $config_file = $argv[1];
615 output_debug("Will use configuration: file='", $config_file, "'");
616 require_once($config_file);