'C', 'cpp' => 'C++', 'cc' => 'C++', 'hpp' => 'C++', 'cs' => 'C#', 'slang' => 'Slang', 'glsl' => 'GLSL', 'hlsl' => 'HLSL', 'msl' => 'MSL', 'wgsl' => 'WGSL', 'spirv' => 'SPIR-V', 'asm' => 'Assembly', 'php' => 'PHP', 'js' => 'JavaScript', 'sh' => 'Shell', 'txt' => 'Text', 'json' => 'JSON', 'go' => 'Go', 'rust' => 'Rust', 'zig' => 'Zig', 'odin' => 'Odin', 'sql' => 'SQL', ]; public function __construct(string $path, string $section, ?string $lang = null) { $this->html = []; if ($lang) $this->path = ROOT.$section.$lang.'/'.$path.'.md'; else $this->path = ROOT.$section.$path.'.md'; if (!file_exists($this->path)) { header('Location: /404'); exit(); } } /** * 文字列から(READMEファイル等、メタデータ無し) * * @param string $text Markdown * @return self */ public static function fromString(string $text): self { $m = (new \ReflectionClass(self::class))->newInstanceWithoutConstructor(); $m->html = []; $m->path = ''; $m->text = $text; return $m; } /** * メタデータを取得する * * @return \stdClass メタデータオブジェクト */ public function getMetadata(): \stdClass { $content = file_get_contents($this->path); $metadata = new \stdClass(); $parts = explode(self::METADATA_LINE, $content, 2); if (count($parts) < 2) return $metadata; $lines = explode("\n", trim($parts[0])); foreach ($lines as $line) { $line = trim($line); if (empty($line)) continue; $colonPos = strpos($line, ':'); if ($colonPos === false) continue; $key = trim(substr($line, 0, $colonPos)); $value = trim(substr($line, $colonPos + 1)); $value = trim($value, '"\''); if ($key == 'category' || $key == 'css') { $cat = explode(',', $value); $value = $cat; } $metadata->$key = $value; } return $metadata; } /** * Markdownをパースする * * @return string HTMLとしてレンダリングされたコンテンツ */ public function parse(): string { if ($this->text !== null) { $this->content = trim(str_replace(["\r\n", "\r"], "\n", $this->text)); } else { $content = file_get_contents($this->path); $parts = explode(self::METADATA_LINE, $content, 2); $this->content = count($parts) > 1 ? trim($parts[1]): trim($content); } $this->html = []; $lines = self::removeComments(explode("\n", $this->content)); $currentParagraph = []; $inList = false; $listItems = []; $listLevel = 0; $inBlockquote = false; $blockquoteContent = []; $tableHeaders = []; $tableRows = []; $inTable = false; $inMathBlock = false; $mathLines = []; $inTabs = false; $tabItems = []; $codeBlockLabel = ''; $headerCount = 1; $this->mathErrors = []; $i = 0; $flush = function() use (&$currentParagraph, &$inList, &$listItems, &$listLevel, &$inTable, &$tableHeaders, &$tableRows, &$inBlockquote, &$blockquoteContent): void { if ($inList) { $this->html[] = $this->createList($listItems); $listItems = []; $inList = false; $listLevel = 0; } if ($inTable) { $this->html[] = $this->createTable($tableHeaders, $tableRows); $tableHeaders = []; $tableRows = []; $inTable = false; } if ($inBlockquote) { $this->html[] = $this->createBlockquote($blockquoteContent); $blockquoteContent = []; $inBlockquote = false; } if (!empty($currentParagraph)) { $this->html[] = "
\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } }; foreach ($lines as $line) { $i++; $rawLine = rtrim($line, "\r\n"); // コードブロック(```言語 ラベル) if (preg_match('/^```([\w\+\-#\.]*)(?:\s+(.*\S))?\s*$/u', $rawLine, $matches)) { if (!$this->inCodeBlock) { $flush(); $this->inCodeBlock = true; $this->codeBlockLanguage = $matches[1]; $codeBlockLabel = $matches[2] ?? ''; continue; } else { if ($inTabs) { $tabItems[] = [ 'lang' => $this->codeBlockLanguage, 'label' => $codeBlockLabel, 'code' => $this->codeBlockContent, ]; } else { $this->html[] = $this->createCodeBlock(); } $this->inCodeBlock = false; $this->codeBlockLanguage = ''; $this->codeBlockContent = []; $codeBlockLabel = ''; continue; } } if ($this->inCodeBlock) { $this->codeBlockContent[] = $rawLine; continue; } if ($inMathBlock) { if (trim($rawLine) === '$$') { $this->html[] = $this->createDisplayMath(implode("\n", $mathLines)); $mathLines = []; $inMathBlock = false; } else { $mathLines[] = $rawLine; } continue; } if (trim($rawLine) === '$$') { $flush(); $inMathBlock = true; continue; } // 1行の数式ブロック:$$…$$だけの行 / a line holding only $$…$$ is display math if (preg_match('/^\s*\$\$(.+)\$\$\s*$/u', $rawLine, $matches) && substr_count($rawLine, '$$') === 2) { $flush(); $this->html[] = $this->createDisplayMath($matches[1]); continue; } // コードタブ(:::tabs … :::) / code tabs if (preg_match('/^:::\s*tabs\s*$/', trim($rawLine))) { $flush(); $inTabs = true; $tabItems = []; continue; } if ($inTabs) { if (trim($rawLine) === ':::') { $this->html[] = $this->createTabs($tabItems); $tabItems = []; $inTabs = false; } continue; // タブの中ではコードブロック以外は無視 / only code blocks inside tabs } $hasBR = str_ends_with($rawLine, '\\'); $line = rtrim($rawLine, "\\"); // テーブルの処理 if (preg_match('/^\|(.+)\|$/', $line)) { if (!empty($currentParagraph)) { $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } $cells = array_map('trim', explode('|', trim($line, '|'))); if (!$inTable) { $tableHeaders = $cells; $inTable = true; } elseif (preg_match('/^\|(\s*:?-+:?\s*\|)+$/', $line)) { continue; } else { $tableRows[] = $cells; } continue; } elseif ($inTable) { $this->html[] = $this->createTable($tableHeaders, $tableRows); $tableHeaders = []; $tableRows = []; $inTable = false; } // 水平線の処理 if (preg_match('/^([\-\*\_])\1{2,}$/', $line)) { if (!empty($currentParagraph)) { $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } $inBlockquote = true; $blockquoteContent[] = $this->parseInline($matches[1]); continue; } elseif ($inBlockquote && empty($line)) { $this->html[] = $this->createBlockquote($blockquoteContent); $blockquoteContent = []; $inBlockquote = false; continue; } // 空行をスキップ if (empty($line)) { if ($inList) { $this->html[] = $this->createList($listItems); $listItems = []; $inList = false; } if (!empty($currentParagraph)) { $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } continue; } // ヘッダー if (preg_match('/^(#{1,6})\s(.+)/', $line, $m)) { if ($inList) { $this->html[] = $this->createList($listItems); $listItems = []; $inList = false; } if (!empty($currentParagraph)) { $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } $level = strlen($m[1]); $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } $inList = true; $currentLevel = strlen($m[1]) / 2; $listLevel = max($listLevel, $currentLevel); $listItems[] = [ 'content' => $this->parseInline($m[3]), 'level' => $currentLevel, 'type' => 'ul', ]; continue; } // 番号付きリスト if (preg_match('/^(\s*)\d+\.\s(.+)/', $line, $m)) { if (!empty($currentParagraph)) { $this->html[] = "\n ".implode("", $currentParagraph)."\n
"; $currentParagraph = []; } $inList = true; $currentLevel = strlen($m[1]) / 2; $listLevel = max($listLevel, $currentLevel); $listItems[] = [ 'content' => $this->parseInline($m[2]), 'level' => $currentLevel, 'type' => 'ol', ]; continue; } if ($inList) { $this->html[] = $this->createList($listItems); $listItems = []; $inList = false; $listLevel = 0; } $parsedLine = $this->parseInline($line); $currentParagraph[] = $parsedLine; if ($hasBR) { $currentParagraph[] = "'.$code.'';
return $placeholder;
}, $text);
$patterns = [
// 数式
'/(? function($matches): string {
$placeholder = "{{ALG".count($this->algebraicPlaceholder).'ALG}}';
$this->algebraicPlaceholder[$placeholder] = $matches[1];
return $placeholder;
},
// 太字
'/(? '$1',
// 斜体
'/(? '$1',
// 下線
'/(? '$1',
// 取り消し線
'/(? '{$code}\n <{$currentType}>\n"; foreach ($items as $item) { $html .= str_repeat(' ', $currentLevel)."
\n
{$code}";
}
/**
* コードをHTMLにする(対応している言語はシンタックスハイライトする)
*
* @param string $raw 生のコード
* @param string $lang 言語名(```の後)
* @return array [エスケープ済みのコード, class属性]
*/
private function renderCode(string $raw, string $lang): array {
$lang = strtolower($lang);
$attr = htmlspecialchars($lang, ENT_QUOTES, 'UTF-8');
// Syntax::colorize()はエスケープ済みのHTMLを返す
if ($lang !== '' && Syntax::isSupported($lang)) {
return [(new Syntax($lang))->colorize($raw), " class=\"language-{$attr} hl\""];
}
return [htmlspecialchars($raw), $lang !== '' ? " class=\"language-{$attr}\"" : ''];
}
private function createBlockquote(array $content): string {
return " \n
\n ".implode("\n "; } /** * テーブルを作成する * * @param array $headers ヘッダー配列 * @param array $rows 行データの配列 * @return string HTMLのテーブル */ private function createTable(array $headers, array $rows): string { $html = "
\n ", $content)."\n
\n
| ".$this->parseInline($header)." | \n"; } $html .= "
|---|
| ".$this->parseInline($cell)." | \n"; } $html .= "