]> git.bts.cx Git - garden.git/blob - garden.php
Better configuration
[garden.git] / garden.php
1 <?php
2
3 /*
4    _____               _               _____ _ _          _____                           _             
5   / ____|             | |             / ____(_) |        / ____|                         | |            
6  | |  __  __ _ _ __ __| | ___ _ __   | (___  _| |_ ___  | |  __  ___ _ __   ___ _ __ __ _| |_ ___  _ __ 
7  | | |_ |/ _` | '__/ _` |/ _ \ '_ \   \___ \| | __/ _ \ | | |_ |/ _ \ '_ \ / _ \ '__/ _` | __/ _ \| '__|
8  | |__| | (_| | | | (_| |  __/ | | |  ____) | | ||  __/ | |__| |  __/ | | |  __/ | | (_| | || (_) | |   
9   \_____|\__,_|_|  \__,_|\___|_| |_| |_____/|_|\__\___|  \_____|\___|_| |_|\___|_|  \__,_|\__\___/|_|
10
11  */
12
13 ///////////////////////////////////////////////////////////////////////////////
14 // Logging
15 ///////////////////////////////////////////////////////////////////////////////
16
17 function output_debug(...$str_segments) {
18         $str = join('', $str_segments);
19         echo($str . PHP_EOL);
20 }
21
22 function output_error(...$str_segments) {
23         $str = join('', $str_segments);
24         die($str);
25 }
26
27 ///////////////////////////////////////////////////////////////////////////////
28 // Paths
29 ///////////////////////////////////////////////////////////////////////////////
30
31 function garden_path(...$path_segments) {
32         $segments = array();
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;
38                         }
39                 }
40         }
41         array_unshift($segments, '');
42         return join(DIRECTORY_SEPARATOR, $segments);
43 }
44
45 ///////////////////////////////////////////////////////////////////////////////
46 // String
47 ///////////////////////////////////////////////////////////////////////////////
48
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);
54         return $str;
55 }
56
57 function garden_url(...$url_segments) {
58         $segments = array();
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);
64                         }
65                 }
66         }
67         array_unshift($segments, '');
68         return join(DIRECTORY_SEPARATOR, $segments);
69 }
70
71 ///////////////////////////////////////////////////////////////////////////////
72 // I/O
73 ///////////////////////////////////////////////////////////////////////////////
74
75 function garden_read_file($filename) {
76         return file_get_contents($filename);
77 }
78
79 function garden_write_file($filename, $content) {
80         $utf8_bom = "\xEF\xBB\xBF";
81         file_put_contents($filename, $utf8_bom . $content);
82 }
83
84 ///////////////////////////////////////////////////////////////////////////////
85 // Items to process
86 ///////////////////////////////////////////////////////////////////////////////
87
88 enum GardenItemType {
89         case Article;
90         case Image;
91         case Raw;
92 }
93
94 class GardenItem {
95         public $path;
96         public $type;
97         public $id;
98         public $title;
99         public $category;
100         public $url;
101         public $date;
102         public $source_filename;
103         public $target_filename;
104
105         public function __construct($path, $type, $id, $title, $category, $url, $date, $source_filename, $target_filename) {
106                 $this->path = $path;
107                 $this->type = $type;
108                 $this->id = $id;
109                 $this->title = $title;
110                 $this->category = $category;
111                 $this->url = $url;
112                 $this->date = $date;
113                 $this->source_filename = $source_filename;
114                 $this->target_filename = $target_filename;
115         }
116 }
117
118 function garden_make_process_items($output_directory, $content_paths) {
119         $output_items = [];
120
121         foreach ($content_paths as $item) {
122                 $id = garden_slug($item->filename);
123
124                 $target_extension = $item->extension;
125
126                 $ignore_file = false;
127                 $type = GardenItemType::Raw;
128                 switch ($item->extension) {
129                         case 'php':
130                                 $ignore_file = true;
131                                 break;
132
133                         case 'md':
134                                 $type = GardenItemType::Article;
135                                 $target_extension = 'html';
136                                 break;
137
138                         case 'png':
139                         case 'jpg':
140                         case 'jpeg':
141                         case 'gif':
142                                 $type = GardenItemType::Image;
143                                 break;
144                 }
145
146                 if ($ignore_file == true) {
147                         continue;
148                 }
149
150                 $target_basename = $item->filename;
151                 if ($target_extension != '') {
152                         $target_basename .= '.' . $target_extension;
153                 }
154
155                 $category_components = explode(DIRECTORY_SEPARATOR, $item->path);
156                 $category = count($category_components) >= 2 ? $category_components[1] : "";
157
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));
160
161                 $date = filemtime($item->full_path);
162
163                 $output_items[] = new GardenItem($item, $type, $id, $item->filename, $category, $url, $date, $item->full_path, $target_path);
164         }
165
166         return $output_items;
167 }
168
169 ///////////////////////////////////////////////////////////////////////////////
170 // Paths
171 ///////////////////////////////////////////////////////////////////////////////
172
173 class GardenContentPath {
174         public $full_path;
175         public $root;
176         public $path;
177         public $basename;
178         public $filename;
179         public $extension;
180
181         public function __construct($full_path, $root, $path, $path_parts) {
182                 $this->full_path = $full_path;
183                 $this->root = $root;
184                 $this->path = $path;
185                 $this->basename = $path_parts['basename'];
186                 $this->filename = $path_parts['filename'];
187                 $this->extension = $path_parts['extension'];
188         }
189 }
190
191 function garden_find_content_files($content_dir) {
192         $content_dir = realpath($content_dir);
193
194         $scan_paths = [''];
195         $output_paths = [];
196
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, '.')) {
202                                 continue;
203                         }
204
205                         $full_path = garden_path($content_dir, $scan_path, $item);
206                         if (is_dir($full_path)) {
207                                 $scan_paths[] = garden_path($scan_path, $item);
208                                 continue;
209                         }
210
211                         $path_parts = pathinfo($full_path);
212                         $output_paths[] = new GardenContentPath($full_path, $content_dir, $scan_path, $path_parts);
213                 }
214         }
215
216         return $output_paths;
217 }
218
219 ///////////////////////////////////////////////////////////////////////////////
220 // Directories
221 ///////////////////////////////////////////////////////////////////////////////
222
223 function garden_make_directories($content_items) {
224         foreach ($content_items as $item) {
225                 $path_parts = pathinfo($item->target_filename);
226
227                 $directory = $path_parts['dirname'];
228                 if (is_dir($directory ) == false) {
229                         mkdir($directory , 0777, true); // FIXME, permissions...
230                 }
231         }
232
233         return $content_items;
234 }
235
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?
239                         continue;
240                 }
241
242                 $success = copy($item->source_filename, $item->target_filename);
243                 if ($success != true) {
244                         error('Failed to copy file: filename="', $item->source_filename, '"');
245                 }
246         }
247
248         return $content_items;
249 }
250
251 ///////////////////////////////////////////////////////////////////////////////
252 // Template
253 ///////////////////////////////////////////////////////////////////////////////
254
255 function garden_template_render($name, $variables = null) {
256         global $garden_template_base, $garden_template_content;
257         
258         $base_template = null;
259
260         $output = '';
261         while ($name != null) {
262                 $path = garden_path(GARDEN_TEMPLATE_DIR, $name . '.php');
263                 
264                 $base_template = null;
265                 $base_template_variables = null;
266
267                 $garden_template_base_previous = $garden_template_base;
268                 $garden_template_base = function($name, $base_variables) use (&$base_template, &$base_template_variables) {
269                         $base_template = $name;
270                         $base_template_variables = $base_variables;
271                 };
272
273                 $garden_template_content_previous = $garden_template_content;
274                 $garden_template_content = function() use ($output) {
275                         return $output;
276                 };
277
278                 if ($variables != null) {
279                         extract($variables);
280                 }
281
282                 ob_start();
283                 include($path);
284                 $output = ob_get_contents();
285                 ob_end_clean();
286
287                 $garden_template_base = $garden_template_base_previous;
288                 $garden_template_content = $garden_template_content_previous;
289
290                 $name = $base_template;
291                 $variables = $base_template_variables;
292         }
293
294         return $output;
295 }
296
297 ///////////////////////////////////////////////////////////////////////////////
298 // Template helpers
299 ///////////////////////////////////////////////////////////////////////////////
300
301 function garden_template_base($name, $variables = null) {
302         global $garden_template_base;
303         $garden_template_base($name, $variables);
304 }
305
306 function garden_template_content() {
307         global $garden_template_content;
308         return $garden_template_content();
309 }
310
311 function garden_site_url(...$url_segments) {
312         return GARDEN_SITE_BASE_URL . garden_url(...$url_segments);
313 }
314
315
316
317 ///////////////////////////////////////////////////////////////////////////////
318 // Images
319 ///////////////////////////////////////////////////////////////////////////////
320
321 class GardenImage {
322         public $content_item;
323         public $width;
324         public $height;
325         public $url;
326         public $target_filename;
327         
328         public function __construct($content_item, $width, $height, $url, $target_filename) {
329                 $this->content_item = $content_item;
330                 $this->width = $width;
331                 $this->height = $height;
332                 $this->url = $url;
333                 $this->target_filename = $target_filename;
334         }
335 }
336
337 function garden_make_images($post_process_items) {
338         foreach ($post_process_items as $item) {
339                 $image = new Imagick($item->content_item->source_filename);
340                 $image->thumbnailImage($item->width, $item->height);
341                 $image->writeImage($item->target_filename);
342         }
343 }
344
345 ///////////////////////////////////////////////////////////////////////////////
346 // HTML
347 ///////////////////////////////////////////////////////////////////////////////
348
349 require_once(garden_path(__DIR__, 'third_party', 'parsedown', 'Parsedown.php'));
350
351 class GardenExtendedParsedown extends Parsedown {
352         public $post_process_items;
353         private $output_directory;
354         private $content_items;
355
356         public function __construct($output_directory, $content_items) {
357                 $this->post_process_items = [];
358                 $this->output_directory = $output_directory;
359                 $this->content_items = $content_items;
360                 $this->InlineTypes['!'][] = 'Youtube';
361                 $this->InlineTypes['!'][] = 'Image';
362                 $this->InlineTypes['['][] = 'WikiLinks';
363                 $this->inlineMarkerList .= '!';
364                 $this->inlineMarkerList .= '[';
365         }
366
367         protected function inlineImage($excerpt) {
368                 if (preg_match('/^!\[\[([^\|\]]+)(\|([0-9]+)x([0-9]+))?\]\]/', $excerpt['text'], $matches)) {
369                         $image_name = $matches[1];
370                         $image_width = count($matches) == 5 ? $matches[3] : null;
371                         $image_height = count($matches) == 5 ? $matches[4] : null;
372
373                         if ($image_name == null) {
374                                 return;
375                         }
376
377                         $image_content_item = null;
378                         foreach ($this->content_items as $content_item) {
379                                 if ($content_item->path->basename == $image_name) {
380                                         $image_content_item = $content_item;
381                                         break;
382                                 }
383                         }
384
385                         if ($image_content_item == null) {
386                                 return;
387                         }
388
389                         $target_url = $image_content_item->url;
390                         if ($image_width != null && $image_height != null) {
391                                 $original_path = $image_content_item->path;
392
393                                 $target_basename = $original_path->filename . '_' . $image_width . 'x' . $image_height . '.png';
394
395                                 $target_url = GARDEN_SITE_BASE_URL . garden_url($original_path->path, $target_basename);
396                                 $target_path = garden_path($this->output_directory, garden_url($original_path->path, $target_basename));
397
398                                 $post_process_item = new GardenImage($image_content_item, $image_width, $image_height, $target_url, $target_path);
399                                 $this->post_process_items[] = $post_process_item;
400                         }
401
402                         return array(
403                                 'extent' => strlen($matches[0]), 
404                                 'element' => array(
405                                         'name' => 'img',
406                                         'attributes' => array(
407                                                 'src' => $target_url,
408                                         ),
409                                 ),
410                         );
411                 }
412         }
413
414         protected function inlineYoutube($excerpt) {
415                 if (preg_match('/^!\[\[yt:([^\]]+)\]\]/', $excerpt['text'], $matches)) {
416                         $video_id = $matches[1];
417
418                         if ($video_id == null) {
419                                 return;
420                         }
421
422                         return array(
423                                 'extent' => strlen($matches[0]), 
424                                 'element' => array(
425                                         'name' => 'iframe',
426                                         'text' => '',
427                                         'attributes' => array(
428                                                 'class' => "video",
429                                                 'type' => "text/html",
430                                                 //'width' => "640",
431                                                 //'height' => "360",
432                                                 'src' => "https://www.youtube.com/embed/" . $video_id,
433                                                 'frameborder' => "0",
434                                                 'loading' => "lazy",
435                                                 'referrerpolicy' => "no-referrer",
436                                                 'sandbox' => "allow-same-origin allow-scripts",
437                                         ),
438                                 ),
439                         );
440                 }
441         }
442
443         protected function inlineWikiLinks($excerpt) {
444                 if (preg_match('/^\[\[(.+)\]\]/', $excerpt['text'], $matches)) {
445                         $target_title = $matches[1];
446
447                         if ($target_title == null) {
448                                 return;
449                         }
450
451                         $target_url = null;
452                         foreach ($this->content_items as $content_item) {
453                                 if ($content_item->title == $target_title) {
454                                         $target_url = $content_item->url;
455                                         break;
456                                 }
457                         }
458
459                         if ($target_url == null) {
460                                 return;
461                         }
462
463                         return array(
464                                 'extent' => strlen($matches[0]), 
465                                 'element' => array(
466                                         'name' => 'a',
467                                         'text' => $target_title,
468                                         'attributes' => array(
469                                                 'href' => $target_url,
470                                         ),
471                                 ),
472                         );
473                 }
474         }
475 }
476
477 function garden_generate_html($output_directory, $content_items) {
478         $output_items = [];
479
480         $categorised_items = [];
481         foreach ($content_items as $item) {
482                 if ($item->type != GardenItemType::Article) {
483                         continue;
484                 }
485
486                 if (isset($categorised_items[$item->category]) == false) {
487                         $categorised_items[$item->category] = [];
488                 }
489                 $categorised_items[$item->category][] = $item;
490         }
491
492         foreach ($content_items as $item) {
493                 if ($item->type != GardenItemType::Article) {
494                         continue;
495                 }
496
497                 $markdown_data = garden_read_file($item->source_filename);
498
499                 $parsedown = new GardenExtendedParsedown($output_directory, $content_items);
500                 $markdown_html = $parsedown->text($markdown_data);
501
502                 if (count($parsedown->post_process_items) > 0) {
503                         array_push($output_items, ...$parsedown->post_process_items);
504                 }
505
506                 $variables = GARDEN_TEMPLATE_CONSTANTS; // PHP will copy by default!
507                 $variables['article'] =  $item;
508                 $variables['article_content'] =  $markdown_html;
509                 $variables['categories'] =  $categorised_items;
510
511                 $html_data = garden_template_render('article', $variables);
512
513                 garden_write_file($item->target_filename, $html_data);
514         }
515
516         return $output_items;
517 }
518
519 ///////////////////////////////////////////////////////////////////////////////
520 // Feed
521 ///////////////////////////////////////////////////////////////////////////////
522
523 function garden_generate_atom($output_directory, $output_filename, $content_items) {
524         $recent_items = [];
525         foreach ($content_items as $item) {
526                 if ($item->type != GardenItemType::Article) {
527                         continue;
528                 }
529
530                 $recent_items[$item->date] = $item;
531         }
532
533         // Sort by date...
534         ksort($recent_items);
535         $recent_items = array_reverse($recent_items);
536
537         $variables = GARDEN_TEMPLATE_CONSTANTS; // PHP will copy by default!
538         $variables['recent_items'] =  $recent_items;
539         $html_data = garden_template_render('atom', $variables);
540
541         garden_write_file(garden_path($output_directory, $output_filename), $html_data);
542 }
543
544 function garden_generate_json($output_directory, $output_filename, $content_items) {
545         $recent_items = [];
546         foreach ($content_items as $item) {
547                 if ($item->type != GardenItemType::Article) {
548                         continue;
549                 }
550
551                 $recent_items[$item->date] = $item;
552         }
553
554         // Sort by date...
555         ksort($recent_items);
556         $recent_items = array_reverse($recent_items);
557
558         $feed = [];
559         $feed['version'] = 'https://jsonfeed.org/version/1.1';
560         $feed['title'] = GARDEN_TEMPLATE_CONSTANTS['site_name'];
561         $feed['home_page_url'] = garden_site_url('/');
562         $feed['feed_url'] = garden_site_url($output_filename);
563         $feed['items'] = [];
564
565         foreach ($recent_items as $item) {
566                 $feed_item = [];
567                 $feed_item['id'] = $item->date + hexdec(hash('crc32', $item->title));
568                 $feed_item['date_published'] = date(DATE_RFC3339, $item->date);
569                 $feed_item['title'] = $item->title;
570                 $feed_item['content_text'] = 'An update was published.';
571                 $feed_item['url'] = $item->url;
572                 $feed['items'][] = $feed_item;
573         }
574
575         $json_data = json_encode($feed);
576         garden_write_file(garden_path($output_directory, $output_filename), $json_data);
577 }
578
579 ///////////////////////////////////////////////////////////////////////////////
580 // Main
581 ///////////////////////////////////////////////////////////////////////////////
582
583 function garden() {
584         $content_files = garden_find_content_files(GARDEN_CONTENT_DIR);
585         array_push($content_files, ...garden_find_content_files(GARDEN_TEMPLATE_DIR));
586         $process_items = garden_make_process_items(GARDEN_OUTPUT_DIR, $content_files);
587         garden_make_directories($process_items);
588         garden_move_raw($process_items);
589         $post_process_items = garden_generate_html(GARDEN_OUTPUT_DIR, $process_items);
590         garden_make_images($post_process_items);
591
592         if (defined('GARDEN_ATOM_FEED_FILENAME')) {
593                 garden_generate_atom(GARDEN_OUTPUT_DIR, GARDEN_ATOM_FEED_FILENAME, $process_items);
594         }
595
596         if (defined('GARDEN_JSON_FEED_FILENAME')) {
597                 garden_generate_json(GARDEN_OUTPUT_DIR, GARDEN_JSON_FEED_FILENAME, $process_items);
598         }
599 }
600
601 ///////////////////////////////////////////////////////////////////////////////
602 // Main code!
603 ///////////////////////////////////////////////////////////////////////////////
604
605 assert(extension_loaded('imagick'), 'Needs Imagick');
606 assert($argc >= 2, 'Please provide configuration file');
607
608 // First parameter needs to be the configuration php
609 $config_file = $argv[1];
610
611 output_debug("Will use configuration: file='", $config_file, "'");
612 require_once($config_file);
613
614 garden();