Hostname of site. Used when generating HTML. --help Displays this message. -o --output Directory where site will be built into. "; exit(1); } function getSiteMetaData(string $src): array { $src = realpath($src); $iter = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($src, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ); $metaData = []; foreach ($iter as $fileInfo) { if (!is_file($fileInfo->getRealPath())) { continue; } $metaData[] = getFileMetaData($src, $fileInfo); } return $metaData; } function getFileMetaData(string $src, \SplFileInfo $fileInfo): array { $input = $fileInfo->getRealPath(); $url = str_replace($src, '', $input); $urlData = parseUrl($url); $contentData = parseContent(file_get_contents($input)); return [ 'input' => $input, 'url' => $url, 'date' => $urlData['date'], 'category' => $urlData['category'], 'isPost' => $urlData['isPost'], 'title' => $contentData['title'], 'author' => $contentData['author'], ]; } function parseUrl(string $url): array { $date = null; $category = null; $isPost = false; /** * Assume that only posts have both a date and a category. */ if (preg_match('/\/posts\/(.+)\/(\d\d\d\d-\d\d-\d\d)\//', $url, $matches) === 1) { $date = $matches[2]; $category = $matches[1]; $isPost = true; } return [ 'date' => $date, 'category' => $category, 'isPost' => $isPost, ]; } 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 buildGeminiSite(array $siteMetaData, string $output): void { foreach ($siteMetaData as $fileMetaData) { $destFile = $output.DIRECTORY_SEPARATOR.$fileMetaData['url']; $destDirectory = dirname($destFile); if (!file_exists($destDirectory) && !mkdir($destDirectory, 0777, true)) { echo "Unable to create Gemini site directory $destDirectory."; exit(1); } copy($fileMetaData['input'], $destFile); } } function buildWWWSite(array $siteMetaData, string $output, string $htmlTemplateDiretory, $assetsDiretory): void { foreach ($siteMetaData as $fileMetaData) { $destFile = $output.DIRECTORY_SEPARATOR.$fileMetaData['url']; $destDirectory = dirname($destFile); if (!file_exists($destDirectory) && !mkdir($destDirectory, 0777, true)) { echo "Unable to create WWW site directory $destDirectory."; exit(1); } file_put_contents( str_replace('.gmi', '.html', $destFile), buildHtmlFile( $fileMetaData, gemtext2hmtl(file_get_contents($fileMetaData['input'])), $htmlTemplateDiretory ) ); } copyWWWAssets($assetsDiretory, $output); } function buildHtmlFile(array $fileMetaData, string $contents, string $htmlTemplateDiretory): string { return str_replace( [ '{{ $title }}', '{{ $contents }}' ], [ $fileMetaData['title'], $contents, ], file_get_contents($htmlTemplateDiretory.DIRECTORY_SEPARATOR.'default.html') ); } function gemtext2hmtl(string $gemtext): string { return renderGemtextTokens(parseGemtext($gemtext)); } function parseGemtext(string $gemtext): array { $tokens = []; $lines = preg_split('/\r?\n/', htmlspecialchars($gemtext)); $index = 0; $numLines = count($lines); $line = fn($index) => trim($lines[$index]); while($index < $numLines) { if (preg_match(HEADER, $line($index), $matches) === 1) { [, $levels, $content] = $matches; $tokens[] = [ 'type' => HEADER, 'level' => strlen($levels), 'content' => $content, ]; } elseif (preg_match(LINK, $line($index), $matches) === 1) { [, $href, $content] = $matches; $tokens[] = [ 'type' => LINK, 'href' => $href, 'content' => $content != '' ? $content : $href, ]; } elseif (preg_match(LIST_ITEM, $line($index), $matches) === 1) { $items = []; while ($index < $numLines) { if (preg_match(LIST_ITEM, $line($index), $matches) === 0) { break; } [, $content] = $matches; $items[] = $content; $index++; } $index--; $tokens[] = [ 'type' => LIST_ITEM, 'items' => $items, ]; } elseif (preg_match(PRE, $line($index), $matches) === 1) { [, $alt] = $matches; $items = []; $index++; while ($index < $numLines) { $item = $line($index); if (preg_match(PRE, $item, $matches) === 1) { break; } $items[] = $item; $index++; } $tokens[] = [ 'type' => PRE, 'alt' => $alt, 'items' => $items, ]; } elseif (preg_match(QUOTE, $line($index), $matches) === 1) { [, $content] = $matches; $tokens[] = [ 'type' => QUOTE, 'content' => $content, ]; } else { $tokens[] = [ 'type' => null, 'content' => $line($index), ]; } $index++; } return $tokens; } function renderGemtextTokens(array $tokens): string { return implode("\n", array_filter(array_map(function ($token) { switch ($token['type']) { case HEADER: return sprintf('%s', $token['level'], $token['content'], $token['level']); case LINK: return sprintf('%s', $token['href'], $token['content']); case LIST_ITEM: return sprintf( "", implode("\n", array_map(function ($item) { return sprintf("
  • %s
  • ", $item);}, $token['items'])) ); case PRE: return $token['alt'] ? sprintf("
    \n%s\n
    ", $token['alt'], implode("\n", $token['items'])) : sprintf("
    \n%s\n
    ", $token['alt'], implode('', $token['items'])); case QUOTE: return sprintf('
    %s
    ', $token['content']); default: return $token['content'] !== '' ? sprintf('

    %s

    ', $token['content']) : null; } }, $tokens))); } function copyWWWAssets(string $assetsDiretory, string $output): void { $assetsDiretory = realpath($assetsDiretory); $iter = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($assetsDiretory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ); foreach ($iter as $fileInfo) { if (!is_file($fileInfo->getRealPath())) { continue; } $input = $fileInfo->getRealPath(); $url = str_replace($assetsDiretory, '', $input); $destFile = $output.DIRECTORY_SEPARATOR.$url; $destDirectory = dirname($destFile); if (!file_exists($destDirectory) && !mkdir($destDirectory, 0777, true)) { echo "Unable to create WWW asset directory $destDirectory."; exit(1); } copy($input, $destFile); } }