650 lines
22 KiB
PHP
650 lines
22 KiB
PHP
<?php
|
||
namespace Std\Lib;
|
||
|
||
class Markdown {
|
||
private string $content;
|
||
private array $html;
|
||
private string $path;
|
||
private bool $inCodeBlock = false;
|
||
private string $codeBlockLanguage = '';
|
||
private array $codeBlockContent = [];
|
||
private const METADATA_LINE = "----";
|
||
private array $algebraicPlaceholder = [];
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* メタデータを取得する
|
||
*
|
||
* @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 {
|
||
$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 = explode("\n", $this->content);
|
||
$currentParagraph = [];
|
||
|
||
$inList = false;
|
||
$listItems = [];
|
||
$listLevel = 0;
|
||
$inBlockquote = false;
|
||
$blockquoteContent = [];
|
||
$tableHeaders = [];
|
||
$tableRows = [];
|
||
$inTable = false;
|
||
$i = 0;
|
||
$headerCount = 1;
|
||
|
||
foreach ($lines as $line) {
|
||
$i++;
|
||
$hasBR = substr($line, -1) === '\\';
|
||
$line = rtrim($line, " \t\r\n\\");
|
||
|
||
// コメント
|
||
// if (str_starts_with($line, '//')) {
|
||
// if ($hasBR && !empty($currentParagraph)) {
|
||
// $currentParagraph[] = '';
|
||
// }
|
||
// continue;
|
||
// }
|
||
|
||
// コードブロック
|
||
if (preg_match('/^```(\w*)$/', $line, $matches)) {
|
||
if (!$this->inCodeBlock) {
|
||
if (!empty($currentParagraph)) {
|
||
$this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$currentParagraph = [];
|
||
}
|
||
$this->inCodeBlock = true;
|
||
$this->codeBlockLanguage = $matches[1];
|
||
continue;
|
||
} else {
|
||
$this->html[] = $this->createCodeBlock();
|
||
$this->inCodeBlock = false;
|
||
$this->codeBlockLanguage = '';
|
||
$this->codeBlockContent = [];
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if ($this->inCodeBlock) {
|
||
$this->codeBlockContent[] = $line;
|
||
continue;
|
||
}
|
||
|
||
// テーブルの処理
|
||
if (preg_match('/^\|(.+)\|$/', $line)) {
|
||
if (!empty($currentParagraph)) {
|
||
$this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$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[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$currentParagraph = [];
|
||
}
|
||
$this->html[] = " <hr />\n";
|
||
continue;
|
||
}
|
||
|
||
// 引用ブロックの処理
|
||
if (preg_match('/^>\s(.+)/', $line, $matches)) {
|
||
if (!empty($currentParagraph)) {
|
||
$this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$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[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$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[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$currentParagraph = [];
|
||
}
|
||
|
||
$level = strlen($m[1]);
|
||
$this->html[] = " <h{$level} id=\"section-{$headerCount}\">".$this->parseInline($m[2])."</h{$level}>";
|
||
$headerCount++;
|
||
continue;
|
||
}
|
||
|
||
// 箇条書きリスト
|
||
if (preg_match('/^(\s*)([\*\-])\s(.+)/', $line, $m)) {
|
||
if (!empty($currentParagraph)) {
|
||
$this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$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[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
$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[] = "<br />\n ";
|
||
}
|
||
}
|
||
|
||
if ($inList) $this->html[] = $this->createList($listItems);
|
||
if ($inBlockquote) $this->html[] = $this->createBlockquote($blockquoteContent);
|
||
if ($inTable) $this->html[] = $this->createTable($tableHeaders, $tableRows);
|
||
if (!empty($currentParagraph))
|
||
$this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
|
||
|
||
return implode("\n", $this->html);
|
||
}
|
||
|
||
// 機能性メソッド
|
||
|
||
/**
|
||
* インラインのMarkdown記法をパースする
|
||
*
|
||
* @param string $text パースするテキスト
|
||
* @return string HTMLとしてレンダリングされたテキスト
|
||
*/
|
||
private function parseInline(string $text): string {
|
||
$this->algebraicPlaceholder = [];
|
||
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||
|
||
$patterns = [
|
||
// 数式
|
||
'/(?<!\\\\)\$\$([^$]+)\$\$/u' => function($matches): string {
|
||
$placeholder = "{{ALG".count($this->algebraicPlaceholder).'ALG}}';
|
||
$this->algebraicPlaceholder[$placeholder] = $matches[1];
|
||
return $placeholder;
|
||
},
|
||
// 太字
|
||
'/(?<!\\\\)\*\*(.+?)\*\*/u' => '<strong>$1</strong>',
|
||
// 斜体
|
||
'/(?<!\\\\)\*(.+?)\*/u' => '<em>$1</em>',
|
||
// 下線
|
||
'/(?<!\\\\)_(.+?)_/u' => '<u>$1</u>',
|
||
// 取り消し線
|
||
'/(?<!\\\\)~(.+?)~/u' => '<s>$1</s>',
|
||
// Blink (with speed)
|
||
'/(?<!\\\\)!:\((.+?)\)(.+?):!/u' => '<span class="blink" style="animation-duration: $1s;">$2</span>',
|
||
// Blink
|
||
'/(?<!\\\\)!:(.+?):!/u' => '<span class="blink">$1</span>',
|
||
// フォントの大きさ
|
||
'/(?<!\\\\)\^\((.+?)\)(.+?)\^/u' => '<span style="font-size: $1px;">$2</span>',
|
||
// フォントカラー
|
||
'/(?<!\\\\)\%\((.+?)\)(.+?)\%/u' => '<span style="color: #$1;">$2</span>',
|
||
// 画像
|
||
'/(?<!\\\\)\!\[(.*?)(?:#([^\]]*))?\]\((.+?)\)/u' => '<img style="width: 100%; $2" src="$3" alt="$1" />',
|
||
// 音楽
|
||
'/(?<!\\\\)\$\[([^\]]+)\]\(([^\)]+)\)/u' => '<audio controls><source src="$2" type="$1" /></audio>',
|
||
// 動画
|
||
'/(?<!\\\\)\#\[([^\]]+)\]\(([^\)]+)\)/u' => '<video style="max-width: 100%;" controls><source src="$2" type="$1" /></video>',
|
||
// リンク
|
||
'/(?<!\\\\)\[(.+?)\]\((.+?)\)/u' => '<a href="$2">$1</a>',
|
||
// 振り仮名
|
||
'/(?<!\\\\)<(.+?)>\((.+?)\)/u' => '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>',
|
||
// インラインコード
|
||
'/(?<!\\\\)`(.+?)`/u' => '<code>$1</code>',
|
||
];
|
||
|
||
foreach ($patterns as $pattern => $replacement) {
|
||
if (is_callable($replacement)) {
|
||
$text = preg_replace_callback($pattern, $replacement, $text);
|
||
} else {
|
||
$result = preg_replace($pattern, $replacement, $text);
|
||
if ($result === null) continue;
|
||
$text = $result;
|
||
}
|
||
}
|
||
|
||
// プレースホルダーの数式に交換
|
||
foreach ($this->algebraicPlaceholder as $placeholder => $expr) {
|
||
$text = str_replace($placeholder, $this->parseAlgebraic($expr), $text);
|
||
}
|
||
|
||
$text = preg_replace('/\\\\(.)/u', '$1', $text);
|
||
|
||
return $text;
|
||
}
|
||
|
||
/**
|
||
* 数式記法をパースする
|
||
*
|
||
* @param string $expression パースする数式
|
||
* @return string MathJax用にフォーマットされた数式
|
||
*/
|
||
private function parseAlgebraic(string $expression): string {
|
||
// HTMLエスケープ
|
||
$expression = htmlspecialchars_decode($expression, ENT_QUOTES);
|
||
|
||
// ふぃりしゃもじのマッピング
|
||
$greekLetters = [
|
||
'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ',
|
||
'epsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ',
|
||
'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ',
|
||
'nu' => 'ν', 'xi' => 'ξ', 'omicron' => 'ο', 'pi' => 'π',
|
||
'rho' => 'ρ', 'sigma' => 'σ', 'tau' => 'τ', 'upsilon' => 'υ',
|
||
'phi' => 'φ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω',
|
||
'Alpha' => 'Α', 'Beta' => 'Β', 'Gamma' => 'Γ', 'Delta' => 'Δ',
|
||
'Epsilon' => 'Ε', 'Zeta' => 'Ζ', 'Eta' => 'Η', 'Theta' => 'Θ',
|
||
'Iota' => 'Ι', 'Kappa' => 'Κ', 'Lambda' => 'Λ', 'Mu' => 'Μ',
|
||
'Nu' => 'Ν', 'Xi' => 'Ξ', 'Omicron' => 'Ο', 'Pi' => 'Π',
|
||
'Rho' => 'Ρ', 'Sigma' => 'Σ', 'Tau' => 'Τ', 'Upsilon' => 'Υ',
|
||
'Phi' => 'Φ', 'Chi' => 'Χ', 'Psi' => 'Ψ', 'Omega' => 'Ω',
|
||
];
|
||
|
||
// ギリシャ文字を置換
|
||
foreach ($greekLetters as $text => $symbol) {
|
||
$expression = preg_replace("/\b$text\b/", $symbol, $expression);
|
||
}
|
||
|
||
// 上付き文字(例: x^2)
|
||
$expression = preg_replace_callback('/(\w+)\^(\{?(\d+|\w+)\}?)/', function($matches) {
|
||
$base = $matches[1];
|
||
$exponent = $matches[2];
|
||
if ($exponent[0] === '{') $exponent = substr($exponent, 1, -1);
|
||
|
||
$superscripts = str_split($exponent);
|
||
$supMap = [
|
||
'0' => '⁰', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴',
|
||
'5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹',
|
||
'a' => 'ᵃ', 'b' => 'ᵇ', 'c' => 'ᶜ', 'd' => 'ᵈ', 'e' => 'ᵉ',
|
||
'i' => 'ⁱ', 'n' => 'ⁿ',
|
||
];
|
||
$converted = '';
|
||
|
||
foreach ($superscripts as $char) $converted .= $supMap[$char] ?? $char;
|
||
|
||
return $base.'<sup>'.$converted.'</sup>';
|
||
}, $expression);
|
||
|
||
// 下付き文字(例: x_1)
|
||
$expression = preg_replace_callback('/(\w+)_((\d+|\w+)\}?)/', function($matches) {
|
||
$base = $matches[1];
|
||
$subscript = $matches[2];
|
||
|
||
if ($subscript[0] === '{') $subscript = substr($subscript, 1, -1);
|
||
|
||
$subscripts = str_split($subscript);
|
||
$subMap = [
|
||
'0' => '₀', '1' => '₁', '2' => '₂', '3' => '₃', '4' => '₄',
|
||
'5' => '₅', '6' => '₆', '7' => '₇', '8' => '₈', '9' => '₉',
|
||
];
|
||
$converted = '';
|
||
|
||
foreach ($subscripts as $char) {
|
||
$converted .= $subMap[$char] ?? $char;
|
||
}
|
||
|
||
return $base.'<sub>'.$converted.'</sub>';
|
||
}, $expression);
|
||
|
||
// 分数(例: 4/5x, a/b)
|
||
$expression = preg_replace('/(\d+|\w+)\/(\d*\w+)/',
|
||
'<span class="fraction"><span class="numerator">$1</span><span class="denominator">$2</span></span>',
|
||
$expression);
|
||
|
||
// 演算子
|
||
$operators = [
|
||
'\*' => '・',
|
||
'!=' => '≠',
|
||
'>=' => '≥',
|
||
'<=' => '≤',
|
||
'sqrt' => '√',
|
||
];
|
||
|
||
foreach ($operators as $op => $symbol) $expression = preg_replace("/\b$op\b/", $symbol, $expression);
|
||
|
||
// 数式全体をspanで囲む
|
||
return '<span class="algebraic">'.$expression.'</span>';
|
||
}
|
||
|
||
/**
|
||
* リストを作成する
|
||
*
|
||
* @param array $items リストアイテムの配列
|
||
* @param int $maxLevel 最大ネストレベル
|
||
* @return string HTMLのリスト
|
||
*/
|
||
private function createList(array $items, int $maxLevel = 1): string {
|
||
if ($items === []) return '';
|
||
|
||
$html = '';
|
||
$currentLevel = 0;
|
||
$listStack = [];
|
||
$currentType = '';
|
||
|
||
foreach ($items as $item) {
|
||
$level = isset($item['level']) ? $item['level'] : $currentLevel;
|
||
|
||
while ($currentLevel > $level)
|
||
$html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n";
|
||
|
||
while ($currentLevel < $level) {
|
||
$currentLevel++;
|
||
$listStack[] = $item['type'];
|
||
$html .= str_repeat(' ', $currentLevel - 1)."<".$item['type'].">\n";
|
||
}
|
||
|
||
if ($currentType != $item['type'] && $currentLevel == $item['level']) {
|
||
if (!empty($listStack)) {
|
||
$html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n";
|
||
$listStack[] = $item['type'];
|
||
$html .= str_repeat(' ', $currentLevel - 1)."<".$item['type'].">\n";
|
||
}
|
||
}
|
||
|
||
$currentType = $item['type'];
|
||
}
|
||
|
||
$html .= " <p>\n <{$currentType}>\n";
|
||
foreach ($items as $item) {
|
||
$html .= str_repeat(' ', $currentLevel)." <li>".$item['content']."</li>\n";
|
||
}
|
||
$html .= " </{$currentType}>\n </p>";
|
||
|
||
while (!empty($listStack)) {
|
||
$html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n";
|
||
$currentLevel--;
|
||
}
|
||
|
||
return rtrim($html);
|
||
}
|
||
|
||
/**
|
||
* コードブロックを作成する
|
||
*
|
||
* @return string HTMLのコードブロック
|
||
*/
|
||
private function createCodeBlock(): string {
|
||
$code = htmlspecialchars(implode("\n", $this->codeBlockContent));
|
||
$class = $this->codeBlockLanguage ? " class=\"language-{$this->codeBlockLanguage}\"" : '';
|
||
return " <p>\n <pre><code{$class}>{$code}</code></pre>";
|
||
// $raw = implode("\n", $this->codeBlockContent);
|
||
// $lang = $this->codeBlockLanguage ?: 'txt';
|
||
// $class = $lang === 'txt' ? '' : " class=\"language-{$lang}\"";
|
||
// $highlighted = $this->highlightCode($raw, $lang);
|
||
// return "<pre><code{$class}>{$highlighted}</code></pre>";
|
||
}
|
||
|
||
private function createBlockquote(array $content): string {
|
||
return " <p>\n <blockquote>\n ".implode("<br />\n ", $content)."\n </blockquote>\n </p>";
|
||
}
|
||
|
||
/**
|
||
* テーブルを作成する
|
||
*
|
||
* @param array $headers ヘッダー配列
|
||
* @param array $rows 行データの配列
|
||
* @return string HTMLのテーブル
|
||
*/
|
||
private function createTable(array $headers, array $rows): string {
|
||
$html = " <p>\n <table>\n";
|
||
|
||
// ヘッダーを追加
|
||
if (!empty($headers)) {
|
||
$html .= " <thead>\n <tr>\n";
|
||
foreach ($headers as $header) {
|
||
$html .= " <th>".$this->parseInline($header)."</th>\n";
|
||
}
|
||
$html .= " </tr>\n </thead>\n";
|
||
}
|
||
|
||
// 行を追加
|
||
if (!empty($rows)) {
|
||
$html .= " <tbody>\n";
|
||
foreach ($rows as $row) {
|
||
$html .= " <tr>\n";
|
||
foreach ($row as $cell) {
|
||
$html .= " <td>".$this->parseInline($cell)."</td>\n";
|
||
}
|
||
$html .= " </tr>\n";
|
||
}
|
||
$html .= " </tbody>\n";
|
||
}
|
||
|
||
$html .= " </table>\n </p>";
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* Pure-PHP syntax highlighter
|
||
*
|
||
* @param string $code Raw source code
|
||
* @param string $lang Language identifier (html, css, js, php, …)
|
||
* @return string HTML with <span class="hl-…"> tokens
|
||
*/
|
||
private function highlightCode(string $code, string $lang): string
|
||
{
|
||
$lang = strtolower(trim($lang));
|
||
$rules = [];
|
||
|
||
// ---- COMMON -------------------------------------------------------
|
||
$rules['comment'] = [
|
||
// /* … */ (multi-line)
|
||
['/\/\*(?:.|\n)*?\*\//', 'hl-comment'],
|
||
// // …\n
|
||
['/\/\/.*(?=\n|$)/', 'hl-comment'],
|
||
];
|
||
|
||
$rules['string'] = [
|
||
// "…" and '…' (allow escaped quotes)
|
||
['/"(?:\\.|[^"\\\n])*"/', 'hl-string'],
|
||
["/'(?:\\.|[^'\\\n])*'/", 'hl-string'],
|
||
];
|
||
|
||
$rules['number'] = [
|
||
['/\b(?:0x[0-9a-fA-F]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/', 'hl-number'],
|
||
];
|
||
|
||
// ---- HTML ---------------------------------------------------------
|
||
if ($lang === 'html') {
|
||
$rules['tag'] = [['/<\/?[a-zA-Z0-9\-]+/', 'hl-tag']];
|
||
$rules['attr'] = [['/\s[a-zA-Z0-9\-]+(?==)/', 'hl-attr']];
|
||
$rules['value'] = [['/(?<==)"(?:\\.|[^"\\\n])*"(?=[>\s])/', 'hl-value']];
|
||
}
|
||
|
||
// ---- CSS ----------------------------------------------------------
|
||
if ($lang === 'css') {
|
||
$rules['selector'] = [['/^[\t ]*[a-zA-Z0-9#\.\-\:$$ $$\=\*\>\+\~\^]+(?=\s*\{)/m', 'hl-selector']];
|
||
$rules['property'] = [['/\b[a-z\-]+(?=\s*:)/', 'hl-property']];
|
||
$rules['value'] = [['/(?<=\:)\s*[^;]+(?=;)/', 'hl-value']];
|
||
$rules['unit'] = [['/\b\d+(px|em|rem|%|vh|vw|pt|pc|cm|mm|in)\b/i', 'hl-unit']];
|
||
}
|
||
|
||
// ---- PHP ----------------------------------------------------------
|
||
if ($lang === 'php') {
|
||
$keywords = 'abstract|and|array|as|break|callable|case|catch|class|clone|'
|
||
. 'const|continue|declare|default|die|do|echo|else|elseif|empty|'
|
||
. 'enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|'
|
||
. 'extends|final|finally|for|foreach|function|global|goto|if|'
|
||
. 'implements|include|include_once|instanceof|insteadof|interface|'
|
||
. 'isset|list|namespace|new|or|print|private|protected|public|'
|
||
. 'require|require_once|return|static|switch|throw|trait|try|unset|'
|
||
. 'use|var|while|xor|yield';
|
||
$rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']];
|
||
$rules['function']= [['/\b([a-zA-Z_\x7f-\xff][\w\x7f-\xff]*)(?=\s*\()/', 'hl-function']];
|
||
$rules['variable']= [['/\$[a-zA-Z_\x7f-\xff][\w\x7f-\xff]*/', 'hl-variable']];
|
||
}
|
||
|
||
// ---- Shell --------------------------------------------------------
|
||
if ($lang === 'sh') {
|
||
$keywords = 'if|then|else|elif|fi|case|esac|for|select|while|until|do|done|in';
|
||
$rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']];
|
||
$rules['variable']= [['/\$\{?[a-zA-Z_][\w]*\}?/', 'hl-variable']];
|
||
}
|
||
|
||
// ---- JSON ---------------------------------------------------------
|
||
if ($lang === 'json') {
|
||
$rules['property'] = [['/"([^"]+)":/', '<span class="hl-property">$1</span>:']];
|
||
}
|
||
|
||
// ---- C / C++ -----------------------------------------------
|
||
if (in_array($lang, ['c', 'cpp', 'cs'])) {
|
||
$keywords = 'abstract|assert|boolean|break|byte|case|catch|char|class|const|'
|
||
. 'continue|default|do|double|else|enum|extends|false|final|finally|'
|
||
. 'float|for|goto|if|implements|import|instanceof|int|interface|long|'
|
||
. 'native|new|null|package|private|protected|public|return|short|static|'
|
||
. 'strictfp|super|switch|synchronized|this|throw|throws|transient|true|'
|
||
. 'try|void|volatile|while';
|
||
$rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']];
|
||
$rules['type'] = [['/\b(?:int|float|double|char|bool|void|string)\b/', 'hl-type']];
|
||
$rules['define'] = [['/(#include)|(#define)/', 'hl-define']];
|
||
}
|
||
|
||
if ($lang === 'markdown') {
|
||
$rules['header'] = [['/^(#{1,6})\s+(.+)$/', '<span class="hl-header">$1 $2</span>']];
|
||
$rules['link'] = [['/\[([^\]]+)\]\(([^)]+) $$/', '<span class="hl-link">[$1]($2)</span>']];
|
||
}
|
||
|
||
$html = htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8');
|
||
|
||
$order = ['comment', 'string', 'number'];
|
||
foreach (['tag','attr','value','selector','property','unit','keyword','function','variable','type','header','link'] as $k) {
|
||
if (isset($rules[$k])) $order[] = $k;
|
||
}
|
||
|
||
foreach ($order as $type) {
|
||
if (!isset($rules[$type])) continue;
|
||
foreach ($rules[$type] as $rule) {
|
||
[$pattern, $class] = $rule;
|
||
$html = preg_replace_callback(
|
||
$pattern,
|
||
function ($m) use ($class) {
|
||
// For JSON property we already built the <span>
|
||
if (strpos($m[0], '<span') === 0) return $m[0];
|
||
$inner = $m[0];
|
||
// Preserve already escaped entities inside strings/comments
|
||
return '<span class="'.$class.'">'.$inner.'</span>';
|
||
},
|
||
$html
|
||
);
|
||
}
|
||
}
|
||
|
||
return $html;
|
||
}
|
||
} |