Merge branch 'master' of 192.168.10.104:suwako/LittleBeast into syntax
諏訪子 <suwako@076.moe>、2026-02-27 03:28
コミット e5b63c7fd62912c2232317dbc7ae73524dc1b767
・親 66a70f2
・親 a1bd29c
・ファイルを見る
82個のファイル、+6622 -6599
変更
autoload.php
| @@ -1,9 +1,11 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | spl_autoload_register(function ($class): void { |
| 3 | - $prefix = 'Site\\'; | |
| 3 | + $prefixes = ['Site\\', 'Std\\']; | |
| 4 | 4 | $base = realpath(__DIR__.'/src'); |
| 5 | + $prefixExist = false; | |
| 5 | 6 | |
| 6 | - if (strncmp($prefix, $class, strlen($prefix)) !== 0) return; | |
| 7 | + foreach ($prefixes as $prefix) { if (strncmp($prefix, $class, strlen($prefix)) === 0) $prefixExist = true; } | |
| 8 | + if (!$prefixExist) return; | |
| 7 | 9 | $ps = str_replace('\\', DIRECTORY_SEPARATOR, $class); |
| 8 | 10 | $file = $base.DIRECTORY_SEPARATOR.$ps.'.php'; |
| 9 | 11 | if (file_exists($file)) require $file; |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # ルートの仕組み |
| 2 | 2 | |
| 3 | -`route.php` では、 `use Site\Lib\Route; `というインクルードもある事に | |
| 3 | +`route.php` では、 `use Std\Lib\Route; `というインクルードもある事に | |
| 4 | 4 | 気づくでしょう。 |
| 5 | 5 | これが全てのルートを処理します。 |
| 6 | 6 | ルートの基本的な流れは次の通りです: |
変更
route.php
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | require_once __DIR__.DIRECTORY_SEPARATOR.'/autoload.php'; |
| 4 | 4 | |
| 5 | -use Site\Lib\Route; | |
| 5 | +use Std\Lib\Route; | |
| 6 | 6 | |
| 7 | 7 | use Site\Controller\Atom; |
| 8 | 8 | use Site\Controller\Fediverse; |
| @@ -19,7 +19,6 @@ | ||
| 19 | 19 | ini_set('display_errors', 1); |
| 20 | 20 | } |
| 21 | 21 | include(ROOT.'/util.php'); |
| 22 | - | |
| 23 | 22 | |
| 24 | 23 | $routes = [ |
| 25 | 24 | Route::add('GET', 'blog/{page}', Home::class.'@article'), |
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | namespace Site\Controller; |
| 3 | 3 | |
| 4 | 4 | use Site\Controller\BlogPost; |
| 5 | -use Site\Lib\Markdown; | |
| 5 | +use Std\Lib\Markdown; | |
| 6 | 6 | |
| 7 | 7 | class Atom extends BlogPost { |
| 8 | 8 | private string $domain = 'technicalsuwako.moe'; |
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | |
| 4 | 4 | use Site\Controller\BlogPost; |
| 5 | 5 | use Site\Controller\Mods; |
| 6 | -use Site\Lib\Activitypub; | |
| 6 | +use Std\Lib\Activitypub; | |
| 7 | 7 | |
| 8 | 8 | class Fediverse extends BlogPost { |
| 9 | 9 | use Mods; |
| @@ -3,10 +3,10 @@ | ||
| 3 | 3 | |
| 4 | 4 | use Site\Controller\BlogPost; |
| 5 | 5 | use Site\Controller\Mods; |
| 6 | -use Site\Lib\Activitypub; | |
| 7 | -use Site\Lib\Auth; | |
| 8 | -use Site\Lib\Markdown; | |
| 9 | -use Site\Lib\Template; | |
| 6 | +use Std\Lib\Activitypub; | |
| 7 | +use Std\Lib\Auth; | |
| 8 | +use Std\Lib\Markdown; | |
| 9 | +use Std\Lib\Template; | |
| 10 | 10 | |
| 11 | 11 | class Home extends BlogPost { |
| 12 | 12 | use Mods; |
| @@ -2,8 +2,8 @@ | ||
| 2 | 2 | namespace Site\Controller; |
| 3 | 3 | |
| 4 | 4 | use Site\Controller\Mods; |
| 5 | -use Site\Lib\Auth; | |
| 6 | -use Site\Lib\Template; | |
| 5 | +use Std\Lib\Auth; | |
| 6 | +use Std\Lib\Template; | |
| 7 | 7 | |
| 8 | 8 | class Notfound { |
| 9 | 9 | use Mods; |
| @@ -2,10 +2,10 @@ | ||
| 2 | 2 | namespace Site\Controller; |
| 3 | 3 | |
| 4 | 4 | use Site\Controller\Mods; |
| 5 | -use Site\Lib\Auth; | |
| 6 | -use Site\Lib\Template; | |
| 5 | +use Std\Lib\Auth; | |
| 6 | +use Std\Lib\Template; | |
| 7 | 7 | |
| 8 | -use Site\Lib\Syntax; // TEST | |
| 8 | +use Std\Lib\Syntax; // TEST | |
| 9 | 9 | |
| 10 | 10 | class Page { |
| 11 | 11 | use Mods; |
| @@ -2,8 +2,8 @@ | ||
| 2 | 2 | namespace Site\Controller; |
| 3 | 3 | |
| 4 | 4 | use Site\Controller\Mods; |
| 5 | -use Site\Lib\Auth; | |
| 6 | -use Site\Lib\Template; | |
| 5 | +use Std\Lib\Auth; | |
| 6 | +use Std\Lib\Template; | |
| 7 | 7 | |
| 8 | 8 | class User { |
| 9 | 9 | use Mods; |
削除
src/Site/Lib/Activitypub.php
| @@ -1,557 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -use Site\Lib\Curl; | |
| 5 | - | |
| 6 | -/** | |
| 7 | - * ActivityPubプロトコルの実装クラス | |
| 8 | - * | |
| 9 | - * このクラスはActivityPubプロトコルを利用して分散型 | |
| 10 | - * ソーシャルネットワーキングを実装します。 | |
| 11 | - */ | |
| 12 | -class ActivityPub { | |
| 13 | - private string $domain; | |
| 14 | - private string $actor; | |
| 15 | - private string $actorNick; | |
| 16 | - private string $desc; | |
| 17 | - private string $icon; | |
| 18 | - private array $posts = []; | |
| 19 | - private string $https; | |
| 20 | - | |
| 21 | - /** | |
| 22 | - * コンストラクタ | |
| 23 | - * | |
| 24 | - * @param array $posts 投稿データの配列 | |
| 25 | - */ | |
| 26 | - public function __construct(array $posts = []) { | |
| 27 | - if (!ACTIVITYPUB_ENABLED) return; | |
| 28 | - $this->https = 'http'.((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 's' : '').'://'; | |
| 29 | - $this->domain = $this->https.($_SERVER['SERVER_NAME'] ?? '127.0.0.1:8000'); | |
| 30 | - $this->actor = FEDIINFO['actor']; | |
| 31 | - $this->actorNick = FEDIINFO['actorNick']; | |
| 32 | - $this->desc = FEDIINFO['desc']; | |
| 33 | - $this->icon = "{$this->domain}".FEDIINFO['icon']; | |
| 34 | - $this->posts = $posts; | |
| 35 | - } | |
| 36 | - | |
| 37 | - /** | |
| 38 | - * ActivityPubアクタープロフィールを受け取る | |
| 39 | - * | |
| 40 | - * @return string アクターオブジェクト | |
| 41 | - * @throws \Exception 公開鍵の読み込みに失敗した場合 | |
| 42 | - */ | |
| 43 | - public function getActor(): string { | |
| 44 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 45 | - $pubkey = file_get_contents(FEDIINFO['pubkey']); | |
| 46 | - if ($pubkey === false) { | |
| 47 | - throw new \Exception('公開鍵の受取に失敗。パス:'.FEDIINFO['pubkey']); | |
| 48 | - } | |
| 49 | - | |
| 50 | - $actor = [ | |
| 51 | - '@context' => [ | |
| 52 | - 'https://www.w3.org/ns/activitystreams', | |
| 53 | - 'https://w3id.org/security/v1', | |
| 54 | - ], | |
| 55 | - 'id' => "{$this->domain}/ap/actor", | |
| 56 | - 'name' => $this->actorNick, | |
| 57 | - 'summary' => $this->desc, | |
| 58 | - 'manuallyApprovesFollowers' => false, | |
| 59 | - 'icon' => [ | |
| 60 | - 'type' => 'Image', | |
| 61 | - 'mediaType' => 'image/png', | |
| 62 | - 'url' => $this->icon, | |
| 63 | - ], | |
| 64 | - 'image' => [ | |
| 65 | - 'type' => 'Image', | |
| 66 | - 'url' => "{$this->domain}/static/article/o_53803618dc1691.28179609.jpg", | |
| 67 | - 'mediaType' => 'image/jpeg', | |
| 68 | - ], | |
| 69 | - 'type' => 'Person', | |
| 70 | - 'url' => "{$this->domain}", | |
| 71 | - 'preferredUsername' => $this->actor, | |
| 72 | - 'inbox' => "{$this->domain}/ap/inbox", | |
| 73 | - 'outbox' => "{$this->domain}/ap/outbox", | |
| 74 | - 'followers' => "{$this->domain}/ap/followers", | |
| 75 | - 'following' => "{$this->domain}/ap/following", | |
| 76 | - 'published' => '2025-03-28T18:00:00Z', | |
| 77 | - 'updated' => gmdate('c'), | |
| 78 | - 'publicKey' => [ | |
| 79 | - 'id' => "{$this->domain}/ap/actor#main-key", | |
| 80 | - 'owner' => "{$this->domain}/ap/actor", | |
| 81 | - 'publicKeyPem' => $pubkey, | |
| 82 | - ], | |
| 83 | - ]; | |
| 84 | - | |
| 85 | - return json_encode($actor, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 86 | - } | |
| 87 | - | |
| 88 | - /** | |
| 89 | - * 特定のUUIDに対応するActivityを取得する | |
| 90 | - * | |
| 91 | - * @param string $uuid 取得するアクティビティのUUID | |
| 92 | - * @return string JSONエンコードされたアクティビティデータ | |
| 93 | - */ | |
| 94 | - public function getActivity(string $uuid): string { | |
| 95 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 96 | - $items = []; | |
| 97 | - | |
| 98 | - foreach ($this->posts as $post) { | |
| 99 | - if ($post['uuid'] != $uuid) continue; | |
| 100 | - | |
| 101 | - $items = [ | |
| 102 | - '@context' => [ | |
| 103 | - 'https://www.w3.org/ns/activitystreams', | |
| 104 | - 'https://w3id.org/security/v1', | |
| 105 | - [ | |
| 106 | - 'Emoji' => 'toot:Emoji', | |
| 107 | - 'EmojiReact' => 'litepub:EmojiReact', | |
| 108 | - 'Hashtag' => 'as:Hashtag', | |
| 109 | - 'litepub' => 'http://litepub.social/ns#', | |
| 110 | - 'sensitive' => 'as:sensitive', | |
| 111 | - 'toot' => 'http://joinmastodon.org/ns#', | |
| 112 | - ], | |
| 113 | - ], | |
| 114 | - 'id' => "{$this->domain}/ap/activities/create/{$post['uuid']}", | |
| 115 | - 'type' => 'Create', | |
| 116 | - 'actor' => "{$this->domain}/ap/actor", | |
| 117 | - 'cc' => [ | |
| 118 | - "{$this->domain}/ap/followers", | |
| 119 | - ], | |
| 120 | - 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 121 | - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 122 | - 'object' => [ | |
| 123 | - 'id' => "{$this->domain}/ap/objects/{$post['uuid']}", | |
| 124 | - 'type' => 'Note', | |
| 125 | - 'name' => $post['title'], | |
| 126 | - 'attributedTo' => "{$this->domain}/ap/actor", | |
| 127 | - 'cc' => [ | |
| 128 | - "{$this->domain}/ap/followers", | |
| 129 | - ], | |
| 130 | - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 131 | - 'content' => $post['preview']."<br /><br /><a href=\"{$this->domain}/blog/{$post['slug']}\">読み続き</a>", | |
| 132 | - 'url' => "{$this->domain}/blog/{$post['slug']}", | |
| 133 | - 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 134 | - 'replies' => "{$this->domain}/ap/objects/{$uuid}/replies", | |
| 135 | - 'sensitive' => false, | |
| 136 | - ], | |
| 137 | - ]; | |
| 138 | - | |
| 139 | - if (isset($post['category']) && !empty($post['category'])) { | |
| 140 | - $item['tag'] = []; | |
| 141 | - foreach ($post['category'] as $cat) { | |
| 142 | - $items['tag'][] = $cat; | |
| 143 | - } | |
| 144 | - } | |
| 145 | - | |
| 146 | - if (isset($post['thumbnail']) && $post['thumbnail'] != '') { | |
| 147 | - $imgurl = "{$this->domain}/static/article/{$post['thumbnail']}"; | |
| 148 | - $imgpath = ROOT."/public/static/article/{$post['thumbnail']}"; | |
| 149 | - $imgraw = file_get_contents($imgpath); | |
| 150 | - | |
| 151 | - $items['attachment'] = [ | |
| 152 | - [ | |
| 153 | - 'digestMultibase' => 'z'.base58btc_encode(hash('sha256', $imgraw, true)), | |
| 154 | - 'mediaType' => mime_content_type($imgpath), | |
| 155 | - 'type' => "Image", | |
| 156 | - 'url' => $imgurl, | |
| 157 | - ], | |
| 158 | - ]; | |
| 159 | - } | |
| 160 | - | |
| 161 | - break; | |
| 162 | - } | |
| 163 | - | |
| 164 | - return json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 165 | - } | |
| 166 | - | |
| 167 | - /** | |
| 168 | - * アウトボックスデータを取得する | |
| 169 | - * | |
| 170 | - * @return string JSONエンコードされたアウトボックスデータ | |
| 171 | - */ | |
| 172 | - public function getOutbox(): string { | |
| 173 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 174 | - $items = []; | |
| 175 | - $counter = 0; | |
| 176 | - | |
| 177 | - foreach ($this->posts as $post) { | |
| 178 | - $uid = $post['uuid']; | |
| 179 | - | |
| 180 | - $items[] = [ | |
| 181 | - '@context' => [ | |
| 182 | - 'https://www.w3.org/ns/activitystreams', | |
| 183 | - 'https://w3id.org/security/v1', | |
| 184 | - [ | |
| 185 | - 'Emoji' => 'toot:Emoji', | |
| 186 | - 'EmojiReact' => 'litepub:EmojiReact', | |
| 187 | - 'Hashtag' => 'as:Hashtag', | |
| 188 | - 'litepub' => 'http://litepub.social/ns#', | |
| 189 | - 'sensitive' => 'as:sensitive', | |
| 190 | - 'toot' => 'http://joinmastodon.org/ns#', | |
| 191 | - ], | |
| 192 | - ], | |
| 193 | - 'id' => "{$this->domain}/ap/activities/create/{$uid}", | |
| 194 | - 'type' => 'Create', | |
| 195 | - 'actor' => "{$this->domain}/ap/actor", | |
| 196 | - 'cc' => [ | |
| 197 | - "{$this->domain}/ap/followers", | |
| 198 | - ], | |
| 199 | - 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 200 | - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 201 | - 'object' => [ | |
| 202 | - 'id' => "{$this->domain}/ap/objects/{$uid}", | |
| 203 | - 'type' => 'Note', | |
| 204 | - 'name' => $post['title'], | |
| 205 | - 'attributedTo' => "{$this->domain}/ap/actor", | |
| 206 | - 'cc' => [ | |
| 207 | - "{$this->domain}/ap/followers", | |
| 208 | - ], | |
| 209 | - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 210 | - 'content' => $post['preview']."<br /><br /><a href=\"{$this->domain}/blog/{$post['slug']}\">読み続き</a>", | |
| 211 | - 'url' => "{$this->domain}/blog/{$post['slug']}", | |
| 212 | - 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 213 | - 'replies' => "{$this->domain}/ap/objects/{$uid}/replies", | |
| 214 | - 'sensitive' => false, | |
| 215 | - ], | |
| 216 | - ]; | |
| 217 | - | |
| 218 | - if (isset($post['category']) && !empty($post['category'])) { | |
| 219 | - $items[$counter]['tag'] = []; | |
| 220 | - foreach ($post['category'] as $cat) { | |
| 221 | - $items[$counter]['tag'][] = $cat; | |
| 222 | - } | |
| 223 | - } | |
| 224 | - | |
| 225 | - if (isset($post['thumbnail']) && $post['thumbnail'] != '') { | |
| 226 | - $imgurl = "{$this->domain}/static/article/{$post['thumbnail']}"; | |
| 227 | - $imgpath = ROOT."/public/static/article/{$post['thumbnail']}"; | |
| 228 | - $imgraw = file_get_contents($imgpath); | |
| 229 | - | |
| 230 | - $items[$counter]['attachment'] = [ | |
| 231 | - [ | |
| 232 | - 'digestMultibase' => 'z'.base58btc_encode(hash('sha256', $imgraw, true)), | |
| 233 | - 'mediaType' => mime_content_type($imgpath), | |
| 234 | - 'type' => "Image", | |
| 235 | - 'url' => $imgurl, | |
| 236 | - ], | |
| 237 | - ]; | |
| 238 | - } | |
| 239 | - | |
| 240 | - $counter++; | |
| 241 | - } | |
| 242 | - | |
| 243 | - $outbox = [ | |
| 244 | - '@context' => [ | |
| 245 | - 'https://www.w3.org/ns/activitystreams', | |
| 246 | - 'https://w3id.org/security/v1', | |
| 247 | - ], | |
| 248 | - 'id' => "{$this->domain}/ap/outbox", | |
| 249 | - 'type' => 'OrderedCollection', | |
| 250 | - 'totalItems' => count($items), | |
| 251 | - 'orderedItems' => $items, | |
| 252 | - ]; | |
| 253 | - | |
| 254 | - return json_encode($outbox, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 255 | - } | |
| 256 | - | |
| 257 | - /** | |
| 258 | - * WebFingerデータを取得する | |
| 259 | - * | |
| 260 | - * @return string JSONエンコードされたWebFingerデータ | |
| 261 | - */ | |
| 262 | - public function getWebfinger(): string { | |
| 263 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 264 | - $domain = str_replace(['http://', 'https://'], '', $this->domain); | |
| 265 | - $webfinger = [ | |
| 266 | - 'subject' => "acct:{$this->actor}@{$domain}", | |
| 267 | - 'links' => [ | |
| 268 | - [ | |
| 269 | - 'rel' => 'self', | |
| 270 | - 'type' => 'application/activity+json', | |
| 271 | - 'href' => "{$domain}/ap/actor", | |
| 272 | - ], | |
| 273 | - ], | |
| 274 | - ]; | |
| 275 | - | |
| 276 | - return json_encode($webfinger, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 277 | - } | |
| 278 | - | |
| 279 | - /** | |
| 280 | - * フォロワーのリストを取得する | |
| 281 | - * | |
| 282 | - * @return string JSONエンコードされたフォロワーのリスト | |
| 283 | - */ | |
| 284 | - public function getFollowers(): string { | |
| 285 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 286 | - $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt'))); | |
| 287 | - | |
| 288 | - $followers = [ | |
| 289 | - '@context' => [ | |
| 290 | - 'https://www.w3.org/ns/activitystreams', | |
| 291 | - 'https://w3id.org/security/v1', | |
| 292 | - ], | |
| 293 | - 'id' => "{$this->domain}/ap/followers", | |
| 294 | - 'type' => 'OrderredCollection', | |
| 295 | - 'totalItems' => count($f), | |
| 296 | - 'orderedItems' => $f, | |
| 297 | - ]; | |
| 298 | - | |
| 299 | - return json_encode($followers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 300 | - } | |
| 301 | - | |
| 302 | - /** | |
| 303 | - * フォローしているアカウントのリストを取得する | |
| 304 | - * | |
| 305 | - * @return string JSONエンコードされたフォローリスト | |
| 306 | - */ | |
| 307 | - public function getFollowing(): string { | |
| 308 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 309 | - $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/following.txt'))); | |
| 310 | - | |
| 311 | - $following = [ | |
| 312 | - '@context' => [ | |
| 313 | - 'https://www.w3.org/ns/activitystreams', | |
| 314 | - 'https://w3id.org/security/v1', | |
| 315 | - ], | |
| 316 | - 'id' => "{$this->domain}/ap/following", | |
| 317 | - 'type' => 'OrderredCollection', | |
| 318 | - 'totalItems' => count($f), | |
| 319 | - 'orderedItems' => $f, | |
| 320 | - ]; | |
| 321 | - | |
| 322 | - return json_encode($following, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 323 | - } | |
| 324 | - | |
| 325 | - /** | |
| 326 | - * インボックスにアクティビティを投稿する | |
| 327 | - * | |
| 328 | - * @param array $activity 処理するアクティビティデータ | |
| 329 | - * @return void | |
| 330 | - */ | |
| 331 | - public function postInbox(array $activity): void { | |
| 332 | - if (!ACTIVITYPUB_ENABLED) return; | |
| 333 | - switch ($activity['type']) { | |
| 334 | - case 'Follow': | |
| 335 | - $this->acceptFollower($activity); | |
| 336 | - break; | |
| 337 | - default: | |
| 338 | - header('HTTP/1.1 501 Not Implemented'); | |
| 339 | - header('Content-Type: application/activity+json'); | |
| 340 | - echo json_encode(['error' => '未対応なアクティビティタイプ: '.$activity['type']]); | |
| 341 | - exit; | |
| 342 | - } | |
| 343 | - | |
| 344 | - header('HTTP/1.1 200 OK'); | |
| 345 | - header('Content-Type: application/activity+json'); | |
| 346 | - echo json_encode(['status' => 'OK'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 347 | - exit; | |
| 348 | - } | |
| 349 | - | |
| 350 | - /** | |
| 351 | - * アクタープロフィールの更新アクティビティを作成する | |
| 352 | - * | |
| 353 | - * @return string JSONエンコードされた更新アクティビティ | |
| 354 | - */ | |
| 355 | - public function update(): string { | |
| 356 | - if (!ACTIVITYPUB_ENABLED) return ''; | |
| 357 | - $update = [ | |
| 358 | - '@context' => [ | |
| 359 | - 'https://www.w3.org/ns/activitystreams', | |
| 360 | - 'https://w3id.org/security/v1', | |
| 361 | - ], | |
| 362 | - 'id' => "{$this->domain}/ap/activities/update/".uuid(), | |
| 363 | - 'type' => 'Update', | |
| 364 | - 'actor' => "{$this->domain}/ap/actor", | |
| 365 | - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 366 | - 'object' => json_decode($this->getActor(), true), | |
| 367 | - ]; | |
| 368 | - | |
| 369 | - return json_encode($update, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 370 | - } | |
| 371 | - | |
| 372 | - /** | |
| 373 | - * アクター更新をフォロワーに送信する | |
| 374 | - * | |
| 375 | - * @param array $params パラメータ配列 | |
| 376 | - * @return void | |
| 377 | - */ | |
| 378 | - public function sendActorUpdate(array $params): void { | |
| 379 | - if (!ACTIVITYPUB_ENABLED) return; | |
| 380 | - $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt'))); | |
| 381 | - $ap = new Activitypub(); | |
| 382 | - $inboxes = implode("\n", $f); | |
| 383 | - $update = json_decode($ap->update(), true); | |
| 384 | - | |
| 385 | - foreach ($f as $inbox) $this->sendActivity($inbox, $update); | |
| 386 | - } | |
| 387 | - | |
| 388 | - // 機能性メソッド | |
| 389 | - | |
| 390 | - /** | |
| 391 | - * 指定されたインボックスURLにアクティビティを送信する | |
| 392 | - * | |
| 393 | - * @param string $inboxUrl 送信先のインボックスURL | |
| 394 | - * @param array $activity 送信するアクティビティデータ | |
| 395 | - * @return void | |
| 396 | - */ | |
| 397 | - private function sendActivity(string $inboxUrl, array $activity): void { | |
| 398 | - $privFile = FEDIINFO['privkey']; | |
| 399 | - $priv = file_get_contents($privFile); | |
| 400 | - if ($priv === false) { | |
| 401 | - logger(\LogType::ActivityPub, "エラー:秘密鍵「{$privFile}」の読込に失敗"); | |
| 402 | - header('HTTP/1.1 500 Internal Server Error'); | |
| 403 | - header('Content-Type: application/activity+json'); | |
| 404 | - echo json_encode(['error' => '秘密鍵の読込に失敗']); | |
| 405 | - exit; | |
| 406 | - } | |
| 407 | - | |
| 408 | - $body = json_encode($activity, JSON_UNESCAPED_SLASHES); | |
| 409 | - $digest = base64_encode(hash('sha256', $body, true)); | |
| 410 | - $date = gmdate('D, d M Y H:i:s \G\M\T'); | |
| 411 | - $host = parse_url($inboxUrl, PHP_URL_HOST); | |
| 412 | - | |
| 413 | - $headers = [ | |
| 414 | - 'Host' => $host, | |
| 415 | - 'Date' => $date, | |
| 416 | - 'Content-Type' => 'application/activity+json', | |
| 417 | - 'Digest' => "SHA-256=$digest", | |
| 418 | - ]; | |
| 419 | - | |
| 420 | - $stringToSign = "host: {$headers['Host']}\n"."date: {$headers['Date']}\n"."digest: {$headers['Digest']}"; | |
| 421 | - logger(\LogType::ActivityPub, "署名対象: {$stringToSign}"); | |
| 422 | - | |
| 423 | - if (!openssl_sign($stringToSign, $signature, $priv, OPENSSL_ALGO_SHA256)) { | |
| 424 | - $error = openssl_error_string(); | |
| 425 | - logger(\LogType::ActivityPub, "エラー:署名に失敗: {$error}"); | |
| 426 | - header('HTTP/1.1 500 Internal Server Error'); | |
| 427 | - header('Content-Type: application/activity+json'); | |
| 428 | - echo json_encode(['error' => '署名に失敗']); | |
| 429 | - exit; | |
| 430 | - } | |
| 431 | - | |
| 432 | - $sigValue = base64_encode($signature); | |
| 433 | - $headers['Signature'] = "keyId=\"{$this->domain}/ap/actor#main-key\","; | |
| 434 | - $headers['Signature'] .= 'algorithm="rsa-sha256",'; | |
| 435 | - $headers['Signature'] .= 'headers="host date digest",'; | |
| 436 | - $headers['Signature'] .= 'signature="'.$sigValue.'"'; | |
| 437 | - logger(\LogType::ActivityPub, "署名: {$headers['Signature']}\n送信データ: {$body}"); | |
| 438 | - | |
| 439 | - $curl = new Curl($inboxUrl); | |
| 440 | - $curl->setMethod('POST') | |
| 441 | - ->setPostRaw($body) | |
| 442 | - ->setHeaders(array_map(fn($k, $v) => "$k: $v", array_keys($headers), $headers)) | |
| 443 | - ->setCaInfo('/etc/ssl/cert.pem') | |
| 444 | - ->setVerbose(true) | |
| 445 | - ->setStderr(fopen(ROOT.'/log/ap_log.txt', 'a')); | |
| 446 | - | |
| 447 | - $success = $curl->execute(); | |
| 448 | - $res = $curl->getResponseBody(); | |
| 449 | - $code = $curl->getResponseCode(); | |
| 450 | - $err = $curl->getError(); | |
| 451 | - | |
| 452 | - var_dump(print_r($res)); | |
| 453 | - logger(\LogType::ActivityPub, "アクティビティは「{$inboxUrl}」に送信しました: HTTP {$code}"); | |
| 454 | - logger(\LogType::ActivityPub, "エラー: {$err}"); | |
| 455 | - logger(\LogType::ActivityPub, "レスポンス: {$res}"); | |
| 456 | - } | |
| 457 | - | |
| 458 | - /** | |
| 459 | - * フォロワーを受け入れる | |
| 460 | - * | |
| 461 | - * @param array $activity フォローアクティビティデータ | |
| 462 | - * @return void | |
| 463 | - */ | |
| 464 | - private function acceptFollower(array $activity): void { | |
| 465 | - $followerActor = $activity['actor'] ?? null; | |
| 466 | - if (!$followerActor) { | |
| 467 | - header('HTTP/1.1 400 Bad Request'); | |
| 468 | - header('Content-Type: application/activity+json'); | |
| 469 | - echo json_encode(['error' => 'アクターがない']); | |
| 470 | - exit; | |
| 471 | - } | |
| 472 | - | |
| 473 | - $this->storeFollower($followerActor); | |
| 474 | - | |
| 475 | - $inbox = $this->getInboxFromActor($followerActor); | |
| 476 | - if (!$inbox) { | |
| 477 | - header('HTTP/1.1 500 Internal Server Error'); | |
| 478 | - header('Content-Type: application/activity+json'); | |
| 479 | - echo json_encode(['error' => 'フォロワーの受付ボックスの受取に失敗']); | |
| 480 | - exit; | |
| 481 | - } | |
| 482 | - | |
| 483 | - $accept = [ | |
| 484 | - '@context' => [ | |
| 485 | - 'https://www.w3.org/ns/activitystreams', | |
| 486 | - 'https://w3id.org/security/v1', | |
| 487 | - ], | |
| 488 | - 'id' => "{$this->domain}/ap/activities/".uniqid(), | |
| 489 | - 'type' => 'Accept', | |
| 490 | - 'actor' => "{$this->domain}/ap/actor", | |
| 491 | - 'object' => $activity, | |
| 492 | - ]; | |
| 493 | - | |
| 494 | - $this->sendActivity($inbox, $accept); | |
| 495 | - } | |
| 496 | - | |
| 497 | - /** | |
| 498 | - * フォロワーをストレージに保存する | |
| 499 | - * | |
| 500 | - * @param string $followerActor フォロワーのアクターURL | |
| 501 | - * @return void | |
| 502 | - */ | |
| 503 | - private function storeFollower(string $followerActor): void { | |
| 504 | - $file = ROOT.'/data/followers.txt'; | |
| 505 | - if (!file_exists($file)) { | |
| 506 | - touch($file); | |
| 507 | - chmod($file, 0644); | |
| 508 | - } | |
| 509 | - | |
| 510 | - $followers = $this->getFollowersList(); | |
| 511 | - if (!in_array($followerActor, $followers)) file_put_contents($file, "$followerActor\n", FILE_APPEND); | |
| 512 | - } | |
| 513 | - | |
| 514 | - /** | |
| 515 | - * フォロワーのリストを配列として取得する | |
| 516 | - * | |
| 517 | - * @return array フォロワーのURLの配列 | |
| 518 | - */ | |
| 519 | - private function getFollowersList(): array { | |
| 520 | - $file = ROOT.'/data/followers.txt'; | |
| 521 | - $f = array_filter(explode("\n", file_get_contents($file))); | |
| 522 | - return file_exists($file) ? array_filter(explode("\n", file_get_contents($file))) : []; | |
| 523 | - } | |
| 524 | - | |
| 525 | - /** | |
| 526 | - * アクターのインボックスURLを取得する | |
| 527 | - * | |
| 528 | - * @param string $actor アクターのURL | |
| 529 | - * @return string|null インボックスURL、取得に失敗した場合はnull | |
| 530 | - */ | |
| 531 | - private function getInboxFromActor(string $actor): ?string { | |
| 532 | - $curl = new Curl($actor); | |
| 533 | - $curl->setHeaders(['Accept: application/activity+json']) | |
| 534 | - ->setFollowRedirects(true) | |
| 535 | - ->setMaxRedirects(5) | |
| 536 | - ->setCaInfo('/etc/ssl/cert.pem'); | |
| 537 | - | |
| 538 | - logger(\LogType::ActivityPub, "アクターURLにリクエスト: {$actor}"); | |
| 539 | - $success = $curl->execute(); | |
| 540 | - if (!$success) { | |
| 541 | - logger(\LogType::ActivityPub, "アクターリクエストに失敗: ".$curl->getError()); | |
| 542 | - return null; | |
| 543 | - } | |
| 544 | - | |
| 545 | - $res = $curl->getResponseBody(); | |
| 546 | - $code = $curl->getResponseCode(); | |
| 547 | - $err = $curl->getError(); | |
| 548 | - | |
| 549 | - if ($code !== 200) { | |
| 550 | - logger(\LogType::ActivityPub, "アクター取得に失敗: HTTP {$code}, エラー: {$err}"); | |
| 551 | - return null; | |
| 552 | - } | |
| 553 | - | |
| 554 | - $data = json_decode($res, true); | |
| 555 | - return $data['inbox'] ?? null; | |
| 556 | - } | |
| 557 | -} |
削除
src/Site/Lib/Auth.php
| @@ -1,451 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -class Auth { | |
| 5 | - private int $id; | |
| 6 | - | |
| 7 | - private \stdClass $user; | |
| 8 | - private \stdClass $pubUser; | |
| 9 | - private ?string $token; | |
| 10 | - | |
| 11 | - protected string $dataDir = ROOT."/data/user/"; | |
| 12 | - protected string $assDir = ROOT."/public/static/user/"; | |
| 13 | - protected int $minPassLen = 20; | |
| 14 | - protected int $tokenDuration = 31536000; // 1年間 | |
| 15 | - protected string|int|null $algo = PASSWORD_ARGON2ID; | |
| 16 | - | |
| 17 | - public function __construct(?string $username = null) { | |
| 18 | - if (!AUTH_ENABLED) return; | |
| 19 | - $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); | |
| 20 | - $this->token = \getcookie('kerozen'); | |
| 21 | - $this->id = $this->getUserId($username, $this->token); | |
| 22 | - if ($this->id > 0) $this->user = $this->getUserData(); | |
| 23 | - } | |
| 24 | - | |
| 25 | - public function getLoggedInUser(): \stdClass|null { | |
| 26 | - if ($this->id === 0) return null; | |
| 27 | - $this->pubUser = $this->user; | |
| 28 | - unset($this->pubUser->password); | |
| 29 | - unset($this->pubUser->tokens); | |
| 30 | - return $this->pubUser; | |
| 31 | - } | |
| 32 | - | |
| 33 | - public function getUser(string $name): \stdClass|null { | |
| 34 | - $user = $this->getUserDataFromName($name); | |
| 35 | - if (!$user) return null; | |
| 36 | - unset($user->password); | |
| 37 | - unset($user->tokens); | |
| 38 | - $myself = $this->getUserData(); | |
| 39 | - if (!$myself || ($myself->id != $user->id && $myself->role < \Roles::STAFF)) $user->email = '(秘密)'; | |
| 40 | - $user->name = namecolor($user); | |
| 41 | - $user->regDate = date('Y年m月d日', $user->regDate); | |
| 42 | - $user->gender = $user->gender === \Gender::MALE ? '男' : ($user->gender === \Gender::FEMALE ? '女' : '不明'); | |
| 43 | - | |
| 44 | - switch (true) { | |
| 45 | - case ($user->role & \Roles::ADMIN): | |
| 46 | - $user->role = '管理者'; | |
| 47 | - break; | |
| 48 | - case ($user->role & \Roles::STAFF): | |
| 49 | - $user->role = '076スタジオの会社員'; | |
| 50 | - break; | |
| 51 | - case ($user->role & (\Roles::FSUBSCRIBER | \Roles::PSUBSCRIBER | \Roles::NSUBSCRIBER | \Roles::SUBSCRIBER)): | |
| 52 | - $user->role = '支援者♡'; | |
| 53 | - break; | |
| 54 | - case ($user->role & \Roles::BANNED): | |
| 55 | - $user->role = 'BANされた'; | |
| 56 | - break; | |
| 57 | - default: | |
| 58 | - $user->role = 'ユーザー'; | |
| 59 | - break; | |
| 60 | - } | |
| 61 | - | |
| 62 | - $user->avatar = '/static/user/'.($user->avatar === '' ? 'noicon.png' : $user->username.'/'.$user->avatar); | |
| 63 | - $user->altName = $user->displayname ?: $user->username; | |
| 64 | - unset($user->username); | |
| 65 | - unset($user->displayname); | |
| 66 | - unset($user->namecolor); | |
| 67 | - return $user; | |
| 68 | - } | |
| 69 | - | |
| 70 | - public function setToken(string $username, #[SensitiveParameter] string $password): \Result { | |
| 71 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 72 | - $userData = $this->getUserData(); | |
| 73 | - if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = []; | |
| 74 | - | |
| 75 | - $verify = $this->verifyLogin($username, $password); | |
| 76 | - if (!$verify->isSuccess) { | |
| 77 | - return \Result::error($verify->message); | |
| 78 | - } | |
| 79 | - | |
| 80 | - $ip = $_SERVER['REMOTE_ADDR']; | |
| 81 | - if (!empty($_SERVER['HTTP_X_FORMARDED_FOR'])) { | |
| 82 | - $ipList = explode(',', $_SERVER['HTTP_X_FORMARDED_FOR']); | |
| 83 | - $ip = trim($ipList[0]); | |
| 84 | - } | |
| 85 | - | |
| 86 | - $userData = $this->purgeOldTokens($userData); | |
| 87 | - $token = bin2hex(random_bytes(64)); | |
| 88 | - $expire = time() + $this->tokenDuration; | |
| 89 | - $newToken = [ | |
| 90 | - 'token' => $token, | |
| 91 | - 'ip' => $ip, | |
| 92 | - 'createDate' => time(), | |
| 93 | - 'expDate' => $expire, | |
| 94 | - 'lastDate' => time(), | |
| 95 | - 'userAgent' => $_SERVER['HTTP_USER_AGENT'] ?? '不明' | |
| 96 | - ]; | |
| 97 | - | |
| 98 | - $userData->tokens[] = $newToken; | |
| 99 | - | |
| 100 | - $path = $this->dataDir.$this->id.'.'.$userData->username.'.json'; | |
| 101 | - $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 102 | - | |
| 103 | - if (file_put_contents($path, $json) === false) { | |
| 104 | - logger(\LogType::Auth, '【setToken】JSONファイルにユーザーデータを保存出来なかった。パス:'.$path); | |
| 105 | - return \Result::error('エラー:ユーザーデータの保存に失敗。'); | |
| 106 | - } | |
| 107 | - | |
| 108 | - if (!setcookie('kerozen', $token, [ | |
| 109 | - 'expires' => $expire, | |
| 110 | - 'path' => '/', | |
| 111 | - 'domain' => $_SERVER['SERVER_NAME'], | |
| 112 | - 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), | |
| 113 | - 'httponly' => true, | |
| 114 | - 'samesite' => 'Strict' | |
| 115 | - ])) { | |
| 116 | - logger(\LogType::Auth, '【setToken】クッキーを設定出来なかった。トークン:'.$token); | |
| 117 | - return \Result::error('エラー:クッキーを設定に失敗。'); | |
| 118 | - } | |
| 119 | - | |
| 120 | - return \Result::success('ログイン成功'); | |
| 121 | - } | |
| 122 | - | |
| 123 | - public function getToken(): string { | |
| 124 | - if (!AUTH_ENABLED) return ''; | |
| 125 | - $userData = $this->getUserData(); | |
| 126 | - if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = []; | |
| 127 | - $userData = $this->purgeOldTokens($userData); | |
| 128 | - } | |
| 129 | - | |
| 130 | - public function logout(?string $token = null): \Result { | |
| 131 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 132 | - $userData = $this->getUserData(); | |
| 133 | - | |
| 134 | - if (!$token) { | |
| 135 | - $userData->tokens = array_filter($userData->tokens, function($t): bool { | |
| 136 | - return ($t->expires ?? 0) > time(); | |
| 137 | - }); | |
| 138 | - } | |
| 139 | - | |
| 140 | - setcookie('kerozen', null, [ | |
| 141 | - 'expires' => 0, | |
| 142 | - 'path' => '/', | |
| 143 | - 'domain' => $_SERVER['SERVER_NAME'], | |
| 144 | - 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), | |
| 145 | - 'httponly' => true, | |
| 146 | - 'samesite' => 'Strict' | |
| 147 | - ]); | |
| 148 | - | |
| 149 | - return \Result::success(); | |
| 150 | - } | |
| 151 | - | |
| 152 | - public function isUserExist(?string $username = null): \Result { | |
| 153 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 154 | - if (null === $username) return \Result::Error('エラー:ユーザー名をご入力下さい。'); | |
| 155 | - $userList = scandir($this->dataDir); | |
| 156 | - | |
| 157 | - foreach ($userList as $list) { | |
| 158 | - if ($list === '.kara' || $list === '.' || $list === '..') continue; | |
| 159 | - $file = str_replace('.json', '', $list); | |
| 160 | - $user = explode('.', $file)[1]; | |
| 161 | - if ($username === $user) return \Result::Success("ユーザー「{$username}」は既に存在します。"); | |
| 162 | - } | |
| 163 | - | |
| 164 | - return \Result::Error(''); | |
| 165 | - } | |
| 166 | - | |
| 167 | - public function mkUser(?string $username = null, #[SensitiveParameter] ?string $password = null, ?string $passwordVerify = null, ?string $email = null): \Result { | |
| 168 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 169 | - if (!AUTH_REGISTER_ENABLED) return \Result::Error('ユーザー登録は無効です。'); | |
| 170 | - $resUsr = $this->verifyUsername($username); | |
| 171 | - $resPwd = $this->verifyPassword($password, $passwordVerify); | |
| 172 | - $resEml = $this->verifyEmail($email); | |
| 173 | - | |
| 174 | - $err = ''; | |
| 175 | - | |
| 176 | - if (!$resUsr->isSuccess) $err .= $resUsr->message."<br />"; | |
| 177 | - if (!$resPwd->isSuccess) $err .= $resPwd->message."<br />"; | |
| 178 | - if (!$resEml->isSuccess) $err .= $resEml->message."<br />"; | |
| 179 | - | |
| 180 | - if ($err != '') return \Result::Error($err); | |
| 181 | - $err = ''; | |
| 182 | - | |
| 183 | - if (!mkdir($this->assDir.$username, 0755)) { | |
| 184 | - logger(\LogType::Auth, '【mkUser】アセットディレクトリを作成出来なかった。chownをご確認下さい。パス:'.$this->assDir.$username); | |
| 185 | - return \Result::Error('エラー:ユーザーのアイコンディレクトリの作成に失敗。'); | |
| 186 | - } | |
| 187 | - | |
| 188 | - $file = scandir($this->dataDir); | |
| 189 | - $lastId = 0; | |
| 190 | - if ($file) { | |
| 191 | - foreach ($file as $f) { | |
| 192 | - if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 193 | - $base = substr($f, 0, -5); // .jsonの削除 | |
| 194 | - $dot = strpos($base, '.'); | |
| 195 | - if (!$dot) continue; | |
| 196 | - $id = substr($base, 0, $dot); | |
| 197 | - if (ctype_digit($id)) { | |
| 198 | - if ((int)$id > $lastId) $lastId = $id; | |
| 199 | - } | |
| 200 | - } | |
| 201 | - $lastId++; | |
| 202 | - } | |
| 203 | - | |
| 204 | - $user = new \stdClass; | |
| 205 | - $user->id = $lastId; | |
| 206 | - $user->username = $username; | |
| 207 | - $user->password = password_hash($password, $this->algo); | |
| 208 | - $user->avatar = ''; | |
| 209 | - $user->email = $email; | |
| 210 | - $user->regDate = time(); | |
| 211 | - $user->namecolor = ''; | |
| 212 | - $user->displayname = ''; | |
| 213 | - $user->gender = \Gender::UNKNOWN; | |
| 214 | - $user->role = \Roles::MEMBER; | |
| 215 | - $user->tokens = []; | |
| 216 | - | |
| 217 | - $path = "{$this->dataDir}{$lastId}.{$username}.json"; | |
| 218 | - $json = json_encode($user, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 219 | - | |
| 220 | - if (file_put_contents($path, $json) === false) { | |
| 221 | - logger(\LogType::Auth, '【mkUser】JSONファイルを作成出来なかった。パス:'.$path); | |
| 222 | - rmdir($this->assDir.$username); | |
| 223 | - return \Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 224 | - } | |
| 225 | - | |
| 226 | - return \Result::Success(); | |
| 227 | - } | |
| 228 | - | |
| 229 | - //////////////////// | |
| 230 | - | |
| 231 | - private function verifyLogin(string $username, #[SensitiveParameter] string $password): \Result { | |
| 232 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 233 | - $userData = $this->getUserData(); | |
| 234 | - if ($username !== $userData->username || !password_verify($password, $userData->password)) { | |
| 235 | - return \Result::Error('エラー:ユーザー名又はパスワードが一致していません。'); | |
| 236 | - } | |
| 237 | - | |
| 238 | - if (password_needs_rehash($userData->password, $this->algo)) { | |
| 239 | - $userData->password = password_hash($password, $this->algo); | |
| 240 | - $path = $this->dataDir.$this->id.'.'.$userData->username.'.json'; | |
| 241 | - $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 242 | - | |
| 243 | - if (file_put_contents($path, $json) === false) { | |
| 244 | - logger(\LogType::Auth, '【verifyLogin】JSONファイルを変更出来なかった。chownをご確認下さい。パス:'.$path); | |
| 245 | - return \Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 246 | - } | |
| 247 | - } | |
| 248 | - | |
| 249 | - return \Result::Success(); | |
| 250 | - } | |
| 251 | - | |
| 252 | - private function purgeOldTokens(\stdClass $userData): \stdClass|\Result { | |
| 253 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 254 | - // 有効期限切れたトークンの削除 | |
| 255 | - $out = $userData; | |
| 256 | - $out->tokens = array_filter($userData->tokens, function($t): bool { | |
| 257 | - return ($t->expDate ?? 0) > time(); | |
| 258 | - }); | |
| 259 | - | |
| 260 | - $path = $this->dataDir.$userData->id.'.'.$userData->username.'.json'; | |
| 261 | - $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 262 | - | |
| 263 | - if (file_put_contents($path, $json) === false) { | |
| 264 | - logger(\LogType::Auth, '【purgeOldTokens】JSONファイルを変更出来なかった。chownをご確認下さい。パス:'.$path); | |
| 265 | - return \Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 266 | - } | |
| 267 | - | |
| 268 | - return $out; | |
| 269 | - } | |
| 270 | - | |
| 271 | - private function getUserId(?string $username = null, ?string $token = null): int { | |
| 272 | - if (!AUTH_ENABLED) return 0; | |
| 273 | - if (!$username && !$token) return 0; | |
| 274 | - $file = scandir($this->dataDir); | |
| 275 | - $matches = []; | |
| 276 | - $id = 0; | |
| 277 | - | |
| 278 | - foreach ($file as $f) { | |
| 279 | - if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 280 | - | |
| 281 | - $path = "{$this->dataDir}{$f}"; | |
| 282 | - if (!is_file($path)) continue; | |
| 283 | - $content = file_get_contents($path); | |
| 284 | - if ($token && str_contains($content, $token)) { | |
| 285 | - $matches[] = $path; | |
| 286 | - break; | |
| 287 | - } else if (!$token && str_contains($content, '"username": "'.$username.'",')) { | |
| 288 | - $matches[] = $path; | |
| 289 | - break; | |
| 290 | - } | |
| 291 | - } | |
| 292 | - if (!isset($matches[0])) return 0; | |
| 293 | - $id = (int)preg_replace('/^(.*?)\.(.*?)$/', "$1", str_replace($this->dataDir, '', $matches[0])); | |
| 294 | - | |
| 295 | - return $id; | |
| 296 | - } | |
| 297 | - | |
| 298 | - private function getUserData(): ?\stdClass { | |
| 299 | - if (!AUTH_ENABLED) return new \stdClass; | |
| 300 | - $file = scandir($this->dataDir); | |
| 301 | - $userFile = ""; | |
| 302 | - $matches = []; | |
| 303 | - | |
| 304 | - foreach ($file as $f) { | |
| 305 | - if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 306 | - if (preg_match('/^'.$this->id.'\.(.*?)\.json$/', $f, $matches)) { | |
| 307 | - $userFile = $matches[0]; | |
| 308 | - break; | |
| 309 | - } | |
| 310 | - } | |
| 311 | - $path = $this->dataDir.$userFile; | |
| 312 | - unset($file, $userFile); | |
| 313 | - | |
| 314 | - if (!file_exists($path) || !is_readable($path)) { | |
| 315 | - kys('エラー'); | |
| 316 | - } | |
| 317 | - | |
| 318 | - $fp = fopen($path, 'r'); | |
| 319 | - assert_not_null($fp); | |
| 320 | - if (!$fp) kys('ユーザーが存在しない。'); | |
| 321 | - $lines = ""; | |
| 322 | - while (($buf = fgets($fp, 4096)) !== false) $lines .= $buf.PHP_EOL; | |
| 323 | - if (!feof($fp)) kys("エラー:不明なエラー"); | |
| 324 | - fclose($fp); | |
| 325 | - unset($path); | |
| 326 | - return json_decode($lines); | |
| 327 | - } | |
| 328 | - | |
| 329 | - private function getUserDataFromName(string $name): \stdClass|null { | |
| 330 | - if (!AUTH_ENABLED) return new \stdClass; | |
| 331 | - $file = scandir($this->dataDir); | |
| 332 | - $userFile = ""; | |
| 333 | - $matches = []; | |
| 334 | - | |
| 335 | - foreach ($file as $f) { | |
| 336 | - if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 337 | - if (preg_match('/^(.*?)\.(.*?)\.json$/', $f, $matches)) { | |
| 338 | - if (isset($matches[2]) && $matches[2] === $name) { | |
| 339 | - $userFile = $matches[0]; | |
| 340 | - break; | |
| 341 | - } | |
| 342 | - } | |
| 343 | - } | |
| 344 | - if (!$userFile) return null; | |
| 345 | - $path = $this->dataDir.$userFile; | |
| 346 | - unset($file, $userFile); | |
| 347 | - | |
| 348 | - if (!file_exists($path) || !is_readable($path)) { | |
| 349 | - kys('エラー'); | |
| 350 | - } | |
| 351 | - | |
| 352 | - $fp = fopen($path, 'r'); | |
| 353 | - assert_not_null($fp); | |
| 354 | - if (!$fp) kys('ユーザーが存在しない。'); | |
| 355 | - $lines = ""; | |
| 356 | - while (($buf = fgets($fp, 4096)) !== false) $lines .= $buf.PHP_EOL; | |
| 357 | - if (!feof($fp)) kys("エラー:不明なエラー"); | |
| 358 | - fclose($fp); | |
| 359 | - unset($path); | |
| 360 | - return json_decode($lines); | |
| 361 | - } | |
| 362 | - | |
| 363 | - private function isEmailExist(?string $email = null): \Result { | |
| 364 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 365 | - $userList = scandir($this->dataDir); | |
| 366 | - $matches = []; | |
| 367 | - | |
| 368 | - foreach ($userList as $f) { | |
| 369 | - if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 370 | - $path = "{$this->dataDir}{$f}"; | |
| 371 | - if (!is_file($path)) continue; | |
| 372 | - $content = file_get_contents($path); | |
| 373 | - $file = str_replace('.json', '', $f); | |
| 374 | - if (str_contains($content, '"email": "'.$email.'",')) { | |
| 375 | - $matches[] = $email; | |
| 376 | - } | |
| 377 | - if (count($matches) > 0) return \Result::Error("ユーザー「{$email}」は既に存在します。"); | |
| 378 | - } | |
| 379 | - | |
| 380 | - return \Result::Success('エラー:メールアドレスが存在しません。'); | |
| 381 | - } | |
| 382 | - | |
| 383 | - private function verifyEmail(?string $email = null): \Result { | |
| 384 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 385 | - if (null === $email || '' === $email) return \Result::Error('エラー:メールアドレスをご入力下さい。'); | |
| 386 | - if (strpos($email, '@') === false) return \Result::Error('エラー:メールアドレスは不正です。'); | |
| 387 | - | |
| 388 | - $domain = explode('@', $email)[1]; | |
| 389 | - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { | |
| 390 | - return \Result::Error('エラー:メールアドレスは不正です。'); | |
| 391 | - } | |
| 392 | - | |
| 393 | - if (!checkdnsrr($domain, 'MX')) { | |
| 394 | - return \Result::Error('エラー:メールアドレスは不正です。'); | |
| 395 | - } | |
| 396 | - | |
| 397 | - $res = $this->isEmailExist($email); | |
| 398 | - if (!$res->isSuccess) return \Result::Error($res->message); | |
| 399 | - | |
| 400 | - return \Result::Success(); | |
| 401 | - } | |
| 402 | - | |
| 403 | - private function verifyUsername(?string $username): \Result { | |
| 404 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 405 | - if (null === $username || '' === $username) return \Result::Error('エラー:ユーザー名をご入力下さい。'); | |
| 406 | - if (strlen($username) < 6) return \Result::Error('エラー:ユーザー名は6文字以上をご入力下さい。'); | |
| 407 | - | |
| 408 | - $res = $this->isUserExist($username); | |
| 409 | - if ($res->isSuccess) return \Result::Error($res->message); | |
| 410 | - | |
| 411 | - $accepted = 'A-Za-z0-9'; | |
| 412 | - if (preg_match("/[^{$accepted}]/", $username)) { | |
| 413 | - return \Result::Error('エラー:ユーザー名に不正な文字が含みます。'); | |
| 414 | - } | |
| 415 | - | |
| 416 | - return \Result::Success(); | |
| 417 | - } | |
| 418 | - | |
| 419 | - private function verifyPassword(#[SensitiveParameter] ?string $password = null, #[SensitiveParameter] ?string $passwordVerify = null): \Result { | |
| 420 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 421 | - if (null === $password || '' === $password) return \Result::Error('エラー:パスワードをご入力下さい。'); | |
| 422 | - if ($password !== $passwordVerify) return \Result::Error('エラー:パスワードは一致していません。'); | |
| 423 | - | |
| 424 | - $res = $this->checkPasswordStandards($password); | |
| 425 | - if (!$res->isSuccess) { | |
| 426 | - return \Result::Error($res->message); | |
| 427 | - } | |
| 428 | - | |
| 429 | - return \Result::Success(); | |
| 430 | - } | |
| 431 | - | |
| 432 | - private function checkPasswordStandards(#[SensitiveParameter] ?string $password = null): \Result { | |
| 433 | - if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 434 | - if (strlen($password) < $this->minPassLen) return \Result::Error("エラー:パスワードは{$this->minPassLen}以上をご入力下さい。"); | |
| 435 | - | |
| 436 | - if (!\countmatch($password)) { | |
| 437 | - return \Result::Error('エラー:パスワードに不正な文字が含みます。'); | |
| 438 | - } | |
| 439 | - | |
| 440 | - $countUpper = preg_match_all('/[A-Z]/', $password) < 2; | |
| 441 | - $countLower = preg_match_all('/[a-z]/', $password) < 2; | |
| 442 | - $countDigit = preg_match_all('/[0-9]/', $password) < 2; | |
| 443 | - $countSymbol = \count_special_chars($password) < 2; | |
| 444 | - | |
| 445 | - if ($countUpper || $countLower || $countDigit || $countSymbol) { | |
| 446 | - return \Result::Error('エラー:パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。'); | |
| 447 | - } | |
| 448 | - | |
| 449 | - return \Result::Success(); | |
| 450 | - } | |
| 451 | -}; |
削除
src/Site/Lib/Curl.php
| @@ -1,693 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -/** | |
| 5 | - * php_curlへの依存を排除するための独自のCURL実装 | |
| 6 | - */ | |
| 7 | -class Curl { | |
| 8 | - // リクエスト関連のプロパティ | |
| 9 | - private string $url = ''; | |
| 10 | - private string $method = 'GET'; | |
| 11 | - private int $timeout = 30; | |
| 12 | - private array $headers = []; | |
| 13 | - private array $cookies = []; | |
| 14 | - private array $postFields = []; | |
| 15 | - private string $postRaw = ''; | |
| 16 | - private string $userAgent = 'LittleBeast/1.0'; | |
| 17 | - private bool $followRedirects = true; | |
| 18 | - private int $maxRedirects = 5; | |
| 19 | - private bool $verbose = false; | |
| 20 | - private $stderr = null; | |
| 21 | - private string $caInfoPath = ''; | |
| 22 | - private bool $verifySSL = true; | |
| 23 | - private string $username = ''; | |
| 24 | - private string $password = ''; | |
| 25 | - private string $referer = ''; | |
| 26 | - | |
| 27 | - // レスポンス関連のプロパティ | |
| 28 | - private array $responseHeaders = []; | |
| 29 | - private string $responseBody = ''; | |
| 30 | - private int $responseCode = 0; | |
| 31 | - private string $responseError = ''; | |
| 32 | - private array $info = []; | |
| 33 | - | |
| 34 | - /** | |
| 35 | - * コンストラクタ | |
| 36 | - * | |
| 37 | - * @param string|null $url リクエスト先のURL | |
| 38 | - */ | |
| 39 | - public function __construct(?string $url = null) { | |
| 40 | - if (!CURL_ENABLED) return; | |
| 41 | - if ($url !== null) { | |
| 42 | - $this->url = $url; | |
| 43 | - } | |
| 44 | - } | |
| 45 | - | |
| 46 | - /** | |
| 47 | - * リクエスト先のURLを設定する | |
| 48 | - * | |
| 49 | - * @param string $url リクエスト先のURL | |
| 50 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 51 | - */ | |
| 52 | - public function setUrl(string $url): Curl { | |
| 53 | - $this->url = $url; | |
| 54 | - return $this; | |
| 55 | - } | |
| 56 | - | |
| 57 | - /** | |
| 58 | - * リクエストメソッドを設定する | |
| 59 | - * | |
| 60 | - * @param string $method GE又はPOST等のHTTPメソッド | |
| 61 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 62 | - */ | |
| 63 | - public function setMethod(string $method): Curl { | |
| 64 | - $this->method = strtoupper($method); | |
| 65 | - return $this; | |
| 66 | - } | |
| 67 | - | |
| 68 | - /** | |
| 69 | - * リクエストのタイムアウト秒数を設定する | |
| 70 | - * | |
| 71 | - * @param int $seconds タイムアウト秒数 | |
| 72 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 73 | - */ | |
| 74 | - public function setTimeout(int $seconds): Curl { | |
| 75 | - $this->timeout = (int)$seconds; | |
| 76 | - return $this; | |
| 77 | - } | |
| 78 | - | |
| 79 | - /** | |
| 80 | - * リクエストヘッダーを設定する | |
| 81 | - * | |
| 82 | - * @param array $headers リクエストヘッダーの配列 | |
| 83 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 84 | - */ | |
| 85 | - public function setHeaders(array $headers): Curl { | |
| 86 | - $this->headers = $headers; | |
| 87 | - return $this; | |
| 88 | - } | |
| 89 | - | |
| 90 | - /** | |
| 91 | - * 単一のヘッダーを追加する | |
| 92 | - * | |
| 93 | - * @param string $name ヘッダー名 | |
| 94 | - * @param mixed $value ヘッダー値 | |
| 95 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 96 | - */ | |
| 97 | - public function addHeader(string $name, mixed $value): Curl { | |
| 98 | - $this->headers[$name] = $value; | |
| 99 | - return $this; | |
| 100 | - } | |
| 101 | - | |
| 102 | - /** | |
| 103 | - * リクエストのクッキーを設定する | |
| 104 | - * | |
| 105 | - * @param array $cookies クッキーの配列 | |
| 106 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 107 | - */ | |
| 108 | - public function setCookies(array $cookies): Curl { | |
| 109 | - $this->cookies = $cookies; | |
| 110 | - return $this; | |
| 111 | - } | |
| 112 | - | |
| 113 | - /** | |
| 114 | - * 単一のクッキーを追加する | |
| 115 | - * | |
| 116 | - * @param string $name クッキー名 | |
| 117 | - * @param mixed $value クッキー値 | |
| 118 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 119 | - */ | |
| 120 | - public function addCookie(string $name, mixed $value): Curl { | |
| 121 | - $this->cookies[$name] = $value; | |
| 122 | - return $this; | |
| 123 | - } | |
| 124 | - | |
| 125 | - /** | |
| 126 | - * POSTフィールドを設定する | |
| 127 | - * | |
| 128 | - * @param array $fields POSTデータの配列 | |
| 129 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 130 | - */ | |
| 131 | - public function setPostFields(array $fields): Curl { | |
| 132 | - $this->postFields = $fields; | |
| 133 | - return $this; | |
| 134 | - } | |
| 135 | - | |
| 136 | - /** | |
| 137 | - * 生のPOSTデータを設定する | |
| 138 | - * | |
| 139 | - * @param string $data 生のPOSTデータ | |
| 140 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 141 | - */ | |
| 142 | - public function setPostRaw(string $data): Curl { | |
| 143 | - $this->postRaw = $data; | |
| 144 | - return $this; | |
| 145 | - } | |
| 146 | - | |
| 147 | - /** | |
| 148 | - * ユーザーエージェントを設定する | |
| 149 | - * | |
| 150 | - * @param string $userAgent カスタムユーザーエージェント | |
| 151 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 152 | - */ | |
| 153 | - public function setUserAgent(string $userAgent): Curl { | |
| 154 | - $this->userAgent = $userAgent; | |
| 155 | - return $this; | |
| 156 | - } | |
| 157 | - | |
| 158 | - /** | |
| 159 | - * リダイレクトを追跡するかどうかを設定する | |
| 160 | - * | |
| 161 | - * @param bool $follow 追跡するかどうか(デフォルトはtrue) | |
| 162 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 163 | - */ | |
| 164 | - public function setFollowRedirects(bool $follow): Curl { | |
| 165 | - $this->followRedirects = (bool)$follow; | |
| 166 | - return $this; | |
| 167 | - } | |
| 168 | - | |
| 169 | - /** | |
| 170 | - * 追跡するリダイレクトの最大数を設定する | |
| 171 | - * | |
| 172 | - * @param int $max リダイレクトの最大数 | |
| 173 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 174 | - */ | |
| 175 | - public function setMaxRedirects(int $max): Curl { | |
| 176 | - $this->maxRedirects = (int)$max; | |
| 177 | - return $this; | |
| 178 | - } | |
| 179 | - | |
| 180 | - /** | |
| 181 | - * SSL証明書を検証するかどうかを設定する | |
| 182 | - * | |
| 183 | - * @param bool $verify SSL検証を行うかどうか(デフォルトはtrue) | |
| 184 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 185 | - */ | |
| 186 | - public function setVerifySSL(bool $verify): Curl { | |
| 187 | - $this->verifySSL = (bool)$verify; | |
| 188 | - return $this; | |
| 189 | - } | |
| 190 | - | |
| 191 | - /** | |
| 192 | - * 基本認証の資格情報を設定する | |
| 193 | - * | |
| 194 | - * @param string $username ユーザー名 | |
| 195 | - * @param string $password パスワード | |
| 196 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 197 | - */ | |
| 198 | - public function setBasicAuth(string $username, string $password): Curl { | |
| 199 | - $this->username = $username; | |
| 200 | - $this->password = $password; | |
| 201 | - return $this; | |
| 202 | - } | |
| 203 | - | |
| 204 | - /** | |
| 205 | - * リファラーURLを設定する | |
| 206 | - * | |
| 207 | - * @param string $referer リファラーURL | |
| 208 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 209 | - */ | |
| 210 | - public function setReferer(string $referer): Curl { | |
| 211 | - $this->referer = $referer; | |
| 212 | - return $this; | |
| 213 | - } | |
| 214 | - | |
| 215 | - /** | |
| 216 | - * 詳細ログを有効にする | |
| 217 | - * | |
| 218 | - * @param bool $verbose 詳細ログを有効にするかどうか | |
| 219 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 220 | - */ | |
| 221 | - public function setVerbose(bool $verbose): Curl { | |
| 222 | - $this->verbose = (bool)$verbose; | |
| 223 | - return $this; | |
| 224 | - } | |
| 225 | - | |
| 226 | - /** | |
| 227 | - * エラー出力先を設定する | |
| 228 | - * | |
| 229 | - * @param resource $handle エラー出力先のファイルハンドル | |
| 230 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 231 | - */ | |
| 232 | - public function setStderr($handle): Curl { | |
| 233 | - $this->stderr = $handle; | |
| 234 | - return $this; | |
| 235 | - } | |
| 236 | - | |
| 237 | - /** | |
| 238 | - * SSL証明書のファイルパスを設定する | |
| 239 | - * | |
| 240 | - * @param string $path 証明書ファイルのパス | |
| 241 | - * @return Curl このインスタンス(メソッドチェーン用) | |
| 242 | - */ | |
| 243 | - public function setCaInfo(string $path): Curl { | |
| 244 | - $this->caInfoPath = $path; | |
| 245 | - return $this; | |
| 246 | - } | |
| 247 | - | |
| 248 | - /** | |
| 249 | - * リクエストを実行する | |
| 250 | - * | |
| 251 | - * @return Result 成功または失敗 | |
| 252 | - */ | |
| 253 | - public function execute(): \Result { | |
| 254 | - if (!CURL_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 255 | - if (empty($this->url)) { | |
| 256 | - $this->responseError = 'URLがありません'; | |
| 257 | - return \Result::Error($this->responseError); | |
| 258 | - } | |
| 259 | - | |
| 260 | - // レスポンスデータのリセット | |
| 261 | - $this->responseHeaders = []; | |
| 262 | - $this->responseBody = ''; | |
| 263 | - $this->responseCode = 0; | |
| 264 | - $this->responseError = ''; | |
| 265 | - $this->info = [ | |
| 266 | - 'url' => $this->url, | |
| 267 | - 'content_type' => '', | |
| 268 | - 'http_code' => 0, | |
| 269 | - 'header_size' => 0, | |
| 270 | - 'request_size' => 0, | |
| 271 | - 'total_time' => 0, | |
| 272 | - 'redirect_count' => 0, | |
| 273 | - 'redirect_url' => '', | |
| 274 | - ]; | |
| 275 | - | |
| 276 | - $startTime = microtime(true); | |
| 277 | - | |
| 278 | - // ソケットベースの実装を使用する | |
| 279 | - $redirectCount = 0; | |
| 280 | - $currentUrl = $this->url; | |
| 281 | - $originalMethod = $this->method; | |
| 282 | - | |
| 283 | - do { | |
| 284 | - if ($this->verbose && $this->stderr) { | |
| 285 | - fwrite($this->stderr, "* 接続中: {$currentUrl}\n"); | |
| 286 | - } | |
| 287 | - | |
| 288 | - $parsed = parse_url($currentUrl); | |
| 289 | - if (!$parsed) { | |
| 290 | - $this->responseError = "無効なURL: {$currentUrl}"; | |
| 291 | - return \Result::Error($this->responseError); | |
| 292 | - } | |
| 293 | - | |
| 294 | - $scheme = isset($parsed['scheme']) ? strtolower($parsed['scheme']) : 'http'; | |
| 295 | - $host = $parsed['host']; | |
| 296 | - $port = isset($parsed['port']) | |
| 297 | - ? $parsed['port'] : ($scheme === 'https' ? 443 : 80); | |
| 298 | - $path = isset($parsed['path']) ? $parsed['path'] : '/'; | |
| 299 | - if (isset($parsed['query'])) { | |
| 300 | - $path .= '?'.$parsed['query']; | |
| 301 | - } | |
| 302 | - | |
| 303 | - // Basic認証 | |
| 304 | - $authHeader = ''; | |
| 305 | - if (!empty($this->username) && !empty($this->password)) { | |
| 306 | - $authHeader = "Authorization: Basic " | |
| 307 | - .base64_encode($this->username.':'.$this->password)."\r\n"; | |
| 308 | - } elseif (isset($parsed['user']) && isset($parsed['pass'])) { | |
| 309 | - $authHeader = "Authorization: Basic " | |
| 310 | - .base64_encode($parsed['user'].':'.$parsed['pass'])."\r\n"; | |
| 311 | - } | |
| 312 | - | |
| 313 | - // 送信するHTTPリクエストを構築 | |
| 314 | - $method = $this->method; | |
| 315 | - $httpData = ''; | |
| 316 | - | |
| 317 | - if ($method === 'POST' || $method === 'PUT') { | |
| 318 | - if (!empty($this->postRaw)) { | |
| 319 | - $httpData = $this->postRaw; | |
| 320 | - } elseif (!empty($this->postFields)) { | |
| 321 | - $httpData = http_build_query($this->postFields); | |
| 322 | - if (!isset($this->headers['Content-Type'])) { | |
| 323 | - $this->headers['Content-Type'] = 'application/x-www-form-urlencoded'; | |
| 324 | - } | |
| 325 | - } | |
| 326 | - $this->headers['Content-Length'] = strlen($httpData); | |
| 327 | - } | |
| 328 | - | |
| 329 | - // HTTPリクエストを構築 | |
| 330 | - $accept = 'Accept: */*'; | |
| 331 | - foreach ($this->headers as $h) { | |
| 332 | - if (str_contains($h, 'Accept:')) $accept = $h; | |
| 333 | - } | |
| 334 | - | |
| 335 | - $request = "{$method} {$path} HTTP/1.1\r\n"; | |
| 336 | - $request .= "Host: {$host}\r\n"; | |
| 337 | - $request .= "User-Agent: {$this->userAgent}\r\n"; | |
| 338 | - $request .= "{$accept}\r\n"; | |
| 339 | - $request .= "Connection: close\r\n"; | |
| 340 | - | |
| 341 | - if (!empty($authHeader)) { | |
| 342 | - $request .= $authHeader; | |
| 343 | - } | |
| 344 | - | |
| 345 | - // ヘッダーを追加 | |
| 346 | - foreach ($this->headers as $name => $value) { | |
| 347 | - $request .= "{$name}: {$value}\r\n"; | |
| 348 | - } | |
| 349 | - | |
| 350 | - // リファラーが設定されていれば追加 | |
| 351 | - if (!empty($this->referer) && !isset($this->headers['Referer'])) { | |
| 352 | - $request .= "Referer: {$this->referer}\r\n"; | |
| 353 | - } | |
| 354 | - | |
| 355 | - // クッキーヘッダーを追加 | |
| 356 | - if (!empty($this->cookies) && !isset($this->headers['Cookie'])) { | |
| 357 | - $cookieStrings = []; | |
| 358 | - foreach ($this->cookies as $name => $value) { | |
| 359 | - $cookieStrings[] = $name.'='.urlencode($value); | |
| 360 | - } | |
| 361 | - $request .= 'Cookie: '.implode('; ', $cookieStrings)."\r\n"; | |
| 362 | - } | |
| 363 | - | |
| 364 | - $request .= "\r\n"; | |
| 365 | - | |
| 366 | - // POSTデータを追加 | |
| 367 | - if ($method === 'POST' || $method === 'PUT') { | |
| 368 | - $request .= $httpData; | |
| 369 | - } | |
| 370 | - | |
| 371 | - if ($this->verbose && $this->stderr) { | |
| 372 | - fwrite($this->stderr, "* リクエストヘッダー:\n{$request}\n"); | |
| 373 | - } | |
| 374 | - | |
| 375 | - // ソケット接続を確立 | |
| 376 | - $errno = 0; | |
| 377 | - $errstr = ''; | |
| 378 | - | |
| 379 | - if ($scheme === 'https') { | |
| 380 | - $sslOptions = [ | |
| 381 | - 'verify_peer' => $this->verifySSL, | |
| 382 | - 'verify_peer_name' => $this->verifySSL | |
| 383 | - ]; | |
| 384 | - | |
| 385 | - if (!empty($this->caInfoPath) && file_exists($this->caInfoPath)) { | |
| 386 | - $sslOptions['cafile'] = $this->caInfoPath; | |
| 387 | - } | |
| 388 | - | |
| 389 | - $context = stream_context_create(['ssl' => $sslOptions]); | |
| 390 | - | |
| 391 | - $socket = @stream_socket_client( | |
| 392 | - "tls://{$host}:{$port}", | |
| 393 | - $errno, | |
| 394 | - $errstr, | |
| 395 | - $this->timeout, | |
| 396 | - STREAM_CLIENT_CONNECT, | |
| 397 | - $context | |
| 398 | - ); | |
| 399 | - } else { | |
| 400 | - $socket = @fsockopen($host, $port, $errno, $errstr, $this->timeout); | |
| 401 | - } | |
| 402 | - | |
| 403 | - if (!$socket) { | |
| 404 | - $this->responseError = "接続出来ません: {$errstr} ({$errno})"; | |
| 405 | - if ($this->verbose && $this->stderr) { | |
| 406 | - fwrite($this->stderr, "* エラー: {$this->responseError}\n"); | |
| 407 | - } | |
| 408 | - return \Result::Error($this->responseError); | |
| 409 | - } | |
| 410 | - | |
| 411 | - // タイムアウトを設定 | |
| 412 | - stream_set_timeout($socket, $this->timeout); | |
| 413 | - | |
| 414 | - // リクエストを送信 | |
| 415 | - fwrite($socket, $request); | |
| 416 | - | |
| 417 | - // レスポンスを読み込む | |
| 418 | - $rawResponse = ''; | |
| 419 | - $headers = ''; | |
| 420 | - $body = ''; | |
| 421 | - $headersComplete = false; | |
| 422 | - | |
| 423 | - // ヘッダーとボディを分けて読み込む | |
| 424 | - while (!feof($socket)) { | |
| 425 | - $line = fgets($socket); | |
| 426 | - if ($line === false) { | |
| 427 | - break; | |
| 428 | - } | |
| 429 | - | |
| 430 | - $rawResponse .= $line; | |
| 431 | - | |
| 432 | - if (!$headersComplete) { | |
| 433 | - if (trim($line) === '') { | |
| 434 | - $headersComplete = true; | |
| 435 | - } else { | |
| 436 | - $headers .= $line; | |
| 437 | - } | |
| 438 | - } else { | |
| 439 | - $body .= $line; | |
| 440 | - } | |
| 441 | - } | |
| 442 | - | |
| 443 | - fclose($socket); | |
| 444 | - | |
| 445 | - // レスポンスヘッダーを解析 | |
| 446 | - $headerLines = explode("\r\n", $headers); | |
| 447 | - | |
| 448 | - // ステータスコードを取得 | |
| 449 | - $statusLine = isset($headerLines[0]) ? $headerLines[0] : ''; | |
| 450 | - $statusParts = explode(' ', $statusLine, 3); | |
| 451 | - $this->responseCode = isset($statusParts[1]) ? (int)$statusParts[1] : 0; | |
| 452 | - $this->info['http_code'] = $this->responseCode; | |
| 453 | - | |
| 454 | - // ヘッダーを解析 | |
| 455 | - $this->responseHeaders = []; | |
| 456 | - $redirectUrl = ''; | |
| 457 | - | |
| 458 | - foreach ($headerLines as $index => $header) { | |
| 459 | - if ($index === 0) continue; | |
| 460 | - | |
| 461 | - if (strpos($header, ':') !== false) { | |
| 462 | - list($name, $value) = explode(':', $header, 2); | |
| 463 | - $name = trim($name); | |
| 464 | - $value = trim($value); | |
| 465 | - $this->responseHeaders[$name] = $value; | |
| 466 | - | |
| 467 | - if (strtolower($name) === 'content-type') { | |
| 468 | - $this->info['content_type'] = $value; | |
| 469 | - } | |
| 470 | - | |
| 471 | - // リダイレクトをチェック | |
| 472 | - if ($this->followRedirects && | |
| 473 | - strtolower($name) === 'location' && | |
| 474 | - $this->responseCode >= 300 && | |
| 475 | - $this->responseCode < 400) { | |
| 476 | - | |
| 477 | - $redirectUrl = $value; | |
| 478 | - | |
| 479 | - // 相対URLを絶対URLに変換 | |
| 480 | - if (strpos($redirectUrl, 'http') !== 0) { | |
| 481 | - if ($redirectUrl[0] === '/') { | |
| 482 | - $redirectUrl = "{$scheme}://{$host}" | |
| 483 | - .($port != 80 && $port != 443 ? ":{$port}" : '').$redirectUrl; | |
| 484 | - } else { | |
| 485 | - $redirectUrl = "{$scheme}://{$host}" | |
| 486 | - .($port != 80 && $port != 443 ? ":{$port}" : '') | |
| 487 | - .dirname($path).'/'.$redirectUrl; | |
| 488 | - } | |
| 489 | - } | |
| 490 | - | |
| 491 | - $this->info['redirect_url'] = $redirectUrl; | |
| 492 | - } | |
| 493 | - } | |
| 494 | - } | |
| 495 | - | |
| 496 | - $this->info['header_size'] += strlen($headers); | |
| 497 | - $this->responseBody .= $body; | |
| 498 | - | |
| 499 | - if ($this->verbose && $this->stderr) { | |
| 500 | - fwrite($this->stderr, "* レスポンスコード: {$this->responseCode}\n"); | |
| 501 | - fwrite($this->stderr, "* レスポンスヘッダー:\n{$headers}\n"); | |
| 502 | - if (!empty($redirectUrl)) { | |
| 503 | - fwrite($this->stderr, "* リダイレクト先: {$redirectUrl}\n"); | |
| 504 | - } | |
| 505 | - } | |
| 506 | - | |
| 507 | - // リダイレクトが必要な場合 | |
| 508 | - if (!empty($redirectUrl) && $redirectCount < $this->maxRedirects) { | |
| 509 | - $currentUrl = $redirectUrl; | |
| 510 | - $redirectCount++; | |
| 511 | - $this->info['redirect_count'] = $redirectCount; | |
| 512 | - | |
| 513 | - // 302や303リダイレクトはGETにメソッドを変更 | |
| 514 | - if ($this->responseCode == 302 || $this->responseCode == 303) { | |
| 515 | - $this->method = 'GET'; | |
| 516 | - $this->postRaw = ''; | |
| 517 | - $this->postFields = []; | |
| 518 | - } | |
| 519 | - } else { | |
| 520 | - break; | |
| 521 | - } | |
| 522 | - } while (true); | |
| 523 | - | |
| 524 | - // リクエスト完了後、元のメソッドに戻す | |
| 525 | - $this->method = $originalMethod; | |
| 526 | - $this->info['total_time'] = microtime(true) - $startTime; | |
| 527 | - | |
| 528 | - return \Result::Success(); | |
| 529 | - } | |
| 530 | - | |
| 531 | - /** | |
| 532 | - * レスポンスボディを取得する | |
| 533 | - * | |
| 534 | - * @return string レスポンスボディ | |
| 535 | - */ | |
| 536 | - public function getResponseBody(): string { | |
| 537 | - return $this->responseBody; | |
| 538 | - } | |
| 539 | - | |
| 540 | - /** | |
| 541 | - * レスポンスヘッダーを取得する | |
| 542 | - * | |
| 543 | - * @return array レスポンスヘッダーの配列 | |
| 544 | - */ | |
| 545 | - public function getResponseHeaders(): array { | |
| 546 | - return $this->responseHeaders; | |
| 547 | - } | |
| 548 | - | |
| 549 | - /** | |
| 550 | - * HTTPレスポンスコードを取得する | |
| 551 | - * | |
| 552 | - * @return int HTTPレスポンスコード | |
| 553 | - */ | |
| 554 | - public function getResponseCode(): int { | |
| 555 | - return $this->responseCode; | |
| 556 | - } | |
| 557 | - | |
| 558 | - /** | |
| 559 | - * エラーメッセージがあれば取得する | |
| 560 | - * | |
| 561 | - * @return string エラーメッセージ | |
| 562 | - */ | |
| 563 | - public function getError(): string { | |
| 564 | - return $this->responseError; | |
| 565 | - } | |
| 566 | - | |
| 567 | - /** | |
| 568 | - * リクエスト/レスポンス情報を取得する | |
| 569 | - * | |
| 570 | - * @return array 情報の配列 | |
| 571 | - */ | |
| 572 | - public function getInfo(): array { | |
| 573 | - return $this->info; | |
| 574 | - } | |
| 575 | - | |
| 576 | - // 機能性メソッド | |
| 577 | - | |
| 578 | - /** | |
| 579 | - * リダイレクトURLを確認する | |
| 580 | - * | |
| 581 | - * @param string $name ヘッダー名 | |
| 582 | - * @param string $value ヘッダー値 | |
| 583 | - * @param string $currentUrl 現在のURL | |
| 584 | - * @return string リダイレクトURL、リダイレクトがない場合は空文字 | |
| 585 | - */ | |
| 586 | - private function checkReds(string $name, string $value, string $currentUrl): string { | |
| 587 | - $redirectUrl = ''; | |
| 588 | - | |
| 589 | - if ($this->followRedirects && (strtolower($name) === 'location' | |
| 590 | - && $this->responseCode >= 300 && $this->responseCode < 400)) { | |
| 591 | - $redirectUrl = $value; | |
| 592 | - | |
| 593 | - if (strpos($redirectUrl, 'http') !== 0) { | |
| 594 | - if ($redirectUrl[0] === '/') { | |
| 595 | - $parsed = parse_url($currentUrl); | |
| 596 | - $redirectUrl = $parsed['scheme'].'://'.$parsed['host'] | |
| 597 | - .(isset($parsed['port']) ? ':'.$parsed['port'] : '') | |
| 598 | - .$redirectUrl; | |
| 599 | - } else { | |
| 600 | - $redirectUrl = dirname($currentUrl).'/'.$redirectUrl; | |
| 601 | - } | |
| 602 | - } | |
| 603 | - } | |
| 604 | - | |
| 605 | - return $redirectUrl; | |
| 606 | - } | |
| 607 | - | |
| 608 | - /** | |
| 609 | - * ヘッダー文字列を構築する | |
| 610 | - * | |
| 611 | - * @return string 構築されたヘッダー文字列 | |
| 612 | - */ | |
| 613 | - private function buildHeaderString(): string { | |
| 614 | - $headers = []; | |
| 615 | - | |
| 616 | - // ユーザー指定のヘッダーを追加 | |
| 617 | - foreach ($this->headers as $name => $value) { | |
| 618 | - $headers[] = "{$name}: {$value}"; | |
| 619 | - } | |
| 620 | - | |
| 621 | - // リファラーが設定されていれば追加 | |
| 622 | - if (!empty($this->referer) && !isset($this->headers['Referer'])) { | |
| 623 | - $headers[] = "Referer: {$this->referer}"; | |
| 624 | - } | |
| 625 | - | |
| 626 | - // 必要に応じてクッキーヘッダーを追加 | |
| 627 | - if (!empty($this->cookies) && !isset($this->headers['Cookie'])) { | |
| 628 | - $cookieStrings = []; | |
| 629 | - foreach ($this->cookies as $name => $value) { | |
| 630 | - $cookieStrings[] = $name.'='.urlencode($value); | |
| 631 | - } | |
| 632 | - | |
| 633 | - $headers[] = 'Cookie: '.implode('; ', $cookieStrings); | |
| 634 | - } | |
| 635 | - | |
| 636 | - return implode("\r\n", $headers)."\r\n"; | |
| 637 | - } | |
| 638 | - | |
| 639 | - /** | |
| 640 | - * レスポンスを解析してヘッダーとボディに分割する | |
| 641 | - * | |
| 642 | - * @param string $response 完全なHTTPレスポンス | |
| 643 | - * @return array [ヘッダー配列, ボディ文字列] | |
| 644 | - */ | |
| 645 | - private function parseResponse(string $response): array { | |
| 646 | - $parts = explode("\r\n\r\n", $response, 2); | |
| 647 | - | |
| 648 | - if (count($parts) < 2) { | |
| 649 | - return [[], '']; | |
| 650 | - } | |
| 651 | - | |
| 652 | - $headers = explode("\r\n", $parts[0]); | |
| 653 | - $body = $parts[1]; | |
| 654 | - | |
| 655 | - // チャンク転送エンコーディングを処理 | |
| 656 | - if (isset($this->responseHeaders['Transfer-Encoding']) && | |
| 657 | - strtolower($this->responseHeaders['Transfer-Encoding']) === 'chunked') { | |
| 658 | - $body = $this->decodeChunkedBody($body); | |
| 659 | - } | |
| 660 | - | |
| 661 | - return [$headers, $body]; | |
| 662 | - } | |
| 663 | - | |
| 664 | - /** | |
| 665 | - * チャンク転送エンコーディングされたボディをデコードする | |
| 666 | - * | |
| 667 | - * @param string $body チャンクエンコードされたボディ | |
| 668 | - * @return string デコードされたボディ | |
| 669 | - */ | |
| 670 | - private function decodeChunkedBody(string $body): string { | |
| 671 | - $decodedBody = ''; | |
| 672 | - $position = 0; | |
| 673 | - | |
| 674 | - while ($position < strlen($body)) { | |
| 675 | - $lineEnd = strpos($body, "\r\n", $position); | |
| 676 | - if ($lineEnd === false) { | |
| 677 | - break; | |
| 678 | - } | |
| 679 | - | |
| 680 | - $chunkSize = hexdec(substr($body, $position, $lineEnd - $position)); | |
| 681 | - | |
| 682 | - if ($chunkSize === 0) { | |
| 683 | - break; | |
| 684 | - } | |
| 685 | - | |
| 686 | - $position = $lineEnd + 2; | |
| 687 | - $decodedBody .= substr($body, $position, $chunkSize); | |
| 688 | - $position += $chunkSize + 2; // チャンクサイズ + CRLF | |
| 689 | - } | |
| 690 | - | |
| 691 | - return $decodedBody; | |
| 692 | - } | |
| 693 | -} |
削除
src/Site/Lib/DiffViewer.php
| @@ -1,130 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -class DiffViewer { | |
| 5 | - private $diffContent; | |
| 6 | - | |
| 7 | - public function __construct(string $filePath) { | |
| 8 | - if (!file_exists($filePath)) { | |
| 9 | - throw new \Exception("Diff file not found: $filePath"); | |
| 10 | - } | |
| 11 | - $this->diffContent = file_get_contents($filePath); | |
| 12 | - } | |
| 13 | - | |
| 14 | - public function displaySideBySide(): string { | |
| 15 | - $lines = explode("\n", $this->diffContent); | |
| 16 | - $fileDiffs = []; | |
| 17 | - $currentFile = null; | |
| 18 | - $hunk = []; | |
| 19 | - $lineNumbers = ['left' => 0, 'right' => 0]; | |
| 20 | - $currentLeftLines = []; | |
| 21 | - $currentRightLines = []; | |
| 22 | - | |
| 23 | - foreach ($lines as $line) { | |
| 24 | - // ファイルヘッダーの処理 | |
| 25 | - if (preg_match('/^---\s+(.+)/', $line, $matches)) { | |
| 26 | - // ファイルを処理する場合、データを保存する | |
| 27 | - if ($currentFile !== null) { | |
| 28 | - $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 29 | - $fileDiffs[$currentFile] = [ | |
| 30 | - 'leftLines' => $currentLeftLines, | |
| 31 | - 'rightLines' => $currentRightLines | |
| 32 | - ]; | |
| 33 | - $hunk = []; | |
| 34 | - $currentLeftLines = []; | |
| 35 | - $currentRightLines = []; | |
| 36 | - $lineNumbers = ['left' => 0, 'right' => 0]; | |
| 37 | - } | |
| 38 | - $currentFile = $matches[1]; | |
| 39 | - continue; | |
| 40 | - } | |
| 41 | - if (preg_match('/^\+\+\+\s+(.+)/', $line)) { | |
| 42 | - continue; | |
| 43 | - } | |
| 44 | - | |
| 45 | - // ハンクヘッダーの処理 (例:@@ -10,6 +10,7 @@) | |
| 46 | - if (preg_match('/^@@\s+-(\d+),\d+\s+\+(\d+),\d+\s+@@/', $line, $matches)) { | |
| 47 | - $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 48 | - $hunk = []; | |
| 49 | - $lineNumbers['left'] = (int)$matches[1]; | |
| 50 | - $lineNumbers['right'] = (int)$matches[2]; | |
| 51 | - continue; | |
| 52 | - } | |
| 53 | - | |
| 54 | - // ハンクでの行列の集まり | |
| 55 | - if (substr($line, 0, 1) === '-' || substr($line, 0, 1) === '+' || substr($line, 0, 1) === ' ') { | |
| 56 | - $hunk[] = $line; | |
| 57 | - } | |
| 58 | - } | |
| 59 | - | |
| 60 | - // 最後のハンク・ファイルの処理 | |
| 61 | - if ($currentFile !== null) { | |
| 62 | - $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 63 | - $fileDiffs[$currentFile] = [ | |
| 64 | - 'leftLines' => $currentLeftLines, | |
| 65 | - 'rightLines' => $currentRightLines | |
| 66 | - ]; | |
| 67 | - } | |
| 68 | - | |
| 69 | - // 各ファイルにHTMLの出力の作成 | |
| 70 | - $html = ''; | |
| 71 | - foreach ($fileDiffs as $fileName => $diff) { | |
| 72 | - $html .= "\n <h2>ファイル: ".htmlspecialchars($fileName)."</h2>\n"; | |
| 73 | - $html .= $this->generateHtml($diff['leftLines'], $diff['rightLines']); | |
| 74 | - } | |
| 75 | - | |
| 76 | - return $html; | |
| 77 | - } | |
| 78 | - | |
| 79 | - private function processHunk(array $hunk, array &$leftLines, array &$rightLines, array &$lineNumbers): void { | |
| 80 | - foreach ($hunk as $line) { | |
| 81 | - $prefix = substr($line, 0, 1); | |
| 82 | - $content = substr($line, 1); | |
| 83 | - | |
| 84 | - if ($prefix === '-') { | |
| 85 | - $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'removed', 'line' => $lineNumbers['left']]; | |
| 86 | - $lineNumbers['left']++; | |
| 87 | - } elseif ($prefix === '+') { | |
| 88 | - $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'added', 'line' => $lineNumbers['right']]; | |
| 89 | - $lineNumbers['right']++; | |
| 90 | - } elseif ($prefix === ' ') { | |
| 91 | - // 両側のコンテキストは同じ行列があるかの確認 | |
| 92 | - while ($lineNumbers['left'] < $lineNumbers['right']) { | |
| 93 | - $leftLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['left']]; | |
| 94 | - $lineNumbers['left']++; | |
| 95 | - } | |
| 96 | - while ($lineNumbers['right'] < $lineNumbers['left']) { | |
| 97 | - $rightLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['right']]; | |
| 98 | - $lineNumbers['right']++; | |
| 99 | - } | |
| 100 | - $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['left']]; | |
| 101 | - $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['right']]; | |
| 102 | - $lineNumbers['left']++; | |
| 103 | - $lineNumbers['right']++; | |
| 104 | - } | |
| 105 | - } | |
| 106 | - } | |
| 107 | - | |
| 108 | - private function generateHtml(array $leftLines, array $rightLines): string { | |
| 109 | - $html = " <p>\n <table class=\"diff-table\">\n"; | |
| 110 | - $html .= " <tr class=\"diff-header\"><th colspan=\"2\">前</th><th colspan=\"2\">新</th></tr>\n"; | |
| 111 | - | |
| 112 | - $maxLines = max(count($leftLines), count($rightLines)); | |
| 113 | - for ($i = 0; $i < $maxLines; $i++) { | |
| 114 | - $left = isset($leftLines[$i]) ? $leftLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; | |
| 115 | - $right = isset($rightLines[$i]) ? $rightLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; | |
| 116 | - | |
| 117 | - $html .= " <tr>\n"; | |
| 118 | - // 左(変更前) | |
| 119 | - $html .= " <td class=\"line-number\">".($left['line'] ?: ' ')."</td>\n"; | |
| 120 | - $html .= " <td class=\"{$left['type']}\">".($left['content'] ?: ' ')."</td>\n"; | |
| 121 | - // 右(変更後) | |
| 122 | - $html .= " <td class=\"line-number\">".($right['line'] ?: ' ')."</td>\n"; | |
| 123 | - $html .= " <td class=\"{$right['type']}\">".($right['content'] ?: ' ')."</td>\n"; | |
| 124 | - $html .= " </tr>\n"; | |
| 125 | - } | |
| 126 | - | |
| 127 | - $html .= " </table>\n </p>\n"; | |
| 128 | - return $html; | |
| 129 | - } | |
| 130 | -} |
削除
src/Site/Lib/Image.php
| @@ -1,79 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -use Site\Lib\Image\ImageInterface; | |
| 5 | -use Site\Lib\Image\Gif; | |
| 6 | -use Site\Lib\Image\Jpeg; | |
| 7 | -use Site\Lib\Image\Png; | |
| 8 | -use Site\Lib\Image\Targa; | |
| 9 | - | |
| 10 | -class RGB { | |
| 11 | - public int $r; | |
| 12 | - public int $g; | |
| 13 | - public int $b; | |
| 14 | - public array $rgb; | |
| 15 | - | |
| 16 | - public function __construct(int $r, int $g, int $b) { | |
| 17 | - $this->r = $r; | |
| 18 | - $this->g = $g; | |
| 19 | - $this->b = $b; | |
| 20 | - $this->rgb = ['r' => $r, 'g' => $g, 'b' => $b]; | |
| 21 | - } | |
| 22 | -} | |
| 23 | - | |
| 24 | -class Image { | |
| 25 | - public string $filename = 'UNKNOWN'; | |
| 26 | - public string $extension = 'UNKNOWN'; | |
| 27 | - public string $type = 'UNSUPPORTED'; | |
| 28 | - public string $size = '0 B'; | |
| 29 | - public int $bytes = 0; | |
| 30 | - public \stdClass $fullInfo; | |
| 31 | - | |
| 32 | - private array $supported = [ | |
| 33 | - 'image/gif' => Gif::class, | |
| 34 | - 'image/jpeg' => Jpeg::class, | |
| 35 | - 'image/png' => Png::class, | |
| 36 | - 'image/x-tga' => Targa::class, | |
| 37 | - ]; | |
| 38 | - | |
| 39 | - public function __construct(string $file) { | |
| 40 | - $file = ROOT.$file; | |
| 41 | - // $file = ROOT.'/public/static/article/mock-screenshot.tga'; | |
| 42 | - if (!file_exists($file)) return; | |
| 43 | - $fp = fopen($file, 'rb'); | |
| 44 | - if (!$fp) return; | |
| 45 | - $extBytes = fread($fp, 15); | |
| 46 | - fclose($fp); | |
| 47 | - | |
| 48 | - $this->bytes = filesize($file); | |
| 49 | - $this->size = $this->formatBytes($this->bytes); | |
| 50 | - $this->extension = pathinfo($file, PATHINFO_EXTENSION); | |
| 51 | - $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); | |
| 52 | - | |
| 53 | - if (substr($extBytes, 0, 3) === "\xff\xd8\xff") $this->type = 'image/jpeg'; | |
| 54 | - else if (substr($extBytes, 0, 15) === "\x52\x49\x46\x46\x1a\x28\x00\x00\x57\x45\x42\x50\x56\x50\x38") $this->type = 'image/webp'; | |
| 55 | - else if (substr($extBytes, 0, 3) === "\x42\x4d\x36") $this->type = 'image/bmp'; | |
| 56 | - else if (substr($extBytes, 0, 6) === "\x89\x50\x4e\x47\x0d\x0a") $this->type = 'image/png'; | |
| 57 | - else if (substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x37\x61" || substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x39\x61") $this->type = 'image/gif'; | |
| 58 | - else if (str_ends_with($file, '.tga')) $this->type = 'image/x-tga'; | |
| 59 | - | |
| 60 | - $this->fullInfo = $this->createHandler($this->type)->parse($file); | |
| 61 | - $this->fullInfo->type = $this->type; | |
| 62 | - $this->fullInfo->size = $this->size; | |
| 63 | - $this->fullInfo->bytes = $this->bytes; | |
| 64 | - } | |
| 65 | - | |
| 66 | - /////////////// | |
| 67 | - | |
| 68 | - private function createHandler(string $ext): ImageInterface { | |
| 69 | - $class = $this->supported[$ext] ?? null; | |
| 70 | - if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$ext}"); | |
| 71 | - return new $class(); | |
| 72 | - } | |
| 73 | - | |
| 74 | - private function formatBytes(int $size, int $precision = 2): string { | |
| 75 | - $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; | |
| 76 | - for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) $size /= 1024; | |
| 77 | - return round($size, $precision).' '.$units[$i]; | |
| 78 | - } | |
| 79 | -} |
削除
src/Site/Lib/Image/Gif.php
| @@ -1,91 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Image; | |
| 3 | - | |
| 4 | -use Site\Lib\Image\ImageInterface; | |
| 5 | - | |
| 6 | -class Gif implements ImageInterface { | |
| 7 | - public int $width = 0; | |
| 8 | - public int $height = 0; | |
| 9 | - public int $bitDepth = 0; | |
| 10 | - public int $numColors = 0; | |
| 11 | - public string $compression = 'NONE'; | |
| 12 | - public int $interlace = 0; | |
| 13 | - public int $bgColor = 0; | |
| 14 | - public float $delayTime = 0; | |
| 15 | - public array $globalColorTable = []; | |
| 16 | - public int $numTransparentPixels = 0; | |
| 17 | - public int $lzwMinCodeSize = 0; | |
| 18 | - public string $imageData = ''; | |
| 19 | - | |
| 20 | - /** | |
| 21 | - * TODO: 現在、インタレースした画像が未対応です。 | |
| 22 | - */ | |
| 23 | - public function parse(string $file): \stdClass { | |
| 24 | - $gif = new \stdClass(); | |
| 25 | - $fp = fopen($file, 'rb'); | |
| 26 | - $magic = fread($fp, 6); // マジックのスキップ | |
| 27 | - | |
| 28 | - // v = リトルエンディアン(2ビット) | |
| 29 | - $this->width = unpack('v', fread($fp, 2))[1]; | |
| 30 | - $this->height = unpack('v', fread($fp, 2))[1]; | |
| 31 | - $packed = ord(fread($fp, 1)); | |
| 32 | - $this->bitDepth = ($packed & 0x07) + 1; | |
| 33 | - $this->numColors = 1 << $this->bitDepth; | |
| 34 | - $hasGct = $this->bitDepth === 8; | |
| 35 | - | |
| 36 | - $gif->width = $this->width; | |
| 37 | - $gif->height = $this->height; | |
| 38 | - $gif->bitDepth = $this->bitDepth; | |
| 39 | - $gif->numColors = $this->numColors; | |
| 40 | - if ($magic === 'GIF87a' || !$hasGct) return $gif; // それ以外情報がない | |
| 41 | - | |
| 42 | - $this->bgColor = (int)bin2hex(fread($fp, 1)); | |
| 43 | - $gif->bgColor = $this->bgColor; | |
| 44 | - bin2hex(fread($fp, 1)); // いつでも 0:0? | |
| 45 | - | |
| 46 | - $gceLen = 0; | |
| 47 | - while (!feof($fp)) { | |
| 48 | - $skip = bin2hex(fread($fp, 3)); | |
| 49 | - $color = new RGB((int)($skip[0].$skip[1]), (int)($skip[2].$skip[3]), (int)($skip[4].$skip[5])); | |
| 50 | - $this->globalColorTable[] = $color->rgb; | |
| 51 | - if ($skip[0].$skip[1] === '21' && $skip[2].$skip[3] === 'f9') { | |
| 52 | - $gceLen = (int)($skip[4].$skip[5]); | |
| 53 | - break; | |
| 54 | - } | |
| 55 | - } | |
| 56 | - | |
| 57 | - // GCE開始 | |
| 58 | - $this->hasAlpha = (ord(fread($fp, 1)) & 0x01) === 0x01; | |
| 59 | - $this->delayTime = (unpack('v', fread($fp, 2))[1]) / 100.0; | |
| 60 | - $this->numTransparentPixels = ord(fread($fp, 1)); | |
| 61 | - $gif->hasAlpha = $this->hasAlpha; | |
| 62 | - $gif->delayTime = $this->delayTime; | |
| 63 | - $gif->numTransparentPixels = $this->numTransparentPixels; | |
| 64 | - fread($fp, 1); // GCE終了 | |
| 65 | - fread($fp, 1); // 画像指定子開始 | |
| 66 | - fread($fp, 4); // 画面のXとY、いつでも(0, 0) | |
| 67 | - fread($fp, 4); // 画像のサイズ、関数の開始で既に保存した | |
| 68 | - $pack = ord(fread($fp, 1)); | |
| 69 | - $this->interlace = ($pack & 0x40); // 画像のサイズ、関数の開始で既に保存した | |
| 70 | - | |
| 71 | - $this->lzwMinCodeSize = ord(fread($fp, 1)); // 画像データ開始 | |
| 72 | - $gif->lzwMinCodeSize = $this->lzwMinCodeSize; | |
| 73 | - $imageData = ''; | |
| 74 | - while (true) { | |
| 75 | - $blockSize = ord(fread($fp, 1)); | |
| 76 | - if ($blockSize === 0) break; | |
| 77 | - $imageData .= fread($fp, 1); | |
| 78 | - } | |
| 79 | - | |
| 80 | - $this->imageData = base64_encode($imageData); | |
| 81 | - $this->colorType = 3; | |
| 82 | - $this->compression = 'LZW'; | |
| 83 | - $gif->colorType = $this->colorType; | |
| 84 | - $gif->compression = $this->compression; | |
| 85 | - $gif->globalColorTable = $this->globalColorTable; | |
| 86 | - $gif->imageData = $this->imageData; | |
| 87 | - | |
| 88 | - fclose($fp); | |
| 89 | - return $gif; | |
| 90 | - } | |
| 91 | -} |
削除
src/Site/Lib/Image/ImageInterface.php
| @@ -1,6 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Image; | |
| 3 | - | |
| 4 | -interface ImageInterface { | |
| 5 | - public function parse(string $file): \stdClass; | |
| 6 | -} |
削除
src/Site/Lib/Image/Jpeg.php
| @@ -1,54 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Image; | |
| 3 | - | |
| 4 | -use Site\Lib\Image\ImageInterface; | |
| 5 | - | |
| 6 | -class Jpeg implements ImageInterface { | |
| 7 | - public int $width = 0; | |
| 8 | - public int $height = 0; | |
| 9 | - public int $bpp = 0; | |
| 10 | - public string $imageData = ''; | |
| 11 | - | |
| 12 | - /** | |
| 13 | - * TODO: 現在、IHDRヘッダーのみを受け取る | |
| 14 | - */ | |
| 15 | - public function parse(string $file): \stdClass { | |
| 16 | - $jpg = new \stdClass; | |
| 17 | - $fp = fopen($file, 'rb'); | |
| 18 | - | |
| 19 | - $i = 0; | |
| 20 | - while (!feof($fp)) { | |
| 21 | - $byte = fread($fp, 1); | |
| 22 | - if ($byte !== "\xFF") continue; | |
| 23 | - | |
| 24 | - $marker = ord(fread($fp, 1)); | |
| 25 | - if ($marker >= 0xC0 && $marker <= 0xC3) { // 0xC0 = SOF0, 0xC1 = SOF1, 0xC2 = SOF2, 0xC3 = SOF3 | |
| 26 | - fread($fp, 2); | |
| 27 | - $this->bpp = ord(fread($fp, 1)); | |
| 28 | - // n = ビッグエンディアン(2ビット) | |
| 29 | - $this->height = unpack('n', fread($fp, 2))[1]; | |
| 30 | - $this->width = unpack('n', fread($fp, 2))[1]; | |
| 31 | - $jpg->width = $this->width; | |
| 32 | - $jpg->height = $this->height; | |
| 33 | - $jpg->bpp = $this->bpp; | |
| 34 | - } else if ($marker == 0xC4) { // DHT (Huffmanテーブル(複数可)) | |
| 35 | - // | |
| 36 | - } else if ($marker == 0xDB) { // DQT (量子化テーブル(複数可)) | |
| 37 | - // | |
| 38 | - } else if ($marker == 0xDD) { // DRI (リセット期間) | |
| 39 | - } else if ($marker == 0xDA) { // SOS(画像開始) | |
| 40 | - } else if ($marker >= 0xD0 && $marker <= 0xD7) { // RST | |
| 41 | - } else if ($marker >= 0xE1 && $marker <= 0xEF) { // APP | |
| 42 | - } else if ($marker == 0xFE) { // COM | |
| 43 | - } else if ($marker == 0xD9) { // EOI(画像終了) | |
| 44 | - break; | |
| 45 | - } | |
| 46 | - } | |
| 47 | - | |
| 48 | - // kys($jpg); | |
| 49 | - | |
| 50 | - $i++; | |
| 51 | - fclose($fp); | |
| 52 | - return $jpg; | |
| 53 | - } | |
| 54 | -} |
削除
src/Site/Lib/Image/Png.php
| @@ -1,616 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Image; | |
| 3 | - | |
| 4 | -use Site\Lib\Image\ImageInterface; | |
| 5 | - | |
| 6 | -class Png implements ImageInterface { | |
| 7 | - public \stdClass $IHDR; // 画像ヘッダー | |
| 8 | - // 横(4バイト) | |
| 9 | - // 丈(4バイト) | |
| 10 | - // ビット深さ(1バイト) | |
| 11 | - // 色類(1バイト) | |
| 12 | - //// | PNG画像タイプ | 色類 | 許可したビット深さ | 解釈 | | |
| 13 | - //// |--------------------------|------|--------------------|----------------------------------------------- | | |
| 14 | - //// | グレースケール | 0 | 1,2,4,8,16 | 各ピクセルはグレースケールのサンプル | | |
| 15 | - //// | トゥルーカラー | 2 | 8,16 | 各ピクセルはR,G,Bトリプル | | |
| 16 | - //// | 指標した色 | 3 | 1,2,4,8 | 各ピクセルはパレット指標(PLTEチャンクあり) | | |
| 17 | - //// | グレースケール+アルファ | 4 | 8,16 | 各ピクセルはグレースケール+アルファのサンプル | | |
| 18 | - //// | トゥルーカラー+アルファ | 6 | 8,16 | 各ピクセルはR,G,B,Aトリプル | | |
| 19 | - // 圧縮関数(1バイト) | |
| 20 | - //// 0 (DEFLATE)しかない | |
| 21 | - // フィルター関数(1バイト) | |
| 22 | - //// 0(アダプティブ・フィルタリング)しかない | |
| 23 | - // インタレース関数(1バイト) | |
| 24 | - //// 0 = なし | |
| 25 | - //// 1 = Adam7 | |
| 26 | - public \stdClass $PLTE; // パレット(赤、緑、青) | |
| 27 | - // 色類3 = 必須 | |
| 28 | - // 色類2と6 = 任意 | |
| 29 | - public \stdClass $IDAT; // 画像データ | |
| 30 | - public \stdClass $IEND; // 画像終了 | |
| 31 | - | |
| 32 | - public \stdClass $tRNS; // 透明情報 | |
| 33 | - // 色類0 = グレーサンプル値(2バイト) | |
| 34 | - // 色類2 = RGBサンプル値(2x2x2バイト) | |
| 35 | - // 色類3 = パレット指標のアルファ(各1バイト) | |
| 36 | - public \stdClass $cHRM; // 主な色度とホワイトポイント | |
| 37 | - // 白X(4バイト) | |
| 38 | - // 白Y(4バイト) | |
| 39 | - // 赤X(4バイト) | |
| 40 | - // 赤Y(4バイト) | |
| 41 | - // 緑X(4バイト) | |
| 42 | - // 緑Y(4バイト) | |
| 43 | - // 青X(4バイト) | |
| 44 | - // 青Y(4バイト) | |
| 45 | - public \stdClass $gAMA; // ガンマ(4バイト) | |
| 46 | - public \stdClass $iCCP; // 組み込みしたICCプロファイル | |
| 47 | - // プロファイル名(1~79バイト) | |
| 48 | - // NULL分離子(1バイト) | |
| 49 | - // 圧縮関数(1バイト) | |
| 50 | - // 圧縮プロファイル(nバイト) | |
| 51 | - public \stdClass $sBIT; // 上位ビット(4バイト) | |
| 52 | - // 色類0 = グレーサンプルビット(1バイト) | |
| 53 | - // 色類2, 3 = RGBサンプルビット(1x1x1バイト) | |
| 54 | - // 色類4 = グレーサンプルビット(1x1バイト) | |
| 55 | - // 色類6 = RGBサンプルビット(1x1x1x1バイト) | |
| 56 | - public \stdClass $sRGB; // レンダリングインテント(1バイト) | |
| 57 | - // 0 = 知覚 | |
| 58 | - // 1 = 相対カラー・メトリック | |
| 59 | - // 2 = 彩度 | |
| 60 | - // 3 = 絶対カラー・メトリック | |
| 61 | - public \stdClass $cICP; // (1バイト) | |
| 62 | - public \stdClass $mDCV; // (1バイト) | |
| 63 | - public \stdClass $cLLI; // (1バイト) | |
| 64 | - public \stdClass $tEXt; // 文字データ | |
| 65 | - public \stdClass $iTXt; // 圧縮した文字データ | |
| 66 | - public \stdClass $zTXt; // 国際文字データ | |
| 67 | - public \stdClass $bKGD; // 拝啓色 | |
| 68 | - public \stdClass $hIST; // 画像ヒストグラム | |
| 69 | - public \stdClass $pHYs; // 物理的なぷくセルのサイズ | |
| 70 | - // 単位当たりのピクセル数(PPU)のX軸(4バイト) | |
| 71 | - // 単位当たりのピクセル数(PPU)のY軸(4バイト) | |
| 72 | - // ユニット指定子(1バイト) | |
| 73 | - //// 0 = 不明 | |
| 74 | - //// 1 = メートル | |
| 75 | - public \stdClass $sPLT; // 提案されている配色パターン | |
| 76 | - // パレット名(1~79バイト) | |
| 77 | - // NULL分離子(1バイト) | |
| 78 | - // 赤(1か2バイト) | |
| 79 | - // 緑(1か2バイト) | |
| 80 | - // 青(1か2バイト) | |
| 81 | - // アルファ(1か2バイト) | |
| 82 | - // 周波数(2バイト) | |
| 83 | - // 等 | |
| 84 | - public \stdClass $eXIf; // Exifプロファイル | |
| 85 | - public \stdClass $tIME; // 画像の最後変更日 | |
| 86 | - // 年(2バイト) | |
| 87 | - // 月(1バイト:1-12) | |
| 88 | - // 日(1バイト:1-31) | |
| 89 | - // 時(1バイト:0-23) | |
| 90 | - // 分(1バイト:0-59) | |
| 91 | - // 秒(1バイト:0-60) | |
| 92 | - public \stdClass $acTL; // アニメーション・コントロール | |
| 93 | - // フレーム数(4バイト) | |
| 94 | - // 再生数(4バイト、0 = 無限) | |
| 95 | - | |
| 96 | - public array $frames = []; | |
| 97 | - private ?array $currentFrame = null; | |
| 98 | - | |
| 99 | - // TODO: 残り: sBIT, iTXt, hIST, sPLT, tIME | |
| 100 | - public function parse(string $file): \stdClass { | |
| 101 | - $png = new \stdClass(); | |
| 102 | - $fp = fopen($file, 'rb'); | |
| 103 | - | |
| 104 | - $magic = bin2hex(fread($fp, 8)); // マジックのスキップ | |
| 105 | - $length = unpack('N', fread($fp, 4))[1]; | |
| 106 | - $header = fread($fp, 4); | |
| 107 | - if ($header !== 'IHDR') { | |
| 108 | - fclose($fp); | |
| 109 | - return $png; | |
| 110 | - } | |
| 111 | - | |
| 112 | - // IHDRデータ | |
| 113 | - $data = fread($fp, $length); | |
| 114 | - | |
| 115 | - // N = ビッグエンディアン(4ビット) | |
| 116 | - $this->IHDR = new \stdClass; | |
| 117 | - $this->IHDR->name = 'IHDR'; | |
| 118 | - $this->IHDR->length = $length; | |
| 119 | - $this->IHDR->width = unpack('N', substr($data, 0, 4))[1]; | |
| 120 | - $this->IHDR->height = unpack('N', substr($data, 4, 4))[1]; | |
| 121 | - $this->IHDR->bitDepth = ord($data[8]); // 1, 2, 4, 8, 16 | |
| 122 | - $this->IHDR->numColors = 1 << $this->IHDR->bitDepth; | |
| 123 | - $this->IHDR->colorType = ord($data[9]); // 0, 2, 3, 4, 6 | |
| 124 | - $this->IHDR->compressionType = ord($data[10]); | |
| 125 | - $this->IHDR->compression = 'DEFLATE'; | |
| 126 | - $this->IHDR->filter = ord($data[11]); | |
| 127 | - $this->IHDR->interlace = ord($data[12]); // 0 = なし, 1 = Adam7 | |
| 128 | - $this->IHDR->bpp = $this->IHDR->bitDepth * $this->getSamplesPerPixel($this->IHDR->colorType); | |
| 129 | - $this->IHDR->hasAlpha = ($this->IHDR->colorType === 4 || $this->IHDR->colorType === 6); | |
| 130 | - $crc = fread($fp, 4); | |
| 131 | - $this->IHDR->crc = $crc; | |
| 132 | - $this->IHDR->crc32 = unpack('N', $crc)[1]; | |
| 133 | - $this->IHDR->crcHex = bin2hex($crc); | |
| 134 | - | |
| 135 | - $png->header = $this->IHDR; | |
| 136 | - | |
| 137 | -next_chunk: | |
| 138 | - $lengthBytes = fread($fp, 4); | |
| 139 | - if (strlen($lengthBytes) < 4) { | |
| 140 | - logger(LogType::Image, 'バイト長さが悪い。ファイル:'.$file.'、バイト長さ:'.'$lengthBytes'); | |
| 141 | - fclose($fp); | |
| 142 | - return $png; | |
| 143 | - } | |
| 144 | - | |
| 145 | - $length = unpack('N', $lengthBytes)[1]; | |
| 146 | - $nextChunk = fread($fp, 4); | |
| 147 | - $data = $length > 0 ? fread($fp, $length) : ''; | |
| 148 | - $crc = fread($fp, 4); | |
| 149 | - | |
| 150 | - // PLTEデータ | |
| 151 | - if ($nextChunk === "\x50\x4c\x54\x45") { // PLTE | |
| 152 | - $palette = []; | |
| 153 | - for ($i = 0; $i < $length; $i += 3) { | |
| 154 | - $colors = new \Site\Lib\RGB(ord($data[$i]), ord($data[$i+1]), ord($data[$i+2])); | |
| 155 | - $palette[] = $colors->rgb; | |
| 156 | - } | |
| 157 | - | |
| 158 | - $this->PLTE = new \stdClass; | |
| 159 | - $this->PLTE->name = 'PLTE'; | |
| 160 | - $this->PLTE->length = $length; | |
| 161 | - $this->PLTE->palette = $palette; | |
| 162 | - $this->PLTE->crc = $crc; | |
| 163 | - $this->PLTE->crc32 = unpack('N', $crc)[1]; | |
| 164 | - $this->PLTE->crcHex = bin2hex($crc); | |
| 165 | - | |
| 166 | - $png->palette = $this->PLTE; | |
| 167 | - goto next_chunk; | |
| 168 | - } else if ($nextChunk === "\x74\x52\x4e\x53") { // tRNS | |
| 169 | - $this->tRNS = new \stdClass; | |
| 170 | - $this->tRNS->name = 'tRNS'; | |
| 171 | - $this->tRNS->length = $length; | |
| 172 | - $vals = []; | |
| 173 | - $bytes = $this->IHDR->colorType === 2 ? 3 : 1; | |
| 174 | - for ($i = 0; $i < $length; $i += $bytes) { | |
| 175 | - if ($this->IHDR->colorType === 2) { | |
| 176 | - $colors = new \Site\Lib\RGB(ord($data[$i]), ord($data[$i+1]), ord($data[$i+2])); | |
| 177 | - $vals[] = $colors->rgb; | |
| 178 | - } else { | |
| 179 | - $vals[] = ord($data[$i]); | |
| 180 | - } | |
| 181 | - } | |
| 182 | - $this->tRNS->values = $vals; | |
| 183 | - $this->tRNS->crc = $crc; | |
| 184 | - $this->tRNS->crc32 = unpack('N', $crc)[1]; | |
| 185 | - $this->tRNS->crcHex = bin2hex($crc); | |
| 186 | - $png->transparency = $this->tRNS; | |
| 187 | - goto next_chunk; | |
| 188 | - } else if ($nextChunk === "\x63\x48\x52\x4d") { // cHRM | |
| 189 | - $this->cHRM = new \stdClass; | |
| 190 | - $this->cHRM->name = 'cHRM'; | |
| 191 | - $this->cHRM->length = $length; | |
| 192 | - $this->cHRM->whitePointX = unpack('N', substr($data, 0, 4))[1]; | |
| 193 | - $this->cHRM->whitePointY = unpack('N', substr($data, 4, 4))[1]; | |
| 194 | - $this->cHRM->redX = unpack('N', substr($data, 8, 4))[1]; | |
| 195 | - $this->cHRM->redY = unpack('N', substr($data, 12, 4))[1]; | |
| 196 | - $this->cHRM->greenX = unpack('N', substr($data, 16, 4))[1]; | |
| 197 | - $this->cHRM->greenY = unpack('N', substr($data, 20, 4))[1]; | |
| 198 | - $this->cHRM->blueX = unpack('N', substr($data, 24, 4))[1]; | |
| 199 | - $this->cHRM->blueY = unpack('N', substr($data, 28, 4))[1]; | |
| 200 | - $this->cHRM->crc = $crc; | |
| 201 | - $this->cHRM->crc32 = unpack('N', $crc)[1]; | |
| 202 | - $this->cHRM->crcHex = bin2hex($crc); | |
| 203 | - $png->chromaticities = $this->cHRM; | |
| 204 | - goto next_chunk; | |
| 205 | - } else if ($nextChunk === "\x67\x41\x4d\x41") { // gAMA | |
| 206 | - $this->gAMA = new \stdClass; | |
| 207 | - $this->gAMA->name = 'gAMA'; | |
| 208 | - $this->gAMA->length = $length; | |
| 209 | - $this->gAMA->value = unpack('N', $data)[1]; | |
| 210 | - $this->gAMA->crc = $crc; | |
| 211 | - $this->gAMA->crc32 = unpack('N', $crc)[1]; | |
| 212 | - $this->gAMA->crcHex = bin2hex($crc); | |
| 213 | - $png->gamma = $this->gAMA; | |
| 214 | - goto next_chunk; | |
| 215 | - } else if ($nextChunk === "\x69\x43\x43\x50") { // iCCP | |
| 216 | - $this->iCCP = new \stdClass; | |
| 217 | - $this->iCCP->name = 'iCCP'; | |
| 218 | - $this->iCCP->length = $length; | |
| 219 | - | |
| 220 | - $nullPos = strpos($data, "\0"); | |
| 221 | - if ($nullPos === false || $nullPos < 1 || $nullPos > 79) { | |
| 222 | - $this->iCCP->valid = false; | |
| 223 | - $this->iCCP->error = "Missing or invalid profile name terminator"; | |
| 224 | - goto iccp_crc; | |
| 225 | - } | |
| 226 | - | |
| 227 | - $this->iCCP->profileName = substr($data, 0, $nullPos); | |
| 228 | - $compressionMethod = ord($data[$nullPos + 1]); | |
| 229 | - $this->iCCP->compressionMethod = $compressionMethod; | |
| 230 | - | |
| 231 | - if ($compressionMethod !== 0) { | |
| 232 | - $this->iCCP->valid = false; | |
| 233 | - $this->iCCP->error = "Unsupported compression method: {$compressionMethod}"; | |
| 234 | - goto iccp_crc; | |
| 235 | - } | |
| 236 | - | |
| 237 | - $compressedData = substr($data, $nullPos + 2); | |
| 238 | - $this->iCCP->compressedData = $compressedData; | |
| 239 | - $this->iCCP->compressedSize = strlen($compressedData); | |
| 240 | - $this->iCCP->valid = true; | |
| 241 | - | |
| 242 | -iccp_crc: | |
| 243 | - $this->iCCP->crc = $crc; | |
| 244 | - $this->iCCP->crc32 = unpack('N', $crc)[1]; | |
| 245 | - $this->iCCP->crcHex = bin2hex($crc); | |
| 246 | - $png->iccProfile = $this->iCCP; | |
| 247 | - goto next_chunk; | |
| 248 | - } else if ($nextChunk === "\x73\x42\x49\x54") { // sBIT | |
| 249 | - $this->sBIT = new \stdClass; | |
| 250 | - $this->sBIT->name = 'sBIT'; | |
| 251 | - $this->sBIT->length = $length; | |
| 252 | - kys(['sBIT', $length, bin2hex($data)]); | |
| 253 | - $this->sBIT->crc = $crc; | |
| 254 | - $this->sBIT->crc32 = unpack('N', $crc)[1]; | |
| 255 | - $this->sBIT->crcHex = bin2hex($crc); | |
| 256 | - $png->significantBits = $this->sBIT; | |
| 257 | - goto next_chunk; | |
| 258 | - } else if ($nextChunk === "\x73\x52\x47\x42") { // sRGB | |
| 259 | - $this->sRGB = new \stdClass; | |
| 260 | - $this->sRGB->name = 'sRGB'; | |
| 261 | - $this->sRGB->length = $length; | |
| 262 | - $this->sRGB->intent = ord($data); | |
| 263 | - $name = [ | |
| 264 | - 0 => 'Perceptual', | |
| 265 | - 1 => 'Relative colorimetric', | |
| 266 | - 2 => 'Saturation', | |
| 267 | - 3 => 'Absolute colorimetric', | |
| 268 | - ]; | |
| 269 | - $this->sRGB->intentName = $name[$this->sRGB->intent]; | |
| 270 | - $this->sRGB->crc = $crc; | |
| 271 | - $this->sRGB->crc32 = unpack('N', $crc)[1]; | |
| 272 | - $this->sRGB->crcHex = bin2hex($crc); | |
| 273 | - $png->rgbColorSpace = $this->sRGB; | |
| 274 | - goto next_chunk; | |
| 275 | - } else if ($nextChunk === "\x63\x49\x43\x50") { // cICP | |
| 276 | - $this->cICP = new \stdClass; | |
| 277 | - $this->cICP->name = 'cICP'; | |
| 278 | - $this->cICP->length = $length; | |
| 279 | - $this->cICP->colorPrimaries = ord($data[0]); | |
| 280 | - $this->cICP->transferFunction = ord($data[1]); | |
| 281 | - $this->cICP->matrixCoefficients = ord($data[2]); | |
| 282 | - $this->cICP->videoFullRangeFlag = ord($data[3]); | |
| 283 | - $this->cICP->crc = $crc; | |
| 284 | - $this->cICP->crc32 = unpack('N', $crc)[1]; | |
| 285 | - $this->cICP->crcHex = bin2hex($crc); | |
| 286 | - $png->videoSignalTypeId = $this->cICP; | |
| 287 | - goto next_chunk; | |
| 288 | - } else if ($nextChunk === "\x6d\x44\x43\x56") { // mDCV | |
| 289 | - $this->mDCV = new \stdClass; | |
| 290 | - $this->mDCV->name = 'mDCV'; | |
| 291 | - $this->mDCV->length = $length; | |
| 292 | - | |
| 293 | - $u16 = fn($off) => unpack('n', substr($data, $off, 2))[1]; | |
| 294 | - $u32 = fn($off) => unpack('N', substr($data, $off, 4))[1]; | |
| 295 | - $this->mDCV->primaries = [ | |
| 296 | - 'red' => ['x' => $u16(0), 'y' => $u16(2)], | |
| 297 | - 'green' => ['x' => $u16(4), 'y' => $u16(6)], | |
| 298 | - 'blue' => ['x' => $u16(8), 'y' => $u16(10)], | |
| 299 | - ]; | |
| 300 | - $this->mDCV->whitePoint = [ | |
| 301 | - 'x' => $u16(12), | |
| 302 | - 'y' => $u16(14), | |
| 303 | - ]; | |
| 304 | - $this->mDCV->luminance = [ | |
| 305 | - 'max' => $u16(16) * 0.0001, | |
| 306 | - 'min' => $u16(20) * 0.0001, | |
| 307 | - 'maxRaw' => $u16(16), | |
| 308 | - 'minRaw' => $u16(20), | |
| 309 | - ]; | |
| 310 | - $this->mDCV->crc = $crc; | |
| 311 | - $this->mDCV->crc32 = unpack('N', $crc)[1]; | |
| 312 | - $this->mDCV->crcHex = bin2hex($crc); | |
| 313 | - $png->masterDisplayColorVolume = $this->mDCV; | |
| 314 | - goto next_chunk; | |
| 315 | - } else if ($nextChunk === "\x63\x4c\x4c\x49") { // cLLI | |
| 316 | - $this->cLLI = new \stdClass; | |
| 317 | - $this->cLLI->name = 'cLLI'; | |
| 318 | - $this->cLLI->length = $length; | |
| 319 | - $this->cLLI->maxCCL = unpack('N', substr($data, 0, 4))[1] * 0.0001; | |
| 320 | - $this->cLLI->maxCCLRaw = unpack('N', substr($data, 0, 4))[1]; | |
| 321 | - $this->cLLI->maxFALL = unpack('N', substr($data, 4, 4))[1] * 0.0001; | |
| 322 | - $this->cLLI->maxFALLRaw = unpack('N', substr($data, 4, 4))[1]; | |
| 323 | - $this->cLLI->crc = $crc; | |
| 324 | - $this->cLLI->crc32 = unpack('N', $crc)[1]; | |
| 325 | - $this->cLLI->crcHex = bin2hex($crc); | |
| 326 | - $png->contentLightLevelInfo = $this->cLLI; | |
| 327 | - goto next_chunk; | |
| 328 | - } else if ($nextChunk === "\x74\x45\x58\x74") { // tEXt | |
| 329 | - if (!isset($png->textualData)) $png->textualData = []; | |
| 330 | - $this->tEXt = new \stdClass; | |
| 331 | - $this->tEXt->name = 'tEXt'; | |
| 332 | - $this->tEXt->length = $length; | |
| 333 | - $this->tEXt->text = $data; | |
| 334 | - $this->tEXt->crc = $crc; | |
| 335 | - $this->tEXt->crc32 = unpack('N', $crc)[1]; | |
| 336 | - $this->tEXt->crcHex = bin2hex($crc); | |
| 337 | - $png->textualData[] = $this->tEXt; | |
| 338 | - goto next_chunk; | |
| 339 | - } else if ($nextChunk === "\x69\x54\x58\x74") { // iTXt | |
| 340 | - if (!isset($png->internationalTextualData)) $png->textualData = []; | |
| 341 | - $this->iTXt = new \stdClass; | |
| 342 | - $this->iTXt->name = 'iTXt'; | |
| 343 | - $this->iTXt->length = $length; | |
| 344 | - kys(['iTXt', $length, bin2hex($data)]); | |
| 345 | - $this->iTXt->crc = $crc; | |
| 346 | - $this->iTXt->crc32 = unpack('N', $crc)[1]; | |
| 347 | - $this->iTXt->crcHex = bin2hex($crc); | |
| 348 | - $png->internationalTextualData[] = $this->iTXt; | |
| 349 | - kys($png); | |
| 350 | - goto next_chunk; | |
| 351 | - } else if ($nextChunk === "\x7a\x54\x58\x74") { // zTXt | |
| 352 | - if (!isset($png->compressedTextualData)) $png->textualData = []; | |
| 353 | - $this->zTXt = new \stdClass; | |
| 354 | - $this->zTXt->name = 'zTXt'; | |
| 355 | - $this->zTXt->length = $length; | |
| 356 | - | |
| 357 | - $nullPos = strpos($data, "\0"); | |
| 358 | - if ($nullPos === false || $nullPos < 1 || $nullPos > 79) { | |
| 359 | - $this->zTXt->valid = false; | |
| 360 | - $this->zTXt->error = "Missing or invalid profile name terminator"; | |
| 361 | - goto zTXt_crc; | |
| 362 | - } | |
| 363 | - | |
| 364 | - $this->zTXt->keyword = rtrim(substr($data, 0, $nullPos)); | |
| 365 | - $compressionMethod = ord($data[$nullPos + 1] ?? "\xFF"); | |
| 366 | - | |
| 367 | - $this->zTXt->compressionMethod = $compressionMethod; | |
| 368 | - | |
| 369 | - if ($compressionMethod !== 0) { | |
| 370 | - $this->zTXt->valid = false; | |
| 371 | - $this->zTXt->error = "Unsupported compression method"; | |
| 372 | - goto zTXt_crc; | |
| 373 | - } | |
| 374 | - | |
| 375 | - $compressedText = substr($data, $nullPos + 2); | |
| 376 | - fread($fp, 13); // ???????????????????????? | |
| 377 | - $this->zTXt->compressedText = $compressedText; | |
| 378 | - $this->zTXt->compressedLength = strlen($compressedText); | |
| 379 | - | |
| 380 | - $text = zlib_decode($compressedText); | |
| 381 | - | |
| 382 | - if ($text === false) { | |
| 383 | - $this->zTXt->valid = false; | |
| 384 | - $this->zTXt->decompressionError = true; | |
| 385 | - $this->zTXt->text = null; | |
| 386 | - goto zTXt_crc; | |
| 387 | - } else { | |
| 388 | - $this->zTXt->text = $text; | |
| 389 | - $this->zTXt->textUtf8 = mb_convert_encoding($text, 'UTF-8', 'ISO-8859-1'); | |
| 390 | - } | |
| 391 | - | |
| 392 | - $this->zTXt->valid = true; | |
| 393 | - | |
| 394 | -zTXt_crc: | |
| 395 | - $this->zTXt->crc = $crc; | |
| 396 | - $this->zTXt->crc32 = unpack('N', $crc)[1]; | |
| 397 | - $this->zTXt->crcHex = bin2hex($crc); | |
| 398 | - $png->compressedTextualData[] = $this->zTXt; | |
| 399 | - goto next_chunk; | |
| 400 | - } else if ($nextChunk === "\x62\x4b\x47\x44") { // bKGD | |
| 401 | - $this->bKGD = new \stdClass; | |
| 402 | - $this->bKGD->name = 'bKGD'; | |
| 403 | - $this->bKGD->length = $length; | |
| 404 | - if ($this->IHDR->colorType === 2 || $this->IHDR->colorType === 6) { | |
| 405 | - $r = (ord($data[0]) << 8) | ord($data[1]); | |
| 406 | - $g = (ord($data[2]) << 8) | ord($data[3]); | |
| 407 | - $b = (ord($data[4]) << 8) | ord($data[5]); | |
| 408 | - | |
| 409 | - $this->bKGD->type = 'rgb16'; | |
| 410 | - $this->bKGD->r = $r; | |
| 411 | - $this->bKGD->g = $g; | |
| 412 | - $this->bKGD->b = $b; | |
| 413 | - | |
| 414 | - $this->bKGD->r8 = $r >> 8; | |
| 415 | - $this->bKGD->g8 = $g >> 8; | |
| 416 | - $this->bKGD->b8 = $b >> 8; | |
| 417 | - $this->bKGD->webColor = sprintf("#%02x%02x%02x", $this->bKGD->r8, $this->bKGD->g8, $this->bKGD->b8); | |
| 418 | - } else if ($this->IHDR->colorType === 2 || $this->IHDR->colorType === 6) { | |
| 419 | - $gray = (ord($data[0]) << 8) | ord($data[1]); | |
| 420 | - $this->bKGD->type = 'gray16'; | |
| 421 | - $this->bKGD->gray = $gray; | |
| 422 | - $this->bKGD->gray8 = $gray >> 8; | |
| 423 | - } else if ($this->IHDR->colorType === 3) { | |
| 424 | - $this->bKGD->type = 'palette_index'; | |
| 425 | - $this->bKGD->value = ord($data); | |
| 426 | - } else { | |
| 427 | - $this->bKGD->type = 'unknown'; | |
| 428 | - $this->bKGD->error = 'Invalid color type for bKGD'; | |
| 429 | - } | |
| 430 | - | |
| 431 | - $this->bKGD->crc = $crc; | |
| 432 | - $this->bKGD->crc32 = unpack('N', $crc)[1]; | |
| 433 | - $this->bKGD->crcHex = bin2hex($crc); | |
| 434 | - $png->backgroundColor = $this->bKGD; | |
| 435 | - goto next_chunk; | |
| 436 | - } else if ($nextChunk === "\x68\x49\x53\x54") { // hIST | |
| 437 | - $this->hIST = new \stdClass; | |
| 438 | - $this->hIST->name = 'hIST'; | |
| 439 | - $this->hIST->length = $length; | |
| 440 | - kys(['hIST', $length, bin2hex($data)]); | |
| 441 | - $this->hIST->crc = $crc; | |
| 442 | - $this->hIST->crc32 = unpack('N', $crc)[1]; | |
| 443 | - $this->hIST->crcHex = bin2hex($crc); | |
| 444 | - $png->imageHistogram = $this->hIST; | |
| 445 | - kys($png); | |
| 446 | - goto next_chunk; | |
| 447 | - } else if ($nextChunk === "\x70\x48\x59\x73") { // pHYs | |
| 448 | - $this->pHYs = new \stdClass; | |
| 449 | - $this->pHYs->name = 'pHYs'; | |
| 450 | - $this->pHYs->length = $length; | |
| 451 | - $this->pHYs->x = unpack('N', substr($data, 0, 4))[1]; | |
| 452 | - $this->pHYs->y = unpack('N', substr($data, 4, 4))[1]; | |
| 453 | - $unit = [ | |
| 454 | - 0 => 'unit is unknown', | |
| 455 | - 1 => 'unit is the metre', | |
| 456 | - ]; | |
| 457 | - $this->pHYs->unitId = ord($data[8]); | |
| 458 | - $this->pHYs->unit = $unit[$this->pHYs->unitId]; | |
| 459 | - $this->pHYs->crc = $crc; | |
| 460 | - $this->pHYs->crc32 = unpack('N', $crc)[1]; | |
| 461 | - $this->pHYs->crcHex = bin2hex($crc); | |
| 462 | - $png->physicalPixelDimensions = $this->pHYs; | |
| 463 | - goto next_chunk; | |
| 464 | - } else if ($nextChunk === "\x73\x50\x4c\x54") { // sPLT | |
| 465 | - $this->sPLT = new \stdClass; | |
| 466 | - $this->sPLT->name = 'sPLT'; | |
| 467 | - $this->sPLT->length = $length; | |
| 468 | - kys(['sPLT', $length, bin2hex($data)]); | |
| 469 | - $this->sPLT->crc = $crc; | |
| 470 | - $this->sPLT->crc32 = unpack('N', $crc)[1]; | |
| 471 | - $this->sPLT->crcHex = bin2hex($crc); | |
| 472 | - $png->suggestedPalette = $this->sPLT; | |
| 473 | - kys($png); | |
| 474 | - goto next_chunk; | |
| 475 | - } else if ($nextChunk === "\x65\x58\x49\x66") { // eXIf | |
| 476 | - $this->eXIf = new \stdClass; | |
| 477 | - $this->eXIf->name = 'eXIf'; | |
| 478 | - $this->eXIf->length = $length; | |
| 479 | - | |
| 480 | - if (strlen($data) < 8) { | |
| 481 | - $this->eXIf->valid = false; | |
| 482 | - $this->eXIf->error = "eXIf chunk too short"; | |
| 483 | - goto exif_crc; | |
| 484 | - } | |
| 485 | - | |
| 486 | - $byteOrder = substr($data, 0, 2); | |
| 487 | - $magic = substr($data, 2, 2); | |
| 488 | - | |
| 489 | - $this->eXIf->rawData = $data; | |
| 490 | - $this->eXIf->rawLength = $length; | |
| 491 | - $this->eXIf->byteOrder = $byteOrder; // II = リトルエンディアン、MM = ビッグエンディアン | |
| 492 | - $this->eXIf->isBigEndian = ($byteOrder === 'MM'); | |
| 493 | - $this->eXIf->magic = unpack('n', $magic)[1]; // いつでも42ではず | |
| 494 | - | |
| 495 | - if (strlen($data) < 8) { | |
| 496 | - $this->eXIf->valid = false; | |
| 497 | - $this->eXIf->error = "Invalid TIFF magic number"; | |
| 498 | - goto exif_crc; | |
| 499 | - } | |
| 500 | - | |
| 501 | - $this->eXIf->valid = true; | |
| 502 | - | |
| 503 | - $ifdOffset = unpack($this->eXIf->isBigEndian ? 'N' : 'V', substr($data, 4, 4))[1]; | |
| 504 | - $this->eXIf->firstIfdOffset = $ifdOffset; | |
| 505 | - | |
| 506 | -exif_crc: | |
| 507 | - $this->eXIf->crc = $crc; | |
| 508 | - $this->eXIf->crc32 = unpack('N', $crc)[1]; | |
| 509 | - $this->eXIf->crcHex = bin2hex($crc); | |
| 510 | - $png->exifProfile = $this->eXIf; | |
| 511 | - goto next_chunk; | |
| 512 | - } else if ($nextChunk === "\x74\x49\x4d\x45") { // tIME | |
| 513 | - $this->tIME = new \stdClass; | |
| 514 | - $this->tIME->name = 'tIME'; | |
| 515 | - $this->tIME->length = $length; | |
| 516 | - kys(['tIME', $length, bin2hex($data)]); | |
| 517 | - $this->tIME->crc = $crc; | |
| 518 | - $this->tIME->crc32 = unpack('N', $crc)[1]; | |
| 519 | - $this->tIME->crcHex = bin2hex($crc); | |
| 520 | - $png->lastModificationTime = $this->tIME; | |
| 521 | - kys($png); | |
| 522 | - goto next_chunk; | |
| 523 | - } else if ($nextChunk === "\x61\x63\x54\x4c") { // acTL | |
| 524 | - $this->acTL = new \stdClass; | |
| 525 | - $this->acTL->name = 'acTL'; | |
| 526 | - $this->acTL->length = $length; | |
| 527 | - $this->acTL->numFrames = unpack('N', substr($data, 0, 4))[1]; | |
| 528 | - $this->acTL->numPlays = unpack('N', substr($data, 4, 4))[1]; | |
| 529 | - $this->acTL->crc = $crc; | |
| 530 | - $this->acTL->crc32 = unpack('N', $crc)[1]; | |
| 531 | - $this->acTL->crcHex = bin2hex($crc); | |
| 532 | - $png->aniationControl = $this->acTL; | |
| 533 | - goto next_chunk; | |
| 534 | - } else if ($nextChunk === "\x66\x63\x54\x4c") { // fcTL | |
| 535 | - if ($this->currentFrame !== null) $this->frames[] = (object)$this->currentFrame; | |
| 536 | - | |
| 537 | - $this->currentFrame = [ | |
| 538 | - 'name' => 'fcTL', | |
| 539 | - 'length' => $length, | |
| 540 | - 'sequenceNumber' => unpack('N', substr($data, 0, 4))[1], | |
| 541 | - 'width' => unpack('N', substr($data, 4, 4))[1], | |
| 542 | - 'height' => unpack('N', substr($data, 8, 4))[1], | |
| 543 | - 'xOffset' => unpack('N', substr($data, 12, 4))[1], | |
| 544 | - 'yOffset' => unpack('N', substr($data, 16, 4))[1], | |
| 545 | - 'delayNum' => unpack('n', substr($data, 20, 2))[1], | |
| 546 | - 'delayDen' => unpack('n', substr($data, 22, 2))[1], | |
| 547 | - 'disposeOp' => ord($data[24]), | |
| 548 | - 'blendOp' => ord($data[25]), | |
| 549 | - 'data' => null, | |
| 550 | - 'crc' => $crc, | |
| 551 | - 'crc32' => unpack('N', $crc)[1], | |
| 552 | - 'crcHex' => bin2hex($crc), | |
| 553 | - ]; | |
| 554 | - | |
| 555 | - goto next_chunk; | |
| 556 | - } else if ($nextChunk === "\x49\x44\x41\x54" || $nextChunk === "\x66\x64\x41\x54") { // IDAT || fdAT | |
| 557 | - if ($this->currentFrame === null) { | |
| 558 | - $this->currentFrame = [ | |
| 559 | - 'sequenceNumber' => 0, | |
| 560 | - 'isFirstFrame' => true, | |
| 561 | - 'width' => $this->IHDR->width, | |
| 562 | - 'height' => $this->IHDR->height, | |
| 563 | - 'data' => null, | |
| 564 | - ]; | |
| 565 | - } | |
| 566 | - | |
| 567 | - $compressedData = ($nextChunk === "\x66\x64\x41\x54") ? substr($data, 4) : $data; | |
| 568 | - $this->currentFrame['name'] = 'fdAT'; | |
| 569 | - $this->currentFrame['length'] = $length; | |
| 570 | - $this->currentFrame['data'] = $compressedData; | |
| 571 | - $this->currentFrame['dataBase64'] = base64_encode($compressedData); | |
| 572 | - $this->currentFrame['dataHex'] = bin2hex($compressedData); | |
| 573 | - $this->currentFrame['dataLength'] = strlen($compressedData); | |
| 574 | - $this->currentFrame['dataChunkType'] = $nextChunk; | |
| 575 | - $this->currentFrame['crc'] = $crc; | |
| 576 | - $this->currentFrame['crcBase64'] = unpack('N', $crc)[1]; | |
| 577 | - $this->currentFrame['crcHex'] = bin2hex($crc); | |
| 578 | - | |
| 579 | - if ($nextChunk === "\x49\x44\x41\x54") { | |
| 580 | - $this->currentFrame['name'] = 'IDAT'; | |
| 581 | - $this->frames[] = (object)$this->currentFrame; | |
| 582 | - $this->currentFrame = null; | |
| 583 | - } | |
| 584 | - | |
| 585 | - $png->data = $this->frames; | |
| 586 | - goto next_chunk; | |
| 587 | - } else if ($nextChunk === "\x49\x45\x4e\x44") { // IEND | |
| 588 | - if ($this->currentFrame !== null) { | |
| 589 | - $this->frames[] = (object)$this->currentFrame; | |
| 590 | - } | |
| 591 | - $png->end = (object)['crc' => $crc, 'crc32' => unpack('N', $crc)[1], 'crcHex' => bin2hex($crc)]; | |
| 592 | - } else { | |
| 593 | - fclose($fp); | |
| 594 | - $err = '不明なチャンク:'.$nextChunk.'、HEX:'.bin2hex($nextChunk); | |
| 595 | - kys($err); | |
| 596 | - logger(\LogType::Image, '【PNG】'.$err); | |
| 597 | - return $png; | |
| 598 | - } | |
| 599 | - | |
| 600 | - fclose($fp); | |
| 601 | - return $png; | |
| 602 | - } | |
| 603 | - | |
| 604 | - /////////////// | |
| 605 | - | |
| 606 | - private function getSamplesPerPixel(int $colorType): int { | |
| 607 | - return match($colorType) { | |
| 608 | - 0 => 1, // グレースケール | |
| 609 | - 2 => 3, // RGB | |
| 610 | - 3 => 1, // 指標した色(ペレット) | |
| 611 | - 4 => 2, // グレースケール+アルファ | |
| 612 | - 6 => 4, // RGBA | |
| 613 | - default => 1, | |
| 614 | - }; | |
| 615 | - } | |
| 616 | -} |
削除
src/Site/Lib/Image/Targa.php
| @@ -1,115 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Image; | |
| 3 | - | |
| 4 | -use Site\Lib\Image\ImageInterface; | |
| 5 | - | |
| 6 | -class Targa implements ImageInterface { | |
| 7 | - public int $id = 0; | |
| 8 | - public int $colorMapType = 0; | |
| 9 | - public int $imageTypeByte = 0; | |
| 10 | - public string $imageType = ''; | |
| 11 | - public int $colorMapEntryIndex = 0; | |
| 12 | - public int $colorMapLength = 0; | |
| 13 | - public int $colorMapBits = 0; | |
| 14 | - public int $x = 0; | |
| 15 | - public int $y = 0; | |
| 16 | - public int $width = 0; | |
| 17 | - public int $height = 0; | |
| 18 | - public int $bitDepth = 0; | |
| 19 | - public int $bpp = 0; | |
| 20 | - public int $interlace = 0; | |
| 21 | - public int $isRightToLeft = 0; // 0 = 左→右, 1 = 右→左 | |
| 22 | - public int $isTopToBottom = 0; // 0 = 下→上, 1 = 上→下 | |
| 23 | - public int $alphaBits = 0; | |
| 24 | - public bool $hasAlpha = false; | |
| 25 | - //-------- | |
| 26 | - public string $imageId = ''; | |
| 27 | - public string $colorMapData = ''; | |
| 28 | - public string $imageData = ''; | |
| 29 | - | |
| 30 | - private array $imageTypes = [ | |
| 31 | - 0 => 'NO DATA', | |
| 32 | - 1 => 'UNCOMPRESSED COLOR-MAPPED IMAGE', | |
| 33 | - 2 => 'UNCOMPRESSED TRUE COLOR IMAGE', | |
| 34 | - 3 => 'UNCOMPRESSED GRAYSCALE IMAGE', | |
| 35 | - 9 => 'RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 36 | - 10 => 'RUN-LENGTH ENCODED TRUE-COLOR IMAGE', | |
| 37 | - 11 => 'RUN-LENGTH ENCODED GRAYSCALE IMAGE', | |
| 38 | - 32 => 'HUFFMAN-DELTA-RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 39 | - 33 => 'HUFFMAN-DELTA-RUN-LENGTH-4-PASS-QUADTREE-TYPE PROCESS ENCODED COLOR-MAPPED IMAGE', | |
| 40 | - ]; | |
| 41 | - | |
| 42 | - public function parse(string $file): \stdClass { | |
| 43 | - $tga = new \stdClass(); | |
| 44 | - $fp = fopen($file, 'rb'); | |
| 45 | - | |
| 46 | - $this->id = ord(fread($fp, 1)); | |
| 47 | - $this->colorMapType = ord(fread($fp, 1)); // 0 = なし, 1 = あり, 2~127 = Truevision向け, 128~255 = 開発者向け | |
| 48 | - $this->imageTypeByte = ord(fread($fp, 1)); | |
| 49 | - $this->imageType = $this->imageTypes[$this->imageTypeByte]; | |
| 50 | - $tga->id = $this->id; | |
| 51 | - $tga->colorMapType = $this->colorMapType; | |
| 52 | - $tga->imageTypeByte = $this->imageTypeByte; | |
| 53 | - $tga->imageType = $this->imageType; | |
| 54 | - | |
| 55 | - $this->colorMapEntryIndex = unpack('v', fread($fp, 2))[1]; | |
| 56 | - $this->colorMapLength = unpack('v', fread($fp, 2))[1]; | |
| 57 | - $this->colorMapBits = ord(fread($fp, 1)); | |
| 58 | - $tga->colorMapEntryIndex = $this->colorMapEntryIndex; | |
| 59 | - $tga->colorMapLength = $this->colorMapLength; | |
| 60 | - $tga->colorMapBits = $this->colorMapBits; | |
| 61 | - | |
| 62 | - $this->x = unpack('v', fread($fp, 2))[1]; | |
| 63 | - $this->y = unpack('v', fread($fp, 2))[1]; | |
| 64 | - $this->width = unpack('v', fread($fp, 2))[1]; | |
| 65 | - $this->height = unpack('v', fread($fp, 2))[1]; | |
| 66 | - $this->bitDepth = ord(fread($fp, 1)); | |
| 67 | - $tga->x = $this->x; | |
| 68 | - $tga->y = $this->y; | |
| 69 | - $tga->width = $this->width; | |
| 70 | - $tga->height = $this->height; | |
| 71 | - $tga->bitDepth = $this->bitDepth; | |
| 72 | - | |
| 73 | - $desc = ord(fread($fp, 1)); | |
| 74 | - $this->interlace = ($desc & 0xC0) >> 6; | |
| 75 | - $this->isTopToBottom = ($desc & 0x20) !== 0; | |
| 76 | - $this->isRightToLeft = ($desc & 0x10) !== 0; | |
| 77 | - $this->alphaBits = $desc & 0x0F; | |
| 78 | - $this->hasAlpha = ($this->alphaBits == 8) && in_array($this->imageTypeByte == [2, 3, 10, 11]); | |
| 79 | - $tga->interlace = $this->interlace; | |
| 80 | - $tga->isRightToLeft = $this->isRightToLeft; | |
| 81 | - $tga->isTopToBottom = $this->isTopToBottom; | |
| 82 | - $tga->alphaBits = $this->alphaBits; | |
| 83 | - $tga->hasAlpha = $this->hasAlpha; | |
| 84 | - | |
| 85 | - $needsXFlip = $tga->isRightToLeft; | |
| 86 | - $needsYFlip = !$tga->isTopToBottom; | |
| 87 | - // 0010 0000 | |
| 88 | - | |
| 89 | - //--------- | |
| 90 | - $imageIdBytes = $this->id; | |
| 91 | - $colorMapBytes = 0; | |
| 92 | - if ($this->colorMapType === 1 && $this->colorMapLength > 0) { | |
| 93 | - $bytesPerEntry = (int)($this->colorMapBits / 8); | |
| 94 | - $colorMapBytes = $this->colorMapLength * $bytesPerEntry; | |
| 95 | - } | |
| 96 | - | |
| 97 | - $this->imageId = $imageIdBytes > 0 ? fread($fp, $imageIdBytes) : ''; | |
| 98 | - $this->colorMapData = $colorMapBytes > 0 ? fread($fp, $colorMapBytes) : ''; | |
| 99 | - $tga->imageId = $this->imageId; | |
| 100 | - $tga->colorMapData = $this->colorMapData; | |
| 101 | - | |
| 102 | - if (in_array($this->imageTypeByte, [9, 10, 11])) { // RLE | |
| 103 | - // TODO | |
| 104 | - } else if (in_array($this->imageTypeByte, [1, 2, 3])) { // 非圧縮化 | |
| 105 | - $this->bpp = (int)($this->bitDepth / 8); | |
| 106 | - $bytes = $this->width * $this->height * $this->bpp; | |
| 107 | - $this->imageData = fread($fp, $bytes); | |
| 108 | - $tga->bpp = $this->bpp; | |
| 109 | - $tga->imageData = $this->imageData; | |
| 110 | - } | |
| 111 | - | |
| 112 | - fclose($fp); | |
| 113 | - return $tga; | |
| 114 | - } | |
| 115 | -} |
削除
src/Site/Lib/Mailer.php
| @@ -1,238 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -class Mailer { | |
| 5 | - private $socket; | |
| 6 | - private string $host; | |
| 7 | - private int $port; | |
| 8 | - private ?string $user; | |
| 9 | - private ?string $pass; | |
| 10 | - | |
| 11 | - private ?string $pgpKey; | |
| 12 | - private ?string $pgpPass; | |
| 13 | - | |
| 14 | - /** | |
| 15 | - * コンストラクタ | |
| 16 | - */ | |
| 17 | - public function __construct() { | |
| 18 | - if (!MAILER_ENABLED) { | |
| 19 | - throw new \Exception("メーラーは無効です。"); | |
| 20 | - } | |
| 21 | - | |
| 22 | - $this->host = MAILINFO['host']; | |
| 23 | - $this->port = MAILINFO['port']; | |
| 24 | - $this->user = MAILINFO['user']; | |
| 25 | - $this->pass = MAILINFO['pass']; | |
| 26 | - } | |
| 27 | - | |
| 28 | - /** | |
| 29 | - * ソケット接続を開く。 | |
| 30 | - * | |
| 31 | - * @return void | |
| 32 | - */ | |
| 33 | - public function connect(): void { | |
| 34 | - $this->socket = fsockopen($this->host, $this->port, $errno, $err, 30); | |
| 35 | - if (!$this->socket) { | |
| 36 | - $msg = "接続に失敗: {$err} ({$errno})"; | |
| 37 | - logger(\LogType::Mailer, $msg); | |
| 38 | - throw new \Exception($msg); | |
| 39 | - } | |
| 40 | - | |
| 41 | - $this->readResponse(); | |
| 42 | - } | |
| 43 | - | |
| 44 | - /** | |
| 45 | - * ソケット接続を切断する。 | |
| 46 | - * | |
| 47 | - * @return void | |
| 48 | - */ | |
| 49 | - public function disconnect(): void { | |
| 50 | - $this->sendCommand('QUIT', 221); | |
| 51 | - fclose($this->socket); | |
| 52 | - } | |
| 53 | - | |
| 54 | - /** | |
| 55 | - * サーバーで認証する。 | |
| 56 | - * ユーザー名とパスワードが指定されている場合はAUTH LOGINを使用する。 | |
| 57 | - * | |
| 58 | - * @return void | |
| 59 | - */ | |
| 60 | - public function authenticate(): void { | |
| 61 | - $ehloRes = $this->sendCommand('EHLO '.$this->host, 250); | |
| 62 | - | |
| 63 | - // STARTTLSは対応するかどうか確認 | |
| 64 | - if (strpos($ehloRes, 'STARTTLS') !== false) { | |
| 65 | - $this->sendCommand('STARTTLS', 220); | |
| 66 | - | |
| 67 | - // TLS暗号化 | |
| 68 | - if (!stream_socket_enable_crypto( | |
| 69 | - $this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { | |
| 70 | - $msg = "TLSハンドシェイクに失敗"; | |
| 71 | - logger(\LogType::Mailer, $msg); | |
| 72 | - throw new \Exception($msg); | |
| 73 | - } | |
| 74 | - | |
| 75 | - $this->sendCommand('EHLO '.gethostname(), 250); | |
| 76 | - } | |
| 77 | - | |
| 78 | - if ($this->user && $this->pass) { | |
| 79 | - $this->sendCommand('AUTH LOGIN', 334); | |
| 80 | - $this->sendCommand(base64_encode($this->user), 334); | |
| 81 | - $this->sendCommand(base64_encode($this->pass), 235); | |
| 82 | - } | |
| 83 | - } | |
| 84 | - | |
| 85 | - /** | |
| 86 | - * TLSハンドシェイクに失敗する。 | |
| 87 | - * | |
| 88 | - * @param string $to 受信者のメールアドレス。 | |
| 89 | - * @param string $subject メールの件名。 | |
| 90 | - * @param string $body メールの本文。 | |
| 91 | - * @param string|null $toName 受信者の名前。 | |
| 92 | - * @param string|null $replyTo 返信先メールアドレス。 | |
| 93 | - * @param string|null $replyToName 返信先の名前。 | |
| 94 | - * @param bool $pgpSign PGPで署名するかどうか(現在は正常に動作していません)。 | |
| 95 | - * | |
| 96 | - * @return void | |
| 97 | - */ | |
| 98 | - public function send( | |
| 99 | - string $to, | |
| 100 | - string $subject, | |
| 101 | - string $body, | |
| 102 | - ?string $toName = null, | |
| 103 | - ?string $replyTo = null, | |
| 104 | - ?string $replyToName = null, | |
| 105 | - bool $pgpSign = false | |
| 106 | - ): void { | |
| 107 | - $from = MAILINFO['from']; | |
| 108 | - | |
| 109 | - $this->sendCommand("MAIL FROM: <{$from}>", 250); | |
| 110 | - $this->sendCommand("RCPT TO: <{$to}>", 250); | |
| 111 | - $this->sendCommand('DATA', 354); | |
| 112 | - | |
| 113 | - $headers = "Date: ".date('r')."\r\n"; // RFC 2822 | |
| 114 | - | |
| 115 | - $encSubject = mb_encode_mimeheader($subject, 'UTF-8', 'Q'); | |
| 116 | - $headers .= "Subject: {$encSubject}\r\n"; | |
| 117 | - | |
| 118 | - $fromHeader = mb_encode_mimeheader(SITEINFO['title'], 'UTF-8', 'Q')." <{$from}>"; | |
| 119 | - $headers .= "From: {$fromHeader}\r\n"; | |
| 120 | - | |
| 121 | - $toHeader = $toName | |
| 122 | - ? mb_encode_mimeheader($toName, 'UTF-8', 'Q')." <{$to}>" : $to; | |
| 123 | - $headers .= "To: {$toHeader}\r\n"; | |
| 124 | - | |
| 125 | - if ($replyTo) { | |
| 126 | - $replyToHeader = $replyToName | |
| 127 | - ? mb_encode_mimeheader($replyToName, 'UTF-8', 'Q')." <{$replyTo}>" : $replyTo; | |
| 128 | - $headers .= "Reply-To: {$replyToHeader}\r\n"; | |
| 129 | - } | |
| 130 | - | |
| 131 | - $headers .= "MIME-Version: 1.0\r\n"; | |
| 132 | - | |
| 133 | - if ($pgpSign) { | |
| 134 | - $boundary = uniqid('BOUNDARY_'); | |
| 135 | - $headers .= "Content-Type: multipart/signed;\r\n"; | |
| 136 | - $headers .= " protocol=\"application/pgp-signature\";\r\n"; | |
| 137 | - $headers .= " micalg=php-sha512;\r\n"; | |
| 138 | - $headers .= " boundary=\"{$boundary}\"\r\n"; | |
| 139 | - } else { | |
| 140 | - $headers .= "Content-Type: text/plain; charset=utf-8\r\n"; | |
| 141 | - $headers .= "Content-Transfer-Encoding: quoted-printable\r\n"; | |
| 142 | - } | |
| 143 | - | |
| 144 | - $headers .= "X-Mailer: 076 Little Beast\r\n"; | |
| 145 | - | |
| 146 | - $encBody = quoted_printable_encode($body); | |
| 147 | - $data = $headers."\r\n".$encBody; | |
| 148 | - | |
| 149 | - if ($pgpSign) { | |
| 150 | - $signature = $this->signMessage($data); | |
| 151 | - $data = "--{$boundary}\r\n" | |
| 152 | - .$data."\r\n" | |
| 153 | - ."Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n" | |
| 154 | - ."Content-Disposition: attachment; filename=\"signature.asc\"\r\n\r\n" | |
| 155 | - .$signature."\r\n" | |
| 156 | - ."--{$boundary}--\r\n"; | |
| 157 | - } | |
| 158 | - | |
| 159 | - $data .= "\r\n.\r\n"; | |
| 160 | - fwrite($this->socket, $data); | |
| 161 | - | |
| 162 | - $response = $this->readResponse(); | |
| 163 | - if (substr($response, 0, 3) != '250') { | |
| 164 | - $msg = "メール送信に失敗: {$response}"; | |
| 165 | - logger(\LogType::Mailer, $msg); | |
| 166 | - throw new \Exception($msg); | |
| 167 | - } | |
| 168 | - } | |
| 169 | - | |
| 170 | - /** | |
| 171 | - * お好みのタイムゾーンを設定する。 | |
| 172 | - * 設定しない場合、php.iniで設定されたタイムゾーンがデフォルトになる。 | |
| 173 | - * それも設定されていない場合、GMTタイムゾーンがデフォルトになる。 | |
| 174 | - * | |
| 175 | - * @param string $zone IANAタイムゾーンデータベース標準のタイムゾーン(例:Asia/Tokyo) | |
| 176 | - * @return void | |
| 177 | - */ | |
| 178 | - public function setTimezone(string $zone): void { | |
| 179 | - date_default_timezone_set($zone); | |
| 180 | - } | |
| 181 | - | |
| 182 | - /** | |
| 183 | - * PGPキーとオプションでパスフレーズを設定する。 | |
| 184 | - * | |
| 185 | - * @param string $keypath PGP署名に使用する秘密鍵へのパス。 | |
| 186 | - * @param string|null $passphrase 設定されている場合、署名用のパスフレーズ。 | |
| 187 | - * | |
| 188 | - * @return void | |
| 189 | - */ | |
| 190 | - public function enablePGP(string $keypath, ?string $passphrase = null): void { | |
| 191 | - $this->pgpKey = file_get_contents($keypath); | |
| 192 | - $this->pgpPass = $passphrase; | |
| 193 | - } | |
| 194 | - | |
| 195 | - // 機能性メソッド | |
| 196 | - | |
| 197 | - private function sendCommand(string $command, int $retcode): string { | |
| 198 | - fwrite($this->socket, $command."\r\n"); | |
| 199 | - $res = $this->readResponse(); | |
| 200 | - if (substr($res, 0, 3) != $retcode) { | |
| 201 | - $msg = "「{$command}」に対する予期しないレスポンス: {$res}"; | |
| 202 | - logger(\LogType::Mailer, $msg); | |
| 203 | - throw new \Exception($msg); | |
| 204 | - } | |
| 205 | - | |
| 206 | - return $res; | |
| 207 | - } | |
| 208 | - | |
| 209 | - private function readResponse(): string { | |
| 210 | - $res = ''; | |
| 211 | - | |
| 212 | - while ($line = fgets($this->socket, 515)) { | |
| 213 | - $res .= $line; | |
| 214 | - if (substr($line, 3, 1) == ' ') break; // レスポンスの終了だ | |
| 215 | - } | |
| 216 | - | |
| 217 | - return $res; | |
| 218 | - } | |
| 219 | - | |
| 220 | - private function signMessage(string $message): string { | |
| 221 | - if (extension_loaded('gnupg')) {// gnupg延長は有効の場合 | |
| 222 | - $gpg = new \gnupg(); | |
| 223 | - $gpg->addsignkey($this->pgpKey, $this->pgpPass); | |
| 224 | - $gpg->setsignmode(\gnupg::SIG_MODE_DETACH); | |
| 225 | - return $gpg->sign($message); | |
| 226 | - } else { // なければ、CLIツールを使う(gnupgをインストールは必須) | |
| 227 | - $tmp = tempnam(sys_get_temp_dir(), 'pgpmsg'); | |
| 228 | - file_put_contents($tmp, $message); | |
| 229 | - $sig = shell_exec( | |
| 230 | - "gpg --batch " | |
| 231 | - .($this->pgpPass ? "--passphrase {$this->pgpPass} " : '') | |
| 232 | - ."--detach-sign --armor {$tmp} 2>&1" | |
| 233 | - ); | |
| 234 | - unlink($tmp); | |
| 235 | - return $sig; | |
| 236 | - } | |
| 237 | - } | |
| 238 | -} |
削除
src/Site/Lib/Markdown.php
| @@ -1,646 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -class Markdown { | |
| 5 | - private string $content; | |
| 6 | - private array $html; | |
| 7 | - private string $path; | |
| 8 | - private bool $inCodeBlock = false; | |
| 9 | - private string $codeBlockLanguage = ''; | |
| 10 | - private array $codeBlockContent = []; | |
| 11 | - private const METADATA_LINE = "----"; | |
| 12 | - private array $algebraicPlaceholder = []; | |
| 13 | - | |
| 14 | - public function __construct(string $path, string $section, ?string $lang = null) { | |
| 15 | - $this->html = []; | |
| 16 | - if ($lang) $this->path = ROOT.$section.$lang.'/'.$path.'.md'; | |
| 17 | - else $this->path = ROOT.$section.$path.'.md'; | |
| 18 | - | |
| 19 | - if (!file_exists($this->path)) { | |
| 20 | - header('Location: /404'); | |
| 21 | - exit(); | |
| 22 | - } | |
| 23 | - } | |
| 24 | - | |
| 25 | - /** | |
| 26 | - * メタデータを取得する | |
| 27 | - * | |
| 28 | - * @return \stdClass メタデータオブジェクト | |
| 29 | - */ | |
| 30 | - public function getMetadata(): \stdClass { | |
| 31 | - $content = file_get_contents($this->path); | |
| 32 | - $metadata = new \stdClass(); | |
| 33 | - | |
| 34 | - $parts = explode(self::METADATA_LINE, $content, 2); | |
| 35 | - if (count($parts) < 2) return $metadata; | |
| 36 | - | |
| 37 | - $lines = explode("\n", trim($parts[0])); | |
| 38 | - foreach ($lines as $line) { | |
| 39 | - $line = trim($line); | |
| 40 | - if (empty($line)) continue; | |
| 41 | - | |
| 42 | - $colonPos = strpos($line, ':'); | |
| 43 | - if ($colonPos === false) continue; | |
| 44 | - | |
| 45 | - $key = trim(substr($line, 0, $colonPos)); | |
| 46 | - $value = trim(substr($line, $colonPos + 1)); | |
| 47 | - $value = trim($value, '"\''); | |
| 48 | - | |
| 49 | - if ($key == 'category' || $key == 'css') { | |
| 50 | - $cat = explode(',', $value); | |
| 51 | - $value = $cat; | |
| 52 | - } | |
| 53 | - | |
| 54 | - $metadata->$key = $value; | |
| 55 | - } | |
| 56 | - | |
| 57 | - return $metadata; | |
| 58 | - } | |
| 59 | - | |
| 60 | - /** | |
| 61 | - * Markdownをパースする | |
| 62 | - * | |
| 63 | - * @return string HTMLとしてレンダリングされたコンテンツ | |
| 64 | - */ | |
| 65 | - public function parse(): string { | |
| 66 | - $content = file_get_contents($this->path); | |
| 67 | - $parts = explode(self::METADATA_LINE, $content, 2); | |
| 68 | - $this->content = count($parts) > 1 ? trim($parts[1]): trim($content); | |
| 69 | - $this->html = []; | |
| 70 | - | |
| 71 | - $lines = explode("\n", $this->content); | |
| 72 | - $currentParagraph = []; | |
| 73 | - | |
| 74 | - $inList = false; | |
| 75 | - $listItems = []; | |
| 76 | - $listLevel = 0; | |
| 77 | - $inBlockquote = false; | |
| 78 | - $blockquoteContent = []; | |
| 79 | - $tableHeaders = []; | |
| 80 | - $tableRows = []; | |
| 81 | - $inTable = false; | |
| 82 | - $i = 0; | |
| 83 | - | |
| 84 | - foreach ($lines as $line) { | |
| 85 | - $i++; | |
| 86 | - $hasBR = substr($line, -1) === '\\'; | |
| 87 | - $line = rtrim($line, " \t\r\n\\"); | |
| 88 | - | |
| 89 | - // コメント | |
| 90 | - // if (str_starts_with($line, '//')) { | |
| 91 | - // if ($hasBR && !empty($currentParagraph)) { | |
| 92 | - // $currentParagraph[] = ''; | |
| 93 | - // } | |
| 94 | - // continue; | |
| 95 | - // } | |
| 96 | - | |
| 97 | - // コードブロック | |
| 98 | - if (preg_match('/^```(\w*)$/', $line, $matches)) { | |
| 99 | - if (!$this->inCodeBlock) { | |
| 100 | - if (!empty($currentParagraph)) { | |
| 101 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 102 | - $currentParagraph = []; | |
| 103 | - } | |
| 104 | - $this->inCodeBlock = true; | |
| 105 | - $this->codeBlockLanguage = $matches[1]; | |
| 106 | - continue; | |
| 107 | - } else { | |
| 108 | - $this->html[] = $this->createCodeBlock(); | |
| 109 | - $this->inCodeBlock = false; | |
| 110 | - $this->codeBlockLanguage = ''; | |
| 111 | - $this->codeBlockContent = []; | |
| 112 | - continue; | |
| 113 | - } | |
| 114 | - } | |
| 115 | - | |
| 116 | - if ($this->inCodeBlock) { | |
| 117 | - $this->codeBlockContent[] = $line; | |
| 118 | - continue; | |
| 119 | - } | |
| 120 | - | |
| 121 | - // テーブルの処理 | |
| 122 | - if (preg_match('/^\|(.+)\|$/', $line)) { | |
| 123 | - if (!empty($currentParagraph)) { | |
| 124 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 125 | - $currentParagraph = []; | |
| 126 | - } | |
| 127 | - $cells = array_map('trim', explode('|', trim($line, '|'))); | |
| 128 | - | |
| 129 | - if (!$inTable) { | |
| 130 | - $tableHeaders = $cells; | |
| 131 | - $inTable = true; | |
| 132 | - } elseif (preg_match('/^\|(\s*:?-+:?\s*\|)+$/', $line)) { | |
| 133 | - continue; | |
| 134 | - } else { | |
| 135 | - $tableRows[] = $cells; | |
| 136 | - } | |
| 137 | - continue; | |
| 138 | - } elseif ($inTable) { | |
| 139 | - $this->html[] = $this->createTable($tableHeaders, $tableRows); | |
| 140 | - $tableHeaders = []; | |
| 141 | - $tableRows = []; | |
| 142 | - $inTable = false; | |
| 143 | - } | |
| 144 | - | |
| 145 | - // 水平線の処理 | |
| 146 | - if (preg_match('/^([\-\*\_])\1{2,}$/', $line)) { | |
| 147 | - if (!empty($currentParagraph)) { | |
| 148 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 149 | - $currentParagraph = []; | |
| 150 | - } | |
| 151 | - $this->html[] = " <hr />\n"; | |
| 152 | - continue; | |
| 153 | - } | |
| 154 | - | |
| 155 | - // 引用ブロックの処理 | |
| 156 | - if (preg_match('/^>\s(.+)/', $line, $matches)) { | |
| 157 | - if (!empty($currentParagraph)) { | |
| 158 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 159 | - $currentParagraph = []; | |
| 160 | - } | |
| 161 | - $inBlockquote = true; | |
| 162 | - $blockquoteContent[] = $this->parseInline($matches[1]); | |
| 163 | - continue; | |
| 164 | - } elseif ($inBlockquote && empty($line)) { | |
| 165 | - $this->html[] = $this->createBlockquote($blockquoteContent); | |
| 166 | - $blockquoteContent = []; | |
| 167 | - $inBlockquote = false; | |
| 168 | - continue; | |
| 169 | - } | |
| 170 | - | |
| 171 | - // 空行をスキップ | |
| 172 | - if (empty($line)) { | |
| 173 | - if ($inList) { | |
| 174 | - $this->html[] = $this->createList($listItems); | |
| 175 | - $listItems = []; | |
| 176 | - $inList = false; | |
| 177 | - } | |
| 178 | - if (!empty($currentParagraph)) { | |
| 179 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 180 | - $currentParagraph = []; | |
| 181 | - } | |
| 182 | - | |
| 183 | - continue; | |
| 184 | - } | |
| 185 | - | |
| 186 | - // ヘッダー | |
| 187 | - if (preg_match('/^(#{1,6})\s(.+)/', $line, $m)) { | |
| 188 | - if ($inList) { | |
| 189 | - $this->html[] = $this->createList($listItems); | |
| 190 | - $listItems = []; | |
| 191 | - $inList = false; | |
| 192 | - } | |
| 193 | - if (!empty($currentParagraph)) { | |
| 194 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 195 | - $currentParagraph = []; | |
| 196 | - } | |
| 197 | - | |
| 198 | - $level = strlen($m[1]); | |
| 199 | - $this->html[] = " <h{$level}>".$this->parseInline($m[2])."</h{$level}>"; | |
| 200 | - continue; | |
| 201 | - } | |
| 202 | - | |
| 203 | - // 箇条書きリスト | |
| 204 | - if (preg_match('/^(\s*)([\*\-])\s(.+)/', $line, $m)) { | |
| 205 | - if (!empty($currentParagraph)) { | |
| 206 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 207 | - $currentParagraph = []; | |
| 208 | - } | |
| 209 | - $inList = true; | |
| 210 | - $currentLevel = strlen($m[1]) / 2; | |
| 211 | - $listLevel = max($listLevel, $currentLevel); | |
| 212 | - $listItems[] = [ | |
| 213 | - 'content' => $this->parseInline($m[3]), | |
| 214 | - 'level' => $currentLevel, | |
| 215 | - 'type' => 'ul', | |
| 216 | - ]; | |
| 217 | - | |
| 218 | - continue; | |
| 219 | - } | |
| 220 | - | |
| 221 | - // 番号付きリスト | |
| 222 | - if (preg_match('/^(\s*)\d+\.\s(.+)/', $line, $m)) { | |
| 223 | - if (!empty($currentParagraph)) { | |
| 224 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 225 | - $currentParagraph = []; | |
| 226 | - } | |
| 227 | - $inList = true; | |
| 228 | - $currentLevel = strlen($m[1]) / 2; | |
| 229 | - $listLevel = max($listLevel, $currentLevel); | |
| 230 | - $listItems[] = [ | |
| 231 | - 'content' => $this->parseInline($m[2]), | |
| 232 | - 'level' => $currentLevel, | |
| 233 | - 'type' => 'ol', | |
| 234 | - ]; | |
| 235 | - | |
| 236 | - continue; | |
| 237 | - } | |
| 238 | - | |
| 239 | - if ($inList) { | |
| 240 | - $this->html[] = $this->createList($listItems); | |
| 241 | - $listItems = []; | |
| 242 | - $inList = false; | |
| 243 | - $listLevel = 0; | |
| 244 | - } | |
| 245 | - | |
| 246 | - $parsedLine = $this->parseInline($line); | |
| 247 | - $currentParagraph[] = $parsedLine; | |
| 248 | - if ($hasBR) { | |
| 249 | - $currentParagraph[] = "<br />\n "; | |
| 250 | - } | |
| 251 | - } | |
| 252 | - | |
| 253 | - if ($inList) $this->html[] = $this->createList($listItems); | |
| 254 | - if ($inBlockquote) $this->html[] = $this->createBlockquote($blockquoteContent); | |
| 255 | - if ($inTable) $this->html[] = $this->createTable($tableHeaders, $tableRows); | |
| 256 | - if (!empty($currentParagraph)) | |
| 257 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 258 | - | |
| 259 | - return implode("\n", $this->html); | |
| 260 | - } | |
| 261 | - | |
| 262 | - // 機能性メソッド | |
| 263 | - | |
| 264 | - /** | |
| 265 | - * インラインのMarkdown記法をパースする | |
| 266 | - * | |
| 267 | - * @param string $text パースするテキスト | |
| 268 | - * @return string HTMLとしてレンダリングされたテキスト | |
| 269 | - */ | |
| 270 | - private function parseInline(string $text): string { | |
| 271 | - $this->algebraicPlaceholder = []; | |
| 272 | - $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); | |
| 273 | - | |
| 274 | - $patterns = [ | |
| 275 | - // 数式 | |
| 276 | - '/\$\$([^$]+)\$\$/u' => function($matches): string { | |
| 277 | - $placeholder = "{{ALG".count($this->algebraicPlaceholder).'ALG}}'; | |
| 278 | - $this->algebraicPlaceholder[$placeholder] = $matches[1]; | |
| 279 | - return $placeholder; | |
| 280 | - }, | |
| 281 | - // 太字 | |
| 282 | - '/\*\*(.+?)\*\*/u' => '<strong>$1</strong>', | |
| 283 | - // 斜体 | |
| 284 | - '/\*(.+?)\*/u' => '<em>$1</em>', | |
| 285 | - // 下線 | |
| 286 | - '/\_(.+?)\_/u' => '<u>$1</u>', | |
| 287 | - // 取り消し線 | |
| 288 | - '/\~(.+?)\~/u' => '<s>$1</s>', | |
| 289 | - // Blink (with speed) | |
| 290 | - '/!:\((.+?)\)(.+?):!/u' => '<span class="blink" style="animation-duration: $1s;">$2</span>', | |
| 291 | - // Blink | |
| 292 | - '/!:(.+?):!/u' => '<span class="blink">$1</span>', | |
| 293 | - // フォントの大きさ | |
| 294 | - '/\^\((.+?)\)(.+?)\^/u' => '<span style="font-size: $1px;">$2</span>', | |
| 295 | - // フォントカラー | |
| 296 | - '/\%\((.+?)\)(.+?)\%/u' => '<span style="color: #$1;">$2</span>', | |
| 297 | - // 画像 | |
| 298 | - '/\!\[(.*?)(?:#([^\]]*))?\]\((.+?)\)/u' => '<img style="width: 100%; $2" src="$3" alt="$1" />', | |
| 299 | - // 音楽 | |
| 300 | - '/\$\[([^\]]+)\]\(([^\)]+)\)/u' => '<audio controls><source src="$2" type="$1" /></audio>', | |
| 301 | - // 動画 | |
| 302 | - '/\#\[([^\]]+)\]\(([^\)]+)\)/u' => '<video style="max-width: 100%;" controls><source src="$2" type="$1" /></video>', | |
| 303 | - // リンク | |
| 304 | - '/\[(.+?)\]\((.+?)\)/u' => '<a href="$2">$1</a>', | |
| 305 | - // 振り仮名 | |
| 306 | - '/\<(.+?)\>\((.+?)\)/u' => '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>', | |
| 307 | - // インラインコード | |
| 308 | - '/`(.+?)`/u' => '<code>$1</code>', | |
| 309 | - ]; | |
| 310 | - | |
| 311 | - foreach ($patterns as $pattern => $replacement) { | |
| 312 | - if (is_callable($replacement)) { | |
| 313 | - $text = preg_replace_callback($pattern, $replacement, $text); | |
| 314 | - } else { | |
| 315 | - $result = preg_replace($pattern, $replacement, $text); | |
| 316 | - if ($result === null) continue; | |
| 317 | - $text = $result; | |
| 318 | - } | |
| 319 | - } | |
| 320 | - | |
| 321 | - // プレースホルダーの数式に交換 | |
| 322 | - foreach ($this->algebraicPlaceholder as $placeholder => $expr) { | |
| 323 | - $text = str_replace($placeholder, $this->parseAlgebraic($expr), $text); | |
| 324 | - } | |
| 325 | - | |
| 326 | - return $text; | |
| 327 | - } | |
| 328 | - | |
| 329 | - /** | |
| 330 | - * 数式記法をパースする | |
| 331 | - * | |
| 332 | - * @param string $expression パースする数式 | |
| 333 | - * @return string MathJax用にフォーマットされた数式 | |
| 334 | - */ | |
| 335 | - private function parseAlgebraic(string $expression): string { | |
| 336 | - // HTMLエスケープ | |
| 337 | - $expression = htmlspecialchars_decode($expression, ENT_QUOTES); | |
| 338 | - | |
| 339 | - // ふぃりしゃもじのマッピング | |
| 340 | - $greekLetters = [ | |
| 341 | - 'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ', | |
| 342 | - 'epsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ', | |
| 343 | - 'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ', | |
| 344 | - 'nu' => 'ν', 'xi' => 'ξ', 'omicron' => 'ο', 'pi' => 'π', | |
| 345 | - 'rho' => 'ρ', 'sigma' => 'σ', 'tau' => 'τ', 'upsilon' => 'υ', | |
| 346 | - 'phi' => 'φ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω', | |
| 347 | - 'Alpha' => 'Α', 'Beta' => 'Β', 'Gamma' => 'Γ', 'Delta' => 'Δ', | |
| 348 | - 'Epsilon' => 'Ε', 'Zeta' => 'Ζ', 'Eta' => 'Η', 'Theta' => 'Θ', | |
| 349 | - 'Iota' => 'Ι', 'Kappa' => 'Κ', 'Lambda' => 'Λ', 'Mu' => 'Μ', | |
| 350 | - 'Nu' => 'Ν', 'Xi' => 'Ξ', 'Omicron' => 'Ο', 'Pi' => 'Π', | |
| 351 | - 'Rho' => 'Ρ', 'Sigma' => 'Σ', 'Tau' => 'Τ', 'Upsilon' => 'Υ', | |
| 352 | - 'Phi' => 'Φ', 'Chi' => 'Χ', 'Psi' => 'Ψ', 'Omega' => 'Ω', | |
| 353 | - ]; | |
| 354 | - | |
| 355 | - // ギリシャ文字を置換 | |
| 356 | - foreach ($greekLetters as $text => $symbol) { | |
| 357 | - $expression = preg_replace("/\b$text\b/", $symbol, $expression); | |
| 358 | - } | |
| 359 | - | |
| 360 | - // 上付き文字(例: x^2) | |
| 361 | - $expression = preg_replace_callback('/(\w+)\^(\{?(\d+|\w+)\}?)/', function($matches) { | |
| 362 | - $base = $matches[1]; | |
| 363 | - $exponent = $matches[2]; | |
| 364 | - if ($exponent[0] === '{') $exponent = substr($exponent, 1, -1); | |
| 365 | - | |
| 366 | - $superscripts = str_split($exponent); | |
| 367 | - $supMap = [ | |
| 368 | - '0' => '⁰', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴', | |
| 369 | - '5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹', | |
| 370 | - 'a' => 'ᵃ', 'b' => 'ᵇ', 'c' => 'ᶜ', 'd' => 'ᵈ', 'e' => 'ᵉ', | |
| 371 | - 'i' => 'ⁱ', 'n' => 'ⁿ', | |
| 372 | - ]; | |
| 373 | - $converted = ''; | |
| 374 | - | |
| 375 | - foreach ($superscripts as $char) $converted .= $supMap[$char] ?? $char; | |
| 376 | - | |
| 377 | - return $base.'<sup>'.$converted.'</sup>'; | |
| 378 | - }, $expression); | |
| 379 | - | |
| 380 | - // 下付き文字(例: x_1) | |
| 381 | - $expression = preg_replace_callback('/(\w+)_((\d+|\w+)\}?)/', function($matches) { | |
| 382 | - $base = $matches[1]; | |
| 383 | - $subscript = $matches[2]; | |
| 384 | - | |
| 385 | - if ($subscript[0] === '{') $subscript = substr($subscript, 1, -1); | |
| 386 | - | |
| 387 | - $subscripts = str_split($subscript); | |
| 388 | - $subMap = [ | |
| 389 | - '0' => '₀', '1' => '₁', '2' => '₂', '3' => '₃', '4' => '₄', | |
| 390 | - '5' => '₅', '6' => '₆', '7' => '₇', '8' => '₈', '9' => '₉', | |
| 391 | - ]; | |
| 392 | - $converted = ''; | |
| 393 | - | |
| 394 | - foreach ($subscripts as $char) { | |
| 395 | - $converted .= $subMap[$char] ?? $char; | |
| 396 | - } | |
| 397 | - | |
| 398 | - return $base.'<sub>'.$converted.'</sub>'; | |
| 399 | - }, $expression); | |
| 400 | - | |
| 401 | - // 分数(例: 4/5x, a/b) | |
| 402 | - $expression = preg_replace('/(\d+|\w+)\/(\d*\w+)/', | |
| 403 | - '<span class="fraction"><span class="numerator">$1</span><span class="denominator">$2</span></span>', | |
| 404 | - $expression); | |
| 405 | - | |
| 406 | - // 演算子 | |
| 407 | - $operators = [ | |
| 408 | - '\*' => '・', | |
| 409 | - '!=' => '≠', | |
| 410 | - '>=' => '≥', | |
| 411 | - '<=' => '≤', | |
| 412 | - 'sqrt' => '√', | |
| 413 | - ]; | |
| 414 | - | |
| 415 | - foreach ($operators as $op => $symbol) $expression = preg_replace("/\b$op\b/", $symbol, $expression); | |
| 416 | - | |
| 417 | - // 数式全体をspanで囲む | |
| 418 | - return '<span class="algebraic">'.$expression.'</span>'; | |
| 419 | - } | |
| 420 | - | |
| 421 | - /** | |
| 422 | - * リストを作成する | |
| 423 | - * | |
| 424 | - * @param array $items リストアイテムの配列 | |
| 425 | - * @param int $maxLevel 最大ネストレベル | |
| 426 | - * @return string HTMLのリスト | |
| 427 | - */ | |
| 428 | - private function createList(array $items, int $maxLevel = 1): string { | |
| 429 | - if ($items === []) return ''; | |
| 430 | - | |
| 431 | - $html = ''; | |
| 432 | - $currentLevel = 0; | |
| 433 | - $listStack = []; | |
| 434 | - $currentType = ''; | |
| 435 | - | |
| 436 | - foreach ($items as $item) { | |
| 437 | - $level = isset($item['level']) ? $item['level'] : $currentLevel; | |
| 438 | - | |
| 439 | - while ($currentLevel > $level) | |
| 440 | - $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; | |
| 441 | - | |
| 442 | - while ($currentLevel < $level) { | |
| 443 | - $currentLevel++; | |
| 444 | - $listStack[] = $item['type']; | |
| 445 | - $html .= str_repeat(' ', $currentLevel - 1)."<".$item['type'].">\n"; | |
| 446 | - } | |
| 447 | - | |
| 448 | - if ($currentType != $item['type'] && $currentLevel == $item['level']) { | |
| 449 | - if (!empty($listStack)) { | |
| 450 | - $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; | |
| 451 | - $listStack[] = $item['type']; | |
| 452 | - $html .= str_repeat(' ', $currentLevel - 1)."<".$item['type'].">\n"; | |
| 453 | - } | |
| 454 | - } | |
| 455 | - | |
| 456 | - $currentType = $item['type']; | |
| 457 | - } | |
| 458 | - | |
| 459 | - $html .= " <p>\n <{$currentType}>\n"; | |
| 460 | - foreach ($items as $item) { | |
| 461 | - $html .= str_repeat(' ', $currentLevel)." <li>".$item['content']."</li>\n"; | |
| 462 | - } | |
| 463 | - $html .= " </{$currentType}>\n </p>"; | |
| 464 | - | |
| 465 | - while (!empty($listStack)) { | |
| 466 | - $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; | |
| 467 | - $currentLevel--; | |
| 468 | - } | |
| 469 | - | |
| 470 | - return rtrim($html); | |
| 471 | - } | |
| 472 | - | |
| 473 | - /** | |
| 474 | - * コードブロックを作成する | |
| 475 | - * | |
| 476 | - * @return string HTMLのコードブロック | |
| 477 | - */ | |
| 478 | - private function createCodeBlock(): string { | |
| 479 | - $code = htmlspecialchars(implode("\n", $this->codeBlockContent)); | |
| 480 | - $class = $this->codeBlockLanguage ? " class=\"language-{$this->codeBlockLanguage}\"" : ''; | |
| 481 | - return " <p>\n <pre><code{$class}>{$code}</code></pre>"; | |
| 482 | - // $raw = implode("\n", $this->codeBlockContent); | |
| 483 | - // $lang = $this->codeBlockLanguage ?: 'txt'; | |
| 484 | - // $class = $lang === 'txt' ? '' : " class=\"language-{$lang}\""; | |
| 485 | - // $highlighted = $this->highlightCode($raw, $lang); | |
| 486 | - // return "<pre><code{$class}>{$highlighted}</code></pre>"; | |
| 487 | - } | |
| 488 | - | |
| 489 | - private function createBlockquote(array $content): string { | |
| 490 | - return " <p>\n <blockquote>\n ".implode("<br />\n ", $content)."\n </blockquote>\n </p>"; | |
| 491 | - } | |
| 492 | - | |
| 493 | - /** | |
| 494 | - * テーブルを作成する | |
| 495 | - * | |
| 496 | - * @param array $headers ヘッダー配列 | |
| 497 | - * @param array $rows 行データの配列 | |
| 498 | - * @return string HTMLのテーブル | |
| 499 | - */ | |
| 500 | - private function createTable(array $headers, array $rows): string { | |
| 501 | - $html = " <p>\n <table>\n"; | |
| 502 | - | |
| 503 | - // ヘッダーを追加 | |
| 504 | - if (!empty($headers)) { | |
| 505 | - $html .= " <thead>\n <tr>\n"; | |
| 506 | - foreach ($headers as $header) { | |
| 507 | - $html .= " <th>".$this->parseInline($header)."</th>\n"; | |
| 508 | - } | |
| 509 | - $html .= " </tr>\n </thead>\n"; | |
| 510 | - } | |
| 511 | - | |
| 512 | - // 行を追加 | |
| 513 | - if (!empty($rows)) { | |
| 514 | - $html .= " <tbody>\n"; | |
| 515 | - foreach ($rows as $row) { | |
| 516 | - $html .= " <tr>\n"; | |
| 517 | - foreach ($row as $cell) { | |
| 518 | - $html .= " <td>".$this->parseInline($cell)."</td>\n"; | |
| 519 | - } | |
| 520 | - $html .= " </tr>\n"; | |
| 521 | - } | |
| 522 | - $html .= " </tbody>\n"; | |
| 523 | - } | |
| 524 | - | |
| 525 | - $html .= " </table>\n </p>"; | |
| 526 | - return $html; | |
| 527 | - } | |
| 528 | - | |
| 529 | - /** | |
| 530 | - * Pure-PHP syntax highlighter | |
| 531 | - * | |
| 532 | - * @param string $code Raw source code | |
| 533 | - * @param string $lang Language identifier (html, css, js, php, …) | |
| 534 | - * @return string HTML with <span class="hl-…"> tokens | |
| 535 | - */ | |
| 536 | - private function highlightCode(string $code, string $lang): string | |
| 537 | - { | |
| 538 | - $lang = strtolower(trim($lang)); | |
| 539 | - $rules = []; | |
| 540 | - | |
| 541 | - // ---- COMMON ------------------------------------------------------- | |
| 542 | - $rules['comment'] = [ | |
| 543 | - // /* … */ (multi-line) | |
| 544 | - ['/\/\*(?:.|\n)*?\*\//', 'hl-comment'], | |
| 545 | - // // …\n | |
| 546 | - ['/\/\/.*(?=\n|$)/', 'hl-comment'], | |
| 547 | - ]; | |
| 548 | - | |
| 549 | - $rules['string'] = [ | |
| 550 | - // "…" and '…' (allow escaped quotes) | |
| 551 | - ['/"(?:\\.|[^"\\\n])*"/', 'hl-string'], | |
| 552 | - ["/'(?:\\.|[^'\\\n])*'/", 'hl-string'], | |
| 553 | - ]; | |
| 554 | - | |
| 555 | - $rules['number'] = [ | |
| 556 | - ['/\b(?:0x[0-9a-fA-F]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/', 'hl-number'], | |
| 557 | - ]; | |
| 558 | - | |
| 559 | - // ---- HTML --------------------------------------------------------- | |
| 560 | - if ($lang === 'html') { | |
| 561 | - $rules['tag'] = [['/<\/?[a-zA-Z0-9\-]+/', 'hl-tag']]; | |
| 562 | - $rules['attr'] = [['/\s[a-zA-Z0-9\-]+(?==)/', 'hl-attr']]; | |
| 563 | - $rules['value'] = [['/(?<==)"(?:\\.|[^"\\\n])*"(?=[>\s])/', 'hl-value']]; | |
| 564 | - } | |
| 565 | - | |
| 566 | - // ---- CSS ---------------------------------------------------------- | |
| 567 | - if ($lang === 'css') { | |
| 568 | - $rules['selector'] = [['/^[\t ]*[a-zA-Z0-9#\.\-\:$$ $$\=\*\>\+\~\^]+(?=\s*\{)/m', 'hl-selector']]; | |
| 569 | - $rules['property'] = [['/\b[a-z\-]+(?=\s*:)/', 'hl-property']]; | |
| 570 | - $rules['value'] = [['/(?<=\:)\s*[^;]+(?=;)/', 'hl-value']]; | |
| 571 | - $rules['unit'] = [['/\b\d+(px|em|rem|%|vh|vw|pt|pc|cm|mm|in)\b/i', 'hl-unit']]; | |
| 572 | - } | |
| 573 | - | |
| 574 | - // ---- PHP ---------------------------------------------------------- | |
| 575 | - if ($lang === 'php') { | |
| 576 | - $keywords = 'abstract|and|array|as|break|callable|case|catch|class|clone|' | |
| 577 | - . 'const|continue|declare|default|die|do|echo|else|elseif|empty|' | |
| 578 | - . 'enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|' | |
| 579 | - . 'extends|final|finally|for|foreach|function|global|goto|if|' | |
| 580 | - . 'implements|include|include_once|instanceof|insteadof|interface|' | |
| 581 | - . 'isset|list|namespace|new|or|print|private|protected|public|' | |
| 582 | - . 'require|require_once|return|static|switch|throw|trait|try|unset|' | |
| 583 | - . 'use|var|while|xor|yield'; | |
| 584 | - $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 585 | - $rules['function']= [['/\b([a-zA-Z_\x7f-\xff][\w\x7f-\xff]*)(?=\s*\()/', 'hl-function']]; | |
| 586 | - $rules['variable']= [['/\$[a-zA-Z_\x7f-\xff][\w\x7f-\xff]*/', 'hl-variable']]; | |
| 587 | - } | |
| 588 | - | |
| 589 | - // ---- Shell -------------------------------------------------------- | |
| 590 | - if ($lang === 'sh') { | |
| 591 | - $keywords = 'if|then|else|elif|fi|case|esac|for|select|while|until|do|done|in'; | |
| 592 | - $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 593 | - $rules['variable']= [['/\$\{?[a-zA-Z_][\w]*\}?/', 'hl-variable']]; | |
| 594 | - } | |
| 595 | - | |
| 596 | - // ---- JSON --------------------------------------------------------- | |
| 597 | - if ($lang === 'json') { | |
| 598 | - $rules['property'] = [['/"([^"]+)":/', '<span class="hl-property">$1</span>:']]; | |
| 599 | - } | |
| 600 | - | |
| 601 | - // ---- C / C++ ----------------------------------------------- | |
| 602 | - if (in_array($lang, ['c', 'cpp', 'cs'])) { | |
| 603 | - $keywords = 'abstract|assert|boolean|break|byte|case|catch|char|class|const|' | |
| 604 | - . 'continue|default|do|double|else|enum|extends|false|final|finally|' | |
| 605 | - . 'float|for|goto|if|implements|import|instanceof|int|interface|long|' | |
| 606 | - . 'native|new|null|package|private|protected|public|return|short|static|' | |
| 607 | - . 'strictfp|super|switch|synchronized|this|throw|throws|transient|true|' | |
| 608 | - . 'try|void|volatile|while'; | |
| 609 | - $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 610 | - $rules['type'] = [['/\b(?:int|float|double|char|bool|void|string)\b/', 'hl-type']]; | |
| 611 | - $rules['define'] = [['/(#include)|(#define)/', 'hl-define']]; | |
| 612 | - } | |
| 613 | - | |
| 614 | - if ($lang === 'markdown') { | |
| 615 | - $rules['header'] = [['/^(#{1,6})\s+(.+)$/', '<span class="hl-header">$1 $2</span>']]; | |
| 616 | - $rules['link'] = [['/\[([^\]]+)\]\(([^)]+) $$/', '<span class="hl-link">[$1]($2)</span>']]; | |
| 617 | - } | |
| 618 | - | |
| 619 | - $html = htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8'); | |
| 620 | - | |
| 621 | - $order = ['comment', 'string', 'number']; | |
| 622 | - foreach (['tag','attr','value','selector','property','unit','keyword','function','variable','type','header','link'] as $k) { | |
| 623 | - if (isset($rules[$k])) $order[] = $k; | |
| 624 | - } | |
| 625 | - | |
| 626 | - foreach ($order as $type) { | |
| 627 | - if (!isset($rules[$type])) continue; | |
| 628 | - foreach ($rules[$type] as $rule) { | |
| 629 | - [$pattern, $class] = $rule; | |
| 630 | - $html = preg_replace_callback( | |
| 631 | - $pattern, | |
| 632 | - function ($m) use ($class) { | |
| 633 | - // For JSON property we already built the <span> | |
| 634 | - if (strpos($m[0], '<span') === 0) return $m[0]; | |
| 635 | - $inner = $m[0]; | |
| 636 | - // Preserve already escaped entities inside strings/comments | |
| 637 | - return '<span class="'.$class.'">'.$inner.'</span>'; | |
| 638 | - }, | |
| 639 | - $html | |
| 640 | - ); | |
| 641 | - } | |
| 642 | - } | |
| 643 | - | |
| 644 | - return $html; | |
| 645 | - } | |
| 646 | -} |
削除
src/Site/Lib/Route.php
| @@ -1,163 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -class Route { | |
| 5 | - protected static array $routes = []; | |
| 6 | - protected static array $fallback = []; | |
| 7 | - | |
| 8 | - /** | |
| 9 | - * ルート設定を言語固有のハンドラで初期化する | |
| 10 | - * | |
| 11 | - * @param array $routes ルート設定の配列 | |
| 12 | - * @return void | |
| 13 | - */ | |
| 14 | - public static function init(array $routes): void { | |
| 15 | - self::$routes = $routes; | |
| 16 | - } | |
| 17 | - | |
| 18 | - /** | |
| 19 | - * ルートを追加する | |
| 20 | - * | |
| 21 | - * @param string $method HTTPメソッド | |
| 22 | - * @param string $path URLパス | |
| 23 | - * @param string|callable $class ハンドラクラスとメソッド、またはコールバック | |
| 24 | - * @param array $params オプションのパラメータ | |
| 25 | - * @return array ルート設定 | |
| 26 | - */ | |
| 27 | - public static function add(string $method, string $path, string|callable $class, | |
| 28 | - array $params = []): array { | |
| 29 | - $route = [ | |
| 30 | - 'method' => $method, | |
| 31 | - 'path' => $path, | |
| 32 | - 'class' => $class, | |
| 33 | - 'params' => $params, | |
| 34 | - ]; | |
| 35 | - | |
| 36 | - self::$routes[] = $route; | |
| 37 | - return $route; | |
| 38 | - } | |
| 39 | - | |
| 40 | - /** | |
| 41 | - * 404処理用のフォールバックルートを設定する | |
| 42 | - * | |
| 43 | - * @param array|string|callable $class | |
| 44 | - * @return void | |
| 45 | - */ | |
| 46 | - public static function setFallback(array|string|callable $class): void { | |
| 47 | - self::$fallback = [ | |
| 48 | - 'class' => $class, | |
| 49 | - 'params' => [], | |
| 50 | - ]; | |
| 51 | - } | |
| 52 | - | |
| 53 | - /** | |
| 54 | - * 適切なルートをマッチさせて実行する | |
| 55 | - * | |
| 56 | - * @param string $uri リクエストURI | |
| 57 | - * @return void | |
| 58 | - */ | |
| 59 | - public static function dispatch(string $uri): void { | |
| 60 | - // URIをパスとクエリ文字列に分割 | |
| 61 | - $uriParts = explode('?', $uri, 2); | |
| 62 | - $path = trim($uriParts[0], " \t\n\r\0\x0B/"); | |
| 63 | - | |
| 64 | - // ルートパスの処理(/?page=2のようなクエリパラメータを含む場合も処理) | |
| 65 | - if ($path === '') { | |
| 66 | - self::executeClass([ | |
| 67 | - 'class' => [new \Site\Controller\Home(), 'show'], | |
| 68 | - 'params' => ['lang' => 'ja'], | |
| 69 | - ]); | |
| 70 | - return; | |
| 71 | - } | |
| 72 | - | |
| 73 | - if ($path === 'en') { | |
| 74 | - self::executeClass([ | |
| 75 | - 'class' => [new \Site\Controller\Home(), 'show'], | |
| 76 | - 'params' => ['lang' => 'en'], | |
| 77 | - ]); | |
| 78 | - return; | |
| 79 | - } | |
| 80 | - | |
| 81 | - // パスに対してルートをマッチングする | |
| 82 | - foreach (self::$routes as $route) { | |
| 83 | - $matches = []; | |
| 84 | - | |
| 85 | - if (self::matchRoute($route['path'], $path, $matches)) { | |
| 86 | - $params = self::extractParams($route['path'], $path); | |
| 87 | - $params = array_merge($route['params'], $params); | |
| 88 | - | |
| 89 | - if (is_string($route['class'])) { | |
| 90 | - [ $class, $method ] = explode('@', $route['class']); | |
| 91 | - $controller = new $class(); | |
| 92 | - self::executeClass([ | |
| 93 | - 'class' => [ $controller, $method ], | |
| 94 | - 'params' => $params, | |
| 95 | - ]); | |
| 96 | - | |
| 97 | - return; | |
| 98 | - } elseif (is_callable($route['class'])) { | |
| 99 | - self::executeClass([ | |
| 100 | - 'class' => $route['class'], | |
| 101 | - 'params' => $params, | |
| 102 | - ]); | |
| 103 | - | |
| 104 | - return; | |
| 105 | - } | |
| 106 | - } | |
| 107 | - } | |
| 108 | - | |
| 109 | - // マッチするルートがない場合、フォールバックを実行 | |
| 110 | - self::executeClass(self::$fallback); | |
| 111 | - } | |
| 112 | - | |
| 113 | - /** | |
| 114 | - * ルートパターンとパスをマッチングする | |
| 115 | - * | |
| 116 | - * @param string $pattern ルートパターン | |
| 117 | - * @param string $path 現在のパス | |
| 118 | - * @param array $matches マッチを格納する参照 | |
| 119 | - * @return bool | |
| 120 | - */ | |
| 121 | - protected static function matchRoute(string $pattern, string $path, | |
| 122 | - array &$matches = []): bool { | |
| 123 | - // ルートパターンを正規表現パターンに変換 | |
| 124 | - $pattern = preg_replace('/\{([^:}]+)(?::([^}]+))?\}/', '(?P<$1>[^/]+)', $pattern); | |
| 125 | - $pattern = str_replace('/', '\/', $pattern); | |
| 126 | - return (bool)preg_match('/^'.$pattern.'$/', $path, $matches); | |
| 127 | - } | |
| 128 | - | |
| 129 | - /** | |
| 130 | - * パターンに基づいてパスから名前付きパラメータを抽出する | |
| 131 | - * | |
| 132 | - * @param string $pattern ルートパターン | |
| 133 | - * @param string $path 現在のパス | |
| 134 | - * @return array | |
| 135 | - */ | |
| 136 | - protected static function extractParams(string $pattern, string $path): array { | |
| 137 | - $params = []; | |
| 138 | - $patternParts = explode('/', $pattern); | |
| 139 | - $pathParts = explode('/', $path); | |
| 140 | - | |
| 141 | - foreach ($patternParts as $k => $v) { | |
| 142 | - if (preg_match('/\{([^:}]+)(?::([^}]+))?\}/', $v, $matches)) { | |
| 143 | - if (isset($pathParts[$k])) { | |
| 144 | - $params[$matches[1]] = $pathParts[$k]; | |
| 145 | - } | |
| 146 | - } | |
| 147 | - } | |
| 148 | - | |
| 149 | - return $params; | |
| 150 | - } | |
| 151 | - | |
| 152 | - /** | |
| 153 | - * ルートクラスを実行する | |
| 154 | - * | |
| 155 | - * @param array $route ルート設定 | |
| 156 | - * @return void | |
| 157 | - */ | |
| 158 | - protected static function executeClass(array $route): void { | |
| 159 | - if (is_callable($route['class'])) { | |
| 160 | - call_user_func($route['class'], $route['params'] ?? []); | |
| 161 | - } | |
| 162 | - } | |
| 163 | -} |
削除
src/Site/Lib/Syntax.php
| @@ -1,280 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | -use Site\Lib\Syntax\AsmAarch32; | |
| 6 | -use Site\Lib\Syntax\AsmAarch64; | |
| 7 | -use Site\Lib\Syntax\AsmX8664; | |
| 8 | -use Site\Lib\Syntax\C; | |
| 9 | -use Site\Lib\Syntax\Cpp; | |
| 10 | -use Site\Lib\Syntax\Glsl; | |
| 11 | -use Site\Lib\Syntax\Go; | |
| 12 | - | |
| 13 | -class Syntax { | |
| 14 | - private array $alt = [ | |
| 15 | - 'asm-aarch32' => ['asm-arm'], | |
| 16 | - 'asm-x8664' => ['asm', 'asm-x86', 'asm-x64'], | |
| 17 | - 'cpp' => ['cc', 'c++'], | |
| 18 | - 'css' => ['css3'], | |
| 19 | - 'go' => ['golang'], | |
| 20 | - ]; | |
| 21 | - | |
| 22 | - private array $include = []; | |
| 23 | - private string $lang; | |
| 24 | - private SyntaxInterface $handler; | |
| 25 | - | |
| 26 | - private array $supported = [ | |
| 27 | - 'asm-aarch32' => AsmAarch32::class, | |
| 28 | - 'asm-aarch64' => AsmAarch64::class, | |
| 29 | - 'asm-x8664' => AsmX8664::class, | |
| 30 | - 'c' => C::class, | |
| 31 | - 'cpp' => Cpp::class, | |
| 32 | - 'glsl' => Glsl::class, | |
| 33 | - 'go' => Go::class, | |
| 34 | - ]; | |
| 35 | - | |
| 36 | - public function __construct(string $lang) { | |
| 37 | - $this->lang = $this->resolveLanguage($lang); | |
| 38 | - $this->handler = $this->createHandler($this->lang); | |
| 39 | - } | |
| 40 | - | |
| 41 | - public function colorize(string $code): string { | |
| 42 | - $lines = explode("\n", $code); | |
| 43 | - foreach ($lines as $c) { | |
| 44 | - $c = str_replace('<', '<', $c); | |
| 45 | - $c = str_replace('>', '>', $c); | |
| 46 | - } | |
| 47 | - | |
| 48 | - switch ($this->lang) { | |
| 49 | - case 'asm-aarch': | |
| 50 | - break; | |
| 51 | - case 'asm-x8664': | |
| 52 | - $lines = $this->addString($lines); | |
| 53 | - $lines = $this->addNumber($lines); | |
| 54 | - $lines = $this->addComment($lines); | |
| 55 | - $lines = $this->add($lines, 'register'); | |
| 56 | - $lines = $this->add($lines, 'operator'); | |
| 57 | - $lines = $this->addLabel($lines); | |
| 58 | - $lines = $this->addSection($lines); | |
| 59 | - $lines = $this->addDirective($lines); | |
| 60 | - break; | |
| 61 | - case 'c': | |
| 62 | - $this->include = $this->handler->getInclude($lines); | |
| 63 | - $lines = $this->addString($lines); | |
| 64 | - $lines = $this->addNumber($lines); | |
| 65 | - $lines = $this->addComment($lines); | |
| 66 | - $lines = $this->add($lines, 'preprocessor'); | |
| 67 | - $lines = $this->add($lines, 'keyword'); | |
| 68 | - foreach ($this->include as $inc) { | |
| 69 | - $lines = $this->add($lines, 'typedef', $inc); | |
| 70 | - $lines = $this->add($lines, 'libc', $inc); | |
| 71 | - } | |
| 72 | - $lines = $this->addFunction($lines); | |
| 73 | - // kys($lines); | |
| 74 | - break; | |
| 75 | - case 'cpp': | |
| 76 | - break; | |
| 77 | - case 'glsl': | |
| 78 | - break; | |
| 79 | - case 'go': | |
| 80 | - break; | |
| 81 | - default: | |
| 82 | - break; | |
| 83 | - } | |
| 84 | - | |
| 85 | - return implode("\n", $lines); | |
| 86 | - } | |
| 87 | - | |
| 88 | - /////////////// | |
| 89 | - | |
| 90 | - private function resolveLanguage(string $lang): string { | |
| 91 | - if (isset($this->supported[$lang])) return $lang; | |
| 92 | - foreach ($this->alt as $k => $v) if (in_array($lang, $v, true)) return $k; | |
| 93 | - throw new \InvalidArgumentException("未対応の言語: {$lang}"); | |
| 94 | - } | |
| 95 | - | |
| 96 | - private function createHandler(string $lang): SyntaxInterface { | |
| 97 | - $class = $this->supported[$lang] ?? null; | |
| 98 | - if (!$class) throw new \RuntimeException("言語のハンドルが存在しない: {$lang}"); | |
| 99 | - return new $class(); | |
| 100 | - } | |
| 101 | - | |
| 102 | - private function addComment(array $code): array { | |
| 103 | - global $colorPalette; | |
| 104 | - | |
| 105 | - $sc = $this->handler->getComment() ?? null; | |
| 106 | - $mc = $this->handler->getMultiline() ?? [null, null]; | |
| 107 | - $mcs = $mc[0]; | |
| 108 | - $mce = $mc[1]; | |
| 109 | - | |
| 110 | - $color = $this->handler->getColors()['comment'] ?? $colorPalette['dark']['green']; | |
| 111 | - | |
| 112 | - $isMulti = false; | |
| 113 | - $out = []; | |
| 114 | - | |
| 115 | - foreach ($code as $c) { | |
| 116 | - $cur = $c; | |
| 117 | - | |
| 118 | - if ($isMulti || strpos($cur, $sc) !== false || ($mcs && strpos($cur, $mcs) !== false)) { | |
| 119 | - $cur = preg_replace('/<span[^>]*>(.*?)<\/span>/s', '$1', $cur); | |
| 120 | - } | |
| 121 | - | |
| 122 | - if ($isMulti) { | |
| 123 | - $endPos = $mcs ? strpos($cur, $mce) : false; | |
| 124 | - | |
| 125 | - if ($endPos !== false) { | |
| 126 | - $before = substr($cur, 0, $endPos + strlen($mce)); | |
| 127 | - $after = substr($cur, $endPos + strlen($mce)); | |
| 128 | - | |
| 129 | - $cur = '<span style="color: '.$color.'; font-style: italics;">'.$before.'</span>'.$after; | |
| 130 | - $isMulti = false; | |
| 131 | - } else { | |
| 132 | - $cur = '<span style="color: '.$color.'; font-style: italics;">'.$cur.'</span>'; | |
| 133 | - } | |
| 134 | - } else { | |
| 135 | - if ($sc && ($pos = strpos($cur, $sc, 1)) !== false) { | |
| 136 | - $before = substr($cur, 0, $pos); | |
| 137 | - $rest = trim(substr($cur, $pos)); | |
| 138 | - $cur = $before.'<span style="color: '.$color.'; font-style: italics;">'.$rest.'</span>'; | |
| 139 | - } | |
| 140 | - | |
| 141 | - if (!$isMulti && $mcs && $mce && str_contains($cur, $mcs)) { | |
| 142 | - $startPos = strpos($cur, $mcs); | |
| 143 | - $endPos = $mce ? strpos($cur, $mce) : false; | |
| 144 | - | |
| 145 | - if ($endPos !== false && $startPos < $endPos) { | |
| 146 | - $before = substr($cur, 0, $startPos); | |
| 147 | - $part = substr($cur, $startPos, $endPos - $startPos + strlen($mce)); | |
| 148 | - $after = substr($cur, $endPos + strlen($mce)); | |
| 149 | - | |
| 150 | - $cur = $before.'<span style="color: '.$color.'; font-style: italics;">'.$part.'</span>'.$after; | |
| 151 | - } else { | |
| 152 | - $before = substr($cur, 0, $startPos); | |
| 153 | - $rest = substr($cur, $startPos); | |
| 154 | - | |
| 155 | - $cur = $before.'<span style="color: '.$color.'; font-style: italics;">'.$rest; | |
| 156 | - $isMulti = true; | |
| 157 | - } | |
| 158 | - } | |
| 159 | - } | |
| 160 | - | |
| 161 | - $out[] = $cur; | |
| 162 | - } | |
| 163 | - | |
| 164 | - return $out; | |
| 165 | - } | |
| 166 | - | |
| 167 | - private function addNumber(array $code): array { | |
| 168 | - $color = $this->handler->getColors()['number']; | |
| 169 | - $pattern = '/(?<![\w$@.])(?: # 関係ない | |
| 170 | - (?:0[xX][0-9a-fA-F]+) # 0xFF, 0XFF | |
| 171 | - |(?:0[bB][01]+) # 0b1010 | |
| 172 | - |(?:[0-9]+[hH]) # FFh, 0FFh | |
| 173 | - |(?:\$[0-9a-fA-F]+) # $FF | |
| 174 | - |(?:%[01]+) # %1010 | |
| 175 | - |(?:\d+(?:\.\d+)?(?:[eE][+-]?\d+)?) # 42, 3.14, 1e-5 | |
| 176 | - |(?:\d+) # 普通数字 | |
| 177 | - )(?![\w$@.])(?![a-zA-Z])/ix'; | |
| 178 | - | |
| 179 | - return array_map(fn($c) => preg_replace_callback($pattern, fn($m) => '<span style="color:'.$color.'">'.$m[0].'</span>', $c), $code); | |
| 180 | - } | |
| 181 | - | |
| 182 | - private function addString(array $code): array { | |
| 183 | - $color = $this->handler->getColors()['string']; | |
| 184 | - $pattern = '/(?:"(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\')/s'; | |
| 185 | - | |
| 186 | - return array_map( | |
| 187 | - function ($c) use ($pattern, $color) { | |
| 188 | - return preg_replace_callback($pattern, fn($m) => '<span style="color:'.$color.'">'.$m[0].'</span>', $c); | |
| 189 | - }, | |
| 190 | - $code | |
| 191 | - ); | |
| 192 | - } | |
| 193 | - | |
| 194 | - private function addLabel(array $code): array { | |
| 195 | - $color = $this->handler->getColors()['label']; | |
| 196 | - $out = []; | |
| 197 | - foreach ($code as $c) $out[] = preg_replace('/^\s*([\.a-zA-Z_][a-zA-Z0-9_]*)\s*:/m', '<span style="color: '.$color.';">$1</span>:', $c); | |
| 198 | - return $out; | |
| 199 | - } | |
| 200 | - | |
| 201 | - private function addSection(array $code): array { | |
| 202 | - $color1 = $this->handler->getColors()['label']; | |
| 203 | - $color2 = $this->handler->getColors()['register']; | |
| 204 | - $color3 = $this->handler->getColors()['number']; | |
| 205 | - $out = []; | |
| 206 | - | |
| 207 | - foreach ($code as $c) { | |
| 208 | - $cur = $c; | |
| 209 | - | |
| 210 | - $cur = preg_replace( | |
| 211 | - '/^(?:(section|default|segment|group)\s+)([\w.][\w.\-]*)(?:\s*(?:;.*)?)?$/im', | |
| 212 | - '<span style="color: '.$color1.';">$1</span> <span style="color: '.$color2.';">$2</span>', | |
| 213 | - $cur); | |
| 214 | - $cur = preg_replace( | |
| 215 | - '/^(bits)\s+(16|32|64)(?:\s*(?:;.*)?)?$/im', | |
| 216 | - '<span style="color: '.$color1.';">$1</span> <span style="color: '.$color3.';">$2</span>', | |
| 217 | - $cur); | |
| 218 | - | |
| 219 | - $out[] = $cur; | |
| 220 | - } | |
| 221 | - | |
| 222 | - return $out; | |
| 223 | - } | |
| 224 | - | |
| 225 | - private function add(array $code, string $type, ?string $header = null): array { | |
| 226 | - $tok = null !== $header ? $this->handler->getTokens()[$type][$header] : $this->handler->getTokens()[$type]; | |
| 227 | - $color = $this->handler->getColors()[$type]; | |
| 228 | - $pattern = '/\b('.implode('|', array_map(fn($r) => preg_quote($r, '/'), $tok)).')\b/i'; | |
| 229 | - $so = '<span style="color: '.$color.';">'; | |
| 230 | - $sc = '</span>'; | |
| 231 | - $out = []; | |
| 232 | - foreach ($code as $c) { | |
| 233 | - $out[] = preg_replace_callback($pattern, function ($m) use ($so, $sc) { return $so.$m[0].$sc; }, $c); | |
| 234 | - } | |
| 235 | - return $out; | |
| 236 | - } | |
| 237 | - | |
| 238 | - private function addFunction(array $code): array { | |
| 239 | - $color = $this->handler->getColors()['function']; | |
| 240 | - $out = []; | |
| 241 | - | |
| 242 | - foreach ($code as $c) { | |
| 243 | - $cur = $c; | |
| 244 | - $cur = preg_replace_callback("/\b(.*)\(\b/i", fn($m) => '<span style="color: '.$color.';">'.$m[0].'</span>', $cur); | |
| 245 | - $out[] = $cur; | |
| 246 | - } | |
| 247 | - | |
| 248 | - return $out; | |
| 249 | - } | |
| 250 | - | |
| 251 | - private function addDirective(array $code): array { | |
| 252 | - $tok = $this->handler->getTokens()['directive']; | |
| 253 | - $color1 = $this->handler->getColors()['directive']; | |
| 254 | - $color2 = $this->handler->getColors()['register']; | |
| 255 | - $pattern = implode('|', array_map(fn($r) => preg_quote($r, '/'), $tok)); | |
| 256 | - $out = []; | |
| 257 | - | |
| 258 | - foreach ($code as $c) { | |
| 259 | - $cur = $c; | |
| 260 | - $cur = preg_replace_callback("/\b({$pattern})\b/i", fn($m) => '<span style="color: '.$color1.';">'.$m[0].'</span>', $cur); | |
| 261 | - $cur = preg_replace_callback( | |
| 262 | - '/^\s*([a-zA-Z_][a-zA-Z0-9_$.@]*)\s+('.$pattern.')\b\s+(.+?)(?:\s*(?:;.*)?)?$/im', | |
| 263 | - function ($m) use ($color1, $color2) { | |
| 264 | - $l = $m[1]; | |
| 265 | - $d = $m[2]; | |
| 266 | - $v = $m[3]; | |
| 267 | - | |
| 268 | - // if (strcasecmp($d, 'equ') === 0) $v = preg_replace('/\$-[a-zA-Z_][a-zA-Z0-9_]*/i', '<span style="color: '.$color1.';">$o</span>', $v); | |
| 269 | - return sprintf( | |
| 270 | - '<span style="color: %s;">%s</span> '.'<span style="color: %s;">%s</span> %s', | |
| 271 | - $color1, $l, $color2, $v, $v | |
| 272 | - ); | |
| 273 | - }, $cur); | |
| 274 | - | |
| 275 | - $out[] = $cur; | |
| 276 | - } | |
| 277 | - | |
| 278 | - return $out; | |
| 279 | - } | |
| 280 | -} |
削除
src/Site/Lib/Syntax/Asm6502.php
| @@ -1,40 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class Asm6502 implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 16ビット | |
| 14 | - // 8ビット | |
| 15 | - ], | |
| 16 | - 'operator' => [ | |
| 17 | - ], | |
| 18 | - 'directive' => [ | |
| 19 | - ], | |
| 20 | - ]; | |
| 21 | - | |
| 22 | - public function __construct() { | |
| 23 | - global $colorPalette; | |
| 24 | - | |
| 25 | - $this->colors = [ | |
| 26 | - 'comment' => $colorPalette['dark']['green'], | |
| 27 | - 'directive' => $colorPalette['dark']['pink'], | |
| 28 | - 'label' => $colorPalette['light']['yellow'], | |
| 29 | - 'number' => $colorPalette['light']['green'], | |
| 30 | - 'operator' => $colorPalette['dark']['blue'], | |
| 31 | - 'register' => $colorPalette['dark']['cyan'], | |
| 32 | - 'string' => $colorPalette['medium']['orange'], | |
| 33 | - ]; | |
| 34 | - } | |
| 35 | - | |
| 36 | - public function getColors(): array { return $this->colors; } | |
| 37 | - public function getComment(): ?string { return $this->comment; } | |
| 38 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | - public function getTokens(): array { return $this->tokens; } | |
| 40 | -} |
削除
src/Site/Lib/Syntax/Asm68000.php
| @@ -1,39 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class Asm68000 implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 16ビット | |
| 14 | - ], | |
| 15 | - 'operator' => [ | |
| 16 | - ], | |
| 17 | - 'directive' => [ | |
| 18 | - ], | |
| 19 | - ]; | |
| 20 | - | |
| 21 | - public function __construct() { | |
| 22 | - global $colorPalette; | |
| 23 | - | |
| 24 | - $this->colors = [ | |
| 25 | - 'comment' => $colorPalette['dark']['green'], | |
| 26 | - 'directive' => $colorPalette['dark']['pink'], | |
| 27 | - 'label' => $colorPalette['light']['yellow'], | |
| 28 | - 'number' => $colorPalette['light']['green'], | |
| 29 | - 'operator' => $colorPalette['dark']['blue'], | |
| 30 | - 'register' => $colorPalette['dark']['cyan'], | |
| 31 | - 'string' => $colorPalette['medium']['orange'], | |
| 32 | - ]; | |
| 33 | - } | |
| 34 | - | |
| 35 | - public function getColors(): array { return $this->colors; } | |
| 36 | - public function getComment(): ?string { return $this->comment; } | |
| 37 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 38 | - public function getTokens(): array { return $this->tokens; } | |
| 39 | -} |
削除
src/Site/Lib/Syntax/AsmAarch32.php
| @@ -1,56 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmAarch32 implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // フロート、SIMD、ベクター | |
| 14 | - 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', // 64 | |
| 15 | - 'd10', 'd11', 'd12', 'd13', 'd14', 'd15', 'd16', 'd17', 'd18', 'd19', // 64 | |
| 16 | - 'd20', 'd21', 'd22', 'd23', 'd24', 'd25', 'd26', 'd27', 'd28', 'd29', 'd30', 'd31', // 64 | |
| 17 | - 's0', 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', // 32 | |
| 18 | - 's10', 's11', 's12', 's13', 's14', 's15', 's16', 's17', 's18', 's19', // 32 | |
| 19 | - 's20', 's21', 's22', 's23', 's24', 's25', 's26', 's27', 's28', 's29', 's30', 's31', // 32 | |
| 20 | - 'q0', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9', 'q10', 'q11', 'q12', 'q13', 'q14', 'q15', | |
| 21 | - // GPR | |
| 22 | - 'r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', | |
| 23 | - 'r13' /*スタックポインター*/, 'r14' /*リンクレジスター*/, 'r15' /*プログラムカウンター*/, | |
| 24 | - ], | |
| 25 | - 'operator' => [ | |
| 26 | - 'add', 'sub', 'mul', 'umul', 'mla', 'madd', 'msub', | |
| 27 | - 'and', 'orr', 'eor', 'bic', 'mvn', 'eon', 'ldr', 'bl', | |
| 28 | - 'ldm', 'stm', 'ldrd', 'strd', 'ldrex', 'strex', | |
| 29 | - 'vadd', 'vmul', 'vmla', 'blx', 'cmp', | |
| 30 | - ], | |
| 31 | - 'directive' => [ | |
| 32 | - '.arch armv8-a', 'arch_extension', '.cfi_', '.global', '.globl', '.section', | |
| 33 | - '.equ', '.set', '.byte', '.hword', '.word', '.quad', | |
| 34 | - '.arm', '.code 32', '.thumb', '.code 16', | |
| 35 | - ], | |
| 36 | - ]; | |
| 37 | - | |
| 38 | - public function __construct() { | |
| 39 | - global $colorPalette; | |
| 40 | - | |
| 41 | - $this->colors = [ | |
| 42 | - 'comment' => $colorPalette['dark']['green'], | |
| 43 | - 'directive' => $colorPalette['dark']['pink'], | |
| 44 | - 'label' => $colorPalette['light']['yellow'], | |
| 45 | - 'number' => $colorPalette['light']['green'], | |
| 46 | - 'operator' => $colorPalette['dark']['blue'], | |
| 47 | - 'register' => $colorPalette['dark']['cyan'], | |
| 48 | - 'string' => $colorPalette['medium']['orange'], | |
| 49 | - ]; | |
| 50 | - } | |
| 51 | - | |
| 52 | - public function getColors(): array { return $this->colors; } | |
| 53 | - public function getComment(): ?string { return $this->comment; } | |
| 54 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 55 | - public function getTokens(): array { return $this->tokens; } | |
| 56 | -} |
削除
src/Site/Lib/Syntax/AsmAarch64.php
| @@ -1,65 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmAarch64 implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // フロート、SIMD、ベクター | |
| 14 | - 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', // 64 | |
| 15 | - 'd10', 'd11', 'd12', 'd13', 'd14', 'd15', 'd16', 'd17', 'd18', 'd19', // 64 | |
| 16 | - 'd20', 'd21', 'd22', 'd23', 'd24', 'd25', 'd26', 'd27', 'd28', 'd29', 'd30', 'd31', // 64 | |
| 17 | - 's0', 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', // 32 | |
| 18 | - 's10', 's11', 's12', 's13', 's14', 's15', 's16', 's17', 's18', 's19', // 32 | |
| 19 | - 's20', 's21', 's22', 's23', 's24', 's25', 's26', 's27', 's28', 's29', 's30', 's31', // 32 | |
| 20 | - 'v0', 'v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7', 'v8', 'v9', | |
| 21 | - 'v10', 'v11', 'v12', 'v13', 'v14', 'v15', 'v16', 'v17', 'v18', 'v19', | |
| 22 | - 'v20', 'v21', 'v22', 'v23', 'v24', 'v25', 'v26', 'v27', 'v28', 'v29', 'v30', 'v31', | |
| 23 | - 'q16', 'q17', 'q18', 'q19', 'q20', 'q21', 'q22', 'q23', 'q24', 'q25', 'q26', 'q27', 'q28', 'q29', 'q30', 'q31', | |
| 24 | - // GPR | |
| 25 | - 'x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', | |
| 26 | - 'x10', 'x11', 'x12', 'x13', 'x14', 'x15', 'x16', 'x17', 'x18', 'x19', | |
| 27 | - 'x20', 'x21', 'x22', 'x23', 'x24', 'x25', 'x26', 'x27', 'x28', 'x29', 'x30', 'x31' /*ゼロレジスター*/, | |
| 28 | - 'w0', 'w1', 'w2', 'w3', 'w4', 'w5', 'w6', 'w7', 'w8', 'w9', | |
| 29 | - 'w10', 'w11', 'w12', 'w13', 'w14', 'w15', 'w16', 'w17', 'w18', 'w19', | |
| 30 | - 'w20', 'w21', 'w22', 'w23', 'w24', 'w25', 'w26', 'w27', 'w28', 'w29', 'w30', | |
| 31 | - 'xzr' /*ゼロレジスター*/, 'wzr' /*ゼロレジスター*/, | |
| 32 | - 'sp' /*スタックポインター*/, 'pc' /*プログラムカウンター*/, | |
| 33 | - ], | |
| 34 | - 'operator' => [ | |
| 35 | - 'add', 'sub', 'mul', 'umul', 'mla', 'madd', 'msub', | |
| 36 | - 'and', 'orr', 'eor', 'bic', 'mvn', 'eon', 'ldr', 'bl', | |
| 37 | - 'csel', 'cinc', 'cset', 'addv', 'smull', 'fmla', 'blr', 'cbz', 'cbnz', 'tbz', 'tbnz', | |
| 38 | - 'adpr', 'movz', 'movk', 'ldp', 'stp', 'ldxr', 'stxr', 'ldadd', 'ldclr', 'ldset', | |
| 39 | - ], | |
| 40 | - 'directive' => [ | |
| 41 | - '.arch armv8-a', 'arch_extension', '.cfi_', '.global', '.globl', '.section', | |
| 42 | - '.equ', '.set', '.byte', '.hword', '.word', '.quad', | |
| 43 | - '.req', '.aarch64', | |
| 44 | - ], | |
| 45 | - ]; | |
| 46 | - | |
| 47 | - public function __construct() { | |
| 48 | - global $colorPalette; | |
| 49 | - | |
| 50 | - $this->colors = [ | |
| 51 | - 'comment' => $colorPalette['dark']['green'], | |
| 52 | - 'directive' => $colorPalette['dark']['pink'], | |
| 53 | - 'label' => $colorPalette['light']['yellow'], | |
| 54 | - 'number' => $colorPalette['light']['green'], | |
| 55 | - 'operator' => $colorPalette['dark']['blue'], | |
| 56 | - 'register' => $colorPalette['dark']['cyan'], | |
| 57 | - 'string' => $colorPalette['medium']['orange'], | |
| 58 | - ]; | |
| 59 | - } | |
| 60 | - | |
| 61 | - public function getColors(): array { return $this->colors; } | |
| 62 | - public function getComment(): ?string { return $this->comment; } | |
| 63 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 64 | - public function getTokens(): array { return $this->tokens; } | |
| 65 | -} |
削除
src/Site/Lib/Syntax/AsmMips.php
| @@ -1,40 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmMips implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 64ビット | |
| 14 | - // 32ビット | |
| 15 | - ], | |
| 16 | - 'operator' => [ | |
| 17 | - ], | |
| 18 | - 'directive' => [ | |
| 19 | - ], | |
| 20 | - ]; | |
| 21 | - | |
| 22 | - public function __construct() { | |
| 23 | - global $colorPalette; | |
| 24 | - | |
| 25 | - $this->colors = [ | |
| 26 | - 'comment' => $colorPalette['dark']['green'], | |
| 27 | - 'directive' => $colorPalette['dark']['pink'], | |
| 28 | - 'label' => $colorPalette['light']['yellow'], | |
| 29 | - 'number' => $colorPalette['light']['green'], | |
| 30 | - 'operator' => $colorPalette['dark']['blue'], | |
| 31 | - 'register' => $colorPalette['dark']['cyan'], | |
| 32 | - 'string' => $colorPalette['medium']['orange'], | |
| 33 | - ]; | |
| 34 | - } | |
| 35 | - | |
| 36 | - public function getColors(): array { return $this->colors; } | |
| 37 | - public function getComment(): ?string { return $this->comment; } | |
| 38 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | - public function getTokens(): array { return $this->tokens; } | |
| 40 | -} |
削除
src/Site/Lib/Syntax/AsmPower.php
| @@ -1,40 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmPower implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 64ビット | |
| 14 | - // 32ビット | |
| 15 | - ], | |
| 16 | - 'operator' => [ | |
| 17 | - ], | |
| 18 | - 'directive' => [ | |
| 19 | - ], | |
| 20 | - ]; | |
| 21 | - | |
| 22 | - public function __construct() { | |
| 23 | - global $colorPalette; | |
| 24 | - | |
| 25 | - $this->colors = [ | |
| 26 | - 'comment' => $colorPalette['dark']['green'], | |
| 27 | - 'directive' => $colorPalette['dark']['pink'], | |
| 28 | - 'label' => $colorPalette['light']['yellow'], | |
| 29 | - 'number' => $colorPalette['light']['green'], | |
| 30 | - 'operator' => $colorPalette['dark']['blue'], | |
| 31 | - 'register' => $colorPalette['dark']['cyan'], | |
| 32 | - 'string' => $colorPalette['medium']['orange'], | |
| 33 | - ]; | |
| 34 | - } | |
| 35 | - | |
| 36 | - public function getColors(): array { return $this->colors; } | |
| 37 | - public function getComment(): ?string { return $this->comment; } | |
| 38 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | - public function getTokens(): array { return $this->tokens; } | |
| 40 | -} |
削除
src/Site/Lib/Syntax/AsmRiscv.php
| @@ -1,41 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmRiscV implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 128ビット | |
| 14 | - // 64ビット | |
| 15 | - // 32ビット | |
| 16 | - ], | |
| 17 | - 'operator' => [ | |
| 18 | - ], | |
| 19 | - 'directive' => [ | |
| 20 | - ], | |
| 21 | - ]; | |
| 22 | - | |
| 23 | - public function __construct() { | |
| 24 | - global $colorPalette; | |
| 25 | - | |
| 26 | - $this->colors = [ | |
| 27 | - 'comment' => $colorPalette['dark']['green'], | |
| 28 | - 'directive' => $colorPalette['dark']['pink'], | |
| 29 | - 'label' => $colorPalette['light']['yellow'], | |
| 30 | - 'number' => $colorPalette['light']['green'], | |
| 31 | - 'operator' => $colorPalette['dark']['blue'], | |
| 32 | - 'register' => $colorPalette['dark']['cyan'], | |
| 33 | - 'string' => $colorPalette['medium']['orange'], | |
| 34 | - ]; | |
| 35 | - } | |
| 36 | - | |
| 37 | - public function getColors(): array { return $this->colors; } | |
| 38 | - public function getComment(): ?string { return $this->comment; } | |
| 39 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 40 | - public function getTokens(): array { return $this->tokens; } | |
| 41 | -} |
削除
src/Site/Lib/Syntax/AsmX8664.php
| @@ -1,56 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmX8664 implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 64ビット | |
| 14 | - 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'r8', 'r8d', 'r9', 'r9d', 'r10', 'r10d', 'r11', 'r11d', 'r12', 'r12d', 'r13', 'r13d', 'r14', 'r14d', 'r15', 'r15d', | |
| 15 | - // 32ビット | |
| 16 | - 'eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', | |
| 17 | - // 16ビット | |
| 18 | - 'al', 'bl', 'cl', 'dl', | |
| 19 | - 'ah', 'bh', 'ch', 'dh', | |
| 20 | - 'spl', 'bpl', 'sil', 'dil', | |
| 21 | - ], | |
| 22 | - 'operator' => [ | |
| 23 | - 'mov', 'lea', | |
| 24 | - 'add', 'sub', 'div', 'imul', 'dec', 'inc', | |
| 25 | - 'xor', 'or', 'and', | |
| 26 | - 'js', 'jmp', 'jz', 'jnz', 'ja', 'jb', | |
| 27 | - 'pop', 'push', | |
| 28 | - 'call', 'ret', 'syscall', 'cmp', 'test', 'stosb', | |
| 29 | - ], | |
| 30 | - 'directive' => [ | |
| 31 | - 'db', 'dw', 'dd', 'dw', | |
| 32 | - 'equ', 'global', 'extern', | |
| 33 | - 'resb', 'resw', 'resd', 'resq', | |
| 34 | - ], | |
| 35 | - ]; | |
| 36 | - | |
| 37 | - public function __construct() { | |
| 38 | - global $colorPalette; | |
| 39 | - | |
| 40 | - $this->colors = [ | |
| 41 | - 'comment' => $colorPalette['dark']['green'], | |
| 42 | - 'directive' => $colorPalette['dark']['pink'], | |
| 43 | - 'label' => $colorPalette['light']['yellow'], | |
| 44 | - 'number' => $colorPalette['light']['green'], | |
| 45 | - 'operator' => $colorPalette['dark']['blue'], | |
| 46 | - 'register' => $colorPalette['dark']['cyan'], | |
| 47 | - 'string' => $colorPalette['medium']['orange'], | |
| 48 | - ]; | |
| 49 | - } | |
| 50 | - | |
| 51 | - public function getColors(): array { return $this->colors; } | |
| 52 | - public function getComment(): ?string { return $this->comment; } | |
| 53 | - public function getInclude(array $code): ?array { return null; } | |
| 54 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 55 | - public function getTokens(): array { return $this->tokens; } | |
| 56 | -} |
削除
src/Site/Lib/Syntax/AsmZ80.php
| @@ -1,66 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class AsmZ80 implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = ';'; | |
| 9 | - private ?array $multiline = null; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'register' => [ | |
| 13 | - // 16ビット | |
| 14 | - "bc'", "de'", "hl'", | |
| 15 | - 'bc', 'de', 'hl', | |
| 16 | - 'ix' /*指標レジスター*/, 'iy' /*指標レジスター*/, | |
| 17 | - 'sp' /*スタックポインター*/, 'pc' /*プログラムカウンター*/, | |
| 18 | - // 8ビット | |
| 19 | - 'a', 'b', 'd', 'h', 'f', 'c', 'e', 'l', | |
| 20 | - "a'", "b'", "d'", "h'", "f'", "c'", "e'", "l'", | |
| 21 | - 'a', 'b', 'd', 'h', 'f', 'c', 'e', 'l', | |
| 22 | - ], | |
| 23 | - 'operator' => [ | |
| 24 | - // Z80 + GBZ80 + eZ80 | |
| 25 | - 'adc', 'add', 'and', 'bit', 'call', 'ccf', 'cp', 'daa', 'dec', 'di', 'djnz', 'ei', | |
| 26 | - 'halt', 'im0', 'im1', 'im2', 'im', 'inc', 'in', 'jp', 'jr', 'ldd', | |
| 27 | - 'nop', 'otir', 'otdr', 'or', 'pop', 'push', 'retn', 'reti', 'ret', 'res', | |
| 28 | - 'rst', 'rrc', 'rr', 'rdc', 'rl', 'sub', 'srl', 'sra', 'sla', 'set', 'scf', 'sbc', 'xor', | |
| 29 | - // Z80 + GBZ80のみ | |
| 30 | - 'sll', 'ld', | |
| 31 | - // eZ80のみ | |
| 32 | - 'ind2', 'indmr', 'indrx', 'indm', 'inimr', 'inim', 'ini2r', 'ini2', 'lea', 'mlt', | |
| 33 | - 'otirx', 'otimr', 'otim', 'oti2r', 'otdrx', 'otdmr', 'otdm', 'otd2r', 'out0', 'outd2', | |
| 34 | - 'outi2', 'pea', 'rsmix', 'slp', 'stmix', 'tstio', 'tst', '.is', 'xxx.S', 'xxx.L', '.il', '.sis', '.lil', | |
| 35 | - // Z80のみ | |
| 36 | - 'cpdr', 'cpir', 'cpd', 'cpi', 'exx', 'ex', 'outd', 'inir', 'ini', 'indr', 'ind', 'lddr', 'ldir', 'ldi', | |
| 37 | - 'neg', 'outi', 'outdi', 'out', 'rld', 'rrd', | |
| 38 | - // GBZ80のみ | |
| 39 | - 'outd', 'stop', 'swap', | |
| 40 | - ], | |
| 41 | - 'directive' => [ | |
| 42 | - // 'db', 'dw', 'dd', 'dw', | |
| 43 | - // 'equ', 'global', 'extern', | |
| 44 | - // 'resb', 'resw', 'resd', 'resq', | |
| 45 | - ], | |
| 46 | - ]; | |
| 47 | - | |
| 48 | - public function __construct() { | |
| 49 | - global $colorPalette; | |
| 50 | - | |
| 51 | - $this->colors = [ | |
| 52 | - 'comment' => $colorPalette['dark']['green'], | |
| 53 | - 'directive' => $colorPalette['dark']['pink'], | |
| 54 | - 'label' => $colorPalette['light']['yellow'], | |
| 55 | - 'number' => $colorPalette['light']['green'], | |
| 56 | - 'operator' => $colorPalette['dark']['blue'], | |
| 57 | - 'register' => $colorPalette['dark']['cyan'], | |
| 58 | - 'string' => $colorPalette['medium']['orange'], | |
| 59 | - ]; | |
| 60 | - } | |
| 61 | - | |
| 62 | - public function getColors(): array { return $this->colors; } | |
| 63 | - public function getComment(): ?string { return $this->comment; } | |
| 64 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 65 | - public function getTokens(): array { return $this->tokens; } | |
| 66 | -} |
削除
src/Site/Lib/Syntax/C.php
| @@ -1,277 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class C implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = '//'; | |
| 9 | - private array $multiline = ['/*', '*/']; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'keyword' => [ | |
| 13 | - // C23 | |
| 14 | - 'alignas', 'alignof', 'bool', 'constexpr', 'false', 'nullptr', 'static_assert', 'thread_local', 'true', 'typeof', 'typeof_unqual', | |
| 15 | - '_BitInt', '_Decimal128', '_Decimal32', '_Decimal64', | |
| 16 | - // C11 | |
| 17 | - '_Alignas', '_Alignof', '_Atomic', '_Generic', '_Noreturn', '_Static_assert', '_Thread_local', | |
| 18 | - // C99 | |
| 19 | - 'inline', 'restrict', '_Bool', '_Complex', '_Imaginary', | |
| 20 | - // C89 | |
| 21 | - 'asm', 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extern', 'float', 'fortran', 'for', 'goto', 'if', 'inline', | |
| 22 | - 'int', 'long', 'noreturn', 'register', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', | |
| 23 | - // ISO | |
| 24 | - 'FILE', | |
| 25 | - ], | |
| 26 | - 'libc' => [ | |
| 27 | - // ISO C | |
| 28 | - 'assert.h' => [ // C89 | |
| 29 | - 'assert', | |
| 30 | - ], | |
| 31 | - 'complex.h' => [ // C99 | |
| 32 | - 'cabs', 'cabsf', 'cabsl', 'cacos', 'cacosf', 'cacosl', 'cacosh', 'cacoshf', 'cacoshl', 'carg', 'cargf', 'cargl', 'casin', 'casinf', 'casinl', 'casinh', 'casinhf', 'casinhl', 'catan', 'catanf', 'catanl', 'catanh', 'catanhf', 'catanhl', 'ccos', 'ccosf', 'ccosl', 'ccosh', 'ccoshf', 'ccoshl', 'cexp', 'cexpf', 'cexpl', | |
| 33 | - 'cimag', 'cimagf', 'cimagl', 'clog', 'clogf', 'clogl', 'conj', 'conjf', 'conjl', 'cpow', 'cpowf', 'cpowl', 'cproj', 'cprojf', 'cprojl', 'creal', 'crealf', 'creall', | |
| 34 | - 'csqrt', 'csqrtf', 'csqrtl', 'csin', 'csinf', 'csinl', 'csinh', 'csinhf', 'csinhl', 'ctan', 'ctanf', 'ctanl', 'ctanh', 'ctanhf', 'ctanhl', | |
| 35 | - ], | |
| 36 | - 'ctype.h' => [ // C89 | |
| 37 | - 'isalnum', 'isalpha', 'isblank', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper', 'isxdigit', 'tolower', 'toupper', | |
| 38 | - ], | |
| 39 | - 'errno.h' => [ // C89 | |
| 40 | - 'errno', | |
| 41 | - ], | |
| 42 | - 'fenv.h' => [ // C99 | |
| 43 | - 'feclearexcept', 'fegetround', 'fegetenv', 'fegetexpectflag', 'feholdexcept', 'feraiseexcept', 'fesetround', 'fesetenv', 'fesetexpectflag', 'fetestexcept', | |
| 44 | - 'feupdateenv', | |
| 45 | - // __STDC_IEC_60559_DFP__ | |
| 46 | - 'fe_dec_getround', 'fe_dec_setround', | |
| 47 | - ], | |
| 48 | - 'inttypes.h' => [ // C99 | |
| 49 | - 'imaxabs', 'imaxdiv', 'strtoimax', 'strtoumax', 'wcstoimax', 'wcstoumax', | |
| 50 | - ], | |
| 51 | - 'locale.h' => [ // C89 | |
| 52 | - 'localeconv', 'setlocale', | |
| 53 | - ], | |
| 54 | - 'math.h' => [ // C89 | |
| 55 | - 'acos', 'acosf', 'acosl', 'acosh', 'acoshf', 'acoshl', 'asin', 'asinf', 'asinl', 'asinh', 'asinhf', 'asinhl', 'atan', 'atanf', 'atanl', 'atanh', 'atanhf', 'atanhl', | |
| 56 | - 'atan2', 'atan2f', 'atan2l', 'cbrt', 'cbrtf', 'cbrtl', 'ceil', 'ceilf', 'ceill', 'copysign', 'copysignf', 'copysignl', 'cos', 'cosf', 'cosl', 'erf', 'erff', 'erfl', | |
| 57 | - 'erfc', 'erfcf', 'erfcl', 'exp', 'expf', 'expl', 'exp2', 'exp2f', 'exp2l', 'expml', 'expmlf', 'expmll', 'fabs', 'fabsf', 'fabsl', 'fdim', 'fdimf', 'fdiml', | |
| 58 | - 'floor', 'floorf', 'floorl', 'fma', 'fmaf', 'fmal', 'fmax', 'fmaxf', 'fmaxl', 'fmin', 'fminf', 'fminl', 'fpclassify', 'frexp', 'frexpf', 'frexpl', | |
| 59 | - 'hypot', 'hypotf', 'hypotl', 'ilogb', 'ilogbf', 'ilogbl', 'isfinite', 'isgreater', 'isgreaterequal', 'isinf', 'isless', 'islessequal', 'islessgreater', 'isnan', | |
| 60 | - 'isnormal', 'isunordered', 'ldexp', 'ldexpf', 'ldexpl', 'lgamma', 'lgammaf', 'lgammal', 'log', 'logf', 'logl', 'log10', 'log10f', 'log10l', 'log2', 'log2f', 'log2l', | |
| 61 | - 'logb', 'logbf', 'logbl', 'loglp', 'loglpf', 'loglpl', 'lrint', 'lrintf', 'lrintl', 'llrint', 'llrintf', 'llrintl', 'lround', 'lroundf', 'lroundl', 'llround', 'llroundf', 'llroundl', 'modf', 'modff', 'modfl', 'nan', 'nanf', 'nanl', 'nearbyint', 'nearbyintf', 'nearbyintl', 'nextafter', 'nextafterf', 'nextafterl', | |
| 62 | - 'nexttoward', 'nexttowardf', 'nexttowardl', 'pow', 'powf', 'powl', 'remainder', 'remainderf', 'remainderl', 'remquo', 'remquof', 'remquol', 'rint', 'rintf', 'rintl', | |
| 63 | - 'round', 'roundf', 'roundl', 'scalbn', 'scalbnf', 'scalbnl', 'scalbln', 'scalblnf', 'scalblnl', 'signbit', 'sin', 'sinf', 'sinl', 'sinh', 'sinhf', 'sinhl', | |
| 64 | - 'sqrt', 'sqrtf', 'sqrtl', 'tan', 'tanf', 'tanl', 'tanh', 'tanhf', 'tanhl', 'tgamma', 'tgammaf', 'tgammal', 'trunc', 'truncf', 'truncl', | |
| 65 | - ], | |
| 66 | - 'setjmp.h' => [ // C89 | |
| 67 | - 'longjmp', 'setjmp', | |
| 68 | - ], | |
| 69 | - 'signal.h' => [ // C89 | |
| 70 | - 'raise', 'signal', | |
| 71 | - ], | |
| 72 | - 'stdarg.h' => [ // C89 | |
| 73 | - 'va_arg', 'va_copy', 'va_end', 'va_start', | |
| 74 | - ], | |
| 75 | - 'stdatomic.h' => [ // C11 | |
| 76 | - 'atomic_fetch_add', 'atomic_fetch_add_explicit', 'atomic_fetch_and', 'atomic_fetch_and_explicit', 'atomic_fetch_or', 'atomic_fetch_or_explicit', | |
| 77 | - 'atomic_fetch_sub', 'atomic_fetch_sub_explicit', 'atomic_fetch_xor', 'atomic_fetch_xor_explicit', 'atomic_flag', 'atomic_flag_clear', 'atomic_flag_clear_explicit', | |
| 78 | - 'atomic_flag_test_and_set', 'atomic_flag_test_and_set_explicit', 'atomic_init', 'atomic_is_lock_free', 'atomic_load', 'atomic_load_explicit', 'atomic_signal_fence', | |
| 79 | - 'atomic_store', 'atomic_store_explicit', 'atomic_thread_fence', 'kill_dependency', 'memory_order', | |
| 80 | - ], | |
| 81 | - 'stdbit.h' => [ // C23 | |
| 82 | - 'stdc_bit_ceil_uc', 'stdc_bit_ceil_us', 'stdc_bit_ceil_ui', 'stdc_bit_ceil_ul', 'stdc_bit_ceil_ull', 'stdc_bit_ceil', | |
| 83 | - 'stdc_bit_floor_uc', 'stdc_bit_floor_us', 'stdc_bit_floor_ui', 'stdc_bit_floor_ul', 'stdc_bit_floor_ull', 'stdc_bit_floor', | |
| 84 | - 'stdc_bit_width_uc', 'stdc_bit_width_us', 'stdc_bit_width_ui', 'stdc_bit_width_ul', 'stdc_bit_width_ull', 'stdc_bit_width', | |
| 85 | - 'stdc_count_ones_uc', 'stdc_count_ones_us', 'stdc_count_ones_ui', 'stdc_count_ones_ul', 'stdc_count_ones_ull', 'stdc_count_ones', | |
| 86 | - 'stdc_count_zeros_uc', 'stdc_count_zeros_us', 'stdc_count_zeros_ui', 'stdc_count_zeros_ul', 'stdc_count_zeros_ull', 'stdc_count_zeros', | |
| 87 | - 'stdc_first_leading_ones_uc', 'stdc_first_leading_ones_us', 'stdc_first_leading_ones_ui', 'stdc_first_leading_ones_ul', 'stdc_first_leading_ones_ull', | |
| 88 | - 'stdc_first_leading_ones', | |
| 89 | - 'stdc_first_leading_zeros_uc', 'stdc_first_leading_zeros_us', 'stdc_first_leading_zeros_ui', 'stdc_first_leading_zeros_ul', 'stdc_first_leading_zeros_ull', | |
| 90 | - 'stdc_first_leading_zeros', | |
| 91 | - 'stdc_first_trailing_ones_uc', 'stdc_first_trailing_ones_us', 'stdc_first_trailing_ones_ui', 'stdc_first_trailing_ones_ul', 'stdc_first_trailing_ones_ull', | |
| 92 | - 'stdc_first_trailing_ones', | |
| 93 | - 'stdc_first_trailing_zeros_uc', 'stdc_first_trailing_zeros_us', 'stdc_first_trailing_zeros_ui', 'stdc_first_trailing_zeros_ul', 'stdc_first_trailing_zeros_ull', | |
| 94 | - 'stdc_first_trailing_zeros', | |
| 95 | - 'stdc_has_single_bit_uc', 'stdc_has_single_bit_us', 'stdc_has_single_bit_ui', 'stdc_has_single_bit_ul', 'stdc_has_single_bit_ull', 'stdc_has_single_bit', | |
| 96 | - 'stdc_leading_ones_uc', 'stdc_leading_ones_us', 'stdc_leading_ones_ui', 'stdc_leading_ones_ul', 'stdc_leading_ones_ull', 'stdc_leading_ones', | |
| 97 | - 'stdc_leading_zeros_uc', 'stdc_leading_zeros_us', 'stdc_leading_zeros_ui', 'stdc_leading_zeros_ul', 'stdc_leading_zeros_ull', 'stdc_leading_zeros', | |
| 98 | - 'stdc_trailing_ones_uc', 'stdc_trailing_ones_us', 'stdc_trailing_ones_ui', 'stdc_trailing_ones_ul', 'stdc_trailing_ones_ull', 'stdc_trailing_ones', | |
| 99 | - 'stdc_trailing_zeros_uc', 'stdc_trailing_zeros_us', 'stdc_trailing_zeros_ui', 'stdc_trailing_zeros_ul', 'stdc_trailing_zeros_ull', 'stdc_trailing_zeros', | |
| 100 | - 'stdc_rotate_left_uc', 'stdc_rotate_left_us', 'stdc_rotate_left_ui', 'stdc_rotate_left_ul', 'stdc_rotate_left_ull', 'stdc_rotate_left', | |
| 101 | - 'stdc_rotate_right_uc', 'stdc_rotate_right_us', 'stdc_rotate_right_ui', 'stdc_rotate_right_ul', 'stdc_rotate_right_ull', 'stdc_rotate_right', | |
| 102 | - ], | |
| 103 | - 'stdckdint.h' => [ // C23 | |
| 104 | - 'ckd_add', 'ckd_sub', 'ckd_mul', | |
| 105 | - ], | |
| 106 | - 'stdio.h' => [ // C89 | |
| 107 | - 'clearerr', 'fclose', 'feof', 'ferror', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen', 'fscanf', 'fseek', | |
| 108 | - 'fseekpos', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'perror', 'printf', 'putc', 'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', | |
| 109 | - 'setvbuf', 'snprintf', 'sprintf', 'sscanf', 'tmpfile', 'tmpnam', 'ungetc', 'vfprintf', 'vfscanf', 'vprintf', 'vscanf', 'vsnprintf', 'vsprintf', 'vsscanf', | |
| 110 | - // __STDC_WANT_LIB_EXT1__ | |
| 111 | - 'fopen_s', 'fprintf_s', 'freopen_s', 'fscanf_s', 'gets_s', 'printf_s', 'scanf_s', 'snprintf_s', 'sprintf_s', 'sscanf_s', 'tmpfile_s', 'tmpnam_s', | |
| 112 | - 'vfprintf_s', 'vfscanf_s', 'vprintf_s', 'vscanf_s', 'vsnprintf_s', 'vsprintf_s', 'vsscanf_s', | |
| 113 | - ], | |
| 114 | - 'stdlib.h' => [ // C89 | |
| 115 | - 'abort', 'abs', 'aligned_alloc', 'at_quick_exit', 'atexit', 'atof', 'atoi', 'atol', 'atoll', 'bsearch', 'calloc', 'div', 'exit', '_Exit', 'free', 'free_sized', | |
| 116 | - 'free_aligned_sized', 'labs', 'llabs', 'ldiv', 'lldiv', 'malloc', 'mblen', 'mbtowc', 'mbstowcs', 'memalignment', 'qsort', 'quick_exit', 'rand', 'realloc', 'srand', | |
| 117 | - 'strfromd', 'strfromf', 'strfroml', 'strtod', 'strtof', 'strtold', 'strtol', 'strtoll', 'strtoul', 'strtoull', 'system', 'wctomb', 'wcstombs', | |
| 118 | - // __STDC_IEC_60559_DFP__ | |
| 119 | - 'strfromd32', 'strfromd64', 'strfromd128', 'strtod32', 'strtod64', 'strtod128', | |
| 120 | - // __STDC_WANT_IEC_60559_TYPES_EXT__ | |
| 121 | - 'strfromdN', 'strfromdNx', 'strfromencfN', 'strfromencdecdN', 'strfromencbindN', 'strfromfN', 'strfromfNx', 'strtodN', 'strtodNx', 'strtofN', 'strtofNx', | |
| 122 | - 'strtoencfN', 'strtoencdecdN', 'strtoencbindN', | |
| 123 | - // __STDC_WANT_LIB_EXT1__ | |
| 124 | - 'abort_handler_s', 'bsearch_s', 'getenv_s', 'ignore_handler_s', 'mbstowcs_s', 'qsort_s', 'set_constraint_handler_s', 'wcstomb_s', 'wcstombs_s', | |
| 125 | - ], | |
| 126 | - 'stdmchar.h' => [ // C29 | |
| 127 | - 'stdc_mcnrtoc16n', 'stdc_mcnrtoc32n', 'stdc_mcnrtoc8n', 'stdc_mcnrtomcn', 'stdc_mcnrtomwcn', 'stdc_mcsnrtoc16sn', 'stdc_mcsnrtoc32sn', 'stdc_mcsnrtoc8sn', | |
| 128 | - 'stdc_mwcnrtomcn', 'stdc_mcsnrtomcsn', 'stdc_mwcnrtomwcn', 'stdc_mcsnrtomwcsn', 'stdc_mwcnrtoc16n', 'stdc_mwcnrtoc32n', 'stdc_mwcnrtoc8n', 'stdc_mwcsnrtoc16sn', | |
| 129 | - 'stdc_mwcsnrtoc32sn', 'stdc_mwcsnrtoc8sn', 'stdc_mwcsnrtomcsn', 'stdc_mwcsnrtomwcsn', 'stdc_c16nrtoc16n', 'stdc_c16nrtoc32n', 'stdc_c16nrtoc8n', 'stdc_c16nrtomcn', | |
| 130 | - 'stdc_c16nrtomwcn', 'stdc_c16snrtoc8sn', 'stdc_c16snrtoc16sn', 'stdc_c16snrtoc32sn', 'stdc_c16snrtomwcsn', 'stdc_c16snrtomcsn', 'stdc_c32nrtoc16n', 'stdc_c32nrtoc32n', | |
| 131 | - 'stdc_c32nrtoc8n', 'stdc_c32nrtomcn', 'stdc_c32nrtomwcn', 'stdc_c32snrtoc16sn', 'stdc_c32snrtoc32sn', 'stdc_c32snrtoc8sn', 'stdc_c32snrtomcsn', 'stdc_c32snrtomwcsn', | |
| 132 | - 'stdc_c8nrtoc16n', 'stdc_c8nrtoc32n', 'stdc_c8nrtoc8n', 'stdc_c8nrtomcn', 'stdc_c8nrtomwcn', 'stdc_c8snrtoc16sn', 'stdc_c8snrtoc32sn', 'stdc_c8snrtoc8sn', 'stdc_c8snrtomwcsn', 'stdc_c8snrtomcsn', | |
| 133 | - ], | |
| 134 | - 'string.h' => [ // C89 | |
| 135 | - 'memccpy', 'memchr', 'memcmp', 'memcpy', 'memcpy_s', 'memmove', 'memmove_s', 'memset', 'memset_explicit', 'memset_s', | |
| 136 | - 'strcat', 'strcat_s', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcpy_s', 'strcspn', 'strdup', 'strlen', 'strncat', 'strncat_s', 'strncmp', 'strncpy', 'strncpy_s', | |
| 137 | - 'strndup', 'strnlen_s', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok', 'strtok_s', 'strxfrm', | |
| 138 | - // __STD_WANT_LIB_EXT1__ | |
| 139 | - 'memcpy_s', 'memmove_s', 'memset_s', 'strcat_s', 'strcpy_s', 'strerror_s', 'strerrorlen_s', 'strncat_s', 'strncpy_s', 'strnlen_s', 'strtok_s', | |
| 140 | - ], | |
| 141 | - 'threads.h' => [ // C11 | |
| 142 | - 'call_once', 'cnd_broadcast', 'cnd_destroy', 'cnd_init', 'cnd_signal', 'cnd_timedwait', 'cnd_wait', | |
| 143 | - 'mtx_destroy', 'mtx_init', 'mtx_lock', 'mtx_timedlock', 'mtx_trylock', 'mtx_unlock', | |
| 144 | - 'thrd_create', 'thrd_current', 'thrd_detach', 'thrd_equal', 'thrd_exit', 'thrd_join', 'thrd_sleep', 'thrd_yield', 'tss_create', 'tss_delete', 'tss_get', 'tss_set', | |
| 145 | - ], | |
| 146 | - 'time.h' => [ // C89 | |
| 147 | - 'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'gmtime_r', 'localtime', 'localtime_r', 'mktime', | |
| 148 | - 'strftime', 'time', 'timegm', | |
| 149 | - // __STD_WANT_LIB_EXT1__ | |
| 150 | - 'asctime_s', 'ctime_s', 'gmtime_s', 'localtime_s', | |
| 151 | - ], | |
| 152 | - 'uchar.h' => [ // C11 | |
| 153 | - 'c8rtomb', 'c16rtomb', 'c32rtomb', 'mbrtoc8', 'mbrtoc16', 'mbrtoc32', | |
| 154 | - ], | |
| 155 | - 'wchar.h' => [ // C95 | |
| 156 | - 'wcscat', 'wcscat_s', 'wcschr', 'wcscmp', 'wcscpy', 'wcscpy_s', 'wcscroll', 'wcscroll', 'wcscspn', 'wcslen', 'wcsncpy', 'wcsncpy_s', 'wcsncat', 'wcsncat_s', | |
| 157 | - 'wcsncmp', 'wcsnlen_s', 'wcsrchr', 'wcspbrk', 'wcsstr', 'wcstok', 'wcstok_s', 'wcsxfrm', | |
| 158 | - 'wmemchr', 'wmemcmp', 'wmemcpy', 'wmemcpy_s', 'wmemmove', 'wmemmove_s', 'wmemset', | |
| 159 | - // __STDC_IEC_60559_DFP__ | |
| 160 | - 'wcstod32', 'wcstod64', 'wcstod128', | |
| 161 | - // __STDC_IEC_60559_TYPES__ | |
| 162 | - 'encptr', 'nptr', | |
| 163 | - // __STDC_WANT_LIB_EXT1__ | |
| 164 | - 'fwprintf_s', 'fwscanf_s', 'mvsrtowcs_s', 'snwprinf_s', 'swprintf_s', 'swscanf_s', 'vfwprintf_s', 'vfwscanf_s', 'vsnwprinf_s', 'vswprintf_s', 'vswscanf_s', | |
| 165 | - 'wcrtomb_s', 'wcscat_s', 'wcsncat_s', 'wcstok_s', 'wcsnlen_s', 'wcscpy_s', 'wcsncpy_s', 'wcsrtombs_s', 'wmemcpy_s', 'wmemmove_s', 'wprintf_s', 'wscanf_s', | |
| 166 | - ], | |
| 167 | - 'wctype.h' => [ // C95 | |
| 168 | - 'iswalnum', 'iswalpha', 'iswblank', 'iswcntrl', 'iswctype', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace', 'iswupper', 'iswxdigit', | |
| 169 | - 'towctrans', 'towlower', 'towupper', | |
| 170 | - 'wctrans', 'cwtype', | |
| 171 | - ], | |
| 172 | - ], | |
| 173 | - 'preprocessor' => [ | |
| 174 | - // C23 | |
| 175 | - '#elifdef', '#elifndef', '#embed', '#warning', '__has_include', '__has_embed', '__has_c_attribute', | |
| 176 | - // C99 | |
| 177 | - '#_Pragma', | |
| 178 | - // C89 | |
| 179 | - '#if', '#elif', '#else', '#endif', '#ifdef', '#ifndef', '#define', '#undef', '#include', '#line', '#error', '#pragma', '#defined', | |
| 180 | - ], | |
| 181 | - 'typedef' => [ | |
| 182 | - 'errno.h' => [ | |
| 183 | - 'errno_t', | |
| 184 | - ], | |
| 185 | - 'inttypes.h' => [ | |
| 186 | - 'intmax_t', 'uintmax_t', 'imaxdiv_t', | |
| 187 | - ], | |
| 188 | - 'iso646.h' => [ | |
| 189 | - 'and'/*&&*/, 'and_eq'/*&=*/, 'bitand'/*&*/, 'bitor'/*|*/, 'compl'/*~*/, 'not'/*!*/, 'not_eq'/*!=*/, 'or'/*||*/, 'or_eq',/*|=*/ 'xor'/*^*/, 'xor_eq'/*^=*/, | |
| 190 | - ], | |
| 191 | - 'setjmp.h' => [ | |
| 192 | - 'jmp_buf', | |
| 193 | - ], | |
| 194 | - 'signal.h' => [ | |
| 195 | - 'sig_atomic_t', | |
| 196 | - ], | |
| 197 | - 'stdatomic.h' => [ | |
| 198 | - 'atomic_bool', 'atomic_char', 'atomic_schar', 'atomic_uchar', 'atomic_short', 'atomic_ushort', 'atomic_int', 'atomic_uint', 'atomic_long', 'atomic_ulong', | |
| 199 | - 'atomic_llong', 'atomic_ullong', 'atomic_char8_t', 'atomic_char16_t', 'atomic_char32_t', 'atomic_wchar_t', 'atomic_int_least8_t', 'atomic_uint_least8_t', | |
| 200 | - 'atomic_int_least16_t', 'atomic_uint_least16_t', 'atomic_int_least32_t', 'atomic_uint_least32_t', 'atomic_int_least64_t', 'atomic_uint_least64_t', 'atomic_int_fast8_t', | |
| 201 | - 'atomic_uint_fast8_t', 'atomic_int_fast16_t', 'atomic_uint_fast16_t', 'atomic_int_fast32_t', 'atomic_uint_fast32_t', 'atomic_int_fast64_t', 'atomic_uint_fast64_t', | |
| 202 | - 'atomic_intptr_t', 'atomic_uintptr_t', 'atomic_size_t', 'atomic_ptrdiff_t', 'atomic_intmax_t', 'atomic_uintmax_t', | |
| 203 | - ], | |
| 204 | - 'stddef.h' => [ | |
| 205 | - 'max_align_t', 'nullptr', 'ptrdiff_t', 'size_t', | |
| 206 | - ], | |
| 207 | - 'stdio.h' => [ | |
| 208 | - 'FILE', 'size_t', 'fpos_t', | |
| 209 | - ], | |
| 210 | - 'stdint.h' => [ | |
| 211 | - 'int8_t', 'int16_t', 'int32_t', 'int64_t', | |
| 212 | - 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', | |
| 213 | - 'fpos_t', 'intptr_t', 'intmax_t', 'uintptr_t', 'uintmax_t', | |
| 214 | - 'int_least8_t', 'int_least16_t', 'int_least32_t', 'int_least64_t', | |
| 215 | - 'uint_least8_t', 'uint_least16_t', 'uint_least32_t', 'uint_least64_t', | |
| 216 | - 'int_fast8_t', 'int_fast16_t', 'int_fast32_t', 'int_fast64_t', | |
| 217 | - 'uint_fast8_t', 'uint_fast16_t', 'uint_fast32_t', 'uint_fast64_t', | |
| 218 | - ], | |
| 219 | - 'stdmchar.h' => [ | |
| 220 | - 'stdc_mcerr', | |
| 221 | - ], | |
| 222 | - 'stdarg.h' => [ | |
| 223 | - 'va_list', | |
| 224 | - ], | |
| 225 | - 'threads.h' => [ | |
| 226 | - 'cnd_t', | |
| 227 | - 'mtx_t', | |
| 228 | - 'thrd_t', | |
| 229 | - 'thrd_start_t', | |
| 230 | - 'tss_t', | |
| 231 | - 'tss_dtor_t', | |
| 232 | - ], | |
| 233 | - 'time.h' => [ | |
| 234 | - 'rsize_t', 'clock_t', 'time_t', | |
| 235 | - ], | |
| 236 | - 'uchar.h' => [ | |
| 237 | - 'char8_t', 'char16_t', 'char32_t', 'mbstate_t', | |
| 238 | - ], | |
| 239 | - 'wchar.h' => [ | |
| 240 | - 'wchar_t', 'wint_t', | |
| 241 | - ], | |
| 242 | - 'wctype.h' => [ | |
| 243 | - 'wctrans_t', 'wctype_t', | |
| 244 | - ], | |
| 245 | - ], | |
| 246 | - ]; | |
| 247 | - | |
| 248 | - public function __construct() { | |
| 249 | - global $colorPalette; | |
| 250 | - | |
| 251 | - $this->colors = [ | |
| 252 | - 'comment' => $colorPalette['dark']['green'], | |
| 253 | - 'keyword' => $colorPalette['dark']['blue'], | |
| 254 | - 'number' => $colorPalette['light']['green'], | |
| 255 | - 'preprocessor' => $colorPalette['medium']['purple'], | |
| 256 | - 'string' => $colorPalette['medium']['orange'], | |
| 257 | - 'typedef' => $colorPalette['dark']['lime'], | |
| 258 | - 'function' => $colorPalette['light']['yellow'], | |
| 259 | - 'libc' => $colorPalette['light']['yellow'], | |
| 260 | - ]; | |
| 261 | - } | |
| 262 | - | |
| 263 | - public function getColors(): array { return $this->colors; } | |
| 264 | - public function getComment(): ?string { return $this->comment; } | |
| 265 | - public function getInclude(array $code): ?array { | |
| 266 | - $headers = []; | |
| 267 | - foreach ($code as $c) { | |
| 268 | - $c = htmlspecialchars($c); | |
| 269 | - if (!str_starts_with($c, '#include')) continue; | |
| 270 | - $header = str_replace(['#include ', '<', '>', '"', '''], '', $c); | |
| 271 | - $headers[] = trim($header); | |
| 272 | - } | |
| 273 | - return $headers; | |
| 274 | - } | |
| 275 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 276 | - public function getTokens(): array { return $this->tokens; } | |
| 277 | -} |
削除
src/Site/Lib/Syntax/Cpp.php
| @@ -1,66 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class Cpp implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = '//'; | |
| 9 | - private array $multiline = ['/*', '*/']; | |
| 10 | - | |
| 11 | - private array $tokens = [ | |
| 12 | - 'keyword' => [ | |
| 13 | - // C++26 | |
| 14 | - 'contract_assert', 'pre', 'post', 'trivially_reallocate_if_eligible', 'replaceable_if_eligible', | |
| 15 | - // C++20 | |
| 16 | - 'char8_t', 'concept', 'consteval', 'constinit', 'co_await', 'co_return', 'co_yield', 'import', 'module', 'requires', | |
| 17 | - // C++11 | |
| 18 | - 'alignas', 'alignof', 'char16_t', 'char32_t', 'constexpr', 'decltype', 'final', 'noexcept', 'nullptr', 'override', 'static_assert', 'thread_local', | |
| 19 | - // C++99 | |
| 20 | - 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'class', 'compl', 'const', 'const_cast', 'continue', | |
| 21 | - 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', | |
| 22 | - 'int', 'long', 'mutable', 'namespace', 'new', 'not', 'not_eq', 'operator', 'or', 'or_eq', 'private', 'protected', 'public', 'reflexpr', 'register', | |
| 23 | - 'reinterpret_cast', 'return', 'short', 'signed', 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'template', 'this', 'throw', 'true', 'try', | |
| 24 | - 'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq', | |
| 25 | - // TMTS | |
| 26 | - 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'synchronized', 'transaction_safe', 'transaction_safe_dynamic', | |
| 27 | - ], | |
| 28 | - 'preprocessor' => [ | |
| 29 | - // C++26 | |
| 30 | - '#embed', '__has_embed', | |
| 31 | - // C++23 | |
| 32 | - '#elifdef', '#elifndef', '#warning', | |
| 33 | - // C++20 | |
| 34 | - '#__has_cpp_attribute', '#export', '#import', '#module', | |
| 35 | - // C++17 | |
| 36 | - '#__has_include', | |
| 37 | - // C++11 | |
| 38 | - '#_Pragma', | |
| 39 | - // C++99 | |
| 40 | - '#if', '#elif', '#else', '#endif', '#ifdef', '#ifndef', '#define', '#undef', '#include', '#line', '#error', '#pragma', '#defined', | |
| 41 | - ], | |
| 42 | - 'typedef' => [ | |
| 43 | - 'int8_t', 'int16_t', 'int32_t', 'int64_t', | |
| 44 | - 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', | |
| 45 | - 'size_t', | |
| 46 | - ], | |
| 47 | - ]; | |
| 48 | - | |
| 49 | - public function __construct() { | |
| 50 | - global $colorPalette; | |
| 51 | - | |
| 52 | - $this->colors = [ | |
| 53 | - 'comment' => $colorPalette['dark']['green'], | |
| 54 | - 'keyword' => $colorPalette['dark']['blue'], | |
| 55 | - 'number' => $colorPalette['light']['green'], | |
| 56 | - 'preprocessor' => $colorPalette['medium']['purple'], | |
| 57 | - 'string' => $colorPalette['medium']['orange'], | |
| 58 | - 'typedef' => $colorPalette['dark']['lime'], | |
| 59 | - ]; | |
| 60 | - } | |
| 61 | - | |
| 62 | - public function getColors(): array { return $this->colors; } | |
| 63 | - public function getComment(): ?string { return $this->comment; } | |
| 64 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 65 | - public function getTokens(): array { return $this->tokens; } | |
| 66 | -} |
削除
src/Site/Lib/Syntax/Glsl.php
| @@ -1,65 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class Glsl implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = '//'; | |
| 9 | - | |
| 10 | - private array $tokens = [ | |
| 11 | - 'function' => [ | |
| 12 | - 'asin', 'acos', 'atan', 'cos', 'degrees', 'radians', 'sin', 'tan', | |
| 13 | - 'exp', 'exp2', 'inversesqrt', 'log', 'log2', 'pow', 'sqrt', | |
| 14 | - 'abs', 'ceil', 'clamp', 'floor', 'fract', 'max', 'min', 'mix', 'mod', 'sign', 'smoothstep', 'step', | |
| 15 | - 'cross', 'distance', 'dot', 'faceforward', 'ftransform', 'length', 'normalize', 'reflect', 'refract', | |
| 16 | - 'dFdx', 'dFdy', 'fwidth', | |
| 17 | - 'matrixCompMulti', | |
| 18 | - 'all', 'any', 'equal', 'greaterThan', 'greaterThanEqual', 'lessThan', 'lessThanEqual', 'not', 'notEqual', | |
| 19 | - 'texture1D', 'texture1DProj', 'texture2D', 'texture2DProj', 'texture3D', 'texture3DProj', 'textureCube', | |
| 20 | - 'texture1DLod', 'texture1DProjLod', 'texture2DLod', 'texture2DProjLod', 'texture3DProjLod', | |
| 21 | - 'shadow1DLog', 'shadow1DProjLog', 'shadow2DLog', 'shadow2DProjLog', | |
| 22 | - 'noise1', 'noise2', 'noise3', 'noise4', | |
| 23 | - ], | |
| 24 | - 'keyword' => [ | |
| 25 | - 'attribute', 'bool', 'bvec2', 'bvec3', 'bvec4', 'const', 'float', 'genType', 'in', 'int', 'inout', 'ivec2', 'ivec3', 'ivec4', 'mat2', 'mat3', 'mat4', 'out', | |
| 26 | - 'sampler1D', 'sampler1DShadow', 'sampler2D', 'sampler2DShadow', 'sampler3D', 'samplerCube', 'struct', 'uniform', 'varying', 'vec2', 'vec3', 'vec4', 'void', | |
| 27 | - ], | |
| 28 | - 'preprocessor' => [ | |
| 29 | - '#', '#define', '#elif', '#else', '#endif', '#error', '#extension', '#if', '#ifdef', '#ifndef', '#line', '#pragma', '#version', '#undef', | |
| 30 | - '__FILE__', '__LINE__', '__VERSION__', | |
| 31 | - ], | |
| 32 | - 'variables' => [ | |
| 33 | - // Vertex | |
| 34 | - 'gl_ClipVertex', 'gl_PointSize', 'gl_Position', 'gl_Color', 'gl_FogCoord', 'gl_Normal', 'gl_SecondaryColor', 'gl_Vertex', | |
| 35 | - 'gl_MultiTexCoord0', 'gl_MultiTexCoord1', 'gl_MultiTexCoord2', 'gl_MultiTexCoord3', 'gl_MultiTexCoord4', 'gl_MultiTexCoord5', 'gl_MultiTexCoord6', 'gl_MultiTexCoord7', | |
| 36 | - 'gl_BackColor', 'gl_BackSecondaryColor', 'gl_FogFragCoord', 'gl_FrontColor', 'gl_FrontSecondaryColor', 'gl_TexCoord', | |
| 37 | - // Fragment | |
| 38 | - 'gl_FragColor', 'gl_FragCoord', 'gl_FragData', 'gl_FragDepth', 'gl_FrontFacing', 'gl_MaxDrawBuffers', | |
| 39 | - // Constant | |
| 40 | - 'gl_MaxClipPlanes', 'gl_MaxCombinedTextureImageUnits', 'gl_MaxDrawBuffers', 'gl_MaxFragmentUniformComponents', 'gl_MaxLights', 'gl_MaxTextureCoords', | |
| 41 | - 'gl_MaxTextureImageUnits', 'gl_MaxTextureUnits', 'gl_MaxVaryingFloats', 'gl_MaxVertexAttribs', 'gl_MaxVertexUniformComponents', | |
| 42 | - // Uniform | |
| 43 | - 'gl_ClipPlane', 'gl_DepthRange', 'gl_DepthRangeParameters', 'gl_EyePlane', 'gl_Fog', 'gl_FogParameters', 'gl_LightModel', 'gl_LightModelParameters', | |
| 44 | - 'gl_LightModelProducts', 'gl_BackLightModelProduct', 'gl_FrontLightModelProduct', 'gl_LightProducts', 'gl_BackLightProduct', 'gl_FrontLightProduct', | |
| 45 | - 'gl_LightSource', 'gl_LightSourceParameters', 'gl_MaterialParameters', 'gl_BackMaterial', 'gl_FrontMaterial', | |
| 46 | - 'gl_ModelViewMatrix', 'gl_ModelViewMatrixInverse', 'gl_ModelViewMatrixTranspose', 'gl_ModelViewMatrixInverseTranspose', | |
| 47 | - 'gl_ModelViewProjectionMatrix', 'gl_ModelViewProjectionMatrixInverse', 'gl_ModelViewProjectionMatrixTranspose', 'gl_ModelViewProjectionMatrixInverseTranspose', | |
| 48 | - 'gl_ObjectPlane', 'gl_Point', 'gl_PointParameters', | |
| 49 | - 'gl_ProjectionMatrix', 'gl_ProjectionMatrixInverse', 'gl_ProjectionMatrixTranspose', 'gl_ProjectionMatrixInverseTranspose', 'gl_NormalMatrix', 'gl_NormalScale', | |
| 50 | - 'gl_TextureEnvColor', 'gl_TextureMatrix', 'gl_TextureMatrixInverse', 'gl_TextureMatrixTranspose', 'gl_TextureMatrixInverseTranspose', | |
| 51 | - ], | |
| 52 | - ]; | |
| 53 | - | |
| 54 | - public function __construct() { | |
| 55 | - global $colorPalette; | |
| 56 | - | |
| 57 | - $this->colors = [ | |
| 58 | - ]; | |
| 59 | - } | |
| 60 | - | |
| 61 | - public function getColors(): array { return $this->colors; } | |
| 62 | - public function getComment(): ?string { return $this->comment; } | |
| 63 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 64 | - public function getTokens(): array { return $this->tokens; } | |
| 65 | -} |
削除
src/Site/Lib/Syntax/Go.php
| @@ -1,40 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -use Site\Lib\Syntax\SyntaxInterface; | |
| 5 | - | |
| 6 | -class Go implements SyntaxInterface { | |
| 7 | - private array $colors = []; | |
| 8 | - private string $comment = '//'; | |
| 9 | - | |
| 10 | - private array $tokens = [ | |
| 11 | - 'keyword' => [ | |
| 12 | - 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else', 'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface', 'map', | |
| 13 | - 'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var', | |
| 14 | - 'uint8', 'uint16', 'uint32', 'uint64', | |
| 15 | - 'int8', 'int16', 'int32', 'int64', | |
| 16 | - 'float32', 'float64', | |
| 17 | - 'complex64', 'complex128', | |
| 18 | - 'byte', 'rune', 'string', 'bool', | |
| 19 | - 'uint', 'int', 'uintptr', | |
| 20 | - ], | |
| 21 | - 'operator' => [ | |
| 22 | - '+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&^', '<', '>', '=', '!', '~', '...', ',', '.', ';', ':', | |
| 23 | - '{', '}', '(', ')', '[', ']', | |
| 24 | - '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&^=', '!=', '<=', '>=', ';=', | |
| 25 | - '&&', '||', '<-', '++', '--', '==', | |
| 26 | - ], | |
| 27 | - ]; | |
| 28 | - | |
| 29 | - public function __construct() { | |
| 30 | - global $colorPalette; | |
| 31 | - | |
| 32 | - $this->colors = [ | |
| 33 | - ]; | |
| 34 | - } | |
| 35 | - | |
| 36 | - public function getColors(): array { return $this->colors; } | |
| 37 | - public function getComment(): ?string { return $this->comment; } | |
| 38 | - public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | - public function getTokens(): array { return $this->tokens; } | |
| 40 | -} |
削除
src/Site/Lib/Syntax/SyntaxInterface.php
| @@ -1,10 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib\Syntax; | |
| 3 | - | |
| 4 | -interface SyntaxInterface { | |
| 5 | - public function getColors(): array; | |
| 6 | - public function getComment(): ?string; | |
| 7 | - public function getInclude(array $code): ?array; | |
| 8 | - public function getMultiline(): ?array; | |
| 9 | - public function getTokens(): array; | |
| 10 | -} |
削除
src/Site/Lib/Template.php
| @@ -1,243 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -class Template { | |
| 5 | - private string $tmplExt = '.maron'; | |
| 6 | - private string $tmplPath; | |
| 7 | - private array $vars = []; | |
| 8 | - private array $blocks = []; | |
| 9 | - private array $custFunc = []; | |
| 10 | - private array $custCss = []; | |
| 11 | - | |
| 12 | - /** | |
| 13 | - * テンプレートクラスのコンストラクタ | |
| 14 | - * | |
| 15 | - * @param string $tmplPath テンプレートのパス | |
| 16 | - * @return void | |
| 17 | - */ | |
| 18 | - public function __construct(string $tmplPath) { | |
| 19 | - $this->tmplPath = rtrim($tmplPath, '/'); | |
| 20 | - | |
| 21 | - if (substr($this->tmplPath, 0, 1) !== '/') { | |
| 22 | - $this->tmplPath = '/'.$this->tmplPath; | |
| 23 | - } | |
| 24 | - } | |
| 25 | - | |
| 26 | - /** | |
| 27 | - * テンプレート変数に値を割り当てる | |
| 28 | - * | |
| 29 | - * @param string $name 変数名 | |
| 30 | - * @param mixed $value 値 | |
| 31 | - * @return void | |
| 32 | - */ | |
| 33 | - public function assign(string $name, mixed $value): void { | |
| 34 | - $this->vars[$name] = $value; | |
| 35 | - } | |
| 36 | - | |
| 37 | - /** | |
| 38 | - * カスタムCSSファイルを追加する | |
| 39 | - * | |
| 40 | - * @param string $name CSSファイル名 | |
| 41 | - * @return void | |
| 42 | - */ | |
| 43 | - public function addCss(string $name): void { | |
| 44 | - $this->custCss[] = | |
| 45 | - "<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style-{$name}.css\" />\n"; | |
| 46 | - $this->assign('custCss', $this->custCss); | |
| 47 | - } | |
| 48 | - | |
| 49 | - /** | |
| 50 | - * カスタム関数を登録する | |
| 51 | - * | |
| 52 | - * @param string $name 関数名 | |
| 53 | - * @param callable $callback コールバック関数 | |
| 54 | - * @return void | |
| 55 | - */ | |
| 56 | - public function registerFunction(string $name, callable $callback): void { | |
| 57 | - $this->custFunc[$name] = $callback; | |
| 58 | - } | |
| 59 | - | |
| 60 | - /** | |
| 61 | - * テンプレートブロックを定義する | |
| 62 | - * | |
| 63 | - * @param string $name ブロック名 | |
| 64 | - * @param string $content ブロック内容 | |
| 65 | - * @return void | |
| 66 | - */ | |
| 67 | - public function defineBlock(string $name, string $content): void { | |
| 68 | - if (!isset($this->blocks[$name])) | |
| 69 | - $this->blocks[$name] = $content; | |
| 70 | - } | |
| 71 | - | |
| 72 | - /** | |
| 73 | - * テンプレートをレンダリングする | |
| 74 | - * | |
| 75 | - * @param string $tmplName テンプレート名 | |
| 76 | - * @return void | |
| 77 | - */ | |
| 78 | - public function render(string $tmplName): void { | |
| 79 | - $tmplPath = ROOT.'/view'.$this->tmplPath.'/'.$tmplName.$this->tmplExt; | |
| 80 | - if (!file_exists($tmplPath)) | |
| 81 | - throw new \RuntimeException("テンプレートファイルを見つけません:{$tmplPath}"); | |
| 82 | - | |
| 83 | - extract($this->vars); | |
| 84 | - | |
| 85 | - $content = file_get_contents($tmplPath); | |
| 86 | - | |
| 87 | - // インクルードディレクティブを処理 | |
| 88 | - while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) { | |
| 89 | - $content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m): bool|string { | |
| 90 | - $inclPath = ROOT.'/view/'.trim($m[1], "'\" ").$this->tmplExt; | |
| 91 | - if (!file_exists($inclPath)) | |
| 92 | - throw new \RuntimeException("ファイルを見つけません: {$inclPath}"); | |
| 93 | - return file_get_contents($inclPath); | |
| 94 | - }, $content); | |
| 95 | - } | |
| 96 | - | |
| 97 | - $content = $this->procDirs($content); | |
| 98 | - $content = $this->procVars($content); | |
| 99 | - $content = $this->procFuncs($content); | |
| 100 | - | |
| 101 | - $tmpFile = tempnam(sys_get_temp_dir(), 'tmpl_'); | |
| 102 | - file_put_contents($tmpFile, $content); | |
| 103 | - | |
| 104 | - include $tmpFile; | |
| 105 | - unlink($tmpFile); | |
| 106 | - } | |
| 107 | - | |
| 108 | - // 機能性メソッド | |
| 109 | - | |
| 110 | - /** | |
| 111 | - * テンプレートディレクティブを処理する | |
| 112 | - * | |
| 113 | - * @param string $content テンプレート内容 | |
| 114 | - * @return string|null 処理後の内容 | |
| 115 | - */ | |
| 116 | - private function procDirs(string $content): string|null { | |
| 117 | - // includeディレクティブの処理 | |
| 118 | - while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) { | |
| 119 | - $content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m): bool|string { | |
| 120 | - $inclPath = ROOT.'/view/'.trim($m[1], "'\" ").'.php'; | |
| 121 | - if (!file_exists($inclPath)) | |
| 122 | - throw new \RuntimeException("ファイルを見つけません: {$inclPath}"); | |
| 123 | - return file_get_contents($inclPath); | |
| 124 | - }, $content); | |
| 125 | - } | |
| 126 | - | |
| 127 | - $content = preg_replace('/\{@\s*if\s*\((.*?)\):\s*@\}/', '{@ if ($1) @}', $content); | |
| 128 | - $content = preg_replace('/\{@\s*endif;\s*@\}/', '{@ endif @}', $content); | |
| 129 | - | |
| 130 | - $processDirectives = function($c): array|string|null { | |
| 131 | - // kysディレクティブの処理 | |
| 132 | - $c = preg_replace_callback('/\{@\s*kys\((.*?)\)\s*@\}/s', function($m): string { | |
| 133 | - return "<?php echo '<pre>'; print_r({$m[1]}); echo '</pre>'; die(); ?>"; | |
| 134 | - }, $c); | |
| 135 | - | |
| 136 | - // foreachループとネストした内容の処理 | |
| 137 | - $c = preg_replace_callback('/\{@\s*foreach\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 138 | - return "<?php foreach({$m[1]}): ?>"; | |
| 139 | - }, $c); | |
| 140 | - | |
| 141 | - $c = preg_replace_callback('/\{@\s*endforeach\s*@\}/s', function($m): string { | |
| 142 | - return "<?php endforeach; ?>"; | |
| 143 | - }, $c); | |
| 144 | - | |
| 145 | - // forループの処理 | |
| 146 | - $c = preg_replace_callback('/\{@\s*for\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 147 | - return "<?php for({$m[1]}): ?>"; | |
| 148 | - }, $c); | |
| 149 | - | |
| 150 | - $c = preg_replace_callback('/\{@\s*endfor\s*@\}/s', function($m): string { | |
| 151 | - return "<?php endfor; ?>"; | |
| 152 | - }, $c); | |
| 153 | - | |
| 154 | - // if-elif-else-endifの処理 | |
| 155 | - $c = preg_replace_callback('/\{@\s*if\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 156 | - return "<?php if ({$m[1]}): ?>"; | |
| 157 | - }, $c); | |
| 158 | - | |
| 159 | - $c = preg_replace_callback('/\{@\s*elif\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 160 | - return "<?php elseif ({$m[1]}): ?>"; | |
| 161 | - }, $c); | |
| 162 | - | |
| 163 | - $c = preg_replace_callback('/\{@\s*else\s*@\}/s', function($m): string { | |
| 164 | - return "<?php else: ?>"; | |
| 165 | - }, $c); | |
| 166 | - | |
| 167 | - $c = preg_replace_callback('/\{@\s*endif\s*@\}/s', function($m): string { | |
| 168 | - return "<?php endif; ?>"; | |
| 169 | - }, $c); | |
| 170 | - | |
| 171 | - return $c; | |
| 172 | - }; | |
| 173 | - | |
| 174 | - $previousContent = ''; | |
| 175 | - $maxIterations = 10; | |
| 176 | - $iterations = 0; | |
| 177 | - | |
| 178 | - while ($previousContent !== $content && $iterations < $maxIterations) { | |
| 179 | - $previousContent = $content; | |
| 180 | - $content = $processDirectives($content); | |
| 181 | - $iterations++; | |
| 182 | - } | |
| 183 | - | |
| 184 | - return $content; | |
| 185 | - } | |
| 186 | - | |
| 187 | - /** | |
| 188 | - * テンプレート変数を処理する | |
| 189 | - * | |
| 190 | - * @param string $content テンプレート内容 | |
| 191 | - * @return string 処理後の内容 | |
| 192 | - */ | |
| 193 | - private function procVars(string $content): string { | |
| 194 | - // 変数の出力(エスケープ処理なし) | |
| 195 | - $content = preg_replace_callback('/\{\{\{s*(.*?)\s*\}\}\}/', function($m): string { | |
| 196 | - return '<?= '.trim($m[1]).' ?>'; | |
| 197 | - }, $content); | |
| 198 | - | |
| 199 | - // 変数の出力(エスケープ処理あり) | |
| 200 | - $content = preg_replace_callback('/\{\{\s*(.*?)\s*\}\}/', function($m): string { | |
| 201 | - return '<?= htmlspecialchars('.trim($m[1]).', ENT_QUOTES, \'UTF-8\') ?>'; | |
| 202 | - }, $content); | |
| 203 | - | |
| 204 | - // 変数の代入 | |
| 205 | - $content = preg_replace_callback('/\{\$\s*(.*?)\s*\$\}/', function($m): string { | |
| 206 | - $parts = explode('=', $m[1], 2); | |
| 207 | - if (count($parts) !== 2) | |
| 208 | - throw new \RuntimeException("不正な値の形式"); | |
| 209 | - return '<?php '.trim($parts[0]).' = '.trim($parts[1]).'; ?>'; | |
| 210 | - }, $content); | |
| 211 | - | |
| 212 | - // コメント | |
| 213 | - $content = preg_replace_callback('/\{#\s*(.*?)\s*#\}/', function($m): string { | |
| 214 | - return '<?php /*'.trim($m[1]).'*/ ?>'; | |
| 215 | - }, $content); | |
| 216 | - | |
| 217 | - // PHPコードの実行 | |
| 218 | - $content = preg_replace_callback('/\{\!\s*(.*?)\s*\!\}/', function($m): string { | |
| 219 | - return '<?php '.trim($m[1]).' ?>'; | |
| 220 | - }, $content); | |
| 221 | - | |
| 222 | - return $content; | |
| 223 | - } | |
| 224 | - | |
| 225 | - /** | |
| 226 | - * カスタム関数を処理する | |
| 227 | - * | |
| 228 | - * @param string $content テンプレート内容 | |
| 229 | - * @return string 処理後の内容 | |
| 230 | - */ | |
| 231 | - private function procFuncs(string $content): string { | |
| 232 | - foreach ($this->custFunc as $name => $cb) { | |
| 233 | - $pattern = "/\{@\s*{$name}\((.*?)\)\s*@\}/"; | |
| 234 | - $content = preg_replace_callback($pattern, function($m) use ($cb): mixed { | |
| 235 | - $args = explode(',', $m[1]); | |
| 236 | - $args = array_map('trim', $args); | |
| 237 | - return call_user_func_array($cb, $args); | |
| 238 | - }, $content); | |
| 239 | - } | |
| 240 | - | |
| 241 | - return $content; | |
| 242 | - } | |
| 243 | -} |
削除
src/Site/Lib/Tester.php
| @@ -1,617 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Lib; | |
| 3 | - | |
| 4 | -/** | |
| 5 | - * アサーション失敗用のカスタム例外 | |
| 6 | - */ | |
| 7 | -class AssertionFailedException extends \Exception { | |
| 8 | -} | |
| 9 | - | |
| 10 | -/** | |
| 11 | - * 軽量なユニットテストフレームワーク | |
| 12 | - */ | |
| 13 | -class Tester { | |
| 14 | - // テスト統計 | |
| 15 | - private int $testCount = 0; | |
| 16 | - private int $passCount = 0; | |
| 17 | - private int $failCount = 0; | |
| 18 | - private int $errorCount = 0; | |
| 19 | - | |
| 20 | - // 現在のテストケース情報 | |
| 21 | - private string $currentTestCase = ''; | |
| 22 | - private string $currentTest = ''; | |
| 23 | - | |
| 24 | - // テストスイート設定 | |
| 25 | - private bool $colorOutput = true; | |
| 26 | - private bool $verboseOutput = true; | |
| 27 | - private bool $stopOnFailure = false; | |
| 28 | - private bool $skipCurrentDescribe = false; | |
| 29 | - private array $beforeEachCallbacks = []; | |
| 30 | - private array $afterEachCallbacks = []; | |
| 31 | - private array $beforeAllCallbacks = []; | |
| 32 | - private array $afterAllCallbacks = []; | |
| 33 | - | |
| 34 | - // ターミナル出力用の色 | |
| 35 | - private array $colors = [ | |
| 36 | - 'reset' => "\033[0m", | |
| 37 | - 'red' => "\033[31m", | |
| 38 | - 'green' => "\033[32m", | |
| 39 | - 'yellow' => "\033[33m", | |
| 40 | - 'blue' => "\033[34m", | |
| 41 | - 'magenta' => "\033[35m", | |
| 42 | - 'cyan' => "\033[36m", | |
| 43 | - 'white' => "\033[37m", | |
| 44 | - 'bold' => "\033[1m", | |
| 45 | - ]; | |
| 46 | - | |
| 47 | - private array $failures = []; | |
| 48 | - private array $errors = []; | |
| 49 | - | |
| 50 | - /** | |
| 51 | - * コンストラクタ | |
| 52 | - * | |
| 53 | - * @param array $options 設定オプション | |
| 54 | - */ | |
| 55 | - public function __construct(array $options = []) { | |
| 56 | - // オプションを設定 | |
| 57 | - if (isset($options['colorOutput'])) { | |
| 58 | - $this->colorOutput = (bool)$options['colorOutput']; | |
| 59 | - } | |
| 60 | - | |
| 61 | - if (isset($options['verboseOutput'])) { | |
| 62 | - $this->verboseOutput = (bool)$options['verboseOutput']; | |
| 63 | - } | |
| 64 | - | |
| 65 | - if (isset($options['stopOnFailure'])) { | |
| 66 | - $this->stopOnFailure = (bool)$options['stopOnFailure']; | |
| 67 | - } | |
| 68 | - | |
| 69 | - // サポートされていない場合は色を無効にする | |
| 70 | - if (PHP_SAPI !== 'cli' || strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !getenv('ANSICON')) { | |
| 71 | - $this->colorOutput = false; | |
| 72 | - } | |
| 73 | - } | |
| 74 | - | |
| 75 | - /** | |
| 76 | - * 各テストの前に実行する関数を登録する | |
| 77 | - * | |
| 78 | - * @param callable $callback コールバック関数 | |
| 79 | - * @return Tester このインスタンス | |
| 80 | - */ | |
| 81 | - public function beforeEach(callable $callback): Tester { | |
| 82 | - $this->beforeEachCallbacks[] = $callback; | |
| 83 | - return $this; | |
| 84 | - } | |
| 85 | - | |
| 86 | - /** | |
| 87 | - * 各テストの後に実行する関数を登録する | |
| 88 | - * | |
| 89 | - * @param callable $callback コールバック関数 | |
| 90 | - * @return Tester このインスタンス | |
| 91 | - */ | |
| 92 | - public function afterEach(callable $callback): Tester { | |
| 93 | - $this->afterEachCallbacks[] = $callback; | |
| 94 | - return $this; | |
| 95 | - } | |
| 96 | - | |
| 97 | - /** | |
| 98 | - * すべてのテストの前に実行する関数を登録する | |
| 99 | - * | |
| 100 | - * @param callable $callback コールバック関数 | |
| 101 | - * @return Tester このインスタンス | |
| 102 | - */ | |
| 103 | - public function beforeAll(callable $callback): Tester { | |
| 104 | - $this->beforeAllCallbacks[] = $callback; | |
| 105 | - return $this; | |
| 106 | - } | |
| 107 | - | |
| 108 | - /** | |
| 109 | - * すべてのテストの後に実行する関数を登録する | |
| 110 | - * | |
| 111 | - * @param callable $callback コールバック関数 | |
| 112 | - * @return Tester このインスタンス | |
| 113 | - */ | |
| 114 | - public function afterAll(callable $callback): Tester { | |
| 115 | - $this->afterAllCallbacks[] = $callback; | |
| 116 | - return $this; | |
| 117 | - } | |
| 118 | - | |
| 119 | - /** | |
| 120 | - * テストケースを定義する | |
| 121 | - * | |
| 122 | - * @param string $description テストケースの説明 | |
| 123 | - * @param callable $callback テストケース関数 | |
| 124 | - * @return Tester このインスタンス | |
| 125 | - */ | |
| 126 | - public function describe(string $description, callable $callback): Tester { | |
| 127 | - $this->currentTestCase = $description; | |
| 128 | - $this->skipCurrentDescribe = false; | |
| 129 | - $this->output($this->colorize('bold', "テストケース: {$description}")); | |
| 130 | - | |
| 131 | - try { | |
| 132 | - foreach ($this->beforeAllCallbacks as $before) { | |
| 133 | - call_user_func($before); | |
| 134 | - } | |
| 135 | - | |
| 136 | - call_user_func($callback, $this); | |
| 137 | - | |
| 138 | - foreach ($this->afterAllCallbacks as $after) { | |
| 139 | - call_user_func($after); | |
| 140 | - } | |
| 141 | - } catch (\Throwable $e) { | |
| 142 | - $this->recordError( | |
| 143 | - "テストケースのセットアップ/ティアダウンでエラー: ".$e->getMessage(), | |
| 144 | - $e->getTraceAsString()); | |
| 145 | - } | |
| 146 | - | |
| 147 | - $this->output(''); | |
| 148 | - return $this; | |
| 149 | - } | |
| 150 | - | |
| 151 | - /** | |
| 152 | - * 単一のテストを実行する | |
| 153 | - * | |
| 154 | - * @param string $description テストの説明 | |
| 155 | - * @param callable $callback テスト関数 | |
| 156 | - * @return Tester このインスタンス | |
| 157 | - */ | |
| 158 | - public function it(string $description, callable $callback): Tester { | |
| 159 | - if ($this->skipCurrentDescribe) { | |
| 160 | - $this->skip($description, 'skipAll() が呼び出されました'); | |
| 161 | - return $this; | |
| 162 | - } | |
| 163 | - | |
| 164 | - $this->currentTest = $description; | |
| 165 | - $this->testCount++; | |
| 166 | - | |
| 167 | - if ($this->verboseOutput) { | |
| 168 | - $this->output(" ⋄ テスト中: {$description}... ", false); | |
| 169 | - } | |
| 170 | - | |
| 171 | - try { | |
| 172 | - foreach ($this->beforeEachCallbacks as $before) { | |
| 173 | - call_user_func($before); | |
| 174 | - } | |
| 175 | - | |
| 176 | - call_user_func($callback, $this); | |
| 177 | - | |
| 178 | - foreach ($this->afterEachCallbacks as $after) { | |
| 179 | - call_user_func($after); | |
| 180 | - } | |
| 181 | - | |
| 182 | - // Test has passed. | |
| 183 | - $this->passCount++; | |
| 184 | - | |
| 185 | - if ($this->verboseOutput) { | |
| 186 | - $this->output($this->colorize('green', "合格")); | |
| 187 | - } | |
| 188 | - } catch (AssertionFailedException $e) { | |
| 189 | - $this->failCount++; | |
| 190 | - | |
| 191 | - if ($this->verboseOutput) { | |
| 192 | - $this->output($this->colorize('red', "失敗")); | |
| 193 | - $this->output($this->colorize('red', " → ".$e->getMessage())); | |
| 194 | - } | |
| 195 | - | |
| 196 | - $this->recordFailure($e->getMessage()); | |
| 197 | - | |
| 198 | - if ($this->stopOnFailure) { | |
| 199 | - $this->printSummary(); | |
| 200 | - exit(1); | |
| 201 | - } | |
| 202 | - } catch (\Throwable $e) { | |
| 203 | - $this->errorCount++; | |
| 204 | - | |
| 205 | - if ($this->verboseOutput) { | |
| 206 | - $this->output($this->colorize('yellow', "エラー")); | |
| 207 | - $this->output($this->colorize('yellow', " → ".$e->getMessage())); | |
| 208 | - } | |
| 209 | - | |
| 210 | - $this->recordError($e->getMessage(), $e->getTraceAsString()); | |
| 211 | - | |
| 212 | - if ($this->stopOnFailure) { | |
| 213 | - $this->printSummary(); | |
| 214 | - exit(1); | |
| 215 | - } | |
| 216 | - } | |
| 217 | - | |
| 218 | - return $this; | |
| 219 | - } | |
| 220 | - | |
| 221 | - /** | |
| 222 | - * テストをスキップする | |
| 223 | - * | |
| 224 | - * @param string $description テストの説明 | |
| 225 | - * @param string $reason スキップする理由。デフォルト: "まだ実装されていません" | |
| 226 | - * @return Tester このインスタンス | |
| 227 | - */ | |
| 228 | - public function skip(string $description, | |
| 229 | - string $reason = 'まだ実装されていません'): Tester { | |
| 230 | - if ($this->verboseOutput) { | |
| 231 | - $this->output(" ⋄ スキップ: {$description}... " | |
| 232 | - .$this->colorize('cyan', "スキップ")); | |
| 233 | - $this->output($this->colorize('cyan', " → {$reason}")); | |
| 234 | - } | |
| 235 | - | |
| 236 | - return $this; | |
| 237 | - } | |
| 238 | - | |
| 239 | - /** | |
| 240 | - * 条件がtrueである事をアサートする | |
| 241 | - * | |
| 242 | - * @param bool $condition チェックする条件 | |
| 243 | - * @param string $message 失敗時のオプションメッセージ | |
| 244 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 245 | - * @return Tester このインスタンス | |
| 246 | - */ | |
| 247 | - public function assertTrue(bool $condition, | |
| 248 | - string $message = '条件がtrueであることを期待しました'): Tester { | |
| 249 | - if ($condition !== true) { | |
| 250 | - throw new AssertionFailedException($message); | |
| 251 | - } | |
| 252 | - | |
| 253 | - return $this; | |
| 254 | - } | |
| 255 | - | |
| 256 | - /** | |
| 257 | - * 条件がfalseである事をアサートする | |
| 258 | - * | |
| 259 | - * @param bool $condition チェックする条件 | |
| 260 | - * @param string $message 失敗時のオプションメッセージ | |
| 261 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 262 | - * @return Tester このインスタンス | |
| 263 | - */ | |
| 264 | - public function assertFalse(bool $condition, | |
| 265 | - string $message = '条件がfalseであることを期待しました'): Tester { | |
| 266 | - if ($condition !== false) { | |
| 267 | - throw new AssertionFailedException($message); | |
| 268 | - } | |
| 269 | - | |
| 270 | - return $this; | |
| 271 | - } | |
| 272 | - | |
| 273 | - /** | |
| 274 | - * 二つの値が等しい事をアサートする | |
| 275 | - * | |
| 276 | - * @param mixed $expected 期待値 | |
| 277 | - * @param mixed $actual 実際の値 | |
| 278 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 279 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 280 | - * @return Tester このインスタンス | |
| 281 | - */ | |
| 282 | - public function assertEquals(mixed $expected, mixed $actual, | |
| 283 | - ?string $message = null): Tester { | |
| 284 | - if ($expected != $actual) { | |
| 285 | - if ($message === null) { | |
| 286 | - $expected = $this->exportValue($expected); | |
| 287 | - $actual = $this->exportValue($actual); | |
| 288 | - $message = "{$expected}を期待しましたが、{$actual}が得られました"; | |
| 289 | - } | |
| 290 | - | |
| 291 | - throw new AssertionFailedException($message); | |
| 292 | - } | |
| 293 | - | |
| 294 | - return $this; | |
| 295 | - } | |
| 296 | - | |
| 297 | - /** | |
| 298 | - * 二つの値が同一である事をアサートする | |
| 299 | - * | |
| 300 | - * @param mixed $expected 期待値 | |
| 301 | - * @param mixed $actual 実際の値 | |
| 302 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 303 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 304 | - * @return Tester このインスタンス | |
| 305 | - */ | |
| 306 | - public function assertSame(mixed $expected, mixed $actual, | |
| 307 | - ?string $message = null): Tester { | |
| 308 | - if ($expected !== $actual) { | |
| 309 | - if ($message === null) { | |
| 310 | - $expected = $this->exportValue($expected); | |
| 311 | - $actual = $this->exportValue($actual); | |
| 312 | - $message = | |
| 313 | - "{$expected}を期待しましたが、{$actual}が得られました(厳密な比較)"; | |
| 314 | - } | |
| 315 | - | |
| 316 | - throw new AssertionFailedException($message); | |
| 317 | - } | |
| 318 | - | |
| 319 | - return $this; | |
| 320 | - } | |
| 321 | - | |
| 322 | - /** | |
| 323 | - * 値がnullである事をアサートする | |
| 324 | - * | |
| 325 | - * @param mixed $actual チェックする値 | |
| 326 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 327 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 328 | - * @return Tester このインスタンス | |
| 329 | - */ | |
| 330 | - public function assertNull(mixed $actual, ?string $message = null): Tester { | |
| 331 | - if ($actual !== null) { | |
| 332 | - if ($message === null) { | |
| 333 | - $actual = $this->exportValue($actual); | |
| 334 | - $message = "nullを期待しましたが、{$actual}が得られました"; | |
| 335 | - } | |
| 336 | - | |
| 337 | - throw new AssertionFailedException($message); | |
| 338 | - } | |
| 339 | - | |
| 340 | - return $this; | |
| 341 | - } | |
| 342 | - | |
| 343 | - /** | |
| 344 | - * 値がnullでない事をアサートする | |
| 345 | - * | |
| 346 | - * @param mixed $actual チェックする値 | |
| 347 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 348 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 349 | - * @return Tester このインスタンス | |
| 350 | - */ | |
| 351 | - public function assertNotNull(mixed $actual, ?string $message = null): Tester { | |
| 352 | - if ($actual === null) { | |
| 353 | - if ($message === null) { | |
| 354 | - $message = "値がnullでない事を期待しました"; | |
| 355 | - } | |
| 356 | - | |
| 357 | - throw new AssertionFailedException($message); | |
| 358 | - } | |
| 359 | - | |
| 360 | - return $this; | |
| 361 | - } | |
| 362 | - | |
| 363 | - /** | |
| 364 | - * 値が特定のキーを持つ事をアサートする | |
| 365 | - * | |
| 366 | - * @param mixed $key チェックするキー | |
| 367 | - * @param array $array チェックする配列 | |
| 368 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 369 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 370 | - * @return Tester このインスタンス | |
| 371 | - */ | |
| 372 | - public function assertArrayHasKey(mixed $key, array $array, | |
| 373 | - ?string $message = null): Tester { | |
| 374 | - if (!is_array($array) && !($array instanceof \ArrayAccess)) { | |
| 375 | - throw new AssertionFailedException( | |
| 376 | - '第2引数は配列又はArrayAccessを実装している必要があります'); | |
| 377 | - } | |
| 378 | - | |
| 379 | - if (!array_key_exists($key, $array)) { | |
| 380 | - if ($message === null) { | |
| 381 | - $message = "配列がキー '{$key}' を持つ事を期待しました"; | |
| 382 | - } | |
| 383 | - | |
| 384 | - throw new AssertionFailedException($message); | |
| 385 | - } | |
| 386 | - | |
| 387 | - return $this; | |
| 388 | - } | |
| 389 | - | |
| 390 | - /** | |
| 391 | - * 文字列がサブ文字列を含むことをアサートする | |
| 392 | - * | |
| 393 | - * @param string $needle 検索するサブ文字列 | |
| 394 | - * @param string $haystack 検索対象の文字列 | |
| 395 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 396 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 397 | - * @return Tester このインスタンス | |
| 398 | - */ | |
| 399 | - public function assertStringContains(string $needle, string $haystack, | |
| 400 | - ?string $message = null): Tester { | |
| 401 | - if (!is_string($needle) || !is_string($haystack)) { | |
| 402 | - throw new AssertionFailedException('両方の引数は文字列である必要があります'); | |
| 403 | - } | |
| 404 | - | |
| 405 | - if (strpos($haystack, $needle) === false) { | |
| 406 | - if ($message === null) { | |
| 407 | - $message = "文字列 '{$haystack}' が '{$needle}' を含む事を期待しました"; | |
| 408 | - } | |
| 409 | - | |
| 410 | - throw new AssertionFailedException($message); | |
| 411 | - } | |
| 412 | - | |
| 413 | - return $this; | |
| 414 | - } | |
| 415 | - | |
| 416 | - /** | |
| 417 | - * コールバックが例外をスローする事をアサートする | |
| 418 | - * | |
| 419 | - * @param callable $callback 実行するコールバック | |
| 420 | - * @param string $exceptionClass 期待される例外クラス | |
| 421 | - * @param string|null $message 失敗時のオプションメッセージ | |
| 422 | - * @throws AssertionFailedException アサーションが失敗した場合 | |
| 423 | - * @return Tester このインスタンス | |
| 424 | - */ | |
| 425 | - public function assertThrows(callable $callback, string $exceptionClass, | |
| 426 | - ?string $message = null): Tester { | |
| 427 | - try { | |
| 428 | - call_user_func($callback); | |
| 429 | - | |
| 430 | - if ($message === null) { | |
| 431 | - $message = "'{$exceptionClass}' 型の例外がスローされる事を期待しましたが、スローされませんでした"; | |
| 432 | - } | |
| 433 | - | |
| 434 | - throw new AssertionFailedException($message); | |
| 435 | - } catch (\Throwable $e) { | |
| 436 | - if (!($e instanceof $exceptionClass)) { | |
| 437 | - if ($message === null) { | |
| 438 | - $message = "'{$exceptionClass}' 型の例外を期待しましたが、" | |
| 439 | - .get_class($e)." が得られました"; | |
| 440 | - } | |
| 441 | - | |
| 442 | - throw new AssertionFailedException($message); | |
| 443 | - } | |
| 444 | - } | |
| 445 | - | |
| 446 | - return $this; | |
| 447 | - } | |
| 448 | - | |
| 449 | - /** | |
| 450 | - * コレクションが指定された要素数を持つ事 | |
| 451 | - * | |
| 452 | - * @param int $expoected 期待される要素数 | |
| 453 | - * @param iterable $collection 配列または Traversable | |
| 454 | - * @param string|null $message 失敗時のカスタムメッセージ | |
| 455 | - * @return Tester | |
| 456 | - * | |
| 457 | - * @throws AssertionFailedException | |
| 458 | - */ | |
| 459 | - public function assertCount(int $expected, iterable $collection, ?string $message = null): Tester { | |
| 460 | - $actual = is_countable($collection) ? count($collection) : iterator_count($collection); | |
| 461 | - | |
| 462 | - if ($actual !== $expected) { | |
| 463 | - $message ??= "要素数が {$expected} であることを期待しましたが、{$actual} でした"; | |
| 464 | - throw new AssertionFailedException($message); | |
| 465 | - } | |
| 466 | - | |
| 467 | - return $this; | |
| 468 | - } | |
| 469 | - | |
| 470 | - /** | |
| 471 | - * 現在の describe ブロック内の残りすべての it() をスキップ | |
| 472 | - * | |
| 473 | - * @param string $reason スキップ理由(出力に表示) | |
| 474 | - * @return void | |
| 475 | - */ | |
| 476 | - public function skipAll(string $reason = 'このテストケースはスキップされました'): void { | |
| 477 | - $this->skipCurrentDescribe = true; | |
| 478 | - | |
| 479 | - if ($this->verboseOutput) { | |
| 480 | - $this->output($this->colorize('cyan', " スキップAll: {$reason}")); | |
| 481 | - } | |
| 482 | - } | |
| 483 | - | |
| 484 | - /** | |
| 485 | - * テスト結果の詳細を表示する | |
| 486 | - * | |
| 487 | - * @return Tester | |
| 488 | - */ | |
| 489 | - public function printSummary(): Tester { | |
| 490 | - $this->output(''); | |
| 491 | - $this->output($this->colorize('bold', "テスト結果の概要:")); | |
| 492 | - $this->output(" テスト総数: {$this->testCount}"); | |
| 493 | - $this->output(" ".$this->colorize('green', "合格: {$this->passCount}")); | |
| 494 | - $this->output(" ".$this->colorize('red', "失敗: {$this->failCount}")); | |
| 495 | - $this->output(" ".$this->colorize('yellow', "エラー: {$this->errorCount}")); | |
| 496 | - $this->output(''); | |
| 497 | - | |
| 498 | - // 失敗を書き出す | |
| 499 | - if (count($this->failures) > 0) { | |
| 500 | - $this->output($this->colorize('bold', "失敗:")); | |
| 501 | - | |
| 502 | - foreach ($this->failures as $i => $f) { | |
| 503 | - $num = $i + 1; | |
| 504 | - $this->output(" {$num}) {$f['testCase']} → {$f['test']}"); | |
| 505 | - $this->output(" ".$this->colorize('red', $f['message'])); | |
| 506 | - $this->output(''); | |
| 507 | - } | |
| 508 | - } | |
| 509 | - | |
| 510 | - // エラーを書き出す | |
| 511 | - if (count($this->errors) > 0) { | |
| 512 | - $this->output($this->colorize('bold', "エラー:")); | |
| 513 | - | |
| 514 | - foreach ($this->errors as $i => $e) { | |
| 515 | - $num = $i + 1; | |
| 516 | - $this->output(" {$num}) {$e['testCase']} → {$e['test']}"); | |
| 517 | - $this->output(" ".$this->colorize('yellow', $e['message'])); | |
| 518 | - | |
| 519 | - if (isset($e['trace'])) { | |
| 520 | - $this->output(" ".$this->colorize('yellow', "スタックトレース:")); | |
| 521 | - $this->output(" ".$this->colorize('yellow', $e['trace'])); | |
| 522 | - } | |
| 523 | - | |
| 524 | - $this->output(''); | |
| 525 | - } | |
| 526 | - } | |
| 527 | - | |
| 528 | - if ($this->failCount === 0 && $this->errorCount === 0) { | |
| 529 | - $this->output($this->colorize('green', "全てのテストに合格しました!")); | |
| 530 | - } else { | |
| 531 | - $this->output($this->colorize('red', "テストが失敗・エラーで完了しました。")); | |
| 532 | - } | |
| 533 | - | |
| 534 | - return $this; | |
| 535 | - } | |
| 536 | - | |
| 537 | - // 機能性メソッド | |
| 538 | - | |
| 539 | - /** | |
| 540 | - * コンソールにテキストを出力する | |
| 541 | - * | |
| 542 | - * @param string $text 出力するテキスト | |
| 543 | - * @param bool $newline 改行を追加するかどうか | |
| 544 | - * @return void | |
| 545 | - */ | |
| 546 | - private function output(string $text, bool $newline = true): void { | |
| 547 | - echo $text.($newline ? PHP_EOL : ''); | |
| 548 | - } | |
| 549 | - | |
| 550 | - /** | |
| 551 | - * 有効な場合はテキストに色を適用する | |
| 552 | - * | |
| 553 | - * @param string $color 色名 | |
| 554 | - * @param string $text 色付けするテキスト | |
| 555 | - * @return string | |
| 556 | - */ | |
| 557 | - private function colorize(string $color, string $text): string { | |
| 558 | - if (!$this->colorOutput || !isset($this->colors[$color])) { | |
| 559 | - return $text; | |
| 560 | - } | |
| 561 | - | |
| 562 | - return $this->colors[$color].$text.$this->colors['reset']; | |
| 563 | - } | |
| 564 | - | |
| 565 | - /** | |
| 566 | - * 値を表示用の文字列としてエクスポートする | |
| 567 | - * | |
| 568 | - * @param mixed $value エクスポートする値 | |
| 569 | - * @return string | |
| 570 | - */ | |
| 571 | - private function exportValue(mixed $value): string { | |
| 572 | - if (is_null($value)) return 'null'; | |
| 573 | - if (is_bool($value)) return $value ? 'true' : 'false'; | |
| 574 | - if (is_array($value)) return 'Array('.count($value).')'; | |
| 575 | - if (is_object($value)) return get_class($value).' Object'; | |
| 576 | - | |
| 577 | - if (is_string($value)) { | |
| 578 | - if (strlen($value) > 40) { | |
| 579 | - return "'".substr($value, 0, 37)."...'"; | |
| 580 | - } | |
| 581 | - | |
| 582 | - return "'{$value}'"; | |
| 583 | - } | |
| 584 | - | |
| 585 | - return (string)$value; | |
| 586 | - } | |
| 587 | - | |
| 588 | - /** | |
| 589 | - * テストの失敗を記録する | |
| 590 | - * | |
| 591 | - * @param string $message 失敗メッセージ | |
| 592 | - * @return void | |
| 593 | - */ | |
| 594 | - private function recordFailure(string $message): void { | |
| 595 | - $this->failures[] = [ | |
| 596 | - 'testCase' => $this->currentTestCase, | |
| 597 | - 'test' => $this->currentTest, | |
| 598 | - 'message' => $message, | |
| 599 | - ]; | |
| 600 | - } | |
| 601 | - | |
| 602 | - /** | |
| 603 | - * テストのエラーを記録する | |
| 604 | - * | |
| 605 | - * @param string $message エラーメッセージ | |
| 606 | - * @param string|null $trace スタックトレース | |
| 607 | - * @return void | |
| 608 | - */ | |
| 609 | - private function recordError(string $message, ?string $trace = null): void { | |
| 610 | - $this->errors[] = [ | |
| 611 | - 'testCase' => $this->currentTestCase, | |
| 612 | - 'test' => $this->currentTest, | |
| 613 | - 'message' => $message, | |
| 614 | - 'trace' => $trace, | |
| 615 | - ]; | |
| 616 | - } | |
| 617 | -} |
| @@ -0,0 +1,19 @@ | ||
| 1 | +<?php | |
| 2 | + | |
| 3 | +namespace Site\Test; | |
| 4 | + | |
| 5 | +require_once __DIR__.'/../../../autoload.php'; | |
| 6 | + | |
| 7 | +use Site\Controller\User; | |
| 8 | +use Std\Lib\Tester; | |
| 9 | + | |
| 10 | +$test = new Tester([ | |
| 11 | + 'colorOutput' => true, | |
| 12 | + 'verboseOutput' => true | |
| 13 | +]); | |
| 14 | + | |
| 15 | +$test->describe('', function($test): void { | |
| 16 | + $test->it('', function($test): void { | |
| 17 | + // TODO | |
| 18 | + }); | |
| 19 | +}); |
削除
src/Site/Test/LibActivitypub.php
| @@ -1,79 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Test; | |
| 3 | - | |
| 4 | -require_once __DIR__.'/../../../autoload.php'; | |
| 5 | - | |
| 6 | -use Site\Lib\Tester; | |
| 7 | -use Site\Lib\Activitypub; | |
| 8 | -use Site\Controller\BlogPost; | |
| 9 | - | |
| 10 | -if (ACTIVITYPUB_ENABLED) { | |
| 11 | - $test = new Tester([ | |
| 12 | - 'colorOutput' => true, | |
| 13 | - 'verboseOutput' => true | |
| 14 | - ]); | |
| 15 | - | |
| 16 | - /* | |
| 17 | - postInbox(array $activity): void | |
| 18 | - update(): string | |
| 19 | - sendActorUpdate(array $params): void | |
| 20 | - */ | |
| 21 | - | |
| 22 | - $test->describe('ActivityPub情報の受け取り', function($test): void { | |
| 23 | - $test->it('このインスタンスのユーザーを受け取れるはず', function($test): void { | |
| 24 | - $ap = new Activitypub(); | |
| 25 | - $res = $ap->getActor(); | |
| 26 | - $test->assertNotNull($res); | |
| 27 | - $json = json_decode($res, true); | |
| 28 | - | |
| 29 | - $expect = 'Person'; | |
| 30 | - $test->assertEquals($json['type'], $expect); | |
| 31 | - | |
| 32 | - $expect = 'http://127.0.0.1:8000'; | |
| 33 | - $test->assertEquals($json['url'], $expect); | |
| 34 | - | |
| 35 | - $expect = 'http://127.0.0.1:8000/ap/actor'; | |
| 36 | - $test->assertEquals($json['id'], $expect); | |
| 37 | - | |
| 38 | - $expect = 'http://127.0.0.1:8000/ap/inbox'; | |
| 39 | - $test->assertEquals($json['inbox'], $expect); | |
| 40 | - | |
| 41 | - $expect = 'http://127.0.0.1:8000/ap/outbox'; | |
| 42 | - $test->assertEquals($json['outbox'], $expect); | |
| 43 | - | |
| 44 | - $expect = 'http://127.0.0.1:8000/ap/followers'; | |
| 45 | - $test->assertEquals($json['followers'], $expect); | |
| 46 | - | |
| 47 | - $expect = 'http://127.0.0.1:8000/ap/following'; | |
| 48 | - $test->assertEquals($json['following'], $expect); | |
| 49 | - }); | |
| 50 | - | |
| 51 | - $test->it('アクティビティを受け取れるはず', function($test): void { | |
| 52 | - $blog = new BlogPost; | |
| 53 | - $posts = $blog->getPosts('/blog/', null); | |
| 54 | - $ap = new Activitypub($posts); | |
| 55 | - $res = $ap->getActivity('a8c04518-4181-4ec6-9ef0-3f88f23b84b6'); // /blog/feature-test | |
| 56 | - $test->assertNotNull($res); | |
| 57 | - $json = json_decode($res, true); | |
| 58 | - | |
| 59 | - $expect = 'Create'; | |
| 60 | - $test->assertEquals($json['type'], $expect); | |
| 61 | - | |
| 62 | - $expect = '【マークダウン】関数'; | |
| 63 | - $test->assertEquals($json['object']['name'], $expect); | |
| 64 | - | |
| 65 | - $expect = 'https://www.w3.org/ns/activitystreams#Public'; | |
| 66 | - $test->assertEquals($json['object']['to'][0], $expect); | |
| 67 | - | |
| 68 | - $expect = 'zBbw7K7L1dEwr6htZX5JevRv3oLtHwXQU4o5jT8jn9RKF'; | |
| 69 | - $test->assertEquals($json['attachment'][0]['digestMultibase'], $expect); | |
| 70 | - | |
| 71 | - $expect = 'http://127.0.0.1:8000/static/article/o_53803618dc1691.28179609-orig.jpg'; | |
| 72 | - $test->assertEquals($json['attachment'][0]['url'], $expect); | |
| 73 | - }); | |
| 74 | - }); | |
| 75 | - | |
| 76 | - // TODO: 未完了 | |
| 77 | - | |
| 78 | - $test->printSummary(); | |
| 79 | -} |
削除
src/Site/Test/LibAuth.php
| @@ -1,229 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Test; | |
| 3 | - | |
| 4 | -require_once __DIR__.'/../../../autoload.php'; | |
| 5 | - | |
| 6 | -use Site\Lib\Tester; | |
| 7 | -use Site\Lib\Auth; | |
| 8 | - | |
| 9 | -if (AUTH_ENABLED) { | |
| 10 | - $test = new Tester([ | |
| 11 | - 'colorOutput' => true, | |
| 12 | - 'verboseOutput' => true | |
| 13 | - ]); | |
| 14 | - | |
| 15 | - if (AUTH_REGISTER_ENABLED) { | |
| 16 | - $test->describe('ユーザー登録', function($test): void { | |
| 17 | - $test->it('アカウントを作成出来るはず', function($test): void { | |
| 18 | - $user = "admin1234"; | |
| 19 | - $pass = "testTEST1234!#$%@[]:;"; | |
| 20 | - $passVerify = "testTEST1234!#$%@[]:;"; | |
| 21 | - $email = "test@076.co.jp"; | |
| 22 | - | |
| 23 | - $auth = new Auth(); | |
| 24 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 25 | - | |
| 26 | - $expect = true; | |
| 27 | - | |
| 28 | - $test->assertNotNull($res); | |
| 29 | - $test->assertEquals($res->isSuccess, $expect); | |
| 30 | - }); | |
| 31 | - | |
| 32 | - $test->it('空フィールドでアカウントを作成出来ないはず', function($test): void { | |
| 33 | - $user = ""; | |
| 34 | - $pass = "testTEST1234!#$%@[]:;"; | |
| 35 | - $passVerify = "testTEST1234!#$%@[]:;"; | |
| 36 | - $email = "test@076.ne.jp"; | |
| 37 | - | |
| 38 | - $auth = new Auth(); | |
| 39 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 40 | - | |
| 41 | - $expect = 'エラー:ユーザー名をご入力下さい。'.'<br />'; | |
| 42 | - | |
| 43 | - $test->assertNotNull($res); | |
| 44 | - $test->assertEquals($res->message, $expect); | |
| 45 | - | |
| 46 | - $user = "admin5678"; | |
| 47 | - $pass = ""; | |
| 48 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 49 | - | |
| 50 | - $expect = 'エラー:パスワードをご入力下さい。'.'<br />'; | |
| 51 | - | |
| 52 | - $test->assertNotNull($res); | |
| 53 | - $test->assertEquals($res->message, $expect); | |
| 54 | - | |
| 55 | - $pass = "testTEST1234!#$%@[]:;"; | |
| 56 | - $email = ""; | |
| 57 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 58 | - | |
| 59 | - $expect = 'エラー:メールアドレスをご入力下さい。'.'<br />'; | |
| 60 | - | |
| 61 | - $test->assertNotNull($res); | |
| 62 | - $test->assertEquals($res->message, $expect); | |
| 63 | - }); | |
| 64 | - | |
| 65 | - $test->it('既にユーザー名やメールアドレスが存在するアカウントを作成出来ないはず', function($test): void { | |
| 66 | - $user = "admin1234"; | |
| 67 | - $pass = "testTEST1234!#$%@[]:;"; | |
| 68 | - $passVerify = "testTEST1234!#$%@[]:;"; | |
| 69 | - $email = "test@076.ne.jp"; | |
| 70 | - | |
| 71 | - $auth = new Auth(); | |
| 72 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 73 | - | |
| 74 | - $expect = 'ユーザー「admin1234」は既に存在します。'.'<br />'; | |
| 75 | - | |
| 76 | - $test->assertNotNull($res); | |
| 77 | - $test->assertEquals($res->message, $expect); | |
| 78 | - | |
| 79 | - $user = "admin5678"; | |
| 80 | - $email = "test@076.co.jp"; | |
| 81 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 82 | - | |
| 83 | - $expect = 'ユーザー「test@076.co.jp」は既に存在します。'.'<br />'; | |
| 84 | - | |
| 85 | - $test->assertNotNull($res); | |
| 86 | - $test->assertEquals($res->message, $expect); | |
| 87 | - }); | |
| 88 | - | |
| 89 | - $test->it('一致しない、短い、又は不安なパスワードでアカウントを作成出来ないはず', function($test): void { | |
| 90 | - $user = "admin5678"; | |
| 91 | - $pass = "testtest1234!#$%@[]:;"; | |
| 92 | - $passVerify = "testTEST1234!#$%@[]:;"; | |
| 93 | - $email = "test@076.ne.jp"; | |
| 94 | - | |
| 95 | - $auth = new Auth(); | |
| 96 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 97 | - | |
| 98 | - $expect = 'エラー:パスワードは一致していません。'.'<br />'; | |
| 99 | - | |
| 100 | - $test->assertNotNull($res); | |
| 101 | - $test->assertEquals($res->message, $expect); | |
| 102 | - | |
| 103 | - $pass = "testTEST1234"; | |
| 104 | - $passVerify = "testTEST1234"; | |
| 105 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 106 | - | |
| 107 | - $expect = 'エラー:パスワードは20以上をご入力下さい。'.'<br />'; | |
| 108 | - | |
| 109 | - $test->assertNotNull($res); | |
| 110 | - $test->assertEquals($res->message, $expect); | |
| 111 | - | |
| 112 | - $pass = "testTEST1234testTEST"; | |
| 113 | - $passVerify = "testTEST1234testTEST"; | |
| 114 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 115 | - | |
| 116 | - $expect = 'エラー:パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。'.'<br />'; | |
| 117 | - | |
| 118 | - $test->assertNotNull($res); | |
| 119 | - $test->assertEquals($res->message, $expect); | |
| 120 | - }); | |
| 121 | - | |
| 122 | - $test->it('不正なメールアドレスでアカウントを作成出来ないはず', function($test): void { | |
| 123 | - $user = "admin5678"; | |
| 124 | - $pass = "testTEST1234!#$%@[]:;"; | |
| 125 | - $passVerify = "testTEST1234!#$%@[]:;"; | |
| 126 | - $email = "test@076.niggerballs"; | |
| 127 | - | |
| 128 | - $auth = new Auth(); | |
| 129 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 130 | - | |
| 131 | - $expect = 'エラー:メールアドレスは不正です。'.'<br />'; | |
| 132 | - | |
| 133 | - $test->assertNotNull($res); | |
| 134 | - $test->assertEquals($res->message, $expect); | |
| 135 | - | |
| 136 | - $email = "test@076"; | |
| 137 | - $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 138 | - | |
| 139 | - $expect = 'エラー:メールアドレスは不正です。'.'<br />'; | |
| 140 | - | |
| 141 | - $test->assertNotNull($res); | |
| 142 | - $test->assertEquals($res->message, $expect); | |
| 143 | - }); | |
| 144 | - | |
| 145 | - // ユーザーの削除 | |
| 146 | - $users = scandir(ROOT.'/data/user'); | |
| 147 | - $userFile = ''; | |
| 148 | - foreach ($users as $u) { | |
| 149 | - if (str_contains($u, 'admin1234')) { | |
| 150 | - $userFile = $u; | |
| 151 | - break; | |
| 152 | - } | |
| 153 | - } | |
| 154 | - unlink(ROOT.'/data/user/'.$userFile); | |
| 155 | - rmdir(ROOT.'/public/static/user/admin1234'); | |
| 156 | - }); | |
| 157 | - } | |
| 158 | - | |
| 159 | - // TODO: CLIからクッキーを設定出来ないから、「Site\Lib\Auth::getUserData(): Return value must be of type stdClass, null returned」って発生する・・・ | |
| 160 | - // $test->describe('ログインとログアウト', function($test): void { | |
| 161 | - // $user = "admin1234"; | |
| 162 | - // $pass = "testTEST1234!#$%@[]:;"; | |
| 163 | - // $passVerify = "testTEST1234!#$%@[]:;"; | |
| 164 | - // $email = "test@076.co.jp"; | |
| 165 | - | |
| 166 | - // $auth = new Auth(); | |
| 167 | - // $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 168 | - // unset($auth); | |
| 169 | - | |
| 170 | - // $test->it('正しい認証情報でログイン出来るはず', function($test) use ($user, $pass): void { | |
| 171 | - // $auth = new Auth(); | |
| 172 | - // $res = $auth->setToken($user, $pass); | |
| 173 | - | |
| 174 | - // $test->assertTrue($res->isSuccess); | |
| 175 | - // $test->assertEquals('ログイン成功', $res->message); | |
| 176 | - | |
| 177 | - // $auth2 = new Auth(); | |
| 178 | - // $usr = $auth2->getLoggedInUser(); | |
| 179 | - | |
| 180 | - // $test->assertNotNull($usr); | |
| 181 | - // $test->assertEquals($user, $usr->username); | |
| 182 | - // }); | |
| 183 | - | |
| 184 | - // $test->it('間違ったパスワードでログイン出来ないはず', function($test) use ($user): void { | |
| 185 | - // $auth = new Auth(); | |
| 186 | - // $res = $auth->setToken($user, 'fuseipassword'); | |
| 187 | - | |
| 188 | - // $test->assertFalse($res->isSuccess); | |
| 189 | - // $test->assertEquals('ユーザー名又はパスワードが一致していません', $res->message); | |
| 190 | - // }); | |
| 191 | - | |
| 192 | - // $test->it('存在しないユーザーでログイン出来ないはず', function($test) use ($pass): void { | |
| 193 | - // $auth = new Auth(); | |
| 194 | - // $res = $auth->setToken('fumeiuser', $pass); | |
| 195 | - | |
| 196 | - // $test->assertFalse($res->isSuccess); | |
| 197 | - // $test->assertEquals('ユーザー名又はパスワードが一致していません', $res->message); | |
| 198 | - // }); | |
| 199 | - | |
| 200 | - // $test->it('ログアウト出来るはず', function($test) use ($user, $pass): void { | |
| 201 | - // $auth = new Auth(); | |
| 202 | - // $auth->setToken($user, $pass); | |
| 203 | - | |
| 204 | - // $res = $auth->logout(); | |
| 205 | - | |
| 206 | - // $test->assertTrue($res->isSuccess); | |
| 207 | - // $test->assertEquals('ログイン成功', $res->message); | |
| 208 | - | |
| 209 | - // $auth2 = new Auth(); | |
| 210 | - // $usr = $auth2->getLoggedInUser(); | |
| 211 | - | |
| 212 | - // $test->assertNull($usr); | |
| 213 | - // }); | |
| 214 | - | |
| 215 | - // // ユーザーの削除 | |
| 216 | - // $users = scandir(ROOT.'/data/user'); | |
| 217 | - // $userFile = ''; | |
| 218 | - // foreach ($users as $u) { | |
| 219 | - // if (str_contains($u, 'admin1234')) { | |
| 220 | - // $userFile = $u; | |
| 221 | - // break; | |
| 222 | - // } | |
| 223 | - // } | |
| 224 | - // unlink(ROOT.'/data/user/'.$userFile); | |
| 225 | - // rmdir(ROOT.'/public/static/user/admin1234'); | |
| 226 | - // }); | |
| 227 | - | |
| 228 | - $test->printSummary(); | |
| 229 | -} |
削除
src/Site/Test/LibCurl.php
| @@ -1,94 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Test; | |
| 3 | - | |
| 4 | -require_once __DIR__.'/../../../autoload.php'; | |
| 5 | - | |
| 6 | -use Site\Lib\Tester; | |
| 7 | -use Site\Lib\Curl; | |
| 8 | - | |
| 9 | -if (CURL_ENABLED) { | |
| 10 | - $test = new Tester([ | |
| 11 | - 'colorOutput' => true, | |
| 12 | - 'verboseOutput' => true | |
| 13 | - ]); | |
| 14 | - | |
| 15 | - $test->describe('Curlの基本的な機能性', function($test): void { | |
| 16 | - $test->it('URLで使って新しいインスタンスを作成するはず', function($test): void { | |
| 17 | - $curl = new Curl('https://076.moe'); | |
| 18 | - $test->assertNotNull($curl); | |
| 19 | - }); | |
| 20 | - | |
| 21 | - $test->it('メソッドでURLを設定出来るはす', function($test): void { | |
| 22 | - $curl = new Curl(); | |
| 23 | - $curl->setUrl('https://076.moe'); | |
| 24 | - $test->assertNotNull($curl); | |
| 25 | - }); | |
| 26 | - | |
| 27 | - $test->it('ヘッダー文字を作成出来るはず', function($test): void { | |
| 28 | - $curl = new Curl('https://076.moe'); | |
| 29 | - $curl->setHeaders([ | |
| 30 | - 'Accept' => 'application/json', | |
| 31 | - 'User-Agent' => 'LoliTest/1.0' | |
| 32 | - ]); | |
| 33 | - | |
| 34 | - $reflectionClass = new \ReflectionClass($curl); | |
| 35 | - $method = $reflectionClass->getMethod('buildHeaderString'); | |
| 36 | - $method->setAccessible(true); | |
| 37 | - | |
| 38 | - $headerString = $method->invoke($curl); | |
| 39 | - $test->assertStringContains('Accept: application/json', $headerString); | |
| 40 | - $test->assertStringContains('User-Agent: LoliTest/1.0', $headerString); | |
| 41 | - }); | |
| 42 | - | |
| 43 | - $test->skip('移転を対応するはず', '作成中・・・'); | |
| 44 | - | |
| 45 | - $test->it('メソッドチェーニングを対応するはず', function($test): void { | |
| 46 | - $curl = new Curl(); | |
| 47 | - $result = $curl->setUrl('https://076.moe') | |
| 48 | - ->setMethod('GET') | |
| 49 | - ->setTimeout(30); | |
| 50 | - | |
| 51 | - $test->assertSame($curl, $result); | |
| 52 | - }); | |
| 53 | - }); | |
| 54 | - | |
| 55 | - $test->describe('Curl HTTP リクエスト', function($test): void { | |
| 56 | - $networkAvailable = true; | |
| 57 | - | |
| 58 | - if (!$networkAvailable) { | |
| 59 | - $test->skip('076.moeでGETリクエストの確認', 'ネットワークが無効です。'); | |
| 60 | - $test->skip('postman-echo.comでPOSTリクエストの確認', 'ネットワークが無効です。'); | |
| 61 | - return; | |
| 62 | - } | |
| 63 | - | |
| 64 | - $test->it('076.moeでGETリクエストの確認', function($test): void { | |
| 65 | - $curl = new Curl('https://076.moe'); | |
| 66 | - $result = $curl->execute(); | |
| 67 | - | |
| 68 | - $test->assertTrue($result->isSuccess); | |
| 69 | - $test->assertEquals(200, $curl->getResponseCode()); | |
| 70 | - $test->assertNotNull($curl->getResponseBody()); | |
| 71 | - $test->assertStringContains('<html', $curl->getResponseBody()); | |
| 72 | - }); | |
| 73 | - | |
| 74 | - $test->it('postman-echo.comでPOSTリクエストの確認', function($test): void { | |
| 75 | - $curl = new Curl(); | |
| 76 | - $curl->setUrl('https://postman-echo.com/post') | |
| 77 | - ->setMethod('POST') | |
| 78 | - ->setPostFields([ | |
| 79 | - 'name' => '山田太郎', | |
| 80 | - 'email' => 't.yamada@example.com' | |
| 81 | - ]); | |
| 82 | - | |
| 83 | - $result = $curl->execute(); | |
| 84 | - $test->assertTrue($result->isSuccess); | |
| 85 | - $test->assertEquals(200, $curl->getResponseCode()); | |
| 86 | - | |
| 87 | - $responseBody = $curl->getResponseBody(); | |
| 88 | - $test->assertStringContains('山田太郎', $responseBody); | |
| 89 | - $test->assertStringContains('t.yamada@example.com', $responseBody); | |
| 90 | - }); | |
| 91 | - }); | |
| 92 | - | |
| 93 | - $test->printSummary(); | |
| 94 | -} |
削除
src/Site/Test/LibMarkdown.php
| @@ -1,234 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Test; | |
| 3 | - | |
| 4 | -require_once __DIR__.'/../../../autoload.php'; | |
| 5 | - | |
| 6 | -use Site\Lib\Tester; | |
| 7 | -use Site\Lib\Markdown; | |
| 8 | - | |
| 9 | -$test = new Tester([ | |
| 10 | - 'colorOutput' => true, | |
| 11 | - 'verboseOutput' => true | |
| 12 | -]); | |
| 13 | - | |
| 14 | -function mkfile(string $body): void { | |
| 15 | - $post = fopen(ROOT."/blog/dietrannynigger.md", "w"); | |
| 16 | - fwrite($post, "title: 【】\n"); | |
| 17 | - fwrite($post, "uuid: ".uuid()."\n"); | |
| 18 | - fwrite($post, "author: 諏訪子\n"); | |
| 19 | - fwrite($post, "date: ".date("Y-m-d H:i:s")."\n"); | |
| 20 | - fwrite($post, "thumbnail: \n"); | |
| 21 | - fwrite($post, "thumborient: center\n"); | |
| 22 | - fwrite($post, "category: \n"); | |
| 23 | - fwrite($post, "----\n"); | |
| 24 | - fwrite($post, $body); | |
| 25 | - fclose($post); | |
| 26 | -} | |
| 27 | - | |
| 28 | -function rmfile(): void { | |
| 29 | - unlink(ROOT."/blog/dietrannynigger.md"); | |
| 30 | -} | |
| 31 | - | |
| 32 | -class Constants { | |
| 33 | - public const START = "<p> "; | |
| 34 | - public const END = " </p>"; | |
| 35 | -} | |
| 36 | - | |
| 37 | -$test->describe('マークダウン', function($test): void { | |
| 38 | - | |
| 39 | - $test->it('メタデータをパーシング出来るはず', function($test): void { | |
| 40 | - $md = new Markdown('feature-test', '/blog/'); | |
| 41 | - $test->assertNotNull($md); | |
| 42 | - | |
| 43 | - $meta = $md->getMetadata(); | |
| 44 | - $test->assertNotNull($meta); | |
| 45 | - | |
| 46 | - $expect = new \stdClass; | |
| 47 | - $expect->title = '【マークダウン】関数'; | |
| 48 | - $expect->uuid = 'a8c04518-4181-4ec6-9ef0-3f88f23b84b6'; | |
| 49 | - $expect->author = '諏訪子'; | |
| 50 | - $expect->date = '2025-11-07 20:33:42'; | |
| 51 | - $expect->thumbnail = 'o_53803618dc1691.28179609-orig.jpg'; | |
| 52 | - $expect->thumborient = 'center'; | |
| 53 | - $expect->category = ['test', 'おちんちん', 'でっかいおっぱい']; | |
| 54 | - $expect->css = ['code', 'blockquote', 'table', 'blink']; | |
| 55 | - | |
| 56 | - $test->assertEquals($expect, $meta); | |
| 57 | - }); | |
| 58 | - | |
| 59 | - $test->it('横線はHTMLに交換出来るはず', function($test): void { | |
| 60 | - mkfile('----'); | |
| 61 | - | |
| 62 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 63 | - $test->assertNotNull($md); | |
| 64 | - | |
| 65 | - $body = $md->parse(); | |
| 66 | - $test->assertNotNull($body); | |
| 67 | - | |
| 68 | - $expect = "<hr />"; | |
| 69 | - $actual = str_replace("\n", '', trim($body)); | |
| 70 | - | |
| 71 | - $test->assertEquals($expect, $actual); | |
| 72 | - }); | |
| 73 | - | |
| 74 | - $test->it('ヘッダーはHTMLに交換出来るはず', function($test): void { | |
| 75 | - mkfile('# ケロケロ'); | |
| 76 | - | |
| 77 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 78 | - $test->assertNotNull($md); | |
| 79 | - | |
| 80 | - $body = $md->parse(); | |
| 81 | - $test->assertNotNull($body); | |
| 82 | - | |
| 83 | - $expect = "<h1>ケロケロ</h1>"; | |
| 84 | - $actual = trim($body); | |
| 85 | - | |
| 86 | - $test->assertEquals($expect, $actual); | |
| 87 | - | |
| 88 | - // --------------- | |
| 89 | - | |
| 90 | - mkfile('## ケロケロ'); | |
| 91 | - | |
| 92 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 93 | - $test->assertNotNull($md); | |
| 94 | - | |
| 95 | - $body = $md->parse(); | |
| 96 | - $test->assertNotNull($body); | |
| 97 | - | |
| 98 | - $expect = "<h2>ケロケロ</h2>"; | |
| 99 | - $actual = trim($body); | |
| 100 | - | |
| 101 | - $test->assertEquals($expect, $actual); | |
| 102 | - | |
| 103 | - // --------------- | |
| 104 | - | |
| 105 | - mkfile('### ケロケロ'); | |
| 106 | - | |
| 107 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 108 | - $test->assertNotNull($md); | |
| 109 | - | |
| 110 | - $body = $md->parse(); | |
| 111 | - $test->assertNotNull($body); | |
| 112 | - | |
| 113 | - $expect = "<h3>ケロケロ</h3>"; | |
| 114 | - $actual = trim($body); | |
| 115 | - | |
| 116 | - $test->assertEquals($expect, $actual); | |
| 117 | - | |
| 118 | - // --------------- | |
| 119 | - | |
| 120 | - mkfile('#### ケロケロ'); | |
| 121 | - | |
| 122 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 123 | - $test->assertNotNull($md); | |
| 124 | - | |
| 125 | - $body = $md->parse(); | |
| 126 | - $test->assertNotNull($body); | |
| 127 | - | |
| 128 | - $expect = "<h4>ケロケロ</h4>"; | |
| 129 | - $actual = trim($body); | |
| 130 | - | |
| 131 | - $test->assertEquals($expect, $actual); | |
| 132 | - | |
| 133 | - // --------------- | |
| 134 | - | |
| 135 | - mkfile('##### ケロケロ'); | |
| 136 | - | |
| 137 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 138 | - $test->assertNotNull($md); | |
| 139 | - | |
| 140 | - $body = $md->parse(); | |
| 141 | - $test->assertNotNull($body); | |
| 142 | - | |
| 143 | - $expect = "<h5>ケロケロ</h5>"; | |
| 144 | - $actual = trim($body); | |
| 145 | - | |
| 146 | - $test->assertEquals($expect, $actual); | |
| 147 | - | |
| 148 | - // --------------- | |
| 149 | - | |
| 150 | - mkfile('###### ケロケロ'); | |
| 151 | - | |
| 152 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 153 | - $test->assertNotNull($md); | |
| 154 | - | |
| 155 | - $body = $md->parse(); | |
| 156 | - $test->assertNotNull($body); | |
| 157 | - | |
| 158 | - $expect = "<h6>ケロケロ</h6>"; | |
| 159 | - $actual = trim($body); | |
| 160 | - | |
| 161 | - $test->assertEquals($expect, $actual); | |
| 162 | - }); | |
| 163 | - | |
| 164 | - $test->skip('コメントはHTMLに交換出来るはず', '作成中・・・'); | |
| 165 | - | |
| 166 | - $test->it('画像、音楽、動画はHTMLに交換出来るはず', function($test): void { | |
| 167 | - mkfile("\n | |
| 168 | -\n | |
| 169 | -\n | |
| 170 | -$[audio/ogg](https://ass.technicalsuwako.moe/砕月着信音.ogg)\n | |
| 171 | -#[video/mp4](https://ass.technicalsuwako.moe/フリーズ.mp4)"); | |
| 172 | - | |
| 173 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 174 | - $test->assertNotNull($md); | |
| 175 | - | |
| 176 | - $body = $md->parse(); | |
| 177 | - $test->assertNotNull($body); | |
| 178 | - | |
| 179 | - $expect = '<p><img style="width: 100%; " src="https://ass.technicalsuwako.moe/banner.png" alt="" /> </p> ' | |
| 180 | - . '<p><img style="width: 100%; " src="https://ass.technicalsuwako.moe/banner.png" alt="ケロケロ" /> </p> ' | |
| 181 | - . '<p><img style="width: 100%; max-width: 200px;" src="https://ass.technicalsuwako.moe/banner.png" alt="ケロケロ" /> </p> ' | |
| 182 | - . '<p><audio controls><source src="https://ass.technicalsuwako.moe/砕月着信音.ogg" type="audio/ogg" /></audio> </p> ' | |
| 183 | - . '<p><video style="max-width: 100%;" controls><source src="https://ass.technicalsuwako.moe/フリーズ.mp4" type="video/mp4" /></video> </p>'; | |
| 184 | - $actual = str_replace("\n", '', str_replace(' ', '', trim($body))); | |
| 185 | - | |
| 186 | - $test->assertEquals($expect, $actual); | |
| 187 | - }); | |
| 188 | - | |
| 189 | - $test->it('基本的なマークダウンはHTMLに交換出来るはず', function($test): void { | |
| 190 | - mkfile("**太文字**\n | |
| 191 | -*イタリック*\n | |
| 192 | -_下線_\n | |
| 193 | -~取消線~\n | |
| 194 | -!:(5)明滅文字:!\n | |
| 195 | -^(72)フォントサイズ^\n | |
| 196 | -%(ff00ff)フォントカラー%"); | |
| 197 | - | |
| 198 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 199 | - $test->assertNotNull($md); | |
| 200 | - | |
| 201 | - $body = $md->parse(); | |
| 202 | - $test->assertNotNull($body); | |
| 203 | - | |
| 204 | - $expect = '<p> <strong>太文字</strong> </p> ' | |
| 205 | - . '<p> <em>イタリック</em> </p> ' | |
| 206 | - . '<p> <u>下線</u> </p> ' | |
| 207 | - . '<p> <s>取消線</s> </p> ' | |
| 208 | - . '<p> <span class="blink" style="animation-duration: 5s;">明滅文字</span> </p> ' | |
| 209 | - . '<p> <span style="font-size: 72px;">フォントサイズ</span> </p> ' | |
| 210 | - . '<p> <span style="color: #ff00ff;">フォントカラー</span> </p>'; | |
| 211 | - $actual = str_replace("\n", '', trim($body)); | |
| 212 | - | |
| 213 | - $test->assertEquals($expect, $actual); | |
| 214 | - }); | |
| 215 | - | |
| 216 | - $test->it('複数マークダウンはHTMLに交換出来るはず', function($test): void { | |
| 217 | - mkfile("[^(500)<God>(すわこ)](https://techsuwako.jp/)^"); | |
| 218 | - | |
| 219 | - $md = new Markdown('dietrannynigger', '/blog/'); | |
| 220 | - $test->assertNotNull($md); | |
| 221 | - | |
| 222 | - $body = $md->parse(); | |
| 223 | - $test->assertNotNull($body); | |
| 224 | - | |
| 225 | - $expect = Constants::START.'<a href="https://techsuwako.jp/"><span style="font-size: 500px;"><ruby>God<rp>(</rp><rt>すわこ</rt><rp>)</rp></ruby></a></span>'.Constants::END; | |
| 226 | - $actual = str_replace("\n", '', trim($body)); | |
| 227 | - | |
| 228 | - $test->assertEquals($expect, $actual); | |
| 229 | - }); | |
| 230 | - | |
| 231 | - rmfile(); | |
| 232 | -}); | |
| 233 | - | |
| 234 | -$test->printSummary(); |
削除
src/Site/Test/LibRoute.php
| @@ -1,38 +0,0 @@ | ||
| 1 | -<?php | |
| 2 | -namespace Site\Test; | |
| 3 | - | |
| 4 | -require_once __DIR__.'/../../../autoload.php'; | |
| 5 | - | |
| 6 | -use Site\Lib\Tester; | |
| 7 | -use Site\Lib\Route; | |
| 8 | - | |
| 9 | -$test = new Tester([ | |
| 10 | - 'colorOutput' => true, | |
| 11 | - 'verboseOutput' => true | |
| 12 | -]); | |
| 13 | - | |
| 14 | -$test->describe('ルート', function($test): void { | |
| 15 | - $test->it('ルートに追加出来るはず', function($test): void { | |
| 16 | - class Webpage { | |
| 17 | - public function about(array $params): void {} | |
| 18 | - } | |
| 19 | - | |
| 20 | - $routes = [ | |
| 21 | - Route::add('GET', 'about', Webpage::class.'@about'), | |
| 22 | - ]; | |
| 23 | - | |
| 24 | - $expect = [ | |
| 25 | - [ | |
| 26 | - 'method' => 'GET', | |
| 27 | - 'path' => 'about', | |
| 28 | - 'class' => 'Site\Test\Webpage@about', | |
| 29 | - 'params' => [], | |
| 30 | - ] | |
| 31 | - ]; | |
| 32 | - | |
| 33 | - $test->assertNotNull($routes); | |
| 34 | - $test->assertEquals($expect, $routes); | |
| 35 | - }); | |
| 36 | -}); | |
| 37 | - | |
| 38 | -$test->printSummary(); |
| @@ -0,0 +1,557 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +use Std\Lib\Curl; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * ActivityPubプロトコルの実装クラス | |
| 8 | + * | |
| 9 | + * このクラスはActivityPubプロトコルを利用して分散型 | |
| 10 | + * ソーシャルネットワーキングを実装します。 | |
| 11 | + */ | |
| 12 | +class ActivityPub { | |
| 13 | + private string $domain; | |
| 14 | + private string $actor; | |
| 15 | + private string $actorNick; | |
| 16 | + private string $desc; | |
| 17 | + private string $icon; | |
| 18 | + private array $posts = []; | |
| 19 | + private string $https; | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * コンストラクタ | |
| 23 | + * | |
| 24 | + * @param array $posts 投稿データの配列 | |
| 25 | + */ | |
| 26 | + public function __construct(array $posts = []) { | |
| 27 | + if (!ACTIVITYPUB_ENABLED) return; | |
| 28 | + $this->https = 'http'.((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 's' : '').'://'; | |
| 29 | + $this->domain = $this->https.($_SERVER['SERVER_NAME'] ?? '127.0.0.1:8000'); | |
| 30 | + $this->actor = FEDIINFO['actor']; | |
| 31 | + $this->actorNick = FEDIINFO['actorNick']; | |
| 32 | + $this->desc = FEDIINFO['desc']; | |
| 33 | + $this->icon = "{$this->domain}".FEDIINFO['icon']; | |
| 34 | + $this->posts = $posts; | |
| 35 | + } | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * ActivityPubアクタープロフィールを受け取る | |
| 39 | + * | |
| 40 | + * @return string アクターオブジェクト | |
| 41 | + * @throws \Exception 公開鍵の読み込みに失敗した場合 | |
| 42 | + */ | |
| 43 | + public function getActor(): string { | |
| 44 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 45 | + $pubkey = file_get_contents(FEDIINFO['pubkey']); | |
| 46 | + if ($pubkey === false) { | |
| 47 | + throw new \Exception('公開鍵の受取に失敗。パス:'.FEDIINFO['pubkey']); | |
| 48 | + } | |
| 49 | + | |
| 50 | + $actor = [ | |
| 51 | + '@context' => [ | |
| 52 | + 'https://www.w3.org/ns/activitystreams', | |
| 53 | + 'https://w3id.org/security/v1', | |
| 54 | + ], | |
| 55 | + 'id' => "{$this->domain}/ap/actor", | |
| 56 | + 'name' => $this->actorNick, | |
| 57 | + 'summary' => $this->desc, | |
| 58 | + 'manuallyApprovesFollowers' => false, | |
| 59 | + 'icon' => [ | |
| 60 | + 'type' => 'Image', | |
| 61 | + 'mediaType' => 'image/png', | |
| 62 | + 'url' => $this->icon, | |
| 63 | + ], | |
| 64 | + 'image' => [ | |
| 65 | + 'type' => 'Image', | |
| 66 | + 'url' => "{$this->domain}/static/article/o_53803618dc1691.28179609.jpg", | |
| 67 | + 'mediaType' => 'image/jpeg', | |
| 68 | + ], | |
| 69 | + 'type' => 'Person', | |
| 70 | + 'url' => "{$this->domain}", | |
| 71 | + 'preferredUsername' => $this->actor, | |
| 72 | + 'inbox' => "{$this->domain}/ap/inbox", | |
| 73 | + 'outbox' => "{$this->domain}/ap/outbox", | |
| 74 | + 'followers' => "{$this->domain}/ap/followers", | |
| 75 | + 'following' => "{$this->domain}/ap/following", | |
| 76 | + 'published' => '2025-03-28T18:00:00Z', | |
| 77 | + 'updated' => gmdate('c'), | |
| 78 | + 'publicKey' => [ | |
| 79 | + 'id' => "{$this->domain}/ap/actor#main-key", | |
| 80 | + 'owner' => "{$this->domain}/ap/actor", | |
| 81 | + 'publicKeyPem' => $pubkey, | |
| 82 | + ], | |
| 83 | + ]; | |
| 84 | + | |
| 85 | + return json_encode($actor, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 86 | + } | |
| 87 | + | |
| 88 | + /** | |
| 89 | + * 特定のUUIDに対応するActivityを取得する | |
| 90 | + * | |
| 91 | + * @param string $uuid 取得するアクティビティのUUID | |
| 92 | + * @return string JSONエンコードされたアクティビティデータ | |
| 93 | + */ | |
| 94 | + public function getActivity(string $uuid): string { | |
| 95 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 96 | + $items = []; | |
| 97 | + | |
| 98 | + foreach ($this->posts as $post) { | |
| 99 | + if ($post['uuid'] != $uuid) continue; | |
| 100 | + | |
| 101 | + $items = [ | |
| 102 | + '@context' => [ | |
| 103 | + 'https://www.w3.org/ns/activitystreams', | |
| 104 | + 'https://w3id.org/security/v1', | |
| 105 | + [ | |
| 106 | + 'Emoji' => 'toot:Emoji', | |
| 107 | + 'EmojiReact' => 'litepub:EmojiReact', | |
| 108 | + 'Hashtag' => 'as:Hashtag', | |
| 109 | + 'litepub' => 'http://litepub.social/ns#', | |
| 110 | + 'sensitive' => 'as:sensitive', | |
| 111 | + 'toot' => 'http://joinmastodon.org/ns#', | |
| 112 | + ], | |
| 113 | + ], | |
| 114 | + 'id' => "{$this->domain}/ap/activities/create/{$post['uuid']}", | |
| 115 | + 'type' => 'Create', | |
| 116 | + 'actor' => "{$this->domain}/ap/actor", | |
| 117 | + 'cc' => [ | |
| 118 | + "{$this->domain}/ap/followers", | |
| 119 | + ], | |
| 120 | + 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 121 | + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 122 | + 'object' => [ | |
| 123 | + 'id' => "{$this->domain}/ap/objects/{$post['uuid']}", | |
| 124 | + 'type' => 'Note', | |
| 125 | + 'name' => $post['title'], | |
| 126 | + 'attributedTo' => "{$this->domain}/ap/actor", | |
| 127 | + 'cc' => [ | |
| 128 | + "{$this->domain}/ap/followers", | |
| 129 | + ], | |
| 130 | + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 131 | + 'content' => $post['preview']."<br /><br /><a href=\"{$this->domain}/blog/{$post['slug']}\">読み続き</a>", | |
| 132 | + 'url' => "{$this->domain}/blog/{$post['slug']}", | |
| 133 | + 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 134 | + 'replies' => "{$this->domain}/ap/objects/{$uuid}/replies", | |
| 135 | + 'sensitive' => false, | |
| 136 | + ], | |
| 137 | + ]; | |
| 138 | + | |
| 139 | + if (isset($post['category']) && !empty($post['category'])) { | |
| 140 | + $item['tag'] = []; | |
| 141 | + foreach ($post['category'] as $cat) { | |
| 142 | + $items['tag'][] = $cat; | |
| 143 | + } | |
| 144 | + } | |
| 145 | + | |
| 146 | + if (isset($post['thumbnail']) && $post['thumbnail'] != '') { | |
| 147 | + $imgurl = "{$this->domain}/static/article/{$post['thumbnail']}"; | |
| 148 | + $imgpath = ROOT."/public/static/article/{$post['thumbnail']}"; | |
| 149 | + $imgraw = file_get_contents($imgpath); | |
| 150 | + | |
| 151 | + $items['attachment'] = [ | |
| 152 | + [ | |
| 153 | + 'digestMultibase' => 'z'.base58btc_encode(hash('sha256', $imgraw, true)), | |
| 154 | + 'mediaType' => mime_content_type($imgpath), | |
| 155 | + 'type' => "Image", | |
| 156 | + 'url' => $imgurl, | |
| 157 | + ], | |
| 158 | + ]; | |
| 159 | + } | |
| 160 | + | |
| 161 | + break; | |
| 162 | + } | |
| 163 | + | |
| 164 | + return json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 165 | + } | |
| 166 | + | |
| 167 | + /** | |
| 168 | + * アウトボックスデータを取得する | |
| 169 | + * | |
| 170 | + * @return string JSONエンコードされたアウトボックスデータ | |
| 171 | + */ | |
| 172 | + public function getOutbox(): string { | |
| 173 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 174 | + $items = []; | |
| 175 | + $counter = 0; | |
| 176 | + | |
| 177 | + foreach ($this->posts as $post) { | |
| 178 | + $uid = $post['uuid']; | |
| 179 | + | |
| 180 | + $items[] = [ | |
| 181 | + '@context' => [ | |
| 182 | + 'https://www.w3.org/ns/activitystreams', | |
| 183 | + 'https://w3id.org/security/v1', | |
| 184 | + [ | |
| 185 | + 'Emoji' => 'toot:Emoji', | |
| 186 | + 'EmojiReact' => 'litepub:EmojiReact', | |
| 187 | + 'Hashtag' => 'as:Hashtag', | |
| 188 | + 'litepub' => 'http://litepub.social/ns#', | |
| 189 | + 'sensitive' => 'as:sensitive', | |
| 190 | + 'toot' => 'http://joinmastodon.org/ns#', | |
| 191 | + ], | |
| 192 | + ], | |
| 193 | + 'id' => "{$this->domain}/ap/activities/create/{$uid}", | |
| 194 | + 'type' => 'Create', | |
| 195 | + 'actor' => "{$this->domain}/ap/actor", | |
| 196 | + 'cc' => [ | |
| 197 | + "{$this->domain}/ap/followers", | |
| 198 | + ], | |
| 199 | + 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 200 | + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 201 | + 'object' => [ | |
| 202 | + 'id' => "{$this->domain}/ap/objects/{$uid}", | |
| 203 | + 'type' => 'Note', | |
| 204 | + 'name' => $post['title'], | |
| 205 | + 'attributedTo' => "{$this->domain}/ap/actor", | |
| 206 | + 'cc' => [ | |
| 207 | + "{$this->domain}/ap/followers", | |
| 208 | + ], | |
| 209 | + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 210 | + 'content' => $post['preview']."<br /><br /><a href=\"{$this->domain}/blog/{$post['slug']}\">読み続き</a>", | |
| 211 | + 'url' => "{$this->domain}/blog/{$post['slug']}", | |
| 212 | + 'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])), | |
| 213 | + 'replies' => "{$this->domain}/ap/objects/{$uid}/replies", | |
| 214 | + 'sensitive' => false, | |
| 215 | + ], | |
| 216 | + ]; | |
| 217 | + | |
| 218 | + if (isset($post['category']) && !empty($post['category'])) { | |
| 219 | + $items[$counter]['tag'] = []; | |
| 220 | + foreach ($post['category'] as $cat) { | |
| 221 | + $items[$counter]['tag'][] = $cat; | |
| 222 | + } | |
| 223 | + } | |
| 224 | + | |
| 225 | + if (isset($post['thumbnail']) && $post['thumbnail'] != '') { | |
| 226 | + $imgurl = "{$this->domain}/static/article/{$post['thumbnail']}"; | |
| 227 | + $imgpath = ROOT."/public/static/article/{$post['thumbnail']}"; | |
| 228 | + $imgraw = file_get_contents($imgpath); | |
| 229 | + | |
| 230 | + $items[$counter]['attachment'] = [ | |
| 231 | + [ | |
| 232 | + 'digestMultibase' => 'z'.base58btc_encode(hash('sha256', $imgraw, true)), | |
| 233 | + 'mediaType' => mime_content_type($imgpath), | |
| 234 | + 'type' => "Image", | |
| 235 | + 'url' => $imgurl, | |
| 236 | + ], | |
| 237 | + ]; | |
| 238 | + } | |
| 239 | + | |
| 240 | + $counter++; | |
| 241 | + } | |
| 242 | + | |
| 243 | + $outbox = [ | |
| 244 | + '@context' => [ | |
| 245 | + 'https://www.w3.org/ns/activitystreams', | |
| 246 | + 'https://w3id.org/security/v1', | |
| 247 | + ], | |
| 248 | + 'id' => "{$this->domain}/ap/outbox", | |
| 249 | + 'type' => 'OrderedCollection', | |
| 250 | + 'totalItems' => count($items), | |
| 251 | + 'orderedItems' => $items, | |
| 252 | + ]; | |
| 253 | + | |
| 254 | + return json_encode($outbox, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 255 | + } | |
| 256 | + | |
| 257 | + /** | |
| 258 | + * WebFingerデータを取得する | |
| 259 | + * | |
| 260 | + * @return string JSONエンコードされたWebFingerデータ | |
| 261 | + */ | |
| 262 | + public function getWebfinger(): string { | |
| 263 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 264 | + $domain = str_replace(['http://', 'https://'], '', $this->domain); | |
| 265 | + $webfinger = [ | |
| 266 | + 'subject' => "acct:{$this->actor}@{$domain}", | |
| 267 | + 'links' => [ | |
| 268 | + [ | |
| 269 | + 'rel' => 'self', | |
| 270 | + 'type' => 'application/activity+json', | |
| 271 | + 'href' => "{$domain}/ap/actor", | |
| 272 | + ], | |
| 273 | + ], | |
| 274 | + ]; | |
| 275 | + | |
| 276 | + return json_encode($webfinger, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 277 | + } | |
| 278 | + | |
| 279 | + /** | |
| 280 | + * フォロワーのリストを取得する | |
| 281 | + * | |
| 282 | + * @return string JSONエンコードされたフォロワーのリスト | |
| 283 | + */ | |
| 284 | + public function getFollowers(): string { | |
| 285 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 286 | + $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt'))); | |
| 287 | + | |
| 288 | + $followers = [ | |
| 289 | + '@context' => [ | |
| 290 | + 'https://www.w3.org/ns/activitystreams', | |
| 291 | + 'https://w3id.org/security/v1', | |
| 292 | + ], | |
| 293 | + 'id' => "{$this->domain}/ap/followers", | |
| 294 | + 'type' => 'OrderredCollection', | |
| 295 | + 'totalItems' => count($f), | |
| 296 | + 'orderedItems' => $f, | |
| 297 | + ]; | |
| 298 | + | |
| 299 | + return json_encode($followers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 300 | + } | |
| 301 | + | |
| 302 | + /** | |
| 303 | + * フォローしているアカウントのリストを取得する | |
| 304 | + * | |
| 305 | + * @return string JSONエンコードされたフォローリスト | |
| 306 | + */ | |
| 307 | + public function getFollowing(): string { | |
| 308 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 309 | + $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/following.txt'))); | |
| 310 | + | |
| 311 | + $following = [ | |
| 312 | + '@context' => [ | |
| 313 | + 'https://www.w3.org/ns/activitystreams', | |
| 314 | + 'https://w3id.org/security/v1', | |
| 315 | + ], | |
| 316 | + 'id' => "{$this->domain}/ap/following", | |
| 317 | + 'type' => 'OrderredCollection', | |
| 318 | + 'totalItems' => count($f), | |
| 319 | + 'orderedItems' => $f, | |
| 320 | + ]; | |
| 321 | + | |
| 322 | + return json_encode($following, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 323 | + } | |
| 324 | + | |
| 325 | + /** | |
| 326 | + * インボックスにアクティビティを投稿する | |
| 327 | + * | |
| 328 | + * @param array $activity 処理するアクティビティデータ | |
| 329 | + * @return void | |
| 330 | + */ | |
| 331 | + public function postInbox(array $activity): void { | |
| 332 | + if (!ACTIVITYPUB_ENABLED) return; | |
| 333 | + switch ($activity['type']) { | |
| 334 | + case 'Follow': | |
| 335 | + $this->acceptFollower($activity); | |
| 336 | + break; | |
| 337 | + default: | |
| 338 | + header('HTTP/1.1 501 Not Implemented'); | |
| 339 | + header('Content-Type: application/activity+json'); | |
| 340 | + echo json_encode(['error' => '未対応なアクティビティタイプ: '.$activity['type']]); | |
| 341 | + exit; | |
| 342 | + } | |
| 343 | + | |
| 344 | + header('HTTP/1.1 200 OK'); | |
| 345 | + header('Content-Type: application/activity+json'); | |
| 346 | + echo json_encode(['status' => 'OK'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 347 | + exit; | |
| 348 | + } | |
| 349 | + | |
| 350 | + /** | |
| 351 | + * アクタープロフィールの更新アクティビティを作成する | |
| 352 | + * | |
| 353 | + * @return string JSONエンコードされた更新アクティビティ | |
| 354 | + */ | |
| 355 | + public function update(): string { | |
| 356 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 357 | + $update = [ | |
| 358 | + '@context' => [ | |
| 359 | + 'https://www.w3.org/ns/activitystreams', | |
| 360 | + 'https://w3id.org/security/v1', | |
| 361 | + ], | |
| 362 | + 'id' => "{$this->domain}/ap/activities/update/".uuid(), | |
| 363 | + 'type' => 'Update', | |
| 364 | + 'actor' => "{$this->domain}/ap/actor", | |
| 365 | + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], | |
| 366 | + 'object' => json_decode($this->getActor(), true), | |
| 367 | + ]; | |
| 368 | + | |
| 369 | + return json_encode($update, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 370 | + } | |
| 371 | + | |
| 372 | + /** | |
| 373 | + * アクター更新をフォロワーに送信する | |
| 374 | + * | |
| 375 | + * @param array $params パラメータ配列 | |
| 376 | + * @return void | |
| 377 | + */ | |
| 378 | + public function sendActorUpdate(array $params): void { | |
| 379 | + if (!ACTIVITYPUB_ENABLED) return; | |
| 380 | + $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt'))); | |
| 381 | + $ap = new Activitypub(); | |
| 382 | + $inboxes = implode("\n", $f); | |
| 383 | + $update = json_decode($ap->update(), true); | |
| 384 | + | |
| 385 | + foreach ($f as $inbox) $this->sendActivity($inbox, $update); | |
| 386 | + } | |
| 387 | + | |
| 388 | + // 機能性メソッド | |
| 389 | + | |
| 390 | + /** | |
| 391 | + * 指定されたインボックスURLにアクティビティを送信する | |
| 392 | + * | |
| 393 | + * @param string $inboxUrl 送信先のインボックスURL | |
| 394 | + * @param array $activity 送信するアクティビティデータ | |
| 395 | + * @return void | |
| 396 | + */ | |
| 397 | + private function sendActivity(string $inboxUrl, array $activity): void { | |
| 398 | + $privFile = FEDIINFO['privkey']; | |
| 399 | + $priv = file_get_contents($privFile); | |
| 400 | + if ($priv === false) { | |
| 401 | + logger(\LogType::ActivityPub, "エラー:秘密鍵「{$privFile}」の読込に失敗"); | |
| 402 | + header('HTTP/1.1 500 Internal Server Error'); | |
| 403 | + header('Content-Type: application/activity+json'); | |
| 404 | + echo json_encode(['error' => '秘密鍵の読込に失敗']); | |
| 405 | + exit; | |
| 406 | + } | |
| 407 | + | |
| 408 | + $body = json_encode($activity, JSON_UNESCAPED_SLASHES); | |
| 409 | + $digest = base64_encode(hash('sha256', $body, true)); | |
| 410 | + $date = gmdate('D, d M Y H:i:s \G\M\T'); | |
| 411 | + $host = parse_url($inboxUrl, PHP_URL_HOST); | |
| 412 | + | |
| 413 | + $headers = [ | |
| 414 | + 'Host' => $host, | |
| 415 | + 'Date' => $date, | |
| 416 | + 'Content-Type' => 'application/activity+json', | |
| 417 | + 'Digest' => "SHA-256=$digest", | |
| 418 | + ]; | |
| 419 | + | |
| 420 | + $stringToSign = "host: {$headers['Host']}\n"."date: {$headers['Date']}\n"."digest: {$headers['Digest']}"; | |
| 421 | + logger(\LogType::ActivityPub, "署名対象: {$stringToSign}"); | |
| 422 | + | |
| 423 | + if (!openssl_sign($stringToSign, $signature, $priv, OPENSSL_ALGO_SHA256)) { | |
| 424 | + $error = openssl_error_string(); | |
| 425 | + logger(\LogType::ActivityPub, "エラー:署名に失敗: {$error}"); | |
| 426 | + header('HTTP/1.1 500 Internal Server Error'); | |
| 427 | + header('Content-Type: application/activity+json'); | |
| 428 | + echo json_encode(['error' => '署名に失敗']); | |
| 429 | + exit; | |
| 430 | + } | |
| 431 | + | |
| 432 | + $sigValue = base64_encode($signature); | |
| 433 | + $headers['Signature'] = "keyId=\"{$this->domain}/ap/actor#main-key\","; | |
| 434 | + $headers['Signature'] .= 'algorithm="rsa-sha256",'; | |
| 435 | + $headers['Signature'] .= 'headers="host date digest",'; | |
| 436 | + $headers['Signature'] .= 'signature="'.$sigValue.'"'; | |
| 437 | + logger(\LogType::ActivityPub, "署名: {$headers['Signature']}\n送信データ: {$body}"); | |
| 438 | + | |
| 439 | + $curl = new Curl($inboxUrl); | |
| 440 | + $curl->setMethod('POST') | |
| 441 | + ->setPostRaw($body) | |
| 442 | + ->setHeaders(array_map(fn($k, $v) => "$k: $v", array_keys($headers), $headers)) | |
| 443 | + ->setCaInfo('/etc/ssl/cert.pem') | |
| 444 | + ->setVerbose(true) | |
| 445 | + ->setStderr(fopen(ROOT.'/log/ap_log.txt', 'a')); | |
| 446 | + | |
| 447 | + $success = $curl->execute(); | |
| 448 | + $res = $curl->getResponseBody(); | |
| 449 | + $code = $curl->getResponseCode(); | |
| 450 | + $err = $curl->getError(); | |
| 451 | + | |
| 452 | + var_dump(print_r($res)); | |
| 453 | + logger(\LogType::ActivityPub, "アクティビティは「{$inboxUrl}」に送信しました: HTTP {$code}"); | |
| 454 | + logger(\LogType::ActivityPub, "エラー: {$err}"); | |
| 455 | + logger(\LogType::ActivityPub, "レスポンス: {$res}"); | |
| 456 | + } | |
| 457 | + | |
| 458 | + /** | |
| 459 | + * フォロワーを受け入れる | |
| 460 | + * | |
| 461 | + * @param array $activity フォローアクティビティデータ | |
| 462 | + * @return void | |
| 463 | + */ | |
| 464 | + private function acceptFollower(array $activity): void { | |
| 465 | + $followerActor = $activity['actor'] ?? null; | |
| 466 | + if (!$followerActor) { | |
| 467 | + header('HTTP/1.1 400 Bad Request'); | |
| 468 | + header('Content-Type: application/activity+json'); | |
| 469 | + echo json_encode(['error' => 'アクターがない']); | |
| 470 | + exit; | |
| 471 | + } | |
| 472 | + | |
| 473 | + $this->storeFollower($followerActor); | |
| 474 | + | |
| 475 | + $inbox = $this->getInboxFromActor($followerActor); | |
| 476 | + if (!$inbox) { | |
| 477 | + header('HTTP/1.1 500 Internal Server Error'); | |
| 478 | + header('Content-Type: application/activity+json'); | |
| 479 | + echo json_encode(['error' => 'フォロワーの受付ボックスの受取に失敗']); | |
| 480 | + exit; | |
| 481 | + } | |
| 482 | + | |
| 483 | + $accept = [ | |
| 484 | + '@context' => [ | |
| 485 | + 'https://www.w3.org/ns/activitystreams', | |
| 486 | + 'https://w3id.org/security/v1', | |
| 487 | + ], | |
| 488 | + 'id' => "{$this->domain}/ap/activities/".uniqid(), | |
| 489 | + 'type' => 'Accept', | |
| 490 | + 'actor' => "{$this->domain}/ap/actor", | |
| 491 | + 'object' => $activity, | |
| 492 | + ]; | |
| 493 | + | |
| 494 | + $this->sendActivity($inbox, $accept); | |
| 495 | + } | |
| 496 | + | |
| 497 | + /** | |
| 498 | + * フォロワーをストレージに保存する | |
| 499 | + * | |
| 500 | + * @param string $followerActor フォロワーのアクターURL | |
| 501 | + * @return void | |
| 502 | + */ | |
| 503 | + private function storeFollower(string $followerActor): void { | |
| 504 | + $file = ROOT.'/data/followers.txt'; | |
| 505 | + if (!file_exists($file)) { | |
| 506 | + touch($file); | |
| 507 | + chmod($file, 0644); | |
| 508 | + } | |
| 509 | + | |
| 510 | + $followers = $this->getFollowersList(); | |
| 511 | + if (!in_array($followerActor, $followers)) file_put_contents($file, "$followerActor\n", FILE_APPEND); | |
| 512 | + } | |
| 513 | + | |
| 514 | + /** | |
| 515 | + * フォロワーのリストを配列として取得する | |
| 516 | + * | |
| 517 | + * @return array フォロワーのURLの配列 | |
| 518 | + */ | |
| 519 | + private function getFollowersList(): array { | |
| 520 | + $file = ROOT.'/data/followers.txt'; | |
| 521 | + $f = array_filter(explode("\n", file_get_contents($file))); | |
| 522 | + return file_exists($file) ? array_filter(explode("\n", file_get_contents($file))) : []; | |
| 523 | + } | |
| 524 | + | |
| 525 | + /** | |
| 526 | + * アクターのインボックスURLを取得する | |
| 527 | + * | |
| 528 | + * @param string $actor アクターのURL | |
| 529 | + * @return string|null インボックスURL、取得に失敗した場合はnull | |
| 530 | + */ | |
| 531 | + private function getInboxFromActor(string $actor): ?string { | |
| 532 | + $curl = new Curl($actor); | |
| 533 | + $curl->setHeaders(['Accept: application/activity+json']) | |
| 534 | + ->setFollowRedirects(true) | |
| 535 | + ->setMaxRedirects(5) | |
| 536 | + ->setCaInfo('/etc/ssl/cert.pem'); | |
| 537 | + | |
| 538 | + logger(\LogType::ActivityPub, "アクターURLにリクエスト: {$actor}"); | |
| 539 | + $success = $curl->execute(); | |
| 540 | + if (!$success) { | |
| 541 | + logger(\LogType::ActivityPub, "アクターリクエストに失敗: ".$curl->getError()); | |
| 542 | + return null; | |
| 543 | + } | |
| 544 | + | |
| 545 | + $res = $curl->getResponseBody(); | |
| 546 | + $code = $curl->getResponseCode(); | |
| 547 | + $err = $curl->getError(); | |
| 548 | + | |
| 549 | + if ($code !== 200) { | |
| 550 | + logger(\LogType::ActivityPub, "アクター取得に失敗: HTTP {$code}, エラー: {$err}"); | |
| 551 | + return null; | |
| 552 | + } | |
| 553 | + | |
| 554 | + $data = json_decode($res, true); | |
| 555 | + return $data['inbox'] ?? null; | |
| 556 | + } | |
| 557 | +} |
| @@ -0,0 +1,451 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +class Auth { | |
| 5 | + private int $id; | |
| 6 | + | |
| 7 | + private \stdClass $user; | |
| 8 | + private \stdClass $pubUser; | |
| 9 | + private ?string $token; | |
| 10 | + | |
| 11 | + protected string $dataDir = ROOT."/data/user/"; | |
| 12 | + protected string $assDir = ROOT."/public/static/user/"; | |
| 13 | + protected int $minPassLen = 20; | |
| 14 | + protected int $tokenDuration = 31536000; // 1年間 | |
| 15 | + protected string|int|null $algo = PASSWORD_ARGON2ID; | |
| 16 | + | |
| 17 | + public function __construct(?string $username = null) { | |
| 18 | + if (!AUTH_ENABLED) return; | |
| 19 | + $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); | |
| 20 | + $this->token = \getcookie('kerozen'); | |
| 21 | + $this->id = $this->getUserId($username, $this->token); | |
| 22 | + if ($this->id > 0) $this->user = $this->getUserData(); | |
| 23 | + } | |
| 24 | + | |
| 25 | + public function getLoggedInUser(): \stdClass|null { | |
| 26 | + if ($this->id === 0) return null; | |
| 27 | + $this->pubUser = $this->user; | |
| 28 | + unset($this->pubUser->password); | |
| 29 | + unset($this->pubUser->tokens); | |
| 30 | + return $this->pubUser; | |
| 31 | + } | |
| 32 | + | |
| 33 | + public function getUser(string $name): \stdClass|null { | |
| 34 | + $user = $this->getUserDataFromName($name); | |
| 35 | + if (!$user) return null; | |
| 36 | + unset($user->password); | |
| 37 | + unset($user->tokens); | |
| 38 | + $myself = $this->getUserData(); | |
| 39 | + if (!$myself || ($myself->id != $user->id && $myself->role < \Roles::STAFF)) $user->email = '(秘密)'; | |
| 40 | + $user->name = namecolor($user); | |
| 41 | + $user->regDate = date('Y年m月d日', $user->regDate); | |
| 42 | + $user->gender = $user->gender === \Gender::MALE ? '男' : ($user->gender === \Gender::FEMALE ? '女' : '不明'); | |
| 43 | + | |
| 44 | + switch (true) { | |
| 45 | + case ($user->role & \Roles::ADMIN): | |
| 46 | + $user->role = '管理者'; | |
| 47 | + break; | |
| 48 | + case ($user->role & \Roles::STAFF): | |
| 49 | + $user->role = '076スタジオの会社員'; | |
| 50 | + break; | |
| 51 | + case ($user->role & (\Roles::FSUBSCRIBER | \Roles::PSUBSCRIBER | \Roles::NSUBSCRIBER | \Roles::SUBSCRIBER)): | |
| 52 | + $user->role = '支援者♡'; | |
| 53 | + break; | |
| 54 | + case ($user->role & \Roles::BANNED): | |
| 55 | + $user->role = 'BANされた'; | |
| 56 | + break; | |
| 57 | + default: | |
| 58 | + $user->role = 'ユーザー'; | |
| 59 | + break; | |
| 60 | + } | |
| 61 | + | |
| 62 | + $user->avatar = '/static/user/'.($user->avatar === '' ? 'noicon.png' : $user->username.'/'.$user->avatar); | |
| 63 | + $user->altName = $user->displayname ?: $user->username; | |
| 64 | + unset($user->username); | |
| 65 | + unset($user->displayname); | |
| 66 | + unset($user->namecolor); | |
| 67 | + return $user; | |
| 68 | + } | |
| 69 | + | |
| 70 | + public function setToken(string $username, #[SensitiveParameter] string $password): \Result { | |
| 71 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 72 | + $userData = $this->getUserData(); | |
| 73 | + if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = []; | |
| 74 | + | |
| 75 | + $verify = $this->verifyLogin($username, $password); | |
| 76 | + if (!$verify->isSuccess) { | |
| 77 | + return \Result::error($verify->message); | |
| 78 | + } | |
| 79 | + | |
| 80 | + $ip = $_SERVER['REMOTE_ADDR']; | |
| 81 | + if (!empty($_SERVER['HTTP_X_FORMARDED_FOR'])) { | |
| 82 | + $ipList = explode(',', $_SERVER['HTTP_X_FORMARDED_FOR']); | |
| 83 | + $ip = trim($ipList[0]); | |
| 84 | + } | |
| 85 | + | |
| 86 | + $userData = $this->purgeOldTokens($userData); | |
| 87 | + $token = bin2hex(random_bytes(64)); | |
| 88 | + $expire = time() + $this->tokenDuration; | |
| 89 | + $newToken = [ | |
| 90 | + 'token' => $token, | |
| 91 | + 'ip' => $ip, | |
| 92 | + 'createDate' => time(), | |
| 93 | + 'expDate' => $expire, | |
| 94 | + 'lastDate' => time(), | |
| 95 | + 'userAgent' => $_SERVER['HTTP_USER_AGENT'] ?? '不明' | |
| 96 | + ]; | |
| 97 | + | |
| 98 | + $userData->tokens[] = $newToken; | |
| 99 | + | |
| 100 | + $path = $this->dataDir.$this->id.'.'.$userData->username.'.json'; | |
| 101 | + $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 102 | + | |
| 103 | + if (file_put_contents($path, $json) === false) { | |
| 104 | + logger(\LogType::Auth, '【setToken】JSONファイルにユーザーデータを保存出来なかった。パス:'.$path); | |
| 105 | + return \Result::error('エラー:ユーザーデータの保存に失敗。'); | |
| 106 | + } | |
| 107 | + | |
| 108 | + if (!setcookie('kerozen', $token, [ | |
| 109 | + 'expires' => $expire, | |
| 110 | + 'path' => '/', | |
| 111 | + 'domain' => $_SERVER['SERVER_NAME'], | |
| 112 | + 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), | |
| 113 | + 'httponly' => true, | |
| 114 | + 'samesite' => 'Strict' | |
| 115 | + ])) { | |
| 116 | + logger(\LogType::Auth, '【setToken】クッキーを設定出来なかった。トークン:'.$token); | |
| 117 | + return \Result::error('エラー:クッキーを設定に失敗。'); | |
| 118 | + } | |
| 119 | + | |
| 120 | + return \Result::success('ログイン成功'); | |
| 121 | + } | |
| 122 | + | |
| 123 | + public function getToken(): string { | |
| 124 | + if (!AUTH_ENABLED) return ''; | |
| 125 | + $userData = $this->getUserData(); | |
| 126 | + if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = []; | |
| 127 | + $userData = $this->purgeOldTokens($userData); | |
| 128 | + } | |
| 129 | + | |
| 130 | + public function logout(?string $token = null): \Result { | |
| 131 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 132 | + $userData = $this->getUserData(); | |
| 133 | + | |
| 134 | + if (!$token) { | |
| 135 | + $userData->tokens = array_filter($userData->tokens, function($t): bool { | |
| 136 | + return ($t->expires ?? 0) > time(); | |
| 137 | + }); | |
| 138 | + } | |
| 139 | + | |
| 140 | + setcookie('kerozen', null, [ | |
| 141 | + 'expires' => 0, | |
| 142 | + 'path' => '/', | |
| 143 | + 'domain' => $_SERVER['SERVER_NAME'], | |
| 144 | + 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), | |
| 145 | + 'httponly' => true, | |
| 146 | + 'samesite' => 'Strict' | |
| 147 | + ]); | |
| 148 | + | |
| 149 | + return \Result::success(); | |
| 150 | + } | |
| 151 | + | |
| 152 | + public function isUserExist(?string $username = null): \Result { | |
| 153 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 154 | + if (null === $username) return \Result::Error('エラー:ユーザー名をご入力下さい。'); | |
| 155 | + $userList = scandir($this->dataDir); | |
| 156 | + | |
| 157 | + foreach ($userList as $list) { | |
| 158 | + if ($list === '.kara' || $list === '.' || $list === '..') continue; | |
| 159 | + $file = str_replace('.json', '', $list); | |
| 160 | + $user = explode('.', $file)[1]; | |
| 161 | + if ($username === $user) return \Result::Success("ユーザー「{$username}」は既に存在します。"); | |
| 162 | + } | |
| 163 | + | |
| 164 | + return \Result::Error(''); | |
| 165 | + } | |
| 166 | + | |
| 167 | + public function mkUser(?string $username = null, #[SensitiveParameter] ?string $password = null, ?string $passwordVerify = null, ?string $email = null): \Result { | |
| 168 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 169 | + if (!AUTH_REGISTER_ENABLED) return \Result::Error('ユーザー登録は無効です。'); | |
| 170 | + $resUsr = $this->verifyUsername($username); | |
| 171 | + $resPwd = $this->verifyPassword($password, $passwordVerify); | |
| 172 | + $resEml = $this->verifyEmail($email); | |
| 173 | + | |
| 174 | + $err = ''; | |
| 175 | + | |
| 176 | + if (!$resUsr->isSuccess) $err .= $resUsr->message."<br />"; | |
| 177 | + if (!$resPwd->isSuccess) $err .= $resPwd->message."<br />"; | |
| 178 | + if (!$resEml->isSuccess) $err .= $resEml->message."<br />"; | |
| 179 | + | |
| 180 | + if ($err != '') return \Result::Error($err); | |
| 181 | + $err = ''; | |
| 182 | + | |
| 183 | + if (!mkdir($this->assDir.$username, 0755)) { | |
| 184 | + logger(\LogType::Auth, '【mkUser】アセットディレクトリを作成出来なかった。chownをご確認下さい。パス:'.$this->assDir.$username); | |
| 185 | + return \Result::Error('エラー:ユーザーのアイコンディレクトリの作成に失敗。'); | |
| 186 | + } | |
| 187 | + | |
| 188 | + $file = scandir($this->dataDir); | |
| 189 | + $lastId = 0; | |
| 190 | + if ($file) { | |
| 191 | + foreach ($file as $f) { | |
| 192 | + if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 193 | + $base = substr($f, 0, -5); // .jsonの削除 | |
| 194 | + $dot = strpos($base, '.'); | |
| 195 | + if (!$dot) continue; | |
| 196 | + $id = substr($base, 0, $dot); | |
| 197 | + if (ctype_digit($id)) { | |
| 198 | + if ((int)$id > $lastId) $lastId = $id; | |
| 199 | + } | |
| 200 | + } | |
| 201 | + $lastId++; | |
| 202 | + } | |
| 203 | + | |
| 204 | + $user = new \stdClass; | |
| 205 | + $user->id = $lastId; | |
| 206 | + $user->username = $username; | |
| 207 | + $user->password = password_hash($password, $this->algo); | |
| 208 | + $user->avatar = ''; | |
| 209 | + $user->email = $email; | |
| 210 | + $user->regDate = time(); | |
| 211 | + $user->namecolor = ''; | |
| 212 | + $user->displayname = ''; | |
| 213 | + $user->gender = \Gender::UNKNOWN; | |
| 214 | + $user->role = \Roles::MEMBER; | |
| 215 | + $user->tokens = []; | |
| 216 | + | |
| 217 | + $path = "{$this->dataDir}{$lastId}.{$username}.json"; | |
| 218 | + $json = json_encode($user, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 219 | + | |
| 220 | + if (file_put_contents($path, $json) === false) { | |
| 221 | + logger(\LogType::Auth, '【mkUser】JSONファイルを作成出来なかった。パス:'.$path); | |
| 222 | + rmdir($this->assDir.$username); | |
| 223 | + return \Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 224 | + } | |
| 225 | + | |
| 226 | + return \Result::Success(); | |
| 227 | + } | |
| 228 | + | |
| 229 | + //////////////////// | |
| 230 | + | |
| 231 | + private function verifyLogin(string $username, #[SensitiveParameter] string $password): \Result { | |
| 232 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 233 | + $userData = $this->getUserData(); | |
| 234 | + if ($username !== $userData->username || !password_verify($password, $userData->password)) { | |
| 235 | + return \Result::Error('エラー:ユーザー名又はパスワードが一致していません。'); | |
| 236 | + } | |
| 237 | + | |
| 238 | + if (password_needs_rehash($userData->password, $this->algo)) { | |
| 239 | + $userData->password = password_hash($password, $this->algo); | |
| 240 | + $path = $this->dataDir.$this->id.'.'.$userData->username.'.json'; | |
| 241 | + $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 242 | + | |
| 243 | + if (file_put_contents($path, $json) === false) { | |
| 244 | + logger(\LogType::Auth, '【verifyLogin】JSONファイルを変更出来なかった。chownをご確認下さい。パス:'.$path); | |
| 245 | + return \Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 246 | + } | |
| 247 | + } | |
| 248 | + | |
| 249 | + return \Result::Success(); | |
| 250 | + } | |
| 251 | + | |
| 252 | + private function purgeOldTokens(\stdClass $userData): \stdClass|\Result { | |
| 253 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 254 | + // 有効期限切れたトークンの削除 | |
| 255 | + $out = $userData; | |
| 256 | + $out->tokens = array_filter($userData->tokens, function($t): bool { | |
| 257 | + return ($t->expDate ?? 0) > time(); | |
| 258 | + }); | |
| 259 | + | |
| 260 | + $path = $this->dataDir.$userData->id.'.'.$userData->username.'.json'; | |
| 261 | + $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 262 | + | |
| 263 | + if (file_put_contents($path, $json) === false) { | |
| 264 | + logger(\LogType::Auth, '【purgeOldTokens】JSONファイルを変更出来なかった。chownをご確認下さい。パス:'.$path); | |
| 265 | + return \Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 266 | + } | |
| 267 | + | |
| 268 | + return $out; | |
| 269 | + } | |
| 270 | + | |
| 271 | + private function getUserId(?string $username = null, ?string $token = null): int { | |
| 272 | + if (!AUTH_ENABLED) return 0; | |
| 273 | + if (!$username && !$token) return 0; | |
| 274 | + $file = scandir($this->dataDir); | |
| 275 | + $matches = []; | |
| 276 | + $id = 0; | |
| 277 | + | |
| 278 | + foreach ($file as $f) { | |
| 279 | + if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 280 | + | |
| 281 | + $path = "{$this->dataDir}{$f}"; | |
| 282 | + if (!is_file($path)) continue; | |
| 283 | + $content = file_get_contents($path); | |
| 284 | + if ($token && str_contains($content, $token)) { | |
| 285 | + $matches[] = $path; | |
| 286 | + break; | |
| 287 | + } else if (!$token && str_contains($content, '"username": "'.$username.'",')) { | |
| 288 | + $matches[] = $path; | |
| 289 | + break; | |
| 290 | + } | |
| 291 | + } | |
| 292 | + if (!isset($matches[0])) return 0; | |
| 293 | + $id = (int)preg_replace('/^(.*?)\.(.*?)$/', "$1", str_replace($this->dataDir, '', $matches[0])); | |
| 294 | + | |
| 295 | + return $id; | |
| 296 | + } | |
| 297 | + | |
| 298 | + private function getUserData(): ?\stdClass { | |
| 299 | + if (!AUTH_ENABLED) return new \stdClass; | |
| 300 | + $file = scandir($this->dataDir); | |
| 301 | + $userFile = ""; | |
| 302 | + $matches = []; | |
| 303 | + | |
| 304 | + foreach ($file as $f) { | |
| 305 | + if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 306 | + if (preg_match('/^'.$this->id.'\.(.*?)\.json$/', $f, $matches)) { | |
| 307 | + $userFile = $matches[0]; | |
| 308 | + break; | |
| 309 | + } | |
| 310 | + } | |
| 311 | + $path = $this->dataDir.$userFile; | |
| 312 | + unset($file, $userFile); | |
| 313 | + | |
| 314 | + if (!file_exists($path) || !is_readable($path)) { | |
| 315 | + kys('エラー'); | |
| 316 | + } | |
| 317 | + | |
| 318 | + $fp = fopen($path, 'r'); | |
| 319 | + assert_not_null($fp); | |
| 320 | + if (!$fp) kys('ユーザーが存在しない。'); | |
| 321 | + $lines = ""; | |
| 322 | + while (($buf = fgets($fp, 4096)) !== false) $lines .= $buf.PHP_EOL; | |
| 323 | + if (!feof($fp)) kys("エラー:不明なエラー"); | |
| 324 | + fclose($fp); | |
| 325 | + unset($path); | |
| 326 | + return json_decode($lines); | |
| 327 | + } | |
| 328 | + | |
| 329 | + private function getUserDataFromName(string $name): \stdClass|null { | |
| 330 | + if (!AUTH_ENABLED) return new \stdClass; | |
| 331 | + $file = scandir($this->dataDir); | |
| 332 | + $userFile = ""; | |
| 333 | + $matches = []; | |
| 334 | + | |
| 335 | + foreach ($file as $f) { | |
| 336 | + if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 337 | + if (preg_match('/^(.*?)\.(.*?)\.json$/', $f, $matches)) { | |
| 338 | + if (isset($matches[2]) && $matches[2] === $name) { | |
| 339 | + $userFile = $matches[0]; | |
| 340 | + break; | |
| 341 | + } | |
| 342 | + } | |
| 343 | + } | |
| 344 | + if (!$userFile) return null; | |
| 345 | + $path = $this->dataDir.$userFile; | |
| 346 | + unset($file, $userFile); | |
| 347 | + | |
| 348 | + if (!file_exists($path) || !is_readable($path)) { | |
| 349 | + kys('エラー'); | |
| 350 | + } | |
| 351 | + | |
| 352 | + $fp = fopen($path, 'r'); | |
| 353 | + assert_not_null($fp); | |
| 354 | + if (!$fp) kys('ユーザーが存在しない。'); | |
| 355 | + $lines = ""; | |
| 356 | + while (($buf = fgets($fp, 4096)) !== false) $lines .= $buf.PHP_EOL; | |
| 357 | + if (!feof($fp)) kys("エラー:不明なエラー"); | |
| 358 | + fclose($fp); | |
| 359 | + unset($path); | |
| 360 | + return json_decode($lines); | |
| 361 | + } | |
| 362 | + | |
| 363 | + private function isEmailExist(?string $email = null): \Result { | |
| 364 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 365 | + $userList = scandir($this->dataDir); | |
| 366 | + $matches = []; | |
| 367 | + | |
| 368 | + foreach ($userList as $f) { | |
| 369 | + if ($f === '.kara' || $f === '.' || $f === '..') continue; | |
| 370 | + $path = "{$this->dataDir}{$f}"; | |
| 371 | + if (!is_file($path)) continue; | |
| 372 | + $content = file_get_contents($path); | |
| 373 | + $file = str_replace('.json', '', $f); | |
| 374 | + if (str_contains($content, '"email": "'.$email.'",')) { | |
| 375 | + $matches[] = $email; | |
| 376 | + } | |
| 377 | + if (count($matches) > 0) return \Result::Error("ユーザー「{$email}」は既に存在します。"); | |
| 378 | + } | |
| 379 | + | |
| 380 | + return \Result::Success('エラー:メールアドレスが存在しません。'); | |
| 381 | + } | |
| 382 | + | |
| 383 | + private function verifyEmail(?string $email = null): \Result { | |
| 384 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 385 | + if (null === $email || '' === $email) return \Result::Error('エラー:メールアドレスをご入力下さい。'); | |
| 386 | + if (strpos($email, '@') === false) return \Result::Error('エラー:メールアドレスは不正です。'); | |
| 387 | + | |
| 388 | + $domain = explode('@', $email)[1]; | |
| 389 | + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { | |
| 390 | + return \Result::Error('エラー:メールアドレスは不正です。'); | |
| 391 | + } | |
| 392 | + | |
| 393 | + if (!checkdnsrr($domain, 'MX')) { | |
| 394 | + return \Result::Error('エラー:メールアドレスは不正です。'); | |
| 395 | + } | |
| 396 | + | |
| 397 | + $res = $this->isEmailExist($email); | |
| 398 | + if (!$res->isSuccess) return \Result::Error($res->message); | |
| 399 | + | |
| 400 | + return \Result::Success(); | |
| 401 | + } | |
| 402 | + | |
| 403 | + private function verifyUsername(?string $username): \Result { | |
| 404 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 405 | + if (null === $username || '' === $username) return \Result::Error('エラー:ユーザー名をご入力下さい。'); | |
| 406 | + if (strlen($username) < 6) return \Result::Error('エラー:ユーザー名は6文字以上をご入力下さい。'); | |
| 407 | + | |
| 408 | + $res = $this->isUserExist($username); | |
| 409 | + if ($res->isSuccess) return \Result::Error($res->message); | |
| 410 | + | |
| 411 | + $accepted = 'A-Za-z0-9'; | |
| 412 | + if (preg_match("/[^{$accepted}]/", $username)) { | |
| 413 | + return \Result::Error('エラー:ユーザー名に不正な文字が含みます。'); | |
| 414 | + } | |
| 415 | + | |
| 416 | + return \Result::Success(); | |
| 417 | + } | |
| 418 | + | |
| 419 | + private function verifyPassword(#[SensitiveParameter] ?string $password = null, #[SensitiveParameter] ?string $passwordVerify = null): \Result { | |
| 420 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 421 | + if (null === $password || '' === $password) return \Result::Error('エラー:パスワードをご入力下さい。'); | |
| 422 | + if ($password !== $passwordVerify) return \Result::Error('エラー:パスワードは一致していません。'); | |
| 423 | + | |
| 424 | + $res = $this->checkPasswordStandards($password); | |
| 425 | + if (!$res->isSuccess) { | |
| 426 | + return \Result::Error($res->message); | |
| 427 | + } | |
| 428 | + | |
| 429 | + return \Result::Success(); | |
| 430 | + } | |
| 431 | + | |
| 432 | + private function checkPasswordStandards(#[SensitiveParameter] ?string $password = null): \Result { | |
| 433 | + if (!AUTH_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 434 | + if (strlen($password) < $this->minPassLen) return \Result::Error("エラー:パスワードは{$this->minPassLen}以上をご入力下さい。"); | |
| 435 | + | |
| 436 | + if (!\countmatch($password)) { | |
| 437 | + return \Result::Error('エラー:パスワードに不正な文字が含みます。'); | |
| 438 | + } | |
| 439 | + | |
| 440 | + $countUpper = preg_match_all('/[A-Z]/', $password) < 2; | |
| 441 | + $countLower = preg_match_all('/[a-z]/', $password) < 2; | |
| 442 | + $countDigit = preg_match_all('/[0-9]/', $password) < 2; | |
| 443 | + $countSymbol = \count_special_chars($password) < 2; | |
| 444 | + | |
| 445 | + if ($countUpper || $countLower || $countDigit || $countSymbol) { | |
| 446 | + return \Result::Error('エラー:パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。'); | |
| 447 | + } | |
| 448 | + | |
| 449 | + return \Result::Success(); | |
| 450 | + } | |
| 451 | +}; |
| @@ -0,0 +1,693 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * php_curlへの依存を排除するための独自のCURL実装 | |
| 6 | + */ | |
| 7 | +class Curl { | |
| 8 | + // リクエスト関連のプロパティ | |
| 9 | + private string $url = ''; | |
| 10 | + private string $method = 'GET'; | |
| 11 | + private int $timeout = 30; | |
| 12 | + private array $headers = []; | |
| 13 | + private array $cookies = []; | |
| 14 | + private array $postFields = []; | |
| 15 | + private string $postRaw = ''; | |
| 16 | + private string $userAgent = 'LittleBeast/1.0'; | |
| 17 | + private bool $followRedirects = true; | |
| 18 | + private int $maxRedirects = 5; | |
| 19 | + private bool $verbose = false; | |
| 20 | + private $stderr = null; | |
| 21 | + private string $caInfoPath = ''; | |
| 22 | + private bool $verifySSL = true; | |
| 23 | + private string $username = ''; | |
| 24 | + private string $password = ''; | |
| 25 | + private string $referer = ''; | |
| 26 | + | |
| 27 | + // レスポンス関連のプロパティ | |
| 28 | + private array $responseHeaders = []; | |
| 29 | + private string $responseBody = ''; | |
| 30 | + private int $responseCode = 0; | |
| 31 | + private string $responseError = ''; | |
| 32 | + private array $info = []; | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * コンストラクタ | |
| 36 | + * | |
| 37 | + * @param string|null $url リクエスト先のURL | |
| 38 | + */ | |
| 39 | + public function __construct(?string $url = null) { | |
| 40 | + if (!CURL_ENABLED) return; | |
| 41 | + if ($url !== null) { | |
| 42 | + $this->url = $url; | |
| 43 | + } | |
| 44 | + } | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * リクエスト先のURLを設定する | |
| 48 | + * | |
| 49 | + * @param string $url リクエスト先のURL | |
| 50 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 51 | + */ | |
| 52 | + public function setUrl(string $url): Curl { | |
| 53 | + $this->url = $url; | |
| 54 | + return $this; | |
| 55 | + } | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * リクエストメソッドを設定する | |
| 59 | + * | |
| 60 | + * @param string $method GE又はPOST等のHTTPメソッド | |
| 61 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 62 | + */ | |
| 63 | + public function setMethod(string $method): Curl { | |
| 64 | + $this->method = strtoupper($method); | |
| 65 | + return $this; | |
| 66 | + } | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * リクエストのタイムアウト秒数を設定する | |
| 70 | + * | |
| 71 | + * @param int $seconds タイムアウト秒数 | |
| 72 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 73 | + */ | |
| 74 | + public function setTimeout(int $seconds): Curl { | |
| 75 | + $this->timeout = (int)$seconds; | |
| 76 | + return $this; | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 80 | + * リクエストヘッダーを設定する | |
| 81 | + * | |
| 82 | + * @param array $headers リクエストヘッダーの配列 | |
| 83 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 84 | + */ | |
| 85 | + public function setHeaders(array $headers): Curl { | |
| 86 | + $this->headers = $headers; | |
| 87 | + return $this; | |
| 88 | + } | |
| 89 | + | |
| 90 | + /** | |
| 91 | + * 単一のヘッダーを追加する | |
| 92 | + * | |
| 93 | + * @param string $name ヘッダー名 | |
| 94 | + * @param mixed $value ヘッダー値 | |
| 95 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 96 | + */ | |
| 97 | + public function addHeader(string $name, mixed $value): Curl { | |
| 98 | + $this->headers[$name] = $value; | |
| 99 | + return $this; | |
| 100 | + } | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * リクエストのクッキーを設定する | |
| 104 | + * | |
| 105 | + * @param array $cookies クッキーの配列 | |
| 106 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 107 | + */ | |
| 108 | + public function setCookies(array $cookies): Curl { | |
| 109 | + $this->cookies = $cookies; | |
| 110 | + return $this; | |
| 111 | + } | |
| 112 | + | |
| 113 | + /** | |
| 114 | + * 単一のクッキーを追加する | |
| 115 | + * | |
| 116 | + * @param string $name クッキー名 | |
| 117 | + * @param mixed $value クッキー値 | |
| 118 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 119 | + */ | |
| 120 | + public function addCookie(string $name, mixed $value): Curl { | |
| 121 | + $this->cookies[$name] = $value; | |
| 122 | + return $this; | |
| 123 | + } | |
| 124 | + | |
| 125 | + /** | |
| 126 | + * POSTフィールドを設定する | |
| 127 | + * | |
| 128 | + * @param array $fields POSTデータの配列 | |
| 129 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 130 | + */ | |
| 131 | + public function setPostFields(array $fields): Curl { | |
| 132 | + $this->postFields = $fields; | |
| 133 | + return $this; | |
| 134 | + } | |
| 135 | + | |
| 136 | + /** | |
| 137 | + * 生のPOSTデータを設定する | |
| 138 | + * | |
| 139 | + * @param string $data 生のPOSTデータ | |
| 140 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 141 | + */ | |
| 142 | + public function setPostRaw(string $data): Curl { | |
| 143 | + $this->postRaw = $data; | |
| 144 | + return $this; | |
| 145 | + } | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * ユーザーエージェントを設定する | |
| 149 | + * | |
| 150 | + * @param string $userAgent カスタムユーザーエージェント | |
| 151 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 152 | + */ | |
| 153 | + public function setUserAgent(string $userAgent): Curl { | |
| 154 | + $this->userAgent = $userAgent; | |
| 155 | + return $this; | |
| 156 | + } | |
| 157 | + | |
| 158 | + /** | |
| 159 | + * リダイレクトを追跡するかどうかを設定する | |
| 160 | + * | |
| 161 | + * @param bool $follow 追跡するかどうか(デフォルトはtrue) | |
| 162 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 163 | + */ | |
| 164 | + public function setFollowRedirects(bool $follow): Curl { | |
| 165 | + $this->followRedirects = (bool)$follow; | |
| 166 | + return $this; | |
| 167 | + } | |
| 168 | + | |
| 169 | + /** | |
| 170 | + * 追跡するリダイレクトの最大数を設定する | |
| 171 | + * | |
| 172 | + * @param int $max リダイレクトの最大数 | |
| 173 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 174 | + */ | |
| 175 | + public function setMaxRedirects(int $max): Curl { | |
| 176 | + $this->maxRedirects = (int)$max; | |
| 177 | + return $this; | |
| 178 | + } | |
| 179 | + | |
| 180 | + /** | |
| 181 | + * SSL証明書を検証するかどうかを設定する | |
| 182 | + * | |
| 183 | + * @param bool $verify SSL検証を行うかどうか(デフォルトはtrue) | |
| 184 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 185 | + */ | |
| 186 | + public function setVerifySSL(bool $verify): Curl { | |
| 187 | + $this->verifySSL = (bool)$verify; | |
| 188 | + return $this; | |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * 基本認証の資格情報を設定する | |
| 193 | + * | |
| 194 | + * @param string $username ユーザー名 | |
| 195 | + * @param string $password パスワード | |
| 196 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 197 | + */ | |
| 198 | + public function setBasicAuth(string $username, string $password): Curl { | |
| 199 | + $this->username = $username; | |
| 200 | + $this->password = $password; | |
| 201 | + return $this; | |
| 202 | + } | |
| 203 | + | |
| 204 | + /** | |
| 205 | + * リファラーURLを設定する | |
| 206 | + * | |
| 207 | + * @param string $referer リファラーURL | |
| 208 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 209 | + */ | |
| 210 | + public function setReferer(string $referer): Curl { | |
| 211 | + $this->referer = $referer; | |
| 212 | + return $this; | |
| 213 | + } | |
| 214 | + | |
| 215 | + /** | |
| 216 | + * 詳細ログを有効にする | |
| 217 | + * | |
| 218 | + * @param bool $verbose 詳細ログを有効にするかどうか | |
| 219 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 220 | + */ | |
| 221 | + public function setVerbose(bool $verbose): Curl { | |
| 222 | + $this->verbose = (bool)$verbose; | |
| 223 | + return $this; | |
| 224 | + } | |
| 225 | + | |
| 226 | + /** | |
| 227 | + * エラー出力先を設定する | |
| 228 | + * | |
| 229 | + * @param resource $handle エラー出力先のファイルハンドル | |
| 230 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 231 | + */ | |
| 232 | + public function setStderr($handle): Curl { | |
| 233 | + $this->stderr = $handle; | |
| 234 | + return $this; | |
| 235 | + } | |
| 236 | + | |
| 237 | + /** | |
| 238 | + * SSL証明書のファイルパスを設定する | |
| 239 | + * | |
| 240 | + * @param string $path 証明書ファイルのパス | |
| 241 | + * @return Curl このインスタンス(メソッドチェーン用) | |
| 242 | + */ | |
| 243 | + public function setCaInfo(string $path): Curl { | |
| 244 | + $this->caInfoPath = $path; | |
| 245 | + return $this; | |
| 246 | + } | |
| 247 | + | |
| 248 | + /** | |
| 249 | + * リクエストを実行する | |
| 250 | + * | |
| 251 | + * @return Result 成功または失敗 | |
| 252 | + */ | |
| 253 | + public function execute(): \Result { | |
| 254 | + if (!CURL_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); | |
| 255 | + if (empty($this->url)) { | |
| 256 | + $this->responseError = 'URLがありません'; | |
| 257 | + return \Result::Error($this->responseError); | |
| 258 | + } | |
| 259 | + | |
| 260 | + // レスポンスデータのリセット | |
| 261 | + $this->responseHeaders = []; | |
| 262 | + $this->responseBody = ''; | |
| 263 | + $this->responseCode = 0; | |
| 264 | + $this->responseError = ''; | |
| 265 | + $this->info = [ | |
| 266 | + 'url' => $this->url, | |
| 267 | + 'content_type' => '', | |
| 268 | + 'http_code' => 0, | |
| 269 | + 'header_size' => 0, | |
| 270 | + 'request_size' => 0, | |
| 271 | + 'total_time' => 0, | |
| 272 | + 'redirect_count' => 0, | |
| 273 | + 'redirect_url' => '', | |
| 274 | + ]; | |
| 275 | + | |
| 276 | + $startTime = microtime(true); | |
| 277 | + | |
| 278 | + // ソケットベースの実装を使用する | |
| 279 | + $redirectCount = 0; | |
| 280 | + $currentUrl = $this->url; | |
| 281 | + $originalMethod = $this->method; | |
| 282 | + | |
| 283 | + do { | |
| 284 | + if ($this->verbose && $this->stderr) { | |
| 285 | + fwrite($this->stderr, "* 接続中: {$currentUrl}\n"); | |
| 286 | + } | |
| 287 | + | |
| 288 | + $parsed = parse_url($currentUrl); | |
| 289 | + if (!$parsed) { | |
| 290 | + $this->responseError = "無効なURL: {$currentUrl}"; | |
| 291 | + return \Result::Error($this->responseError); | |
| 292 | + } | |
| 293 | + | |
| 294 | + $scheme = isset($parsed['scheme']) ? strtolower($parsed['scheme']) : 'http'; | |
| 295 | + $host = $parsed['host']; | |
| 296 | + $port = isset($parsed['port']) | |
| 297 | + ? $parsed['port'] : ($scheme === 'https' ? 443 : 80); | |
| 298 | + $path = isset($parsed['path']) ? $parsed['path'] : '/'; | |
| 299 | + if (isset($parsed['query'])) { | |
| 300 | + $path .= '?'.$parsed['query']; | |
| 301 | + } | |
| 302 | + | |
| 303 | + // Basic認証 | |
| 304 | + $authHeader = ''; | |
| 305 | + if (!empty($this->username) && !empty($this->password)) { | |
| 306 | + $authHeader = "Authorization: Basic " | |
| 307 | + .base64_encode($this->username.':'.$this->password)."\r\n"; | |
| 308 | + } elseif (isset($parsed['user']) && isset($parsed['pass'])) { | |
| 309 | + $authHeader = "Authorization: Basic " | |
| 310 | + .base64_encode($parsed['user'].':'.$parsed['pass'])."\r\n"; | |
| 311 | + } | |
| 312 | + | |
| 313 | + // 送信するHTTPリクエストを構築 | |
| 314 | + $method = $this->method; | |
| 315 | + $httpData = ''; | |
| 316 | + | |
| 317 | + if ($method === 'POST' || $method === 'PUT') { | |
| 318 | + if (!empty($this->postRaw)) { | |
| 319 | + $httpData = $this->postRaw; | |
| 320 | + } elseif (!empty($this->postFields)) { | |
| 321 | + $httpData = http_build_query($this->postFields); | |
| 322 | + if (!isset($this->headers['Content-Type'])) { | |
| 323 | + $this->headers['Content-Type'] = 'application/x-www-form-urlencoded'; | |
| 324 | + } | |
| 325 | + } | |
| 326 | + $this->headers['Content-Length'] = strlen($httpData); | |
| 327 | + } | |
| 328 | + | |
| 329 | + // HTTPリクエストを構築 | |
| 330 | + $accept = 'Accept: */*'; | |
| 331 | + foreach ($this->headers as $h) { | |
| 332 | + if (str_contains($h, 'Accept:')) $accept = $h; | |
| 333 | + } | |
| 334 | + | |
| 335 | + $request = "{$method} {$path} HTTP/1.1\r\n"; | |
| 336 | + $request .= "Host: {$host}\r\n"; | |
| 337 | + $request .= "User-Agent: {$this->userAgent}\r\n"; | |
| 338 | + $request .= "{$accept}\r\n"; | |
| 339 | + $request .= "Connection: close\r\n"; | |
| 340 | + | |
| 341 | + if (!empty($authHeader)) { | |
| 342 | + $request .= $authHeader; | |
| 343 | + } | |
| 344 | + | |
| 345 | + // ヘッダーを追加 | |
| 346 | + foreach ($this->headers as $name => $value) { | |
| 347 | + $request .= "{$name}: {$value}\r\n"; | |
| 348 | + } | |
| 349 | + | |
| 350 | + // リファラーが設定されていれば追加 | |
| 351 | + if (!empty($this->referer) && !isset($this->headers['Referer'])) { | |
| 352 | + $request .= "Referer: {$this->referer}\r\n"; | |
| 353 | + } | |
| 354 | + | |
| 355 | + // クッキーヘッダーを追加 | |
| 356 | + if (!empty($this->cookies) && !isset($this->headers['Cookie'])) { | |
| 357 | + $cookieStrings = []; | |
| 358 | + foreach ($this->cookies as $name => $value) { | |
| 359 | + $cookieStrings[] = $name.'='.urlencode($value); | |
| 360 | + } | |
| 361 | + $request .= 'Cookie: '.implode('; ', $cookieStrings)."\r\n"; | |
| 362 | + } | |
| 363 | + | |
| 364 | + $request .= "\r\n"; | |
| 365 | + | |
| 366 | + // POSTデータを追加 | |
| 367 | + if ($method === 'POST' || $method === 'PUT') { | |
| 368 | + $request .= $httpData; | |
| 369 | + } | |
| 370 | + | |
| 371 | + if ($this->verbose && $this->stderr) { | |
| 372 | + fwrite($this->stderr, "* リクエストヘッダー:\n{$request}\n"); | |
| 373 | + } | |
| 374 | + | |
| 375 | + // ソケット接続を確立 | |
| 376 | + $errno = 0; | |
| 377 | + $errstr = ''; | |
| 378 | + | |
| 379 | + if ($scheme === 'https') { | |
| 380 | + $sslOptions = [ | |
| 381 | + 'verify_peer' => $this->verifySSL, | |
| 382 | + 'verify_peer_name' => $this->verifySSL | |
| 383 | + ]; | |
| 384 | + | |
| 385 | + if (!empty($this->caInfoPath) && file_exists($this->caInfoPath)) { | |
| 386 | + $sslOptions['cafile'] = $this->caInfoPath; | |
| 387 | + } | |
| 388 | + | |
| 389 | + $context = stream_context_create(['ssl' => $sslOptions]); | |
| 390 | + | |
| 391 | + $socket = @stream_socket_client( | |
| 392 | + "tls://{$host}:{$port}", | |
| 393 | + $errno, | |
| 394 | + $errstr, | |
| 395 | + $this->timeout, | |
| 396 | + STREAM_CLIENT_CONNECT, | |
| 397 | + $context | |
| 398 | + ); | |
| 399 | + } else { | |
| 400 | + $socket = @fsockopen($host, $port, $errno, $errstr, $this->timeout); | |
| 401 | + } | |
| 402 | + | |
| 403 | + if (!$socket) { | |
| 404 | + $this->responseError = "接続出来ません: {$errstr} ({$errno})"; | |
| 405 | + if ($this->verbose && $this->stderr) { | |
| 406 | + fwrite($this->stderr, "* エラー: {$this->responseError}\n"); | |
| 407 | + } | |
| 408 | + return \Result::Error($this->responseError); | |
| 409 | + } | |
| 410 | + | |
| 411 | + // タイムアウトを設定 | |
| 412 | + stream_set_timeout($socket, $this->timeout); | |
| 413 | + | |
| 414 | + // リクエストを送信 | |
| 415 | + fwrite($socket, $request); | |
| 416 | + | |
| 417 | + // レスポンスを読み込む | |
| 418 | + $rawResponse = ''; | |
| 419 | + $headers = ''; | |
| 420 | + $body = ''; | |
| 421 | + $headersComplete = false; | |
| 422 | + | |
| 423 | + // ヘッダーとボディを分けて読み込む | |
| 424 | + while (!feof($socket)) { | |
| 425 | + $line = fgets($socket); | |
| 426 | + if ($line === false) { | |
| 427 | + break; | |
| 428 | + } | |
| 429 | + | |
| 430 | + $rawResponse .= $line; | |
| 431 | + | |
| 432 | + if (!$headersComplete) { | |
| 433 | + if (trim($line) === '') { | |
| 434 | + $headersComplete = true; | |
| 435 | + } else { | |
| 436 | + $headers .= $line; | |
| 437 | + } | |
| 438 | + } else { | |
| 439 | + $body .= $line; | |
| 440 | + } | |
| 441 | + } | |
| 442 | + | |
| 443 | + fclose($socket); | |
| 444 | + | |
| 445 | + // レスポンスヘッダーを解析 | |
| 446 | + $headerLines = explode("\r\n", $headers); | |
| 447 | + | |
| 448 | + // ステータスコードを取得 | |
| 449 | + $statusLine = isset($headerLines[0]) ? $headerLines[0] : ''; | |
| 450 | + $statusParts = explode(' ', $statusLine, 3); | |
| 451 | + $this->responseCode = isset($statusParts[1]) ? (int)$statusParts[1] : 0; | |
| 452 | + $this->info['http_code'] = $this->responseCode; | |
| 453 | + | |
| 454 | + // ヘッダーを解析 | |
| 455 | + $this->responseHeaders = []; | |
| 456 | + $redirectUrl = ''; | |
| 457 | + | |
| 458 | + foreach ($headerLines as $index => $header) { | |
| 459 | + if ($index === 0) continue; | |
| 460 | + | |
| 461 | + if (strpos($header, ':') !== false) { | |
| 462 | + list($name, $value) = explode(':', $header, 2); | |
| 463 | + $name = trim($name); | |
| 464 | + $value = trim($value); | |
| 465 | + $this->responseHeaders[$name] = $value; | |
| 466 | + | |
| 467 | + if (strtolower($name) === 'content-type') { | |
| 468 | + $this->info['content_type'] = $value; | |
| 469 | + } | |
| 470 | + | |
| 471 | + // リダイレクトをチェック | |
| 472 | + if ($this->followRedirects && | |
| 473 | + strtolower($name) === 'location' && | |
| 474 | + $this->responseCode >= 300 && | |
| 475 | + $this->responseCode < 400) { | |
| 476 | + | |
| 477 | + $redirectUrl = $value; | |
| 478 | + | |
| 479 | + // 相対URLを絶対URLに変換 | |
| 480 | + if (strpos($redirectUrl, 'http') !== 0) { | |
| 481 | + if ($redirectUrl[0] === '/') { | |
| 482 | + $redirectUrl = "{$scheme}://{$host}" | |
| 483 | + .($port != 80 && $port != 443 ? ":{$port}" : '').$redirectUrl; | |
| 484 | + } else { | |
| 485 | + $redirectUrl = "{$scheme}://{$host}" | |
| 486 | + .($port != 80 && $port != 443 ? ":{$port}" : '') | |
| 487 | + .dirname($path).'/'.$redirectUrl; | |
| 488 | + } | |
| 489 | + } | |
| 490 | + | |
| 491 | + $this->info['redirect_url'] = $redirectUrl; | |
| 492 | + } | |
| 493 | + } | |
| 494 | + } | |
| 495 | + | |
| 496 | + $this->info['header_size'] += strlen($headers); | |
| 497 | + $this->responseBody .= $body; | |
| 498 | + | |
| 499 | + if ($this->verbose && $this->stderr) { | |
| 500 | + fwrite($this->stderr, "* レスポンスコード: {$this->responseCode}\n"); | |
| 501 | + fwrite($this->stderr, "* レスポンスヘッダー:\n{$headers}\n"); | |
| 502 | + if (!empty($redirectUrl)) { | |
| 503 | + fwrite($this->stderr, "* リダイレクト先: {$redirectUrl}\n"); | |
| 504 | + } | |
| 505 | + } | |
| 506 | + | |
| 507 | + // リダイレクトが必要な場合 | |
| 508 | + if (!empty($redirectUrl) && $redirectCount < $this->maxRedirects) { | |
| 509 | + $currentUrl = $redirectUrl; | |
| 510 | + $redirectCount++; | |
| 511 | + $this->info['redirect_count'] = $redirectCount; | |
| 512 | + | |
| 513 | + // 302や303リダイレクトはGETにメソッドを変更 | |
| 514 | + if ($this->responseCode == 302 || $this->responseCode == 303) { | |
| 515 | + $this->method = 'GET'; | |
| 516 | + $this->postRaw = ''; | |
| 517 | + $this->postFields = []; | |
| 518 | + } | |
| 519 | + } else { | |
| 520 | + break; | |
| 521 | + } | |
| 522 | + } while (true); | |
| 523 | + | |
| 524 | + // リクエスト完了後、元のメソッドに戻す | |
| 525 | + $this->method = $originalMethod; | |
| 526 | + $this->info['total_time'] = microtime(true) - $startTime; | |
| 527 | + | |
| 528 | + return \Result::Success(); | |
| 529 | + } | |
| 530 | + | |
| 531 | + /** | |
| 532 | + * レスポンスボディを取得する | |
| 533 | + * | |
| 534 | + * @return string レスポンスボディ | |
| 535 | + */ | |
| 536 | + public function getResponseBody(): string { | |
| 537 | + return $this->responseBody; | |
| 538 | + } | |
| 539 | + | |
| 540 | + /** | |
| 541 | + * レスポンスヘッダーを取得する | |
| 542 | + * | |
| 543 | + * @return array レスポンスヘッダーの配列 | |
| 544 | + */ | |
| 545 | + public function getResponseHeaders(): array { | |
| 546 | + return $this->responseHeaders; | |
| 547 | + } | |
| 548 | + | |
| 549 | + /** | |
| 550 | + * HTTPレスポンスコードを取得する | |
| 551 | + * | |
| 552 | + * @return int HTTPレスポンスコード | |
| 553 | + */ | |
| 554 | + public function getResponseCode(): int { | |
| 555 | + return $this->responseCode; | |
| 556 | + } | |
| 557 | + | |
| 558 | + /** | |
| 559 | + * エラーメッセージがあれば取得する | |
| 560 | + * | |
| 561 | + * @return string エラーメッセージ | |
| 562 | + */ | |
| 563 | + public function getError(): string { | |
| 564 | + return $this->responseError; | |
| 565 | + } | |
| 566 | + | |
| 567 | + /** | |
| 568 | + * リクエスト/レスポンス情報を取得する | |
| 569 | + * | |
| 570 | + * @return array 情報の配列 | |
| 571 | + */ | |
| 572 | + public function getInfo(): array { | |
| 573 | + return $this->info; | |
| 574 | + } | |
| 575 | + | |
| 576 | + // 機能性メソッド | |
| 577 | + | |
| 578 | + /** | |
| 579 | + * リダイレクトURLを確認する | |
| 580 | + * | |
| 581 | + * @param string $name ヘッダー名 | |
| 582 | + * @param string $value ヘッダー値 | |
| 583 | + * @param string $currentUrl 現在のURL | |
| 584 | + * @return string リダイレクトURL、リダイレクトがない場合は空文字 | |
| 585 | + */ | |
| 586 | + private function checkReds(string $name, string $value, string $currentUrl): string { | |
| 587 | + $redirectUrl = ''; | |
| 588 | + | |
| 589 | + if ($this->followRedirects && (strtolower($name) === 'location' | |
| 590 | + && $this->responseCode >= 300 && $this->responseCode < 400)) { | |
| 591 | + $redirectUrl = $value; | |
| 592 | + | |
| 593 | + if (strpos($redirectUrl, 'http') !== 0) { | |
| 594 | + if ($redirectUrl[0] === '/') { | |
| 595 | + $parsed = parse_url($currentUrl); | |
| 596 | + $redirectUrl = $parsed['scheme'].'://'.$parsed['host'] | |
| 597 | + .(isset($parsed['port']) ? ':'.$parsed['port'] : '') | |
| 598 | + .$redirectUrl; | |
| 599 | + } else { | |
| 600 | + $redirectUrl = dirname($currentUrl).'/'.$redirectUrl; | |
| 601 | + } | |
| 602 | + } | |
| 603 | + } | |
| 604 | + | |
| 605 | + return $redirectUrl; | |
| 606 | + } | |
| 607 | + | |
| 608 | + /** | |
| 609 | + * ヘッダー文字列を構築する | |
| 610 | + * | |
| 611 | + * @return string 構築されたヘッダー文字列 | |
| 612 | + */ | |
| 613 | + private function buildHeaderString(): string { | |
| 614 | + $headers = []; | |
| 615 | + | |
| 616 | + // ユーザー指定のヘッダーを追加 | |
| 617 | + foreach ($this->headers as $name => $value) { | |
| 618 | + $headers[] = "{$name}: {$value}"; | |
| 619 | + } | |
| 620 | + | |
| 621 | + // リファラーが設定されていれば追加 | |
| 622 | + if (!empty($this->referer) && !isset($this->headers['Referer'])) { | |
| 623 | + $headers[] = "Referer: {$this->referer}"; | |
| 624 | + } | |
| 625 | + | |
| 626 | + // 必要に応じてクッキーヘッダーを追加 | |
| 627 | + if (!empty($this->cookies) && !isset($this->headers['Cookie'])) { | |
| 628 | + $cookieStrings = []; | |
| 629 | + foreach ($this->cookies as $name => $value) { | |
| 630 | + $cookieStrings[] = $name.'='.urlencode($value); | |
| 631 | + } | |
| 632 | + | |
| 633 | + $headers[] = 'Cookie: '.implode('; ', $cookieStrings); | |
| 634 | + } | |
| 635 | + | |
| 636 | + return implode("\r\n", $headers)."\r\n"; | |
| 637 | + } | |
| 638 | + | |
| 639 | + /** | |
| 640 | + * レスポンスを解析してヘッダーとボディに分割する | |
| 641 | + * | |
| 642 | + * @param string $response 完全なHTTPレスポンス | |
| 643 | + * @return array [ヘッダー配列, ボディ文字列] | |
| 644 | + */ | |
| 645 | + private function parseResponse(string $response): array { | |
| 646 | + $parts = explode("\r\n\r\n", $response, 2); | |
| 647 | + | |
| 648 | + if (count($parts) < 2) { | |
| 649 | + return [[], '']; | |
| 650 | + } | |
| 651 | + | |
| 652 | + $headers = explode("\r\n", $parts[0]); | |
| 653 | + $body = $parts[1]; | |
| 654 | + | |
| 655 | + // チャンク転送エンコーディングを処理 | |
| 656 | + if (isset($this->responseHeaders['Transfer-Encoding']) && | |
| 657 | + strtolower($this->responseHeaders['Transfer-Encoding']) === 'chunked') { | |
| 658 | + $body = $this->decodeChunkedBody($body); | |
| 659 | + } | |
| 660 | + | |
| 661 | + return [$headers, $body]; | |
| 662 | + } | |
| 663 | + | |
| 664 | + /** | |
| 665 | + * チャンク転送エンコーディングされたボディをデコードする | |
| 666 | + * | |
| 667 | + * @param string $body チャンクエンコードされたボディ | |
| 668 | + * @return string デコードされたボディ | |
| 669 | + */ | |
| 670 | + private function decodeChunkedBody(string $body): string { | |
| 671 | + $decodedBody = ''; | |
| 672 | + $position = 0; | |
| 673 | + | |
| 674 | + while ($position < strlen($body)) { | |
| 675 | + $lineEnd = strpos($body, "\r\n", $position); | |
| 676 | + if ($lineEnd === false) { | |
| 677 | + break; | |
| 678 | + } | |
| 679 | + | |
| 680 | + $chunkSize = hexdec(substr($body, $position, $lineEnd - $position)); | |
| 681 | + | |
| 682 | + if ($chunkSize === 0) { | |
| 683 | + break; | |
| 684 | + } | |
| 685 | + | |
| 686 | + $position = $lineEnd + 2; | |
| 687 | + $decodedBody .= substr($body, $position, $chunkSize); | |
| 688 | + $position += $chunkSize + 2; // チャンクサイズ + CRLF | |
| 689 | + } | |
| 690 | + | |
| 691 | + return $decodedBody; | |
| 692 | + } | |
| 693 | +} |
| @@ -0,0 +1,130 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +class DiffViewer { | |
| 5 | + private $diffContent; | |
| 6 | + | |
| 7 | + public function __construct(string $filePath) { | |
| 8 | + if (!file_exists($filePath)) { | |
| 9 | + throw new \Exception("Diff file not found: $filePath"); | |
| 10 | + } | |
| 11 | + $this->diffContent = file_get_contents($filePath); | |
| 12 | + } | |
| 13 | + | |
| 14 | + public function displaySideBySide(): string { | |
| 15 | + $lines = explode("\n", $this->diffContent); | |
| 16 | + $fileDiffs = []; | |
| 17 | + $currentFile = null; | |
| 18 | + $hunk = []; | |
| 19 | + $lineNumbers = ['left' => 0, 'right' => 0]; | |
| 20 | + $currentLeftLines = []; | |
| 21 | + $currentRightLines = []; | |
| 22 | + | |
| 23 | + foreach ($lines as $line) { | |
| 24 | + // ファイルヘッダーの処理 | |
| 25 | + if (preg_match('/^---\s+(.+)/', $line, $matches)) { | |
| 26 | + // ファイルを処理する場合、データを保存する | |
| 27 | + if ($currentFile !== null) { | |
| 28 | + $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 29 | + $fileDiffs[$currentFile] = [ | |
| 30 | + 'leftLines' => $currentLeftLines, | |
| 31 | + 'rightLines' => $currentRightLines | |
| 32 | + ]; | |
| 33 | + $hunk = []; | |
| 34 | + $currentLeftLines = []; | |
| 35 | + $currentRightLines = []; | |
| 36 | + $lineNumbers = ['left' => 0, 'right' => 0]; | |
| 37 | + } | |
| 38 | + $currentFile = $matches[1]; | |
| 39 | + continue; | |
| 40 | + } | |
| 41 | + if (preg_match('/^\+\+\+\s+(.+)/', $line)) { | |
| 42 | + continue; | |
| 43 | + } | |
| 44 | + | |
| 45 | + // ハンクヘッダーの処理 (例:@@ -10,6 +10,7 @@) | |
| 46 | + if (preg_match('/^@@\s+-(\d+),\d+\s+\+(\d+),\d+\s+@@/', $line, $matches)) { | |
| 47 | + $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 48 | + $hunk = []; | |
| 49 | + $lineNumbers['left'] = (int)$matches[1]; | |
| 50 | + $lineNumbers['right'] = (int)$matches[2]; | |
| 51 | + continue; | |
| 52 | + } | |
| 53 | + | |
| 54 | + // ハンクでの行列の集まり | |
| 55 | + if (substr($line, 0, 1) === '-' || substr($line, 0, 1) === '+' || substr($line, 0, 1) === ' ') { | |
| 56 | + $hunk[] = $line; | |
| 57 | + } | |
| 58 | + } | |
| 59 | + | |
| 60 | + // 最後のハンク・ファイルの処理 | |
| 61 | + if ($currentFile !== null) { | |
| 62 | + $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 63 | + $fileDiffs[$currentFile] = [ | |
| 64 | + 'leftLines' => $currentLeftLines, | |
| 65 | + 'rightLines' => $currentRightLines | |
| 66 | + ]; | |
| 67 | + } | |
| 68 | + | |
| 69 | + // 各ファイルにHTMLの出力の作成 | |
| 70 | + $html = ''; | |
| 71 | + foreach ($fileDiffs as $fileName => $diff) { | |
| 72 | + $html .= "\n <h2>ファイル: ".htmlspecialchars($fileName)."</h2>\n"; | |
| 73 | + $html .= $this->generateHtml($diff['leftLines'], $diff['rightLines']); | |
| 74 | + } | |
| 75 | + | |
| 76 | + return $html; | |
| 77 | + } | |
| 78 | + | |
| 79 | + private function processHunk(array $hunk, array &$leftLines, array &$rightLines, array &$lineNumbers): void { | |
| 80 | + foreach ($hunk as $line) { | |
| 81 | + $prefix = substr($line, 0, 1); | |
| 82 | + $content = substr($line, 1); | |
| 83 | + | |
| 84 | + if ($prefix === '-') { | |
| 85 | + $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'removed', 'line' => $lineNumbers['left']]; | |
| 86 | + $lineNumbers['left']++; | |
| 87 | + } elseif ($prefix === '+') { | |
| 88 | + $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'added', 'line' => $lineNumbers['right']]; | |
| 89 | + $lineNumbers['right']++; | |
| 90 | + } elseif ($prefix === ' ') { | |
| 91 | + // 両側のコンテキストは同じ行列があるかの確認 | |
| 92 | + while ($lineNumbers['left'] < $lineNumbers['right']) { | |
| 93 | + $leftLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['left']]; | |
| 94 | + $lineNumbers['left']++; | |
| 95 | + } | |
| 96 | + while ($lineNumbers['right'] < $lineNumbers['left']) { | |
| 97 | + $rightLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['right']]; | |
| 98 | + $lineNumbers['right']++; | |
| 99 | + } | |
| 100 | + $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['left']]; | |
| 101 | + $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['right']]; | |
| 102 | + $lineNumbers['left']++; | |
| 103 | + $lineNumbers['right']++; | |
| 104 | + } | |
| 105 | + } | |
| 106 | + } | |
| 107 | + | |
| 108 | + private function generateHtml(array $leftLines, array $rightLines): string { | |
| 109 | + $html = " <p>\n <table class=\"diff-table\">\n"; | |
| 110 | + $html .= " <tr class=\"diff-header\"><th colspan=\"2\">前</th><th colspan=\"2\">新</th></tr>\n"; | |
| 111 | + | |
| 112 | + $maxLines = max(count($leftLines), count($rightLines)); | |
| 113 | + for ($i = 0; $i < $maxLines; $i++) { | |
| 114 | + $left = isset($leftLines[$i]) ? $leftLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; | |
| 115 | + $right = isset($rightLines[$i]) ? $rightLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; | |
| 116 | + | |
| 117 | + $html .= " <tr>\n"; | |
| 118 | + // 左(変更前) | |
| 119 | + $html .= " <td class=\"line-number\">".($left['line'] ?: ' ')."</td>\n"; | |
| 120 | + $html .= " <td class=\"{$left['type']}\">".($left['content'] ?: ' ')."</td>\n"; | |
| 121 | + // 右(変更後) | |
| 122 | + $html .= " <td class=\"line-number\">".($right['line'] ?: ' ')."</td>\n"; | |
| 123 | + $html .= " <td class=\"{$right['type']}\">".($right['content'] ?: ' ')."</td>\n"; | |
| 124 | + $html .= " </tr>\n"; | |
| 125 | + } | |
| 126 | + | |
| 127 | + $html .= " </table>\n </p>\n"; | |
| 128 | + return $html; | |
| 129 | + } | |
| 130 | +} |
| @@ -0,0 +1,79 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +use Std\Lib\Image\ImageInterface; | |
| 5 | +use Std\Lib\Image\Gif; | |
| 6 | +use Std\Lib\Image\Jpeg; | |
| 7 | +use Std\Lib\Image\Png; | |
| 8 | +use Std\Lib\Image\Targa; | |
| 9 | + | |
| 10 | +class RGB { | |
| 11 | + public int $r; | |
| 12 | + public int $g; | |
| 13 | + public int $b; | |
| 14 | + public array $rgb; | |
| 15 | + | |
| 16 | + public function __construct(int $r, int $g, int $b) { | |
| 17 | + $this->r = $r; | |
| 18 | + $this->g = $g; | |
| 19 | + $this->b = $b; | |
| 20 | + $this->rgb = ['r' => $r, 'g' => $g, 'b' => $b]; | |
| 21 | + } | |
| 22 | +} | |
| 23 | + | |
| 24 | +class Image { | |
| 25 | + public string $filename = 'UNKNOWN'; | |
| 26 | + public string $extension = 'UNKNOWN'; | |
| 27 | + public string $type = 'UNSUPPORTED'; | |
| 28 | + public string $size = '0 B'; | |
| 29 | + public int $bytes = 0; | |
| 30 | + public \stdClass $fullInfo; | |
| 31 | + | |
| 32 | + private array $supported = [ | |
| 33 | + 'image/gif' => Gif::class, | |
| 34 | + 'image/jpeg' => Jpeg::class, | |
| 35 | + 'image/png' => Png::class, | |
| 36 | + 'image/x-tga' => Targa::class, | |
| 37 | + ]; | |
| 38 | + | |
| 39 | + public function __construct(string $file) { | |
| 40 | + $file = ROOT.$file; | |
| 41 | + // $file = ROOT.'/public/static/article/mock-screenshot.tga'; | |
| 42 | + if (!file_exists($file)) return; | |
| 43 | + $fp = fopen($file, 'rb'); | |
| 44 | + if (!$fp) return; | |
| 45 | + $extBytes = fread($fp, 15); | |
| 46 | + fclose($fp); | |
| 47 | + | |
| 48 | + $this->bytes = filesize($file); | |
| 49 | + $this->size = $this->formatBytes($this->bytes); | |
| 50 | + $this->extension = pathinfo($file, PATHINFO_EXTENSION); | |
| 51 | + $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); | |
| 52 | + | |
| 53 | + if (substr($extBytes, 0, 3) === "\xff\xd8\xff") $this->type = 'image/jpeg'; | |
| 54 | + else if (substr($extBytes, 0, 15) === "\x52\x49\x46\x46\x1a\x28\x00\x00\x57\x45\x42\x50\x56\x50\x38") $this->type = 'image/webp'; | |
| 55 | + else if (substr($extBytes, 0, 3) === "\x42\x4d\x36") $this->type = 'image/bmp'; | |
| 56 | + else if (substr($extBytes, 0, 6) === "\x89\x50\x4e\x47\x0d\x0a") $this->type = 'image/png'; | |
| 57 | + else if (substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x37\x61" || substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x39\x61") $this->type = 'image/gif'; | |
| 58 | + else if (str_ends_with($file, '.tga')) $this->type = 'image/x-tga'; | |
| 59 | + | |
| 60 | + $this->fullInfo = $this->createHandler($this->type)->parse($file); | |
| 61 | + $this->fullInfo->type = $this->type; | |
| 62 | + $this->fullInfo->size = $this->size; | |
| 63 | + $this->fullInfo->bytes = $this->bytes; | |
| 64 | + } | |
| 65 | + | |
| 66 | + /////////////// | |
| 67 | + | |
| 68 | + private function createHandler(string $ext): ImageInterface { | |
| 69 | + $class = $this->supported[$ext] ?? null; | |
| 70 | + if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$ext}"); | |
| 71 | + return new $class(); | |
| 72 | + } | |
| 73 | + | |
| 74 | + private function formatBytes(int $size, int $precision = 2): string { | |
| 75 | + $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; | |
| 76 | + for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) $size /= 1024; | |
| 77 | + return round($size, $precision).' '.$units[$i]; | |
| 78 | + } | |
| 79 | +} |
| @@ -0,0 +1,91 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Image; | |
| 3 | + | |
| 4 | +use Std\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Gif implements ImageInterface { | |
| 7 | + public int $width = 0; | |
| 8 | + public int $height = 0; | |
| 9 | + public int $bitDepth = 0; | |
| 10 | + public int $numColors = 0; | |
| 11 | + public string $compression = 'NONE'; | |
| 12 | + public int $interlace = 0; | |
| 13 | + public int $bgColor = 0; | |
| 14 | + public float $delayTime = 0; | |
| 15 | + public array $globalColorTable = []; | |
| 16 | + public int $numTransparentPixels = 0; | |
| 17 | + public int $lzwMinCodeSize = 0; | |
| 18 | + public string $imageData = ''; | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * TODO: 現在、インタレースした画像が未対応です。 | |
| 22 | + */ | |
| 23 | + public function parse(string $file): \stdClass { | |
| 24 | + $gif = new \stdClass(); | |
| 25 | + $fp = fopen($file, 'rb'); | |
| 26 | + $magic = fread($fp, 6); // マジックのスキップ | |
| 27 | + | |
| 28 | + // v = リトルエンディアン(2ビット) | |
| 29 | + $this->width = unpack('v', fread($fp, 2))[1]; | |
| 30 | + $this->height = unpack('v', fread($fp, 2))[1]; | |
| 31 | + $packed = ord(fread($fp, 1)); | |
| 32 | + $this->bitDepth = ($packed & 0x07) + 1; | |
| 33 | + $this->numColors = 1 << $this->bitDepth; | |
| 34 | + $hasGct = $this->bitDepth === 8; | |
| 35 | + | |
| 36 | + $gif->width = $this->width; | |
| 37 | + $gif->height = $this->height; | |
| 38 | + $gif->bitDepth = $this->bitDepth; | |
| 39 | + $gif->numColors = $this->numColors; | |
| 40 | + if ($magic === 'GIF87a' || !$hasGct) return $gif; // それ以外情報がない | |
| 41 | + | |
| 42 | + $this->bgColor = (int)bin2hex(fread($fp, 1)); | |
| 43 | + $gif->bgColor = $this->bgColor; | |
| 44 | + bin2hex(fread($fp, 1)); // いつでも 0:0? | |
| 45 | + | |
| 46 | + $gceLen = 0; | |
| 47 | + while (!feof($fp)) { | |
| 48 | + $skip = bin2hex(fread($fp, 3)); | |
| 49 | + $color = new RGB((int)($skip[0].$skip[1]), (int)($skip[2].$skip[3]), (int)($skip[4].$skip[5])); | |
| 50 | + $this->globalColorTable[] = $color->rgb; | |
| 51 | + if ($skip[0].$skip[1] === '21' && $skip[2].$skip[3] === 'f9') { | |
| 52 | + $gceLen = (int)($skip[4].$skip[5]); | |
| 53 | + break; | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + // GCE開始 | |
| 58 | + $this->hasAlpha = (ord(fread($fp, 1)) & 0x01) === 0x01; | |
| 59 | + $this->delayTime = (unpack('v', fread($fp, 2))[1]) / 100.0; | |
| 60 | + $this->numTransparentPixels = ord(fread($fp, 1)); | |
| 61 | + $gif->hasAlpha = $this->hasAlpha; | |
| 62 | + $gif->delayTime = $this->delayTime; | |
| 63 | + $gif->numTransparentPixels = $this->numTransparentPixels; | |
| 64 | + fread($fp, 1); // GCE終了 | |
| 65 | + fread($fp, 1); // 画像指定子開始 | |
| 66 | + fread($fp, 4); // 画面のXとY、いつでも(0, 0) | |
| 67 | + fread($fp, 4); // 画像のサイズ、関数の開始で既に保存した | |
| 68 | + $pack = ord(fread($fp, 1)); | |
| 69 | + $this->interlace = ($pack & 0x40); // 画像のサイズ、関数の開始で既に保存した | |
| 70 | + | |
| 71 | + $this->lzwMinCodeSize = ord(fread($fp, 1)); // 画像データ開始 | |
| 72 | + $gif->lzwMinCodeSize = $this->lzwMinCodeSize; | |
| 73 | + $imageData = ''; | |
| 74 | + while (true) { | |
| 75 | + $blockSize = ord(fread($fp, 1)); | |
| 76 | + if ($blockSize === 0) break; | |
| 77 | + $imageData .= fread($fp, 1); | |
| 78 | + } | |
| 79 | + | |
| 80 | + $this->imageData = base64_encode($imageData); | |
| 81 | + $this->colorType = 3; | |
| 82 | + $this->compression = 'LZW'; | |
| 83 | + $gif->colorType = $this->colorType; | |
| 84 | + $gif->compression = $this->compression; | |
| 85 | + $gif->globalColorTable = $this->globalColorTable; | |
| 86 | + $gif->imageData = $this->imageData; | |
| 87 | + | |
| 88 | + fclose($fp); | |
| 89 | + return $gif; | |
| 90 | + } | |
| 91 | +} |
| @@ -0,0 +1,6 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Image; | |
| 3 | + | |
| 4 | +interface ImageInterface { | |
| 5 | + public function parse(string $file): \stdClass; | |
| 6 | +} |
| @@ -0,0 +1,54 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Image; | |
| 3 | + | |
| 4 | +use Std\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Jpeg implements ImageInterface { | |
| 7 | + public int $width = 0; | |
| 8 | + public int $height = 0; | |
| 9 | + public int $bpp = 0; | |
| 10 | + public string $imageData = ''; | |
| 11 | + | |
| 12 | + /** | |
| 13 | + * TODO: 現在、IHDRヘッダーのみを受け取る | |
| 14 | + */ | |
| 15 | + public function parse(string $file): \stdClass { | |
| 16 | + $jpg = new \stdClass; | |
| 17 | + $fp = fopen($file, 'rb'); | |
| 18 | + | |
| 19 | + $i = 0; | |
| 20 | + while (!feof($fp)) { | |
| 21 | + $byte = fread($fp, 1); | |
| 22 | + if ($byte !== "\xFF") continue; | |
| 23 | + | |
| 24 | + $marker = ord(fread($fp, 1)); | |
| 25 | + if ($marker >= 0xC0 && $marker <= 0xC3) { // 0xC0 = SOF0, 0xC1 = SOF1, 0xC2 = SOF2, 0xC3 = SOF3 | |
| 26 | + fread($fp, 2); | |
| 27 | + $this->bpp = ord(fread($fp, 1)); | |
| 28 | + // n = ビッグエンディアン(2ビット) | |
| 29 | + $this->height = unpack('n', fread($fp, 2))[1]; | |
| 30 | + $this->width = unpack('n', fread($fp, 2))[1]; | |
| 31 | + $jpg->width = $this->width; | |
| 32 | + $jpg->height = $this->height; | |
| 33 | + $jpg->bpp = $this->bpp; | |
| 34 | + } else if ($marker == 0xC4) { // DHT (Huffmanテーブル(複数可)) | |
| 35 | + // | |
| 36 | + } else if ($marker == 0xDB) { // DQT (量子化テーブル(複数可)) | |
| 37 | + // | |
| 38 | + } else if ($marker == 0xDD) { // DRI (リセット期間) | |
| 39 | + } else if ($marker == 0xDA) { // SOS(画像開始) | |
| 40 | + } else if ($marker >= 0xD0 && $marker <= 0xD7) { // RST | |
| 41 | + } else if ($marker >= 0xE1 && $marker <= 0xEF) { // APP | |
| 42 | + } else if ($marker == 0xFE) { // COM | |
| 43 | + } else if ($marker == 0xD9) { // EOI(画像終了) | |
| 44 | + break; | |
| 45 | + } | |
| 46 | + } | |
| 47 | + | |
| 48 | + // kys($jpg); | |
| 49 | + | |
| 50 | + $i++; | |
| 51 | + fclose($fp); | |
| 52 | + return $jpg; | |
| 53 | + } | |
| 54 | +} |
| @@ -0,0 +1,616 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Image; | |
| 3 | + | |
| 4 | +use Std\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Png implements ImageInterface { | |
| 7 | + public \stdClass $IHDR; // 画像ヘッダー | |
| 8 | + // 横(4バイト) | |
| 9 | + // 丈(4バイト) | |
| 10 | + // ビット深さ(1バイト) | |
| 11 | + // 色類(1バイト) | |
| 12 | + //// | PNG画像タイプ | 色類 | 許可したビット深さ | 解釈 | | |
| 13 | + //// |--------------------------|------|--------------------|----------------------------------------------- | | |
| 14 | + //// | グレースケール | 0 | 1,2,4,8,16 | 各ピクセルはグレースケールのサンプル | | |
| 15 | + //// | トゥルーカラー | 2 | 8,16 | 各ピクセルはR,G,Bトリプル | | |
| 16 | + //// | 指標した色 | 3 | 1,2,4,8 | 各ピクセルはパレット指標(PLTEチャンクあり) | | |
| 17 | + //// | グレースケール+アルファ | 4 | 8,16 | 各ピクセルはグレースケール+アルファのサンプル | | |
| 18 | + //// | トゥルーカラー+アルファ | 6 | 8,16 | 各ピクセルはR,G,B,Aトリプル | | |
| 19 | + // 圧縮関数(1バイト) | |
| 20 | + //// 0 (DEFLATE)しかない | |
| 21 | + // フィルター関数(1バイト) | |
| 22 | + //// 0(アダプティブ・フィルタリング)しかない | |
| 23 | + // インタレース関数(1バイト) | |
| 24 | + //// 0 = なし | |
| 25 | + //// 1 = Adam7 | |
| 26 | + public \stdClass $PLTE; // パレット(赤、緑、青) | |
| 27 | + // 色類3 = 必須 | |
| 28 | + // 色類2と6 = 任意 | |
| 29 | + public \stdClass $IDAT; // 画像データ | |
| 30 | + public \stdClass $IEND; // 画像終了 | |
| 31 | + | |
| 32 | + public \stdClass $tRNS; // 透明情報 | |
| 33 | + // 色類0 = グレーサンプル値(2バイト) | |
| 34 | + // 色類2 = RGBサンプル値(2x2x2バイト) | |
| 35 | + // 色類3 = パレット指標のアルファ(各1バイト) | |
| 36 | + public \stdClass $cHRM; // 主な色度とホワイトポイント | |
| 37 | + // 白X(4バイト) | |
| 38 | + // 白Y(4バイト) | |
| 39 | + // 赤X(4バイト) | |
| 40 | + // 赤Y(4バイト) | |
| 41 | + // 緑X(4バイト) | |
| 42 | + // 緑Y(4バイト) | |
| 43 | + // 青X(4バイト) | |
| 44 | + // 青Y(4バイト) | |
| 45 | + public \stdClass $gAMA; // ガンマ(4バイト) | |
| 46 | + public \stdClass $iCCP; // 組み込みしたICCプロファイル | |
| 47 | + // プロファイル名(1~79バイト) | |
| 48 | + // NULL分離子(1バイト) | |
| 49 | + // 圧縮関数(1バイト) | |
| 50 | + // 圧縮プロファイル(nバイト) | |
| 51 | + public \stdClass $sBIT; // 上位ビット(4バイト) | |
| 52 | + // 色類0 = グレーサンプルビット(1バイト) | |
| 53 | + // 色類2, 3 = RGBサンプルビット(1x1x1バイト) | |
| 54 | + // 色類4 = グレーサンプルビット(1x1バイト) | |
| 55 | + // 色類6 = RGBサンプルビット(1x1x1x1バイト) | |
| 56 | + public \stdClass $sRGB; // レンダリングインテント(1バイト) | |
| 57 | + // 0 = 知覚 | |
| 58 | + // 1 = 相対カラー・メトリック | |
| 59 | + // 2 = 彩度 | |
| 60 | + // 3 = 絶対カラー・メトリック | |
| 61 | + public \stdClass $cICP; // (1バイト) | |
| 62 | + public \stdClass $mDCV; // (1バイト) | |
| 63 | + public \stdClass $cLLI; // (1バイト) | |
| 64 | + public \stdClass $tEXt; // 文字データ | |
| 65 | + public \stdClass $iTXt; // 圧縮した文字データ | |
| 66 | + public \stdClass $zTXt; // 国際文字データ | |
| 67 | + public \stdClass $bKGD; // 拝啓色 | |
| 68 | + public \stdClass $hIST; // 画像ヒストグラム | |
| 69 | + public \stdClass $pHYs; // 物理的なぷくセルのサイズ | |
| 70 | + // 単位当たりのピクセル数(PPU)のX軸(4バイト) | |
| 71 | + // 単位当たりのピクセル数(PPU)のY軸(4バイト) | |
| 72 | + // ユニット指定子(1バイト) | |
| 73 | + //// 0 = 不明 | |
| 74 | + //// 1 = メートル | |
| 75 | + public \stdClass $sPLT; // 提案されている配色パターン | |
| 76 | + // パレット名(1~79バイト) | |
| 77 | + // NULL分離子(1バイト) | |
| 78 | + // 赤(1か2バイト) | |
| 79 | + // 緑(1か2バイト) | |
| 80 | + // 青(1か2バイト) | |
| 81 | + // アルファ(1か2バイト) | |
| 82 | + // 周波数(2バイト) | |
| 83 | + // 等 | |
| 84 | + public \stdClass $eXIf; // Exifプロファイル | |
| 85 | + public \stdClass $tIME; // 画像の最後変更日 | |
| 86 | + // 年(2バイト) | |
| 87 | + // 月(1バイト:1-12) | |
| 88 | + // 日(1バイト:1-31) | |
| 89 | + // 時(1バイト:0-23) | |
| 90 | + // 分(1バイト:0-59) | |
| 91 | + // 秒(1バイト:0-60) | |
| 92 | + public \stdClass $acTL; // アニメーション・コントロール | |
| 93 | + // フレーム数(4バイト) | |
| 94 | + // 再生数(4バイト、0 = 無限) | |
| 95 | + | |
| 96 | + public array $frames = []; | |
| 97 | + private ?array $currentFrame = null; | |
| 98 | + | |
| 99 | + // TODO: 残り: sBIT, iTXt, hIST, sPLT, tIME | |
| 100 | + public function parse(string $file): \stdClass { | |
| 101 | + $png = new \stdClass(); | |
| 102 | + $fp = fopen($file, 'rb'); | |
| 103 | + | |
| 104 | + $magic = bin2hex(fread($fp, 8)); // マジックのスキップ | |
| 105 | + $length = unpack('N', fread($fp, 4))[1]; | |
| 106 | + $header = fread($fp, 4); | |
| 107 | + if ($header !== 'IHDR') { | |
| 108 | + fclose($fp); | |
| 109 | + return $png; | |
| 110 | + } | |
| 111 | + | |
| 112 | + // IHDRデータ | |
| 113 | + $data = fread($fp, $length); | |
| 114 | + | |
| 115 | + // N = ビッグエンディアン(4ビット) | |
| 116 | + $this->IHDR = new \stdClass; | |
| 117 | + $this->IHDR->name = 'IHDR'; | |
| 118 | + $this->IHDR->length = $length; | |
| 119 | + $this->IHDR->width = unpack('N', substr($data, 0, 4))[1]; | |
| 120 | + $this->IHDR->height = unpack('N', substr($data, 4, 4))[1]; | |
| 121 | + $this->IHDR->bitDepth = ord($data[8]); // 1, 2, 4, 8, 16 | |
| 122 | + $this->IHDR->numColors = 1 << $this->IHDR->bitDepth; | |
| 123 | + $this->IHDR->colorType = ord($data[9]); // 0, 2, 3, 4, 6 | |
| 124 | + $this->IHDR->compressionType = ord($data[10]); | |
| 125 | + $this->IHDR->compression = 'DEFLATE'; | |
| 126 | + $this->IHDR->filter = ord($data[11]); | |
| 127 | + $this->IHDR->interlace = ord($data[12]); // 0 = なし, 1 = Adam7 | |
| 128 | + $this->IHDR->bpp = $this->IHDR->bitDepth * $this->getSamplesPerPixel($this->IHDR->colorType); | |
| 129 | + $this->IHDR->hasAlpha = ($this->IHDR->colorType === 4 || $this->IHDR->colorType === 6); | |
| 130 | + $crc = fread($fp, 4); | |
| 131 | + $this->IHDR->crc = $crc; | |
| 132 | + $this->IHDR->crc32 = unpack('N', $crc)[1]; | |
| 133 | + $this->IHDR->crcHex = bin2hex($crc); | |
| 134 | + | |
| 135 | + $png->header = $this->IHDR; | |
| 136 | + | |
| 137 | +next_chunk: | |
| 138 | + $lengthBytes = fread($fp, 4); | |
| 139 | + if (strlen($lengthBytes) < 4) { | |
| 140 | + logger(LogType::Image, 'バイト長さが悪い。ファイル:'.$file.'、バイト長さ:'.'$lengthBytes'); | |
| 141 | + fclose($fp); | |
| 142 | + return $png; | |
| 143 | + } | |
| 144 | + | |
| 145 | + $length = unpack('N', $lengthBytes)[1]; | |
| 146 | + $nextChunk = fread($fp, 4); | |
| 147 | + $data = $length > 0 ? fread($fp, $length) : ''; | |
| 148 | + $crc = fread($fp, 4); | |
| 149 | + | |
| 150 | + // PLTEデータ | |
| 151 | + if ($nextChunk === "\x50\x4c\x54\x45") { // PLTE | |
| 152 | + $palette = []; | |
| 153 | + for ($i = 0; $i < $length; $i += 3) { | |
| 154 | + $colors = new \Std\Lib\RGB(ord($data[$i]), ord($data[$i+1]), ord($data[$i+2])); | |
| 155 | + $palette[] = $colors->rgb; | |
| 156 | + } | |
| 157 | + | |
| 158 | + $this->PLTE = new \stdClass; | |
| 159 | + $this->PLTE->name = 'PLTE'; | |
| 160 | + $this->PLTE->length = $length; | |
| 161 | + $this->PLTE->palette = $palette; | |
| 162 | + $this->PLTE->crc = $crc; | |
| 163 | + $this->PLTE->crc32 = unpack('N', $crc)[1]; | |
| 164 | + $this->PLTE->crcHex = bin2hex($crc); | |
| 165 | + | |
| 166 | + $png->palette = $this->PLTE; | |
| 167 | + goto next_chunk; | |
| 168 | + } else if ($nextChunk === "\x74\x52\x4e\x53") { // tRNS | |
| 169 | + $this->tRNS = new \stdClass; | |
| 170 | + $this->tRNS->name = 'tRNS'; | |
| 171 | + $this->tRNS->length = $length; | |
| 172 | + $vals = []; | |
| 173 | + $bytes = $this->IHDR->colorType === 2 ? 3 : 1; | |
| 174 | + for ($i = 0; $i < $length; $i += $bytes) { | |
| 175 | + if ($this->IHDR->colorType === 2) { | |
| 176 | + $colors = new \Std\Lib\RGB(ord($data[$i]), ord($data[$i+1]), ord($data[$i+2])); | |
| 177 | + $vals[] = $colors->rgb; | |
| 178 | + } else { | |
| 179 | + $vals[] = ord($data[$i]); | |
| 180 | + } | |
| 181 | + } | |
| 182 | + $this->tRNS->values = $vals; | |
| 183 | + $this->tRNS->crc = $crc; | |
| 184 | + $this->tRNS->crc32 = unpack('N', $crc)[1]; | |
| 185 | + $this->tRNS->crcHex = bin2hex($crc); | |
| 186 | + $png->transparency = $this->tRNS; | |
| 187 | + goto next_chunk; | |
| 188 | + } else if ($nextChunk === "\x63\x48\x52\x4d") { // cHRM | |
| 189 | + $this->cHRM = new \stdClass; | |
| 190 | + $this->cHRM->name = 'cHRM'; | |
| 191 | + $this->cHRM->length = $length; | |
| 192 | + $this->cHRM->whitePointX = unpack('N', substr($data, 0, 4))[1]; | |
| 193 | + $this->cHRM->whitePointY = unpack('N', substr($data, 4, 4))[1]; | |
| 194 | + $this->cHRM->redX = unpack('N', substr($data, 8, 4))[1]; | |
| 195 | + $this->cHRM->redY = unpack('N', substr($data, 12, 4))[1]; | |
| 196 | + $this->cHRM->greenX = unpack('N', substr($data, 16, 4))[1]; | |
| 197 | + $this->cHRM->greenY = unpack('N', substr($data, 20, 4))[1]; | |
| 198 | + $this->cHRM->blueX = unpack('N', substr($data, 24, 4))[1]; | |
| 199 | + $this->cHRM->blueY = unpack('N', substr($data, 28, 4))[1]; | |
| 200 | + $this->cHRM->crc = $crc; | |
| 201 | + $this->cHRM->crc32 = unpack('N', $crc)[1]; | |
| 202 | + $this->cHRM->crcHex = bin2hex($crc); | |
| 203 | + $png->chromaticities = $this->cHRM; | |
| 204 | + goto next_chunk; | |
| 205 | + } else if ($nextChunk === "\x67\x41\x4d\x41") { // gAMA | |
| 206 | + $this->gAMA = new \stdClass; | |
| 207 | + $this->gAMA->name = 'gAMA'; | |
| 208 | + $this->gAMA->length = $length; | |
| 209 | + $this->gAMA->value = unpack('N', $data)[1]; | |
| 210 | + $this->gAMA->crc = $crc; | |
| 211 | + $this->gAMA->crc32 = unpack('N', $crc)[1]; | |
| 212 | + $this->gAMA->crcHex = bin2hex($crc); | |
| 213 | + $png->gamma = $this->gAMA; | |
| 214 | + goto next_chunk; | |
| 215 | + } else if ($nextChunk === "\x69\x43\x43\x50") { // iCCP | |
| 216 | + $this->iCCP = new \stdClass; | |
| 217 | + $this->iCCP->name = 'iCCP'; | |
| 218 | + $this->iCCP->length = $length; | |
| 219 | + | |
| 220 | + $nullPos = strpos($data, "\0"); | |
| 221 | + if ($nullPos === false || $nullPos < 1 || $nullPos > 79) { | |
| 222 | + $this->iCCP->valid = false; | |
| 223 | + $this->iCCP->error = "Missing or invalid profile name terminator"; | |
| 224 | + goto iccp_crc; | |
| 225 | + } | |
| 226 | + | |
| 227 | + $this->iCCP->profileName = substr($data, 0, $nullPos); | |
| 228 | + $compressionMethod = ord($data[$nullPos + 1]); | |
| 229 | + $this->iCCP->compressionMethod = $compressionMethod; | |
| 230 | + | |
| 231 | + if ($compressionMethod !== 0) { | |
| 232 | + $this->iCCP->valid = false; | |
| 233 | + $this->iCCP->error = "Unsupported compression method: {$compressionMethod}"; | |
| 234 | + goto iccp_crc; | |
| 235 | + } | |
| 236 | + | |
| 237 | + $compressedData = substr($data, $nullPos + 2); | |
| 238 | + $this->iCCP->compressedData = $compressedData; | |
| 239 | + $this->iCCP->compressedSize = strlen($compressedData); | |
| 240 | + $this->iCCP->valid = true; | |
| 241 | + | |
| 242 | +iccp_crc: | |
| 243 | + $this->iCCP->crc = $crc; | |
| 244 | + $this->iCCP->crc32 = unpack('N', $crc)[1]; | |
| 245 | + $this->iCCP->crcHex = bin2hex($crc); | |
| 246 | + $png->iccProfile = $this->iCCP; | |
| 247 | + goto next_chunk; | |
| 248 | + } else if ($nextChunk === "\x73\x42\x49\x54") { // sBIT | |
| 249 | + $this->sBIT = new \stdClass; | |
| 250 | + $this->sBIT->name = 'sBIT'; | |
| 251 | + $this->sBIT->length = $length; | |
| 252 | + kys(['sBIT', $length, bin2hex($data)]); | |
| 253 | + $this->sBIT->crc = $crc; | |
| 254 | + $this->sBIT->crc32 = unpack('N', $crc)[1]; | |
| 255 | + $this->sBIT->crcHex = bin2hex($crc); | |
| 256 | + $png->significantBits = $this->sBIT; | |
| 257 | + goto next_chunk; | |
| 258 | + } else if ($nextChunk === "\x73\x52\x47\x42") { // sRGB | |
| 259 | + $this->sRGB = new \stdClass; | |
| 260 | + $this->sRGB->name = 'sRGB'; | |
| 261 | + $this->sRGB->length = $length; | |
| 262 | + $this->sRGB->intent = ord($data); | |
| 263 | + $name = [ | |
| 264 | + 0 => 'Perceptual', | |
| 265 | + 1 => 'Relative colorimetric', | |
| 266 | + 2 => 'Saturation', | |
| 267 | + 3 => 'Absolute colorimetric', | |
| 268 | + ]; | |
| 269 | + $this->sRGB->intentName = $name[$this->sRGB->intent]; | |
| 270 | + $this->sRGB->crc = $crc; | |
| 271 | + $this->sRGB->crc32 = unpack('N', $crc)[1]; | |
| 272 | + $this->sRGB->crcHex = bin2hex($crc); | |
| 273 | + $png->rgbColorSpace = $this->sRGB; | |
| 274 | + goto next_chunk; | |
| 275 | + } else if ($nextChunk === "\x63\x49\x43\x50") { // cICP | |
| 276 | + $this->cICP = new \stdClass; | |
| 277 | + $this->cICP->name = 'cICP'; | |
| 278 | + $this->cICP->length = $length; | |
| 279 | + $this->cICP->colorPrimaries = ord($data[0]); | |
| 280 | + $this->cICP->transferFunction = ord($data[1]); | |
| 281 | + $this->cICP->matrixCoefficients = ord($data[2]); | |
| 282 | + $this->cICP->videoFullRangeFlag = ord($data[3]); | |
| 283 | + $this->cICP->crc = $crc; | |
| 284 | + $this->cICP->crc32 = unpack('N', $crc)[1]; | |
| 285 | + $this->cICP->crcHex = bin2hex($crc); | |
| 286 | + $png->videoSignalTypeId = $this->cICP; | |
| 287 | + goto next_chunk; | |
| 288 | + } else if ($nextChunk === "\x6d\x44\x43\x56") { // mDCV | |
| 289 | + $this->mDCV = new \stdClass; | |
| 290 | + $this->mDCV->name = 'mDCV'; | |
| 291 | + $this->mDCV->length = $length; | |
| 292 | + | |
| 293 | + $u16 = fn($off) => unpack('n', substr($data, $off, 2))[1]; | |
| 294 | + $u32 = fn($off) => unpack('N', substr($data, $off, 4))[1]; | |
| 295 | + $this->mDCV->primaries = [ | |
| 296 | + 'red' => ['x' => $u16(0), 'y' => $u16(2)], | |
| 297 | + 'green' => ['x' => $u16(4), 'y' => $u16(6)], | |
| 298 | + 'blue' => ['x' => $u16(8), 'y' => $u16(10)], | |
| 299 | + ]; | |
| 300 | + $this->mDCV->whitePoint = [ | |
| 301 | + 'x' => $u16(12), | |
| 302 | + 'y' => $u16(14), | |
| 303 | + ]; | |
| 304 | + $this->mDCV->luminance = [ | |
| 305 | + 'max' => $u16(16) * 0.0001, | |
| 306 | + 'min' => $u16(20) * 0.0001, | |
| 307 | + 'maxRaw' => $u16(16), | |
| 308 | + 'minRaw' => $u16(20), | |
| 309 | + ]; | |
| 310 | + $this->mDCV->crc = $crc; | |
| 311 | + $this->mDCV->crc32 = unpack('N', $crc)[1]; | |
| 312 | + $this->mDCV->crcHex = bin2hex($crc); | |
| 313 | + $png->masterDisplayColorVolume = $this->mDCV; | |
| 314 | + goto next_chunk; | |
| 315 | + } else if ($nextChunk === "\x63\x4c\x4c\x49") { // cLLI | |
| 316 | + $this->cLLI = new \stdClass; | |
| 317 | + $this->cLLI->name = 'cLLI'; | |
| 318 | + $this->cLLI->length = $length; | |
| 319 | + $this->cLLI->maxCCL = unpack('N', substr($data, 0, 4))[1] * 0.0001; | |
| 320 | + $this->cLLI->maxCCLRaw = unpack('N', substr($data, 0, 4))[1]; | |
| 321 | + $this->cLLI->maxFALL = unpack('N', substr($data, 4, 4))[1] * 0.0001; | |
| 322 | + $this->cLLI->maxFALLRaw = unpack('N', substr($data, 4, 4))[1]; | |
| 323 | + $this->cLLI->crc = $crc; | |
| 324 | + $this->cLLI->crc32 = unpack('N', $crc)[1]; | |
| 325 | + $this->cLLI->crcHex = bin2hex($crc); | |
| 326 | + $png->contentLightLevelInfo = $this->cLLI; | |
| 327 | + goto next_chunk; | |
| 328 | + } else if ($nextChunk === "\x74\x45\x58\x74") { // tEXt | |
| 329 | + if (!isset($png->textualData)) $png->textualData = []; | |
| 330 | + $this->tEXt = new \stdClass; | |
| 331 | + $this->tEXt->name = 'tEXt'; | |
| 332 | + $this->tEXt->length = $length; | |
| 333 | + $this->tEXt->text = $data; | |
| 334 | + $this->tEXt->crc = $crc; | |
| 335 | + $this->tEXt->crc32 = unpack('N', $crc)[1]; | |
| 336 | + $this->tEXt->crcHex = bin2hex($crc); | |
| 337 | + $png->textualData[] = $this->tEXt; | |
| 338 | + goto next_chunk; | |
| 339 | + } else if ($nextChunk === "\x69\x54\x58\x74") { // iTXt | |
| 340 | + if (!isset($png->internationalTextualData)) $png->textualData = []; | |
| 341 | + $this->iTXt = new \stdClass; | |
| 342 | + $this->iTXt->name = 'iTXt'; | |
| 343 | + $this->iTXt->length = $length; | |
| 344 | + kys(['iTXt', $length, bin2hex($data)]); | |
| 345 | + $this->iTXt->crc = $crc; | |
| 346 | + $this->iTXt->crc32 = unpack('N', $crc)[1]; | |
| 347 | + $this->iTXt->crcHex = bin2hex($crc); | |
| 348 | + $png->internationalTextualData[] = $this->iTXt; | |
| 349 | + kys($png); | |
| 350 | + goto next_chunk; | |
| 351 | + } else if ($nextChunk === "\x7a\x54\x58\x74") { // zTXt | |
| 352 | + if (!isset($png->compressedTextualData)) $png->textualData = []; | |
| 353 | + $this->zTXt = new \stdClass; | |
| 354 | + $this->zTXt->name = 'zTXt'; | |
| 355 | + $this->zTXt->length = $length; | |
| 356 | + | |
| 357 | + $nullPos = strpos($data, "\0"); | |
| 358 | + if ($nullPos === false || $nullPos < 1 || $nullPos > 79) { | |
| 359 | + $this->zTXt->valid = false; | |
| 360 | + $this->zTXt->error = "Missing or invalid profile name terminator"; | |
| 361 | + goto zTXt_crc; | |
| 362 | + } | |
| 363 | + | |
| 364 | + $this->zTXt->keyword = rtrim(substr($data, 0, $nullPos)); | |
| 365 | + $compressionMethod = ord($data[$nullPos + 1] ?? "\xFF"); | |
| 366 | + | |
| 367 | + $this->zTXt->compressionMethod = $compressionMethod; | |
| 368 | + | |
| 369 | + if ($compressionMethod !== 0) { | |
| 370 | + $this->zTXt->valid = false; | |
| 371 | + $this->zTXt->error = "Unsupported compression method"; | |
| 372 | + goto zTXt_crc; | |
| 373 | + } | |
| 374 | + | |
| 375 | + $compressedText = substr($data, $nullPos + 2); | |
| 376 | + fread($fp, 13); // ???????????????????????? | |
| 377 | + $this->zTXt->compressedText = $compressedText; | |
| 378 | + $this->zTXt->compressedLength = strlen($compressedText); | |
| 379 | + | |
| 380 | + $text = zlib_decode($compressedText); | |
| 381 | + | |
| 382 | + if ($text === false) { | |
| 383 | + $this->zTXt->valid = false; | |
| 384 | + $this->zTXt->decompressionError = true; | |
| 385 | + $this->zTXt->text = null; | |
| 386 | + goto zTXt_crc; | |
| 387 | + } else { | |
| 388 | + $this->zTXt->text = $text; | |
| 389 | + $this->zTXt->textUtf8 = mb_convert_encoding($text, 'UTF-8', 'ISO-8859-1'); | |
| 390 | + } | |
| 391 | + | |
| 392 | + $this->zTXt->valid = true; | |
| 393 | + | |
| 394 | +zTXt_crc: | |
| 395 | + $this->zTXt->crc = $crc; | |
| 396 | + $this->zTXt->crc32 = unpack('N', $crc)[1]; | |
| 397 | + $this->zTXt->crcHex = bin2hex($crc); | |
| 398 | + $png->compressedTextualData[] = $this->zTXt; | |
| 399 | + goto next_chunk; | |
| 400 | + } else if ($nextChunk === "\x62\x4b\x47\x44") { // bKGD | |
| 401 | + $this->bKGD = new \stdClass; | |
| 402 | + $this->bKGD->name = 'bKGD'; | |
| 403 | + $this->bKGD->length = $length; | |
| 404 | + if ($this->IHDR->colorType === 2 || $this->IHDR->colorType === 6) { | |
| 405 | + $r = (ord($data[0]) << 8) | ord($data[1]); | |
| 406 | + $g = (ord($data[2]) << 8) | ord($data[3]); | |
| 407 | + $b = (ord($data[4]) << 8) | ord($data[5]); | |
| 408 | + | |
| 409 | + $this->bKGD->type = 'rgb16'; | |
| 410 | + $this->bKGD->r = $r; | |
| 411 | + $this->bKGD->g = $g; | |
| 412 | + $this->bKGD->b = $b; | |
| 413 | + | |
| 414 | + $this->bKGD->r8 = $r >> 8; | |
| 415 | + $this->bKGD->g8 = $g >> 8; | |
| 416 | + $this->bKGD->b8 = $b >> 8; | |
| 417 | + $this->bKGD->webColor = sprintf("#%02x%02x%02x", $this->bKGD->r8, $this->bKGD->g8, $this->bKGD->b8); | |
| 418 | + } else if ($this->IHDR->colorType === 2 || $this->IHDR->colorType === 6) { | |
| 419 | + $gray = (ord($data[0]) << 8) | ord($data[1]); | |
| 420 | + $this->bKGD->type = 'gray16'; | |
| 421 | + $this->bKGD->gray = $gray; | |
| 422 | + $this->bKGD->gray8 = $gray >> 8; | |
| 423 | + } else if ($this->IHDR->colorType === 3) { | |
| 424 | + $this->bKGD->type = 'palette_index'; | |
| 425 | + $this->bKGD->value = ord($data); | |
| 426 | + } else { | |
| 427 | + $this->bKGD->type = 'unknown'; | |
| 428 | + $this->bKGD->error = 'Invalid color type for bKGD'; | |
| 429 | + } | |
| 430 | + | |
| 431 | + $this->bKGD->crc = $crc; | |
| 432 | + $this->bKGD->crc32 = unpack('N', $crc)[1]; | |
| 433 | + $this->bKGD->crcHex = bin2hex($crc); | |
| 434 | + $png->backgroundColor = $this->bKGD; | |
| 435 | + goto next_chunk; | |
| 436 | + } else if ($nextChunk === "\x68\x49\x53\x54") { // hIST | |
| 437 | + $this->hIST = new \stdClass; | |
| 438 | + $this->hIST->name = 'hIST'; | |
| 439 | + $this->hIST->length = $length; | |
| 440 | + kys(['hIST', $length, bin2hex($data)]); | |
| 441 | + $this->hIST->crc = $crc; | |
| 442 | + $this->hIST->crc32 = unpack('N', $crc)[1]; | |
| 443 | + $this->hIST->crcHex = bin2hex($crc); | |
| 444 | + $png->imageHistogram = $this->hIST; | |
| 445 | + kys($png); | |
| 446 | + goto next_chunk; | |
| 447 | + } else if ($nextChunk === "\x70\x48\x59\x73") { // pHYs | |
| 448 | + $this->pHYs = new \stdClass; | |
| 449 | + $this->pHYs->name = 'pHYs'; | |
| 450 | + $this->pHYs->length = $length; | |
| 451 | + $this->pHYs->x = unpack('N', substr($data, 0, 4))[1]; | |
| 452 | + $this->pHYs->y = unpack('N', substr($data, 4, 4))[1]; | |
| 453 | + $unit = [ | |
| 454 | + 0 => 'unit is unknown', | |
| 455 | + 1 => 'unit is the metre', | |
| 456 | + ]; | |
| 457 | + $this->pHYs->unitId = ord($data[8]); | |
| 458 | + $this->pHYs->unit = $unit[$this->pHYs->unitId]; | |
| 459 | + $this->pHYs->crc = $crc; | |
| 460 | + $this->pHYs->crc32 = unpack('N', $crc)[1]; | |
| 461 | + $this->pHYs->crcHex = bin2hex($crc); | |
| 462 | + $png->physicalPixelDimensions = $this->pHYs; | |
| 463 | + goto next_chunk; | |
| 464 | + } else if ($nextChunk === "\x73\x50\x4c\x54") { // sPLT | |
| 465 | + $this->sPLT = new \stdClass; | |
| 466 | + $this->sPLT->name = 'sPLT'; | |
| 467 | + $this->sPLT->length = $length; | |
| 468 | + kys(['sPLT', $length, bin2hex($data)]); | |
| 469 | + $this->sPLT->crc = $crc; | |
| 470 | + $this->sPLT->crc32 = unpack('N', $crc)[1]; | |
| 471 | + $this->sPLT->crcHex = bin2hex($crc); | |
| 472 | + $png->suggestedPalette = $this->sPLT; | |
| 473 | + kys($png); | |
| 474 | + goto next_chunk; | |
| 475 | + } else if ($nextChunk === "\x65\x58\x49\x66") { // eXIf | |
| 476 | + $this->eXIf = new \stdClass; | |
| 477 | + $this->eXIf->name = 'eXIf'; | |
| 478 | + $this->eXIf->length = $length; | |
| 479 | + | |
| 480 | + if (strlen($data) < 8) { | |
| 481 | + $this->eXIf->valid = false; | |
| 482 | + $this->eXIf->error = "eXIf chunk too short"; | |
| 483 | + goto exif_crc; | |
| 484 | + } | |
| 485 | + | |
| 486 | + $byteOrder = substr($data, 0, 2); | |
| 487 | + $magic = substr($data, 2, 2); | |
| 488 | + | |
| 489 | + $this->eXIf->rawData = $data; | |
| 490 | + $this->eXIf->rawLength = $length; | |
| 491 | + $this->eXIf->byteOrder = $byteOrder; // II = リトルエンディアン、MM = ビッグエンディアン | |
| 492 | + $this->eXIf->isBigEndian = ($byteOrder === 'MM'); | |
| 493 | + $this->eXIf->magic = unpack('n', $magic)[1]; // いつでも42ではず | |
| 494 | + | |
| 495 | + if (strlen($data) < 8) { | |
| 496 | + $this->eXIf->valid = false; | |
| 497 | + $this->eXIf->error = "Invalid TIFF magic number"; | |
| 498 | + goto exif_crc; | |
| 499 | + } | |
| 500 | + | |
| 501 | + $this->eXIf->valid = true; | |
| 502 | + | |
| 503 | + $ifdOffset = unpack($this->eXIf->isBigEndian ? 'N' : 'V', substr($data, 4, 4))[1]; | |
| 504 | + $this->eXIf->firstIfdOffset = $ifdOffset; | |
| 505 | + | |
| 506 | +exif_crc: | |
| 507 | + $this->eXIf->crc = $crc; | |
| 508 | + $this->eXIf->crc32 = unpack('N', $crc)[1]; | |
| 509 | + $this->eXIf->crcHex = bin2hex($crc); | |
| 510 | + $png->exifProfile = $this->eXIf; | |
| 511 | + goto next_chunk; | |
| 512 | + } else if ($nextChunk === "\x74\x49\x4d\x45") { // tIME | |
| 513 | + $this->tIME = new \stdClass; | |
| 514 | + $this->tIME->name = 'tIME'; | |
| 515 | + $this->tIME->length = $length; | |
| 516 | + kys(['tIME', $length, bin2hex($data)]); | |
| 517 | + $this->tIME->crc = $crc; | |
| 518 | + $this->tIME->crc32 = unpack('N', $crc)[1]; | |
| 519 | + $this->tIME->crcHex = bin2hex($crc); | |
| 520 | + $png->lastModificationTime = $this->tIME; | |
| 521 | + kys($png); | |
| 522 | + goto next_chunk; | |
| 523 | + } else if ($nextChunk === "\x61\x63\x54\x4c") { // acTL | |
| 524 | + $this->acTL = new \stdClass; | |
| 525 | + $this->acTL->name = 'acTL'; | |
| 526 | + $this->acTL->length = $length; | |
| 527 | + $this->acTL->numFrames = unpack('N', substr($data, 0, 4))[1]; | |
| 528 | + $this->acTL->numPlays = unpack('N', substr($data, 4, 4))[1]; | |
| 529 | + $this->acTL->crc = $crc; | |
| 530 | + $this->acTL->crc32 = unpack('N', $crc)[1]; | |
| 531 | + $this->acTL->crcHex = bin2hex($crc); | |
| 532 | + $png->aniationControl = $this->acTL; | |
| 533 | + goto next_chunk; | |
| 534 | + } else if ($nextChunk === "\x66\x63\x54\x4c") { // fcTL | |
| 535 | + if ($this->currentFrame !== null) $this->frames[] = (object)$this->currentFrame; | |
| 536 | + | |
| 537 | + $this->currentFrame = [ | |
| 538 | + 'name' => 'fcTL', | |
| 539 | + 'length' => $length, | |
| 540 | + 'sequenceNumber' => unpack('N', substr($data, 0, 4))[1], | |
| 541 | + 'width' => unpack('N', substr($data, 4, 4))[1], | |
| 542 | + 'height' => unpack('N', substr($data, 8, 4))[1], | |
| 543 | + 'xOffset' => unpack('N', substr($data, 12, 4))[1], | |
| 544 | + 'yOffset' => unpack('N', substr($data, 16, 4))[1], | |
| 545 | + 'delayNum' => unpack('n', substr($data, 20, 2))[1], | |
| 546 | + 'delayDen' => unpack('n', substr($data, 22, 2))[1], | |
| 547 | + 'disposeOp' => ord($data[24]), | |
| 548 | + 'blendOp' => ord($data[25]), | |
| 549 | + 'data' => null, | |
| 550 | + 'crc' => $crc, | |
| 551 | + 'crc32' => unpack('N', $crc)[1], | |
| 552 | + 'crcHex' => bin2hex($crc), | |
| 553 | + ]; | |
| 554 | + | |
| 555 | + goto next_chunk; | |
| 556 | + } else if ($nextChunk === "\x49\x44\x41\x54" || $nextChunk === "\x66\x64\x41\x54") { // IDAT || fdAT | |
| 557 | + if ($this->currentFrame === null) { | |
| 558 | + $this->currentFrame = [ | |
| 559 | + 'sequenceNumber' => 0, | |
| 560 | + 'isFirstFrame' => true, | |
| 561 | + 'width' => $this->IHDR->width, | |
| 562 | + 'height' => $this->IHDR->height, | |
| 563 | + 'data' => null, | |
| 564 | + ]; | |
| 565 | + } | |
| 566 | + | |
| 567 | + $compressedData = ($nextChunk === "\x66\x64\x41\x54") ? substr($data, 4) : $data; | |
| 568 | + $this->currentFrame['name'] = 'fdAT'; | |
| 569 | + $this->currentFrame['length'] = $length; | |
| 570 | + $this->currentFrame['data'] = $compressedData; | |
| 571 | + $this->currentFrame['dataBase64'] = base64_encode($compressedData); | |
| 572 | + $this->currentFrame['dataHex'] = bin2hex($compressedData); | |
| 573 | + $this->currentFrame['dataLength'] = strlen($compressedData); | |
| 574 | + $this->currentFrame['dataChunkType'] = $nextChunk; | |
| 575 | + $this->currentFrame['crc'] = $crc; | |
| 576 | + $this->currentFrame['crcBase64'] = unpack('N', $crc)[1]; | |
| 577 | + $this->currentFrame['crcHex'] = bin2hex($crc); | |
| 578 | + | |
| 579 | + if ($nextChunk === "\x49\x44\x41\x54") { | |
| 580 | + $this->currentFrame['name'] = 'IDAT'; | |
| 581 | + $this->frames[] = (object)$this->currentFrame; | |
| 582 | + $this->currentFrame = null; | |
| 583 | + } | |
| 584 | + | |
| 585 | + $png->data = $this->frames; | |
| 586 | + goto next_chunk; | |
| 587 | + } else if ($nextChunk === "\x49\x45\x4e\x44") { // IEND | |
| 588 | + if ($this->currentFrame !== null) { | |
| 589 | + $this->frames[] = (object)$this->currentFrame; | |
| 590 | + } | |
| 591 | + $png->end = (object)['crc' => $crc, 'crc32' => unpack('N', $crc)[1], 'crcHex' => bin2hex($crc)]; | |
| 592 | + } else { | |
| 593 | + fclose($fp); | |
| 594 | + $err = '不明なチャンク:'.$nextChunk.'、HEX:'.bin2hex($nextChunk); | |
| 595 | + kys($err); | |
| 596 | + logger(\LogType::Image, '【PNG】'.$err); | |
| 597 | + return $png; | |
| 598 | + } | |
| 599 | + | |
| 600 | + fclose($fp); | |
| 601 | + return $png; | |
| 602 | + } | |
| 603 | + | |
| 604 | + /////////////// | |
| 605 | + | |
| 606 | + private function getSamplesPerPixel(int $colorType): int { | |
| 607 | + return match($colorType) { | |
| 608 | + 0 => 1, // グレースケール | |
| 609 | + 2 => 3, // RGB | |
| 610 | + 3 => 1, // 指標した色(ペレット) | |
| 611 | + 4 => 2, // グレースケール+アルファ | |
| 612 | + 6 => 4, // RGBA | |
| 613 | + default => 1, | |
| 614 | + }; | |
| 615 | + } | |
| 616 | +} |
| @@ -0,0 +1,115 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Image; | |
| 3 | + | |
| 4 | +use Std\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Targa implements ImageInterface { | |
| 7 | + public int $id = 0; | |
| 8 | + public int $colorMapType = 0; | |
| 9 | + public int $imageTypeByte = 0; | |
| 10 | + public string $imageType = ''; | |
| 11 | + public int $colorMapEntryIndex = 0; | |
| 12 | + public int $colorMapLength = 0; | |
| 13 | + public int $colorMapBits = 0; | |
| 14 | + public int $x = 0; | |
| 15 | + public int $y = 0; | |
| 16 | + public int $width = 0; | |
| 17 | + public int $height = 0; | |
| 18 | + public int $bitDepth = 0; | |
| 19 | + public int $bpp = 0; | |
| 20 | + public int $interlace = 0; | |
| 21 | + public int $isRightToLeft = 0; // 0 = 左→右, 1 = 右→左 | |
| 22 | + public int $isTopToBottom = 0; // 0 = 下→上, 1 = 上→下 | |
| 23 | + public int $alphaBits = 0; | |
| 24 | + public bool $hasAlpha = false; | |
| 25 | + //-------- | |
| 26 | + public string $imageId = ''; | |
| 27 | + public string $colorMapData = ''; | |
| 28 | + public string $imageData = ''; | |
| 29 | + | |
| 30 | + private array $imageTypes = [ | |
| 31 | + 0 => 'NO DATA', | |
| 32 | + 1 => 'UNCOMPRESSED COLOR-MAPPED IMAGE', | |
| 33 | + 2 => 'UNCOMPRESSED TRUE COLOR IMAGE', | |
| 34 | + 3 => 'UNCOMPRESSED GRAYSCALE IMAGE', | |
| 35 | + 9 => 'RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 36 | + 10 => 'RUN-LENGTH ENCODED TRUE-COLOR IMAGE', | |
| 37 | + 11 => 'RUN-LENGTH ENCODED GRAYSCALE IMAGE', | |
| 38 | + 32 => 'HUFFMAN-DELTA-RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 39 | + 33 => 'HUFFMAN-DELTA-RUN-LENGTH-4-PASS-QUADTREE-TYPE PROCESS ENCODED COLOR-MAPPED IMAGE', | |
| 40 | + ]; | |
| 41 | + | |
| 42 | + public function parse(string $file): \stdClass { | |
| 43 | + $tga = new \stdClass(); | |
| 44 | + $fp = fopen($file, 'rb'); | |
| 45 | + | |
| 46 | + $this->id = ord(fread($fp, 1)); | |
| 47 | + $this->colorMapType = ord(fread($fp, 1)); // 0 = なし, 1 = あり, 2~127 = Truevision向け, 128~255 = 開発者向け | |
| 48 | + $this->imageTypeByte = ord(fread($fp, 1)); | |
| 49 | + $this->imageType = $this->imageTypes[$this->imageTypeByte]; | |
| 50 | + $tga->id = $this->id; | |
| 51 | + $tga->colorMapType = $this->colorMapType; | |
| 52 | + $tga->imageTypeByte = $this->imageTypeByte; | |
| 53 | + $tga->imageType = $this->imageType; | |
| 54 | + | |
| 55 | + $this->colorMapEntryIndex = unpack('v', fread($fp, 2))[1]; | |
| 56 | + $this->colorMapLength = unpack('v', fread($fp, 2))[1]; | |
| 57 | + $this->colorMapBits = ord(fread($fp, 1)); | |
| 58 | + $tga->colorMapEntryIndex = $this->colorMapEntryIndex; | |
| 59 | + $tga->colorMapLength = $this->colorMapLength; | |
| 60 | + $tga->colorMapBits = $this->colorMapBits; | |
| 61 | + | |
| 62 | + $this->x = unpack('v', fread($fp, 2))[1]; | |
| 63 | + $this->y = unpack('v', fread($fp, 2))[1]; | |
| 64 | + $this->width = unpack('v', fread($fp, 2))[1]; | |
| 65 | + $this->height = unpack('v', fread($fp, 2))[1]; | |
| 66 | + $this->bitDepth = ord(fread($fp, 1)); | |
| 67 | + $tga->x = $this->x; | |
| 68 | + $tga->y = $this->y; | |
| 69 | + $tga->width = $this->width; | |
| 70 | + $tga->height = $this->height; | |
| 71 | + $tga->bitDepth = $this->bitDepth; | |
| 72 | + | |
| 73 | + $desc = ord(fread($fp, 1)); | |
| 74 | + $this->interlace = ($desc & 0xC0) >> 6; | |
| 75 | + $this->isTopToBottom = ($desc & 0x20) !== 0; | |
| 76 | + $this->isRightToLeft = ($desc & 0x10) !== 0; | |
| 77 | + $this->alphaBits = $desc & 0x0F; | |
| 78 | + $this->hasAlpha = ($this->alphaBits == 8) && in_array($this->imageTypeByte == [2, 3, 10, 11]); | |
| 79 | + $tga->interlace = $this->interlace; | |
| 80 | + $tga->isRightToLeft = $this->isRightToLeft; | |
| 81 | + $tga->isTopToBottom = $this->isTopToBottom; | |
| 82 | + $tga->alphaBits = $this->alphaBits; | |
| 83 | + $tga->hasAlpha = $this->hasAlpha; | |
| 84 | + | |
| 85 | + $needsXFlip = $tga->isRightToLeft; | |
| 86 | + $needsYFlip = !$tga->isTopToBottom; | |
| 87 | + // 0010 0000 | |
| 88 | + | |
| 89 | + //--------- | |
| 90 | + $imageIdBytes = $this->id; | |
| 91 | + $colorMapBytes = 0; | |
| 92 | + if ($this->colorMapType === 1 && $this->colorMapLength > 0) { | |
| 93 | + $bytesPerEntry = (int)($this->colorMapBits / 8); | |
| 94 | + $colorMapBytes = $this->colorMapLength * $bytesPerEntry; | |
| 95 | + } | |
| 96 | + | |
| 97 | + $this->imageId = $imageIdBytes > 0 ? fread($fp, $imageIdBytes) : ''; | |
| 98 | + $this->colorMapData = $colorMapBytes > 0 ? fread($fp, $colorMapBytes) : ''; | |
| 99 | + $tga->imageId = $this->imageId; | |
| 100 | + $tga->colorMapData = $this->colorMapData; | |
| 101 | + | |
| 102 | + if (in_array($this->imageTypeByte, [9, 10, 11])) { // RLE | |
| 103 | + // TODO | |
| 104 | + } else if (in_array($this->imageTypeByte, [1, 2, 3])) { // 非圧縮化 | |
| 105 | + $this->bpp = (int)($this->bitDepth / 8); | |
| 106 | + $bytes = $this->width * $this->height * $this->bpp; | |
| 107 | + $this->imageData = fread($fp, $bytes); | |
| 108 | + $tga->bpp = $this->bpp; | |
| 109 | + $tga->imageData = $this->imageData; | |
| 110 | + } | |
| 111 | + | |
| 112 | + fclose($fp); | |
| 113 | + return $tga; | |
| 114 | + } | |
| 115 | +} |
| @@ -0,0 +1,238 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +class Mailer { | |
| 5 | + private $socket; | |
| 6 | + private string $host; | |
| 7 | + private int $port; | |
| 8 | + private ?string $user; | |
| 9 | + private ?string $pass; | |
| 10 | + | |
| 11 | + private ?string $pgpKey; | |
| 12 | + private ?string $pgpPass; | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * コンストラクタ | |
| 16 | + */ | |
| 17 | + public function __construct() { | |
| 18 | + if (!MAILER_ENABLED) { | |
| 19 | + throw new \Exception("メーラーは無効です。"); | |
| 20 | + } | |
| 21 | + | |
| 22 | + $this->host = MAILINFO['host']; | |
| 23 | + $this->port = MAILINFO['port']; | |
| 24 | + $this->user = MAILINFO['user']; | |
| 25 | + $this->pass = MAILINFO['pass']; | |
| 26 | + } | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * ソケット接続を開く。 | |
| 30 | + * | |
| 31 | + * @return void | |
| 32 | + */ | |
| 33 | + public function connect(): void { | |
| 34 | + $this->socket = fsockopen($this->host, $this->port, $errno, $err, 30); | |
| 35 | + if (!$this->socket) { | |
| 36 | + $msg = "接続に失敗: {$err} ({$errno})"; | |
| 37 | + logger(\LogType::Mailer, $msg); | |
| 38 | + throw new \Exception($msg); | |
| 39 | + } | |
| 40 | + | |
| 41 | + $this->readResponse(); | |
| 42 | + } | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * ソケット接続を切断する。 | |
| 46 | + * | |
| 47 | + * @return void | |
| 48 | + */ | |
| 49 | + public function disconnect(): void { | |
| 50 | + $this->sendCommand('QUIT', 221); | |
| 51 | + fclose($this->socket); | |
| 52 | + } | |
| 53 | + | |
| 54 | + /** | |
| 55 | + * サーバーで認証する。 | |
| 56 | + * ユーザー名とパスワードが指定されている場合はAUTH LOGINを使用する。 | |
| 57 | + * | |
| 58 | + * @return void | |
| 59 | + */ | |
| 60 | + public function authenticate(): void { | |
| 61 | + $ehloRes = $this->sendCommand('EHLO '.$this->host, 250); | |
| 62 | + | |
| 63 | + // STARTTLSは対応するかどうか確認 | |
| 64 | + if (strpos($ehloRes, 'STARTTLS') !== false) { | |
| 65 | + $this->sendCommand('STARTTLS', 220); | |
| 66 | + | |
| 67 | + // TLS暗号化 | |
| 68 | + if (!stream_socket_enable_crypto( | |
| 69 | + $this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { | |
| 70 | + $msg = "TLSハンドシェイクに失敗"; | |
| 71 | + logger(\LogType::Mailer, $msg); | |
| 72 | + throw new \Exception($msg); | |
| 73 | + } | |
| 74 | + | |
| 75 | + $this->sendCommand('EHLO '.gethostname(), 250); | |
| 76 | + } | |
| 77 | + | |
| 78 | + if ($this->user && $this->pass) { | |
| 79 | + $this->sendCommand('AUTH LOGIN', 334); | |
| 80 | + $this->sendCommand(base64_encode($this->user), 334); | |
| 81 | + $this->sendCommand(base64_encode($this->pass), 235); | |
| 82 | + } | |
| 83 | + } | |
| 84 | + | |
| 85 | + /** | |
| 86 | + * TLSハンドシェイクに失敗する。 | |
| 87 | + * | |
| 88 | + * @param string $to 受信者のメールアドレス。 | |
| 89 | + * @param string $subject メールの件名。 | |
| 90 | + * @param string $body メールの本文。 | |
| 91 | + * @param string|null $toName 受信者の名前。 | |
| 92 | + * @param string|null $replyTo 返信先メールアドレス。 | |
| 93 | + * @param string|null $replyToName 返信先の名前。 | |
| 94 | + * @param bool $pgpSign PGPで署名するかどうか(現在は正常に動作していません)。 | |
| 95 | + * | |
| 96 | + * @return void | |
| 97 | + */ | |
| 98 | + public function send( | |
| 99 | + string $to, | |
| 100 | + string $subject, | |
| 101 | + string $body, | |
| 102 | + ?string $toName = null, | |
| 103 | + ?string $replyTo = null, | |
| 104 | + ?string $replyToName = null, | |
| 105 | + bool $pgpSign = false | |
| 106 | + ): void { | |
| 107 | + $from = MAILINFO['from']; | |
| 108 | + | |
| 109 | + $this->sendCommand("MAIL FROM: <{$from}>", 250); | |
| 110 | + $this->sendCommand("RCPT TO: <{$to}>", 250); | |
| 111 | + $this->sendCommand('DATA', 354); | |
| 112 | + | |
| 113 | + $headers = "Date: ".date('r')."\r\n"; // RFC 2822 | |
| 114 | + | |
| 115 | + $encSubject = mb_encode_mimeheader($subject, 'UTF-8', 'Q'); | |
| 116 | + $headers .= "Subject: {$encSubject}\r\n"; | |
| 117 | + | |
| 118 | + $fromHeader = mb_encode_mimeheader(SITEINFO['title'], 'UTF-8', 'Q')." <{$from}>"; | |
| 119 | + $headers .= "From: {$fromHeader}\r\n"; | |
| 120 | + | |
| 121 | + $toHeader = $toName | |
| 122 | + ? mb_encode_mimeheader($toName, 'UTF-8', 'Q')." <{$to}>" : $to; | |
| 123 | + $headers .= "To: {$toHeader}\r\n"; | |
| 124 | + | |
| 125 | + if ($replyTo) { | |
| 126 | + $replyToHeader = $replyToName | |
| 127 | + ? mb_encode_mimeheader($replyToName, 'UTF-8', 'Q')." <{$replyTo}>" : $replyTo; | |
| 128 | + $headers .= "Reply-To: {$replyToHeader}\r\n"; | |
| 129 | + } | |
| 130 | + | |
| 131 | + $headers .= "MIME-Version: 1.0\r\n"; | |
| 132 | + | |
| 133 | + if ($pgpSign) { | |
| 134 | + $boundary = uniqid('BOUNDARY_'); | |
| 135 | + $headers .= "Content-Type: multipart/signed;\r\n"; | |
| 136 | + $headers .= " protocol=\"application/pgp-signature\";\r\n"; | |
| 137 | + $headers .= " micalg=php-sha512;\r\n"; | |
| 138 | + $headers .= " boundary=\"{$boundary}\"\r\n"; | |
| 139 | + } else { | |
| 140 | + $headers .= "Content-Type: text/plain; charset=utf-8\r\n"; | |
| 141 | + $headers .= "Content-Transfer-Encoding: quoted-printable\r\n"; | |
| 142 | + } | |
| 143 | + | |
| 144 | + $headers .= "X-Mailer: 076 Little Beast\r\n"; | |
| 145 | + | |
| 146 | + $encBody = quoted_printable_encode($body); | |
| 147 | + $data = $headers."\r\n".$encBody; | |
| 148 | + | |
| 149 | + if ($pgpSign) { | |
| 150 | + $signature = $this->signMessage($data); | |
| 151 | + $data = "--{$boundary}\r\n" | |
| 152 | + .$data."\r\n" | |
| 153 | + ."Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n" | |
| 154 | + ."Content-Disposition: attachment; filename=\"signature.asc\"\r\n\r\n" | |
| 155 | + .$signature."\r\n" | |
| 156 | + ."--{$boundary}--\r\n"; | |
| 157 | + } | |
| 158 | + | |
| 159 | + $data .= "\r\n.\r\n"; | |
| 160 | + fwrite($this->socket, $data); | |
| 161 | + | |
| 162 | + $response = $this->readResponse(); | |
| 163 | + if (substr($response, 0, 3) != '250') { | |
| 164 | + $msg = "メール送信に失敗: {$response}"; | |
| 165 | + logger(\LogType::Mailer, $msg); | |
| 166 | + throw new \Exception($msg); | |
| 167 | + } | |
| 168 | + } | |
| 169 | + | |
| 170 | + /** | |
| 171 | + * お好みのタイムゾーンを設定する。 | |
| 172 | + * 設定しない場合、php.iniで設定されたタイムゾーンがデフォルトになる。 | |
| 173 | + * それも設定されていない場合、GMTタイムゾーンがデフォルトになる。 | |
| 174 | + * | |
| 175 | + * @param string $zone IANAタイムゾーンデータベース標準のタイムゾーン(例:Asia/Tokyo) | |
| 176 | + * @return void | |
| 177 | + */ | |
| 178 | + public function setTimezone(string $zone): void { | |
| 179 | + date_default_timezone_set($zone); | |
| 180 | + } | |
| 181 | + | |
| 182 | + /** | |
| 183 | + * PGPキーとオプションでパスフレーズを設定する。 | |
| 184 | + * | |
| 185 | + * @param string $keypath PGP署名に使用する秘密鍵へのパス。 | |
| 186 | + * @param string|null $passphrase 設定されている場合、署名用のパスフレーズ。 | |
| 187 | + * | |
| 188 | + * @return void | |
| 189 | + */ | |
| 190 | + public function enablePGP(string $keypath, ?string $passphrase = null): void { | |
| 191 | + $this->pgpKey = file_get_contents($keypath); | |
| 192 | + $this->pgpPass = $passphrase; | |
| 193 | + } | |
| 194 | + | |
| 195 | + // 機能性メソッド | |
| 196 | + | |
| 197 | + private function sendCommand(string $command, int $retcode): string { | |
| 198 | + fwrite($this->socket, $command."\r\n"); | |
| 199 | + $res = $this->readResponse(); | |
| 200 | + if (substr($res, 0, 3) != $retcode) { | |
| 201 | + $msg = "「{$command}」に対する予期しないレスポンス: {$res}"; | |
| 202 | + logger(\LogType::Mailer, $msg); | |
| 203 | + throw new \Exception($msg); | |
| 204 | + } | |
| 205 | + | |
| 206 | + return $res; | |
| 207 | + } | |
| 208 | + | |
| 209 | + private function readResponse(): string { | |
| 210 | + $res = ''; | |
| 211 | + | |
| 212 | + while ($line = fgets($this->socket, 515)) { | |
| 213 | + $res .= $line; | |
| 214 | + if (substr($line, 3, 1) == ' ') break; // レスポンスの終了だ | |
| 215 | + } | |
| 216 | + | |
| 217 | + return $res; | |
| 218 | + } | |
| 219 | + | |
| 220 | + private function signMessage(string $message): string { | |
| 221 | + if (extension_loaded('gnupg')) {// gnupg延長は有効の場合 | |
| 222 | + $gpg = new \gnupg(); | |
| 223 | + $gpg->addsignkey($this->pgpKey, $this->pgpPass); | |
| 224 | + $gpg->setsignmode(\gnupg::SIG_MODE_DETACH); | |
| 225 | + return $gpg->sign($message); | |
| 226 | + } else { // なければ、CLIツールを使う(gnupgをインストールは必須) | |
| 227 | + $tmp = tempnam(sys_get_temp_dir(), 'pgpmsg'); | |
| 228 | + file_put_contents($tmp, $message); | |
| 229 | + $sig = shell_exec( | |
| 230 | + "gpg --batch " | |
| 231 | + .($this->pgpPass ? "--passphrase {$this->pgpPass} " : '') | |
| 232 | + ."--detach-sign --armor {$tmp} 2>&1" | |
| 233 | + ); | |
| 234 | + unlink($tmp); | |
| 235 | + return $sig; | |
| 236 | + } | |
| 237 | + } | |
| 238 | +} |
| @@ -0,0 +1,648 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +class Markdown { | |
| 5 | + private string $content; | |
| 6 | + private array $html; | |
| 7 | + private string $path; | |
| 8 | + private bool $inCodeBlock = false; | |
| 9 | + private string $codeBlockLanguage = ''; | |
| 10 | + private array $codeBlockContent = []; | |
| 11 | + private const METADATA_LINE = "----"; | |
| 12 | + private array $algebraicPlaceholder = []; | |
| 13 | + | |
| 14 | + public function __construct(string $path, string $section, ?string $lang = null) { | |
| 15 | + $this->html = []; | |
| 16 | + if ($lang) $this->path = ROOT.$section.$lang.'/'.$path.'.md'; | |
| 17 | + else $this->path = ROOT.$section.$path.'.md'; | |
| 18 | + | |
| 19 | + if (!file_exists($this->path)) { | |
| 20 | + header('Location: /404'); | |
| 21 | + exit(); | |
| 22 | + } | |
| 23 | + } | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * メタデータを取得する | |
| 27 | + * | |
| 28 | + * @return \stdClass メタデータオブジェクト | |
| 29 | + */ | |
| 30 | + public function getMetadata(): \stdClass { | |
| 31 | + $content = file_get_contents($this->path); | |
| 32 | + $metadata = new \stdClass(); | |
| 33 | + | |
| 34 | + $parts = explode(self::METADATA_LINE, $content, 2); | |
| 35 | + if (count($parts) < 2) return $metadata; | |
| 36 | + | |
| 37 | + $lines = explode("\n", trim($parts[0])); | |
| 38 | + foreach ($lines as $line) { | |
| 39 | + $line = trim($line); | |
| 40 | + if (empty($line)) continue; | |
| 41 | + | |
| 42 | + $colonPos = strpos($line, ':'); | |
| 43 | + if ($colonPos === false) continue; | |
| 44 | + | |
| 45 | + $key = trim(substr($line, 0, $colonPos)); | |
| 46 | + $value = trim(substr($line, $colonPos + 1)); | |
| 47 | + $value = trim($value, '"\''); | |
| 48 | + | |
| 49 | + if ($key == 'category' || $key == 'css') { | |
| 50 | + $cat = explode(',', $value); | |
| 51 | + $value = $cat; | |
| 52 | + } | |
| 53 | + | |
| 54 | + $metadata->$key = $value; | |
| 55 | + } | |
| 56 | + | |
| 57 | + return $metadata; | |
| 58 | + } | |
| 59 | + | |
| 60 | + /** | |
| 61 | + * Markdownをパースする | |
| 62 | + * | |
| 63 | + * @return string HTMLとしてレンダリングされたコンテンツ | |
| 64 | + */ | |
| 65 | + public function parse(): string { | |
| 66 | + $content = file_get_contents($this->path); | |
| 67 | + $parts = explode(self::METADATA_LINE, $content, 2); | |
| 68 | + $this->content = count($parts) > 1 ? trim($parts[1]): trim($content); | |
| 69 | + $this->html = []; | |
| 70 | + | |
| 71 | + $lines = explode("\n", $this->content); | |
| 72 | + $currentParagraph = []; | |
| 73 | + | |
| 74 | + $inList = false; | |
| 75 | + $listItems = []; | |
| 76 | + $listLevel = 0; | |
| 77 | + $inBlockquote = false; | |
| 78 | + $blockquoteContent = []; | |
| 79 | + $tableHeaders = []; | |
| 80 | + $tableRows = []; | |
| 81 | + $inTable = false; | |
| 82 | + $i = 0; | |
| 83 | + $headerCount = 1; | |
| 84 | + | |
| 85 | + foreach ($lines as $line) { | |
| 86 | + $i++; | |
| 87 | + $hasBR = substr($line, -1) === '\\'; | |
| 88 | + $line = rtrim($line, " \t\r\n\\"); | |
| 89 | + | |
| 90 | + // コメント | |
| 91 | + // if (str_starts_with($line, '//')) { | |
| 92 | + // if ($hasBR && !empty($currentParagraph)) { | |
| 93 | + // $currentParagraph[] = ''; | |
| 94 | + // } | |
| 95 | + // continue; | |
| 96 | + // } | |
| 97 | + | |
| 98 | + // コードブロック | |
| 99 | + if (preg_match('/^```(\w*)$/', $line, $matches)) { | |
| 100 | + if (!$this->inCodeBlock) { | |
| 101 | + if (!empty($currentParagraph)) { | |
| 102 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 103 | + $currentParagraph = []; | |
| 104 | + } | |
| 105 | + $this->inCodeBlock = true; | |
| 106 | + $this->codeBlockLanguage = $matches[1]; | |
| 107 | + continue; | |
| 108 | + } else { | |
| 109 | + $this->html[] = $this->createCodeBlock(); | |
| 110 | + $this->inCodeBlock = false; | |
| 111 | + $this->codeBlockLanguage = ''; | |
| 112 | + $this->codeBlockContent = []; | |
| 113 | + continue; | |
| 114 | + } | |
| 115 | + } | |
| 116 | + | |
| 117 | + if ($this->inCodeBlock) { | |
| 118 | + $this->codeBlockContent[] = $line; | |
| 119 | + continue; | |
| 120 | + } | |
| 121 | + | |
| 122 | + // テーブルの処理 | |
| 123 | + if (preg_match('/^\|(.+)\|$/', $line)) { | |
| 124 | + if (!empty($currentParagraph)) { | |
| 125 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 126 | + $currentParagraph = []; | |
| 127 | + } | |
| 128 | + $cells = array_map('trim', explode('|', trim($line, '|'))); | |
| 129 | + | |
| 130 | + if (!$inTable) { | |
| 131 | + $tableHeaders = $cells; | |
| 132 | + $inTable = true; | |
| 133 | + } elseif (preg_match('/^\|(\s*:?-+:?\s*\|)+$/', $line)) { | |
| 134 | + continue; | |
| 135 | + } else { | |
| 136 | + $tableRows[] = $cells; | |
| 137 | + } | |
| 138 | + continue; | |
| 139 | + } elseif ($inTable) { | |
| 140 | + $this->html[] = $this->createTable($tableHeaders, $tableRows); | |
| 141 | + $tableHeaders = []; | |
| 142 | + $tableRows = []; | |
| 143 | + $inTable = false; | |
| 144 | + } | |
| 145 | + | |
| 146 | + // 水平線の処理 | |
| 147 | + if (preg_match('/^([\-\*\_])\1{2,}$/', $line)) { | |
| 148 | + if (!empty($currentParagraph)) { | |
| 149 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 150 | + $currentParagraph = []; | |
| 151 | + } | |
| 152 | + $this->html[] = " <hr />\n"; | |
| 153 | + continue; | |
| 154 | + } | |
| 155 | + | |
| 156 | + // 引用ブロックの処理 | |
| 157 | + if (preg_match('/^>\s(.+)/', $line, $matches)) { | |
| 158 | + if (!empty($currentParagraph)) { | |
| 159 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 160 | + $currentParagraph = []; | |
| 161 | + } | |
| 162 | + $inBlockquote = true; | |
| 163 | + $blockquoteContent[] = $this->parseInline($matches[1]); | |
| 164 | + continue; | |
| 165 | + } elseif ($inBlockquote && empty($line)) { | |
| 166 | + $this->html[] = $this->createBlockquote($blockquoteContent); | |
| 167 | + $blockquoteContent = []; | |
| 168 | + $inBlockquote = false; | |
| 169 | + continue; | |
| 170 | + } | |
| 171 | + | |
| 172 | + // 空行をスキップ | |
| 173 | + if (empty($line)) { | |
| 174 | + if ($inList) { | |
| 175 | + $this->html[] = $this->createList($listItems); | |
| 176 | + $listItems = []; | |
| 177 | + $inList = false; | |
| 178 | + } | |
| 179 | + if (!empty($currentParagraph)) { | |
| 180 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 181 | + $currentParagraph = []; | |
| 182 | + } | |
| 183 | + | |
| 184 | + continue; | |
| 185 | + } | |
| 186 | + | |
| 187 | + // ヘッダー | |
| 188 | + if (preg_match('/^(#{1,6})\s(.+)/', $line, $m)) { | |
| 189 | + if ($inList) { | |
| 190 | + $this->html[] = $this->createList($listItems); | |
| 191 | + $listItems = []; | |
| 192 | + $inList = false; | |
| 193 | + } | |
| 194 | + if (!empty($currentParagraph)) { | |
| 195 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 196 | + $currentParagraph = []; | |
| 197 | + } | |
| 198 | + | |
| 199 | + $level = strlen($m[1]); | |
| 200 | + $this->html[] = " <h{$level} id=\"section-{$headerCount}\">".$this->parseInline($m[2])."</h{$level}>"; | |
| 201 | + $headerCount++; | |
| 202 | + continue; | |
| 203 | + } | |
| 204 | + | |
| 205 | + // 箇条書きリスト | |
| 206 | + if (preg_match('/^(\s*)([\*\-])\s(.+)/', $line, $m)) { | |
| 207 | + if (!empty($currentParagraph)) { | |
| 208 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 209 | + $currentParagraph = []; | |
| 210 | + } | |
| 211 | + $inList = true; | |
| 212 | + $currentLevel = strlen($m[1]) / 2; | |
| 213 | + $listLevel = max($listLevel, $currentLevel); | |
| 214 | + $listItems[] = [ | |
| 215 | + 'content' => $this->parseInline($m[3]), | |
| 216 | + 'level' => $currentLevel, | |
| 217 | + 'type' => 'ul', | |
| 218 | + ]; | |
| 219 | + | |
| 220 | + continue; | |
| 221 | + } | |
| 222 | + | |
| 223 | + // 番号付きリスト | |
| 224 | + if (preg_match('/^(\s*)\d+\.\s(.+)/', $line, $m)) { | |
| 225 | + if (!empty($currentParagraph)) { | |
| 226 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 227 | + $currentParagraph = []; | |
| 228 | + } | |
| 229 | + $inList = true; | |
| 230 | + $currentLevel = strlen($m[1]) / 2; | |
| 231 | + $listLevel = max($listLevel, $currentLevel); | |
| 232 | + $listItems[] = [ | |
| 233 | + 'content' => $this->parseInline($m[2]), | |
| 234 | + 'level' => $currentLevel, | |
| 235 | + 'type' => 'ol', | |
| 236 | + ]; | |
| 237 | + | |
| 238 | + continue; | |
| 239 | + } | |
| 240 | + | |
| 241 | + if ($inList) { | |
| 242 | + $this->html[] = $this->createList($listItems); | |
| 243 | + $listItems = []; | |
| 244 | + $inList = false; | |
| 245 | + $listLevel = 0; | |
| 246 | + } | |
| 247 | + | |
| 248 | + $parsedLine = $this->parseInline($line); | |
| 249 | + $currentParagraph[] = $parsedLine; | |
| 250 | + if ($hasBR) { | |
| 251 | + $currentParagraph[] = "<br />\n "; | |
| 252 | + } | |
| 253 | + } | |
| 254 | + | |
| 255 | + if ($inList) $this->html[] = $this->createList($listItems); | |
| 256 | + if ($inBlockquote) $this->html[] = $this->createBlockquote($blockquoteContent); | |
| 257 | + if ($inTable) $this->html[] = $this->createTable($tableHeaders, $tableRows); | |
| 258 | + if (!empty($currentParagraph)) | |
| 259 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 260 | + | |
| 261 | + return implode("\n", $this->html); | |
| 262 | + } | |
| 263 | + | |
| 264 | + // 機能性メソッド | |
| 265 | + | |
| 266 | + /** | |
| 267 | + * インラインのMarkdown記法をパースする | |
| 268 | + * | |
| 269 | + * @param string $text パースするテキスト | |
| 270 | + * @return string HTMLとしてレンダリングされたテキスト | |
| 271 | + */ | |
| 272 | + private function parseInline(string $text): string { | |
| 273 | + $this->algebraicPlaceholder = []; | |
| 274 | + $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); | |
| 275 | + | |
| 276 | + $patterns = [ | |
| 277 | + // 数式 | |
| 278 | + '/\$\$([^$]+)\$\$/u' => function($matches): string { | |
| 279 | + $placeholder = "{{ALG".count($this->algebraicPlaceholder).'ALG}}'; | |
| 280 | + $this->algebraicPlaceholder[$placeholder] = $matches[1]; | |
| 281 | + return $placeholder; | |
| 282 | + }, | |
| 283 | + // 太字 | |
| 284 | + '/\*\*(.+?)\*\*/u' => '<strong>$1</strong>', | |
| 285 | + // 斜体 | |
| 286 | + '/\*(.+?)\*/u' => '<em>$1</em>', | |
| 287 | + // 下線 | |
| 288 | + '/\_(.+?)\_/u' => '<u>$1</u>', | |
| 289 | + // 取り消し線 | |
| 290 | + '/\~(.+?)\~/u' => '<s>$1</s>', | |
| 291 | + // Blink (with speed) | |
| 292 | + '/!:\((.+?)\)(.+?):!/u' => '<span class="blink" style="animation-duration: $1s;">$2</span>', | |
| 293 | + // Blink | |
| 294 | + '/!:(.+?):!/u' => '<span class="blink">$1</span>', | |
| 295 | + // フォントの大きさ | |
| 296 | + '/\^\((.+?)\)(.+?)\^/u' => '<span style="font-size: $1px;">$2</span>', | |
| 297 | + // フォントカラー | |
| 298 | + '/\%\((.+?)\)(.+?)\%/u' => '<span style="color: #$1;">$2</span>', | |
| 299 | + // 画像 | |
| 300 | + '/\!\[(.*?)(?:#([^\]]*))?\]\((.+?)\)/u' => '<img style="width: 100%; $2" src="$3" alt="$1" />', | |
| 301 | + // 音楽 | |
| 302 | + '/\$\[([^\]]+)\]\(([^\)]+)\)/u' => '<audio controls><source src="$2" type="$1" /></audio>', | |
| 303 | + // 動画 | |
| 304 | + '/\#\[([^\]]+)\]\(([^\)]+)\)/u' => '<video style="max-width: 100%;" controls><source src="$2" type="$1" /></video>', | |
| 305 | + // リンク | |
| 306 | + '/\[(.+?)\]\((.+?)\)/u' => '<a href="$2">$1</a>', | |
| 307 | + // 振り仮名 | |
| 308 | + '/\<(.+?)\>\((.+?)\)/u' => '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>', | |
| 309 | + // インラインコード | |
| 310 | + '/`(.+?)`/u' => '<code>$1</code>', | |
| 311 | + ]; | |
| 312 | + | |
| 313 | + foreach ($patterns as $pattern => $replacement) { | |
| 314 | + if (is_callable($replacement)) { | |
| 315 | + $text = preg_replace_callback($pattern, $replacement, $text); | |
| 316 | + } else { | |
| 317 | + $result = preg_replace($pattern, $replacement, $text); | |
| 318 | + if ($result === null) continue; | |
| 319 | + $text = $result; | |
| 320 | + } | |
| 321 | + } | |
| 322 | + | |
| 323 | + // プレースホルダーの数式に交換 | |
| 324 | + foreach ($this->algebraicPlaceholder as $placeholder => $expr) { | |
| 325 | + $text = str_replace($placeholder, $this->parseAlgebraic($expr), $text); | |
| 326 | + } | |
| 327 | + | |
| 328 | + return $text; | |
| 329 | + } | |
| 330 | + | |
| 331 | + /** | |
| 332 | + * 数式記法をパースする | |
| 333 | + * | |
| 334 | + * @param string $expression パースする数式 | |
| 335 | + * @return string MathJax用にフォーマットされた数式 | |
| 336 | + */ | |
| 337 | + private function parseAlgebraic(string $expression): string { | |
| 338 | + // HTMLエスケープ | |
| 339 | + $expression = htmlspecialchars_decode($expression, ENT_QUOTES); | |
| 340 | + | |
| 341 | + // ふぃりしゃもじのマッピング | |
| 342 | + $greekLetters = [ | |
| 343 | + 'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ', | |
| 344 | + 'epsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ', | |
| 345 | + 'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ', | |
| 346 | + 'nu' => 'ν', 'xi' => 'ξ', 'omicron' => 'ο', 'pi' => 'π', | |
| 347 | + 'rho' => 'ρ', 'sigma' => 'σ', 'tau' => 'τ', 'upsilon' => 'υ', | |
| 348 | + 'phi' => 'φ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω', | |
| 349 | + 'Alpha' => 'Α', 'Beta' => 'Β', 'Gamma' => 'Γ', 'Delta' => 'Δ', | |
| 350 | + 'Epsilon' => 'Ε', 'Zeta' => 'Ζ', 'Eta' => 'Η', 'Theta' => 'Θ', | |
| 351 | + 'Iota' => 'Ι', 'Kappa' => 'Κ', 'Lambda' => 'Λ', 'Mu' => 'Μ', | |
| 352 | + 'Nu' => 'Ν', 'Xi' => 'Ξ', 'Omicron' => 'Ο', 'Pi' => 'Π', | |
| 353 | + 'Rho' => 'Ρ', 'Sigma' => 'Σ', 'Tau' => 'Τ', 'Upsilon' => 'Υ', | |
| 354 | + 'Phi' => 'Φ', 'Chi' => 'Χ', 'Psi' => 'Ψ', 'Omega' => 'Ω', | |
| 355 | + ]; | |
| 356 | + | |
| 357 | + // ギリシャ文字を置換 | |
| 358 | + foreach ($greekLetters as $text => $symbol) { | |
| 359 | + $expression = preg_replace("/\b$text\b/", $symbol, $expression); | |
| 360 | + } | |
| 361 | + | |
| 362 | + // 上付き文字(例: x^2) | |
| 363 | + $expression = preg_replace_callback('/(\w+)\^(\{?(\d+|\w+)\}?)/', function($matches) { | |
| 364 | + $base = $matches[1]; | |
| 365 | + $exponent = $matches[2]; | |
| 366 | + if ($exponent[0] === '{') $exponent = substr($exponent, 1, -1); | |
| 367 | + | |
| 368 | + $superscripts = str_split($exponent); | |
| 369 | + $supMap = [ | |
| 370 | + '0' => '⁰', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴', | |
| 371 | + '5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹', | |
| 372 | + 'a' => 'ᵃ', 'b' => 'ᵇ', 'c' => 'ᶜ', 'd' => 'ᵈ', 'e' => 'ᵉ', | |
| 373 | + 'i' => 'ⁱ', 'n' => 'ⁿ', | |
| 374 | + ]; | |
| 375 | + $converted = ''; | |
| 376 | + | |
| 377 | + foreach ($superscripts as $char) $converted .= $supMap[$char] ?? $char; | |
| 378 | + | |
| 379 | + return $base.'<sup>'.$converted.'</sup>'; | |
| 380 | + }, $expression); | |
| 381 | + | |
| 382 | + // 下付き文字(例: x_1) | |
| 383 | + $expression = preg_replace_callback('/(\w+)_((\d+|\w+)\}?)/', function($matches) { | |
| 384 | + $base = $matches[1]; | |
| 385 | + $subscript = $matches[2]; | |
| 386 | + | |
| 387 | + if ($subscript[0] === '{') $subscript = substr($subscript, 1, -1); | |
| 388 | + | |
| 389 | + $subscripts = str_split($subscript); | |
| 390 | + $subMap = [ | |
| 391 | + '0' => '₀', '1' => '₁', '2' => '₂', '3' => '₃', '4' => '₄', | |
| 392 | + '5' => '₅', '6' => '₆', '7' => '₇', '8' => '₈', '9' => '₉', | |
| 393 | + ]; | |
| 394 | + $converted = ''; | |
| 395 | + | |
| 396 | + foreach ($subscripts as $char) { | |
| 397 | + $converted .= $subMap[$char] ?? $char; | |
| 398 | + } | |
| 399 | + | |
| 400 | + return $base.'<sub>'.$converted.'</sub>'; | |
| 401 | + }, $expression); | |
| 402 | + | |
| 403 | + // 分数(例: 4/5x, a/b) | |
| 404 | + $expression = preg_replace('/(\d+|\w+)\/(\d*\w+)/', | |
| 405 | + '<span class="fraction"><span class="numerator">$1</span><span class="denominator">$2</span></span>', | |
| 406 | + $expression); | |
| 407 | + | |
| 408 | + // 演算子 | |
| 409 | + $operators = [ | |
| 410 | + '\*' => '・', | |
| 411 | + '!=' => '≠', | |
| 412 | + '>=' => '≥', | |
| 413 | + '<=' => '≤', | |
| 414 | + 'sqrt' => '√', | |
| 415 | + ]; | |
| 416 | + | |
| 417 | + foreach ($operators as $op => $symbol) $expression = preg_replace("/\b$op\b/", $symbol, $expression); | |
| 418 | + | |
| 419 | + // 数式全体をspanで囲む | |
| 420 | + return '<span class="algebraic">'.$expression.'</span>'; | |
| 421 | + } | |
| 422 | + | |
| 423 | + /** | |
| 424 | + * リストを作成する | |
| 425 | + * | |
| 426 | + * @param array $items リストアイテムの配列 | |
| 427 | + * @param int $maxLevel 最大ネストレベル | |
| 428 | + * @return string HTMLのリスト | |
| 429 | + */ | |
| 430 | + private function createList(array $items, int $maxLevel = 1): string { | |
| 431 | + if ($items === []) return ''; | |
| 432 | + | |
| 433 | + $html = ''; | |
| 434 | + $currentLevel = 0; | |
| 435 | + $listStack = []; | |
| 436 | + $currentType = ''; | |
| 437 | + | |
| 438 | + foreach ($items as $item) { | |
| 439 | + $level = isset($item['level']) ? $item['level'] : $currentLevel; | |
| 440 | + | |
| 441 | + while ($currentLevel > $level) | |
| 442 | + $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; | |
| 443 | + | |
| 444 | + while ($currentLevel < $level) { | |
| 445 | + $currentLevel++; | |
| 446 | + $listStack[] = $item['type']; | |
| 447 | + $html .= str_repeat(' ', $currentLevel - 1)."<".$item['type'].">\n"; | |
| 448 | + } | |
| 449 | + | |
| 450 | + if ($currentType != $item['type'] && $currentLevel == $item['level']) { | |
| 451 | + if (!empty($listStack)) { | |
| 452 | + $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; | |
| 453 | + $listStack[] = $item['type']; | |
| 454 | + $html .= str_repeat(' ', $currentLevel - 1)."<".$item['type'].">\n"; | |
| 455 | + } | |
| 456 | + } | |
| 457 | + | |
| 458 | + $currentType = $item['type']; | |
| 459 | + } | |
| 460 | + | |
| 461 | + $html .= " <p>\n <{$currentType}>\n"; | |
| 462 | + foreach ($items as $item) { | |
| 463 | + $html .= str_repeat(' ', $currentLevel)." <li>".$item['content']."</li>\n"; | |
| 464 | + } | |
| 465 | + $html .= " </{$currentType}>\n </p>"; | |
| 466 | + | |
| 467 | + while (!empty($listStack)) { | |
| 468 | + $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; | |
| 469 | + $currentLevel--; | |
| 470 | + } | |
| 471 | + | |
| 472 | + return rtrim($html); | |
| 473 | + } | |
| 474 | + | |
| 475 | + /** | |
| 476 | + * コードブロックを作成する | |
| 477 | + * | |
| 478 | + * @return string HTMLのコードブロック | |
| 479 | + */ | |
| 480 | + private function createCodeBlock(): string { | |
| 481 | + $code = htmlspecialchars(implode("\n", $this->codeBlockContent)); | |
| 482 | + $class = $this->codeBlockLanguage ? " class=\"language-{$this->codeBlockLanguage}\"" : ''; | |
| 483 | + return " <p>\n <pre><code{$class}>{$code}</code></pre>"; | |
| 484 | + // $raw = implode("\n", $this->codeBlockContent); | |
| 485 | + // $lang = $this->codeBlockLanguage ?: 'txt'; | |
| 486 | + // $class = $lang === 'txt' ? '' : " class=\"language-{$lang}\""; | |
| 487 | + // $highlighted = $this->highlightCode($raw, $lang); | |
| 488 | + // return "<pre><code{$class}>{$highlighted}</code></pre>"; | |
| 489 | + } | |
| 490 | + | |
| 491 | + private function createBlockquote(array $content): string { | |
| 492 | + return " <p>\n <blockquote>\n ".implode("<br />\n ", $content)."\n </blockquote>\n </p>"; | |
| 493 | + } | |
| 494 | + | |
| 495 | + /** | |
| 496 | + * テーブルを作成する | |
| 497 | + * | |
| 498 | + * @param array $headers ヘッダー配列 | |
| 499 | + * @param array $rows 行データの配列 | |
| 500 | + * @return string HTMLのテーブル | |
| 501 | + */ | |
| 502 | + private function createTable(array $headers, array $rows): string { | |
| 503 | + $html = " <p>\n <table>\n"; | |
| 504 | + | |
| 505 | + // ヘッダーを追加 | |
| 506 | + if (!empty($headers)) { | |
| 507 | + $html .= " <thead>\n <tr>\n"; | |
| 508 | + foreach ($headers as $header) { | |
| 509 | + $html .= " <th>".$this->parseInline($header)."</th>\n"; | |
| 510 | + } | |
| 511 | + $html .= " </tr>\n </thead>\n"; | |
| 512 | + } | |
| 513 | + | |
| 514 | + // 行を追加 | |
| 515 | + if (!empty($rows)) { | |
| 516 | + $html .= " <tbody>\n"; | |
| 517 | + foreach ($rows as $row) { | |
| 518 | + $html .= " <tr>\n"; | |
| 519 | + foreach ($row as $cell) { | |
| 520 | + $html .= " <td>".$this->parseInline($cell)."</td>\n"; | |
| 521 | + } | |
| 522 | + $html .= " </tr>\n"; | |
| 523 | + } | |
| 524 | + $html .= " </tbody>\n"; | |
| 525 | + } | |
| 526 | + | |
| 527 | + $html .= " </table>\n </p>"; | |
| 528 | + return $html; | |
| 529 | + } | |
| 530 | + | |
| 531 | + /** | |
| 532 | + * Pure-PHP syntax highlighter | |
| 533 | + * | |
| 534 | + * @param string $code Raw source code | |
| 535 | + * @param string $lang Language identifier (html, css, js, php, …) | |
| 536 | + * @return string HTML with <span class="hl-…"> tokens | |
| 537 | + */ | |
| 538 | + private function highlightCode(string $code, string $lang): string | |
| 539 | + { | |
| 540 | + $lang = strtolower(trim($lang)); | |
| 541 | + $rules = []; | |
| 542 | + | |
| 543 | + // ---- COMMON ------------------------------------------------------- | |
| 544 | + $rules['comment'] = [ | |
| 545 | + // /* … */ (multi-line) | |
| 546 | + ['/\/\*(?:.|\n)*?\*\//', 'hl-comment'], | |
| 547 | + // // …\n | |
| 548 | + ['/\/\/.*(?=\n|$)/', 'hl-comment'], | |
| 549 | + ]; | |
| 550 | + | |
| 551 | + $rules['string'] = [ | |
| 552 | + // "…" and '…' (allow escaped quotes) | |
| 553 | + ['/"(?:\\.|[^"\\\n])*"/', 'hl-string'], | |
| 554 | + ["/'(?:\\.|[^'\\\n])*'/", 'hl-string'], | |
| 555 | + ]; | |
| 556 | + | |
| 557 | + $rules['number'] = [ | |
| 558 | + ['/\b(?:0x[0-9a-fA-F]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/', 'hl-number'], | |
| 559 | + ]; | |
| 560 | + | |
| 561 | + // ---- HTML --------------------------------------------------------- | |
| 562 | + if ($lang === 'html') { | |
| 563 | + $rules['tag'] = [['/<\/?[a-zA-Z0-9\-]+/', 'hl-tag']]; | |
| 564 | + $rules['attr'] = [['/\s[a-zA-Z0-9\-]+(?==)/', 'hl-attr']]; | |
| 565 | + $rules['value'] = [['/(?<==)"(?:\\.|[^"\\\n])*"(?=[>\s])/', 'hl-value']]; | |
| 566 | + } | |
| 567 | + | |
| 568 | + // ---- CSS ---------------------------------------------------------- | |
| 569 | + if ($lang === 'css') { | |
| 570 | + $rules['selector'] = [['/^[\t ]*[a-zA-Z0-9#\.\-\:$$ $$\=\*\>\+\~\^]+(?=\s*\{)/m', 'hl-selector']]; | |
| 571 | + $rules['property'] = [['/\b[a-z\-]+(?=\s*:)/', 'hl-property']]; | |
| 572 | + $rules['value'] = [['/(?<=\:)\s*[^;]+(?=;)/', 'hl-value']]; | |
| 573 | + $rules['unit'] = [['/\b\d+(px|em|rem|%|vh|vw|pt|pc|cm|mm|in)\b/i', 'hl-unit']]; | |
| 574 | + } | |
| 575 | + | |
| 576 | + // ---- PHP ---------------------------------------------------------- | |
| 577 | + if ($lang === 'php') { | |
| 578 | + $keywords = 'abstract|and|array|as|break|callable|case|catch|class|clone|' | |
| 579 | + . 'const|continue|declare|default|die|do|echo|else|elseif|empty|' | |
| 580 | + . 'enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|' | |
| 581 | + . 'extends|final|finally|for|foreach|function|global|goto|if|' | |
| 582 | + . 'implements|include|include_once|instanceof|insteadof|interface|' | |
| 583 | + . 'isset|list|namespace|new|or|print|private|protected|public|' | |
| 584 | + . 'require|require_once|return|static|switch|throw|trait|try|unset|' | |
| 585 | + . 'use|var|while|xor|yield'; | |
| 586 | + $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 587 | + $rules['function']= [['/\b([a-zA-Z_\x7f-\xff][\w\x7f-\xff]*)(?=\s*\()/', 'hl-function']]; | |
| 588 | + $rules['variable']= [['/\$[a-zA-Z_\x7f-\xff][\w\x7f-\xff]*/', 'hl-variable']]; | |
| 589 | + } | |
| 590 | + | |
| 591 | + // ---- Shell -------------------------------------------------------- | |
| 592 | + if ($lang === 'sh') { | |
| 593 | + $keywords = 'if|then|else|elif|fi|case|esac|for|select|while|until|do|done|in'; | |
| 594 | + $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 595 | + $rules['variable']= [['/\$\{?[a-zA-Z_][\w]*\}?/', 'hl-variable']]; | |
| 596 | + } | |
| 597 | + | |
| 598 | + // ---- JSON --------------------------------------------------------- | |
| 599 | + if ($lang === 'json') { | |
| 600 | + $rules['property'] = [['/"([^"]+)":/', '<span class="hl-property">$1</span>:']]; | |
| 601 | + } | |
| 602 | + | |
| 603 | + // ---- C / C++ ----------------------------------------------- | |
| 604 | + if (in_array($lang, ['c', 'cpp', 'cs'])) { | |
| 605 | + $keywords = 'abstract|assert|boolean|break|byte|case|catch|char|class|const|' | |
| 606 | + . 'continue|default|do|double|else|enum|extends|false|final|finally|' | |
| 607 | + . 'float|for|goto|if|implements|import|instanceof|int|interface|long|' | |
| 608 | + . 'native|new|null|package|private|protected|public|return|short|static|' | |
| 609 | + . 'strictfp|super|switch|synchronized|this|throw|throws|transient|true|' | |
| 610 | + . 'try|void|volatile|while'; | |
| 611 | + $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 612 | + $rules['type'] = [['/\b(?:int|float|double|char|bool|void|string)\b/', 'hl-type']]; | |
| 613 | + $rules['define'] = [['/(#include)|(#define)/', 'hl-define']]; | |
| 614 | + } | |
| 615 | + | |
| 616 | + if ($lang === 'markdown') { | |
| 617 | + $rules['header'] = [['/^(#{1,6})\s+(.+)$/', '<span class="hl-header">$1 $2</span>']]; | |
| 618 | + $rules['link'] = [['/\[([^\]]+)\]\(([^)]+) $$/', '<span class="hl-link">[$1]($2)</span>']]; | |
| 619 | + } | |
| 620 | + | |
| 621 | + $html = htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8'); | |
| 622 | + | |
| 623 | + $order = ['comment', 'string', 'number']; | |
| 624 | + foreach (['tag','attr','value','selector','property','unit','keyword','function','variable','type','header','link'] as $k) { | |
| 625 | + if (isset($rules[$k])) $order[] = $k; | |
| 626 | + } | |
| 627 | + | |
| 628 | + foreach ($order as $type) { | |
| 629 | + if (!isset($rules[$type])) continue; | |
| 630 | + foreach ($rules[$type] as $rule) { | |
| 631 | + [$pattern, $class] = $rule; | |
| 632 | + $html = preg_replace_callback( | |
| 633 | + $pattern, | |
| 634 | + function ($m) use ($class) { | |
| 635 | + // For JSON property we already built the <span> | |
| 636 | + if (strpos($m[0], '<span') === 0) return $m[0]; | |
| 637 | + $inner = $m[0]; | |
| 638 | + // Preserve already escaped entities inside strings/comments | |
| 639 | + return '<span class="'.$class.'">'.$inner.'</span>'; | |
| 640 | + }, | |
| 641 | + $html | |
| 642 | + ); | |
| 643 | + } | |
| 644 | + } | |
| 645 | + | |
| 646 | + return $html; | |
| 647 | + } | |
| 648 | +} |
| @@ -0,0 +1,163 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +class Route { | |
| 5 | + protected static array $routes = []; | |
| 6 | + protected static array $fallback = []; | |
| 7 | + | |
| 8 | + /** | |
| 9 | + * ルート設定を言語固有のハンドラで初期化する | |
| 10 | + * | |
| 11 | + * @param array $routes ルート設定の配列 | |
| 12 | + * @return void | |
| 13 | + */ | |
| 14 | + public static function init(array $routes): void { | |
| 15 | + self::$routes = $routes; | |
| 16 | + } | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * ルートを追加する | |
| 20 | + * | |
| 21 | + * @param string $method HTTPメソッド | |
| 22 | + * @param string $path URLパス | |
| 23 | + * @param string|callable $class ハンドラクラスとメソッド、またはコールバック | |
| 24 | + * @param array $params オプションのパラメータ | |
| 25 | + * @return array ルート設定 | |
| 26 | + */ | |
| 27 | + public static function add(string $method, string $path, string|callable $class, | |
| 28 | + array $params = []): array { | |
| 29 | + $route = [ | |
| 30 | + 'method' => $method, | |
| 31 | + 'path' => $path, | |
| 32 | + 'class' => $class, | |
| 33 | + 'params' => $params, | |
| 34 | + ]; | |
| 35 | + | |
| 36 | + self::$routes[] = $route; | |
| 37 | + return $route; | |
| 38 | + } | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 404処理用のフォールバックルートを設定する | |
| 42 | + * | |
| 43 | + * @param array|string|callable $class | |
| 44 | + * @return void | |
| 45 | + */ | |
| 46 | + public static function setFallback(array|string|callable $class): void { | |
| 47 | + self::$fallback = [ | |
| 48 | + 'class' => $class, | |
| 49 | + 'params' => [], | |
| 50 | + ]; | |
| 51 | + } | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 適切なルートをマッチさせて実行する | |
| 55 | + * | |
| 56 | + * @param string $uri リクエストURI | |
| 57 | + * @return void | |
| 58 | + */ | |
| 59 | + public static function dispatch(string $uri): void { | |
| 60 | + // URIをパスとクエリ文字列に分割 | |
| 61 | + $uriParts = explode('?', $uri, 2); | |
| 62 | + $path = trim($uriParts[0], " \t\n\r\0\x0B/"); | |
| 63 | + | |
| 64 | + // ルートパスの処理(/?page=2のようなクエリパラメータを含む場合も処理) | |
| 65 | + if ($path === '') { | |
| 66 | + self::executeClass([ | |
| 67 | + 'class' => [new \Site\Controller\Home(), 'show'], | |
| 68 | + 'params' => ['lang' => 'ja'], | |
| 69 | + ]); | |
| 70 | + return; | |
| 71 | + } | |
| 72 | + | |
| 73 | + if ($path === 'en') { | |
| 74 | + self::executeClass([ | |
| 75 | + 'class' => [new \Site\Controller\Home(), 'show'], | |
| 76 | + 'params' => ['lang' => 'en'], | |
| 77 | + ]); | |
| 78 | + return; | |
| 79 | + } | |
| 80 | + | |
| 81 | + // パスに対してルートをマッチングする | |
| 82 | + foreach (self::$routes as $route) { | |
| 83 | + $matches = []; | |
| 84 | + | |
| 85 | + if (self::matchRoute($route['path'], $path, $matches)) { | |
| 86 | + $params = self::extractParams($route['path'], $path); | |
| 87 | + $params = array_merge($route['params'], $params); | |
| 88 | + | |
| 89 | + if (is_string($route['class'])) { | |
| 90 | + [ $class, $method ] = explode('@', $route['class']); | |
| 91 | + $controller = new $class(); | |
| 92 | + self::executeClass([ | |
| 93 | + 'class' => [ $controller, $method ], | |
| 94 | + 'params' => $params, | |
| 95 | + ]); | |
| 96 | + | |
| 97 | + return; | |
| 98 | + } elseif (is_callable($route['class'])) { | |
| 99 | + self::executeClass([ | |
| 100 | + 'class' => $route['class'], | |
| 101 | + 'params' => $params, | |
| 102 | + ]); | |
| 103 | + | |
| 104 | + return; | |
| 105 | + } | |
| 106 | + } | |
| 107 | + } | |
| 108 | + | |
| 109 | + // マッチするルートがない場合、フォールバックを実行 | |
| 110 | + self::executeClass(self::$fallback); | |
| 111 | + } | |
| 112 | + | |
| 113 | + /** | |
| 114 | + * ルートパターンとパスをマッチングする | |
| 115 | + * | |
| 116 | + * @param string $pattern ルートパターン | |
| 117 | + * @param string $path 現在のパス | |
| 118 | + * @param array $matches マッチを格納する参照 | |
| 119 | + * @return bool | |
| 120 | + */ | |
| 121 | + protected static function matchRoute(string $pattern, string $path, | |
| 122 | + array &$matches = []): bool { | |
| 123 | + // ルートパターンを正規表現パターンに変換 | |
| 124 | + $pattern = preg_replace('/\{([^:}]+)(?::([^}]+))?\}/', '(?P<$1>[^/]+)', $pattern); | |
| 125 | + $pattern = str_replace('/', '\/', $pattern); | |
| 126 | + return (bool)preg_match('/^'.$pattern.'$/', $path, $matches); | |
| 127 | + } | |
| 128 | + | |
| 129 | + /** | |
| 130 | + * パターンに基づいてパスから名前付きパラメータを抽出する | |
| 131 | + * | |
| 132 | + * @param string $pattern ルートパターン | |
| 133 | + * @param string $path 現在のパス | |
| 134 | + * @return array | |
| 135 | + */ | |
| 136 | + protected static function extractParams(string $pattern, string $path): array { | |
| 137 | + $params = []; | |
| 138 | + $patternParts = explode('/', $pattern); | |
| 139 | + $pathParts = explode('/', $path); | |
| 140 | + | |
| 141 | + foreach ($patternParts as $k => $v) { | |
| 142 | + if (preg_match('/\{([^:}]+)(?::([^}]+))?\}/', $v, $matches)) { | |
| 143 | + if (isset($pathParts[$k])) { | |
| 144 | + $params[$matches[1]] = $pathParts[$k]; | |
| 145 | + } | |
| 146 | + } | |
| 147 | + } | |
| 148 | + | |
| 149 | + return $params; | |
| 150 | + } | |
| 151 | + | |
| 152 | + /** | |
| 153 | + * ルートクラスを実行する | |
| 154 | + * | |
| 155 | + * @param array $route ルート設定 | |
| 156 | + * @return void | |
| 157 | + */ | |
| 158 | + protected static function executeClass(array $route): void { | |
| 159 | + if (is_callable($route['class'])) { | |
| 160 | + call_user_func($route['class'], $route['params'] ?? []); | |
| 161 | + } | |
| 162 | + } | |
| 163 | +} |
| @@ -0,0 +1,280 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | +use Std\Lib\Syntax\AsmAarch32; | |
| 6 | +use Std\Lib\Syntax\AsmAarch64; | |
| 7 | +use Std\Lib\Syntax\AsmX8664; | |
| 8 | +use Std\Lib\Syntax\C; | |
| 9 | +use Std\Lib\Syntax\Cpp; | |
| 10 | +use Std\Lib\Syntax\Glsl; | |
| 11 | +use Std\Lib\Syntax\Go; | |
| 12 | + | |
| 13 | +class Syntax { | |
| 14 | + private array $alt = [ | |
| 15 | + 'asm-aarch32' => ['asm-arm'], | |
| 16 | + 'asm-x8664' => ['asm', 'asm-x86', 'asm-x64'], | |
| 17 | + 'cpp' => ['cc', 'c++'], | |
| 18 | + 'css' => ['css3'], | |
| 19 | + 'go' => ['golang'], | |
| 20 | + ]; | |
| 21 | + | |
| 22 | + private array $include = []; | |
| 23 | + private string $lang; | |
| 24 | + private SyntaxInterface $handler; | |
| 25 | + | |
| 26 | + private array $supported = [ | |
| 27 | + 'asm-aarch32' => AsmAarch32::class, | |
| 28 | + 'asm-aarch64' => AsmAarch64::class, | |
| 29 | + 'asm-x8664' => AsmX8664::class, | |
| 30 | + 'c' => C::class, | |
| 31 | + 'cpp' => Cpp::class, | |
| 32 | + 'glsl' => Glsl::class, | |
| 33 | + 'go' => Go::class, | |
| 34 | + ]; | |
| 35 | + | |
| 36 | + public function __construct(string $lang) { | |
| 37 | + $this->lang = $this->resolveLanguage($lang); | |
| 38 | + $this->handler = $this->createHandler($this->lang); | |
| 39 | + } | |
| 40 | + | |
| 41 | + public function colorize(string $code): string { | |
| 42 | + $lines = explode("\n", $code); | |
| 43 | + foreach ($lines as $c) { | |
| 44 | + $c = str_replace('<', '<', $c); | |
| 45 | + $c = str_replace('>', '>', $c); | |
| 46 | + } | |
| 47 | + | |
| 48 | + switch ($this->lang) { | |
| 49 | + case 'asm-aarch': | |
| 50 | + break; | |
| 51 | + case 'asm-x8664': | |
| 52 | + $lines = $this->addString($lines); | |
| 53 | + $lines = $this->addNumber($lines); | |
| 54 | + $lines = $this->addComment($lines); | |
| 55 | + $lines = $this->add($lines, 'register'); | |
| 56 | + $lines = $this->add($lines, 'operator'); | |
| 57 | + $lines = $this->addLabel($lines); | |
| 58 | + $lines = $this->addSection($lines); | |
| 59 | + $lines = $this->addDirective($lines); | |
| 60 | + break; | |
| 61 | + case 'c': | |
| 62 | + $this->include = $this->handler->getInclude($lines); | |
| 63 | + $lines = $this->addString($lines); | |
| 64 | + $lines = $this->addNumber($lines); | |
| 65 | + $lines = $this->addComment($lines); | |
| 66 | + $lines = $this->add($lines, 'preprocessor'); | |
| 67 | + $lines = $this->add($lines, 'keyword'); | |
| 68 | + foreach ($this->include as $inc) { | |
| 69 | + $lines = $this->add($lines, 'typedef', $inc); | |
| 70 | + $lines = $this->add($lines, 'libc', $inc); | |
| 71 | + } | |
| 72 | + $lines = $this->addFunction($lines); | |
| 73 | + // kys($lines); | |
| 74 | + break; | |
| 75 | + case 'cpp': | |
| 76 | + break; | |
| 77 | + case 'glsl': | |
| 78 | + break; | |
| 79 | + case 'go': | |
| 80 | + break; | |
| 81 | + default: | |
| 82 | + break; | |
| 83 | + } | |
| 84 | + | |
| 85 | + return implode("\n", $lines); | |
| 86 | + } | |
| 87 | + | |
| 88 | + /////////////// | |
| 89 | + | |
| 90 | + private function resolveLanguage(string $lang): string { | |
| 91 | + if (isset($this->supported[$lang])) return $lang; | |
| 92 | + foreach ($this->alt as $k => $v) if (in_array($lang, $v, true)) return $k; | |
| 93 | + throw new \InvalidArgumentException("未対応の言語: {$lang}"); | |
| 94 | + } | |
| 95 | + | |
| 96 | + private function createHandler(string $lang): SyntaxInterface { | |
| 97 | + $class = $this->supported[$lang] ?? null; | |
| 98 | + if (!$class) throw new \RuntimeException("言語のハンドルが存在しない: {$lang}"); | |
| 99 | + return new $class(); | |
| 100 | + } | |
| 101 | + | |
| 102 | + private function addComment(array $code): array { | |
| 103 | + global $colorPalette; | |
| 104 | + | |
| 105 | + $sc = $this->handler->getComment() ?? null; | |
| 106 | + $mc = $this->handler->getMultiline() ?? [null, null]; | |
| 107 | + $mcs = $mc[0]; | |
| 108 | + $mce = $mc[1]; | |
| 109 | + | |
| 110 | + $color = $this->handler->getColors()['comment'] ?? $colorPalette['dark']['green']; | |
| 111 | + | |
| 112 | + $isMulti = false; | |
| 113 | + $out = []; | |
| 114 | + | |
| 115 | + foreach ($code as $c) { | |
| 116 | + $cur = $c; | |
| 117 | + | |
| 118 | + if ($isMulti || strpos($cur, $sc) !== false || ($mcs && strpos($cur, $mcs) !== false)) { | |
| 119 | + $cur = preg_replace('/<span[^>]*>(.*?)<\/span>/s', '$1', $cur); | |
| 120 | + } | |
| 121 | + | |
| 122 | + if ($isMulti) { | |
| 123 | + $endPos = $mcs ? strpos($cur, $mce) : false; | |
| 124 | + | |
| 125 | + if ($endPos !== false) { | |
| 126 | + $before = substr($cur, 0, $endPos + strlen($mce)); | |
| 127 | + $after = substr($cur, $endPos + strlen($mce)); | |
| 128 | + | |
| 129 | + $cur = '<span style="color: '.$color.'; font-style: italics;">'.$before.'</span>'.$after; | |
| 130 | + $isMulti = false; | |
| 131 | + } else { | |
| 132 | + $cur = '<span style="color: '.$color.'; font-style: italics;">'.$cur.'</span>'; | |
| 133 | + } | |
| 134 | + } else { | |
| 135 | + if ($sc && ($pos = strpos($cur, $sc, 1)) !== false) { | |
| 136 | + $before = substr($cur, 0, $pos); | |
| 137 | + $rest = trim(substr($cur, $pos)); | |
| 138 | + $cur = $before.'<span style="color: '.$color.'; font-style: italics;">'.$rest.'</span>'; | |
| 139 | + } | |
| 140 | + | |
| 141 | + if (!$isMulti && $mcs && $mce && str_contains($cur, $mcs)) { | |
| 142 | + $startPos = strpos($cur, $mcs); | |
| 143 | + $endPos = $mce ? strpos($cur, $mce) : false; | |
| 144 | + | |
| 145 | + if ($endPos !== false && $startPos < $endPos) { | |
| 146 | + $before = substr($cur, 0, $startPos); | |
| 147 | + $part = substr($cur, $startPos, $endPos - $startPos + strlen($mce)); | |
| 148 | + $after = substr($cur, $endPos + strlen($mce)); | |
| 149 | + | |
| 150 | + $cur = $before.'<span style="color: '.$color.'; font-style: italics;">'.$part.'</span>'.$after; | |
| 151 | + } else { | |
| 152 | + $before = substr($cur, 0, $startPos); | |
| 153 | + $rest = substr($cur, $startPos); | |
| 154 | + | |
| 155 | + $cur = $before.'<span style="color: '.$color.'; font-style: italics;">'.$rest; | |
| 156 | + $isMulti = true; | |
| 157 | + } | |
| 158 | + } | |
| 159 | + } | |
| 160 | + | |
| 161 | + $out[] = $cur; | |
| 162 | + } | |
| 163 | + | |
| 164 | + return $out; | |
| 165 | + } | |
| 166 | + | |
| 167 | + private function addNumber(array $code): array { | |
| 168 | + $color = $this->handler->getColors()['number']; | |
| 169 | + $pattern = '/(?<![\w$@.])(?: # 関係ない | |
| 170 | + (?:0[xX][0-9a-fA-F]+) # 0xFF, 0XFF | |
| 171 | + |(?:0[bB][01]+) # 0b1010 | |
| 172 | + |(?:[0-9]+[hH]) # FFh, 0FFh | |
| 173 | + |(?:\$[0-9a-fA-F]+) # $FF | |
| 174 | + |(?:%[01]+) # %1010 | |
| 175 | + |(?:\d+(?:\.\d+)?(?:[eE][+-]?\d+)?) # 42, 3.14, 1e-5 | |
| 176 | + |(?:\d+) # 普通数字 | |
| 177 | + )(?![\w$@.])(?![a-zA-Z])/ix'; | |
| 178 | + | |
| 179 | + return array_map(fn($c) => preg_replace_callback($pattern, fn($m) => '<span style="color:'.$color.'">'.$m[0].'</span>', $c), $code); | |
| 180 | + } | |
| 181 | + | |
| 182 | + private function addString(array $code): array { | |
| 183 | + $color = $this->handler->getColors()['string']; | |
| 184 | + $pattern = '/(?:"(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\')/s'; | |
| 185 | + | |
| 186 | + return array_map( | |
| 187 | + function ($c) use ($pattern, $color) { | |
| 188 | + return preg_replace_callback($pattern, fn($m) => '<span style="color:'.$color.'">'.$m[0].'</span>', $c); | |
| 189 | + }, | |
| 190 | + $code | |
| 191 | + ); | |
| 192 | + } | |
| 193 | + | |
| 194 | + private function addLabel(array $code): array { | |
| 195 | + $color = $this->handler->getColors()['label']; | |
| 196 | + $out = []; | |
| 197 | + foreach ($code as $c) $out[] = preg_replace('/^\s*([\.a-zA-Z_][a-zA-Z0-9_]*)\s*:/m', '<span style="color: '.$color.';">$1</span>:', $c); | |
| 198 | + return $out; | |
| 199 | + } | |
| 200 | + | |
| 201 | + private function addSection(array $code): array { | |
| 202 | + $color1 = $this->handler->getColors()['label']; | |
| 203 | + $color2 = $this->handler->getColors()['register']; | |
| 204 | + $color3 = $this->handler->getColors()['number']; | |
| 205 | + $out = []; | |
| 206 | + | |
| 207 | + foreach ($code as $c) { | |
| 208 | + $cur = $c; | |
| 209 | + | |
| 210 | + $cur = preg_replace( | |
| 211 | + '/^(?:(section|default|segment|group)\s+)([\w.][\w.\-]*)(?:\s*(?:;.*)?)?$/im', | |
| 212 | + '<span style="color: '.$color1.';">$1</span> <span style="color: '.$color2.';">$2</span>', | |
| 213 | + $cur); | |
| 214 | + $cur = preg_replace( | |
| 215 | + '/^(bits)\s+(16|32|64)(?:\s*(?:;.*)?)?$/im', | |
| 216 | + '<span style="color: '.$color1.';">$1</span> <span style="color: '.$color3.';">$2</span>', | |
| 217 | + $cur); | |
| 218 | + | |
| 219 | + $out[] = $cur; | |
| 220 | + } | |
| 221 | + | |
| 222 | + return $out; | |
| 223 | + } | |
| 224 | + | |
| 225 | + private function add(array $code, string $type, ?string $header = null): array { | |
| 226 | + $tok = null !== $header ? $this->handler->getTokens()[$type][$header] : $this->handler->getTokens()[$type]; | |
| 227 | + $color = $this->handler->getColors()[$type]; | |
| 228 | + $pattern = '/\b('.implode('|', array_map(fn($r) => preg_quote($r, '/'), $tok)).')\b/i'; | |
| 229 | + $so = '<span style="color: '.$color.';">'; | |
| 230 | + $sc = '</span>'; | |
| 231 | + $out = []; | |
| 232 | + foreach ($code as $c) { | |
| 233 | + $out[] = preg_replace_callback($pattern, function ($m) use ($so, $sc) { return $so.$m[0].$sc; }, $c); | |
| 234 | + } | |
| 235 | + return $out; | |
| 236 | + } | |
| 237 | + | |
| 238 | + private function addFunction(array $code): array { | |
| 239 | + $color = $this->handler->getColors()['function']; | |
| 240 | + $out = []; | |
| 241 | + | |
| 242 | + foreach ($code as $c) { | |
| 243 | + $cur = $c; | |
| 244 | + $cur = preg_replace_callback("/\b(.*)\(\b/i", fn($m) => '<span style="color: '.$color.';">'.$m[0].'</span>', $cur); | |
| 245 | + $out[] = $cur; | |
| 246 | + } | |
| 247 | + | |
| 248 | + return $out; | |
| 249 | + } | |
| 250 | + | |
| 251 | + private function addDirective(array $code): array { | |
| 252 | + $tok = $this->handler->getTokens()['directive']; | |
| 253 | + $color1 = $this->handler->getColors()['directive']; | |
| 254 | + $color2 = $this->handler->getColors()['register']; | |
| 255 | + $pattern = implode('|', array_map(fn($r) => preg_quote($r, '/'), $tok)); | |
| 256 | + $out = []; | |
| 257 | + | |
| 258 | + foreach ($code as $c) { | |
| 259 | + $cur = $c; | |
| 260 | + $cur = preg_replace_callback("/\b({$pattern})\b/i", fn($m) => '<span style="color: '.$color1.';">'.$m[0].'</span>', $cur); | |
| 261 | + $cur = preg_replace_callback( | |
| 262 | + '/^\s*([a-zA-Z_][a-zA-Z0-9_$.@]*)\s+('.$pattern.')\b\s+(.+?)(?:\s*(?:;.*)?)?$/im', | |
| 263 | + function ($m) use ($color1, $color2) { | |
| 264 | + $l = $m[1]; | |
| 265 | + $d = $m[2]; | |
| 266 | + $v = $m[3]; | |
| 267 | + | |
| 268 | + // if (strcasecmp($d, 'equ') === 0) $v = preg_replace('/\$-[a-zA-Z_][a-zA-Z0-9_]*/i', '<span style="color: '.$color1.';">$o</span>', $v); | |
| 269 | + return sprintf( | |
| 270 | + '<span style="color: %s;">%s</span> '.'<span style="color: %s;">%s</span> %s', | |
| 271 | + $color1, $l, $color2, $v, $v | |
| 272 | + ); | |
| 273 | + }, $cur); | |
| 274 | + | |
| 275 | + $out[] = $cur; | |
| 276 | + } | |
| 277 | + | |
| 278 | + return $out; | |
| 279 | + } | |
| 280 | +} |
| @@ -0,0 +1,40 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class Asm6502 implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 16ビット | |
| 14 | + // 8ビット | |
| 15 | + ], | |
| 16 | + 'operator' => [ | |
| 17 | + ], | |
| 18 | + 'directive' => [ | |
| 19 | + ], | |
| 20 | + ]; | |
| 21 | + | |
| 22 | + public function __construct() { | |
| 23 | + global $colorPalette; | |
| 24 | + | |
| 25 | + $this->colors = [ | |
| 26 | + 'comment' => $colorPalette['dark']['green'], | |
| 27 | + 'directive' => $colorPalette['dark']['pink'], | |
| 28 | + 'label' => $colorPalette['light']['yellow'], | |
| 29 | + 'number' => $colorPalette['light']['green'], | |
| 30 | + 'operator' => $colorPalette['dark']['blue'], | |
| 31 | + 'register' => $colorPalette['dark']['cyan'], | |
| 32 | + 'string' => $colorPalette['medium']['orange'], | |
| 33 | + ]; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public function getColors(): array { return $this->colors; } | |
| 37 | + public function getComment(): ?string { return $this->comment; } | |
| 38 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | + public function getTokens(): array { return $this->tokens; } | |
| 40 | +} |
| @@ -0,0 +1,39 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class Asm68000 implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 16ビット | |
| 14 | + ], | |
| 15 | + 'operator' => [ | |
| 16 | + ], | |
| 17 | + 'directive' => [ | |
| 18 | + ], | |
| 19 | + ]; | |
| 20 | + | |
| 21 | + public function __construct() { | |
| 22 | + global $colorPalette; | |
| 23 | + | |
| 24 | + $this->colors = [ | |
| 25 | + 'comment' => $colorPalette['dark']['green'], | |
| 26 | + 'directive' => $colorPalette['dark']['pink'], | |
| 27 | + 'label' => $colorPalette['light']['yellow'], | |
| 28 | + 'number' => $colorPalette['light']['green'], | |
| 29 | + 'operator' => $colorPalette['dark']['blue'], | |
| 30 | + 'register' => $colorPalette['dark']['cyan'], | |
| 31 | + 'string' => $colorPalette['medium']['orange'], | |
| 32 | + ]; | |
| 33 | + } | |
| 34 | + | |
| 35 | + public function getColors(): array { return $this->colors; } | |
| 36 | + public function getComment(): ?string { return $this->comment; } | |
| 37 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 38 | + public function getTokens(): array { return $this->tokens; } | |
| 39 | +} |
| @@ -0,0 +1,56 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmAarch32 implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // フロート、SIMD、ベクター | |
| 14 | + 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', // 64 | |
| 15 | + 'd10', 'd11', 'd12', 'd13', 'd14', 'd15', 'd16', 'd17', 'd18', 'd19', // 64 | |
| 16 | + 'd20', 'd21', 'd22', 'd23', 'd24', 'd25', 'd26', 'd27', 'd28', 'd29', 'd30', 'd31', // 64 | |
| 17 | + 's0', 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', // 32 | |
| 18 | + 's10', 's11', 's12', 's13', 's14', 's15', 's16', 's17', 's18', 's19', // 32 | |
| 19 | + 's20', 's21', 's22', 's23', 's24', 's25', 's26', 's27', 's28', 's29', 's30', 's31', // 32 | |
| 20 | + 'q0', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9', 'q10', 'q11', 'q12', 'q13', 'q14', 'q15', | |
| 21 | + // GPR | |
| 22 | + 'r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', | |
| 23 | + 'r13' /*スタックポインター*/, 'r14' /*リンクレジスター*/, 'r15' /*プログラムカウンター*/, | |
| 24 | + ], | |
| 25 | + 'operator' => [ | |
| 26 | + 'add', 'sub', 'mul', 'umul', 'mla', 'madd', 'msub', | |
| 27 | + 'and', 'orr', 'eor', 'bic', 'mvn', 'eon', 'ldr', 'bl', | |
| 28 | + 'ldm', 'stm', 'ldrd', 'strd', 'ldrex', 'strex', | |
| 29 | + 'vadd', 'vmul', 'vmla', 'blx', 'cmp', | |
| 30 | + ], | |
| 31 | + 'directive' => [ | |
| 32 | + '.arch armv8-a', 'arch_extension', '.cfi_', '.global', '.globl', '.section', | |
| 33 | + '.equ', '.set', '.byte', '.hword', '.word', '.quad', | |
| 34 | + '.arm', '.code 32', '.thumb', '.code 16', | |
| 35 | + ], | |
| 36 | + ]; | |
| 37 | + | |
| 38 | + public function __construct() { | |
| 39 | + global $colorPalette; | |
| 40 | + | |
| 41 | + $this->colors = [ | |
| 42 | + 'comment' => $colorPalette['dark']['green'], | |
| 43 | + 'directive' => $colorPalette['dark']['pink'], | |
| 44 | + 'label' => $colorPalette['light']['yellow'], | |
| 45 | + 'number' => $colorPalette['light']['green'], | |
| 46 | + 'operator' => $colorPalette['dark']['blue'], | |
| 47 | + 'register' => $colorPalette['dark']['cyan'], | |
| 48 | + 'string' => $colorPalette['medium']['orange'], | |
| 49 | + ]; | |
| 50 | + } | |
| 51 | + | |
| 52 | + public function getColors(): array { return $this->colors; } | |
| 53 | + public function getComment(): ?string { return $this->comment; } | |
| 54 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 55 | + public function getTokens(): array { return $this->tokens; } | |
| 56 | +} |
| @@ -0,0 +1,65 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmAarch64 implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // フロート、SIMD、ベクター | |
| 14 | + 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', // 64 | |
| 15 | + 'd10', 'd11', 'd12', 'd13', 'd14', 'd15', 'd16', 'd17', 'd18', 'd19', // 64 | |
| 16 | + 'd20', 'd21', 'd22', 'd23', 'd24', 'd25', 'd26', 'd27', 'd28', 'd29', 'd30', 'd31', // 64 | |
| 17 | + 's0', 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', // 32 | |
| 18 | + 's10', 's11', 's12', 's13', 's14', 's15', 's16', 's17', 's18', 's19', // 32 | |
| 19 | + 's20', 's21', 's22', 's23', 's24', 's25', 's26', 's27', 's28', 's29', 's30', 's31', // 32 | |
| 20 | + 'v0', 'v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7', 'v8', 'v9', | |
| 21 | + 'v10', 'v11', 'v12', 'v13', 'v14', 'v15', 'v16', 'v17', 'v18', 'v19', | |
| 22 | + 'v20', 'v21', 'v22', 'v23', 'v24', 'v25', 'v26', 'v27', 'v28', 'v29', 'v30', 'v31', | |
| 23 | + 'q16', 'q17', 'q18', 'q19', 'q20', 'q21', 'q22', 'q23', 'q24', 'q25', 'q26', 'q27', 'q28', 'q29', 'q30', 'q31', | |
| 24 | + // GPR | |
| 25 | + 'x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', | |
| 26 | + 'x10', 'x11', 'x12', 'x13', 'x14', 'x15', 'x16', 'x17', 'x18', 'x19', | |
| 27 | + 'x20', 'x21', 'x22', 'x23', 'x24', 'x25', 'x26', 'x27', 'x28', 'x29', 'x30', 'x31' /*ゼロレジスター*/, | |
| 28 | + 'w0', 'w1', 'w2', 'w3', 'w4', 'w5', 'w6', 'w7', 'w8', 'w9', | |
| 29 | + 'w10', 'w11', 'w12', 'w13', 'w14', 'w15', 'w16', 'w17', 'w18', 'w19', | |
| 30 | + 'w20', 'w21', 'w22', 'w23', 'w24', 'w25', 'w26', 'w27', 'w28', 'w29', 'w30', | |
| 31 | + 'xzr' /*ゼロレジスター*/, 'wzr' /*ゼロレジスター*/, | |
| 32 | + 'sp' /*スタックポインター*/, 'pc' /*プログラムカウンター*/, | |
| 33 | + ], | |
| 34 | + 'operator' => [ | |
| 35 | + 'add', 'sub', 'mul', 'umul', 'mla', 'madd', 'msub', | |
| 36 | + 'and', 'orr', 'eor', 'bic', 'mvn', 'eon', 'ldr', 'bl', | |
| 37 | + 'csel', 'cinc', 'cset', 'addv', 'smull', 'fmla', 'blr', 'cbz', 'cbnz', 'tbz', 'tbnz', | |
| 38 | + 'adpr', 'movz', 'movk', 'ldp', 'stp', 'ldxr', 'stxr', 'ldadd', 'ldclr', 'ldset', | |
| 39 | + ], | |
| 40 | + 'directive' => [ | |
| 41 | + '.arch armv8-a', 'arch_extension', '.cfi_', '.global', '.globl', '.section', | |
| 42 | + '.equ', '.set', '.byte', '.hword', '.word', '.quad', | |
| 43 | + '.req', '.aarch64', | |
| 44 | + ], | |
| 45 | + ]; | |
| 46 | + | |
| 47 | + public function __construct() { | |
| 48 | + global $colorPalette; | |
| 49 | + | |
| 50 | + $this->colors = [ | |
| 51 | + 'comment' => $colorPalette['dark']['green'], | |
| 52 | + 'directive' => $colorPalette['dark']['pink'], | |
| 53 | + 'label' => $colorPalette['light']['yellow'], | |
| 54 | + 'number' => $colorPalette['light']['green'], | |
| 55 | + 'operator' => $colorPalette['dark']['blue'], | |
| 56 | + 'register' => $colorPalette['dark']['cyan'], | |
| 57 | + 'string' => $colorPalette['medium']['orange'], | |
| 58 | + ]; | |
| 59 | + } | |
| 60 | + | |
| 61 | + public function getColors(): array { return $this->colors; } | |
| 62 | + public function getComment(): ?string { return $this->comment; } | |
| 63 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 64 | + public function getTokens(): array { return $this->tokens; } | |
| 65 | +} |
| @@ -0,0 +1,40 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmMips implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 64ビット | |
| 14 | + // 32ビット | |
| 15 | + ], | |
| 16 | + 'operator' => [ | |
| 17 | + ], | |
| 18 | + 'directive' => [ | |
| 19 | + ], | |
| 20 | + ]; | |
| 21 | + | |
| 22 | + public function __construct() { | |
| 23 | + global $colorPalette; | |
| 24 | + | |
| 25 | + $this->colors = [ | |
| 26 | + 'comment' => $colorPalette['dark']['green'], | |
| 27 | + 'directive' => $colorPalette['dark']['pink'], | |
| 28 | + 'label' => $colorPalette['light']['yellow'], | |
| 29 | + 'number' => $colorPalette['light']['green'], | |
| 30 | + 'operator' => $colorPalette['dark']['blue'], | |
| 31 | + 'register' => $colorPalette['dark']['cyan'], | |
| 32 | + 'string' => $colorPalette['medium']['orange'], | |
| 33 | + ]; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public function getColors(): array { return $this->colors; } | |
| 37 | + public function getComment(): ?string { return $this->comment; } | |
| 38 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | + public function getTokens(): array { return $this->tokens; } | |
| 40 | +} |
| @@ -0,0 +1,40 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmPower implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 64ビット | |
| 14 | + // 32ビット | |
| 15 | + ], | |
| 16 | + 'operator' => [ | |
| 17 | + ], | |
| 18 | + 'directive' => [ | |
| 19 | + ], | |
| 20 | + ]; | |
| 21 | + | |
| 22 | + public function __construct() { | |
| 23 | + global $colorPalette; | |
| 24 | + | |
| 25 | + $this->colors = [ | |
| 26 | + 'comment' => $colorPalette['dark']['green'], | |
| 27 | + 'directive' => $colorPalette['dark']['pink'], | |
| 28 | + 'label' => $colorPalette['light']['yellow'], | |
| 29 | + 'number' => $colorPalette['light']['green'], | |
| 30 | + 'operator' => $colorPalette['dark']['blue'], | |
| 31 | + 'register' => $colorPalette['dark']['cyan'], | |
| 32 | + 'string' => $colorPalette['medium']['orange'], | |
| 33 | + ]; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public function getColors(): array { return $this->colors; } | |
| 37 | + public function getComment(): ?string { return $this->comment; } | |
| 38 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | + public function getTokens(): array { return $this->tokens; } | |
| 40 | +} |
| @@ -0,0 +1,41 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmRiscV implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 128ビット | |
| 14 | + // 64ビット | |
| 15 | + // 32ビット | |
| 16 | + ], | |
| 17 | + 'operator' => [ | |
| 18 | + ], | |
| 19 | + 'directive' => [ | |
| 20 | + ], | |
| 21 | + ]; | |
| 22 | + | |
| 23 | + public function __construct() { | |
| 24 | + global $colorPalette; | |
| 25 | + | |
| 26 | + $this->colors = [ | |
| 27 | + 'comment' => $colorPalette['dark']['green'], | |
| 28 | + 'directive' => $colorPalette['dark']['pink'], | |
| 29 | + 'label' => $colorPalette['light']['yellow'], | |
| 30 | + 'number' => $colorPalette['light']['green'], | |
| 31 | + 'operator' => $colorPalette['dark']['blue'], | |
| 32 | + 'register' => $colorPalette['dark']['cyan'], | |
| 33 | + 'string' => $colorPalette['medium']['orange'], | |
| 34 | + ]; | |
| 35 | + } | |
| 36 | + | |
| 37 | + public function getColors(): array { return $this->colors; } | |
| 38 | + public function getComment(): ?string { return $this->comment; } | |
| 39 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 40 | + public function getTokens(): array { return $this->tokens; } | |
| 41 | +} |
| @@ -0,0 +1,56 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmX8664 implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 64ビット | |
| 14 | + 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'r8', 'r8d', 'r9', 'r9d', 'r10', 'r10d', 'r11', 'r11d', 'r12', 'r12d', 'r13', 'r13d', 'r14', 'r14d', 'r15', 'r15d', | |
| 15 | + // 32ビット | |
| 16 | + 'eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', | |
| 17 | + // 16ビット | |
| 18 | + 'al', 'bl', 'cl', 'dl', | |
| 19 | + 'ah', 'bh', 'ch', 'dh', | |
| 20 | + 'spl', 'bpl', 'sil', 'dil', | |
| 21 | + ], | |
| 22 | + 'operator' => [ | |
| 23 | + 'mov', 'lea', | |
| 24 | + 'add', 'sub', 'div', 'imul', 'dec', 'inc', | |
| 25 | + 'xor', 'or', 'and', | |
| 26 | + 'js', 'jmp', 'jz', 'jnz', 'ja', 'jb', | |
| 27 | + 'pop', 'push', | |
| 28 | + 'call', 'ret', 'syscall', 'cmp', 'test', 'stosb', | |
| 29 | + ], | |
| 30 | + 'directive' => [ | |
| 31 | + 'db', 'dw', 'dd', 'dw', | |
| 32 | + 'equ', 'global', 'extern', | |
| 33 | + 'resb', 'resw', 'resd', 'resq', | |
| 34 | + ], | |
| 35 | + ]; | |
| 36 | + | |
| 37 | + public function __construct() { | |
| 38 | + global $colorPalette; | |
| 39 | + | |
| 40 | + $this->colors = [ | |
| 41 | + 'comment' => $colorPalette['dark']['green'], | |
| 42 | + 'directive' => $colorPalette['dark']['pink'], | |
| 43 | + 'label' => $colorPalette['light']['yellow'], | |
| 44 | + 'number' => $colorPalette['light']['green'], | |
| 45 | + 'operator' => $colorPalette['dark']['blue'], | |
| 46 | + 'register' => $colorPalette['dark']['cyan'], | |
| 47 | + 'string' => $colorPalette['medium']['orange'], | |
| 48 | + ]; | |
| 49 | + } | |
| 50 | + | |
| 51 | + public function getColors(): array { return $this->colors; } | |
| 52 | + public function getComment(): ?string { return $this->comment; } | |
| 53 | + public function getInclude(array $code): ?array { return null; } | |
| 54 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 55 | + public function getTokens(): array { return $this->tokens; } | |
| 56 | +} |
| @@ -0,0 +1,66 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class AsmZ80 implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = ';'; | |
| 9 | + private ?array $multiline = null; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 16ビット | |
| 14 | + "bc'", "de'", "hl'", | |
| 15 | + 'bc', 'de', 'hl', | |
| 16 | + 'ix' /*指標レジスター*/, 'iy' /*指標レジスター*/, | |
| 17 | + 'sp' /*スタックポインター*/, 'pc' /*プログラムカウンター*/, | |
| 18 | + // 8ビット | |
| 19 | + 'a', 'b', 'd', 'h', 'f', 'c', 'e', 'l', | |
| 20 | + "a'", "b'", "d'", "h'", "f'", "c'", "e'", "l'", | |
| 21 | + 'a', 'b', 'd', 'h', 'f', 'c', 'e', 'l', | |
| 22 | + ], | |
| 23 | + 'operator' => [ | |
| 24 | + // Z80 + GBZ80 + eZ80 | |
| 25 | + 'adc', 'add', 'and', 'bit', 'call', 'ccf', 'cp', 'daa', 'dec', 'di', 'djnz', 'ei', | |
| 26 | + 'halt', 'im0', 'im1', 'im2', 'im', 'inc', 'in', 'jp', 'jr', 'ldd', | |
| 27 | + 'nop', 'otir', 'otdr', 'or', 'pop', 'push', 'retn', 'reti', 'ret', 'res', | |
| 28 | + 'rst', 'rrc', 'rr', 'rdc', 'rl', 'sub', 'srl', 'sra', 'sla', 'set', 'scf', 'sbc', 'xor', | |
| 29 | + // Z80 + GBZ80のみ | |
| 30 | + 'sll', 'ld', | |
| 31 | + // eZ80のみ | |
| 32 | + 'ind2', 'indmr', 'indrx', 'indm', 'inimr', 'inim', 'ini2r', 'ini2', 'lea', 'mlt', | |
| 33 | + 'otirx', 'otimr', 'otim', 'oti2r', 'otdrx', 'otdmr', 'otdm', 'otd2r', 'out0', 'outd2', | |
| 34 | + 'outi2', 'pea', 'rsmix', 'slp', 'stmix', 'tstio', 'tst', '.is', 'xxx.S', 'xxx.L', '.il', '.sis', '.lil', | |
| 35 | + // Z80のみ | |
| 36 | + 'cpdr', 'cpir', 'cpd', 'cpi', 'exx', 'ex', 'outd', 'inir', 'ini', 'indr', 'ind', 'lddr', 'ldir', 'ldi', | |
| 37 | + 'neg', 'outi', 'outdi', 'out', 'rld', 'rrd', | |
| 38 | + // GBZ80のみ | |
| 39 | + 'outd', 'stop', 'swap', | |
| 40 | + ], | |
| 41 | + 'directive' => [ | |
| 42 | + // 'db', 'dw', 'dd', 'dw', | |
| 43 | + // 'equ', 'global', 'extern', | |
| 44 | + // 'resb', 'resw', 'resd', 'resq', | |
| 45 | + ], | |
| 46 | + ]; | |
| 47 | + | |
| 48 | + public function __construct() { | |
| 49 | + global $colorPalette; | |
| 50 | + | |
| 51 | + $this->colors = [ | |
| 52 | + 'comment' => $colorPalette['dark']['green'], | |
| 53 | + 'directive' => $colorPalette['dark']['pink'], | |
| 54 | + 'label' => $colorPalette['light']['yellow'], | |
| 55 | + 'number' => $colorPalette['light']['green'], | |
| 56 | + 'operator' => $colorPalette['dark']['blue'], | |
| 57 | + 'register' => $colorPalette['dark']['cyan'], | |
| 58 | + 'string' => $colorPalette['medium']['orange'], | |
| 59 | + ]; | |
| 60 | + } | |
| 61 | + | |
| 62 | + public function getColors(): array { return $this->colors; } | |
| 63 | + public function getComment(): ?string { return $this->comment; } | |
| 64 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 65 | + public function getTokens(): array { return $this->tokens; } | |
| 66 | +} |
| @@ -0,0 +1,277 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class C implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = '//'; | |
| 9 | + private array $multiline = ['/*', '*/']; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'keyword' => [ | |
| 13 | + // C23 | |
| 14 | + 'alignas', 'alignof', 'bool', 'constexpr', 'false', 'nullptr', 'static_assert', 'thread_local', 'true', 'typeof', 'typeof_unqual', | |
| 15 | + '_BitInt', '_Decimal128', '_Decimal32', '_Decimal64', | |
| 16 | + // C11 | |
| 17 | + '_Alignas', '_Alignof', '_Atomic', '_Generic', '_Noreturn', '_Static_assert', '_Thread_local', | |
| 18 | + // C99 | |
| 19 | + 'inline', 'restrict', '_Bool', '_Complex', '_Imaginary', | |
| 20 | + // C89 | |
| 21 | + 'asm', 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extern', 'float', 'fortran', 'for', 'goto', 'if', 'inline', | |
| 22 | + 'int', 'long', 'noreturn', 'register', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', | |
| 23 | + // ISO | |
| 24 | + 'FILE', | |
| 25 | + ], | |
| 26 | + 'libc' => [ | |
| 27 | + // ISO C | |
| 28 | + 'assert.h' => [ // C89 | |
| 29 | + 'assert', | |
| 30 | + ], | |
| 31 | + 'complex.h' => [ // C99 | |
| 32 | + 'cabs', 'cabsf', 'cabsl', 'cacos', 'cacosf', 'cacosl', 'cacosh', 'cacoshf', 'cacoshl', 'carg', 'cargf', 'cargl', 'casin', 'casinf', 'casinl', 'casinh', 'casinhf', 'casinhl', 'catan', 'catanf', 'catanl', 'catanh', 'catanhf', 'catanhl', 'ccos', 'ccosf', 'ccosl', 'ccosh', 'ccoshf', 'ccoshl', 'cexp', 'cexpf', 'cexpl', | |
| 33 | + 'cimag', 'cimagf', 'cimagl', 'clog', 'clogf', 'clogl', 'conj', 'conjf', 'conjl', 'cpow', 'cpowf', 'cpowl', 'cproj', 'cprojf', 'cprojl', 'creal', 'crealf', 'creall', | |
| 34 | + 'csqrt', 'csqrtf', 'csqrtl', 'csin', 'csinf', 'csinl', 'csinh', 'csinhf', 'csinhl', 'ctan', 'ctanf', 'ctanl', 'ctanh', 'ctanhf', 'ctanhl', | |
| 35 | + ], | |
| 36 | + 'ctype.h' => [ // C89 | |
| 37 | + 'isalnum', 'isalpha', 'isblank', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper', 'isxdigit', 'tolower', 'toupper', | |
| 38 | + ], | |
| 39 | + 'errno.h' => [ // C89 | |
| 40 | + 'errno', | |
| 41 | + ], | |
| 42 | + 'fenv.h' => [ // C99 | |
| 43 | + 'feclearexcept', 'fegetround', 'fegetenv', 'fegetexpectflag', 'feholdexcept', 'feraiseexcept', 'fesetround', 'fesetenv', 'fesetexpectflag', 'fetestexcept', | |
| 44 | + 'feupdateenv', | |
| 45 | + // __STDC_IEC_60559_DFP__ | |
| 46 | + 'fe_dec_getround', 'fe_dec_setround', | |
| 47 | + ], | |
| 48 | + 'inttypes.h' => [ // C99 | |
| 49 | + 'imaxabs', 'imaxdiv', 'strtoimax', 'strtoumax', 'wcstoimax', 'wcstoumax', | |
| 50 | + ], | |
| 51 | + 'locale.h' => [ // C89 | |
| 52 | + 'localeconv', 'setlocale', | |
| 53 | + ], | |
| 54 | + 'math.h' => [ // C89 | |
| 55 | + 'acos', 'acosf', 'acosl', 'acosh', 'acoshf', 'acoshl', 'asin', 'asinf', 'asinl', 'asinh', 'asinhf', 'asinhl', 'atan', 'atanf', 'atanl', 'atanh', 'atanhf', 'atanhl', | |
| 56 | + 'atan2', 'atan2f', 'atan2l', 'cbrt', 'cbrtf', 'cbrtl', 'ceil', 'ceilf', 'ceill', 'copysign', 'copysignf', 'copysignl', 'cos', 'cosf', 'cosl', 'erf', 'erff', 'erfl', | |
| 57 | + 'erfc', 'erfcf', 'erfcl', 'exp', 'expf', 'expl', 'exp2', 'exp2f', 'exp2l', 'expml', 'expmlf', 'expmll', 'fabs', 'fabsf', 'fabsl', 'fdim', 'fdimf', 'fdiml', | |
| 58 | + 'floor', 'floorf', 'floorl', 'fma', 'fmaf', 'fmal', 'fmax', 'fmaxf', 'fmaxl', 'fmin', 'fminf', 'fminl', 'fpclassify', 'frexp', 'frexpf', 'frexpl', | |
| 59 | + 'hypot', 'hypotf', 'hypotl', 'ilogb', 'ilogbf', 'ilogbl', 'isfinite', 'isgreater', 'isgreaterequal', 'isinf', 'isless', 'islessequal', 'islessgreater', 'isnan', | |
| 60 | + 'isnormal', 'isunordered', 'ldexp', 'ldexpf', 'ldexpl', 'lgamma', 'lgammaf', 'lgammal', 'log', 'logf', 'logl', 'log10', 'log10f', 'log10l', 'log2', 'log2f', 'log2l', | |
| 61 | + 'logb', 'logbf', 'logbl', 'loglp', 'loglpf', 'loglpl', 'lrint', 'lrintf', 'lrintl', 'llrint', 'llrintf', 'llrintl', 'lround', 'lroundf', 'lroundl', 'llround', 'llroundf', 'llroundl', 'modf', 'modff', 'modfl', 'nan', 'nanf', 'nanl', 'nearbyint', 'nearbyintf', 'nearbyintl', 'nextafter', 'nextafterf', 'nextafterl', | |
| 62 | + 'nexttoward', 'nexttowardf', 'nexttowardl', 'pow', 'powf', 'powl', 'remainder', 'remainderf', 'remainderl', 'remquo', 'remquof', 'remquol', 'rint', 'rintf', 'rintl', | |
| 63 | + 'round', 'roundf', 'roundl', 'scalbn', 'scalbnf', 'scalbnl', 'scalbln', 'scalblnf', 'scalblnl', 'signbit', 'sin', 'sinf', 'sinl', 'sinh', 'sinhf', 'sinhl', | |
| 64 | + 'sqrt', 'sqrtf', 'sqrtl', 'tan', 'tanf', 'tanl', 'tanh', 'tanhf', 'tanhl', 'tgamma', 'tgammaf', 'tgammal', 'trunc', 'truncf', 'truncl', | |
| 65 | + ], | |
| 66 | + 'setjmp.h' => [ // C89 | |
| 67 | + 'longjmp', 'setjmp', | |
| 68 | + ], | |
| 69 | + 'signal.h' => [ // C89 | |
| 70 | + 'raise', 'signal', | |
| 71 | + ], | |
| 72 | + 'stdarg.h' => [ // C89 | |
| 73 | + 'va_arg', 'va_copy', 'va_end', 'va_start', | |
| 74 | + ], | |
| 75 | + 'stdatomic.h' => [ // C11 | |
| 76 | + 'atomic_fetch_add', 'atomic_fetch_add_explicit', 'atomic_fetch_and', 'atomic_fetch_and_explicit', 'atomic_fetch_or', 'atomic_fetch_or_explicit', | |
| 77 | + 'atomic_fetch_sub', 'atomic_fetch_sub_explicit', 'atomic_fetch_xor', 'atomic_fetch_xor_explicit', 'atomic_flag', 'atomic_flag_clear', 'atomic_flag_clear_explicit', | |
| 78 | + 'atomic_flag_test_and_set', 'atomic_flag_test_and_set_explicit', 'atomic_init', 'atomic_is_lock_free', 'atomic_load', 'atomic_load_explicit', 'atomic_signal_fence', | |
| 79 | + 'atomic_store', 'atomic_store_explicit', 'atomic_thread_fence', 'kill_dependency', 'memory_order', | |
| 80 | + ], | |
| 81 | + 'stdbit.h' => [ // C23 | |
| 82 | + 'stdc_bit_ceil_uc', 'stdc_bit_ceil_us', 'stdc_bit_ceil_ui', 'stdc_bit_ceil_ul', 'stdc_bit_ceil_ull', 'stdc_bit_ceil', | |
| 83 | + 'stdc_bit_floor_uc', 'stdc_bit_floor_us', 'stdc_bit_floor_ui', 'stdc_bit_floor_ul', 'stdc_bit_floor_ull', 'stdc_bit_floor', | |
| 84 | + 'stdc_bit_width_uc', 'stdc_bit_width_us', 'stdc_bit_width_ui', 'stdc_bit_width_ul', 'stdc_bit_width_ull', 'stdc_bit_width', | |
| 85 | + 'stdc_count_ones_uc', 'stdc_count_ones_us', 'stdc_count_ones_ui', 'stdc_count_ones_ul', 'stdc_count_ones_ull', 'stdc_count_ones', | |
| 86 | + 'stdc_count_zeros_uc', 'stdc_count_zeros_us', 'stdc_count_zeros_ui', 'stdc_count_zeros_ul', 'stdc_count_zeros_ull', 'stdc_count_zeros', | |
| 87 | + 'stdc_first_leading_ones_uc', 'stdc_first_leading_ones_us', 'stdc_first_leading_ones_ui', 'stdc_first_leading_ones_ul', 'stdc_first_leading_ones_ull', | |
| 88 | + 'stdc_first_leading_ones', | |
| 89 | + 'stdc_first_leading_zeros_uc', 'stdc_first_leading_zeros_us', 'stdc_first_leading_zeros_ui', 'stdc_first_leading_zeros_ul', 'stdc_first_leading_zeros_ull', | |
| 90 | + 'stdc_first_leading_zeros', | |
| 91 | + 'stdc_first_trailing_ones_uc', 'stdc_first_trailing_ones_us', 'stdc_first_trailing_ones_ui', 'stdc_first_trailing_ones_ul', 'stdc_first_trailing_ones_ull', | |
| 92 | + 'stdc_first_trailing_ones', | |
| 93 | + 'stdc_first_trailing_zeros_uc', 'stdc_first_trailing_zeros_us', 'stdc_first_trailing_zeros_ui', 'stdc_first_trailing_zeros_ul', 'stdc_first_trailing_zeros_ull', | |
| 94 | + 'stdc_first_trailing_zeros', | |
| 95 | + 'stdc_has_single_bit_uc', 'stdc_has_single_bit_us', 'stdc_has_single_bit_ui', 'stdc_has_single_bit_ul', 'stdc_has_single_bit_ull', 'stdc_has_single_bit', | |
| 96 | + 'stdc_leading_ones_uc', 'stdc_leading_ones_us', 'stdc_leading_ones_ui', 'stdc_leading_ones_ul', 'stdc_leading_ones_ull', 'stdc_leading_ones', | |
| 97 | + 'stdc_leading_zeros_uc', 'stdc_leading_zeros_us', 'stdc_leading_zeros_ui', 'stdc_leading_zeros_ul', 'stdc_leading_zeros_ull', 'stdc_leading_zeros', | |
| 98 | + 'stdc_trailing_ones_uc', 'stdc_trailing_ones_us', 'stdc_trailing_ones_ui', 'stdc_trailing_ones_ul', 'stdc_trailing_ones_ull', 'stdc_trailing_ones', | |
| 99 | + 'stdc_trailing_zeros_uc', 'stdc_trailing_zeros_us', 'stdc_trailing_zeros_ui', 'stdc_trailing_zeros_ul', 'stdc_trailing_zeros_ull', 'stdc_trailing_zeros', | |
| 100 | + 'stdc_rotate_left_uc', 'stdc_rotate_left_us', 'stdc_rotate_left_ui', 'stdc_rotate_left_ul', 'stdc_rotate_left_ull', 'stdc_rotate_left', | |
| 101 | + 'stdc_rotate_right_uc', 'stdc_rotate_right_us', 'stdc_rotate_right_ui', 'stdc_rotate_right_ul', 'stdc_rotate_right_ull', 'stdc_rotate_right', | |
| 102 | + ], | |
| 103 | + 'stdckdint.h' => [ // C23 | |
| 104 | + 'ckd_add', 'ckd_sub', 'ckd_mul', | |
| 105 | + ], | |
| 106 | + 'stdio.h' => [ // C89 | |
| 107 | + 'clearerr', 'fclose', 'feof', 'ferror', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen', 'fscanf', 'fseek', | |
| 108 | + 'fseekpos', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'perror', 'printf', 'putc', 'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', | |
| 109 | + 'setvbuf', 'snprintf', 'sprintf', 'sscanf', 'tmpfile', 'tmpnam', 'ungetc', 'vfprintf', 'vfscanf', 'vprintf', 'vscanf', 'vsnprintf', 'vsprintf', 'vsscanf', | |
| 110 | + // __STDC_WANT_LIB_EXT1__ | |
| 111 | + 'fopen_s', 'fprintf_s', 'freopen_s', 'fscanf_s', 'gets_s', 'printf_s', 'scanf_s', 'snprintf_s', 'sprintf_s', 'sscanf_s', 'tmpfile_s', 'tmpnam_s', | |
| 112 | + 'vfprintf_s', 'vfscanf_s', 'vprintf_s', 'vscanf_s', 'vsnprintf_s', 'vsprintf_s', 'vsscanf_s', | |
| 113 | + ], | |
| 114 | + 'stdlib.h' => [ // C89 | |
| 115 | + 'abort', 'abs', 'aligned_alloc', 'at_quick_exit', 'atexit', 'atof', 'atoi', 'atol', 'atoll', 'bsearch', 'calloc', 'div', 'exit', '_Exit', 'free', 'free_sized', | |
| 116 | + 'free_aligned_sized', 'labs', 'llabs', 'ldiv', 'lldiv', 'malloc', 'mblen', 'mbtowc', 'mbstowcs', 'memalignment', 'qsort', 'quick_exit', 'rand', 'realloc', 'srand', | |
| 117 | + 'strfromd', 'strfromf', 'strfroml', 'strtod', 'strtof', 'strtold', 'strtol', 'strtoll', 'strtoul', 'strtoull', 'system', 'wctomb', 'wcstombs', | |
| 118 | + // __STDC_IEC_60559_DFP__ | |
| 119 | + 'strfromd32', 'strfromd64', 'strfromd128', 'strtod32', 'strtod64', 'strtod128', | |
| 120 | + // __STDC_WANT_IEC_60559_TYPES_EXT__ | |
| 121 | + 'strfromdN', 'strfromdNx', 'strfromencfN', 'strfromencdecdN', 'strfromencbindN', 'strfromfN', 'strfromfNx', 'strtodN', 'strtodNx', 'strtofN', 'strtofNx', | |
| 122 | + 'strtoencfN', 'strtoencdecdN', 'strtoencbindN', | |
| 123 | + // __STDC_WANT_LIB_EXT1__ | |
| 124 | + 'abort_handler_s', 'bsearch_s', 'getenv_s', 'ignore_handler_s', 'mbstowcs_s', 'qsort_s', 'set_constraint_handler_s', 'wcstomb_s', 'wcstombs_s', | |
| 125 | + ], | |
| 126 | + 'stdmchar.h' => [ // C29 | |
| 127 | + 'stdc_mcnrtoc16n', 'stdc_mcnrtoc32n', 'stdc_mcnrtoc8n', 'stdc_mcnrtomcn', 'stdc_mcnrtomwcn', 'stdc_mcsnrtoc16sn', 'stdc_mcsnrtoc32sn', 'stdc_mcsnrtoc8sn', | |
| 128 | + 'stdc_mwcnrtomcn', 'stdc_mcsnrtomcsn', 'stdc_mwcnrtomwcn', 'stdc_mcsnrtomwcsn', 'stdc_mwcnrtoc16n', 'stdc_mwcnrtoc32n', 'stdc_mwcnrtoc8n', 'stdc_mwcsnrtoc16sn', | |
| 129 | + 'stdc_mwcsnrtoc32sn', 'stdc_mwcsnrtoc8sn', 'stdc_mwcsnrtomcsn', 'stdc_mwcsnrtomwcsn', 'stdc_c16nrtoc16n', 'stdc_c16nrtoc32n', 'stdc_c16nrtoc8n', 'stdc_c16nrtomcn', | |
| 130 | + 'stdc_c16nrtomwcn', 'stdc_c16snrtoc8sn', 'stdc_c16snrtoc16sn', 'stdc_c16snrtoc32sn', 'stdc_c16snrtomwcsn', 'stdc_c16snrtomcsn', 'stdc_c32nrtoc16n', 'stdc_c32nrtoc32n', | |
| 131 | + 'stdc_c32nrtoc8n', 'stdc_c32nrtomcn', 'stdc_c32nrtomwcn', 'stdc_c32snrtoc16sn', 'stdc_c32snrtoc32sn', 'stdc_c32snrtoc8sn', 'stdc_c32snrtomcsn', 'stdc_c32snrtomwcsn', | |
| 132 | + 'stdc_c8nrtoc16n', 'stdc_c8nrtoc32n', 'stdc_c8nrtoc8n', 'stdc_c8nrtomcn', 'stdc_c8nrtomwcn', 'stdc_c8snrtoc16sn', 'stdc_c8snrtoc32sn', 'stdc_c8snrtoc8sn', 'stdc_c8snrtomwcsn', 'stdc_c8snrtomcsn', | |
| 133 | + ], | |
| 134 | + 'string.h' => [ // C89 | |
| 135 | + 'memccpy', 'memchr', 'memcmp', 'memcpy', 'memcpy_s', 'memmove', 'memmove_s', 'memset', 'memset_explicit', 'memset_s', | |
| 136 | + 'strcat', 'strcat_s', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcpy_s', 'strcspn', 'strdup', 'strlen', 'strncat', 'strncat_s', 'strncmp', 'strncpy', 'strncpy_s', | |
| 137 | + 'strndup', 'strnlen_s', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok', 'strtok_s', 'strxfrm', | |
| 138 | + // __STD_WANT_LIB_EXT1__ | |
| 139 | + 'memcpy_s', 'memmove_s', 'memset_s', 'strcat_s', 'strcpy_s', 'strerror_s', 'strerrorlen_s', 'strncat_s', 'strncpy_s', 'strnlen_s', 'strtok_s', | |
| 140 | + ], | |
| 141 | + 'threads.h' => [ // C11 | |
| 142 | + 'call_once', 'cnd_broadcast', 'cnd_destroy', 'cnd_init', 'cnd_signal', 'cnd_timedwait', 'cnd_wait', | |
| 143 | + 'mtx_destroy', 'mtx_init', 'mtx_lock', 'mtx_timedlock', 'mtx_trylock', 'mtx_unlock', | |
| 144 | + 'thrd_create', 'thrd_current', 'thrd_detach', 'thrd_equal', 'thrd_exit', 'thrd_join', 'thrd_sleep', 'thrd_yield', 'tss_create', 'tss_delete', 'tss_get', 'tss_set', | |
| 145 | + ], | |
| 146 | + 'time.h' => [ // C89 | |
| 147 | + 'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'gmtime_r', 'localtime', 'localtime_r', 'mktime', | |
| 148 | + 'strftime', 'time', 'timegm', | |
| 149 | + // __STD_WANT_LIB_EXT1__ | |
| 150 | + 'asctime_s', 'ctime_s', 'gmtime_s', 'localtime_s', | |
| 151 | + ], | |
| 152 | + 'uchar.h' => [ // C11 | |
| 153 | + 'c8rtomb', 'c16rtomb', 'c32rtomb', 'mbrtoc8', 'mbrtoc16', 'mbrtoc32', | |
| 154 | + ], | |
| 155 | + 'wchar.h' => [ // C95 | |
| 156 | + 'wcscat', 'wcscat_s', 'wcschr', 'wcscmp', 'wcscpy', 'wcscpy_s', 'wcscroll', 'wcscroll', 'wcscspn', 'wcslen', 'wcsncpy', 'wcsncpy_s', 'wcsncat', 'wcsncat_s', | |
| 157 | + 'wcsncmp', 'wcsnlen_s', 'wcsrchr', 'wcspbrk', 'wcsstr', 'wcstok', 'wcstok_s', 'wcsxfrm', | |
| 158 | + 'wmemchr', 'wmemcmp', 'wmemcpy', 'wmemcpy_s', 'wmemmove', 'wmemmove_s', 'wmemset', | |
| 159 | + // __STDC_IEC_60559_DFP__ | |
| 160 | + 'wcstod32', 'wcstod64', 'wcstod128', | |
| 161 | + // __STDC_IEC_60559_TYPES__ | |
| 162 | + 'encptr', 'nptr', | |
| 163 | + // __STDC_WANT_LIB_EXT1__ | |
| 164 | + 'fwprintf_s', 'fwscanf_s', 'mvsrtowcs_s', 'snwprinf_s', 'swprintf_s', 'swscanf_s', 'vfwprintf_s', 'vfwscanf_s', 'vsnwprinf_s', 'vswprintf_s', 'vswscanf_s', | |
| 165 | + 'wcrtomb_s', 'wcscat_s', 'wcsncat_s', 'wcstok_s', 'wcsnlen_s', 'wcscpy_s', 'wcsncpy_s', 'wcsrtombs_s', 'wmemcpy_s', 'wmemmove_s', 'wprintf_s', 'wscanf_s', | |
| 166 | + ], | |
| 167 | + 'wctype.h' => [ // C95 | |
| 168 | + 'iswalnum', 'iswalpha', 'iswblank', 'iswcntrl', 'iswctype', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace', 'iswupper', 'iswxdigit', | |
| 169 | + 'towctrans', 'towlower', 'towupper', | |
| 170 | + 'wctrans', 'cwtype', | |
| 171 | + ], | |
| 172 | + ], | |
| 173 | + 'preprocessor' => [ | |
| 174 | + // C23 | |
| 175 | + '#elifdef', '#elifndef', '#embed', '#warning', '__has_include', '__has_embed', '__has_c_attribute', | |
| 176 | + // C99 | |
| 177 | + '#_Pragma', | |
| 178 | + // C89 | |
| 179 | + '#if', '#elif', '#else', '#endif', '#ifdef', '#ifndef', '#define', '#undef', '#include', '#line', '#error', '#pragma', '#defined', | |
| 180 | + ], | |
| 181 | + 'typedef' => [ | |
| 182 | + 'errno.h' => [ | |
| 183 | + 'errno_t', | |
| 184 | + ], | |
| 185 | + 'inttypes.h' => [ | |
| 186 | + 'intmax_t', 'uintmax_t', 'imaxdiv_t', | |
| 187 | + ], | |
| 188 | + 'iso646.h' => [ | |
| 189 | + 'and'/*&&*/, 'and_eq'/*&=*/, 'bitand'/*&*/, 'bitor'/*|*/, 'compl'/*~*/, 'not'/*!*/, 'not_eq'/*!=*/, 'or'/*||*/, 'or_eq',/*|=*/ 'xor'/*^*/, 'xor_eq'/*^=*/, | |
| 190 | + ], | |
| 191 | + 'setjmp.h' => [ | |
| 192 | + 'jmp_buf', | |
| 193 | + ], | |
| 194 | + 'signal.h' => [ | |
| 195 | + 'sig_atomic_t', | |
| 196 | + ], | |
| 197 | + 'stdatomic.h' => [ | |
| 198 | + 'atomic_bool', 'atomic_char', 'atomic_schar', 'atomic_uchar', 'atomic_short', 'atomic_ushort', 'atomic_int', 'atomic_uint', 'atomic_long', 'atomic_ulong', | |
| 199 | + 'atomic_llong', 'atomic_ullong', 'atomic_char8_t', 'atomic_char16_t', 'atomic_char32_t', 'atomic_wchar_t', 'atomic_int_least8_t', 'atomic_uint_least8_t', | |
| 200 | + 'atomic_int_least16_t', 'atomic_uint_least16_t', 'atomic_int_least32_t', 'atomic_uint_least32_t', 'atomic_int_least64_t', 'atomic_uint_least64_t', 'atomic_int_fast8_t', | |
| 201 | + 'atomic_uint_fast8_t', 'atomic_int_fast16_t', 'atomic_uint_fast16_t', 'atomic_int_fast32_t', 'atomic_uint_fast32_t', 'atomic_int_fast64_t', 'atomic_uint_fast64_t', | |
| 202 | + 'atomic_intptr_t', 'atomic_uintptr_t', 'atomic_size_t', 'atomic_ptrdiff_t', 'atomic_intmax_t', 'atomic_uintmax_t', | |
| 203 | + ], | |
| 204 | + 'stddef.h' => [ | |
| 205 | + 'max_align_t', 'nullptr', 'ptrdiff_t', 'size_t', | |
| 206 | + ], | |
| 207 | + 'stdio.h' => [ | |
| 208 | + 'FILE', 'size_t', 'fpos_t', | |
| 209 | + ], | |
| 210 | + 'stdint.h' => [ | |
| 211 | + 'int8_t', 'int16_t', 'int32_t', 'int64_t', | |
| 212 | + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', | |
| 213 | + 'fpos_t', 'intptr_t', 'intmax_t', 'uintptr_t', 'uintmax_t', | |
| 214 | + 'int_least8_t', 'int_least16_t', 'int_least32_t', 'int_least64_t', | |
| 215 | + 'uint_least8_t', 'uint_least16_t', 'uint_least32_t', 'uint_least64_t', | |
| 216 | + 'int_fast8_t', 'int_fast16_t', 'int_fast32_t', 'int_fast64_t', | |
| 217 | + 'uint_fast8_t', 'uint_fast16_t', 'uint_fast32_t', 'uint_fast64_t', | |
| 218 | + ], | |
| 219 | + 'stdmchar.h' => [ | |
| 220 | + 'stdc_mcerr', | |
| 221 | + ], | |
| 222 | + 'stdarg.h' => [ | |
| 223 | + 'va_list', | |
| 224 | + ], | |
| 225 | + 'threads.h' => [ | |
| 226 | + 'cnd_t', | |
| 227 | + 'mtx_t', | |
| 228 | + 'thrd_t', | |
| 229 | + 'thrd_start_t', | |
| 230 | + 'tss_t', | |
| 231 | + 'tss_dtor_t', | |
| 232 | + ], | |
| 233 | + 'time.h' => [ | |
| 234 | + 'rsize_t', 'clock_t', 'time_t', | |
| 235 | + ], | |
| 236 | + 'uchar.h' => [ | |
| 237 | + 'char8_t', 'char16_t', 'char32_t', 'mbstate_t', | |
| 238 | + ], | |
| 239 | + 'wchar.h' => [ | |
| 240 | + 'wchar_t', 'wint_t', | |
| 241 | + ], | |
| 242 | + 'wctype.h' => [ | |
| 243 | + 'wctrans_t', 'wctype_t', | |
| 244 | + ], | |
| 245 | + ], | |
| 246 | + ]; | |
| 247 | + | |
| 248 | + public function __construct() { | |
| 249 | + global $colorPalette; | |
| 250 | + | |
| 251 | + $this->colors = [ | |
| 252 | + 'comment' => $colorPalette['dark']['green'], | |
| 253 | + 'keyword' => $colorPalette['dark']['blue'], | |
| 254 | + 'number' => $colorPalette['light']['green'], | |
| 255 | + 'preprocessor' => $colorPalette['medium']['purple'], | |
| 256 | + 'string' => $colorPalette['medium']['orange'], | |
| 257 | + 'typedef' => $colorPalette['dark']['lime'], | |
| 258 | + 'function' => $colorPalette['light']['yellow'], | |
| 259 | + 'libc' => $colorPalette['light']['yellow'], | |
| 260 | + ]; | |
| 261 | + } | |
| 262 | + | |
| 263 | + public function getColors(): array { return $this->colors; } | |
| 264 | + public function getComment(): ?string { return $this->comment; } | |
| 265 | + public function getInclude(array $code): ?array { | |
| 266 | + $headers = []; | |
| 267 | + foreach ($code as $c) { | |
| 268 | + $c = htmlspecialchars($c); | |
| 269 | + if (!str_starts_with($c, '#include')) continue; | |
| 270 | + $header = str_replace(['#include ', '<', '>', '"', '''], '', $c); | |
| 271 | + $headers[] = trim($header); | |
| 272 | + } | |
| 273 | + return $headers; | |
| 274 | + } | |
| 275 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 276 | + public function getTokens(): array { return $this->tokens; } | |
| 277 | +} |
| @@ -0,0 +1,66 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class Cpp implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = '//'; | |
| 9 | + private array $multiline = ['/*', '*/']; | |
| 10 | + | |
| 11 | + private array $tokens = [ | |
| 12 | + 'keyword' => [ | |
| 13 | + // C++26 | |
| 14 | + 'contract_assert', 'pre', 'post', 'trivially_reallocate_if_eligible', 'replaceable_if_eligible', | |
| 15 | + // C++20 | |
| 16 | + 'char8_t', 'concept', 'consteval', 'constinit', 'co_await', 'co_return', 'co_yield', 'import', 'module', 'requires', | |
| 17 | + // C++11 | |
| 18 | + 'alignas', 'alignof', 'char16_t', 'char32_t', 'constexpr', 'decltype', 'final', 'noexcept', 'nullptr', 'override', 'static_assert', 'thread_local', | |
| 19 | + // C++99 | |
| 20 | + 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'class', 'compl', 'const', 'const_cast', 'continue', | |
| 21 | + 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', | |
| 22 | + 'int', 'long', 'mutable', 'namespace', 'new', 'not', 'not_eq', 'operator', 'or', 'or_eq', 'private', 'protected', 'public', 'reflexpr', 'register', | |
| 23 | + 'reinterpret_cast', 'return', 'short', 'signed', 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'template', 'this', 'throw', 'true', 'try', | |
| 24 | + 'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq', | |
| 25 | + // TMTS | |
| 26 | + 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'synchronized', 'transaction_safe', 'transaction_safe_dynamic', | |
| 27 | + ], | |
| 28 | + 'preprocessor' => [ | |
| 29 | + // C++26 | |
| 30 | + '#embed', '__has_embed', | |
| 31 | + // C++23 | |
| 32 | + '#elifdef', '#elifndef', '#warning', | |
| 33 | + // C++20 | |
| 34 | + '#__has_cpp_attribute', '#export', '#import', '#module', | |
| 35 | + // C++17 | |
| 36 | + '#__has_include', | |
| 37 | + // C++11 | |
| 38 | + '#_Pragma', | |
| 39 | + // C++99 | |
| 40 | + '#if', '#elif', '#else', '#endif', '#ifdef', '#ifndef', '#define', '#undef', '#include', '#line', '#error', '#pragma', '#defined', | |
| 41 | + ], | |
| 42 | + 'typedef' => [ | |
| 43 | + 'int8_t', 'int16_t', 'int32_t', 'int64_t', | |
| 44 | + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', | |
| 45 | + 'size_t', | |
| 46 | + ], | |
| 47 | + ]; | |
| 48 | + | |
| 49 | + public function __construct() { | |
| 50 | + global $colorPalette; | |
| 51 | + | |
| 52 | + $this->colors = [ | |
| 53 | + 'comment' => $colorPalette['dark']['green'], | |
| 54 | + 'keyword' => $colorPalette['dark']['blue'], | |
| 55 | + 'number' => $colorPalette['light']['green'], | |
| 56 | + 'preprocessor' => $colorPalette['medium']['purple'], | |
| 57 | + 'string' => $colorPalette['medium']['orange'], | |
| 58 | + 'typedef' => $colorPalette['dark']['lime'], | |
| 59 | + ]; | |
| 60 | + } | |
| 61 | + | |
| 62 | + public function getColors(): array { return $this->colors; } | |
| 63 | + public function getComment(): ?string { return $this->comment; } | |
| 64 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 65 | + public function getTokens(): array { return $this->tokens; } | |
| 66 | +} |
| @@ -0,0 +1,65 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class Glsl implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = '//'; | |
| 9 | + | |
| 10 | + private array $tokens = [ | |
| 11 | + 'function' => [ | |
| 12 | + 'asin', 'acos', 'atan', 'cos', 'degrees', 'radians', 'sin', 'tan', | |
| 13 | + 'exp', 'exp2', 'inversesqrt', 'log', 'log2', 'pow', 'sqrt', | |
| 14 | + 'abs', 'ceil', 'clamp', 'floor', 'fract', 'max', 'min', 'mix', 'mod', 'sign', 'smoothstep', 'step', | |
| 15 | + 'cross', 'distance', 'dot', 'faceforward', 'ftransform', 'length', 'normalize', 'reflect', 'refract', | |
| 16 | + 'dFdx', 'dFdy', 'fwidth', | |
| 17 | + 'matrixCompMulti', | |
| 18 | + 'all', 'any', 'equal', 'greaterThan', 'greaterThanEqual', 'lessThan', 'lessThanEqual', 'not', 'notEqual', | |
| 19 | + 'texture1D', 'texture1DProj', 'texture2D', 'texture2DProj', 'texture3D', 'texture3DProj', 'textureCube', | |
| 20 | + 'texture1DLod', 'texture1DProjLod', 'texture2DLod', 'texture2DProjLod', 'texture3DProjLod', | |
| 21 | + 'shadow1DLog', 'shadow1DProjLog', 'shadow2DLog', 'shadow2DProjLog', | |
| 22 | + 'noise1', 'noise2', 'noise3', 'noise4', | |
| 23 | + ], | |
| 24 | + 'keyword' => [ | |
| 25 | + 'attribute', 'bool', 'bvec2', 'bvec3', 'bvec4', 'const', 'float', 'genType', 'in', 'int', 'inout', 'ivec2', 'ivec3', 'ivec4', 'mat2', 'mat3', 'mat4', 'out', | |
| 26 | + 'sampler1D', 'sampler1DShadow', 'sampler2D', 'sampler2DShadow', 'sampler3D', 'samplerCube', 'struct', 'uniform', 'varying', 'vec2', 'vec3', 'vec4', 'void', | |
| 27 | + ], | |
| 28 | + 'preprocessor' => [ | |
| 29 | + '#', '#define', '#elif', '#else', '#endif', '#error', '#extension', '#if', '#ifdef', '#ifndef', '#line', '#pragma', '#version', '#undef', | |
| 30 | + '__FILE__', '__LINE__', '__VERSION__', | |
| 31 | + ], | |
| 32 | + 'variables' => [ | |
| 33 | + // Vertex | |
| 34 | + 'gl_ClipVertex', 'gl_PointSize', 'gl_Position', 'gl_Color', 'gl_FogCoord', 'gl_Normal', 'gl_SecondaryColor', 'gl_Vertex', | |
| 35 | + 'gl_MultiTexCoord0', 'gl_MultiTexCoord1', 'gl_MultiTexCoord2', 'gl_MultiTexCoord3', 'gl_MultiTexCoord4', 'gl_MultiTexCoord5', 'gl_MultiTexCoord6', 'gl_MultiTexCoord7', | |
| 36 | + 'gl_BackColor', 'gl_BackSecondaryColor', 'gl_FogFragCoord', 'gl_FrontColor', 'gl_FrontSecondaryColor', 'gl_TexCoord', | |
| 37 | + // Fragment | |
| 38 | + 'gl_FragColor', 'gl_FragCoord', 'gl_FragData', 'gl_FragDepth', 'gl_FrontFacing', 'gl_MaxDrawBuffers', | |
| 39 | + // Constant | |
| 40 | + 'gl_MaxClipPlanes', 'gl_MaxCombinedTextureImageUnits', 'gl_MaxDrawBuffers', 'gl_MaxFragmentUniformComponents', 'gl_MaxLights', 'gl_MaxTextureCoords', | |
| 41 | + 'gl_MaxTextureImageUnits', 'gl_MaxTextureUnits', 'gl_MaxVaryingFloats', 'gl_MaxVertexAttribs', 'gl_MaxVertexUniformComponents', | |
| 42 | + // Uniform | |
| 43 | + 'gl_ClipPlane', 'gl_DepthRange', 'gl_DepthRangeParameters', 'gl_EyePlane', 'gl_Fog', 'gl_FogParameters', 'gl_LightModel', 'gl_LightModelParameters', | |
| 44 | + 'gl_LightModelProducts', 'gl_BackLightModelProduct', 'gl_FrontLightModelProduct', 'gl_LightProducts', 'gl_BackLightProduct', 'gl_FrontLightProduct', | |
| 45 | + 'gl_LightSource', 'gl_LightSourceParameters', 'gl_MaterialParameters', 'gl_BackMaterial', 'gl_FrontMaterial', | |
| 46 | + 'gl_ModelViewMatrix', 'gl_ModelViewMatrixInverse', 'gl_ModelViewMatrixTranspose', 'gl_ModelViewMatrixInverseTranspose', | |
| 47 | + 'gl_ModelViewProjectionMatrix', 'gl_ModelViewProjectionMatrixInverse', 'gl_ModelViewProjectionMatrixTranspose', 'gl_ModelViewProjectionMatrixInverseTranspose', | |
| 48 | + 'gl_ObjectPlane', 'gl_Point', 'gl_PointParameters', | |
| 49 | + 'gl_ProjectionMatrix', 'gl_ProjectionMatrixInverse', 'gl_ProjectionMatrixTranspose', 'gl_ProjectionMatrixInverseTranspose', 'gl_NormalMatrix', 'gl_NormalScale', | |
| 50 | + 'gl_TextureEnvColor', 'gl_TextureMatrix', 'gl_TextureMatrixInverse', 'gl_TextureMatrixTranspose', 'gl_TextureMatrixInverseTranspose', | |
| 51 | + ], | |
| 52 | + ]; | |
| 53 | + | |
| 54 | + public function __construct() { | |
| 55 | + global $colorPalette; | |
| 56 | + | |
| 57 | + $this->colors = [ | |
| 58 | + ]; | |
| 59 | + } | |
| 60 | + | |
| 61 | + public function getColors(): array { return $this->colors; } | |
| 62 | + public function getComment(): ?string { return $this->comment; } | |
| 63 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 64 | + public function getTokens(): array { return $this->tokens; } | |
| 65 | +} |
| @@ -0,0 +1,40 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | + | |
| 6 | +class Go implements SyntaxInterface { | |
| 7 | + private array $colors = []; | |
| 8 | + private string $comment = '//'; | |
| 9 | + | |
| 10 | + private array $tokens = [ | |
| 11 | + 'keyword' => [ | |
| 12 | + 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else', 'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface', 'map', | |
| 13 | + 'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var', | |
| 14 | + 'uint8', 'uint16', 'uint32', 'uint64', | |
| 15 | + 'int8', 'int16', 'int32', 'int64', | |
| 16 | + 'float32', 'float64', | |
| 17 | + 'complex64', 'complex128', | |
| 18 | + 'byte', 'rune', 'string', 'bool', | |
| 19 | + 'uint', 'int', 'uintptr', | |
| 20 | + ], | |
| 21 | + 'operator' => [ | |
| 22 | + '+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&^', '<', '>', '=', '!', '~', '...', ',', '.', ';', ':', | |
| 23 | + '{', '}', '(', ')', '[', ']', | |
| 24 | + '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&^=', '!=', '<=', '>=', ';=', | |
| 25 | + '&&', '||', '<-', '++', '--', '==', | |
| 26 | + ], | |
| 27 | + ]; | |
| 28 | + | |
| 29 | + public function __construct() { | |
| 30 | + global $colorPalette; | |
| 31 | + | |
| 32 | + $this->colors = [ | |
| 33 | + ]; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public function getColors(): array { return $this->colors; } | |
| 37 | + public function getComment(): ?string { return $this->comment; } | |
| 38 | + public function getMultiline(): ?array { return $this->multiline; } | |
| 39 | + public function getTokens(): array { return $this->tokens; } | |
| 40 | +} |
| @@ -0,0 +1,10 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +interface SyntaxInterface { | |
| 5 | + public function getColors(): array; | |
| 6 | + public function getComment(): ?string; | |
| 7 | + public function getInclude(array $code): ?array; | |
| 8 | + public function getMultiline(): ?array; | |
| 9 | + public function getTokens(): array; | |
| 10 | +} |
| @@ -0,0 +1,243 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +class Template { | |
| 5 | + private string $tmplExt = '.maron'; | |
| 6 | + private string $tmplPath; | |
| 7 | + private array $vars = []; | |
| 8 | + private array $blocks = []; | |
| 9 | + private array $custFunc = []; | |
| 10 | + private array $custCss = []; | |
| 11 | + | |
| 12 | + /** | |
| 13 | + * テンプレートクラスのコンストラクタ | |
| 14 | + * | |
| 15 | + * @param string $tmplPath テンプレートのパス | |
| 16 | + * @return void | |
| 17 | + */ | |
| 18 | + public function __construct(string $tmplPath) { | |
| 19 | + $this->tmplPath = rtrim($tmplPath, '/'); | |
| 20 | + | |
| 21 | + if (substr($this->tmplPath, 0, 1) !== '/') { | |
| 22 | + $this->tmplPath = '/'.$this->tmplPath; | |
| 23 | + } | |
| 24 | + } | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * テンプレート変数に値を割り当てる | |
| 28 | + * | |
| 29 | + * @param string $name 変数名 | |
| 30 | + * @param mixed $value 値 | |
| 31 | + * @return void | |
| 32 | + */ | |
| 33 | + public function assign(string $name, mixed $value): void { | |
| 34 | + $this->vars[$name] = $value; | |
| 35 | + } | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * カスタムCSSファイルを追加する | |
| 39 | + * | |
| 40 | + * @param string $name CSSファイル名 | |
| 41 | + * @return void | |
| 42 | + */ | |
| 43 | + public function addCss(string $name): void { | |
| 44 | + $this->custCss[] = | |
| 45 | + "<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style-{$name}.css\" />\n"; | |
| 46 | + $this->assign('custCss', $this->custCss); | |
| 47 | + } | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * カスタム関数を登録する | |
| 51 | + * | |
| 52 | + * @param string $name 関数名 | |
| 53 | + * @param callable $callback コールバック関数 | |
| 54 | + * @return void | |
| 55 | + */ | |
| 56 | + public function registerFunction(string $name, callable $callback): void { | |
| 57 | + $this->custFunc[$name] = $callback; | |
| 58 | + } | |
| 59 | + | |
| 60 | + /** | |
| 61 | + * テンプレートブロックを定義する | |
| 62 | + * | |
| 63 | + * @param string $name ブロック名 | |
| 64 | + * @param string $content ブロック内容 | |
| 65 | + * @return void | |
| 66 | + */ | |
| 67 | + public function defineBlock(string $name, string $content): void { | |
| 68 | + if (!isset($this->blocks[$name])) | |
| 69 | + $this->blocks[$name] = $content; | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * テンプレートをレンダリングする | |
| 74 | + * | |
| 75 | + * @param string $tmplName テンプレート名 | |
| 76 | + * @return void | |
| 77 | + */ | |
| 78 | + public function render(string $tmplName): void { | |
| 79 | + $tmplPath = ROOT.'/view'.$this->tmplPath.'/'.$tmplName.$this->tmplExt; | |
| 80 | + if (!file_exists($tmplPath)) | |
| 81 | + throw new \RuntimeException("テンプレートファイルを見つけません:{$tmplPath}"); | |
| 82 | + | |
| 83 | + extract($this->vars); | |
| 84 | + | |
| 85 | + $content = file_get_contents($tmplPath); | |
| 86 | + | |
| 87 | + // インクルードディレクティブを処理 | |
| 88 | + while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) { | |
| 89 | + $content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m): bool|string { | |
| 90 | + $inclPath = ROOT.'/view/'.trim($m[1], "'\" ").$this->tmplExt; | |
| 91 | + if (!file_exists($inclPath)) | |
| 92 | + throw new \RuntimeException("ファイルを見つけません: {$inclPath}"); | |
| 93 | + return file_get_contents($inclPath); | |
| 94 | + }, $content); | |
| 95 | + } | |
| 96 | + | |
| 97 | + $content = $this->procDirs($content); | |
| 98 | + $content = $this->procVars($content); | |
| 99 | + $content = $this->procFuncs($content); | |
| 100 | + | |
| 101 | + $tmpFile = tempnam(sys_get_temp_dir(), 'tmpl_'); | |
| 102 | + file_put_contents($tmpFile, $content); | |
| 103 | + | |
| 104 | + include $tmpFile; | |
| 105 | + unlink($tmpFile); | |
| 106 | + } | |
| 107 | + | |
| 108 | + // 機能性メソッド | |
| 109 | + | |
| 110 | + /** | |
| 111 | + * テンプレートディレクティブを処理する | |
| 112 | + * | |
| 113 | + * @param string $content テンプレート内容 | |
| 114 | + * @return string|null 処理後の内容 | |
| 115 | + */ | |
| 116 | + private function procDirs(string $content): string|null { | |
| 117 | + // includeディレクティブの処理 | |
| 118 | + while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) { | |
| 119 | + $content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m): bool|string { | |
| 120 | + $inclPath = ROOT.'/view/'.trim($m[1], "'\" ").'.php'; | |
| 121 | + if (!file_exists($inclPath)) | |
| 122 | + throw new \RuntimeException("ファイルを見つけません: {$inclPath}"); | |
| 123 | + return file_get_contents($inclPath); | |
| 124 | + }, $content); | |
| 125 | + } | |
| 126 | + | |
| 127 | + $content = preg_replace('/\{@\s*if\s*\((.*?)\):\s*@\}/', '{@ if ($1) @}', $content); | |
| 128 | + $content = preg_replace('/\{@\s*endif;\s*@\}/', '{@ endif @}', $content); | |
| 129 | + | |
| 130 | + $processDirectives = function($c): array|string|null { | |
| 131 | + // kysディレクティブの処理 | |
| 132 | + $c = preg_replace_callback('/\{@\s*kys\((.*?)\)\s*@\}/s', function($m): string { | |
| 133 | + return "<?php echo '<pre>'; print_r({$m[1]}); echo '</pre>'; die(); ?>"; | |
| 134 | + }, $c); | |
| 135 | + | |
| 136 | + // foreachループとネストした内容の処理 | |
| 137 | + $c = preg_replace_callback('/\{@\s*foreach\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 138 | + return "<?php foreach({$m[1]}): ?>"; | |
| 139 | + }, $c); | |
| 140 | + | |
| 141 | + $c = preg_replace_callback('/\{@\s*endforeach\s*@\}/s', function($m): string { | |
| 142 | + return "<?php endforeach; ?>"; | |
| 143 | + }, $c); | |
| 144 | + | |
| 145 | + // forループの処理 | |
| 146 | + $c = preg_replace_callback('/\{@\s*for\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 147 | + return "<?php for({$m[1]}): ?>"; | |
| 148 | + }, $c); | |
| 149 | + | |
| 150 | + $c = preg_replace_callback('/\{@\s*endfor\s*@\}/s', function($m): string { | |
| 151 | + return "<?php endfor; ?>"; | |
| 152 | + }, $c); | |
| 153 | + | |
| 154 | + // if-elif-else-endifの処理 | |
| 155 | + $c = preg_replace_callback('/\{@\s*if\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 156 | + return "<?php if ({$m[1]}): ?>"; | |
| 157 | + }, $c); | |
| 158 | + | |
| 159 | + $c = preg_replace_callback('/\{@\s*elif\s*\((.*?)\)\s*@\}/s', function($m): string { | |
| 160 | + return "<?php elseif ({$m[1]}): ?>"; | |
| 161 | + }, $c); | |
| 162 | + | |
| 163 | + $c = preg_replace_callback('/\{@\s*else\s*@\}/s', function($m): string { | |
| 164 | + return "<?php else: ?>"; | |
| 165 | + }, $c); | |
| 166 | + | |
| 167 | + $c = preg_replace_callback('/\{@\s*endif\s*@\}/s', function($m): string { | |
| 168 | + return "<?php endif; ?>"; | |
| 169 | + }, $c); | |
| 170 | + | |
| 171 | + return $c; | |
| 172 | + }; | |
| 173 | + | |
| 174 | + $previousContent = ''; | |
| 175 | + $maxIterations = 10; | |
| 176 | + $iterations = 0; | |
| 177 | + | |
| 178 | + while ($previousContent !== $content && $iterations < $maxIterations) { | |
| 179 | + $previousContent = $content; | |
| 180 | + $content = $processDirectives($content); | |
| 181 | + $iterations++; | |
| 182 | + } | |
| 183 | + | |
| 184 | + return $content; | |
| 185 | + } | |
| 186 | + | |
| 187 | + /** | |
| 188 | + * テンプレート変数を処理する | |
| 189 | + * | |
| 190 | + * @param string $content テンプレート内容 | |
| 191 | + * @return string 処理後の内容 | |
| 192 | + */ | |
| 193 | + private function procVars(string $content): string { | |
| 194 | + // 変数の出力(エスケープ処理なし) | |
| 195 | + $content = preg_replace_callback('/\{\{\{s*(.*?)\s*\}\}\}/', function($m): string { | |
| 196 | + return '<?= '.trim($m[1]).' ?>'; | |
| 197 | + }, $content); | |
| 198 | + | |
| 199 | + // 変数の出力(エスケープ処理あり) | |
| 200 | + $content = preg_replace_callback('/\{\{\s*(.*?)\s*\}\}/', function($m): string { | |
| 201 | + return '<?= htmlspecialchars('.trim($m[1]).', ENT_QUOTES, \'UTF-8\') ?>'; | |
| 202 | + }, $content); | |
| 203 | + | |
| 204 | + // 変数の代入 | |
| 205 | + $content = preg_replace_callback('/\{\$\s*(.*?)\s*\$\}/', function($m): string { | |
| 206 | + $parts = explode('=', $m[1], 2); | |
| 207 | + if (count($parts) !== 2) | |
| 208 | + throw new \RuntimeException("不正な値の形式"); | |
| 209 | + return '<?php '.trim($parts[0]).' = '.trim($parts[1]).'; ?>'; | |
| 210 | + }, $content); | |
| 211 | + | |
| 212 | + // コメント | |
| 213 | + $content = preg_replace_callback('/\{#\s*(.*?)\s*#\}/', function($m): string { | |
| 214 | + return '<?php /*'.trim($m[1]).'*/ ?>'; | |
| 215 | + }, $content); | |
| 216 | + | |
| 217 | + // PHPコードの実行 | |
| 218 | + $content = preg_replace_callback('/\{\!\s*(.*?)\s*\!\}/', function($m): string { | |
| 219 | + return '<?php '.trim($m[1]).' ?>'; | |
| 220 | + }, $content); | |
| 221 | + | |
| 222 | + return $content; | |
| 223 | + } | |
| 224 | + | |
| 225 | + /** | |
| 226 | + * カスタム関数を処理する | |
| 227 | + * | |
| 228 | + * @param string $content テンプレート内容 | |
| 229 | + * @return string 処理後の内容 | |
| 230 | + */ | |
| 231 | + private function procFuncs(string $content): string { | |
| 232 | + foreach ($this->custFunc as $name => $cb) { | |
| 233 | + $pattern = "/\{@\s*{$name}\((.*?)\)\s*@\}/"; | |
| 234 | + $content = preg_replace_callback($pattern, function($m) use ($cb): mixed { | |
| 235 | + $args = explode(',', $m[1]); | |
| 236 | + $args = array_map('trim', $args); | |
| 237 | + return call_user_func_array($cb, $args); | |
| 238 | + }, $content); | |
| 239 | + } | |
| 240 | + | |
| 241 | + return $content; | |
| 242 | + } | |
| 243 | +} |
| @@ -0,0 +1,617 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * アサーション失敗用のカスタム例外 | |
| 6 | + */ | |
| 7 | +class AssertionFailedException extends \Exception { | |
| 8 | +} | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 軽量なユニットテストフレームワーク | |
| 12 | + */ | |
| 13 | +class Tester { | |
| 14 | + // テスト統計 | |
| 15 | + private int $testCount = 0; | |
| 16 | + private int $passCount = 0; | |
| 17 | + private int $failCount = 0; | |
| 18 | + private int $errorCount = 0; | |
| 19 | + | |
| 20 | + // 現在のテストケース情報 | |
| 21 | + private string $currentTestCase = ''; | |
| 22 | + private string $currentTest = ''; | |
| 23 | + | |
| 24 | + // テストスイート設定 | |
| 25 | + private bool $colorOutput = true; | |
| 26 | + private bool $verboseOutput = true; | |
| 27 | + private bool $stopOnFailure = false; | |
| 28 | + private bool $skipCurrentDescribe = false; | |
| 29 | + private array $beforeEachCallbacks = []; | |
| 30 | + private array $afterEachCallbacks = []; | |
| 31 | + private array $beforeAllCallbacks = []; | |
| 32 | + private array $afterAllCallbacks = []; | |
| 33 | + | |
| 34 | + // ターミナル出力用の色 | |
| 35 | + private array $colors = [ | |
| 36 | + 'reset' => "\033[0m", | |
| 37 | + 'red' => "\033[31m", | |
| 38 | + 'green' => "\033[32m", | |
| 39 | + 'yellow' => "\033[33m", | |
| 40 | + 'blue' => "\033[34m", | |
| 41 | + 'magenta' => "\033[35m", | |
| 42 | + 'cyan' => "\033[36m", | |
| 43 | + 'white' => "\033[37m", | |
| 44 | + 'bold' => "\033[1m", | |
| 45 | + ]; | |
| 46 | + | |
| 47 | + private array $failures = []; | |
| 48 | + private array $errors = []; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * コンストラクタ | |
| 52 | + * | |
| 53 | + * @param array $options 設定オプション | |
| 54 | + */ | |
| 55 | + public function __construct(array $options = []) { | |
| 56 | + // オプションを設定 | |
| 57 | + if (isset($options['colorOutput'])) { | |
| 58 | + $this->colorOutput = (bool)$options['colorOutput']; | |
| 59 | + } | |
| 60 | + | |
| 61 | + if (isset($options['verboseOutput'])) { | |
| 62 | + $this->verboseOutput = (bool)$options['verboseOutput']; | |
| 63 | + } | |
| 64 | + | |
| 65 | + if (isset($options['stopOnFailure'])) { | |
| 66 | + $this->stopOnFailure = (bool)$options['stopOnFailure']; | |
| 67 | + } | |
| 68 | + | |
| 69 | + // サポートされていない場合は色を無効にする | |
| 70 | + if (PHP_SAPI !== 'cli' || strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !getenv('ANSICON')) { | |
| 71 | + $this->colorOutput = false; | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * 各テストの前に実行する関数を登録する | |
| 77 | + * | |
| 78 | + * @param callable $callback コールバック関数 | |
| 79 | + * @return Tester このインスタンス | |
| 80 | + */ | |
| 81 | + public function beforeEach(callable $callback): Tester { | |
| 82 | + $this->beforeEachCallbacks[] = $callback; | |
| 83 | + return $this; | |
| 84 | + } | |
| 85 | + | |
| 86 | + /** | |
| 87 | + * 各テストの後に実行する関数を登録する | |
| 88 | + * | |
| 89 | + * @param callable $callback コールバック関数 | |
| 90 | + * @return Tester このインスタンス | |
| 91 | + */ | |
| 92 | + public function afterEach(callable $callback): Tester { | |
| 93 | + $this->afterEachCallbacks[] = $callback; | |
| 94 | + return $this; | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * すべてのテストの前に実行する関数を登録する | |
| 99 | + * | |
| 100 | + * @param callable $callback コールバック関数 | |
| 101 | + * @return Tester このインスタンス | |
| 102 | + */ | |
| 103 | + public function beforeAll(callable $callback): Tester { | |
| 104 | + $this->beforeAllCallbacks[] = $callback; | |
| 105 | + return $this; | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** | |
| 109 | + * すべてのテストの後に実行する関数を登録する | |
| 110 | + * | |
| 111 | + * @param callable $callback コールバック関数 | |
| 112 | + * @return Tester このインスタンス | |
| 113 | + */ | |
| 114 | + public function afterAll(callable $callback): Tester { | |
| 115 | + $this->afterAllCallbacks[] = $callback; | |
| 116 | + return $this; | |
| 117 | + } | |
| 118 | + | |
| 119 | + /** | |
| 120 | + * テストケースを定義する | |
| 121 | + * | |
| 122 | + * @param string $description テストケースの説明 | |
| 123 | + * @param callable $callback テストケース関数 | |
| 124 | + * @return Tester このインスタンス | |
| 125 | + */ | |
| 126 | + public function describe(string $description, callable $callback): Tester { | |
| 127 | + $this->currentTestCase = $description; | |
| 128 | + $this->skipCurrentDescribe = false; | |
| 129 | + $this->output($this->colorize('bold', "テストケース: {$description}")); | |
| 130 | + | |
| 131 | + try { | |
| 132 | + foreach ($this->beforeAllCallbacks as $before) { | |
| 133 | + call_user_func($before); | |
| 134 | + } | |
| 135 | + | |
| 136 | + call_user_func($callback, $this); | |
| 137 | + | |
| 138 | + foreach ($this->afterAllCallbacks as $after) { | |
| 139 | + call_user_func($after); | |
| 140 | + } | |
| 141 | + } catch (\Throwable $e) { | |
| 142 | + $this->recordError( | |
| 143 | + "テストケースのセットアップ/ティアダウンでエラー: ".$e->getMessage(), | |
| 144 | + $e->getTraceAsString()); | |
| 145 | + } | |
| 146 | + | |
| 147 | + $this->output(''); | |
| 148 | + return $this; | |
| 149 | + } | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * 単一のテストを実行する | |
| 153 | + * | |
| 154 | + * @param string $description テストの説明 | |
| 155 | + * @param callable $callback テスト関数 | |
| 156 | + * @return Tester このインスタンス | |
| 157 | + */ | |
| 158 | + public function it(string $description, callable $callback): Tester { | |
| 159 | + if ($this->skipCurrentDescribe) { | |
| 160 | + $this->skip($description, 'skipAll() が呼び出されました'); | |
| 161 | + return $this; | |
| 162 | + } | |
| 163 | + | |
| 164 | + $this->currentTest = $description; | |
| 165 | + $this->testCount++; | |
| 166 | + | |
| 167 | + if ($this->verboseOutput) { | |
| 168 | + $this->output(" ⋄ テスト中: {$description}... ", false); | |
| 169 | + } | |
| 170 | + | |
| 171 | + try { | |
| 172 | + foreach ($this->beforeEachCallbacks as $before) { | |
| 173 | + call_user_func($before); | |
| 174 | + } | |
| 175 | + | |
| 176 | + call_user_func($callback, $this); | |
| 177 | + | |
| 178 | + foreach ($this->afterEachCallbacks as $after) { | |
| 179 | + call_user_func($after); | |
| 180 | + } | |
| 181 | + | |
| 182 | + // Test has passed. | |
| 183 | + $this->passCount++; | |
| 184 | + | |
| 185 | + if ($this->verboseOutput) { | |
| 186 | + $this->output($this->colorize('green', "合格")); | |
| 187 | + } | |
| 188 | + } catch (AssertionFailedException $e) { | |
| 189 | + $this->failCount++; | |
| 190 | + | |
| 191 | + if ($this->verboseOutput) { | |
| 192 | + $this->output($this->colorize('red', "失敗")); | |
| 193 | + $this->output($this->colorize('red', " → ".$e->getMessage())); | |
| 194 | + } | |
| 195 | + | |
| 196 | + $this->recordFailure($e->getMessage()); | |
| 197 | + | |
| 198 | + if ($this->stopOnFailure) { | |
| 199 | + $this->printSummary(); | |
| 200 | + exit(1); | |
| 201 | + } | |
| 202 | + } catch (\Throwable $e) { | |
| 203 | + $this->errorCount++; | |
| 204 | + | |
| 205 | + if ($this->verboseOutput) { | |
| 206 | + $this->output($this->colorize('yellow', "エラー")); | |
| 207 | + $this->output($this->colorize('yellow', " → ".$e->getMessage())); | |
| 208 | + } | |
| 209 | + | |
| 210 | + $this->recordError($e->getMessage(), $e->getTraceAsString()); | |
| 211 | + | |
| 212 | + if ($this->stopOnFailure) { | |
| 213 | + $this->printSummary(); | |
| 214 | + exit(1); | |
| 215 | + } | |
| 216 | + } | |
| 217 | + | |
| 218 | + return $this; | |
| 219 | + } | |
| 220 | + | |
| 221 | + /** | |
| 222 | + * テストをスキップする | |
| 223 | + * | |
| 224 | + * @param string $description テストの説明 | |
| 225 | + * @param string $reason スキップする理由。デフォルト: "まだ実装されていません" | |
| 226 | + * @return Tester このインスタンス | |
| 227 | + */ | |
| 228 | + public function skip(string $description, | |
| 229 | + string $reason = 'まだ実装されていません'): Tester { | |
| 230 | + if ($this->verboseOutput) { | |
| 231 | + $this->output(" ⋄ スキップ: {$description}... " | |
| 232 | + .$this->colorize('cyan', "スキップ")); | |
| 233 | + $this->output($this->colorize('cyan', " → {$reason}")); | |
| 234 | + } | |
| 235 | + | |
| 236 | + return $this; | |
| 237 | + } | |
| 238 | + | |
| 239 | + /** | |
| 240 | + * 条件がtrueである事をアサートする | |
| 241 | + * | |
| 242 | + * @param bool $condition チェックする条件 | |
| 243 | + * @param string $message 失敗時のオプションメッセージ | |
| 244 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 245 | + * @return Tester このインスタンス | |
| 246 | + */ | |
| 247 | + public function assertTrue(bool $condition, | |
| 248 | + string $message = '条件がtrueであることを期待しました'): Tester { | |
| 249 | + if ($condition !== true) { | |
| 250 | + throw new AssertionFailedException($message); | |
| 251 | + } | |
| 252 | + | |
| 253 | + return $this; | |
| 254 | + } | |
| 255 | + | |
| 256 | + /** | |
| 257 | + * 条件がfalseである事をアサートする | |
| 258 | + * | |
| 259 | + * @param bool $condition チェックする条件 | |
| 260 | + * @param string $message 失敗時のオプションメッセージ | |
| 261 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 262 | + * @return Tester このインスタンス | |
| 263 | + */ | |
| 264 | + public function assertFalse(bool $condition, | |
| 265 | + string $message = '条件がfalseであることを期待しました'): Tester { | |
| 266 | + if ($condition !== false) { | |
| 267 | + throw new AssertionFailedException($message); | |
| 268 | + } | |
| 269 | + | |
| 270 | + return $this; | |
| 271 | + } | |
| 272 | + | |
| 273 | + /** | |
| 274 | + * 二つの値が等しい事をアサートする | |
| 275 | + * | |
| 276 | + * @param mixed $expected 期待値 | |
| 277 | + * @param mixed $actual 実際の値 | |
| 278 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 279 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 280 | + * @return Tester このインスタンス | |
| 281 | + */ | |
| 282 | + public function assertEquals(mixed $expected, mixed $actual, | |
| 283 | + ?string $message = null): Tester { | |
| 284 | + if ($expected != $actual) { | |
| 285 | + if ($message === null) { | |
| 286 | + $expected = $this->exportValue($expected); | |
| 287 | + $actual = $this->exportValue($actual); | |
| 288 | + $message = "{$expected}を期待しましたが、{$actual}が得られました"; | |
| 289 | + } | |
| 290 | + | |
| 291 | + throw new AssertionFailedException($message); | |
| 292 | + } | |
| 293 | + | |
| 294 | + return $this; | |
| 295 | + } | |
| 296 | + | |
| 297 | + /** | |
| 298 | + * 二つの値が同一である事をアサートする | |
| 299 | + * | |
| 300 | + * @param mixed $expected 期待値 | |
| 301 | + * @param mixed $actual 実際の値 | |
| 302 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 303 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 304 | + * @return Tester このインスタンス | |
| 305 | + */ | |
| 306 | + public function assertSame(mixed $expected, mixed $actual, | |
| 307 | + ?string $message = null): Tester { | |
| 308 | + if ($expected !== $actual) { | |
| 309 | + if ($message === null) { | |
| 310 | + $expected = $this->exportValue($expected); | |
| 311 | + $actual = $this->exportValue($actual); | |
| 312 | + $message = | |
| 313 | + "{$expected}を期待しましたが、{$actual}が得られました(厳密な比較)"; | |
| 314 | + } | |
| 315 | + | |
| 316 | + throw new AssertionFailedException($message); | |
| 317 | + } | |
| 318 | + | |
| 319 | + return $this; | |
| 320 | + } | |
| 321 | + | |
| 322 | + /** | |
| 323 | + * 値がnullである事をアサートする | |
| 324 | + * | |
| 325 | + * @param mixed $actual チェックする値 | |
| 326 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 327 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 328 | + * @return Tester このインスタンス | |
| 329 | + */ | |
| 330 | + public function assertNull(mixed $actual, ?string $message = null): Tester { | |
| 331 | + if ($actual !== null) { | |
| 332 | + if ($message === null) { | |
| 333 | + $actual = $this->exportValue($actual); | |
| 334 | + $message = "nullを期待しましたが、{$actual}が得られました"; | |
| 335 | + } | |
| 336 | + | |
| 337 | + throw new AssertionFailedException($message); | |
| 338 | + } | |
| 339 | + | |
| 340 | + return $this; | |
| 341 | + } | |
| 342 | + | |
| 343 | + /** | |
| 344 | + * 値がnullでない事をアサートする | |
| 345 | + * | |
| 346 | + * @param mixed $actual チェックする値 | |
| 347 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 348 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 349 | + * @return Tester このインスタンス | |
| 350 | + */ | |
| 351 | + public function assertNotNull(mixed $actual, ?string $message = null): Tester { | |
| 352 | + if ($actual === null) { | |
| 353 | + if ($message === null) { | |
| 354 | + $message = "値がnullでない事を期待しました"; | |
| 355 | + } | |
| 356 | + | |
| 357 | + throw new AssertionFailedException($message); | |
| 358 | + } | |
| 359 | + | |
| 360 | + return $this; | |
| 361 | + } | |
| 362 | + | |
| 363 | + /** | |
| 364 | + * 値が特定のキーを持つ事をアサートする | |
| 365 | + * | |
| 366 | + * @param mixed $key チェックするキー | |
| 367 | + * @param array $array チェックする配列 | |
| 368 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 369 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 370 | + * @return Tester このインスタンス | |
| 371 | + */ | |
| 372 | + public function assertArrayHasKey(mixed $key, array $array, | |
| 373 | + ?string $message = null): Tester { | |
| 374 | + if (!is_array($array) && !($array instanceof \ArrayAccess)) { | |
| 375 | + throw new AssertionFailedException( | |
| 376 | + '第2引数は配列又はArrayAccessを実装している必要があります'); | |
| 377 | + } | |
| 378 | + | |
| 379 | + if (!array_key_exists($key, $array)) { | |
| 380 | + if ($message === null) { | |
| 381 | + $message = "配列がキー '{$key}' を持つ事を期待しました"; | |
| 382 | + } | |
| 383 | + | |
| 384 | + throw new AssertionFailedException($message); | |
| 385 | + } | |
| 386 | + | |
| 387 | + return $this; | |
| 388 | + } | |
| 389 | + | |
| 390 | + /** | |
| 391 | + * 文字列がサブ文字列を含むことをアサートする | |
| 392 | + * | |
| 393 | + * @param string $needle 検索するサブ文字列 | |
| 394 | + * @param string $haystack 検索対象の文字列 | |
| 395 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 396 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 397 | + * @return Tester このインスタンス | |
| 398 | + */ | |
| 399 | + public function assertStringContains(string $needle, string $haystack, | |
| 400 | + ?string $message = null): Tester { | |
| 401 | + if (!is_string($needle) || !is_string($haystack)) { | |
| 402 | + throw new AssertionFailedException('両方の引数は文字列である必要があります'); | |
| 403 | + } | |
| 404 | + | |
| 405 | + if (strpos($haystack, $needle) === false) { | |
| 406 | + if ($message === null) { | |
| 407 | + $message = "文字列 '{$haystack}' が '{$needle}' を含む事を期待しました"; | |
| 408 | + } | |
| 409 | + | |
| 410 | + throw new AssertionFailedException($message); | |
| 411 | + } | |
| 412 | + | |
| 413 | + return $this; | |
| 414 | + } | |
| 415 | + | |
| 416 | + /** | |
| 417 | + * コールバックが例外をスローする事をアサートする | |
| 418 | + * | |
| 419 | + * @param callable $callback 実行するコールバック | |
| 420 | + * @param string $exceptionClass 期待される例外クラス | |
| 421 | + * @param string|null $message 失敗時のオプションメッセージ | |
| 422 | + * @throws AssertionFailedException アサーションが失敗した場合 | |
| 423 | + * @return Tester このインスタンス | |
| 424 | + */ | |
| 425 | + public function assertThrows(callable $callback, string $exceptionClass, | |
| 426 | + ?string $message = null): Tester { | |
| 427 | + try { | |
| 428 | + call_user_func($callback); | |
| 429 | + | |
| 430 | + if ($message === null) { | |
| 431 | + $message = "'{$exceptionClass}' 型の例外がスローされる事を期待しましたが、スローされませんでした"; | |
| 432 | + } | |
| 433 | + | |
| 434 | + throw new AssertionFailedException($message); | |
| 435 | + } catch (\Throwable $e) { | |
| 436 | + if (!($e instanceof $exceptionClass)) { | |
| 437 | + if ($message === null) { | |
| 438 | + $message = "'{$exceptionClass}' 型の例外を期待しましたが、" | |
| 439 | + .get_class($e)." が得られました"; | |
| 440 | + } | |
| 441 | + | |
| 442 | + throw new AssertionFailedException($message); | |
| 443 | + } | |
| 444 | + } | |
| 445 | + | |
| 446 | + return $this; | |
| 447 | + } | |
| 448 | + | |
| 449 | + /** | |
| 450 | + * コレクションが指定された要素数を持つ事 | |
| 451 | + * | |
| 452 | + * @param int $expoected 期待される要素数 | |
| 453 | + * @param iterable $collection 配列または Traversable | |
| 454 | + * @param string|null $message 失敗時のカスタムメッセージ | |
| 455 | + * @return Tester | |
| 456 | + * | |
| 457 | + * @throws AssertionFailedException | |
| 458 | + */ | |
| 459 | + public function assertCount(int $expected, iterable $collection, ?string $message = null): Tester { | |
| 460 | + $actual = is_countable($collection) ? count($collection) : iterator_count($collection); | |
| 461 | + | |
| 462 | + if ($actual !== $expected) { | |
| 463 | + $message ??= "要素数が {$expected} であることを期待しましたが、{$actual} でした"; | |
| 464 | + throw new AssertionFailedException($message); | |
| 465 | + } | |
| 466 | + | |
| 467 | + return $this; | |
| 468 | + } | |
| 469 | + | |
| 470 | + /** | |
| 471 | + * 現在の describe ブロック内の残りすべての it() をスキップ | |
| 472 | + * | |
| 473 | + * @param string $reason スキップ理由(出力に表示) | |
| 474 | + * @return void | |
| 475 | + */ | |
| 476 | + public function skipAll(string $reason = 'このテストケースはスキップされました'): void { | |
| 477 | + $this->skipCurrentDescribe = true; | |
| 478 | + | |
| 479 | + if ($this->verboseOutput) { | |
| 480 | + $this->output($this->colorize('cyan', " スキップAll: {$reason}")); | |
| 481 | + } | |
| 482 | + } | |
| 483 | + | |
| 484 | + /** | |
| 485 | + * テスト結果の詳細を表示する | |
| 486 | + * | |
| 487 | + * @return Tester | |
| 488 | + */ | |
| 489 | + public function printSummary(): Tester { | |
| 490 | + $this->output(''); | |
| 491 | + $this->output($this->colorize('bold', "テスト結果の概要:")); | |
| 492 | + $this->output(" テスト総数: {$this->testCount}"); | |
| 493 | + $this->output(" ".$this->colorize('green', "合格: {$this->passCount}")); | |
| 494 | + $this->output(" ".$this->colorize('red', "失敗: {$this->failCount}")); | |
| 495 | + $this->output(" ".$this->colorize('yellow', "エラー: {$this->errorCount}")); | |
| 496 | + $this->output(''); | |
| 497 | + | |
| 498 | + // 失敗を書き出す | |
| 499 | + if (count($this->failures) > 0) { | |
| 500 | + $this->output($this->colorize('bold', "失敗:")); | |
| 501 | + | |
| 502 | + foreach ($this->failures as $i => $f) { | |
| 503 | + $num = $i + 1; | |
| 504 | + $this->output(" {$num}) {$f['testCase']} → {$f['test']}"); | |
| 505 | + $this->output(" ".$this->colorize('red', $f['message'])); | |
| 506 | + $this->output(''); | |
| 507 | + } | |
| 508 | + } | |
| 509 | + | |
| 510 | + // エラーを書き出す | |
| 511 | + if (count($this->errors) > 0) { | |
| 512 | + $this->output($this->colorize('bold', "エラー:")); | |
| 513 | + | |
| 514 | + foreach ($this->errors as $i => $e) { | |
| 515 | + $num = $i + 1; | |
| 516 | + $this->output(" {$num}) {$e['testCase']} → {$e['test']}"); | |
| 517 | + $this->output(" ".$this->colorize('yellow', $e['message'])); | |
| 518 | + | |
| 519 | + if (isset($e['trace'])) { | |
| 520 | + $this->output(" ".$this->colorize('yellow', "スタックトレース:")); | |
| 521 | + $this->output(" ".$this->colorize('yellow', $e['trace'])); | |
| 522 | + } | |
| 523 | + | |
| 524 | + $this->output(''); | |
| 525 | + } | |
| 526 | + } | |
| 527 | + | |
| 528 | + if ($this->failCount === 0 && $this->errorCount === 0) { | |
| 529 | + $this->output($this->colorize('green', "全てのテストに合格しました!")); | |
| 530 | + } else { | |
| 531 | + $this->output($this->colorize('red', "テストが失敗・エラーで完了しました。")); | |
| 532 | + } | |
| 533 | + | |
| 534 | + return $this; | |
| 535 | + } | |
| 536 | + | |
| 537 | + // 機能性メソッド | |
| 538 | + | |
| 539 | + /** | |
| 540 | + * コンソールにテキストを出力する | |
| 541 | + * | |
| 542 | + * @param string $text 出力するテキスト | |
| 543 | + * @param bool $newline 改行を追加するかどうか | |
| 544 | + * @return void | |
| 545 | + */ | |
| 546 | + private function output(string $text, bool $newline = true): void { | |
| 547 | + echo $text.($newline ? PHP_EOL : ''); | |
| 548 | + } | |
| 549 | + | |
| 550 | + /** | |
| 551 | + * 有効な場合はテキストに色を適用する | |
| 552 | + * | |
| 553 | + * @param string $color 色名 | |
| 554 | + * @param string $text 色付けするテキスト | |
| 555 | + * @return string | |
| 556 | + */ | |
| 557 | + private function colorize(string $color, string $text): string { | |
| 558 | + if (!$this->colorOutput || !isset($this->colors[$color])) { | |
| 559 | + return $text; | |
| 560 | + } | |
| 561 | + | |
| 562 | + return $this->colors[$color].$text.$this->colors['reset']; | |
| 563 | + } | |
| 564 | + | |
| 565 | + /** | |
| 566 | + * 値を表示用の文字列としてエクスポートする | |
| 567 | + * | |
| 568 | + * @param mixed $value エクスポートする値 | |
| 569 | + * @return string | |
| 570 | + */ | |
| 571 | + private function exportValue(mixed $value): string { | |
| 572 | + if (is_null($value)) return 'null'; | |
| 573 | + if (is_bool($value)) return $value ? 'true' : 'false'; | |
| 574 | + if (is_array($value)) return 'Array('.count($value).')'; | |
| 575 | + if (is_object($value)) return get_class($value).' Object'; | |
| 576 | + | |
| 577 | + if (is_string($value)) { | |
| 578 | + if (strlen($value) > 40) { | |
| 579 | + return "'".substr($value, 0, 37)."...'"; | |
| 580 | + } | |
| 581 | + | |
| 582 | + return "'{$value}'"; | |
| 583 | + } | |
| 584 | + | |
| 585 | + return (string)$value; | |
| 586 | + } | |
| 587 | + | |
| 588 | + /** | |
| 589 | + * テストの失敗を記録する | |
| 590 | + * | |
| 591 | + * @param string $message 失敗メッセージ | |
| 592 | + * @return void | |
| 593 | + */ | |
| 594 | + private function recordFailure(string $message): void { | |
| 595 | + $this->failures[] = [ | |
| 596 | + 'testCase' => $this->currentTestCase, | |
| 597 | + 'test' => $this->currentTest, | |
| 598 | + 'message' => $message, | |
| 599 | + ]; | |
| 600 | + } | |
| 601 | + | |
| 602 | + /** | |
| 603 | + * テストのエラーを記録する | |
| 604 | + * | |
| 605 | + * @param string $message エラーメッセージ | |
| 606 | + * @param string|null $trace スタックトレース | |
| 607 | + * @return void | |
| 608 | + */ | |
| 609 | + private function recordError(string $message, ?string $trace = null): void { | |
| 610 | + $this->errors[] = [ | |
| 611 | + 'testCase' => $this->currentTestCase, | |
| 612 | + 'test' => $this->currentTest, | |
| 613 | + 'message' => $message, | |
| 614 | + 'trace' => $trace, | |
| 615 | + ]; | |
| 616 | + } | |
| 617 | +} |
| @@ -0,0 +1,79 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Test; | |
| 3 | + | |
| 4 | +require_once __DIR__.'/../../../autoload.php'; | |
| 5 | + | |
| 6 | +use Std\Lib\Tester; | |
| 7 | +use Std\Lib\Activitypub; | |
| 8 | +use Site\Controller\BlogPost; | |
| 9 | + | |
| 10 | +if (ACTIVITYPUB_ENABLED) { | |
| 11 | + $test = new Tester([ | |
| 12 | + 'colorOutput' => true, | |
| 13 | + 'verboseOutput' => true | |
| 14 | + ]); | |
| 15 | + | |
| 16 | + /* | |
| 17 | + postInbox(array $activity): void | |
| 18 | + update(): string | |
| 19 | + sendActorUpdate(array $params): void | |
| 20 | + */ | |
| 21 | + | |
| 22 | + $test->describe('ActivityPub情報の受け取り', function($test): void { | |
| 23 | + $test->it('このインスタンスのユーザーを受け取れるはず', function($test): void { | |
| 24 | + $ap = new Activitypub(); | |
| 25 | + $res = $ap->getActor(); | |
| 26 | + $test->assertNotNull($res); | |
| 27 | + $json = json_decode($res, true); | |
| 28 | + | |
| 29 | + $expect = 'Person'; | |
| 30 | + $test->assertEquals($json['type'], $expect); | |
| 31 | + | |
| 32 | + $expect = 'http://127.0.0.1:8000'; | |
| 33 | + $test->assertEquals($json['url'], $expect); | |
| 34 | + | |
| 35 | + $expect = 'http://127.0.0.1:8000/ap/actor'; | |
| 36 | + $test->assertEquals($json['id'], $expect); | |
| 37 | + | |
| 38 | + $expect = 'http://127.0.0.1:8000/ap/inbox'; | |
| 39 | + $test->assertEquals($json['inbox'], $expect); | |
| 40 | + | |
| 41 | + $expect = 'http://127.0.0.1:8000/ap/outbox'; | |
| 42 | + $test->assertEquals($json['outbox'], $expect); | |
| 43 | + | |
| 44 | + $expect = 'http://127.0.0.1:8000/ap/followers'; | |
| 45 | + $test->assertEquals($json['followers'], $expect); | |
| 46 | + | |
| 47 | + $expect = 'http://127.0.0.1:8000/ap/following'; | |
| 48 | + $test->assertEquals($json['following'], $expect); | |
| 49 | + }); | |
| 50 | + | |
| 51 | + $test->it('アクティビティを受け取れるはず', function($test): void { | |
| 52 | + $blog = new BlogPost; | |
| 53 | + $posts = $blog->getPosts('/blog/', null); | |
| 54 | + $ap = new Activitypub($posts); | |
| 55 | + $res = $ap->getActivity('a8c04518-4181-4ec6-9ef0-3f88f23b84b6'); // /blog/feature-test | |
| 56 | + $test->assertNotNull($res); | |
| 57 | + $json = json_decode($res, true); | |
| 58 | + | |
| 59 | + $expect = 'Create'; | |
| 60 | + $test->assertEquals($json['type'], $expect); | |
| 61 | + | |
| 62 | + $expect = '【マークダウン】関数'; | |
| 63 | + $test->assertEquals($json['object']['name'], $expect); | |
| 64 | + | |
| 65 | + $expect = 'https://www.w3.org/ns/activitystreams#Public'; | |
| 66 | + $test->assertEquals($json['object']['to'][0], $expect); | |
| 67 | + | |
| 68 | + $expect = 'zBbw7K7L1dEwr6htZX5JevRv3oLtHwXQU4o5jT8jn9RKF'; | |
| 69 | + $test->assertEquals($json['attachment'][0]['digestMultibase'], $expect); | |
| 70 | + | |
| 71 | + $expect = 'http://127.0.0.1:8000/static/article/o_53803618dc1691.28179609-orig.jpg'; | |
| 72 | + $test->assertEquals($json['attachment'][0]['url'], $expect); | |
| 73 | + }); | |
| 74 | + }); | |
| 75 | + | |
| 76 | + // TODO: 未完了 | |
| 77 | + | |
| 78 | + $test->printSummary(); | |
| 79 | +} |
| @@ -0,0 +1,229 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Test; | |
| 3 | + | |
| 4 | +require_once __DIR__.'/../../../autoload.php'; | |
| 5 | + | |
| 6 | +use Std\Lib\Tester; | |
| 7 | +use Std\Lib\Auth; | |
| 8 | + | |
| 9 | +if (AUTH_ENABLED) { | |
| 10 | + $test = new Tester([ | |
| 11 | + 'colorOutput' => true, | |
| 12 | + 'verboseOutput' => true | |
| 13 | + ]); | |
| 14 | + | |
| 15 | + if (AUTH_REGISTER_ENABLED) { | |
| 16 | + $test->describe('ユーザー登録', function($test): void { | |
| 17 | + $test->it('アカウントを作成出来るはず', function($test): void { | |
| 18 | + $user = "admin1234"; | |
| 19 | + $pass = "testTEST1234!#$%@[]:;"; | |
| 20 | + $passVerify = "testTEST1234!#$%@[]:;"; | |
| 21 | + $email = "test@076.co.jp"; | |
| 22 | + | |
| 23 | + $auth = new Auth(); | |
| 24 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 25 | + | |
| 26 | + $expect = true; | |
| 27 | + | |
| 28 | + $test->assertNotNull($res); | |
| 29 | + $test->assertEquals($res->isSuccess, $expect); | |
| 30 | + }); | |
| 31 | + | |
| 32 | + $test->it('空フィールドでアカウントを作成出来ないはず', function($test): void { | |
| 33 | + $user = ""; | |
| 34 | + $pass = "testTEST1234!#$%@[]:;"; | |
| 35 | + $passVerify = "testTEST1234!#$%@[]:;"; | |
| 36 | + $email = "test@076.ne.jp"; | |
| 37 | + | |
| 38 | + $auth = new Auth(); | |
| 39 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 40 | + | |
| 41 | + $expect = 'エラー:ユーザー名をご入力下さい。'.'<br />'; | |
| 42 | + | |
| 43 | + $test->assertNotNull($res); | |
| 44 | + $test->assertEquals($res->message, $expect); | |
| 45 | + | |
| 46 | + $user = "admin5678"; | |
| 47 | + $pass = ""; | |
| 48 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 49 | + | |
| 50 | + $expect = 'エラー:パスワードをご入力下さい。'.'<br />'; | |
| 51 | + | |
| 52 | + $test->assertNotNull($res); | |
| 53 | + $test->assertEquals($res->message, $expect); | |
| 54 | + | |
| 55 | + $pass = "testTEST1234!#$%@[]:;"; | |
| 56 | + $email = ""; | |
| 57 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 58 | + | |
| 59 | + $expect = 'エラー:メールアドレスをご入力下さい。'.'<br />'; | |
| 60 | + | |
| 61 | + $test->assertNotNull($res); | |
| 62 | + $test->assertEquals($res->message, $expect); | |
| 63 | + }); | |
| 64 | + | |
| 65 | + $test->it('既にユーザー名やメールアドレスが存在するアカウントを作成出来ないはず', function($test): void { | |
| 66 | + $user = "admin1234"; | |
| 67 | + $pass = "testTEST1234!#$%@[]:;"; | |
| 68 | + $passVerify = "testTEST1234!#$%@[]:;"; | |
| 69 | + $email = "test@076.ne.jp"; | |
| 70 | + | |
| 71 | + $auth = new Auth(); | |
| 72 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 73 | + | |
| 74 | + $expect = 'ユーザー「admin1234」は既に存在します。'.'<br />'; | |
| 75 | + | |
| 76 | + $test->assertNotNull($res); | |
| 77 | + $test->assertEquals($res->message, $expect); | |
| 78 | + | |
| 79 | + $user = "admin5678"; | |
| 80 | + $email = "test@076.co.jp"; | |
| 81 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 82 | + | |
| 83 | + $expect = 'ユーザー「test@076.co.jp」は既に存在します。'.'<br />'; | |
| 84 | + | |
| 85 | + $test->assertNotNull($res); | |
| 86 | + $test->assertEquals($res->message, $expect); | |
| 87 | + }); | |
| 88 | + | |
| 89 | + $test->it('一致しない、短い、又は不安なパスワードでアカウントを作成出来ないはず', function($test): void { | |
| 90 | + $user = "admin5678"; | |
| 91 | + $pass = "testtest1234!#$%@[]:;"; | |
| 92 | + $passVerify = "testTEST1234!#$%@[]:;"; | |
| 93 | + $email = "test@076.ne.jp"; | |
| 94 | + | |
| 95 | + $auth = new Auth(); | |
| 96 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 97 | + | |
| 98 | + $expect = 'エラー:パスワードは一致していません。'.'<br />'; | |
| 99 | + | |
| 100 | + $test->assertNotNull($res); | |
| 101 | + $test->assertEquals($res->message, $expect); | |
| 102 | + | |
| 103 | + $pass = "testTEST1234"; | |
| 104 | + $passVerify = "testTEST1234"; | |
| 105 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 106 | + | |
| 107 | + $expect = 'エラー:パスワードは20以上をご入力下さい。'.'<br />'; | |
| 108 | + | |
| 109 | + $test->assertNotNull($res); | |
| 110 | + $test->assertEquals($res->message, $expect); | |
| 111 | + | |
| 112 | + $pass = "testTEST1234testTEST"; | |
| 113 | + $passVerify = "testTEST1234testTEST"; | |
| 114 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 115 | + | |
| 116 | + $expect = 'エラー:パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。'.'<br />'; | |
| 117 | + | |
| 118 | + $test->assertNotNull($res); | |
| 119 | + $test->assertEquals($res->message, $expect); | |
| 120 | + }); | |
| 121 | + | |
| 122 | + $test->it('不正なメールアドレスでアカウントを作成出来ないはず', function($test): void { | |
| 123 | + $user = "admin5678"; | |
| 124 | + $pass = "testTEST1234!#$%@[]:;"; | |
| 125 | + $passVerify = "testTEST1234!#$%@[]:;"; | |
| 126 | + $email = "test@076.niggerballs"; | |
| 127 | + | |
| 128 | + $auth = new Auth(); | |
| 129 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 130 | + | |
| 131 | + $expect = 'エラー:メールアドレスは不正です。'.'<br />'; | |
| 132 | + | |
| 133 | + $test->assertNotNull($res); | |
| 134 | + $test->assertEquals($res->message, $expect); | |
| 135 | + | |
| 136 | + $email = "test@076"; | |
| 137 | + $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 138 | + | |
| 139 | + $expect = 'エラー:メールアドレスは不正です。'.'<br />'; | |
| 140 | + | |
| 141 | + $test->assertNotNull($res); | |
| 142 | + $test->assertEquals($res->message, $expect); | |
| 143 | + }); | |
| 144 | + | |
| 145 | + // ユーザーの削除 | |
| 146 | + $users = scandir(ROOT.'/data/user'); | |
| 147 | + $userFile = ''; | |
| 148 | + foreach ($users as $u) { | |
| 149 | + if (str_contains($u, 'admin1234')) { | |
| 150 | + $userFile = $u; | |
| 151 | + break; | |
| 152 | + } | |
| 153 | + } | |
| 154 | + unlink(ROOT.'/data/user/'.$userFile); | |
| 155 | + rmdir(ROOT.'/public/static/user/admin1234'); | |
| 156 | + }); | |
| 157 | + } | |
| 158 | + | |
| 159 | + // TODO: CLIからクッキーを設定出来ないから、「Lib\Auth::getUserData(): Return value must be of type stdClass, null returned」って発生する・・・ | |
| 160 | + // $test->describe('ログインとログアウト', function($test): void { | |
| 161 | + // $user = "admin1234"; | |
| 162 | + // $pass = "testTEST1234!#$%@[]:;"; | |
| 163 | + // $passVerify = "testTEST1234!#$%@[]:;"; | |
| 164 | + // $email = "test@076.co.jp"; | |
| 165 | + | |
| 166 | + // $auth = new Auth(); | |
| 167 | + // $res = $auth->mkUser($user, $pass, $passVerify, $email); | |
| 168 | + // unset($auth); | |
| 169 | + | |
| 170 | + // $test->it('正しい認証情報でログイン出来るはず', function($test) use ($user, $pass): void { | |
| 171 | + // $auth = new Auth(); | |
| 172 | + // $res = $auth->setToken($user, $pass); | |
| 173 | + | |
| 174 | + // $test->assertTrue($res->isSuccess); | |
| 175 | + // $test->assertEquals('ログイン成功', $res->message); | |
| 176 | + | |
| 177 | + // $auth2 = new Auth(); | |
| 178 | + // $usr = $auth2->getLoggedInUser(); | |
| 179 | + | |
| 180 | + // $test->assertNotNull($usr); | |
| 181 | + // $test->assertEquals($user, $usr->username); | |
| 182 | + // }); | |
| 183 | + | |
| 184 | + // $test->it('間違ったパスワードでログイン出来ないはず', function($test) use ($user): void { | |
| 185 | + // $auth = new Auth(); | |
| 186 | + // $res = $auth->setToken($user, 'fuseipassword'); | |
| 187 | + | |
| 188 | + // $test->assertFalse($res->isSuccess); | |
| 189 | + // $test->assertEquals('ユーザー名又はパスワードが一致していません', $res->message); | |
| 190 | + // }); | |
| 191 | + | |
| 192 | + // $test->it('存在しないユーザーでログイン出来ないはず', function($test) use ($pass): void { | |
| 193 | + // $auth = new Auth(); | |
| 194 | + // $res = $auth->setToken('fumeiuser', $pass); | |
| 195 | + | |
| 196 | + // $test->assertFalse($res->isSuccess); | |
| 197 | + // $test->assertEquals('ユーザー名又はパスワードが一致していません', $res->message); | |
| 198 | + // }); | |
| 199 | + | |
| 200 | + // $test->it('ログアウト出来るはず', function($test) use ($user, $pass): void { | |
| 201 | + // $auth = new Auth(); | |
| 202 | + // $auth->setToken($user, $pass); | |
| 203 | + | |
| 204 | + // $res = $auth->logout(); | |
| 205 | + | |
| 206 | + // $test->assertTrue($res->isSuccess); | |
| 207 | + // $test->assertEquals('ログイン成功', $res->message); | |
| 208 | + | |
| 209 | + // $auth2 = new Auth(); | |
| 210 | + // $usr = $auth2->getLoggedInUser(); | |
| 211 | + | |
| 212 | + // $test->assertNull($usr); | |
| 213 | + // }); | |
| 214 | + | |
| 215 | + // // ユーザーの削除 | |
| 216 | + // $users = scandir(ROOT.'/data/user'); | |
| 217 | + // $userFile = ''; | |
| 218 | + // foreach ($users as $u) { | |
| 219 | + // if (str_contains($u, 'admin1234')) { | |
| 220 | + // $userFile = $u; | |
| 221 | + // break; | |
| 222 | + // } | |
| 223 | + // } | |
| 224 | + // unlink(ROOT.'/data/user/'.$userFile); | |
| 225 | + // rmdir(ROOT.'/public/static/user/admin1234'); | |
| 226 | + // }); | |
| 227 | + | |
| 228 | + $test->printSummary(); | |
| 229 | +} |
| @@ -0,0 +1,94 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Test; | |
| 3 | + | |
| 4 | +require_once __DIR__.'/../../../autoload.php'; | |
| 5 | + | |
| 6 | +use Std\Lib\Tester; | |
| 7 | +use Std\Lib\Curl; | |
| 8 | + | |
| 9 | +if (CURL_ENABLED) { | |
| 10 | + $test = new Tester([ | |
| 11 | + 'colorOutput' => true, | |
| 12 | + 'verboseOutput' => true | |
| 13 | + ]); | |
| 14 | + | |
| 15 | + $test->describe('Curlの基本的な機能性', function($test): void { | |
| 16 | + $test->it('URLで使って新しいインスタンスを作成するはず', function($test): void { | |
| 17 | + $curl = new Curl('https://076.moe'); | |
| 18 | + $test->assertNotNull($curl); | |
| 19 | + }); | |
| 20 | + | |
| 21 | + $test->it('メソッドでURLを設定出来るはす', function($test): void { | |
| 22 | + $curl = new Curl(); | |
| 23 | + $curl->setUrl('https://076.moe'); | |
| 24 | + $test->assertNotNull($curl); | |
| 25 | + }); | |
| 26 | + | |
| 27 | + $test->it('ヘッダー文字を作成出来るはず', function($test): void { | |
| 28 | + $curl = new Curl('https://076.moe'); | |
| 29 | + $curl->setHeaders([ | |
| 30 | + 'Accept' => 'application/json', | |
| 31 | + 'User-Agent' => 'LoliTest/1.0' | |
| 32 | + ]); | |
| 33 | + | |
| 34 | + $reflectionClass = new \ReflectionClass($curl); | |
| 35 | + $method = $reflectionClass->getMethod('buildHeaderString'); | |
| 36 | + $method->setAccessible(true); | |
| 37 | + | |
| 38 | + $headerString = $method->invoke($curl); | |
| 39 | + $test->assertStringContains('Accept: application/json', $headerString); | |
| 40 | + $test->assertStringContains('User-Agent: LoliTest/1.0', $headerString); | |
| 41 | + }); | |
| 42 | + | |
| 43 | + $test->skip('移転を対応するはず', '作成中・・・'); | |
| 44 | + | |
| 45 | + $test->it('メソッドチェーニングを対応するはず', function($test): void { | |
| 46 | + $curl = new Curl(); | |
| 47 | + $result = $curl->setUrl('https://076.moe') | |
| 48 | + ->setMethod('GET') | |
| 49 | + ->setTimeout(30); | |
| 50 | + | |
| 51 | + $test->assertSame($curl, $result); | |
| 52 | + }); | |
| 53 | + }); | |
| 54 | + | |
| 55 | + $test->describe('Curl HTTP リクエスト', function($test): void { | |
| 56 | + $networkAvailable = true; | |
| 57 | + | |
| 58 | + if (!$networkAvailable) { | |
| 59 | + $test->skip('076.moeでGETリクエストの確認', 'ネットワークが無効です。'); | |
| 60 | + $test->skip('postman-echo.comでPOSTリクエストの確認', 'ネットワークが無効です。'); | |
| 61 | + return; | |
| 62 | + } | |
| 63 | + | |
| 64 | + $test->it('076.moeでGETリクエストの確認', function($test): void { | |
| 65 | + $curl = new Curl('https://076.moe'); | |
| 66 | + $result = $curl->execute(); | |
| 67 | + | |
| 68 | + $test->assertTrue($result->isSuccess); | |
| 69 | + $test->assertEquals(200, $curl->getResponseCode()); | |
| 70 | + $test->assertNotNull($curl->getResponseBody()); | |
| 71 | + $test->assertStringContains('<html', $curl->getResponseBody()); | |
| 72 | + }); | |
| 73 | + | |
| 74 | + $test->it('postman-echo.comでPOSTリクエストの確認', function($test): void { | |
| 75 | + $curl = new Curl(); | |
| 76 | + $curl->setUrl('https://postman-echo.com/post') | |
| 77 | + ->setMethod('POST') | |
| 78 | + ->setPostFields([ | |
| 79 | + 'name' => '山田太郎', | |
| 80 | + 'email' => 't.yamada@example.com' | |
| 81 | + ]); | |
| 82 | + | |
| 83 | + $result = $curl->execute(); | |
| 84 | + $test->assertTrue($result->isSuccess); | |
| 85 | + $test->assertEquals(200, $curl->getResponseCode()); | |
| 86 | + | |
| 87 | + $responseBody = $curl->getResponseBody(); | |
| 88 | + $test->assertStringContains('山田太郎', $responseBody); | |
| 89 | + $test->assertStringContains('t.yamada@example.com', $responseBody); | |
| 90 | + }); | |
| 91 | + }); | |
| 92 | + | |
| 93 | + $test->printSummary(); | |
| 94 | +} |
| @@ -0,0 +1,234 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Test; | |
| 3 | + | |
| 4 | +require_once __DIR__.'/../../../autoload.php'; | |
| 5 | + | |
| 6 | +use Std\Lib\Tester; | |
| 7 | +use Std\Lib\Markdown; | |
| 8 | + | |
| 9 | +$test = new Tester([ | |
| 10 | + 'colorOutput' => true, | |
| 11 | + 'verboseOutput' => true | |
| 12 | +]); | |
| 13 | + | |
| 14 | +function mkfile(string $body): void { | |
| 15 | + $post = fopen(ROOT."/blog/dietrannynigger.md", "w"); | |
| 16 | + fwrite($post, "title: 【】\n"); | |
| 17 | + fwrite($post, "uuid: ".uuid()."\n"); | |
| 18 | + fwrite($post, "author: 諏訪子\n"); | |
| 19 | + fwrite($post, "date: ".date("Y-m-d H:i:s")."\n"); | |
| 20 | + fwrite($post, "thumbnail: \n"); | |
| 21 | + fwrite($post, "thumborient: center\n"); | |
| 22 | + fwrite($post, "category: \n"); | |
| 23 | + fwrite($post, "----\n"); | |
| 24 | + fwrite($post, $body); | |
| 25 | + fclose($post); | |
| 26 | +} | |
| 27 | + | |
| 28 | +function rmfile(): void { | |
| 29 | + unlink(ROOT."/blog/dietrannynigger.md"); | |
| 30 | +} | |
| 31 | + | |
| 32 | +class Constants { | |
| 33 | + public const START = "<p> "; | |
| 34 | + public const END = " </p>"; | |
| 35 | +} | |
| 36 | + | |
| 37 | +$test->describe('マークダウン', function($test): void { | |
| 38 | + | |
| 39 | + $test->it('メタデータをパーシング出来るはず', function($test): void { | |
| 40 | + $md = new Markdown('feature-test', '/blog/'); | |
| 41 | + $test->assertNotNull($md); | |
| 42 | + | |
| 43 | + $meta = $md->getMetadata(); | |
| 44 | + $test->assertNotNull($meta); | |
| 45 | + | |
| 46 | + $expect = new \stdClass; | |
| 47 | + $expect->title = '【マークダウン】関数'; | |
| 48 | + $expect->uuid = 'a8c04518-4181-4ec6-9ef0-3f88f23b84b6'; | |
| 49 | + $expect->author = '諏訪子'; | |
| 50 | + $expect->date = '2025-11-07 20:33:42'; | |
| 51 | + $expect->thumbnail = 'o_53803618dc1691.28179609-orig.jpg'; | |
| 52 | + $expect->thumborient = 'center'; | |
| 53 | + $expect->category = ['test', 'おちんちん', 'でっかいおっぱい']; | |
| 54 | + $expect->css = ['code', 'blockquote', 'table', 'blink']; | |
| 55 | + | |
| 56 | + $test->assertEquals($expect, $meta); | |
| 57 | + }); | |
| 58 | + | |
| 59 | + $test->it('横線はHTMLに交換出来るはず', function($test): void { | |
| 60 | + mkfile('----'); | |
| 61 | + | |
| 62 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 63 | + $test->assertNotNull($md); | |
| 64 | + | |
| 65 | + $body = $md->parse(); | |
| 66 | + $test->assertNotNull($body); | |
| 67 | + | |
| 68 | + $expect = "<hr />"; | |
| 69 | + $actual = str_replace("\n", '', trim($body)); | |
| 70 | + | |
| 71 | + $test->assertEquals($expect, $actual); | |
| 72 | + }); | |
| 73 | + | |
| 74 | + $test->it('ヘッダーはHTMLに交換出来るはず', function($test): void { | |
| 75 | + mkfile('# ケロケロ'); | |
| 76 | + | |
| 77 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 78 | + $test->assertNotNull($md); | |
| 79 | + | |
| 80 | + $body = $md->parse(); | |
| 81 | + $test->assertNotNull($body); | |
| 82 | + | |
| 83 | + $expect = "<h1 id=\"section-1\">ケロケロ</h1>"; | |
| 84 | + $actual = trim($body); | |
| 85 | + | |
| 86 | + $test->assertEquals($expect, $actual); | |
| 87 | + | |
| 88 | + // --------------- | |
| 89 | + | |
| 90 | + mkfile('## ケロケロ'); | |
| 91 | + | |
| 92 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 93 | + $test->assertNotNull($md); | |
| 94 | + | |
| 95 | + $body = $md->parse(); | |
| 96 | + $test->assertNotNull($body); | |
| 97 | + | |
| 98 | + $expect = "<h2 id=\"section-1\">ケロケロ</h2>"; | |
| 99 | + $actual = trim($body); | |
| 100 | + | |
| 101 | + $test->assertEquals($expect, $actual); | |
| 102 | + | |
| 103 | + // --------------- | |
| 104 | + | |
| 105 | + mkfile('### ケロケロ'); | |
| 106 | + | |
| 107 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 108 | + $test->assertNotNull($md); | |
| 109 | + | |
| 110 | + $body = $md->parse(); | |
| 111 | + $test->assertNotNull($body); | |
| 112 | + | |
| 113 | + $expect = "<h3 id=\"section-1\">ケロケロ</h3>"; | |
| 114 | + $actual = trim($body); | |
| 115 | + | |
| 116 | + $test->assertEquals($expect, $actual); | |
| 117 | + | |
| 118 | + // --------------- | |
| 119 | + | |
| 120 | + mkfile('#### ケロケロ'); | |
| 121 | + | |
| 122 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 123 | + $test->assertNotNull($md); | |
| 124 | + | |
| 125 | + $body = $md->parse(); | |
| 126 | + $test->assertNotNull($body); | |
| 127 | + | |
| 128 | + $expect = "<h4 id=\"section-1\">ケロケロ</h4>"; | |
| 129 | + $actual = trim($body); | |
| 130 | + | |
| 131 | + $test->assertEquals($expect, $actual); | |
| 132 | + | |
| 133 | + // --------------- | |
| 134 | + | |
| 135 | + mkfile('##### ケロケロ'); | |
| 136 | + | |
| 137 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 138 | + $test->assertNotNull($md); | |
| 139 | + | |
| 140 | + $body = $md->parse(); | |
| 141 | + $test->assertNotNull($body); | |
| 142 | + | |
| 143 | + $expect = "<h5 id=\"section-1\">ケロケロ</h5>"; | |
| 144 | + $actual = trim($body); | |
| 145 | + | |
| 146 | + $test->assertEquals($expect, $actual); | |
| 147 | + | |
| 148 | + // --------------- | |
| 149 | + | |
| 150 | + mkfile('###### ケロケロ'); | |
| 151 | + | |
| 152 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 153 | + $test->assertNotNull($md); | |
| 154 | + | |
| 155 | + $body = $md->parse(); | |
| 156 | + $test->assertNotNull($body); | |
| 157 | + | |
| 158 | + $expect = "<h6 id=\"section-1\">ケロケロ</h6>"; | |
| 159 | + $actual = trim($body); | |
| 160 | + | |
| 161 | + $test->assertEquals($expect, $actual); | |
| 162 | + }); | |
| 163 | + | |
| 164 | + $test->skip('コメントはHTMLに交換出来るはず', '作成中・・・'); | |
| 165 | + | |
| 166 | + $test->it('画像、音楽、動画はHTMLに交換出来るはず', function($test): void { | |
| 167 | + mkfile("\n | |
| 168 | +\n | |
| 169 | +\n | |
| 170 | +$[audio/ogg](https://ass.technicalsuwako.moe/砕月着信音.ogg)\n | |
| 171 | +#[video/mp4](https://ass.technicalsuwako.moe/フリーズ.mp4)"); | |
| 172 | + | |
| 173 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 174 | + $test->assertNotNull($md); | |
| 175 | + | |
| 176 | + $body = $md->parse(); | |
| 177 | + $test->assertNotNull($body); | |
| 178 | + | |
| 179 | + $expect = '<p><img style="width: 100%; " src="https://ass.technicalsuwako.moe/banner.png" alt="" /> </p> ' | |
| 180 | + . '<p><img style="width: 100%; " src="https://ass.technicalsuwako.moe/banner.png" alt="ケロケロ" /> </p> ' | |
| 181 | + . '<p><img style="width: 100%; max-width: 200px;" src="https://ass.technicalsuwako.moe/banner.png" alt="ケロケロ" /> </p> ' | |
| 182 | + . '<p><audio controls><source src="https://ass.technicalsuwako.moe/砕月着信音.ogg" type="audio/ogg" /></audio> </p> ' | |
| 183 | + . '<p><video style="max-width: 100%;" controls><source src="https://ass.technicalsuwako.moe/フリーズ.mp4" type="video/mp4" /></video> </p>'; | |
| 184 | + $actual = str_replace("\n", '', str_replace(' ', '', trim($body))); | |
| 185 | + | |
| 186 | + $test->assertEquals($expect, $actual); | |
| 187 | + }); | |
| 188 | + | |
| 189 | + $test->it('基本的なマークダウンはHTMLに交換出来るはず', function($test): void { | |
| 190 | + mkfile("**太文字**\n | |
| 191 | +*イタリック*\n | |
| 192 | +_下線_\n | |
| 193 | +~取消線~\n | |
| 194 | +!:(5)明滅文字:!\n | |
| 195 | +^(72)フォントサイズ^\n | |
| 196 | +%(ff00ff)フォントカラー%"); | |
| 197 | + | |
| 198 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 199 | + $test->assertNotNull($md); | |
| 200 | + | |
| 201 | + $body = $md->parse(); | |
| 202 | + $test->assertNotNull($body); | |
| 203 | + | |
| 204 | + $expect = '<p> <strong>太文字</strong> </p> ' | |
| 205 | + . '<p> <em>イタリック</em> </p> ' | |
| 206 | + . '<p> <u>下線</u> </p> ' | |
| 207 | + . '<p> <s>取消線</s> </p> ' | |
| 208 | + . '<p> <span class="blink" style="animation-duration: 5s;">明滅文字</span> </p> ' | |
| 209 | + . '<p> <span style="font-size: 72px;">フォントサイズ</span> </p> ' | |
| 210 | + . '<p> <span style="color: #ff00ff;">フォントカラー</span> </p>'; | |
| 211 | + $actual = str_replace("\n", '', trim($body)); | |
| 212 | + | |
| 213 | + $test->assertEquals($expect, $actual); | |
| 214 | + }); | |
| 215 | + | |
| 216 | + $test->it('複数マークダウンはHTMLに交換出来るはず', function($test): void { | |
| 217 | + mkfile("[^(500)<God>(すわこ)](https://techsuwako.jp/)^"); | |
| 218 | + | |
| 219 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 220 | + $test->assertNotNull($md); | |
| 221 | + | |
| 222 | + $body = $md->parse(); | |
| 223 | + $test->assertNotNull($body); | |
| 224 | + | |
| 225 | + $expect = Constants::START.'<a href="https://techsuwako.jp/"><span style="font-size: 500px;"><ruby>God<rp>(</rp><rt>すわこ</rt><rp>)</rp></ruby></a></span>'.Constants::END; | |
| 226 | + $actual = str_replace("\n", '', trim($body)); | |
| 227 | + | |
| 228 | + $test->assertEquals($expect, $actual); | |
| 229 | + }); | |
| 230 | + | |
| 231 | + rmfile(); | |
| 232 | +}); | |
| 233 | + | |
| 234 | +$test->printSummary(); |
| @@ -0,0 +1,38 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Test; | |
| 3 | + | |
| 4 | +require_once __DIR__.'/../../../autoload.php'; | |
| 5 | + | |
| 6 | +use Std\Lib\Tester; | |
| 7 | +use Std\Lib\Route; | |
| 8 | + | |
| 9 | +$test = new Tester([ | |
| 10 | + 'colorOutput' => true, | |
| 11 | + 'verboseOutput' => true | |
| 12 | +]); | |
| 13 | + | |
| 14 | +$test->describe('ルート', function($test): void { | |
| 15 | + $test->it('ルートに追加出来るはず', function($test): void { | |
| 16 | + class Webpage { | |
| 17 | + public function about(array $params): void {} | |
| 18 | + } | |
| 19 | + | |
| 20 | + $routes = [ | |
| 21 | + Route::add('GET', 'about', Webpage::class.'@about'), | |
| 22 | + ]; | |
| 23 | + | |
| 24 | + $expect = [ | |
| 25 | + [ | |
| 26 | + 'method' => 'GET', | |
| 27 | + 'path' => 'about', | |
| 28 | + 'class' => 'Std\Test\Webpage@about', | |
| 29 | + 'params' => [], | |
| 30 | + ] | |
| 31 | + ]; | |
| 32 | + | |
| 33 | + $test->assertNotNull($routes); | |
| 34 | + $test->assertEquals($expect, $routes); | |
| 35 | + }); | |
| 36 | +}); | |
| 37 | + | |
| 38 | +$test->printSummary(); |
変更
tester.php
| @@ -8,15 +8,17 @@ | ||
| 8 | 8 | mb_internal_encoding('UTF-8'); |
| 9 | 9 | header('Content-Type: text/plain; charset=UTF-8'); |
| 10 | 10 | |
| 11 | -$testDir = ROOT.'/src/Site/Test'; | |
| 12 | -$testFiles = glob($testDir.'/*.php'); | |
| 11 | +$testDirs = [ROOT.'/src/Site/Test', ROOT.'/src/Std/Test']; | |
| 12 | +$testFiles = []; | |
| 13 | +foreach ($testDirs as $d) $testFiles[] = glob($d.'/*.php'); | |
| 14 | +$testFiles = array_merge(...$testFiles); | |
| 13 | 15 | |
| 14 | 16 | if (empty($testFiles)) { |
| 15 | - echo "テストファイルは{$testDir}にありません\n"; | |
| 17 | + echo "テストファイルではありません\n"; | |
| 16 | 18 | exit(1); |
| 17 | 19 | } |
| 18 | 20 | |
| 19 | -echo "{$testDir}にある".count($testFiles)."個テストファイルを実行中:\n"; | |
| 21 | +echo count($testFiles)."個テストファイルを実行中:\n"; | |
| 20 | 22 | echo "------------------------------------------------\n"; |
| 21 | 23 | |
| 22 | 24 | $totalFiles = 0; |
変更
util.php
| @@ -209,29 +209,28 @@ | ||
| 209 | 209 | } |
| 210 | 210 | |
| 211 | 211 | function logger(LogType $section, mixed $arg): void { |
| 212 | - if (LOGGING_ENABLED) { | |
| 213 | - $success = false; | |
| 214 | - $logfile = ROOT.'/log/'; | |
| 212 | + if (!LOGGING_ENABLED) return; | |
| 213 | + $success = false; | |
| 214 | + $logfile = ROOT.'/log/'; | |
| 215 | 215 | |
| 216 | - switch ($section) { | |
| 217 | - case LogType::ActivityPub: | |
| 218 | - $logfile .= 'ap_log.text'; | |
| 219 | - $success = true; | |
| 220 | - break; | |
| 221 | - case LogType::Auth: | |
| 222 | - $logfile .= 'auth_log.text'; | |
| 223 | - $success = true; | |
| 224 | - break; | |
| 225 | - case LogType::Mailer: | |
| 226 | - $logfile .= 'mail_log.text'; | |
| 227 | - $success = true; | |
| 228 | - break; | |
| 229 | - default: | |
| 230 | - break; | |
| 231 | - } | |
| 232 | - | |
| 233 | - if ($success) file_put_contents($logfile, $arg."\n", FILE_APPEND); | |
| 216 | + switch ($section) { | |
| 217 | + case LogType::ActivityPub: | |
| 218 | + $logfile .= 'ap_log.text'; | |
| 219 | + $success = true; | |
| 220 | + break; | |
| 221 | + case LogType::Auth: | |
| 222 | + $logfile .= 'auth_log.text'; | |
| 223 | + $success = true; | |
| 224 | + break; | |
| 225 | + case LogType::Mailer: | |
| 226 | + $logfile .= 'mail_log.text'; | |
| 227 | + $success = true; | |
| 228 | + break; | |
| 229 | + default: | |
| 230 | + break; | |
| 234 | 231 | } |
| 232 | + | |
| 233 | + if ($success) file_put_contents($logfile, $arg."\n", FILE_APPEND); | |
| 235 | 234 | } |
| 236 | 235 | |
| 237 | 236 | function to_money($amount, $lang) { |
| @@ -385,8 +384,8 @@ | ||
| 385 | 384 | return $len == $sum; |
| 386 | 385 | } |
| 387 | 386 | |
| 388 | -function getImageInfo(string $url): \Site\Lib\Image { | |
| 389 | - $img = new \Site\Lib\Image($url); | |
| 387 | +function getImageInfo(string $url): \Std\Lib\Image { | |
| 388 | + $img = new \Std\Lib\Image($url); | |
| 390 | 389 | return $img; |
| 391 | 390 | } |
| 392 | 391 |