getRealPath())) {
            continue;
        }
        $pages[] = getPageMetaData($fileInfo, $src, $output);
    }
    return $pages;
}
function getPageMetaData(\SplFileInfo $fileInfo, string $src, string $output): array
{
    $input = $fileInfo->getRealPath();
    $pathData = parsePath($input);
    $url = str_replace([$src, '.gmi'], ['', '.html'], $input);
    $contentData = parseContent(file_get_contents($input));
    return [
        'input'  => $input,
        'url'    => $url,
        'date'   => $pathData['date'],
        'tag'    => $pathData['tag'],
        'isPost' => $pathData['isPost'],
        'isTag'  => $pathData['isTag'],
        'title'  => $contentData['title'],
        'author' => $contentData['author'],
        'html'   => gemtext2hmtl(file_get_contents($input)),
        'output' => "$output$url",
    ];
}
function parsePath(string $path): array
{
    $date = null;
    $tag = null;
    $isPost = false;
    $isTag = false;
    /**
     * Assume that only posts have both a date and a tag.
     */
    if (preg_match('/\/posts\/(.+)\/(\d\d\d\d-\d\d-\d\d)\//', $path, $matches) === 1) {
        [, $tag, $date] = $matches;
        $isPost = true;
    }
    /**
     * Assume tags have an index file that contains related posts.
     */
    if (preg_match('/posts\/(?:[^\/]|\/\/)+\/index/', $path, $matches) === 1) {
        $tag = explode('/', $matches[0])[1];
        $isTag = true;
    }
    return [
        'date'   => $date,
        'tag'    => $tag,
        'isPost' => $isPost,
        'isTag'  => $isTag,
    ];
}
function parseContent(string $content): array
{
    $title = null;
    $author = null;
    if (preg_match('/^# (.+)$/m', $content, $matches) === 1) {
        $title = $matches[1];
    }
    if (preg_match('/^> .+ By (.+)$/m', $content, $matches) === 1) {
        $author = $matches[1];
    }
    return [
        'title'  => $title,
        'author' => $author,
    ];
}
function buildWWWSite(array $pages, string $hostname, string $htmlTemplateDiretory): void
{
    foreach ($pages as $page) {
        $destDirectory = dirname($page['output']);
        if (!file_exists($destDirectory) && !mkdir($destDirectory, 0777, true)) {
            echo "Unable to create WWW site directory $destDirectory.";
            exit(1);
        }
        file_put_contents(
            $page['output'],
            buildHtmlFile(
                $page['title'],
                $page['html'],
                file_get_contents($htmlTemplateDiretory.DIRECTORY_SEPARATOR.'default.html')
            )
        );
    }
    generateAtomFeeds($pages, $hostname);
}
function buildHtmlFile(string $title, string $contents, string $template): string
{
    return str_replace(
        [
            '{{ $title }}',
            '{{ $contents }}'
        ],
        [
            $title,
            $contents,
        ],
        $template
    );
}
function gemtext2hmtl(string $gemtext): string
{
    $html = [];
    $lines = preg_split('/\r?\n/', htmlspecialchars($gemtext));
    $line = fn ($index) => $lines[$index];
    $index = 0;
    $numLines = count($lines);
    while($index < $numLines) {
        if (preg_match(HEADER, $line($index), $matches) === 1) {
            [, $levels, $content] = $matches;
            $level = strlen($levels);
            $html[] = "
$items" : "$items"; } elseif (preg_match(QUOTE, $line($index), $matches) === 1) { $html[] = "
$matches[1]"; } else { if ($line($index) !== '') { $html[] = "
{$line($index)}
"; } } $index++; } return implode('', $html); } function generateAtomFeeds(array $pages, string $hostname): void { $posts = array_filter($pages, fn ($post) => $post['isPost']); $tags = array_filter($pages, fn ($post) => $post['isTag']); /** * Sort by latest to previous date. */ usort($posts, fn ($a, $b) => $b['date'] <=> $a['date']); /** * Group posts by tag. */ $groupedPosts = array_reduce($posts, function ($carry, $post) { $carry[$post['tag']][] = $post; return $carry; }, []); /** * Put posts with their tag page. */ $tags = array_map(function ($tag) use ($groupedPosts) { return array_merge($tag, ['posts' => $groupedPosts[$tag['tag']]]); }, $tags); foreach ($tags as $tag) { tagToAtomFeed($tag, $hostname); } /** * Get the page that lists all posts. */ $allPostsIndex = array_values(array_filter($pages, fn ($post) => preg_match('/posts\/index/', $post['url']) == 1))[0]; $allPostsIndex['posts'] = $posts; tagToAtomFeed($allPostsIndex, $hostname); } function tagToAtomFeed(array $tag, string $hostname): void { file_put_contents( str_replace('index.html', 'atom.xml', $tag['output']), buildAtomFeed( $tag['title'], "https://$hostname".str_replace('index.html', 'atom.xml', $tag['url']), "https://$hostname".$tag['url'], $tag['posts'][0]['date'].'T12:00:00Z', implode('', array_map(fn ($post) => postToAtomEntry($post, $hostname), $tag['posts'])) ) ); } function postToAtomEntry(array $post, string $hostname): string { return buildAtomEntry( $post['title'], "https://$hostname{$post['url']}", $post['author'], "{$post['date']}T12:00:00Z", $post['html'], ); } function buildAtomFeed(string $title, string $href, string $altHref, string $date, string $entries): string { return <<