@kirigami/php-prepros 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1114 -869
- package/index.d.ts +75 -2
- package/index.js +1 -1
- package/package.json +4 -4
- package/src/libraries/curl.class.php +5 -0
- package/src/libraries/html.class.php +23 -22
- package/src/libraries/img.class.php +356 -16
- package/src/libraries/md.class.php +476 -170
- package/src/libraries/md.plugins.php +19 -19
- package/src/libraries/normalizer.class.php +310 -0
- package/src/libraries/prepros.class.php +36 -7
- package/src/libraries/prepros.plugins.php +24 -0
- package/src/libraries/schema.class.php +550 -0
- package/src/libraries/scraper.class.php +64 -59
- package/src/libraries/str.class.php +49 -15
- package/src/libraries/yaml.class.php +66 -66
- package/src/phpjs/fstat.js +9 -0
- package/src/prepros.js +51 -44
- package/src/utils/getfilestats.js +30 -0
- package/src/utils/isbinary.js +14 -14
- package/src/utils.inc.php +9 -8
|
@@ -98,45 +98,45 @@ class SCRAPER
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
if(($results = $xpath->query('//meta[@name="dc.title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
|
|
102
|
-
if(($results = $xpath->query('//meta[@name="dcterms.title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
|
|
103
|
-
if(($results = $xpath->query('//meta[@name="twitter:title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
|
|
104
|
-
if(($results = $xpath->query('//meta[@property="og:title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
|
|
105
|
-
if(($results = $xpath->query('//shreddit-title')) && $results->length) $metas->title = getFirstItemAttr($results, 'title');
|
|
106
|
-
if(($results = $xpath->query('//meta[@name="dc.description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
|
|
107
|
-
if(($results = $xpath->query('//meta[@name="dcterms.description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
|
|
108
|
-
if(($results = $xpath->query('//meta[@name="twitter:description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
|
|
109
|
-
if(($results = $xpath->query('//meta[@property="og:description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
|
|
110
|
-
if(($results = $xpath->query('//meta[@name="description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
|
|
111
|
-
if(($results = $xpath->query('//meta[@name="twitter:image"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'content');
|
|
112
|
-
if(($results = $xpath->query('//meta[@property="og:image"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'content');
|
|
113
|
-
if(($results = $xpath->query('//meta[@name="og:image"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'content');
|
|
114
|
-
if(($results = $xpath->query('//img[@rel="og:image rdfs:seeAlso"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'src');
|
|
115
|
-
if(($results = $xpath->query('//link[@rel="canonical"]')) && $results->length) $metas->url = getFirstItemAttr($results, 'href');
|
|
116
|
-
if(($results = $xpath->query('//meta[@name="twitter:url"]')) && $results->length) $metas->url = getFirstItemAttr($results, 'content');
|
|
117
|
-
if(($results = $xpath->query('//meta[@property="og:url"]')) && $results->length) $metas->url = getFirstItemAttr($results, 'content');
|
|
118
|
-
|
|
119
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:iphone"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
120
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:googleplay"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
121
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="og:site_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
122
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="og:site_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
123
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dc.publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
124
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.Publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
125
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
126
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="article:author"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
127
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="author"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
128
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:creator"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
129
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dcterms.creator"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
130
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:ios:app_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
131
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:android:app_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
132
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
|
|
101
|
+
if(($results = $xpath->query('//meta[@name="dc.title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
|
|
102
|
+
if(($results = $xpath->query('//meta[@name="dcterms.title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
|
|
103
|
+
if(($results = $xpath->query('//meta[@name="twitter:title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
|
|
104
|
+
if(($results = $xpath->query('//meta[@property="og:title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
|
|
105
|
+
if(($results = $xpath->query('//shreddit-title')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'title');
|
|
106
|
+
if(($results = $xpath->query('//meta[@name="dc.description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
|
|
107
|
+
if(($results = $xpath->query('//meta[@name="dcterms.description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
|
|
108
|
+
if(($results = $xpath->query('//meta[@name="twitter:description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
|
|
109
|
+
if(($results = $xpath->query('//meta[@property="og:description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
|
|
110
|
+
if(($results = $xpath->query('//meta[@name="description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
|
|
111
|
+
if(($results = $xpath->query('//meta[@name="twitter:image"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'content');
|
|
112
|
+
if(($results = $xpath->query('//meta[@property="og:image"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'content');
|
|
113
|
+
if(($results = $xpath->query('//meta[@name="og:image"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'content');
|
|
114
|
+
if(($results = $xpath->query('//img[@rel="og:image rdfs:seeAlso"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'src');
|
|
115
|
+
if(($results = $xpath->query('//link[@rel="canonical"]')) && $results->length) $metas->url = self::getFirstItemAttr($results, 'href');
|
|
116
|
+
if(($results = $xpath->query('//meta[@name="twitter:url"]')) && $results->length) $metas->url = self::getFirstItemAttr($results, 'content');
|
|
117
|
+
if(($results = $xpath->query('//meta[@property="og:url"]')) && $results->length) $metas->url = self::getFirstItemAttr($results, 'content');
|
|
118
|
+
|
|
119
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:iphone"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
120
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:googleplay"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
121
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="og:site_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
122
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="og:site_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
123
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dc.publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
124
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.Publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
125
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
126
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="article:author"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
127
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="author"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
128
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:creator"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
129
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dcterms.creator"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
130
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:ios:app_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
131
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:android:app_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
132
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
|
|
133
133
|
if(empty($metas->label) && ($results = $xpath->query('//span[@class="footer__signature"]')) && $results->length) $metas->label = $results->item(0)->textContent;
|
|
134
134
|
if(empty($metas->label) && ($results = $xpath->query('//p[@class="site-title"]')) && $results->length) $metas->label = trim($results->item(0)->textContent);
|
|
135
|
-
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="copyright"]')) && $results->length) $metas->label = trim(getFirstItemAttr($results, 'content'), '© ');
|
|
135
|
+
if(empty($metas->label) && ($results = $xpath->query('//meta[@name="copyright"]')) && $results->length) $metas->label = trim(self::getFirstItemAttr($results, 'content'), '© ');
|
|
136
136
|
if(empty($metas->label) && preg_match("#\{'blogId': '[0-9]+', 'title': '([^']+)'#i", $contents, $m)) $metas->label = $m[1];
|
|
137
137
|
|
|
138
138
|
if(($results = $xpath->query('//link[@rel="alternate" and @type="application/json+oembed" and @href]')) && $results->length) {
|
|
139
|
-
if(STR::is_url(($oembedurl = getFirstItemAttr($results, 'href')))) {
|
|
139
|
+
if(STR::is_url(($oembedurl = self::getFirstItemAttr($results, 'href')))) {
|
|
140
140
|
if($json = self::getContents($oembedurl)) {
|
|
141
141
|
if($oem = json_decode($json)) {
|
|
142
142
|
if(empty($metas->title) && !empty($oem->title)) $metas->title = $oem->title;
|
|
@@ -171,7 +171,7 @@ class SCRAPER
|
|
|
171
171
|
}
|
|
172
172
|
if(empty($metas->label)) {
|
|
173
173
|
if(($results = $xpath->query('//link[@rel="manifest" and @href]')) && $results->length) {
|
|
174
|
-
if($manifesturl = get_absolute_url($url, getFirstItemAttr($results, 'href'))) {
|
|
174
|
+
if($manifesturl = get_absolute_url($url, self::getFirstItemAttr($results, 'href'))) {
|
|
175
175
|
if($json = self::getContents($manifesturl)) {
|
|
176
176
|
if($manifest = json_decode($json)) {
|
|
177
177
|
if(!empty($manifest->name)) $metas->label = $manifest->name;
|
|
@@ -181,10 +181,10 @@ class SCRAPER
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
if(empty($metas->label) && (($results = $xpath->query('//meta[@property="fb:page_id"]')) && $results->length)) {
|
|
184
|
-
if($_contents = self::getContents('https://facebook.com/' . getFirstItemAttr($results, 'content'))) {
|
|
184
|
+
if($_contents = self::getContents('https://facebook.com/' . self::getFirstItemAttr($results, 'content'))) {
|
|
185
185
|
if($xp = self::loadHTML($_contents)) {
|
|
186
186
|
if(($results = $xp->query('//meta[@property="og:title"]')) && $results->length) {
|
|
187
|
-
$metas->label = getFirstItemAttr($results, 'content');
|
|
187
|
+
$metas->label = self::getFirstItemAttr($results, 'content');
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -196,14 +196,14 @@ class SCRAPER
|
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
if(empty($metas->image) && ($results = $xpath->query('//span[@class="background-image not-mobile"]')) && $results->length)
|
|
199
|
-
if(preg_match('#url\(\'(.*?)\'\)#i', getFirstItemAttr($results, 'style'), $m))
|
|
199
|
+
if(preg_match('#url\(\'(.*?)\'\)#i', self::getFirstItemAttr($results, 'style'), $m))
|
|
200
200
|
$metas->image = $m[1];
|
|
201
201
|
|
|
202
|
-
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/jpeg"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
|
|
203
|
-
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/png"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
|
|
204
|
-
if(empty($metas->image) && ($results = $xpath->query('//article//img')) && $results->length) $metas->image = getFirstItemAttr($results, 'src');
|
|
205
|
-
if(empty($metas->image) && ($results = $xpath->query('//div[@id="image_box"]//img')) && $results->length) $metas->image = getFirstItemAttr($results, 'src');
|
|
206
|
-
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="shortcut icon" and @type="image/jpeg" and @href]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
|
|
202
|
+
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/jpeg"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
|
|
203
|
+
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/png"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
|
|
204
|
+
if(empty($metas->image) && ($results = $xpath->query('//article//img')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'src');
|
|
205
|
+
if(empty($metas->image) && ($results = $xpath->query('//div[@id="image_box"]//img')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'src');
|
|
206
|
+
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="shortcut icon" and @type="image/jpeg" and @href]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
|
|
207
207
|
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="apple-touch-icon" and @sizes]')) && $results->length) {
|
|
208
208
|
$size = 0;
|
|
209
209
|
foreach($results as $item) {
|
|
@@ -215,14 +215,14 @@ class SCRAPER
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
-
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="apple-touch-icon"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
|
|
218
|
+
if(empty($metas->image) && ($results = $xpath->query('//link[@rel="apple-touch-icon"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
|
|
219
219
|
if(empty($metas->title) && ($results = $xpath->query('//title')) && $results->length) $metas->title = $results->item(0)->textContent;
|
|
220
220
|
if(empty($metas->label)) { $parts = explode('|', $metas->title); if(count($parts) > 1) $metas->label = trim(array_pop($parts)); }
|
|
221
221
|
if(empty($metas->label)) { $parts = explode(' - ', $metas->title); if(count($parts) > 1) $metas->label = trim(array_pop($parts)); }
|
|
222
222
|
if(strpos($metas->label, '@') === 0) $metas->label = ucwords(preg_replace('#^@#i', '', $metas->label));
|
|
223
223
|
|
|
224
224
|
if(empty($metas->label) && ($results = $xpath->query('//div[@id="elevate-global" and @data-elevate-global]')) && $results->length)
|
|
225
|
-
if($data = json_decode(getFirstItemAttr($results, 'data-elevate-global')))
|
|
225
|
+
if($data = json_decode(self::getFirstItemAttr($results, 'data-elevate-global')))
|
|
226
226
|
if(!empty($data->content_organization))
|
|
227
227
|
$metas->label = $data->content_organization;
|
|
228
228
|
|
|
@@ -248,9 +248,9 @@ class SCRAPER
|
|
|
248
248
|
if($_contents = self::getContents(str_replace('\\/', '/', $m[1])))
|
|
249
249
|
if($xp = self::loadHTML($_contents))
|
|
250
250
|
if(($results = $xp->query('//link[@rel="preload" and @as="image" and contains(@href, ".jpg")]')) && $results->length)
|
|
251
|
-
$metas->image = getFirstItemAttr($results, 'href');
|
|
251
|
+
$metas->image = self::getFirstItemAttr($results, 'href');
|
|
252
252
|
|
|
253
|
-
if(!url_exists($metas->image, '^image/') && ($results = $xpath->query('//img[@src]')) && $results->length) {
|
|
253
|
+
if(!self::url_exists($metas->image, '^image/') && ($results = $xpath->query('//img[@src]')) && $results->length) {
|
|
254
254
|
foreach($results as $item) {
|
|
255
255
|
/** @var DOMElement $item */
|
|
256
256
|
if(in_array(pathinfo(parse_url($item->getAttribute('src'), PHP_URL_PATH), PATHINFO_EXTENSION), ['jpg', 'jpeg', 'png', 'webp'])) {
|
|
@@ -301,20 +301,25 @@ class SCRAPER
|
|
|
301
301
|
return $metas;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
}
|
|
305
304
|
|
|
305
|
+
private static function getFirstItemAttr(\Dom\Node|\Dom\NodeList $results, string $attr) {
|
|
306
|
+
/** @var DOM\Element $item */
|
|
307
|
+
$item = $results->item(0);
|
|
308
|
+
return $item->getAttribute($attr);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
private static function url_exists(string $url, string|null $mimereg = null) {
|
|
313
|
+
if(($exists = CACHE::get(($key = 'urlexists_' . STR::shorthash($url)))) !== null) return $exists->response;
|
|
314
|
+
$exists = new stdClass;
|
|
315
|
+
$exists->response = CURL::urlExists($url, $mimereg);
|
|
316
|
+
CACHE::set($key, $exists);
|
|
317
|
+
return $exists->response;
|
|
318
|
+
}
|
|
306
319
|
|
|
307
|
-
function getFirstItemAttr(\Dom\Node|\Dom\NodeList $results, string $attr) {
|
|
308
|
-
/** @var DOM\Element $item */
|
|
309
|
-
$item = $results->item(0);
|
|
310
|
-
return $item->getAttribute($attr);
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
$exists->response = CURL::urlExists($url, $mimereg);
|
|
318
|
-
CACHE::set($key, $exists);
|
|
319
|
-
return $exists->response;
|
|
320
|
-
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
@@ -12,21 +12,51 @@ class STR
|
|
|
12
12
|
|
|
13
13
|
public static function replaceTags(string $tag, string $contents, callable $clb): string
|
|
14
14
|
{
|
|
15
|
-
$
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
$t = preg_quote($tag, '#');
|
|
16
|
+
|
|
17
|
+
$pattern = '#<' . $t . '([^>]*)>(.*?)</' . $t . '>'
|
|
18
|
+
. '|<' . $t . '([^>]*)/>'
|
|
19
|
+
. '|<' . $t . '([^>]*)>#msi';
|
|
20
|
+
|
|
21
|
+
return preg_replace_callback($pattern, function ($m) use ($clb) {
|
|
22
|
+
if (isset($m[1]) || isset($m[2])) {
|
|
23
|
+
// paired form: <tag>content</tag>
|
|
24
|
+
$attrs = $m[1] ?? '';
|
|
25
|
+
$inner = $m[2] ?? '';
|
|
26
|
+
} elseif (isset($m[3])) {
|
|
27
|
+
// self-closing: <tag ... />
|
|
28
|
+
$attrs = $m[3];
|
|
29
|
+
$inner = '';
|
|
30
|
+
} else {
|
|
31
|
+
// opening only, no closing (img, meta, br, ...)
|
|
32
|
+
$attrs = $m[4] ?? '';
|
|
33
|
+
$inner = '';
|
|
34
|
+
}
|
|
35
|
+
return call_user_func($clb, $m[0], self::parseHtmlAttributes($attrs), $inner);
|
|
36
|
+
}, $contents, -1, $count, PREG_UNMATCHED_AS_NULL); // <-- the flag that fixes everything
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
|
|
22
40
|
public static function parseHtmlAttributes(string $attributes): array
|
|
23
41
|
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
$attrs = [];
|
|
43
|
+
if (preg_match_all('#(\w+)(?:\s*=\s*("[^"]*"|\'[^\']*\'|[^"\'\s>]*))?#i', $attributes, $m, PREG_SET_ORDER)) {
|
|
44
|
+
foreach ($m as $match) {
|
|
45
|
+
$key = strtolower($match[1]);
|
|
46
|
+
if (isset($match[2]) && $match[2] !== '') {
|
|
47
|
+
$value = $match[2];
|
|
48
|
+
// strip the quotes only if they're present
|
|
49
|
+
if (($value[0] === '"' || $value[0] === "'") && $value[0] === substr($value, -1)) {
|
|
50
|
+
$value = substr($value, 1, -1);
|
|
51
|
+
}
|
|
52
|
+
$attrs[$key] = stripslashes($value);
|
|
53
|
+
} else {
|
|
54
|
+
// boolean attribute: selected, muted, disabled, checked...
|
|
55
|
+
$attrs[$key] = true;
|
|
56
|
+
}
|
|
27
57
|
}
|
|
28
58
|
}
|
|
29
|
-
return
|
|
59
|
+
return $attrs;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
|
|
@@ -55,26 +85,30 @@ class STR
|
|
|
55
85
|
|
|
56
86
|
public static function html_entities_decode(string $str): string
|
|
57
87
|
{
|
|
58
|
-
// $str = html_entity_decode(trim($str), ENT_QUOTES, 'UTF-8');
|
|
59
|
-
// $str = htmlentities($str, ENT_QUOTES, 'UTF-8');
|
|
60
|
-
// $str = mb_convert_encoding($str, 'HTML-ENTITIES', 'UTF-8');
|
|
61
88
|
$str = html_entity_decode(trim($str), ENT_QUOTES, 'UTF-8');
|
|
62
|
-
// $str = htmlentities($str, ENT_QUOTES, 'UTF-8');
|
|
63
89
|
return $str;
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
|
|
67
|
-
public static function shorthash(string$str): string
|
|
93
|
+
public static function shorthash(string $str): string
|
|
68
94
|
{
|
|
69
95
|
return substr(hash('sha256', $str), 0, 12);
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
|
|
73
|
-
public static function
|
|
99
|
+
public static function normalize(string $str) {
|
|
100
|
+
$str = Normalizer::normalize($str, Normalizer::FORM_D);
|
|
101
|
+
$str = preg_replace('/\p{Mn}/u', '', $str);
|
|
102
|
+
return $str;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
public static function slug(string $str, $sep = ''): string
|
|
74
107
|
{
|
|
108
|
+
$str = self::normalize($str);
|
|
75
109
|
$str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
|
|
76
110
|
$str = strtolower($str);
|
|
77
|
-
$str = preg_replace('/[^a-z0-9]/',
|
|
111
|
+
$str = preg_replace('/[^a-z0-9]/', $sep, $str);
|
|
78
112
|
return $str;
|
|
79
113
|
}
|
|
80
114
|
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* YAML —
|
|
4
|
+
* YAML — lightweight, fully static YAML parser.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* $data = YAML::parse($yamlString); // mappings → stdClass (
|
|
8
|
-
* $data = YAML::parse($yamlString, true); // mappings → array
|
|
9
|
-
* $data = YAML::parseFile('/
|
|
6
|
+
* Usage:
|
|
7
|
+
* $data = YAML::parse($yamlString); // mappings → stdClass (default)
|
|
8
|
+
* $data = YAML::parse($yamlString, true); // mappings → associative array
|
|
9
|
+
* $data = YAML::parseFile('/path/to/config.yaml');
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
11
|
+
* Supports:
|
|
12
|
+
* - Typed scalars (string, int, float, bool, null)
|
|
13
|
+
* - Single and double quotes (with escape sequences)
|
|
14
|
+
* - Multi-line blocks (| literal and > folded, with chomping -, +)
|
|
15
|
+
* - Multi-line plain scalars (continuation with no indicator)
|
|
16
|
+
* - Nested mappings and sequences
|
|
17
|
+
* - Inline collections [a, b] and {k: v}
|
|
18
|
+
* - Comments (#)
|
|
19
|
+
* - Multiple documents separated by ---
|
|
20
20
|
*/
|
|
21
21
|
class YAML
|
|
22
22
|
{
|
|
23
23
|
private function __construct() {}
|
|
24
24
|
|
|
25
25
|
// -------------------------------------------------------------------------
|
|
26
|
-
//
|
|
26
|
+
// Public entry points
|
|
27
27
|
// -------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
public static function parse(string $yaml, bool $assoc = false): mixed
|
|
@@ -49,60 +49,60 @@ class YAML
|
|
|
49
49
|
public static function parseFile(string $path, bool $assoc = false): mixed
|
|
50
50
|
{
|
|
51
51
|
if (!is_readable($path)) {
|
|
52
|
-
throw new \RuntimeException("
|
|
52
|
+
throw new \RuntimeException("Cannot read file: $path");
|
|
53
53
|
}
|
|
54
54
|
return self::parse(file_get_contents($path), $assoc);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Loads a YAML or JSON file, then walks the result recursively and replaces
|
|
59
|
+
* any string value that matches a relative path to an existing YAML/JSON
|
|
60
|
+
* file with that file's deserialized content.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* Each included file is itself resolved relative to its own directory, and
|
|
63
|
+
* so on (recursive).
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* If the string doesn't end in .yml/.yaml/.json, or the resolved file
|
|
66
|
+
* doesn't exist, the value is kept as-is.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
68
|
+
* Circular references (e.g. A → B → A) throw a RuntimeException.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
* $data = YAML::loadFile('/
|
|
72
|
-
* $data = YAML::loadFile('/
|
|
70
|
+
* Usage:
|
|
71
|
+
* $data = YAML::loadFile('/path/to/config.yaml');
|
|
72
|
+
* $data = YAML::loadFile('/path/to/config.yaml', true); // assoc arrays
|
|
73
73
|
*
|
|
74
|
-
* @param string $path
|
|
75
|
-
* @param bool $assoc true → mappings
|
|
74
|
+
* @param string $path Path to the root file (YAML or JSON).
|
|
75
|
+
* @param bool $assoc true → mappings as arrays, false → stdClass.
|
|
76
76
|
* @return mixed
|
|
77
77
|
*/
|
|
78
78
|
public static function loadFile(string $path, bool $assoc = false): mixed
|
|
79
79
|
{
|
|
80
80
|
$absolute = realpath($path);
|
|
81
81
|
if ($absolute === false || !is_readable($absolute)) {
|
|
82
|
-
throw new \RuntimeException("
|
|
82
|
+
throw new \RuntimeException("Cannot read file: $path");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return self::loadFileRecursive($absolute, $assoc, []);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// -------------------------------------------------------------------------
|
|
89
|
-
//
|
|
89
|
+
// Private methods for loadFile
|
|
90
90
|
// -------------------------------------------------------------------------
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
93
|
+
* Loads and resolves a file, propagating the list of ancestors to detect
|
|
94
|
+
* cycles.
|
|
95
95
|
*
|
|
96
|
-
* @param string $absolute
|
|
96
|
+
* @param string $absolute Canonical absolute path of the file to load.
|
|
97
97
|
* @param bool $assoc
|
|
98
|
-
* @param string[] $ancestors
|
|
98
|
+
* @param string[] $ancestors Absolute paths of the files currently being processed.
|
|
99
99
|
* @return mixed
|
|
100
100
|
*/
|
|
101
101
|
private static function loadFileRecursive(string $absolute, bool $assoc, array $ancestors): mixed
|
|
102
102
|
{
|
|
103
103
|
if (in_array($absolute, $ancestors, true)) {
|
|
104
104
|
throw new \RuntimeException(
|
|
105
|
-
"
|
|
105
|
+
"Circular reference detected: " . implode(' → ', $ancestors) . " → $absolute"
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -113,7 +113,7 @@ class YAML
|
|
|
113
113
|
if ($ext === 'json') {
|
|
114
114
|
$data = json_decode($raw, $assoc, 512, JSON_THROW_ON_ERROR);
|
|
115
115
|
} else {
|
|
116
|
-
// .yml, .yaml
|
|
116
|
+
// .yml, .yaml or any other extension treated as YAML
|
|
117
117
|
$data = self::parse($raw, $assoc);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -121,11 +121,11 @@ class YAML
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* Recursively walks a PHP value (stdClass object, array, string, scalar)
|
|
125
|
+
* and resolves references to external files.
|
|
126
126
|
*
|
|
127
127
|
* @param mixed $node
|
|
128
|
-
* @param string $dir
|
|
128
|
+
* @param string $dir Directory of the file that contains this node.
|
|
129
129
|
* @param bool $assoc
|
|
130
130
|
* @param string[] $ancestors
|
|
131
131
|
* @return mixed
|
|
@@ -150,14 +150,14 @@ class YAML
|
|
|
150
150
|
return $node;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// int, float, bool, null →
|
|
153
|
+
// int, float, bool, null → returned as-is
|
|
154
154
|
return $node;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
* If the string points to an existing YAML/JSON file (path relative to the
|
|
159
|
+
* $dir directory), loads that file recursively. Otherwise returns the
|
|
160
|
+
* original string.
|
|
161
161
|
*
|
|
162
162
|
* @param string $str
|
|
163
163
|
* @param string $dir
|
|
@@ -169,24 +169,24 @@ class YAML
|
|
|
169
169
|
{
|
|
170
170
|
$trimmed = trim($str);
|
|
171
171
|
|
|
172
|
-
//
|
|
172
|
+
// Quick filter on the extension
|
|
173
173
|
if (!preg_match('/\.(ya?ml|json)$/i', $trimmed)) {
|
|
174
174
|
return $str;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// Resolve the path relative to the parent file's directory
|
|
178
178
|
$candidate = $dir . DIRECTORY_SEPARATOR . $trimmed;
|
|
179
179
|
$absolute = realpath($candidate);
|
|
180
180
|
|
|
181
181
|
if ($absolute === false || !is_readable($absolute)) {
|
|
182
|
-
return $str; //
|
|
182
|
+
return $str; // File not found → plain string
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
return self::loadFileRecursive($absolute, $assoc, $ancestors);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// -------------------------------------------------------------------------
|
|
189
|
-
//
|
|
189
|
+
// Recursive parsing
|
|
190
190
|
// -------------------------------------------------------------------------
|
|
191
191
|
|
|
192
192
|
private static function parseBlock(array $lines, int &$pos, int $indent, bool $assoc = false): mixed
|
|
@@ -231,13 +231,13 @@ class YAML
|
|
|
231
231
|
$pos++;
|
|
232
232
|
|
|
233
233
|
if ($rest === null) {
|
|
234
|
-
//
|
|
234
|
+
// Value on the following lines: sub-block or multi-line plain scalar
|
|
235
235
|
self::skipEmptyAndComments($lines, $pos);
|
|
236
236
|
if ($pos < count($lines)) {
|
|
237
237
|
$nextIndent = self::getIndent($lines[$pos]);
|
|
238
238
|
if ($nextIndent > $indent) {
|
|
239
239
|
$nextTrimmed = ltrim($lines[$pos]);
|
|
240
|
-
// Plain scalar
|
|
240
|
+
// Plain scalar if it's neither a mapping nor a sequence
|
|
241
241
|
if (!self::isMapping($nextTrimmed) && !str_starts_with($nextTrimmed, '- ')) {
|
|
242
242
|
$result[$key] = self::parseScalar(
|
|
243
243
|
self::collectPlainScalar($lines, $pos, $indent, '')
|
|
@@ -258,7 +258,7 @@ class YAML
|
|
|
258
258
|
} elseif ($rest !== '' && ($rest[0] === '[' || $rest[0] === '{')) {
|
|
259
259
|
$result[$key] = self::parseInlineCollection($rest, $assoc);
|
|
260
260
|
} else {
|
|
261
|
-
//
|
|
261
|
+
// Inline scalar, may be followed by continuation lines
|
|
262
262
|
$result[$key] = self::parseScalar(
|
|
263
263
|
self::collectPlainScalar($lines, $pos, $indent, $rest)
|
|
264
264
|
);
|
|
@@ -299,10 +299,10 @@ class YAML
|
|
|
299
299
|
$result[] = null;
|
|
300
300
|
}
|
|
301
301
|
} elseif (self::isMapping($itemContent)) {
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
302
|
+
// Inline mapping inside the sequence:
|
|
303
|
+
// Rebuild a virtual array of lines by prefixing the first key
|
|
304
|
+
// with fakeIndent, then delegate entirely to parseMapping to get
|
|
305
|
+
// all of its logic (|, >, plain scalars, sub-blocks…).
|
|
306
306
|
$fakeIndent = $lineIndent + 2;
|
|
307
307
|
$fakePrefix = str_repeat(' ', $fakeIndent);
|
|
308
308
|
$virtualLines = array_merge(
|
|
@@ -324,12 +324,12 @@ class YAML
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
// -------------------------------------------------------------------------
|
|
327
|
-
//
|
|
327
|
+
// Multi-line plain scalar
|
|
328
328
|
// -------------------------------------------------------------------------
|
|
329
329
|
|
|
330
330
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
331
|
+
* Collects a scalar that may continue on lines more indented than $parentIndent.
|
|
332
|
+
* Continuation lines are joined with a space (implicit folding).
|
|
333
333
|
*/
|
|
334
334
|
private static function collectPlainScalar(array $lines, int &$pos, int $parentIndent, string $first): string
|
|
335
335
|
{
|
|
@@ -339,18 +339,18 @@ class YAML
|
|
|
339
339
|
$raw = $lines[$pos];
|
|
340
340
|
$trimmed = trim($raw);
|
|
341
341
|
|
|
342
|
-
//
|
|
342
|
+
// Blank line: end of the scalar
|
|
343
343
|
if ($trimmed === '') break;
|
|
344
344
|
|
|
345
345
|
$lineIndent = self::getIndent($raw);
|
|
346
346
|
|
|
347
|
-
//
|
|
347
|
+
// Back to the parent indentation or less: end
|
|
348
348
|
if ($lineIndent <= $parentIndent) break;
|
|
349
349
|
|
|
350
|
-
//
|
|
350
|
+
// Comment alone on the line: end
|
|
351
351
|
if (str_starts_with($trimmed, '#')) break;
|
|
352
352
|
|
|
353
|
-
//
|
|
353
|
+
// It's a mapping or a sequence: end
|
|
354
354
|
if (self::isMapping($trimmed) || str_starts_with($trimmed, '- ')) break;
|
|
355
355
|
|
|
356
356
|
$parts[] = $trimmed;
|
|
@@ -361,7 +361,7 @@ class YAML
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
// -------------------------------------------------------------------------
|
|
364
|
-
//
|
|
364
|
+
// Multi-line blocks
|
|
365
365
|
// -------------------------------------------------------------------------
|
|
366
366
|
|
|
367
367
|
private static function parseLiteralBlock(array $lines, int &$pos, int $parentIndent, string $indicator): string
|
|
@@ -435,7 +435,7 @@ class YAML
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
// -------------------------------------------------------------------------
|
|
438
|
-
//
|
|
438
|
+
// Inline collections [a, b] {k: v}
|
|
439
439
|
// -------------------------------------------------------------------------
|
|
440
440
|
|
|
441
441
|
private static function parseInlineCollection(string $raw, bool $assoc = false): mixed
|
|
@@ -500,7 +500,7 @@ class YAML
|
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
// -------------------------------------------------------------------------
|
|
503
|
-
//
|
|
503
|
+
// Scalars
|
|
504
504
|
// -------------------------------------------------------------------------
|
|
505
505
|
|
|
506
506
|
private static function parseScalar(string $value): mixed
|
|
@@ -534,7 +534,7 @@ class YAML
|
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
// -------------------------------------------------------------------------
|
|
537
|
-
//
|
|
537
|
+
// Helpers
|
|
538
538
|
// -------------------------------------------------------------------------
|
|
539
539
|
|
|
540
540
|
private static function getIndent(string $line): int
|