page: move showdate() into formatting include
[minimedit.git] / article.inc.php
1 <?php
2 class ArchiveArticle
3 {
4         public $raw, $title, $body; # file contents
5         public $meta = [];  # head metadata properties
6         public $place = []; # template variables replaced in render()
7
8         function __construct($path)
9         {
10                 $this->page = preg_replace('{^\.(?:/|$)}', '', $path);
11                 $this->link = preg_replace('{(?:(?:/|^)index)?\.html$}', '', $this->page);
12                 $this->raw($this->page);
13         }
14
15         function raw($page)
16         {
17                 if (!file_exists($page)) {
18                         return;
19                 }
20                 $this->raw = file_get_contents($page);
21
22                 if (preg_match_all('{
23                         \G <meta \s+ property="( [^"]+ )" \s+ content="( [^"]* )" > \s*
24                 }x', $this->raw, $meta)) {
25                         $matchlen = array_sum(array_map('strlen', $meta[0]));
26                         $this->raw = substr($this->raw, $matchlen); # delete matched contents
27                         $this->meta = array_combine($meta[1], $meta[2]); # [property => content]
28                 }
29
30                 // find significant contents
31                 $this->body = preg_replace('{<aside\b.*?</aside>}s', '', $this->raw);
32                 if (preg_match('{<h2>(.*?)</h2>\s*(.*)}s', $this->body, $titlematch)) {
33                         list (, $this->title, $this->body) = $titlematch;
34                 }
35
36                 return $this->raw;
37         }
38
39         function __get($col)
40         {
41                 return $this->$col = $this->$col();  # run method and cache
42         }
43
44         function handler()
45         {
46                 $path = $this->link;
47                 $this->path = '';
48                 $this->restricted = FALSE;
49                 while (TRUE) {
50                         if (file_exists("$path/.private")) {
51                                 $this->restricted = $path;
52                         }
53
54                         if (file_exists("$path/index.php")) {
55                                 return $path;
56                         }
57
58                         $up = strrpos($path, '/');
59                         $this->path = substr($path, $up) . $this->path;
60                         $path = substr($path, 0, $up);
61                         if ($up === FALSE) {
62                                 break;
63                         }
64                 }
65                 return;
66         }
67
68         function restricted()
69         {
70                 $this->handler;
71                 return $this->restricted;
72         }
73
74         function safetitle()
75         {
76                 return trim($this->meta['og:title'] ?? strip_tags($this->title));
77         }
78         function name()
79         {
80                 return $this->safetitle ?: $this->link;
81         }
82
83         function last()
84         {
85                 return filemtime($this->page);
86         }
87         function lastiso()
88         {
89                 return date(DATE_ATOM, $this->last);
90         }
91
92         function dateparts()
93         {
94                 preg_match('< / (\d{4}) [/-] (\d{2}) (?:- (\d{2}) )? - >x', $this->page, $ymd);
95                 array_shift($ymd);
96                 return $ymd;
97         }
98         function dateiso()
99         {
100                 return implode('-', $this->dateparts()) . 'T12:00:00+02:00';
101         }
102
103         function story()
104         {
105                 if ( preg_match('{
106                         (?: < (?: p | figure [^>]* ) >\s* )+ (<img\ [^>]*>) | \n <hr\ />
107                 }x', $this->body, $img, PREG_OFFSET_CAPTURE) ) {
108                         # strip part after matching divider (image)
109                         if (isset($img[1])) {
110                                 $this->img = $img[1][0];
111                         }
112                         return substr($this->body, 0, $img[0][1]);
113                 }
114                 return $this->body;
115         }
116
117         function teaser()
118         {
119                 if ($override = @$this->meta['og:description']) {
120                         # prefer specific page description if found in metadata
121                         return $override;
122                 }
123
124                 # paragraph contents following the page header if any
125                 if (preg_match('{
126                         \G (?> \s+ | <div [^>]*> | \[\[[^]]*\]\] )* <p> \s* (.*?) </p>
127                 }sx', $this->body, $bodyp, 0)) {
128                         return $bodyp[1];
129                 }
130         }
131
132         function img()
133         {
134                 $this->img = NULL;
135                 $this->story;
136                 return $this->img;
137         }
138         function image()
139         {
140                 if ($override = @$this->meta['og:image']) {
141                         # prefer specific page image if found in metadata
142                         return $override;
143                 }
144
145                 if ( preg_match('/\bsrc="([^"]*)"/', $this->img, $src) ) {
146                         return $src[1];
147                 }
148         }
149         function thumb($size = '300x')
150         {
151                 if (!$this->image or $this->image[0] !== '/') return;
152                 return preg_replace(
153                         ['{^(?:/thumb/[^/]*)?}', '/\.groot(?=\.\w+$)/'], ["thumb/$size", ''],
154                         $this->image
155                 );
156         }
157
158         function widget($name, $params = [])
159         {
160                 $path = stream_resolve_include_path("widget/$name.php");
161                 if (!file_exists($path)) {
162                         return '<strong class="warn"><em>'.$name.'</em> ontbreekt</strong>';
163                 }
164
165                 ob_start();
166                 $Page = clone $this;
167                 $Page->handler = $Page->handler . $Page->path; // .= with explicit getter
168                 $Page->path = '';
169                 foreach ($params as $param) {
170                         if ($set = strpos($param, '=')) {
171                                 $Page->place[ substr($param, 0, $set) ] = substr($param, $set + 1);
172                         }
173                         elseif (!empty($param)) {
174                                 $Page->path .= '/'.$param;
175                         }
176                 }
177                 $Page->link .= $Page->path;
178                 try {
179                         include "widget/$name.php";
180                         return ob_get_clean();
181                 }
182                 catch (Exception $e) {
183                         return sprintf('<strong class="warn">%s</strong>',
184                                 "fout in <em>$name</em>: {$e->getMessage()}"
185                         );
186                 }
187         }
188
189         function render()
190         {
191                 $doc = $this->raw;
192
193                 if (!empty($this->place['warn'])) {
194                         $warn = '<p class="warn">[[warn]]</p>';
195                         if ($offset = strpos($doc, '</h2>')) {
196                                 $doc = substr_replace($doc, "\n\n".$warn, $offset + 5, 0);
197                         }
198                         else {
199                                 $doc = $warn . "\n\n" . $doc;
200                         }
201                 }
202
203                 # keep either login or logout parts depending on user level
204                 global $User;
205                 $hideclass = $User && property_exists($User, 'login') && $User->login ? 'logout' : 'login';
206                 $doc = preg_replace('{\s*<([a-z]+) class="'.$hideclass.'">.*?</\1>}s', '', $doc);
207
208                 return preg_replace_callback(
209                         '{ \[\[ ([^] ]+) ([^]]*) \]\] }x',
210                         function ($sub) {
211                                 list ($placeholder, $name, $params) = $sub;
212                                 $html = $this->place[$name] ??
213                                         $this->widget($name, explode(' ', $params));
214                                 if (empty($html) or $html[0] != '<') {
215                                         $html = "<span>$html</span>";
216                                 }
217                                 $attr = sprintf(' data-dyn="%s"', is_numeric($name) ? '' : $name.$params);
218                                 # contents with identifier in first tag
219                                 return preg_replace( '/(?=>)/', $attr, $html, 1);
220                         },
221                         $doc
222                 );
223         }
224 }
225
226 class PageSearch
227 {
228         function __construct($path = '.')
229         {
230                 $this->iterator = new RecursiveCallbackFilterIterator(
231                         new RecursiveDirectoryIterator($path),
232                         function ($current) {
233                                 if ($current->getFilename()[0] === '.') {
234                                         # skip hidden files and directories
235                                         return FALSE;
236                                 }
237                                 if ($current->isLink()) {
238                                         # ignore symlinks, original contents only
239                                         return FALSE;
240                                 }
241                                 if ($current->isDir()) {
242                                         # traverse subdirectories unless untracked in any amount
243                                         return !file_exists("$current/.gitignore");
244                                 }
245                                 # match **/*.html
246                                 return preg_match('/(?<!\.inc)\.html$/', $current->getFilename());
247                         }
248                 );
249         }
250
251         function files()
252         {
253                 # order alphabetically by link
254                 $dir = iterator_to_array(new RecursiveIteratorIterator($this->iterator));
255                 array_walk($dir, function (&$row, $name) {
256                         # prepare values for sorting (directory index first)
257                         $row = preg_replace('{/index\.html$}', '', $name);
258                 });
259                 asort($dir);
260                 return $dir;
261         }
262 }