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