{\[|]/', $value) || strpos($value, ' #') !== false) { throw new RuntimeException('Unsupported YAML syntax. Use plain scalars, quoted strings or a tags list.'); } return $value; } public static function document($raw, $slug) { self::slug($slug); if (!preg_match('//u', $raw)) { throw new RuntimeException('Article must be UTF-8 text.'); } $raw = str_replace("\r\n", "\n", $raw); $raw = preg_replace('/\A\xEF\xBB\xBF/', '', $raw); if (!preg_match('/\A---\n(.*?)\n---[ \t]*(?:\n|\z)(.*)\z/s', $raw, $match)) { throw new RuntimeException('Expected YAML front matter between --- lines.'); } $meta = []; $inTags = false; foreach (explode("\n", $match[1]) as $line) { if (trim($line) === '' || preg_match('/^\s*#/', $line)) { continue; } if ($inTags && preg_match('/^ - (.+)$/', $line, $tag)) { $meta['tags'][] = self::scalar($tag[1]); continue; } $inTags = false; if (!preg_match('/^([a-z]+):[ \t]*(.*)$/', $line, $field)) { throw new RuntimeException('Unsupported front matter line: ' . $line); } $name = $field[1]; if (!in_array($name, ['title', 'date', 'slug', 'author', 'status', 'tags', 'description', 'category', 'cover'], true) || array_key_exists($name, $meta)) { throw new RuntimeException('Unknown or duplicate front matter field: ' . $name); } if ($name === 'tags') { $value = trim($field[2]); if ($value !== '' && $value !== '[]') { throw new RuntimeException('Use tags: [] or an indented list of tags.'); } $meta['tags'] = []; $inTags = $value === ''; } else { $meta[$name] = self::scalar($field[2]); } } if (empty($meta['title']) || (isset($meta['slug']) && $meta['slug'] !== $slug)) { throw new RuntimeException('A title is required and slug must match the selected article folder.'); } if (isset($meta['date']) && $meta['date'] !== '') { $date = DateTimeImmutable::createFromFormat('!Y-m-d', $meta['date']); if (!$date || $date->format('Y-m-d') !== $meta['date']) { throw new RuntimeException('Date must be a valid YYYY-MM-DD date.'); } } if (!in_array($meta['status'] ?? '', ['', 'draft', 'published', 'static', 'sticky', 'scheduled'], true)) { throw new RuntimeException('Unsupported article status.'); } if (trim($match[2]) === '') { throw new RuntimeException('Empty article content is not imported.'); } return ['meta' => $meta, 'content' => $match[2]]; } /** Only link/image destinations, not arbitrary prose or external URLs. */ public static function links($content, $rewrite = null) { $found = []; $pattern = '~((?:\b(?:src|href)\s*=\s*["\x27])|(?:!?\[[^\]\r\n]*\]\(\s*))([^"\x27\s<>\)]+)~i'; // Literal code examples must not be treated as assets to fetch or rewrite. $chunks = preg_split('~(
]*>.*?|
]*>.*?|^```[^\n]*\n.*?^```[ \t]*$|^\~\~\~[^\n]*\n.*?^\~\~\~[ \t]*$|`[^`\r\n]+`)~ims', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($chunks as $index => $chunk) {
if ($index % 2 !== 0) { continue; }
$chunks[$index] = preg_replace_callback($pattern, function ($m) use (&$found, $rewrite) {
$url = $m[2];
if (strpos($url, '../') !== 0) {
return $m[0];
}
if (!preg_match('~\A\.\./(img|files)/(.+)\z~', $url, $parts)) {
throw new RuntimeException('Unsupported relative asset link: ' . $url);
}
self::path($parts[1] . '/' . $parts[2]);
$found[$url] = $parts[1] . '/' . $parts[2];
return $m[1] . ($rewrite ? $rewrite($url) : $url);
}, $chunk);
}
return $rewrite ? implode('', $chunks) : $found;
}
public static function extension($path)
{
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (!in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'txt', 'pdf', 'csv', 'json', 'zip'], true)) {
throw new RuntimeException('Unsupported asset type: ' . $path . '. HTML, scripts and SVG are not imported.');
}
return $ext;
}
public static function blob($response, $sha, $limit)
{
if (!is_array($response) || ($response['encoding'] ?? '') !== 'base64' || !isset($response['content'])) {
throw new RuntimeException('GitHub did not return a base64 blob.');
}
$data = base64_decode(str_replace(["\n", "\r"], '', $response['content']), true);
if ($data === false || strlen($data) > $limit || sha1('blob ' . strlen($data) . "\0" . $data) !== $sha) {
throw new RuntimeException('Blob verification failed or file exceeds the import size limit.');
}
return $data;
}
}