]> git.bts.cx Git - garden.git/blob - garden/garden.php
e73804a9061c358297fbc94b8a226fbf073a854e
[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
225 function garden_move_raw($content_items) {
226         foreach ($content_items as $item) {
227                 if ($item->type != GardenItemType::Raw && $item->type != GardenItemType::Image) {
228                         continue;
229                 }
230
231                 $success = copy($item->source_filename, $item->target_filename);
232                 if ($success != true) {
233                         error('Failed to copy file: filename="', $item->source_filename, '"');
234                 }
235         }
236 }
237
238 ///////////////////////////////////////////////////////////////////////////////
239 // Template
240 ///////////////////////////////////////////////////////////////////////////////
241
242 function garden_template_render($name, $variables = null) {
243         global $garden_template_base, $garden_template_content;
244         
245         $base_template = null;
246
247         $output = '';
248         while ($name != null) {
249                 $path = garden_path(GARDEN_TEMPLATE_DIR, $name . '.php');
250                 
251                 $base_template = null;
252                 $base_template_variables = null;
253
254                 $garden_template_base_previous = $garden_template_base;
255                 $garden_template_base = function($name, $base_variables) use (&$base_template, &$base_template_variables) {
256                         $base_template = $name;
257                         $base_template_variables = $base_variables;
258                 };
259
260                 $garden_template_content_previous = $garden_template_content;
261                 $garden_template_content = function() use ($output) {
262                         return $output;
263                 };
264
265                 if ($variables != null) {
266                         extract($variables);
267                 }
268
269                 ob_start();
270                 include($path);
271                 $output = ob_get_contents();
272                 ob_end_clean();
273
274                 $garden_template_base = $garden_template_base_previous;
275                 $garden_template_content = $garden_template_content_previous;
276
277                 $name = $base_template;
278                 $variables = $base_template_variables;
279         }
280
281         return $output;
282 }
283
284 ///////////////////////////////////////////////////////////////////////////////
285 // Template helpers
286 ///////////////////////////////////////////////////////////////////////////////
287
288 function garden_template_base($name, $variables = null) {
289         global $garden_template_base;
290         $garden_template_base($name, $variables);
291 }
292
293 function garden_template_content() {
294         global $garden_template_content;
295         return $garden_template_content();
296 }
297
298 ///////////////////////////////////////////////////////////////////////////////
299 // HTML
300 ///////////////////////////////////////////////////////////////////////////////
301
302 require_once(garden_path(__DIR__, 'third_party', 'parsedown', 'Parsedown.php'));
303
304 class GardenExtendedParsedown extends Parsedown {
305         private $content_items;
306
307         public function __construct($content_items) {
308                 $this->content_items = $content_items;
309                 $this->InlineTypes['!'][] = 'Youtube';
310                 $this->InlineTypes['!'][] = 'Image';
311                 $this->InlineTypes['['][] = 'WikiLinks';
312                 $this->inlineMarkerList .= '!';
313                 $this->inlineMarkerList .= '[';
314         }
315
316         protected function inlineImage($excerpt) {
317                 if (preg_match('/^!\[\[([^\|\]]+)(\|([0-9]+)x([0-9]+))?\]\]/', $excerpt['text'], $matches)) {
318                         $image_name = $matches[1];
319                         $image_width = count($matches) == 5 ? $matches[3] : null;
320                         $image_height = count($matches) == 5 ? $matches[4] : null;
321
322                         if ($image_name == null) {
323                                 return;
324                         }
325
326                         $target_url = null;
327                         foreach ($this->content_items as $content_item) {
328                                 if ($content_item->path->basename == $image_name) {
329                                         $target_url = $content_item->url;
330                                         break;
331                                 }
332                         }
333
334                         if ($target_url == null) {
335                                 return;
336                         }
337
338                         return array(
339                                 'extent' => strlen($matches[0]), 
340                                 'element' => array(
341                                         'name' => 'img',
342                                         'text' => '',
343                                         'attributes' => array(
344                                                 'src' => $target_url,
345                                         ),
346                                 ),
347                         );
348                 }
349         }
350
351         protected function inlineYoutube($excerpt) {
352                 if (preg_match('/^!\[\[yt:([^\]]+)\]\]/', $excerpt['text'], $matches)) {
353                         $video_id = $matches[1];
354
355                         if ($video_id == null) {
356                                 return;
357                         }
358
359                         return array(
360                                 'extent' => strlen($matches[0]), 
361                                 'element' => array(
362                                         'name' => 'iframe',
363                                         'text' => '',
364                                         'attributes' => array(
365                                                 'class' => "video",
366                                                 'type' => "text/html",
367                                                 //'width' => "640",
368                                                 //'height' => "360",
369                                                 'src' => "https://www.youtube.com/embed/" . $video_id,
370                                                 'frameborder' => "0",
371                                                 'loading' => "lazy",
372                                                 'referrerpolicy' => "no-referrer",
373                                                 'sandbox' => "allow-same-origin allow-scripts",
374                                         ),
375                                 ),
376                         );
377                 }
378         }
379
380         protected function inlineWikiLinks($excerpt) {
381                 if (preg_match('/^\[\[(.+)\]\]/', $excerpt['text'], $matches)) {
382                         $target_title = $matches[1];
383
384                         if ($target_title == null) {
385                                 return;
386                         }
387
388                         $target_url = null;
389                         foreach ($this->content_items as $content_item) {
390                                 if ($content_item->title == $target_title) {
391                                         $target_url = $content_item->url;
392                                         break;
393                                 }
394                         }
395
396                         if ($target_url == null) {
397                                 return;
398                         }
399
400                         return array(
401                                 'extent' => strlen($matches[0]), 
402                                 'element' => array(
403                                         'name' => 'a',
404                                         'text' => $target_title,
405                                         'attributes' => array(
406                                                 'href' => $target_url,
407                                         ),
408                                 ),
409                         );
410                 }
411         }
412 }
413
414 function garden_generate_html($content_items) {
415         $parsedown = new GardenExtendedParsedown($content_items);
416
417         foreach ($content_items as $item) {
418                 if ($item->type != GardenItemType::Article) {
419                         continue;
420                 }
421
422                 $markdown_data = garden_read_file($item->source_filename);
423                 $markdown_html = $parsedown->text($markdown_data);
424                 
425                 $html_data = garden_template_render('article', [ 'article' => $item, 'article_content' => $markdown_html ]);
426
427                 garden_write_file($item->target_filename, $html_data);
428         }
429 }
430
431 ///////////////////////////////////////////////////////////////////////////////
432 // Main
433 ///////////////////////////////////////////////////////////////////////////////
434
435 function garden() {
436         $content_files = garden_find_content_files(GARDEN_CONTENT_DIR);
437         array_push($content_files, ...garden_find_content_files(GARDEN_TEMPLATE_DIR));
438         $process_items = garden_make_process_items(GARDEN_OUTPUT_DIR, $content_files);
439         garden_make_directories($process_items);
440         garden_move_raw($process_items);
441         garden_generate_html($process_items);
442 }
443
444 ///////////////////////////////////////////////////////////////////////////////
445 // Main code!
446 ///////////////////////////////////////////////////////////////////////////////
447
448 assert($argc >= 2);
449
450 // First parameter needs to be the configuration php
451 $config_file = $argv[1];
452 output_debug("Will use configuration: file='", $config_file, "'");
453 require_once($config_file);
454
455 garden();