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