Merge pull request 'Syntax' (#15) from syntax into master
Reviewed-on: http://192.168.10.104:3000/suwako/LittleBeast/pulls/15
suwako <suwako@076.moe>、2026-11-10 18:36
コミット de8b809bbb6f0b8f20b7766848be8d91dd3d7f6f
・親 6287571
・親 902102d
・ファイルを見る
39個のファイル、+3534 -236
追加
data/main.c
| @@ -0,0 +1,10 @@ | ||
| 1 | +#include <stdio.h> | |
| 2 | + | |
| 3 | +int main(int argc, char *argv[]) { | |
| 4 | + for (int i = 0; i < 9000; ++i) { | |
| 5 | + size_t size = 0; | |
| 6 | + puts("うんこちんこ"); // 現実:マンコ | |
| 7 | + } | |
| 8 | + | |
| 9 | + return 0; | |
| 10 | +} |
追加
data/main.s
| @@ -0,0 +1,175 @@ | ||
| 1 | +; FreeBSDで、シスコール番号を調べるには:/usr/include/sys/syscall.h | |
| 2 | +; Linuxで、シスコール番号を調べるには:/usr/include/asm-generic/unistd.h | |
| 3 | +; 下記のコードはFreeBSD向けの物です。 | |
| 4 | + | |
| 5 | +bits 64 | |
| 6 | +default rel | |
| 7 | + | |
| 8 | +section .data | |
| 9 | + | |
| 10 | +section .rodata | |
| 11 | + basename db "output-",0 | |
| 12 | + suffix db ".ppm", 0 | |
| 13 | + hdr db "P6", 10, "960 540", 10, "255", 10 | |
| 14 | + | |
| 15 | + err db "開くに失敗", 10 | |
| 16 | + errlen equ $-err | |
| 17 | + suc db "60フレームを作成しました。", 10 | |
| 18 | + suclen equ $-suc | |
| 19 | + | |
| 20 | + w equ 960 | |
| 21 | + h equ 540 | |
| 22 | + | |
| 23 | +section .bss | |
| 24 | + buf resb 960*540*3 | |
| 25 | + outfile resb 64 ; "output-nn.ppm\0" | |
| 26 | + | |
| 27 | +section .text | |
| 28 | + global _start | |
| 29 | + | |
| 30 | +_start: | |
| 31 | + mov r12, 0 ; i = 0 | |
| 32 | + | |
| 33 | +frameloop: | |
| 34 | + ; ファイル名 | |
| 35 | + lea rdi, [outfile] | |
| 36 | + lea rsi, [basename] | |
| 37 | + call strcpy | |
| 38 | + | |
| 39 | + ; iは2つの文字に変換 | |
| 40 | + mov rax, r12 | |
| 41 | + mov rcx, 10 | |
| 42 | + xor rdx, rdx | |
| 43 | + div rcx ; rax = 10, rdx = 01 | |
| 44 | + | |
| 45 | + add al, '0' | |
| 46 | + add dl, '0' | |
| 47 | + mov [rdi], al ; 10 | |
| 48 | + mov [rdi+1], dl; 01 | |
| 49 | + add rdi, 2 | |
| 50 | + | |
| 51 | + lea rsi, [suffix] | |
| 52 | + call strcpy ; 「.ppm\0」の追加 | |
| 53 | + | |
| 54 | + mov rax, 5 ; sys_open | |
| 55 | + lea rdi, [outfile] | |
| 56 | + mov rsi, 0x100601 ; 0x100601 = 0001 0000 0000 0110 0000 0001 = 1048576 + 1024 + 512 + 1 = 1050113 | |
| 57 | + ; O_WRONLY = 0x0001 | |
| 58 | + ; O_CREAT = 0x0200 | |
| 59 | + ; O_TRUNC = 0x0400 | |
| 60 | + ; O_CLOEXEC = 0x010000 | |
| 61 | + ; /usr/include/sys/fcntl.h | |
| 62 | + mov rdx, 0o666 ; chmod 666 | |
| 63 | + syscall | |
| 64 | + | |
| 65 | + test rax, rax | |
| 66 | + js error ; エラーがあれば | |
| 67 | + mov r15, rax ; ファイルディスクリプターをr15に保存 | |
| 68 | + | |
| 69 | + ; ヘッダーの書き込み | |
| 70 | + mov eax, 4 ; sys_write | |
| 71 | + mov rdi, r15 ; fd | |
| 72 | + lea rsi, [hdr] | |
| 73 | + mov rdx, 18 | |
| 74 | + syscall | |
| 75 | + | |
| 76 | + ; チェッカーボードループ | |
| 77 | + lea rdi, [buf] | |
| 78 | + mov rcx, h | |
| 79 | + xor r13, r13 | |
| 80 | + | |
| 81 | +yloop: | |
| 82 | + mov r8d, w | |
| 83 | + | |
| 84 | +xloop: | |
| 85 | + ; (x/60 + y/60) % 2の計算 | |
| 86 | + mov eax, r8d ; x | |
| 87 | + add eax, r12d ; x + i | |
| 88 | + xor edx, edx | |
| 89 | + mov ebx, 60 | |
| 90 | + div ebx ; eax = (x + i)/60 | |
| 91 | + push rax | |
| 92 | + | |
| 93 | + mov eax, r13d | |
| 94 | + add eax, r12d ; y + i | |
| 95 | + xor edx, edx | |
| 96 | + div ebx ; eax = (y + i)/60 | |
| 97 | + | |
| 98 | + pop rdx | |
| 99 | + add eax, edx ; (x + i)/60 + (y + i)/60 | |
| 100 | + and al, 1 ; % 2 | |
| 101 | + jz .black | |
| 102 | + | |
| 103 | +.green: | |
| 104 | + mov al, 0 | |
| 105 | + stosb ; R | |
| 106 | + mov al, 0xFF | |
| 107 | + stosb ; G | |
| 108 | + mov al, 0 | |
| 109 | + stosb ; B | |
| 110 | + jmp .next | |
| 111 | + | |
| 112 | +.black: | |
| 113 | + xor eax, eax | |
| 114 | + stosb | |
| 115 | + stosb | |
| 116 | + stosb | |
| 117 | + | |
| 118 | +.next: | |
| 119 | + dec r8d | |
| 120 | + jnz xloop | |
| 121 | + | |
| 122 | + inc r13d | |
| 123 | + dec rcx | |
| 124 | + jnz yloop | |
| 125 | + | |
| 126 | + ; ピクセルデータの書き込み | |
| 127 | + mov eax, 4 | |
| 128 | + mov rdi, r15 | |
| 129 | + lea rsi, [buf] | |
| 130 | + mov edx, 960*540*3 | |
| 131 | + syscall | |
| 132 | + | |
| 133 | +close: | |
| 134 | + mov rax, 6 ; sys_close | |
| 135 | + mov rdi, r15 | |
| 136 | + syscall | |
| 137 | + | |
| 138 | + inc r12 | |
| 139 | + cmp r12, 60 | |
| 140 | + jb frameloop | |
| 141 | + | |
| 142 | + mov rax, 4 | |
| 143 | + mov rdi, 1 | |
| 144 | + lea rsi, [suc] | |
| 145 | + mov rdx, suclen | |
| 146 | + syscall | |
| 147 | + | |
| 148 | +end: | |
| 149 | + mov eax, 1 ; sys_exit | |
| 150 | + xor edi, edi | |
| 151 | + syscall | |
| 152 | + | |
| 153 | +error: | |
| 154 | + mov rax, 4 | |
| 155 | + mov rdi, 2 ; stderr | |
| 156 | + lea rsi, [err] | |
| 157 | + mov rdx, errlen | |
| 158 | + syscall | |
| 159 | + | |
| 160 | + mov rax, 1 | |
| 161 | + mov rdi, 1 | |
| 162 | + syscall | |
| 163 | + | |
| 164 | +strcpy: | |
| 165 | +.loop: | |
| 166 | + mov al, [rsi] | |
| 167 | + mov [rdi], al | |
| 168 | + test al, al | |
| 169 | + jz .done | |
| 170 | + inc rdi | |
| 171 | + inc rsi | |
| 172 | + jmp .loop | |
| 173 | + | |
| 174 | +.done: | |
| 175 | + ret |
| @@ -1,3 +1,24 @@ | ||
| 1 | +math { | |
| 2 | + font-family: 'STIX Two Math', 'Cambria Math', 'Latin Modern Math', 'Noto Sans Math', math; | |
| 3 | + font-size: 1.1em; | |
| 4 | +} | |
| 5 | + | |
| 6 | +.math-display { | |
| 7 | + margin: 1em 0; | |
| 8 | + overflow-x: auto; | |
| 9 | + overflow-y: hidden; | |
| 10 | + padding: 0.25em 0; | |
| 11 | +} | |
| 12 | + | |
| 13 | +.math-display > math { | |
| 14 | + font-size: 1.2em; | |
| 15 | +} | |
| 16 | + | |
| 17 | +merror { | |
| 18 | + color: #f358ad; | |
| 19 | + border: 1px dashed var(--color-f35869, #f358ad); | |
| 20 | +} | |
| 21 | + | |
| 1 | 22 | .fraction { |
| 2 | 23 | display: inline-block; |
| 3 | 24 | vertical-align: middle; |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | pre { |
| 2 | 2 | background: #232023; |
| 3 | - color: fcfcfc; | |
| 3 | + color: #fcfcfc; | |
| 4 | 4 | padding: 1rem; |
| 5 | 5 | border-radius: 6px; |
| 6 | 6 | overflow: auto; |
| @@ -9,11 +9,28 @@ | ||
| 9 | 9 | overflow-x: auto; |
| 10 | 10 | } |
| 11 | 11 | |
| 12 | -/*pre > code { | |
| 13 | - color: #fcfcfc; | |
| 14 | -}*/ | |
| 12 | +pre > code.hl { | |
| 13 | + color: #fcfcfc; | |
| 14 | +} | |
| 15 | 15 | |
| 16 | 16 | code { |
| 17 | 17 | color: #b421f8; |
| 18 | 18 | line-height: 1.45; |
| 19 | 19 | } |
| 20 | + | |
| 21 | +.hl-comment { color: #2c980c; font-style: italic; } /* dark.green */ | |
| 22 | +.hl-keyword { color: #2687f7; } /* medium.blue */ | |
| 23 | +.hl-type { color: #10c074; } /* dark.lime */ | |
| 24 | +.hl-builtin { color: #ecea71; } /* light.yellow */ | |
| 25 | +.hl-function { color: #ecea71; } /* light.yellow */ | |
| 26 | +.hl-string { color: #f7a717; } /* medium.orange */ | |
| 27 | +.hl-number { color: #6cf344; } /* light.green */ | |
| 28 | +.hl-constant { color: #ea79d8; } /* light.pink */ | |
| 29 | +.hl-preprocessor { color: #ae6bdb; } /* light.purple */ | |
| 30 | +.hl-attribute { color: #f8c56a; } /* light.orange */ | |
| 31 | +.hl-variable { color: #29d3ff; } /* medium.cyan */ | |
| 32 | +/* アセンブリ */ | |
| 33 | +.hl-instruction { color: #2687f7; } /* medium.blue */ | |
| 34 | +.hl-register { color: #1cbcd0; } /* dark.cyan */ | |
| 35 | +.hl-directive { color: #f545f5; } /* medium.pink */ | |
| 36 | +.hl-label { color: #ecea71; } /* light.yellow */ |
| @@ -0,0 +1,55 @@ | ||
| 1 | +.code-tabs { | |
| 2 | + margin: 1em 0; | |
| 3 | +} | |
| 4 | + | |
| 5 | +.code-tabs > input { | |
| 6 | + position: absolute; | |
| 7 | + opacity: 0; | |
| 8 | + pointer-events: none; | |
| 9 | +} | |
| 10 | + | |
| 11 | +.code-tabs > label { | |
| 12 | + display: inline-block; | |
| 13 | + padding: 4px 14px; | |
| 14 | + margin-right: 4px; | |
| 15 | + border: 1px solid #f545f5; | |
| 16 | + border-bottom: none; | |
| 17 | + border-radius: 6px 6px 0 0; | |
| 18 | + background: #120f12; | |
| 19 | + color: #bcb4bc; | |
| 20 | + cursor: pointer; | |
| 21 | + user-select: none; | |
| 22 | +} | |
| 23 | + | |
| 24 | +.code-tabs > label:hover { | |
| 25 | + color: #fcfcfc; | |
| 26 | +} | |
| 27 | + | |
| 28 | +.code-tabs > input:checked + label { | |
| 29 | + background: #f545f5; | |
| 30 | + color: #120f12; | |
| 31 | + font-weight: bold; | |
| 32 | +} | |
| 33 | + | |
| 34 | +.code-tabs > input:focus-visible + label { | |
| 35 | + outline: 2px solid #b421f8; | |
| 36 | + outline-offset: 2px; | |
| 37 | +} | |
| 38 | + | |
| 39 | +.code-tabs > .code-tab-panel { | |
| 40 | + display: none; | |
| 41 | +} | |
| 42 | + | |
| 43 | +.code-tabs > .code-tab-panel > pre { | |
| 44 | + margin-top: 0; | |
| 45 | + border-top-left-radius: 0; | |
| 46 | +} | |
| 47 | + | |
| 48 | +.code-tabs > input:nth-of-type(1):checked ~ .code-tab-panel:nth-of-type(1), | |
| 49 | +.code-tabs > input:nth-of-type(2):checked ~ .code-tab-panel:nth-of-type(2), | |
| 50 | +.code-tabs > input:nth-of-type(3):checked ~ .code-tab-panel:nth-of-type(3), | |
| 51 | +.code-tabs > input:nth-of-type(4):checked ~ .code-tab-panel:nth-of-type(4), | |
| 52 | +.code-tabs > input:nth-of-type(5):checked ~ .code-tab-panel:nth-of-type(5), | |
| 53 | +.code-tabs > input:nth-of-type(6):checked ~ .code-tab-panel:nth-of-type(6) { | |
| 54 | + display: block; | |
| 55 | +} |
変更
route.php
| @@ -60,6 +60,7 @@ | ||
| 60 | 60 | $routes = [ |
| 61 | 61 | Route::add('GET', 'robots.txt', Home::class.'@robots'), |
| 62 | 62 | Route::add('GET', 'blog/{page}', Home::class.'@article'), |
| 63 | + Route::add('GET', 'test', Page::class.'@test'), | |
| 63 | 64 | Route::add('GET', 'about', Page::class.'@about'), |
| 64 | 65 | Route::add('GET', 'monero', Page::class.'@monero'), |
| 65 | 66 | Route::add('GET', 'secret', Page::class.'@secret'), |
| @@ -179,6 +179,7 @@ | ||
| 179 | 179 | $tmpl->addCss('news-article'); |
| 180 | 180 | $tmpl->addCss('search'); |
| 181 | 181 | $tmpl->addCss('blogtype'); |
| 182 | + $tmpl->addCss('code'); | |
| 182 | 183 | $tmpl->render('article'); |
| 183 | 184 | exit(); |
| 184 | 185 |
| @@ -42,8 +42,42 @@ | ||
| 42 | 42 | use Std\Lib\Template; |
| 43 | 43 | use Roles; |
| 44 | 44 | |
| 45 | +use Std\Lib\Syntax; // TEST | |
| 46 | + | |
| 45 | 47 | class Page { |
| 46 | 48 | use Mods; |
| 49 | + | |
| 50 | + public function test(array $params): void { | |
| 51 | + try { | |
| 52 | + $tmpl = new Template('/'); | |
| 53 | + $pagetit = '新ページ'; | |
| 54 | + $description = 'PHPフレームワークについて'; | |
| 55 | + | |
| 56 | + // ユーザー | |
| 57 | + $auth = new Auth(); | |
| 58 | + $user = $auth->getLoggedInUser(); | |
| 59 | + $tmpl->assign('user', $user); | |
| 60 | + | |
| 61 | + $codeAsm = file_get_contents(ROOT.'/data/main.s'); | |
| 62 | + $codeC = file_get_contents(ROOT.'/data/main.c'); | |
| 63 | + $asm = new Syntax('asm'); | |
| 64 | + $c = new Syntax('c'); | |
| 65 | + | |
| 66 | + $tmpl->assign('cCode', $c->colorize($codeC)); | |
| 67 | + $tmpl->assign('asmCode', $asm->colorize($codeAsm)); | |
| 68 | + | |
| 69 | + $tmpl->assign('pagetit', $pagetit); | |
| 70 | + $tmpl->assign('curPage', 'test'); | |
| 71 | + $tmpl->assign('custCss', false); | |
| 72 | + $tmpl->assign('menu', $this->getMenu()); | |
| 73 | + $tmpl->assign('description', $description); | |
| 74 | + | |
| 75 | + $tmpl->addCss('code'); | |
| 76 | + $tmpl->render('test'); | |
| 77 | + } catch (\Exception $e) { | |
| 78 | + throw new \Exception($e->getMessage()); | |
| 79 | + } | |
| 80 | + } | |
| 47 | 81 | |
| 48 | 82 | public function about(array $params): void { |
| 49 | 83 | try { |
| @@ -121,7 +121,7 @@ | ||
| 121 | 121 | $ip = $_SERVER['REMOTE_ADDR']; |
| 122 | 122 | $res = $this->purgeOldTokens($userData); |
| 123 | 123 | if (!$res->isSuccess) return Result::Error($res->message); |
| 124 | - $userData = $res->data; | |
| 124 | + $userData = $res->data['user']; | |
| 125 | 125 | $token = bin2hex(random_bytes(64)); |
| 126 | 126 | $expire = time() + $this->tokenDuration; |
| 127 | 127 | $newToken = [ |
| @@ -164,7 +164,7 @@ | ||
| 164 | 164 | if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = []; |
| 165 | 165 | $res = $this->purgeOldTokens($userData); |
| 166 | 166 | if (!$res->isSuccess) return null; |
| 167 | - $userData = $res->data; | |
| 167 | + $userData = $res->data['user']; | |
| 168 | 168 | } |
| 169 | 169 | |
| 170 | 170 | public function logout(?string $token = null): Result { |
| @@ -363,9 +363,9 @@ | ||
| 363 | 363 | if (!AUTH_ENABLED) return Result::Error('エラー:認証システムは無効です。'); |
| 364 | 364 | // 有効期限切れたトークンの削除 |
| 365 | 365 | $out = $userData; |
| 366 | - $out->tokens = array_filter($userData->tokens, function($t): bool { | |
| 367 | - return Result::Success('', ($t->expDate ?? 0) > time()); | |
| 368 | - }); | |
| 366 | + $out->tokens = array_values(array_filter($userData->tokens, function($t): bool { | |
| 367 | + return ($t->expDate ?? 0) > time(); | |
| 368 | + })); | |
| 369 | 369 | |
| 370 | 370 | $path = $this->dataDir.$userData->id.'.'.$userData->username.'.json'; |
| 371 | 371 | $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); |
| @@ -375,7 +375,7 @@ | ||
| 375 | 375 | return Result::Error('エラー:ユーザーデータの保存に失敗。'); |
| 376 | 376 | } |
| 377 | 377 | |
| 378 | - return Result::Success('', $out); | |
| 378 | + return Result::Success('', ['user' => $out]); | |
| 379 | 379 | } |
| 380 | 380 | |
| 381 | 381 | private function getUserId(?string $username = null, ?string $token = null): int { |
| @@ -562,4 +562,4 @@ | ||
| 562 | 562 | |
| 563 | 563 | return Result::Success(); |
| 564 | 564 | } |
| 565 | -}; | |
| 565 | +}; |
| @@ -97,6 +97,10 @@ | ||
| 97 | 97 | $this->fullInfo->type = $this->type; |
| 98 | 98 | $this->fullInfo->size = $this->size; |
| 99 | 99 | $this->fullInfo->bytes = $this->bytes; |
| 100 | + if (!isset($this->fullInfo->width) && isset($this->fullInfo->header->width)) { | |
| 101 | + $this->fullInfo->width = $this->fullInfo->header->width; | |
| 102 | + $this->fullInfo->height = $this->fullInfo->header->height; | |
| 103 | + } | |
| 100 | 104 | } |
| 101 | 105 | |
| 102 | 106 | /////////////// |
| @@ -0,0 +1,822 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * LaTeX数式をMathMLに変換する(JavaScript不要、ブラウザがネイティブで描画する) | |
| 6 | + * | |
| 7 | + * 対応範囲はチュートリアルで使うLaTeXのサブセット: | |
| 8 | + * 上付き・下付き、\frac、\sqrt、\left…\right、行列環境、\text、\mathbf等の書体、 | |
| 9 | + * アクセント、ギリシャ文字、関数名、演算子・関係記号・矢印、空白。 | |
| 10 | + * 未対応のコマンドは<merror>として出力し、getErrors()で取得出来る。 | |
| 11 | + */ | |
| 12 | +class Latex { | |
| 13 | + private string $src = ''; | |
| 14 | + private int $pos = 0; | |
| 15 | + private bool $display = false; | |
| 16 | + private string $variant = ''; | |
| 17 | + private array $errors = []; | |
| 18 | + | |
| 19 | + private const GREEK = [ | |
| 20 | + 'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ', 'epsilon' => 'ϵ', | |
| 21 | + 'varepsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ', 'vartheta' => 'ϑ', | |
| 22 | + 'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ', 'nu' => 'ν', 'xi' => 'ξ', | |
| 23 | + 'omicron' => 'ο', 'pi' => 'π', 'varpi' => 'ϖ', 'rho' => 'ρ', 'varrho' => 'ϱ', | |
| 24 | + 'sigma' => 'σ', 'varsigma' => 'ς', 'tau' => 'τ', 'upsilon' => 'υ', 'phi' => 'ϕ', | |
| 25 | + 'varphi' => 'φ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω', | |
| 26 | + ]; | |
| 27 | + | |
| 28 | + private const GREEK_UPPER = [ | |
| 29 | + 'Gamma' => 'Γ', 'Delta' => 'Δ', 'Theta' => 'Θ', 'Lambda' => 'Λ', 'Xi' => 'Ξ', | |
| 30 | + 'Pi' => 'Π', 'Sigma' => 'Σ', 'Upsilon' => 'Υ', 'Phi' => 'Φ', 'Psi' => 'Ψ', 'Omega' => 'Ω', | |
| 31 | + ]; | |
| 32 | + | |
| 33 | + // 識別子として扱う記号 / symbols rendered as identifiers | |
| 34 | + private const IDENT = [ | |
| 35 | + 'infty' => '∞', 'partial' => '∂', 'nabla' => '∇', 'ell' => 'ℓ', 'hbar' => 'ℏ', | |
| 36 | + 'top' => '⊤', 'bot' => '⊥', 'degree' => '°', 'emptyset' => '∅', 'Re' => 'ℜ', 'Im' => 'ℑ', | |
| 37 | + 'aleph' => 'ℵ', 'imath' => 'ı', 'jmath' => 'ȷ', | |
| 38 | + ]; | |
| 39 | + | |
| 40 | + // 演算子・関係記号・矢印 / operators, relations, arrows | |
| 41 | + private const OPS = [ | |
| 42 | + 'cdot' => '⋅', 'times' => '×', 'div' => '÷', 'pm' => '±', 'mp' => '∓', 'ast' => '∗', | |
| 43 | + 'star' => '⋆', 'circ' => '∘', 'bullet' => '∙', 'otimes' => '⊗', 'oplus' => '⊕', | |
| 44 | + 'le' => '≤', 'leq' => '≤', 'ge' => '≥', 'geq' => '≥', 'ne' => '≠', 'neq' => '≠', | |
| 45 | + 'approx' => '≈', 'equiv' => '≡', 'sim' => '∼', 'simeq' => '≃', 'cong' => '≅', | |
| 46 | + 'propto' => '∝', 'll' => '≪', 'gg' => '≫', 'perp' => '⊥', 'parallel' => '∥', | |
| 47 | + 'to' => '→', 'rightarrow' => '→', 'leftarrow' => '←', 'gets' => '←', | |
| 48 | + 'leftrightarrow' => '↔', 'Rightarrow' => '⇒', 'Leftarrow' => '⇐', | |
| 49 | + 'Leftrightarrow' => '⇔', 'iff' => '⟺', 'implies' => '⟹', 'mapsto' => '↦', | |
| 50 | + 'uparrow' => '↑', 'downarrow' => '↓', 'longrightarrow' => '⟶', | |
| 51 | + 'in' => '∈', 'notin' => '∉', 'ni' => '∋', 'subset' => '⊂', 'subseteq' => '⊆', | |
| 52 | + 'supset' => '⊃', 'supseteq' => '⊇', 'cup' => '∪', 'cap' => '∩', 'setminus' => '∖', | |
| 53 | + 'forall' => '∀', 'exists' => '∃', 'neg' => '¬', 'lnot' => '¬', 'land' => '∧', | |
| 54 | + 'wedge' => '∧', 'lor' => '∨', 'vee' => '∨', 'angle' => '∠', | |
| 55 | + 'ldots' => '…', 'dots' => '…', 'cdots' => '⋯', 'vdots' => '⋮', 'ddots' => '⋱', | |
| 56 | + 'prime' => '′', 'colon' => ':', 'mid' => '∣', | |
| 57 | + ]; | |
| 58 | + | |
| 59 | + // 区切り記号 / delimiters | |
| 60 | + private const DELIMS = [ | |
| 61 | + '(' => '(', ')' => ')', '[' => '[', ']' => ']', '|' => '|', '/' => '/', | |
| 62 | + '{' => '{', '}' => '}', '\\{' => '{', '\\}' => '}', '\\|' => '‖', | |
| 63 | + '\\lVert' => '‖', '\\rVert' => '‖', '\\Vert' => '‖', '\\lvert' => '|', '\\rvert' => '|', | |
| 64 | + '\\vert' => '|', '\\langle' => '⟨', '\\rangle' => '⟩', '\\lfloor' => '⌊', | |
| 65 | + '\\rfloor' => '⌋', '\\lceil' => '⌈', '\\rceil' => '⌉', '.' => '', | |
| 66 | + ]; | |
| 67 | + | |
| 68 | + // 括弧として使う記号 / fence-like commands usable outside \left…\right | |
| 69 | + private const FENCES = [ | |
| 70 | + 'lVert' => '‖', 'rVert' => '‖', 'Vert' => '‖', '|' => '‖', 'lvert' => '|', | |
| 71 | + 'rvert' => '|', 'vert' => '|', 'langle' => '⟨', 'rangle' => '⟩', 'lfloor' => '⌊', | |
| 72 | + 'rfloor' => '⌋', 'lceil' => '⌈', 'rceil' => '⌉', '{' => '{', '}' => '}', | |
| 73 | + ]; | |
| 74 | + | |
| 75 | + // 関数名 / function names (upright) | |
| 76 | + private const FUNCS = [ | |
| 77 | + 'sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'arcsin', 'arccos', 'arctan', | |
| 78 | + 'sinh', 'cosh', 'tanh', 'log', 'ln', 'exp', 'det', 'dim', 'ker', 'deg', 'gcd', | |
| 79 | + 'arg', 'Pr', 'hom', | |
| 80 | + ]; | |
| 81 | + | |
| 82 | + // 下に添え字を付ける関数 / functions that take limits underneath in display mode | |
| 83 | + private const LIMIT_FUNCS = ['lim', 'max', 'min', 'sup', 'inf', 'limsup', 'liminf']; | |
| 84 | + | |
| 85 | + // 大型演算子 / large operators | |
| 86 | + private const BIG_OPS = [ | |
| 87 | + 'sum' => ['∑', true], 'prod' => ['∏', true], 'coprod' => ['∐', true], | |
| 88 | + 'bigcup' => ['⋃', true], 'bigcap' => ['⋂', true], | |
| 89 | + 'int' => ['∫', false], 'iint' => ['∬', false], 'iiint' => ['∭', false], 'oint' => ['∮', false], | |
| 90 | + ]; | |
| 91 | + | |
| 92 | + private const ACCENTS = [ | |
| 93 | + 'vec' => '→', 'hat' => '^', 'widehat' => '^', 'bar' => '¯', 'dot' => '˙', | |
| 94 | + 'ddot' => '¨', 'tilde' => '~', 'widetilde' => '~', 'check' => 'ˇ', 'breve' => '˘', | |
| 95 | + ]; | |
| 96 | + | |
| 97 | + private const SPACES = [ | |
| 98 | + ',' => '0.1667em', ':' => '0.2222em', '>' => '0.2222em', ';' => '0.2778em', | |
| 99 | + ' ' => '0.25em', 'quad' => '1em', 'qquad' => '2em', 'enspace' => '0.5em', | |
| 100 | + 'thinspace' => '0.1667em', | |
| 101 | + ]; | |
| 102 | + | |
| 103 | + private const ENV_FENCES = [ | |
| 104 | + 'matrix' => ['', ''], 'pmatrix' => ['(', ')'], 'bmatrix' => ['[', ']'], | |
| 105 | + 'Bmatrix' => ['{', '}'], 'vmatrix' => ['|', '|'], 'Vmatrix' => ['‖', '‖'], | |
| 106 | + 'smallmatrix' => ['', ''], | |
| 107 | + ]; | |
| 108 | + | |
| 109 | + /** | |
| 110 | + * LaTeXをMathMLに変換する | |
| 111 | + * | |
| 112 | + * @param string $tex LaTeXの数式($$は含まない) | |
| 113 | + * @param bool $display ブロック表示ならtrue | |
| 114 | + * @return string <math>要素 | |
| 115 | + */ | |
| 116 | + public function render(string $tex, bool $display = false): string { | |
| 117 | + $this->src = $tex; | |
| 118 | + $this->pos = 0; | |
| 119 | + $this->display = $display; | |
| 120 | + $this->variant = ''; | |
| 121 | + $this->errors = []; | |
| 122 | + | |
| 123 | + $nodes = $this->parseList(); | |
| 124 | + // 外側に余ったトークンを読み飛ばす / skip stray tokens at the top level | |
| 125 | + while ($this->pos < strlen($this->src)) { | |
| 126 | + if ($this->startsWith('\\\\')) $this->pos += 2; | |
| 127 | + else if ($this->startsWith('\\end')) { $this->readCommand(); $this->readGroupRaw(); } | |
| 128 | + else if ($this->startsWith('\\right')) { $this->readCommand(); $this->readDelimiter(); } | |
| 129 | + else $this->pos++; | |
| 130 | + $nodes = array_merge($nodes, $this->parseList()); | |
| 131 | + } | |
| 132 | + | |
| 133 | + $alt = htmlspecialchars(trim(preg_replace('/\s+/', ' ', $tex)), ENT_QUOTES, 'UTF-8'); | |
| 134 | + $attr = $display ? ' display="block"' : ''; | |
| 135 | + return "<math{$attr} alttext=\"{$alt}\">".$this->row($nodes).'</math>'; | |
| 136 | + } | |
| 137 | + | |
| 138 | + /** | |
| 139 | + * 直前のrender()で見つかった未対応のコマンド | |
| 140 | + * | |
| 141 | + * @return array エラーメッセージの配列 | |
| 142 | + */ | |
| 143 | + public function getErrors(): array { | |
| 144 | + return $this->errors; | |
| 145 | + } | |
| 146 | + | |
| 147 | + //------------------------------------------ | |
| 148 | + // パーサー | |
| 149 | + //------------------------------------------ | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * 終端記号(}、&、\\、\end、\right)又は文末まで要素を読む | |
| 153 | + * | |
| 154 | + * @return array ノードの配列 | |
| 155 | + */ | |
| 156 | + private function parseList(): array { | |
| 157 | + $nodes = []; | |
| 158 | + while (true) { | |
| 159 | + $this->skipSpace(); | |
| 160 | + if ($this->atStop()) break; | |
| 161 | + $node = $this->parseScripted(); | |
| 162 | + if ($node === null) continue; | |
| 163 | + $nodes[] = $node; | |
| 164 | + } | |
| 165 | + | |
| 166 | + // 先頭、又は演算子・開き括弧の直後の+−は符号(空白無し) / a +/− at the start or after an operator is a sign | |
| 167 | + $count = count($nodes); | |
| 168 | + for ($i = 0; $i < $count; $i++) { | |
| 169 | + if (empty($nodes[$i]['sign'])) continue; | |
| 170 | + $prev = $i > 0 ? $nodes[$i - 1]['xml'] : ''; | |
| 171 | + $isClose = preg_match('/^<mo[^>]*>[\)\]\}|‖⟩⌋⌉]<\/mo>$/u', $prev); | |
| 172 | + if ($i === 0 || (str_starts_with($prev, '<mo') && !$isClose)) { | |
| 173 | + $nodes[$i]['xml'] = str_replace('<mo>', '<mo form="prefix">', $nodes[$i]['xml']); | |
| 174 | + } | |
| 175 | + } | |
| 176 | + | |
| 177 | + // 関数名の後に細い空白を入れる / thin space after function names | |
| 178 | + $out = []; | |
| 179 | + for ($i = 0; $i < $count; $i++) { | |
| 180 | + // 識別子の直後の関数名の前にも細い空白(a\tan x) / thin space between an identifier and a function name | |
| 181 | + if (!empty($nodes[$i]['func']) && $i > 0 && !str_starts_with($nodes[$i - 1]['xml'], '<mo') | |
| 182 | + && !str_starts_with($nodes[$i - 1]['xml'], '<mspace')) | |
| 183 | + $out[] = '<mspace width="0.1667em"></mspace>'; | |
| 184 | + $out[] = $nodes[$i]['xml']; | |
| 185 | + if (!empty($nodes[$i]['func']) && $i + 1 < $count) { | |
| 186 | + $next = $nodes[$i + 1]['xml']; | |
| 187 | + if (!str_starts_with($next, '<mo stretchy="false">(</mo>') | |
| 188 | + && !str_starts_with($next, '<mrow><mo fence="true"')) | |
| 189 | + $out[] = '<mspace width="0.1667em"></mspace>'; | |
| 190 | + } | |
| 191 | + } | |
| 192 | + return $out; | |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * 一つの要素と其の上付き・下付きを読む | |
| 197 | + * | |
| 198 | + * @return array|null ['xml' => string, 'func' => bool] | |
| 199 | + */ | |
| 200 | + private function parseScripted(): ?array { | |
| 201 | + $this->skipSpace(); | |
| 202 | + $c = $this->peek(); | |
| 203 | + | |
| 204 | + if ($c === '^' || $c === '_') { | |
| 205 | + $base = ['xml' => '<mrow></mrow>', 'limits' => false, 'func' => false]; | |
| 206 | + } else { | |
| 207 | + $base = $this->parseAtom(); | |
| 208 | + if ($base === null) return null; | |
| 209 | + } | |
| 210 | + | |
| 211 | + $sub = null; | |
| 212 | + $sup = null; | |
| 213 | + $primes = ''; | |
| 214 | + while (true) { | |
| 215 | + $this->skipSpace(); | |
| 216 | + $c = $this->peek(); | |
| 217 | + if ($c === '^' && $sup === null) { | |
| 218 | + $this->pos++; | |
| 219 | + $sup = $this->parseScriptArg(); | |
| 220 | + } else if ($c === '_' && $sub === null) { | |
| 221 | + $this->pos++; | |
| 222 | + $sub = $this->parseScriptArg(); | |
| 223 | + } else if ($c === "'") { | |
| 224 | + $this->pos++; | |
| 225 | + $primes .= '′'; | |
| 226 | + } else break; | |
| 227 | + } | |
| 228 | + | |
| 229 | + // ′は既に上付きの形の字なので、他の上付きが無ければ横に並べるだけ | |
| 230 | + // The prime glyph is already raised, so with no other superscript just place it after the base | |
| 231 | + if ($primes !== '') { | |
| 232 | + if ($sup === null) { | |
| 233 | + $p = '<mo lspace="0" rspace="0">'.$primes.'</mo>'; | |
| 234 | + $xml = '<mrow>'.$base['xml'].$p.'</mrow>'; | |
| 235 | + if ($sub === null) return ['xml' => $xml, 'func' => !empty($base['func'])]; | |
| 236 | + $base = ['xml' => $base['xml'], 'func' => !empty($base['func'])]; | |
| 237 | + $b = "<msub>{$base['xml']}".$this->wrap($sub)."</msub>"; | |
| 238 | + return ['xml' => '<mrow>'.$b.$p.'</mrow>', 'func' => false]; | |
| 239 | + } | |
| 240 | + $sup = '<mo>'.$primes.'</mo>'.$sup; | |
| 241 | + } | |
| 242 | + | |
| 243 | + if ($sub === null && $sup === null) return $base; | |
| 244 | + | |
| 245 | + $b = $base['xml']; | |
| 246 | + $under = !empty($base['under']) || (!empty($base['limits']) && $this->display); | |
| 247 | + if ($sub !== null) $sub = $this->wrap($sub); | |
| 248 | + if ($sup !== null) $sup = $this->wrap($sup); | |
| 249 | + | |
| 250 | + if ($under) { | |
| 251 | + if ($sub !== null && $sup !== null) $xml = "<munderover>{$b}{$sub}{$sup}</munderover>"; | |
| 252 | + else if ($sub !== null) $xml = "<munder>{$b}{$sub}</munder>"; | |
| 253 | + else $xml = "<mover>{$b}{$sup}</mover>"; | |
| 254 | + } else { | |
| 255 | + if ($sub !== null && $sup !== null) $xml = "<msubsup>{$b}{$sub}{$sup}</msubsup>"; | |
| 256 | + else if ($sub !== null) $xml = "<msub>{$b}{$sub}</msub>"; | |
| 257 | + else $xml = "<msup>{$b}{$sup}</msup>"; | |
| 258 | + } | |
| 259 | + | |
| 260 | + return ['xml' => $xml, 'func' => !empty($base['func'])]; | |
| 261 | + } | |
| 262 | + | |
| 263 | + /** | |
| 264 | + * 上付き・下付きの引数(1文字、1コマンド、又は{…}) | |
| 265 | + * | |
| 266 | + * @return string MathML | |
| 267 | + */ | |
| 268 | + private function parseScriptArg(): string { | |
| 269 | + $this->skipSpace(); | |
| 270 | + $c = $this->peek(); | |
| 271 | + if ($c === '{') return $this->parseGroup(); | |
| 272 | + if ($c === '' ) return '<mrow></mrow>'; | |
| 273 | + if (ctype_digit($c)) { | |
| 274 | + $this->pos++; | |
| 275 | + return '<mn>'.$this->styleChar($c).'</mn>'; | |
| 276 | + } | |
| 277 | + $atom = $this->parseAtom(); | |
| 278 | + return $atom === null ? '<mrow></mrow>' : $atom['xml']; | |
| 279 | + } | |
| 280 | + | |
| 281 | + /** | |
| 282 | + * {…}を読む | |
| 283 | + * | |
| 284 | + * @return string MathML(mrow) | |
| 285 | + */ | |
| 286 | + private function parseGroup(): string { | |
| 287 | + $this->skipSpace(); | |
| 288 | + if ($this->peek() !== '{') { | |
| 289 | + // 波括弧の無い引数:1要素だけ / argument without braces: a single element | |
| 290 | + $atom = $this->parseAtom(); | |
| 291 | + return $atom === null ? '<mrow></mrow>' : $atom['xml']; | |
| 292 | + } | |
| 293 | + $this->pos++; | |
| 294 | + $nodes = $this->parseList(); | |
| 295 | + if ($this->peek() === '}') $this->pos++; | |
| 296 | + return $this->row($nodes, true); | |
| 297 | + } | |
| 298 | + | |
| 299 | + /** | |
| 300 | + * 一つの要素を読む | |
| 301 | + * | |
| 302 | + * @return array|null ノード | |
| 303 | + */ | |
| 304 | + private function parseAtom(): ?array { | |
| 305 | + $c = $this->peek(); | |
| 306 | + if ($c === '') return null; | |
| 307 | + | |
| 308 | + if ($c === '{') { | |
| 309 | + return $this->node($this->parseGroup()); | |
| 310 | + } | |
| 311 | + | |
| 312 | + if ($c === '\\') return $this->parseCommand(); | |
| 313 | + | |
| 314 | + // 数字 / numbers | |
| 315 | + if (preg_match('/\G[0-9]+(?:\.[0-9]+)?/', $this->src, $m, 0, $this->pos)) { | |
| 316 | + $this->pos += strlen($m[0]); | |
| 317 | + return $this->node('<mn>'.$this->styleString($m[0]).'</mn>'); | |
| 318 | + } | |
| 319 | + | |
| 320 | + // 英字 / latin letters | |
| 321 | + if (ctype_alpha($c)) { | |
| 322 | + $this->pos++; | |
| 323 | + return $this->node($this->ident($c)); | |
| 324 | + } | |
| 325 | + | |
| 326 | + if ($c === '~') { | |
| 327 | + $this->pos++; | |
| 328 | + return $this->node('<mspace width="0.25em"></mspace>'); | |
| 329 | + } | |
| 330 | + | |
| 331 | + // 1文字(UTF-8対応) / one character (UTF-8 aware) | |
| 332 | + preg_match('/\G./us', $this->src, $m, 0, $this->pos); | |
| 333 | + $ch = $m[0] ?? $c; | |
| 334 | + $this->pos += strlen($ch); | |
| 335 | + | |
| 336 | + switch ($ch) { | |
| 337 | + case '-': return ['xml' => '<mo>−</mo>', 'func' => false, 'limits' => false, 'sign' => true]; | |
| 338 | + case '+': return ['xml' => '<mo>+</mo>', 'func' => false, 'limits' => false, 'sign' => true]; | |
| 339 | + case '*': return $this->node('<mo>∗</mo>'); | |
| 340 | + case '(': case ')': case '[': case ']': case '|': | |
| 341 | + return $this->node('<mo stretchy="false">'.$this->esc($ch).'</mo>'); | |
| 342 | + case '/': // TeXでは/は普通の文字(空白無し) / in TeX the slash is an ordinary symbol | |
| 343 | + return $this->node('<mo lspace="0" rspace="0">/</mo>'); | |
| 344 | + case '=': case '<': case '>': case ',': case ';': case ':': | |
| 345 | + case '!': case '?': case '.': case '@': | |
| 346 | + return $this->node('<mo>'.$this->esc($ch).'</mo>'); | |
| 347 | + } | |
| 348 | + | |
| 349 | + // ASCII以外:ギリシャ文字は識別子、記号は演算子、其れ以外は文字列 | |
| 350 | + if (preg_match('/^[\x{0370}-\x{03FF}]$/u', $ch)) return $this->node($this->ident($ch)); | |
| 351 | + if (preg_match('/^[\x{2190}-\x{22FF}\x{2A00}-\x{2AFF}×÷±·]$/u', $ch)) | |
| 352 | + return $this->node('<mo>'.$ch.'</mo>'); | |
| 353 | + if (preg_match('/^[\x{2100}-\x{214F}\x{1D400}-\x{1D7FF}]$/u', $ch)) | |
| 354 | + return $this->node('<mi>'.$ch.'</mi>'); | |
| 355 | + return $this->node('<mtext>'.$this->esc($ch).'</mtext>'); | |
| 356 | + } | |
| 357 | + | |
| 358 | + /** | |
| 359 | + * バックスラッシュで始まるコマンドを読む | |
| 360 | + * | |
| 361 | + * @return array|null ノード | |
| 362 | + */ | |
| 363 | + private function parseCommand(): ?array { | |
| 364 | + $name = $this->readCommand(); | |
| 365 | + | |
| 366 | + // 空白 / spacing | |
| 367 | + if (isset(self::SPACES[$name])) { | |
| 368 | + return $this->node('<mspace width="'.self::SPACES[$name].'"></mspace>'); | |
| 369 | + } | |
| 370 | + if ($name === '!') return null; | |
| 371 | + if ($name === '\\') return null; // 行列の外の改行は無視 / line break outside a table | |
| 372 | + | |
| 373 | + // エスケープされた記号 / escaped characters | |
| 374 | + switch ($name) { | |
| 375 | + case '{': return $this->node('<mo stretchy="false">{</mo>'); | |
| 376 | + case '}': return $this->node('<mo stretchy="false">}</mo>'); | |
| 377 | + case '%': return $this->node('<mo>%</mo>'); | |
| 378 | + case '#': return $this->node('<mo>#</mo>'); | |
| 379 | + case '&': return $this->node('<mo>&</mo>'); | |
| 380 | + case '_': return $this->node('<mo>_</mo>'); | |
| 381 | + case '$': return $this->node('<mo>$</mo>'); | |
| 382 | + } | |
| 383 | + | |
| 384 | + if (isset(self::GREEK[$name])) return $this->node($this->ident(self::GREEK[$name])); | |
| 385 | + if (isset(self::GREEK_UPPER[$name])) { | |
| 386 | + $ch = $this->styleChar(self::GREEK_UPPER[$name]); | |
| 387 | + $attr = $ch === self::GREEK_UPPER[$name] ? ' mathvariant="normal"' : ''; | |
| 388 | + return $this->node("<mi{$attr}>{$ch}</mi>"); | |
| 389 | + } | |
| 390 | + if (isset(self::IDENT[$name])) { | |
| 391 | + return $this->node('<mi mathvariant="normal">'.self::IDENT[$name].'</mi>'); | |
| 392 | + } | |
| 393 | + if (isset(self::FENCES[$name])) { | |
| 394 | + return $this->node('<mo stretchy="false">'.self::FENCES[$name].'</mo>'); | |
| 395 | + } | |
| 396 | + if (isset(self::OPS[$name])) return $this->node('<mo>'.self::OPS[$name].'</mo>'); | |
| 397 | + | |
| 398 | + if (in_array($name, self::FUNCS, true)) { | |
| 399 | + return ['xml' => '<mi>'.$name.'</mi>', 'func' => true, 'limits' => false]; | |
| 400 | + } | |
| 401 | + if (in_array($name, self::LIMIT_FUNCS, true)) { | |
| 402 | + return ['xml' => '<mo movablelimits="true" form="prefix">'.$name.'</mo>', | |
| 403 | + 'func' => true, 'limits' => true]; | |
| 404 | + } | |
| 405 | + if (isset(self::BIG_OPS[$name])) { | |
| 406 | + [$sym, $limits] = self::BIG_OPS[$name]; | |
| 407 | + return ['xml' => '<mo largeop="true" movablelimits="'.($limits ? 'true' : 'false').'">'.$sym.'</mo>', | |
| 408 | + 'func' => false, 'limits' => $limits]; | |
| 409 | + } | |
| 410 | + | |
| 411 | + switch ($name) { | |
| 412 | + case 'frac': case 'dfrac': case 'tfrac': case 'cfrac': | |
| 413 | + $num = $this->parseGroup(); | |
| 414 | + $den = $this->parseGroup(); | |
| 415 | + $attr = ''; | |
| 416 | + if ($name === 'dfrac' || $name === 'cfrac') $attr = ' displaystyle="true"'; | |
| 417 | + if ($name === 'tfrac') $attr = ' displaystyle="false"'; | |
| 418 | + return $this->node("<mfrac{$attr}>{$num}{$den}</mfrac>"); | |
| 419 | + | |
| 420 | + case 'sqrt': | |
| 421 | + $this->skipSpace(); | |
| 422 | + if ($this->peek() === '[') { | |
| 423 | + $index = $this->renderSub($this->readBracketRaw()); | |
| 424 | + $base = $this->parseGroup(); | |
| 425 | + return $this->node("<mroot>{$base}{$index}</mroot>"); | |
| 426 | + } | |
| 427 | + return $this->node('<msqrt>'.$this->parseGroup().'</msqrt>'); | |
| 428 | + | |
| 429 | + case 'left': | |
| 430 | + return $this->parseLeftRight(); | |
| 431 | + | |
| 432 | + case 'begin': | |
| 433 | + return $this->parseEnvironment(); | |
| 434 | + | |
| 435 | + case 'text': case 'textrm': case 'textnormal': case 'mbox': case 'textup': | |
| 436 | + return $this->node('<mtext>'.$this->esc($this->readGroupRaw()).'</mtext>'); | |
| 437 | + case 'textbf': | |
| 438 | + return $this->node('<mtext style="font-weight: bold;">'.$this->esc($this->readGroupRaw()).'</mtext>'); | |
| 439 | + case 'textit': | |
| 440 | + return $this->node('<mtext style="font-style: italic;">'.$this->esc($this->readGroupRaw()).'</mtext>'); | |
| 441 | + | |
| 442 | + case 'operatorname': | |
| 443 | + $fn = $this->esc(trim($this->readGroupRaw())); | |
| 444 | + return ['xml' => '<mi>'.$fn.'</mi>', 'func' => true, 'limits' => false]; | |
| 445 | + | |
| 446 | + case 'mathbf': case 'boldsymbol': case 'bm': case 'mathrm': case 'mathit': | |
| 447 | + case 'mathbb': case 'mathcal': case 'mathsf': case 'mathtt': | |
| 448 | + $map = [ | |
| 449 | + 'mathbf' => 'bold', 'boldsymbol' => 'bold-italic', 'bm' => 'bold-italic', | |
| 450 | + 'mathrm' => 'normal', 'mathit' => 'italic', 'mathbb' => 'double-struck', | |
| 451 | + 'mathcal' => 'script', 'mathsf' => 'sans-serif', 'mathtt' => 'monospace', | |
| 452 | + ]; | |
| 453 | + $prev = $this->variant; | |
| 454 | + $this->variant = $map[$name]; | |
| 455 | + $xml = $this->parseGroup(); | |
| 456 | + $this->variant = $prev; | |
| 457 | + return $this->node($xml); | |
| 458 | + | |
| 459 | + case 'overline': | |
| 460 | + $base = $this->parseGroup(); | |
| 461 | + return $this->node("<mover>{$base}<mo stretchy=\"true\">‾</mo></mover>"); | |
| 462 | + case 'underline': | |
| 463 | + $base = $this->parseGroup(); | |
| 464 | + return $this->node("<munder>{$base}<mo stretchy=\"true\">_</mo></munder>"); | |
| 465 | + case 'underbrace': | |
| 466 | + $base = $this->parseGroup(); | |
| 467 | + return ['xml' => "<munder>{$base}<mo stretchy=\"true\">⏟</mo></munder>", | |
| 468 | + 'func' => false, 'limits' => true, 'under' => true]; | |
| 469 | + case 'overbrace': | |
| 470 | + $base = $this->parseGroup(); | |
| 471 | + return ['xml' => "<mover>{$base}<mo stretchy=\"true\">⏞</mo></mover>", | |
| 472 | + 'func' => false, 'limits' => true, 'under' => true]; | |
| 473 | + case 'overrightarrow': | |
| 474 | + $base = $this->parseGroup(); | |
| 475 | + return $this->node("<mover>{$base}<mo stretchy=\"true\">→</mo></mover>"); | |
| 476 | + } | |
| 477 | + | |
| 478 | + if (isset(self::ACCENTS[$name])) { | |
| 479 | + $base = $this->parseGroup(); | |
| 480 | + $acc = self::ACCENTS[$name]; | |
| 481 | + $stretch = str_starts_with($name, 'wide') ? 'true' : 'false'; | |
| 482 | + return $this->node("<mover accent=\"true\">{$base}<mo stretchy=\"{$stretch}\">{$acc}</mo></mover>"); | |
| 483 | + } | |
| 484 | + | |
| 485 | + $this->errors[] = '\\'.$name; | |
| 486 | + return $this->node('<merror><mtext>\\'.$this->esc($name).'</mtext></merror>'); | |
| 487 | + } | |
| 488 | + | |
| 489 | + /** | |
| 490 | + * \left … \right を読む | |
| 491 | + * | |
| 492 | + * @return array ノード | |
| 493 | + */ | |
| 494 | + private function parseLeftRight(): array { | |
| 495 | + $open = $this->readDelimiter(); | |
| 496 | + $inner = $this->parseList(); | |
| 497 | + $close = ''; | |
| 498 | + if ($this->startsWith('\\right')) { | |
| 499 | + $this->readCommand(); | |
| 500 | + $close = $this->readDelimiter(); | |
| 501 | + } else { | |
| 502 | + $this->errors[] = '\\left without \\right'; | |
| 503 | + } | |
| 504 | + | |
| 505 | + $xml = '<mrow>'; | |
| 506 | + if ($open !== '') $xml .= '<mo fence="true" stretchy="true" form="prefix">'.$this->esc($open).'</mo>'; | |
| 507 | + $xml .= $this->row($inner); | |
| 508 | + if ($close !== '') $xml .= '<mo fence="true" stretchy="true" form="postfix">'.$this->esc($close).'</mo>'; | |
| 509 | + $xml .= '</mrow>'; | |
| 510 | + return $this->node($xml); | |
| 511 | + } | |
| 512 | + | |
| 513 | + /** | |
| 514 | + * \begin{env} … \end{env} を読む | |
| 515 | + * | |
| 516 | + * @return array ノード | |
| 517 | + */ | |
| 518 | + private function parseEnvironment(): array { | |
| 519 | + $env = trim($this->readGroupRaw()); | |
| 520 | + $align = []; | |
| 521 | + | |
| 522 | + if ($env === 'array') { | |
| 523 | + $spec = $this->readGroupRaw(); | |
| 524 | + foreach (str_split(preg_replace('/[^lcr]/', '', $spec)) as $ch) | |
| 525 | + $align[] = ['l' => 'left', 'c' => 'center', 'r' => 'right'][$ch]; | |
| 526 | + } | |
| 527 | + | |
| 528 | + $known = isset(self::ENV_FENCES[$env]) | |
| 529 | + || in_array($env, ['cases', 'aligned', 'align', 'align*', 'gathered', 'array', 'split'], true); | |
| 530 | + if (!$known) $this->errors[] = '\\begin{'.$env.'}'; | |
| 531 | + | |
| 532 | + // 行と列を読む / read rows and cells | |
| 533 | + $rows = []; | |
| 534 | + $cells = []; | |
| 535 | + while (true) { | |
| 536 | + $cells[] = $this->row($this->parseList()); | |
| 537 | + if ($this->peek() === '&') { | |
| 538 | + $this->pos++; | |
| 539 | + continue; | |
| 540 | + } | |
| 541 | + if ($this->startsWith('\\\\')) { | |
| 542 | + $this->pos += 2; | |
| 543 | + $rows[] = $cells; | |
| 544 | + $cells = []; | |
| 545 | + continue; | |
| 546 | + } | |
| 547 | + if ($this->startsWith('\\end')) { | |
| 548 | + $this->readCommand(); | |
| 549 | + $this->readGroupRaw(); | |
| 550 | + break; | |
| 551 | + } | |
| 552 | + if ($this->peek() === '}') { // 迷子の}を読み飛ばす / skip a stray } | |
| 553 | + $this->pos++; | |
| 554 | + continue; | |
| 555 | + } | |
| 556 | + break; // 文末 / end of input | |
| 557 | + } | |
| 558 | + // 最後の空行を捨てる / drop an empty trailing row | |
| 559 | + if (!(count($cells) === 1 && $cells[0] === '')) $rows[] = $cells; | |
| 560 | + | |
| 561 | + $isAligned = in_array($env, ['aligned', 'align', 'align*', 'split'], true); | |
| 562 | + $table = '<mtable'.($isAligned || $env === 'cases' ? ' displaystyle="true"' : '').'>'; | |
| 563 | + foreach ($rows as $r) { | |
| 564 | + $table .= '<mtr>'; | |
| 565 | + foreach ($r as $i => $cell) { | |
| 566 | + $ca = ''; | |
| 567 | + if ($isAligned) $ca = $i % 2 === 0 ? 'right' : 'left'; | |
| 568 | + else if ($env === 'cases') $ca = 'left'; | |
| 569 | + else if (isset($align[$i])) $ca = $align[$i]; | |
| 570 | + $style = $ca !== '' && $ca !== 'center' ? " style=\"text-align: {$ca};\"" : ''; | |
| 571 | + // alignedの列間の隙間を詰める / close the gap between aligned columns | |
| 572 | + if ($isAligned) $style = " style=\"text-align: {$ca}; padding-".($i % 2 === 0 ? 'right' : 'left').': 0;"'; | |
| 573 | + $table .= "<mtd{$style}>{$cell}</mtd>"; | |
| 574 | + } | |
| 575 | + $table .= '</mtr>'; | |
| 576 | + } | |
| 577 | + $table .= '</mtable>'; | |
| 578 | + | |
| 579 | + [$open, $close] = self::ENV_FENCES[$env] ?? ['', '']; | |
| 580 | + if ($env === 'cases') $open = '{'; | |
| 581 | + | |
| 582 | + if ($open === '' && $close === '') return $this->node($table); | |
| 583 | + | |
| 584 | + $xml = '<mrow>'; | |
| 585 | + if ($open !== '') $xml .= '<mo fence="true" stretchy="true" form="prefix">'.$this->esc($open).'</mo>'; | |
| 586 | + $xml .= $table; | |
| 587 | + if ($close !== '') $xml .= '<mo fence="true" stretchy="true" form="postfix">'.$this->esc($close).'</mo>'; | |
| 588 | + $xml .= '</mrow>'; | |
| 589 | + return $this->node($xml); | |
| 590 | + } | |
| 591 | + | |
| 592 | + //------------------------------------------ | |
| 593 | + // 字句解析 | |
| 594 | + //------------------------------------------ | |
| 595 | + | |
| 596 | + private function peek(): string { | |
| 597 | + return $this->pos < strlen($this->src) ? $this->src[$this->pos] : ''; | |
| 598 | + } | |
| 599 | + | |
| 600 | + private function startsWith(string $s): bool { | |
| 601 | + if (substr($this->src, $this->pos, strlen($s)) !== $s) return false; | |
| 602 | + // \endや\rightの後ろに英字が続く場合は別のコマンド / \endpoint is not \end | |
| 603 | + if (ctype_alpha(substr($s, -1))) { | |
| 604 | + $after = substr($this->src, $this->pos + strlen($s), 1); | |
| 605 | + if ($after !== '' && ctype_alpha($after)) return false; | |
| 606 | + } | |
| 607 | + return true; | |
| 608 | + } | |
| 609 | + | |
| 610 | + private function skipSpace(): void { | |
| 611 | + while ($this->pos < strlen($this->src) && ctype_space($this->src[$this->pos])) $this->pos++; | |
| 612 | + } | |
| 613 | + | |
| 614 | + /** | |
| 615 | + * 現在位置が終端記号かどうか | |
| 616 | + */ | |
| 617 | + private function atStop(): bool { | |
| 618 | + $c = $this->peek(); | |
| 619 | + if ($c === '' || $c === '}' || $c === '&') return true; | |
| 620 | + return $this->startsWith('\\\\') || $this->startsWith('\\end') || $this->startsWith('\\right'); | |
| 621 | + } | |
| 622 | + | |
| 623 | + /** | |
| 624 | + * \の後のコマンド名を読む(英字列、又は記号1文字) | |
| 625 | + */ | |
| 626 | + private function readCommand(): string { | |
| 627 | + $this->pos++; // \ | |
| 628 | + if (preg_match('/\G[a-zA-Z]+/', $this->src, $m, 0, $this->pos)) { | |
| 629 | + $this->pos += strlen($m[0]); | |
| 630 | + return $m[0]; | |
| 631 | + } | |
| 632 | + preg_match('/\G./us', $this->src, $m, 0, $this->pos); | |
| 633 | + $ch = $m[0] ?? ''; | |
| 634 | + $this->pos += strlen($ch); | |
| 635 | + return $ch; | |
| 636 | + } | |
| 637 | + | |
| 638 | + /** | |
| 639 | + * {…}の中身を加工せずに読む(\text等) | |
| 640 | + */ | |
| 641 | + private function readGroupRaw(): string { | |
| 642 | + $this->skipSpace(); | |
| 643 | + if ($this->peek() !== '{') { | |
| 644 | + preg_match('/\G./us', $this->src, $m, 0, $this->pos); | |
| 645 | + $ch = $m[0] ?? ''; | |
| 646 | + $this->pos += strlen($ch); | |
| 647 | + return $ch; | |
| 648 | + } | |
| 649 | + $this->pos++; | |
| 650 | + $depth = 1; | |
| 651 | + $out = ''; | |
| 652 | + $len = strlen($this->src); | |
| 653 | + while ($this->pos < $len) { | |
| 654 | + $c = $this->src[$this->pos]; | |
| 655 | + if ($c === '\\' && $this->pos + 1 < $len) { | |
| 656 | + $next = $this->src[$this->pos + 1]; | |
| 657 | + // \{ \} \\ はそのまま文字にする / escaped braces become literal characters | |
| 658 | + $out .= ($next === '{' || $next === '}' || $next === '\\' || $next === '_' | |
| 659 | + || $next === '%' || $next === '&' || $next === '#' || $next === '$') ? $next : $c.$next; | |
| 660 | + $this->pos += 2; | |
| 661 | + continue; | |
| 662 | + } | |
| 663 | + if ($c === '{') $depth++; | |
| 664 | + if ($c === '}') { | |
| 665 | + $depth--; | |
| 666 | + if ($depth === 0) { | |
| 667 | + $this->pos++; | |
| 668 | + break; | |
| 669 | + } | |
| 670 | + } | |
| 671 | + $out .= $c; | |
| 672 | + $this->pos++; | |
| 673 | + } | |
| 674 | + return $out; | |
| 675 | + } | |
| 676 | + | |
| 677 | + /** | |
| 678 | + * [...]の中身を読む(\sqrt[n]用) | |
| 679 | + */ | |
| 680 | + private function readBracketRaw(): string { | |
| 681 | + $this->pos++; // [ | |
| 682 | + $depth = 0; | |
| 683 | + $out = ''; | |
| 684 | + $len = strlen($this->src); | |
| 685 | + while ($this->pos < $len) { | |
| 686 | + $c = $this->src[$this->pos]; | |
| 687 | + if ($c === '{') $depth++; | |
| 688 | + if ($c === '}') $depth--; | |
| 689 | + if ($c === ']' && $depth === 0) { | |
| 690 | + $this->pos++; | |
| 691 | + break; | |
| 692 | + } | |
| 693 | + $out .= $c; | |
| 694 | + $this->pos++; | |
| 695 | + } | |
| 696 | + return $out; | |
| 697 | + } | |
| 698 | + | |
| 699 | + /** | |
| 700 | + * \left、\rightの後の区切り記号を読む | |
| 701 | + */ | |
| 702 | + private function readDelimiter(): string { | |
| 703 | + $this->skipSpace(); | |
| 704 | + $c = $this->peek(); | |
| 705 | + if ($c === '\\') { | |
| 706 | + $start = $this->pos; | |
| 707 | + $name = $this->readCommand(); | |
| 708 | + $key = '\\'.$name; | |
| 709 | + if (isset(self::DELIMS[$key])) return self::DELIMS[$key]; | |
| 710 | + $this->errors[] = 'delimiter '.$key; | |
| 711 | + return ''; | |
| 712 | + } | |
| 713 | + if ($c === '') return ''; | |
| 714 | + $this->pos++; | |
| 715 | + if (isset(self::DELIMS[$c])) return self::DELIMS[$c]; | |
| 716 | + $this->errors[] = 'delimiter '.$c; | |
| 717 | + return $c; | |
| 718 | + } | |
| 719 | + | |
| 720 | + //------------------------------------------ | |
| 721 | + // 出力ヘルパー | |
| 722 | + //------------------------------------------ | |
| 723 | + | |
| 724 | + /** | |
| 725 | + * 部分文字列を同じ設定で変換する(\sqrtの指数等) | |
| 726 | + */ | |
| 727 | + private function renderSub(string $tex): string { | |
| 728 | + [$src, $pos] = [$this->src, $this->pos]; | |
| 729 | + $this->src = $tex; | |
| 730 | + $this->pos = 0; | |
| 731 | + $xml = $this->row($this->parseList()); | |
| 732 | + [$this->src, $this->pos] = [$src, $pos]; | |
| 733 | + return $xml; | |
| 734 | + } | |
| 735 | + | |
| 736 | + private function node(string $xml): array { | |
| 737 | + return ['xml' => $xml, 'func' => false, 'limits' => false]; | |
| 738 | + } | |
| 739 | + | |
| 740 | + /** | |
| 741 | + * 複数のノードをmrowで包む(1つだけならそのまま) | |
| 742 | + */ | |
| 743 | + private function row(array $nodes, bool $forceRow = false): string { | |
| 744 | + if (count($nodes) === 1 && !$forceRow) return $nodes[0]; | |
| 745 | + if (count($nodes) === 1 && str_starts_with($nodes[0], '<mrow>')) return $nodes[0]; | |
| 746 | + return '<mrow>'.implode('', $nodes).'</mrow>'; | |
| 747 | + } | |
| 748 | + | |
| 749 | + /** | |
| 750 | + * 添え字はmrowで包んで1要素にする | |
| 751 | + */ | |
| 752 | + private function wrap(string $xml): string { | |
| 753 | + if (preg_match('/^<(mi|mn|mo|mrow|mtext|mfrac|msqrt)[ >]/', $xml) && substr_count($xml, '<m') === 1) | |
| 754 | + return $xml; | |
| 755 | + if (str_starts_with($xml, '<mrow>') && str_ends_with($xml, '</mrow>')) return $xml; | |
| 756 | + return '<mrow>'.$xml.'</mrow>'; | |
| 757 | + } | |
| 758 | + | |
| 759 | + private function esc(string $s): string { | |
| 760 | + return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); | |
| 761 | + } | |
| 762 | + | |
| 763 | + /** | |
| 764 | + * 識別子(1文字)を書体付きで出力する | |
| 765 | + */ | |
| 766 | + private function ident(string $ch): string { | |
| 767 | + $styled = $this->styleChar($ch); | |
| 768 | + if ($this->variant === 'normal' && $styled === $ch) { | |
| 769 | + return '<mi mathvariant="normal">'.$this->esc($ch).'</mi>'; | |
| 770 | + } | |
| 771 | + return '<mi>'.$this->esc($styled).'</mi>'; | |
| 772 | + } | |
| 773 | + | |
| 774 | + private function styleString(string $s): string { | |
| 775 | + $out = ''; | |
| 776 | + foreach (preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY) as $ch) $out .= $this->styleChar($ch); | |
| 777 | + return $out; | |
| 778 | + } | |
| 779 | + | |
| 780 | + /** | |
| 781 | + * 書体をUnicodeの数学用英数字記号に変換する | |
| 782 | + * (MathML Coreではmathvariant="normal"以外は使えない為) | |
| 783 | + */ | |
| 784 | + private function styleChar(string $ch): string { | |
| 785 | + $v = $this->variant; | |
| 786 | + if ($v === '' || $v === 'normal' || $v === 'italic') return $ch; | |
| 787 | + | |
| 788 | + $cp = mb_ord($ch, 'UTF-8'); | |
| 789 | + $isUpper = $cp >= 0x41 && $cp <= 0x5A; | |
| 790 | + $isLower = $cp >= 0x61 && $cp <= 0x7A; | |
| 791 | + $isDigit = $cp >= 0x30 && $cp <= 0x39; | |
| 792 | + $isGreekUpper = $cp >= 0x391 && $cp <= 0x3A9; | |
| 793 | + $isGreekLower = $cp >= 0x3B1 && $cp <= 0x3C9; | |
| 794 | + | |
| 795 | + $holes = [ | |
| 796 | + 'double-struck' => ['C' => 0x2102, 'H' => 0x210D, 'N' => 0x2115, 'P' => 0x2119, | |
| 797 | + 'Q' => 0x211A, 'R' => 0x211D, 'Z' => 0x2124], | |
| 798 | + 'script' => ['B' => 0x212C, 'E' => 0x2130, 'F' => 0x2131, 'H' => 0x210B, 'I' => 0x2110, | |
| 799 | + 'L' => 0x2112, 'M' => 0x2133, 'R' => 0x211B, 'e' => 0x212F, 'g' => 0x210A, | |
| 800 | + 'o' => 0x2134], | |
| 801 | + ]; | |
| 802 | + if (isset($holes[$v][$ch])) return mb_chr($holes[$v][$ch], 'UTF-8'); | |
| 803 | + | |
| 804 | + // [大文字, 小文字, 数字, ギリシャ大文字, ギリシャ小文字] | |
| 805 | + $base = [ | |
| 806 | + 'bold' => [0x1D400, 0x1D41A, 0x1D7CE, 0x1D6A8, 0x1D6C2], | |
| 807 | + 'bold-italic' => [0x1D468, 0x1D482, 0x1D7CE, 0x1D71C, 0x1D736], | |
| 808 | + 'double-struck' => [0x1D538, 0x1D552, 0x1D7D8, null, null], | |
| 809 | + 'script' => [0x1D49C, 0x1D4B6, null, null, null], | |
| 810 | + 'sans-serif' => [0x1D5A0, 0x1D5BA, 0x1D7E2, null, null], | |
| 811 | + 'monospace' => [0x1D670, 0x1D68A, 0x1D7F6, null, null], | |
| 812 | + ][$v] ?? null; | |
| 813 | + if ($base === null) return $ch; | |
| 814 | + | |
| 815 | + if ($isUpper && $base[0]) return mb_chr($base[0] + $cp - 0x41, 'UTF-8'); | |
| 816 | + if ($isLower && $base[1]) return mb_chr($base[1] + $cp - 0x61, 'UTF-8'); | |
| 817 | + if ($isDigit && $base[2]) return mb_chr($base[2] + $cp - 0x30, 'UTF-8'); | |
| 818 | + if ($isGreekUpper && $base[3]) return mb_chr($base[3] + $cp - 0x391, 'UTF-8'); | |
| 819 | + if ($isGreekLower && $base[4]) return mb_chr($base[4] + $cp - 0x3B1, 'UTF-8'); | |
| 820 | + return $ch; | |
| 821 | + } | |
| 822 | +} |
| @@ -46,7 +46,20 @@ | ||
| 46 | 46 | private array $codeBlockContent = []; |
| 47 | 47 | private const METADATA_LINE = "----"; |
| 48 | 48 | private array $algebraicPlaceholder = []; |
| 49 | + private array $codePlaceholder = []; | |
| 50 | + private array $mathErrors = []; | |
| 51 | + private ?Latex $latex = null; | |
| 52 | + private static int $tabGroupCount = 0; | |
| 49 | 53 | |
| 54 | + // コードタブのラベル / labels for code tabs | |
| 55 | + private const LANG_LABELS = [ | |
| 56 | + 'c' => 'C', 'cpp' => 'C++', 'cc' => 'C++', 'hpp' => 'C++', 'cs' => 'C#', | |
| 57 | + 'slang' => 'Slang', 'glsl' => 'GLSL', 'hlsl' => 'HLSL', 'msl' => 'MSL', | |
| 58 | + 'wgsl' => 'WGSL', 'spirv' => 'SPIR-V', 'asm' => 'Assembly', 'php' => 'PHP', | |
| 59 | + 'js' => 'JavaScript', 'sh' => 'Shell', 'txt' => 'Text', 'json' => 'JSON', | |
| 60 | + 'go' => 'Go', 'rust' => 'Rust', 'zig' => 'Zig', 'odin' => 'Odin', 'sql' => 'SQL', | |
| 61 | + ]; | |
| 62 | + | |
| 50 | 63 | public function __construct(string $path, string $section, ?string $lang = null) { |
| 51 | 64 | $this->html = []; |
| 52 | 65 | if ($lang) $this->path = ROOT.$section.$lang.'/'.$path.'.md'; |
| @@ -115,14 +128,44 @@ | ||
| 115 | 128 | $tableHeaders = []; |
| 116 | 129 | $tableRows = []; |
| 117 | 130 | $inTable = false; |
| 118 | - $i = 0; | |
| 131 | + $inMathBlock = false; | |
| 132 | + $mathLines = []; | |
| 133 | + $inTabs = false; | |
| 134 | + $tabItems = []; | |
| 135 | + $codeBlockLabel = ''; | |
| 119 | 136 | $headerCount = 1; |
| 137 | + $this->mathErrors = []; | |
| 138 | + $i = 0; | |
| 120 | 139 | |
| 140 | + $flush = function() use (&$currentParagraph, &$inList, &$listItems, &$listLevel, | |
| 141 | + &$inTable, &$tableHeaders, &$tableRows, | |
| 142 | + &$inBlockquote, &$blockquoteContent): void { | |
| 143 | + if ($inList) { | |
| 144 | + $this->html[] = $this->createList($listItems); | |
| 145 | + $listItems = []; | |
| 146 | + $inList = false; | |
| 147 | + $listLevel = 0; | |
| 148 | + } | |
| 149 | + if ($inTable) { | |
| 150 | + $this->html[] = $this->createTable($tableHeaders, $tableRows); | |
| 151 | + $tableHeaders = []; | |
| 152 | + $tableRows = []; | |
| 153 | + $inTable = false; | |
| 154 | + } | |
| 155 | + if ($inBlockquote) { | |
| 156 | + $this->html[] = $this->createBlockquote($blockquoteContent); | |
| 157 | + $blockquoteContent = []; | |
| 158 | + $inBlockquote = false; | |
| 159 | + } | |
| 160 | + if (!empty($currentParagraph)) { | |
| 161 | + $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 162 | + $currentParagraph = []; | |
| 163 | + } | |
| 164 | + }; | |
| 165 | + | |
| 121 | 166 | foreach ($lines as $line) { |
| 122 | 167 | $i++; |
| 123 | - $line = rtrim($line, "\r\n"); | |
| 124 | - $hasBR = str_ends_with($line, '\\'); | |
| 125 | - $line = rtrim($line, "\\"); | |
| 168 | + $rawLine = rtrim($line, "\r\n"); | |
| 126 | 169 | |
| 127 | 170 | // コメント |
| 128 | 171 | // if (str_starts_with($line, '//')) { |
| @@ -132,30 +175,82 @@ | ||
| 132 | 175 | // continue; |
| 133 | 176 | // } |
| 134 | 177 | |
| 135 | - // コードブロック | |
| 136 | - if (preg_match('/^```(\w*)$/', $line, $matches)) { | |
| 178 | + // コードブロック(```言語 ラベル) | |
| 179 | + if (preg_match('/^```([\w\+\-#\.]*)(?:\s+(.*\S))?\s*$/u', $rawLine, $matches)) { | |
| 137 | 180 | if (!$this->inCodeBlock) { |
| 138 | - if (!empty($currentParagraph)) { | |
| 139 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 140 | - $currentParagraph = []; | |
| 141 | - } | |
| 181 | + $flush(); | |
| 142 | 182 | $this->inCodeBlock = true; |
| 143 | 183 | $this->codeBlockLanguage = $matches[1]; |
| 184 | + $codeBlockLabel = $matches[2] ?? ''; | |
| 144 | 185 | continue; |
| 145 | 186 | } else { |
| 146 | - $this->html[] = $this->createCodeBlock(); | |
| 187 | + if ($inTabs) { | |
| 188 | + $tabItems[] = [ | |
| 189 | + 'lang' => $this->codeBlockLanguage, | |
| 190 | + 'label' => $codeBlockLabel, | |
| 191 | + 'code' => $this->codeBlockContent, | |
| 192 | + ]; | |
| 193 | + } else { | |
| 194 | + $this->html[] = $this->createCodeBlock(); | |
| 195 | + } | |
| 147 | 196 | $this->inCodeBlock = false; |
| 148 | 197 | $this->codeBlockLanguage = ''; |
| 149 | 198 | $this->codeBlockContent = []; |
| 199 | + $codeBlockLabel = ''; | |
| 150 | 200 | continue; |
| 151 | 201 | } |
| 152 | 202 | } |
| 153 | 203 | |
| 154 | 204 | if ($this->inCodeBlock) { |
| 155 | - $this->codeBlockContent[] = $line; | |
| 205 | + $this->codeBlockContent[] = $rawLine; | |
| 156 | 206 | continue; |
| 157 | 207 | } |
| 158 | 208 | |
| 209 | + if ($inMathBlock) { | |
| 210 | + if (trim($rawLine) === '$$') { | |
| 211 | + $this->html[] = $this->createDisplayMath(implode("\n", $mathLines)); | |
| 212 | + $mathLines = []; | |
| 213 | + $inMathBlock = false; | |
| 214 | + } else { | |
| 215 | + $mathLines[] = $rawLine; | |
| 216 | + } | |
| 217 | + continue; | |
| 218 | + } | |
| 219 | + | |
| 220 | + if (trim($rawLine) === '$$') { | |
| 221 | + $flush(); | |
| 222 | + $inMathBlock = true; | |
| 223 | + continue; | |
| 224 | + } | |
| 225 | + | |
| 226 | + // 1行の数式ブロック:$$…$$だけの行 / a line holding only $$…$$ is display math | |
| 227 | + if (preg_match('/^\s*\$\$(.+)\$\$\s*$/u', $rawLine, $matches) | |
| 228 | + && substr_count($rawLine, '$$') === 2) { | |
| 229 | + $flush(); | |
| 230 | + $this->html[] = $this->createDisplayMath($matches[1]); | |
| 231 | + continue; | |
| 232 | + } | |
| 233 | + | |
| 234 | + // コードタブ(:::tabs … :::) / code tabs | |
| 235 | + if (preg_match('/^:::\s*tabs\s*$/', trim($rawLine))) { | |
| 236 | + $flush(); | |
| 237 | + $inTabs = true; | |
| 238 | + $tabItems = []; | |
| 239 | + continue; | |
| 240 | + } | |
| 241 | + | |
| 242 | + if ($inTabs) { | |
| 243 | + if (trim($rawLine) === ':::') { | |
| 244 | + $this->html[] = $this->createTabs($tabItems); | |
| 245 | + $tabItems = []; | |
| 246 | + $inTabs = false; | |
| 247 | + } | |
| 248 | + continue; // タブの中ではコードブロック以外は無視 / only code blocks inside tabs | |
| 249 | + } | |
| 250 | + | |
| 251 | + $hasBR = str_ends_with($rawLine, '\\'); | |
| 252 | + $line = rtrim($rawLine, "\\"); | |
| 253 | + | |
| 159 | 254 | // テーブルの処理 |
| 160 | 255 | if (preg_match('/^\|(.+)\|$/', $line)) { |
| 161 | 256 | if (!empty($currentParagraph)) { |
| @@ -289,15 +384,27 @@ | ||
| 289 | 384 | } |
| 290 | 385 | } |
| 291 | 386 | |
| 292 | - if ($inList) $this->html[] = $this->createList($listItems); | |
| 293 | - if ($inBlockquote) $this->html[] = $this->createBlockquote($blockquoteContent); | |
| 294 | - if ($inTable) $this->html[] = $this->createTable($tableHeaders, $tableRows); | |
| 295 | - if (!empty($currentParagraph)) | |
| 296 | - $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>"; | |
| 387 | + if ($this->inCodeBlock) { | |
| 388 | + $this->html[] = $this->createCodeBlock(); | |
| 389 | + $this->inCodeBlock = false; | |
| 390 | + $this->codeBlockContent = []; | |
| 391 | + } | |
| 392 | + if ($inMathBlock) $this->html[] = $this->createDisplayMath(implode("\n", $mathLines)); | |
| 393 | + if ($inTabs) $this->html[] = $this->createTabs($tabItems); | |
| 394 | + $flush(); | |
| 297 | 395 | |
| 298 | 396 | return implode("\n", $this->html); |
| 299 | 397 | } |
| 300 | 398 | |
| 399 | + /** | |
| 400 | + * 直前のparse()で見つかった、変換出来なかったLaTeXコマンド | |
| 401 | + * | |
| 402 | + * @return array エラーの配列 | |
| 403 | + */ | |
| 404 | + public function getMathErrors(): array { | |
| 405 | + return $this->mathErrors; | |
| 406 | + } | |
| 407 | + | |
| 301 | 408 | // 機能性メソッド |
| 302 | 409 | |
| 303 | 410 | /** |
| @@ -308,8 +415,18 @@ | ||
| 308 | 415 | */ |
| 309 | 416 | private function parseInline(string $text): string { |
| 310 | 417 | $this->algebraicPlaceholder = []; |
| 418 | + $this->codePlaceholder = []; | |
| 311 | 419 | $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); |
| 312 | 420 | |
| 421 | + // インラインコードは他の記法より先に保護する(中の*や_や\をそのまま表示する為) | |
| 422 | + $text = preg_replace_callback('/(?<!\\\\)`(.+?)`/u', function($matches): string { | |
| 423 | + $placeholder = "{{CODE".count($this->codePlaceholder).'CODE}}'; | |
| 424 | + // 記号の前の\だけをエスケープとして消す(`json\_encode`や`C:\\`の為)。\nや\0はそのまま。 | |
| 425 | + $code = preg_replace('/\\\\([\\\\`*_~$%^!\[\]()#{}|.+\-])/u', '$1', $matches[1]); | |
| 426 | + $this->codePlaceholder[$placeholder] = '<code>'.$code.'</code>'; | |
| 427 | + return $placeholder; | |
| 428 | + }, $text); | |
| 429 | + | |
| 313 | 430 | $patterns = [ |
| 314 | 431 | // 数式 |
| 315 | 432 | '/(?<!\\\\)\$\$([^$]+)\$\$/u' => function($matches): string { |
| @@ -343,26 +460,32 @@ | ||
| 343 | 460 | '/(?<!\\\\)\[(.+?)\]\((.+?)\)/u' => '<a href="$2">$1</a>', |
| 344 | 461 | // 振り仮名 |
| 345 | 462 | '/(?<!\\\\)<(.+?)>\((.+?)\)/u' => '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>', |
| 346 | - // インラインコード | |
| 347 | - '/(?<!\\\\)`(.+?)`/u' => '<code>$1</code>', | |
| 348 | 463 | ]; |
| 349 | 464 | |
| 350 | 465 | foreach ($patterns as $pattern => $replacement) { |
| 351 | 466 | if (is_callable($replacement)) { |
| 352 | 467 | $text = preg_replace_callback($pattern, $replacement, $text); |
| 353 | 468 | } else { |
| 469 | + // kys([$pattern, $replacement, $text]); | |
| 354 | 470 | $result = preg_replace($pattern, $replacement, $text); |
| 471 | + // kys($result); | |
| 355 | 472 | if ($result === null) continue; |
| 473 | + // kys($text); | |
| 356 | 474 | $text = $result; |
| 357 | 475 | } |
| 358 | 476 | } |
| 359 | 477 | |
| 360 | - // プレースホルダーの数式に交換 | |
| 478 | + // エスケープ文字の\を消す(数式とコードを戻す前に) | |
| 479 | + $text = preg_replace('/\\\\(.)/u', '$1', $text); | |
| 480 | + | |
| 481 | + // プレースホルダーを数式とコードに交換 | |
| 361 | 482 | foreach ($this->algebraicPlaceholder as $placeholder => $expr) { |
| 362 | 483 | $text = str_replace($placeholder, $this->parseAlgebraic($expr), $text); |
| 363 | 484 | } |
| 364 | 485 | |
| 365 | - $text = preg_replace('/\\\\(.)/u', '$1', $text); | |
| 486 | + foreach ($this->codePlaceholder as $placeholder => $code) { | |
| 487 | + $text = str_replace($placeholder, $code, $text); | |
| 488 | + } | |
| 366 | 489 | |
| 367 | 490 | return $text; |
| 368 | 491 | } |
| @@ -371,92 +494,56 @@ | ||
| 371 | 494 | * 数式記法をパースする |
| 372 | 495 | * |
| 373 | 496 | * @param string $expression パースする数式 |
| 374 | - * @return string MathJax用にフォーマットされた数式 | |
| 497 | + * @param bool $display ブロック表示ならtrue | |
| 498 | + * @return string MathMLとしてレンダリングされた数式 | |
| 375 | 499 | */ |
| 376 | - private function parseAlgebraic(string $expression): string { | |
| 377 | - // HTMLエスケープ | |
| 500 | + private function parseAlgebraic(string $expression, bool $display = false): string { | |
| 501 | + // HTMLエスケープを戻す(Latexクラスが自分でエスケープする) | |
| 378 | 502 | $expression = htmlspecialchars_decode($expression, ENT_QUOTES); |
| 379 | 503 | |
| 380 | - // ふぃりしゃもじのマッピング | |
| 381 | - $greekLetters = [ | |
| 382 | - 'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ', | |
| 383 | - 'epsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ', | |
| 384 | - 'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ', | |
| 385 | - 'nu' => 'ν', 'xi' => 'ξ', 'omicron' => 'ο', 'pi' => 'π', | |
| 386 | - 'rho' => 'ρ', 'sigma' => 'σ', 'tau' => 'τ', 'upsilon' => 'υ', | |
| 387 | - 'phi' => 'φ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω', | |
| 388 | - 'Alpha' => 'Α', 'Beta' => 'Β', 'Gamma' => 'Γ', 'Delta' => 'Δ', | |
| 389 | - 'Epsilon' => 'Ε', 'Zeta' => 'Ζ', 'Eta' => 'Η', 'Theta' => 'Θ', | |
| 390 | - 'Iota' => 'Ι', 'Kappa' => 'Κ', 'Lambda' => 'Λ', 'Mu' => 'Μ', | |
| 391 | - 'Nu' => 'Ν', 'Xi' => 'Ξ', 'Omicron' => 'Ο', 'Pi' => 'Π', | |
| 392 | - 'Rho' => 'Ρ', 'Sigma' => 'Σ', 'Tau' => 'Τ', 'Upsilon' => 'Υ', | |
| 393 | - 'Phi' => 'Φ', 'Chi' => 'Χ', 'Psi' => 'Ψ', 'Omega' => 'Ω', | |
| 394 | - ]; | |
| 504 | + if ($this->latex === null) $this->latex = new Latex(); | |
| 505 | + $mathml = $this->latex->render($expression, $display); | |
| 506 | + foreach ($this->latex->getErrors() as $err) $this->mathErrors[] = $err; | |
| 395 | 507 | |
| 396 | - // ギリシャ文字を置換 | |
| 397 | - foreach ($greekLetters as $text => $symbol) { | |
| 398 | - $expression = preg_replace("/\b$text\b/", $symbol, $expression); | |
| 399 | - } | |
| 508 | + return $mathml; | |
| 509 | + } | |
| 400 | 510 | |
| 401 | - // 上付き文字(例: x^2) | |
| 402 | - $expression = preg_replace_callback('/(\w+)\^(\{?(\d+|\w+)\}?)/', function($matches) { | |
| 403 | - $base = $matches[1]; | |
| 404 | - $exponent = $matches[2]; | |
| 405 | - if ($exponent[0] === '{') $exponent = substr($exponent, 1, -1); | |
| 511 | + /** | |
| 512 | + * ブロック表示の数式を作成する | |
| 513 | + * | |
| 514 | + * @param string $expression LaTeXの数式 | |
| 515 | + * @return string HTMLの数式ブロック | |
| 516 | + */ | |
| 517 | + private function createDisplayMath(string $expression): string { | |
| 518 | + return " <div class=\"math-display\">".$this->parseAlgebraic($expression, true)."</div>"; | |
| 519 | + } | |
| 406 | 520 | |
| 407 | - $superscripts = str_split($exponent); | |
| 408 | - $supMap = [ | |
| 409 | - '0' => '⁰', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴', | |
| 410 | - '5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹', | |
| 411 | - 'a' => 'ᵃ', 'b' => 'ᵇ', 'c' => 'ᶜ', 'd' => 'ᵈ', 'e' => 'ᵉ', | |
| 412 | - 'i' => 'ⁱ', 'n' => 'ⁿ', | |
| 413 | - ]; | |
| 414 | - $converted = ''; | |
| 521 | + /** | |
| 522 | + * コードタブを作成する(JavaScript無し、ラジオボタンとCSSだけ) | |
| 523 | + * | |
| 524 | + * @param array $items ['lang' => 言語, 'label' => ラベル, 'code' => 行の配列]の配列 | |
| 525 | + * @return string HTMLのタブ | |
| 526 | + */ | |
| 527 | + private function createTabs(array $items): string { | |
| 528 | + if (empty($items)) return ''; | |
| 415 | 529 | |
| 416 | - foreach ($superscripts as $char) $converted .= $supMap[$char] ?? $char; | |
| 530 | + $group = 'code-tabs-'.(++self::$tabGroupCount); | |
| 531 | + $inputs = ''; | |
| 532 | + $panels = ''; | |
| 417 | 533 | |
| 418 | - return $base.'<sup>'.$converted.'</sup>'; | |
| 419 | - }, $expression); | |
| 534 | + foreach ($items as $n => $item) { | |
| 535 | + $id = "{$group}-{$n}"; | |
| 536 | + $label = $item['label'] !== '' ? $item['label'] | |
| 537 | + : (self::LANG_LABELS[strtolower($item['lang'])] ?? ($item['lang'] !== '' ? $item['lang'] : 'Code')); | |
| 538 | + $checked = $n === 0 ? ' checked="checked"' : ''; | |
| 539 | + [$code, $class] = $this->renderCode(implode("\n", $item['code']), $item['lang']); | |
| 420 | 540 | |
| 421 | - // 下付き文字(例: x_1) | |
| 422 | - $expression = preg_replace_callback('/(\w+)_((\d+|\w+)\}?)/', function($matches) { | |
| 423 | - $base = $matches[1]; | |
| 424 | - $subscript = $matches[2]; | |
| 541 | + $inputs .= " <input type=\"radio\" name=\"{$group}\" id=\"{$id}\"{$checked} />\n"; | |
| 542 | + $inputs .= " <label for=\"{$id}\">".htmlspecialchars($label)."</label>\n"; | |
| 543 | + $panels .= " <div class=\"code-tab-panel\"><pre><code{$class}>{$code}</code></pre></div>\n"; | |
| 544 | + } | |
| 425 | 545 | |
| 426 | - if ($subscript[0] === '{') $subscript = substr($subscript, 1, -1); | |
| 427 | - | |
| 428 | - $subscripts = str_split($subscript); | |
| 429 | - $subMap = [ | |
| 430 | - '0' => '₀', '1' => '₁', '2' => '₂', '3' => '₃', '4' => '₄', | |
| 431 | - '5' => '₅', '6' => '₆', '7' => '₇', '8' => '₈', '9' => '₉', | |
| 432 | - ]; | |
| 433 | - $converted = ''; | |
| 434 | - | |
| 435 | - foreach ($subscripts as $char) { | |
| 436 | - $converted .= $subMap[$char] ?? $char; | |
| 437 | - } | |
| 438 | - | |
| 439 | - return $base.'<sub>'.$converted.'</sub>'; | |
| 440 | - }, $expression); | |
| 441 | - | |
| 442 | - // 分数(例: 4/5x, a/b) | |
| 443 | - $expression = preg_replace('/(\d+|\w+)\/(\d*\w+)/', | |
| 444 | - '<span class="fraction"><span class="numerator">$1</span><span class="denominator">$2</span></span>', | |
| 445 | - $expression); | |
| 446 | - | |
| 447 | - // 演算子 | |
| 448 | - $operators = [ | |
| 449 | - '\*' => '・', | |
| 450 | - '!=' => '≠', | |
| 451 | - '>=' => '≥', | |
| 452 | - '<=' => '≤', | |
| 453 | - 'sqrt' => '√', | |
| 454 | - ]; | |
| 455 | - | |
| 456 | - foreach ($operators as $op => $symbol) $expression = preg_replace("/\b$op\b/", $symbol, $expression); | |
| 457 | - | |
| 458 | - // 数式全体をspanで囲む | |
| 459 | - return '<span class="algebraic">'.$expression.'</span>'; | |
| 546 | + return " <div class=\"code-tabs\">\n{$inputs}{$panels} </div>"; | |
| 460 | 547 | } |
| 461 | 548 | |
| 462 | 549 | /** |
| @@ -477,8 +564,11 @@ | ||
| 477 | 564 | foreach ($items as $item) { |
| 478 | 565 | $level = isset($item['level']) ? $item['level'] : $currentLevel; |
| 479 | 566 | |
| 480 | - while ($currentLevel > $level) | |
| 567 | + // ネストから戻る時はレベルを下げる(下げないと無限ループになる) | |
| 568 | + while ($currentLevel > $level) { | |
| 481 | 569 | $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n"; |
| 570 | + $currentLevel--; | |
| 571 | + } | |
| 482 | 572 | |
| 483 | 573 | while ($currentLevel < $level) { |
| 484 | 574 | $currentLevel++; |
| @@ -517,16 +607,29 @@ | ||
| 517 | 607 | * @return string HTMLのコードブロック |
| 518 | 608 | */ |
| 519 | 609 | private function createCodeBlock(): string { |
| 520 | - $code = htmlspecialchars(implode("\n", $this->codeBlockContent)); | |
| 521 | - $class = $this->codeBlockLanguage ? " class=\"language-{$this->codeBlockLanguage}\"" : ''; | |
| 610 | + [$code, $class] = $this->renderCode(implode("\n", $this->codeBlockContent), $this->codeBlockLanguage); | |
| 522 | 611 | return " <p>\n <pre><code{$class}>{$code}</code></pre>"; |
| 523 | - // $raw = implode("\n", $this->codeBlockContent); | |
| 524 | - // $lang = $this->codeBlockLanguage ?: 'txt'; | |
| 525 | - // $class = $lang === 'txt' ? '' : " class=\"language-{$lang}\""; | |
| 526 | - // $highlighted = $this->highlightCode($raw, $lang); | |
| 527 | - // return "<pre><code{$class}>{$highlighted}</code></pre>"; | |
| 528 | 612 | } |
| 529 | 613 | |
| 614 | + /** | |
| 615 | + * コードをHTMLにする(対応している言語はシンタックスハイライトする) | |
| 616 | + * | |
| 617 | + * @param string $raw 生のコード | |
| 618 | + * @param string $lang 言語名(```の後) | |
| 619 | + * @return array [エスケープ済みのコード, class属性] | |
| 620 | + */ | |
| 621 | + private function renderCode(string $raw, string $lang): array { | |
| 622 | + $lang = strtolower($lang); | |
| 623 | + $attr = htmlspecialchars($lang, ENT_QUOTES, 'UTF-8'); | |
| 624 | + | |
| 625 | + // Syntax::colorize()はエスケープ済みのHTMLを返す | |
| 626 | + if ($lang !== '' && Syntax::isSupported($lang)) { | |
| 627 | + return [(new Syntax($lang))->colorize($raw), " class=\"language-{$attr} hl\""]; | |
| 628 | + } | |
| 629 | + | |
| 630 | + return [htmlspecialchars($raw), $lang !== '' ? " class=\"language-{$attr}\"" : '']; | |
| 631 | + } | |
| 632 | + | |
| 530 | 633 | private function createBlockquote(array $content): string { |
| 531 | 634 | return " <p>\n <blockquote>\n ".implode("<br />\n ", $content)."\n </blockquote>\n </p>"; |
| 532 | 635 | } |
| @@ -561,127 +664,9 @@ | ||
| 561 | 664 | $html .= " </tr>\n"; |
| 562 | 665 | } |
| 563 | 666 | $html .= " </tbody>\n"; |
| 564 | - } | |
| 565 | - | |
| 566 | - $html .= " </table>\n </p>"; | |
| 567 | - return $html; | |
| 568 | - } | |
| 569 | - | |
| 570 | - /** | |
| 571 | - * Pure-PHP syntax highlighter | |
| 572 | - * | |
| 573 | - * @param string $code Raw source code | |
| 574 | - * @param string $lang Language identifier (html, css, js, php, …) | |
| 575 | - * @return string HTML with <span class="hl-…"> tokens | |
| 576 | - */ | |
| 577 | - private function highlightCode(string $code, string $lang): string | |
| 578 | - { | |
| 579 | - $lang = strtolower(trim($lang)); | |
| 580 | - $rules = []; | |
| 581 | - | |
| 582 | - // ---- COMMON ------------------------------------------------------- | |
| 583 | - $rules['comment'] = [ | |
| 584 | - // /* … */ (multi-line) | |
| 585 | - ['/\/\*(?:.|\n)*?\*\//', 'hl-comment'], | |
| 586 | - // // …\n | |
| 587 | - ['/\/\/.*(?=\n|$)/', 'hl-comment'], | |
| 588 | - ]; | |
| 589 | - | |
| 590 | - $rules['string'] = [ | |
| 591 | - // "…" and '…' (allow escaped quotes) | |
| 592 | - ['/"(?:\\.|[^"\\\n])*"/', 'hl-string'], | |
| 593 | - ["/'(?:\\.|[^'\\\n])*'/", 'hl-string'], | |
| 594 | - ]; | |
| 595 | - | |
| 596 | - $rules['number'] = [ | |
| 597 | - ['/\b(?:0x[0-9a-fA-F]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/', 'hl-number'], | |
| 598 | - ]; | |
| 599 | - | |
| 600 | - // ---- HTML --------------------------------------------------------- | |
| 601 | - if ($lang === 'html') { | |
| 602 | - $rules['tag'] = [['/<\/?[a-zA-Z0-9\-]+/', 'hl-tag']]; | |
| 603 | - $rules['attr'] = [['/\s[a-zA-Z0-9\-]+(?==)/', 'hl-attr']]; | |
| 604 | - $rules['value'] = [['/(?<==)"(?:\\.|[^"\\\n])*"(?=[>\s])/', 'hl-value']]; | |
| 605 | 667 | } |
| 606 | 668 | |
| 607 | - // ---- CSS ---------------------------------------------------------- | |
| 608 | - if ($lang === 'css') { | |
| 609 | - $rules['selector'] = [['/^[\t ]*[a-zA-Z0-9#\.\-\:$$ $$\=\*\>\+\~\^]+(?=\s*\{)/m', 'hl-selector']]; | |
| 610 | - $rules['property'] = [['/\b[a-z\-]+(?=\s*:)/', 'hl-property']]; | |
| 611 | - $rules['value'] = [['/(?<=\:)\s*[^;]+(?=;)/', 'hl-value']]; | |
| 612 | - $rules['unit'] = [['/\b\d+(px|em|rem|%|vh|vw|pt|pc|cm|mm|in)\b/i', 'hl-unit']]; | |
| 613 | - } | |
| 614 | - | |
| 615 | - // ---- PHP ---------------------------------------------------------- | |
| 616 | - if ($lang === 'php') { | |
| 617 | - $keywords = 'abstract|and|array|as|break|callable|case|catch|class|clone|' | |
| 618 | - . 'const|continue|declare|default|die|do|echo|else|elseif|empty|' | |
| 619 | - . 'enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|' | |
| 620 | - . 'extends|final|finally|for|foreach|function|global|goto|if|' | |
| 621 | - . 'implements|include|include_once|instanceof|insteadof|interface|' | |
| 622 | - . 'isset|list|namespace|new|or|print|private|protected|public|' | |
| 623 | - . 'require|require_once|return|static|switch|throw|trait|try|unset|' | |
| 624 | - . 'use|var|while|xor|yield'; | |
| 625 | - $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 626 | - $rules['function']= [['/\b([a-zA-Z_\x7f-\xff][\w\x7f-\xff]*)(?=\s*\()/', 'hl-function']]; | |
| 627 | - $rules['variable']= [['/\$[a-zA-Z_\x7f-\xff][\w\x7f-\xff]*/', 'hl-variable']]; | |
| 628 | - } | |
| 629 | - | |
| 630 | - // ---- Shell -------------------------------------------------------- | |
| 631 | - if ($lang === 'sh') { | |
| 632 | - $keywords = 'if|then|else|elif|fi|case|esac|for|select|while|until|do|done|in'; | |
| 633 | - $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 634 | - $rules['variable']= [['/\$\{?[a-zA-Z_][\w]*\}?/', 'hl-variable']]; | |
| 635 | - } | |
| 636 | - | |
| 637 | - // ---- JSON --------------------------------------------------------- | |
| 638 | - if ($lang === 'json') { | |
| 639 | - $rules['property'] = [['/"([^"]+)":/', '<span class="hl-property">$1</span>:']]; | |
| 640 | - } | |
| 641 | - | |
| 642 | - // ---- C / C++ ----------------------------------------------- | |
| 643 | - if (in_array($lang, ['c', 'cpp', 'cs'])) { | |
| 644 | - $keywords = 'abstract|assert|boolean|break|byte|case|catch|char|class|const|' | |
| 645 | - . 'continue|default|do|double|else|enum|extends|false|final|finally|' | |
| 646 | - . 'float|for|goto|if|implements|import|instanceof|int|interface|long|' | |
| 647 | - . 'native|new|null|package|private|protected|public|return|short|static|' | |
| 648 | - . 'strictfp|super|switch|synchronized|this|throw|throws|transient|true|' | |
| 649 | - . 'try|void|volatile|while'; | |
| 650 | - $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']]; | |
| 651 | - $rules['type'] = [['/\b(?:int|float|double|char|bool|void|string)\b/', 'hl-type']]; | |
| 652 | - $rules['define'] = [['/(#include)|(#define)/', 'hl-define']]; | |
| 653 | - } | |
| 654 | - | |
| 655 | - if ($lang === 'markdown') { | |
| 656 | - $rules['header'] = [['/^(#{1,6})\s+(.+)$/', '<span class="hl-header">$1 $2</span>']]; | |
| 657 | - $rules['link'] = [['/\[([^\]]+)\]\(([^)]+) $$/', '<span class="hl-link">[$1]($2)</span>']]; | |
| 658 | - } | |
| 659 | - | |
| 660 | - $html = htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8'); | |
| 661 | - | |
| 662 | - $order = ['comment', 'string', 'number']; | |
| 663 | - foreach (['tag','attr','value','selector','property','unit','keyword','function','variable','type','header','link'] as $k) { | |
| 664 | - if (isset($rules[$k])) $order[] = $k; | |
| 665 | - } | |
| 666 | - | |
| 667 | - foreach ($order as $type) { | |
| 668 | - if (!isset($rules[$type])) continue; | |
| 669 | - foreach ($rules[$type] as $rule) { | |
| 670 | - [$pattern, $class] = $rule; | |
| 671 | - $html = preg_replace_callback( | |
| 672 | - $pattern, | |
| 673 | - function ($m) use ($class) { | |
| 674 | - // For JSON property we already built the <span> | |
| 675 | - if (strpos($m[0], '<span') === 0) return $m[0]; | |
| 676 | - $inner = $m[0]; | |
| 677 | - // Preserve already escaped entities inside strings/comments | |
| 678 | - return '<span class="'.$class.'">'.$inner.'</span>'; | |
| 679 | - }, | |
| 680 | - $html | |
| 681 | - ); | |
| 682 | - } | |
| 683 | - } | |
| 684 | - | |
| 669 | + $html .= " </table>\n </p>"; | |
| 685 | 670 | return $html; |
| 686 | 671 | } |
| 687 | 672 | } |
| @@ -0,0 +1,333 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib; | |
| 3 | + | |
| 4 | +use Std\Lib\Syntax\SyntaxInterface; | |
| 5 | +use Std\Lib\Syntax\Asm; | |
| 6 | +use Std\Lib\Syntax\Asm6502; | |
| 7 | +use Std\Lib\Syntax\Asm68000; | |
| 8 | +use Std\Lib\Syntax\AsmAarch32; | |
| 9 | +use Std\Lib\Syntax\AsmAarch64; | |
| 10 | +use Std\Lib\Syntax\AsmMips; | |
| 11 | +use Std\Lib\Syntax\AsmPower; | |
| 12 | +use Std\Lib\Syntax\AsmRiscv; | |
| 13 | +use Std\Lib\Syntax\AsmX8664; | |
| 14 | +use Std\Lib\Syntax\AsmZ80; | |
| 15 | +use Std\Lib\Syntax\C; | |
| 16 | +use Std\Lib\Syntax\Cpp; | |
| 17 | +use Std\Lib\Syntax\CSharp; | |
| 18 | +use Std\Lib\Syntax\Glsl; | |
| 19 | +use Std\Lib\Syntax\Go; | |
| 20 | +use Std\Lib\Syntax\Hlsl; | |
| 21 | +use Std\Lib\Syntax\ObjC; | |
| 22 | +use Std\Lib\Syntax\Slang; | |
| 23 | +use Std\Lib\Syntax\Swift; | |
| 24 | + | |
| 25 | +/** | |
| 26 | + * コードブロックのシンタックスハイライト | |
| 27 | + * | |
| 28 | + * コードを先頭から1回だけ走査し、トークン毎に<span class="hl-種類">で囲む。 | |
| 29 | + * 色はstyle-code.cssで定義する。 | |
| 30 | + * | |
| 31 | + * 使い方: | |
| 32 | + * if (Syntax::isSupported($lang)) $html = (new Syntax($lang))->colorize($code); | |
| 33 | + */ | |
| 34 | +class Syntax { | |
| 35 | + private const SUPPORTED = [ | |
| 36 | + 'asm' => Asm::class, | |
| 37 | + 'asm-6502' => Asm6502::class, | |
| 38 | + 'asm-68000' => Asm68000::class, | |
| 39 | + 'asm-aarch32' => AsmAarch32::class, | |
| 40 | + 'asm-aarch64' => AsmAarch64::class, | |
| 41 | + 'asm-mips' => AsmMips::class, | |
| 42 | + 'asm-power' => AsmPower::class, | |
| 43 | + 'asm-riscv' => AsmRiscv::class, | |
| 44 | + 'asm-x8664' => AsmX8664::class, | |
| 45 | + 'asm-z80' => AsmZ80::class, | |
| 46 | + 'c' => C::class, | |
| 47 | + 'cpp' => Cpp::class, | |
| 48 | + 'csharp' => CSharp::class, | |
| 49 | + 'glsl' => Glsl::class, | |
| 50 | + 'go' => Go::class, | |
| 51 | + 'hlsl' => Hlsl::class, | |
| 52 | + 'objc' => ObjC::class, | |
| 53 | + 'slang' => Slang::class, | |
| 54 | + 'swift' => Swift::class, | |
| 55 | + ]; | |
| 56 | + | |
| 57 | + private const ALIASES = [ | |
| 58 | + 'asm-6502' => ['6502', 'asm6502', 'asm-65c02'], | |
| 59 | + 'asm-68000' => ['68000', '68k', 'm68k', 'asm-68k', 'asm-m68k'], | |
| 60 | + 'asm-aarch32' => ['asm-arm', 'asm-arm32', 'arm', 'arm32', 'aarch32', 'asm-thumb'], | |
| 61 | + 'asm-aarch64' => ['asm-arm64', 'arm64', 'aarch64'], | |
| 62 | + 'asm-mips' => ['mips', 'asm-mips64'], | |
| 63 | + 'asm-power' => ['asm-ppc', 'asm-powerpc', 'asm-ppc64', 'ppc', 'powerpc', 'power'], | |
| 64 | + 'asm-riscv' => ['riscv', 'asm-riscv64', 'asm-rv64', 'rv64', 'risc-v'], | |
| 65 | + 'asm-x8664' => ['asm-x86', 'asm-x64', 'asm-x86-64', 'asm-amd64', 'x86', 'x64', 'x86-64', 'nasm'], | |
| 66 | + 'asm-z80' => ['z80', 'asm-gbz80', 'gbz80'], | |
| 67 | + 'c' => ['h', 'c99', 'c11', 'c23'], | |
| 68 | + 'cpp' => ['c++', 'cc', 'cxx', 'hpp', 'hh', 'hxx', 'h++'], | |
| 69 | + 'csharp' => ['c#', 'cs'], | |
| 70 | + 'glsl' => ['vert', 'frag', 'geom', 'comp', 'tesc', 'tese'], | |
| 71 | + 'go' => ['golang'], | |
| 72 | + 'hlsl' => ['fx', 'fxh', 'hlsli'], | |
| 73 | + 'objc' => ['objective-c', 'obj-c', 'objectivec', 'm', 'mm', 'objc++'], | |
| 74 | + 'slang' => [], | |
| 75 | + 'swift' => [], | |
| 76 | + ]; | |
| 77 | + | |
| 78 | + private string $lang; | |
| 79 | + private SyntaxInterface $handler; | |
| 80 | + | |
| 81 | + /** @var array 規則の正規表現をキャッシュ(言語 => [行頭用, 通常用, 種類一覧]) */ | |
| 82 | + private static array $compiled = []; | |
| 83 | + | |
| 84 | + public function __construct(string $lang) { | |
| 85 | + $resolved = self::resolve($lang); | |
| 86 | + if ($resolved === null) throw new \InvalidArgumentException("未対応の言語: {$lang}"); | |
| 87 | + $this->lang = $resolved; | |
| 88 | + $class = self::SUPPORTED[$resolved]; | |
| 89 | + $this->handler = new $class(); | |
| 90 | + } | |
| 91 | + | |
| 92 | + /** | |
| 93 | + * 言語が対応しているか | |
| 94 | + * | |
| 95 | + * @param string $lang 言語名又は別名 | |
| 96 | + * @return bool | |
| 97 | + */ | |
| 98 | + public static function isSupported(string $lang): bool { | |
| 99 | + return self::resolve($lang) !== null; | |
| 100 | + } | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * 別名を正式な言語名にする | |
| 104 | + * | |
| 105 | + * @param string $lang 言語名又は別名 | |
| 106 | + * @return string|null 正式な言語名、未対応ならnull | |
| 107 | + */ | |
| 108 | + public static function resolve(string $lang): ?string { | |
| 109 | + $lang = strtolower(trim($lang)); | |
| 110 | + if (isset(self::SUPPORTED[$lang])) return $lang; | |
| 111 | + foreach (self::ALIASES as $k => $v) if (in_array($lang, $v, true)) return $k; | |
| 112 | + return null; | |
| 113 | + } | |
| 114 | + | |
| 115 | + public function getLanguage(): string { | |
| 116 | + return $this->lang; | |
| 117 | + } | |
| 118 | + | |
| 119 | + /** | |
| 120 | + * コードをハイライトしたHTMLにする(HTMLエスケープ済み) | |
| 121 | + * 失敗した場合はエスケープしただけのコードを返す | |
| 122 | + * | |
| 123 | + * @param string $code 生のコード | |
| 124 | + * @return string HTML | |
| 125 | + */ | |
| 126 | + public function colorize(string $code): string { | |
| 127 | + $code = str_replace(["\r\n", "\r"], "\n", $code); | |
| 128 | + if ($code === '' || preg_match('//u', $code) !== 1) return self::escape($code); | |
| 129 | + | |
| 130 | + try { | |
| 131 | + return $this->tokenize($code); | |
| 132 | + } catch (\Throwable $e) { | |
| 133 | + return self::escape($code); | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + /////////////// | |
| 138 | + | |
| 139 | + private function tokenize(string $code): string { | |
| 140 | + $h = $this->handler; | |
| 141 | + $h->prepare($code); | |
| 142 | + [$reBol, $reAny, $classes] = $this->compile(); | |
| 143 | + $nested = $h->getNestedComment(); | |
| 144 | + $headers = $h->getHeaderDirectives(); | |
| 145 | + | |
| 146 | + $out = ''; | |
| 147 | + $plain = ''; | |
| 148 | + $pos = 0; | |
| 149 | + $len = strlen($code); | |
| 150 | + $bol = true; // 行頭から空白しか無い | |
| 151 | + $first = true; // この行でまだ単語が無い(アセンブリの命令判定用) | |
| 152 | + $prev = ''; // 直前の空白以外の文字 | |
| 153 | + $prevWord = ''; // この行の直前の単語 | |
| 154 | + $lineWord = ''; // この行の最初の単語 | |
| 155 | + $expectHeader = false; | |
| 156 | + | |
| 157 | + $emit = function(string $text, ?string $class) use (&$out, &$plain): void { | |
| 158 | + if ($class === null) { | |
| 159 | + $plain .= $text; | |
| 160 | + return; | |
| 161 | + } | |
| 162 | + if ($plain !== '') { | |
| 163 | + $out .= self::escape($plain); | |
| 164 | + $plain = ''; | |
| 165 | + } | |
| 166 | + $out .= '<span class="hl-'.$class.'">'.self::escape($text).'</span>'; | |
| 167 | + }; | |
| 168 | + | |
| 169 | + while ($pos < $len) { | |
| 170 | + $ch = $code[$pos]; | |
| 171 | + | |
| 172 | + if ($ch === "\n") { | |
| 173 | + $plain .= "\n"; | |
| 174 | + $pos++; | |
| 175 | + $bol = true; | |
| 176 | + $first = true; | |
| 177 | + $prevWord = ''; | |
| 178 | + $lineWord = ''; | |
| 179 | + $expectHeader = false; | |
| 180 | + continue; | |
| 181 | + } | |
| 182 | + | |
| 183 | + if ($ch === ' ' || $ch === "\t") { | |
| 184 | + $plain .= $ch; | |
| 185 | + $pos++; | |
| 186 | + continue; | |
| 187 | + } | |
| 188 | + | |
| 189 | + // 入れ子のコメント(/* /* */ */) | |
| 190 | + if ($nested && substr_compare($code, $nested[0], $pos, strlen($nested[0])) === 0) { | |
| 191 | + $end = $this->scanNested($code, $pos, $nested[0], $nested[1]); | |
| 192 | + $text = substr($code, $pos, $end - $pos); | |
| 193 | + $emit($text, 'comment'); | |
| 194 | + $pos = $end; | |
| 195 | + $bol = false; | |
| 196 | + if (str_contains($text, "\n")) $first = false; | |
| 197 | + continue; | |
| 198 | + } | |
| 199 | + | |
| 200 | + // #include <stdio.h> のファイル名 | |
| 201 | + if ($expectHeader && $ch === '<' && preg_match('~\G<[^>\n]*>~', $code, $m, 0, $pos)) { | |
| 202 | + $emit($m[0], 'string'); | |
| 203 | + $pos += strlen($m[0]); | |
| 204 | + $bol = false; | |
| 205 | + $expectHeader = false; | |
| 206 | + continue; | |
| 207 | + } | |
| 208 | + | |
| 209 | + if (preg_match($bol ? $reBol : $reAny, $code, $m, PREG_UNMATCHED_AS_NULL, $pos) === 1 && $m[0] !== '') { | |
| 210 | + $text = $m[0]; | |
| 211 | + $class = null; | |
| 212 | + foreach ($classes as $i => $c) { | |
| 213 | + if (isset($m["r{$i}"])) { | |
| 214 | + $class = $c; | |
| 215 | + break; | |
| 216 | + } | |
| 217 | + } | |
| 218 | + | |
| 219 | + $after = $pos + strlen($text); | |
| 220 | + if ($class === 'ident') { | |
| 221 | + $class = $h->classify($text, $this->context($code, $after, $bol, $first, $prev, $prevWord, $lineWord)); | |
| 222 | + if ($class !== 'label') { | |
| 223 | + if ($first) $lineWord = $text; | |
| 224 | + $first = false; | |
| 225 | + } | |
| 226 | + $prevWord = $text; | |
| 227 | + } else if ($class === 'preprocessor') { | |
| 228 | + $name = strtolower(ltrim(substr($text, 1), " \t")); | |
| 229 | + if (in_array($name, $headers, true)) $expectHeader = true; | |
| 230 | + } | |
| 231 | + | |
| 232 | + $emit($text, $class); | |
| 233 | + $pos = $after; | |
| 234 | + $bol = false; | |
| 235 | + $prev = substr($text, -1); | |
| 236 | + if (str_contains($text, "\n")) $first = false; | |
| 237 | + continue; | |
| 238 | + } | |
| 239 | + | |
| 240 | + // どの規則にも合わない文字(演算子等) | |
| 241 | + preg_match('~\G.~su', $code, $m, 0, $pos); | |
| 242 | + $c = $m[0] ?? $ch; | |
| 243 | + $plain .= $c; | |
| 244 | + $pos += strlen($c); | |
| 245 | + $bol = false; | |
| 246 | + $prev = $c; | |
| 247 | + } | |
| 248 | + | |
| 249 | + if ($plain !== '') $out .= self::escape($plain); | |
| 250 | + return $out; | |
| 251 | + } | |
| 252 | + | |
| 253 | + /** | |
| 254 | + * 規則を2つの正規表現にまとめる(行頭用と通常用) | |
| 255 | + * | |
| 256 | + * @return array [行頭用, 通常用, 番号 => 種類] | |
| 257 | + */ | |
| 258 | + private function compile(): array { | |
| 259 | + if (isset(self::$compiled[$this->lang])) return self::$compiled[$this->lang]; | |
| 260 | + | |
| 261 | + $rules = $this->handler->getRules(); | |
| 262 | + $bol = []; | |
| 263 | + $any = []; | |
| 264 | + $classes = []; | |
| 265 | + foreach ($rules as $i => $r) { | |
| 266 | + $classes[$i] = $r['class']; | |
| 267 | + $part = "(?<r{$i}>{$r['re']})"; | |
| 268 | + $bol[] = $part; | |
| 269 | + if (empty($r['bol'])) $any[] = $part; | |
| 270 | + } | |
| 271 | + | |
| 272 | + $mk = fn(array $parts) => '~\G(?:'.implode('|', $parts).')~su'; | |
| 273 | + return self::$compiled[$this->lang] = [$mk($bol), $mk($any), $classes]; | |
| 274 | + } | |
| 275 | + | |
| 276 | + /** | |
| 277 | + * 識別子の分類に使う文脈を作る | |
| 278 | + * | |
| 279 | + * @param string $code コード全体 | |
| 280 | + * @param int $after 識別子の直後の位置 | |
| 281 | + * @param bool $bol 識別子が行頭か | |
| 282 | + * @param bool $first 行の最初の単語か | |
| 283 | + * @param string $prev 直前の空白以外の文字 | |
| 284 | + * @param string $prevWord この行の直前の単語 | |
| 285 | + * @param string $lineWord この行の最初の単語(ラベルを除く) | |
| 286 | + * @return array | |
| 287 | + */ | |
| 288 | + private function context(string $code, int $after, bool $bol, bool $first, string $prev, string $prevWord, string $lineWord): array { | |
| 289 | + preg_match('~\G[ \t]*([^ \t\n]?)~u', $code, $m, 0, $after); | |
| 290 | + $next = $m[1] ?? ''; | |
| 291 | + preg_match('~\G.~su', $code, $r, 0, $after); | |
| 292 | + preg_match('~\G[ \t]*:?[ \t]*([^\s,;:()]*)~u', $code, $w, 0, $after); | |
| 293 | + return [ | |
| 294 | + 'bol' => $bol, | |
| 295 | + 'first' => $first, | |
| 296 | + 'prev' => $prev, | |
| 297 | + 'prevWord' => $prevWord, | |
| 298 | + 'lineWord' => $lineWord, | |
| 299 | + 'next' => $next, | |
| 300 | + 'nextRaw' => $r[0] ?? '', | |
| 301 | + 'nextWord' => $w[1] ?? '', | |
| 302 | + ]; | |
| 303 | + } | |
| 304 | + | |
| 305 | + /** | |
| 306 | + * 入れ子のコメントの終わりを探す | |
| 307 | + * | |
| 308 | + * @return int コメントの直後の位置(閉じていなければコードの最後) | |
| 309 | + */ | |
| 310 | + private function scanNested(string $code, int $pos, string $start, string $end): int { | |
| 311 | + $depth = 0; | |
| 312 | + $len = strlen($code); | |
| 313 | + $ls = strlen($start); | |
| 314 | + $le = strlen($end); | |
| 315 | + while ($pos < $len) { | |
| 316 | + if (substr_compare($code, $start, $pos, $ls) === 0) { | |
| 317 | + $depth++; | |
| 318 | + $pos += $ls; | |
| 319 | + } else if (substr_compare($code, $end, $pos, $le) === 0) { | |
| 320 | + $depth--; | |
| 321 | + $pos += $le; | |
| 322 | + if ($depth === 0) return $pos; | |
| 323 | + } else { | |
| 324 | + $pos++; | |
| 325 | + } | |
| 326 | + } | |
| 327 | + return $len; | |
| 328 | + } | |
| 329 | + | |
| 330 | + private static function escape(string $s): string { | |
| 331 | + return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); | |
| 332 | + } | |
| 333 | +} |
| @@ -0,0 +1,45 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * 「```asm」用の汎用アセンブリ | |
| 6 | + * 1つの記事でx86-64・AArch64・RISC-V・POWER等を全部「asm」で書く事があるので、 | |
| 7 | + * 主なアーキテクチャのレジスター・命令をまとめて使う。 | |
| 8 | + * | |
| 9 | + * 「#」はコメント(RISC-V・POWER・GAS)、「#数字」は即値(ARM)として扱う。 | |
| 10 | + */ | |
| 11 | +class Asm extends Assembly { | |
| 12 | + protected array $comment = [';', '//', '#']; | |
| 13 | + protected ?array $multiline = ['/*', '*/']; | |
| 14 | + protected string $immediatePrefix = ''; | |
| 15 | + protected bool $percentBinary = false; | |
| 16 | + protected bool $percentMacros = true; | |
| 17 | + | |
| 18 | + protected array $rules = [ | |
| 19 | + // ARMの即値(#64, #-1, #0x10)はコメントより先に確認する | |
| 20 | + ['re' => '#[-+]?(?:0[xX][0-9A-Fa-f]+|0[bB][01]+|\d+(?:\.\d+)?)', 'class' => 'number'], | |
| 21 | + ]; | |
| 22 | + | |
| 23 | + /** @var string[] まとめるアーキテクチャ(8ビット機等は単一文字のレジスターが多いので含めない) */ | |
| 24 | + protected array $archs = [AsmX8664::class, AsmAarch64::class, AsmAarch32::class, AsmRiscv::class, AsmMips::class, AsmPower::class]; | |
| 25 | + | |
| 26 | + public function prepare(string $code): void { | |
| 27 | + $words = []; | |
| 28 | + $suffixes = []; | |
| 29 | + $prefixes = []; | |
| 30 | + foreach ($this->archs as $class) { | |
| 31 | + $a = new $class(); | |
| 32 | + $a->prepare(''); | |
| 33 | + [$w2, $s2, $p2] = $a->getTables(); | |
| 34 | + foreach ($w2 as $w => $c) { | |
| 35 | + // 命令の色を優先(「and」等が他のアーキテクチャでキーワードの場合) | |
| 36 | + if (!isset($words[$w]) || $c === 'instruction') $words[$w] = $c; | |
| 37 | + } | |
| 38 | + $suffixes = array_merge($suffixes, $s2); | |
| 39 | + $prefixes = array_merge($prefixes, $p2); | |
| 40 | + } | |
| 41 | + $this->words = $words; | |
| 42 | + $this->suffixes = array_values(array_unique($suffixes)); | |
| 43 | + $this->prefixes = array_values(array_unique($prefixes)); | |
| 44 | + } | |
| 45 | +} |
| @@ -0,0 +1,28 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * 6502・65C02 | |
| 6 | + */ | |
| 7 | +class Asm6502 extends Assembly { | |
| 8 | + protected string $identifier = '[.A-Za-z_@][\w.$]*'; | |
| 9 | + | |
| 10 | + protected array $tokens = [ | |
| 11 | + 'register' => [ | |
| 12 | + 'a', 'x', 'y', 's', 'p', | |
| 13 | + ], | |
| 14 | + 'instruction' => [ | |
| 15 | + 'adc', 'and', 'asl', 'bcc', 'bcs', 'beq', 'bit', 'bmi', 'bne', 'bpl', 'brk', 'bvc', 'bvs', 'clc', 'cld', 'cli', 'clv', | |
| 16 | + 'cmp', 'cpx', 'cpy', 'dec', 'dex', 'dey', 'eor', 'inc', 'inx', 'iny', 'jmp', 'jsr', 'lda', 'ldx', 'ldy', 'lsr', 'nop', | |
| 17 | + 'ora', 'pha', 'php', 'pla', 'plp', 'rol', 'ror', 'rti', 'rts', 'sbc', 'sec', 'sed', 'sei', 'sta', 'stx', 'sty', | |
| 18 | + 'tax', 'tay', 'tsx', 'txa', 'txs', 'tya', | |
| 19 | + // 65C02 | |
| 20 | + 'bra', 'phx', 'phy', 'plx', 'ply', 'stz', 'trb', 'tsb', 'stp', 'wai', | |
| 21 | + 'bbr0', 'bbr1', 'bbr2', 'bbr3', 'bbr4', 'bbr5', 'bbr6', 'bbr7', 'bbs0', 'bbs1', 'bbs2', 'bbs3', 'bbs4', 'bbs5', 'bbs6', 'bbs7', | |
| 22 | + 'rmb0', 'rmb1', 'rmb2', 'rmb3', 'rmb4', 'rmb5', 'rmb6', 'rmb7', 'smb0', 'smb1', 'smb2', 'smb3', 'smb4', 'smb5', 'smb6', 'smb7', | |
| 23 | + ], | |
| 24 | + 'directive' => [ | |
| 25 | + 'org', 'equ', 'db', 'dw', 'ds', 'byte', 'word', 'include', 'incbin', 'macro', 'endm', 'if', 'else', 'endif', | |
| 26 | + ], | |
| 27 | + ]; | |
| 28 | +} |
| @@ -0,0 +1,44 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Motorola 68000(68k) | |
| 6 | + */ | |
| 7 | +class Asm68000 extends Assembly { | |
| 8 | + protected string $identifier = '[.A-Za-z_][\w.$]*'; | |
| 9 | + // move.l → move、dc.w → dc | |
| 10 | + protected array $suffixes = ['/\.[bwlsdxp]$/']; | |
| 11 | + | |
| 12 | + protected array $rules = [ | |
| 13 | + // 行頭の「*」もコメント | |
| 14 | + ['re' => '\*[^\n]*', 'class' => 'comment', 'bol' => true], | |
| 15 | + ]; | |
| 16 | + | |
| 17 | + protected array $tokens = [ | |
| 18 | + 'register' => [ | |
| 19 | + 'sp', 'usp', 'ssp', 'pc', 'sr', 'ccr', 'vbr', 'sfc', 'dfc', | |
| 20 | + ], | |
| 21 | + 'instruction' => [ | |
| 22 | + 'move', 'movea', 'moveq', 'movem', 'movep', 'lea', 'pea', 'exg', 'swap', 'ext', 'extb', 'clr', 'tst', 'tas', | |
| 23 | + 'add', 'adda', 'addi', 'addq', 'addx', 'sub', 'suba', 'subi', 'subq', 'subx', 'muls', 'mulu', 'divs', 'divu', 'neg', 'negx', | |
| 24 | + 'cmp', 'cmpa', 'cmpi', 'cmpm', 'chk', 'abcd', 'sbcd', 'nbcd', | |
| 25 | + 'and', 'andi', 'or', 'ori', 'eor', 'eori', 'not', 'lsl', 'lsr', 'asl', 'asr', 'rol', 'ror', 'roxl', 'roxr', | |
| 26 | + 'btst', 'bset', 'bclr', 'bchg', 'bra', 'bsr', 'jmp', 'jsr', 'rts', 'rte', 'rtr', 'trap', 'trapv', 'link', 'unlk', | |
| 27 | + 'nop', 'reset', 'stop', 'illegal', 'dbra', | |
| 28 | + ], | |
| 29 | + 'directive' => [ | |
| 30 | + 'dc', 'ds', 'dcb', 'even', 'odd', 'cnop', 'org', 'equ', 'set', 'rs', 'rsreset', 'rsset', 'section', 'text', 'data', 'bss', | |
| 31 | + 'xdef', 'xref', 'public', 'include', 'incbin', 'end', 'macro', 'endm', 'rept', 'endr', 'if', 'else', 'endc', 'endif', | |
| 32 | + 'ifd', 'ifnd', 'ifeq', 'ifne', 'opt', 'align', | |
| 33 | + ], | |
| 34 | + ]; | |
| 35 | + | |
| 36 | + public function prepare(string $code): void { | |
| 37 | + $tokens = $this->tokens; | |
| 38 | + foreach (['d', 'a', 'fp'] as $p) foreach (self::numbered($p, 0, 7) as $r) $tokens['register'][] = $r; | |
| 39 | + // 条件付き命令(beq, dbne, sne, trapcc等) | |
| 40 | + $cc = ['t', 'f', 'hi', 'ls', 'cc', 'hs', 'cs', 'lo', 'ne', 'eq', 'vc', 'vs', 'pl', 'mi', 'ge', 'lt', 'gt', 'le']; | |
| 41 | + foreach ($cc as $c) array_push($tokens['instruction'], "b{$c}", "db{$c}", "s{$c}", "trap{$c}"); | |
| 42 | + $this->words = $this->buildWords($tokens); | |
| 43 | + } | |
| 44 | +} |
| @@ -0,0 +1,52 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * AArch32(ARM・Thumb、GASのUAL記法) | |
| 6 | + */ | |
| 7 | +class AsmAarch32 extends Assembly { | |
| 8 | + protected array $comment = ['@', '//']; | |
| 9 | + protected ?array $multiline = ['/*', '*/']; | |
| 10 | + protected bool $dollarHex = false; | |
| 11 | + protected bool $percentBinary = false; | |
| 12 | + // 幅・型(.w, .f32)→ 条件コード(addeq)→ フラグ更新(adds)の順に外す | |
| 13 | + protected array $suffixes = [ | |
| 14 | + '/\.(?:w|n|[iusfp]?(?:8|16|32|64)|f16|f32|f64)$/', | |
| 15 | + '/(?:eq|ne|cs|hs|cc|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al)$/', | |
| 16 | + '/s$/', | |
| 17 | + ]; | |
| 18 | + | |
| 19 | + protected array $tokens = [ | |
| 20 | + 'register' => [ | |
| 21 | + 'sp', 'lr', 'pc', 'fp', 'ip', 'sl', 'sb', 'apsr', 'cpsr', 'spsr', 'fpscr', 'fpexc', | |
| 22 | + ], | |
| 23 | + 'instruction' => [ | |
| 24 | + // データ処理 | |
| 25 | + 'add', 'adc', 'sub', 'sbc', 'rsb', 'rsc', 'mul', 'mla', 'mls', 'umull', 'umlal', 'smull', 'smlal', 'sdiv', 'udiv', | |
| 26 | + 'and', 'orr', 'orn', 'eor', 'bic', 'mvn', 'mov', 'movw', 'movt', 'lsl', 'lsr', 'asr', 'ror', 'rrx', | |
| 27 | + 'cmp', 'cmn', 'tst', 'teq', 'clz', 'rev', 'rev16', 'rbit', 'ubfx', 'sbfx', 'bfi', 'bfc', 'uxtb', 'uxth', 'sxtb', 'sxth', 'usat', 'ssat', | |
| 28 | + 'qadd', 'qsub', 'uadd8', 'usub8', 'sel', | |
| 29 | + // 分岐 | |
| 30 | + 'b', 'bl', 'bx', 'blx', 'cbz', 'cbnz', 'tbb', 'tbh', 'it', 'ite', 'itt', 'itte', 'ittt', 'itee', | |
| 31 | + // ロード・ストア | |
| 32 | + 'ldr', 'ldrb', 'ldrh', 'ldrsb', 'ldrsh', 'ldrd', 'str', 'strb', 'strh', 'strd', 'ldm', 'ldmia', 'ldmdb', 'stm', 'stmia', 'stmdb', | |
| 33 | + 'push', 'pop', 'ldrex', 'strex', 'clrex', 'pld', 'adr', | |
| 34 | + // システム | |
| 35 | + 'svc', 'swi', 'bkpt', 'nop', 'wfi', 'wfe', 'sev', 'dmb', 'dsb', 'isb', 'mrs', 'msr', 'cpsie', 'cpsid', 'mrc', 'mcr', | |
| 36 | + // VFP・NEON | |
| 37 | + 'vmov', 'vadd', 'vsub', 'vmul', 'vdiv', 'vmla', 'vmls', 'vfma', 'vneg', 'vabs', 'vsqrt', 'vcmp', 'vcvt', 'vldr', 'vstr', 'vldm', 'vstm', | |
| 38 | + 'vpush', 'vpop', 'vmrs', 'vmsr', 'vld1', 'vst1', 'vdup', 'vand', 'vorr', 'veor', 'vmax', 'vmin', | |
| 39 | + ], | |
| 40 | + 'keyword' => [ | |
| 41 | + 'eq', 'ne', 'cs', 'hs', 'cc', 'lo', 'mi', 'pl', 'vs', 'vc', 'hi', 'ls', 'ge', 'lt', 'gt', 'le', 'al', | |
| 42 | + ], | |
| 43 | + ]; | |
| 44 | + | |
| 45 | + public function prepare(string $code): void { | |
| 46 | + $tokens = $this->tokens; | |
| 47 | + foreach (self::numbered('r', 0, 15) as $r) $tokens['register'][] = $r; | |
| 48 | + foreach (['s', 'd'] as $p) foreach (self::numbered($p, 0, 31) as $r) $tokens['register'][] = $r; | |
| 49 | + foreach (self::numbered('q', 0, 15) as $r) $tokens['register'][] = $r; | |
| 50 | + $this->words = $this->buildWords($tokens); | |
| 51 | + } | |
| 52 | +} |
| @@ -0,0 +1,56 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * AArch64(GAS。Appleのアセンブラの「;」コメントも対応) | |
| 6 | + */ | |
| 7 | +class AsmAarch64 extends Assembly { | |
| 8 | + protected array $comment = ['//', ';']; | |
| 9 | + protected ?array $multiline = ['/*', '*/']; | |
| 10 | + protected bool $dollarHex = false; | |
| 11 | + protected bool $percentBinary = false; | |
| 12 | + // b.eq → b、v0.4s → v0、ld1.16b等 | |
| 13 | + protected array $suffixes = ['/\.[a-z0-9]+$/']; | |
| 14 | + | |
| 15 | + protected array $tokens = [ | |
| 16 | + 'register' => [ | |
| 17 | + 'sp', 'wsp', 'xzr', 'wzr', 'lr', 'fp', 'pc', 'nzcv', 'fpcr', 'fpsr', 'tpidr_el0', | |
| 18 | + ], | |
| 19 | + 'instruction' => [ | |
| 20 | + // データ処理 | |
| 21 | + 'add', 'adds', 'sub', 'subs', 'adc', 'adcs', 'sbc', 'sbcs', 'neg', 'negs', 'ngc', 'cmp', 'cmn', 'tst', | |
| 22 | + 'mul', 'madd', 'msub', 'mneg', 'smull', 'umull', 'smulh', 'umulh', 'smaddl', 'umaddl', 'sdiv', 'udiv', | |
| 23 | + 'and', 'ands', 'orr', 'orn', 'eor', 'eon', 'bic', 'bics', 'mvn', 'lsl', 'lsr', 'asr', 'ror', | |
| 24 | + 'mov', 'movz', 'movn', 'movk', 'adr', 'adrp', 'sxtb', 'sxth', 'sxtw', 'uxtb', 'uxth', 'ubfx', 'sbfx', 'ubfiz', 'sbfiz', 'bfi', 'bfxil', | |
| 25 | + 'ubfm', 'sbfm', 'bfm', 'extr', 'clz', 'cls', 'rbit', 'rev', 'rev16', 'rev32', | |
| 26 | + 'csel', 'csinc', 'csinv', 'csneg', 'cset', 'csetm', 'cinc', 'cinv', 'cneg', 'ccmp', 'ccmn', | |
| 27 | + // 分岐 | |
| 28 | + 'b', 'bl', 'br', 'blr', 'ret', 'cbz', 'cbnz', 'tbz', 'tbnz', 'braa', 'blraa', 'retaa', | |
| 29 | + // ロード・ストア | |
| 30 | + 'ldr', 'ldrb', 'ldrh', 'ldrsb', 'ldrsh', 'ldrsw', 'ldur', 'ldurb', 'ldurh', 'str', 'strb', 'strh', 'stur', 'sturb', 'sturh', | |
| 31 | + 'ldp', 'stp', 'ldnp', 'stnp', 'ldxr', 'stxr', 'ldaxr', 'stlxr', 'ldar', 'stlr', 'ldxp', 'stxp', 'prfm', | |
| 32 | + 'ldadd', 'ldadda', 'ldaddal', 'ldclr', 'ldset', 'ldeor', 'swp', 'swpal', 'cas', 'casal', | |
| 33 | + // システム | |
| 34 | + 'svc', 'hvc', 'smc', 'brk', 'hlt', 'nop', 'yield', 'wfe', 'wfi', 'sev', 'dmb', 'dsb', 'isb', 'mrs', 'msr', 'sys', 'paciasp', 'autiasp', 'bti', | |
| 35 | + // 浮動小数点・NEON | |
| 36 | + 'fmov', 'fadd', 'fsub', 'fmul', 'fdiv', 'fmadd', 'fmsub', 'fnmadd', 'fneg', 'fabs', 'fsqrt', 'fmin', 'fmax', 'fminnm', 'fmaxnm', 'fcmp', 'fcsel', | |
| 37 | + 'fcvt', 'fcvtzs', 'fcvtzu', 'fcvtns', 'scvtf', 'ucvtf', 'frintn', 'frintm', 'frintp', 'frintz', | |
| 38 | + 'fmla', 'fmls', 'addv', 'addp', 'faddp', 'umov', 'smov', 'ins', 'dup', 'movi', 'zip1', 'zip2', 'uzp1', 'uzp2', 'trn1', 'trn2', 'ext', 'tbl', | |
| 39 | + 'ld1', 'ld2', 'ld3', 'ld4', 'st1', 'st2', 'st3', 'st4', 'ld1r', 'cnt', 'uaddlv', 'saddlv', 'shl', 'sshr', 'ushr', 'xtn', 'sqxtn', 'uqxtn', | |
| 40 | + 'cmeq', 'cmgt', 'cmge', 'fcmeq', 'fcmgt', 'bsl', 'bit', 'bif', 'not', 'abs', 'mla', 'mls', 'smlal', 'umlal', 'sdot', 'udot', 'fmlal', | |
| 41 | + ], | |
| 42 | + 'keyword' => [ | |
| 43 | + // 条件コード・シフト・拡張 | |
| 44 | + 'eq', 'ne', 'cs', 'hs', 'cc', 'lo', 'mi', 'pl', 'vs', 'vc', 'hi', 'ls', 'ge', 'lt', 'gt', 'le', 'al', 'nv', | |
| 45 | + 'uxtw', 'sxtx', 'uxtx', 'msl', | |
| 46 | + ], | |
| 47 | + ]; | |
| 48 | + | |
| 49 | + public function prepare(string $code): void { | |
| 50 | + $tokens = $this->tokens; | |
| 51 | + foreach (['x', 'w'] as $p) foreach (self::numbered($p, 0, 30) as $r) $tokens['register'][] = $r; | |
| 52 | + foreach (['v', 'q', 'd', 's', 'h', 'b', 'z'] as $p) foreach (self::numbered($p, 0, 31) as $r) $tokens['register'][] = $r; | |
| 53 | + foreach (self::numbered('p', 0, 15) as $r) $tokens['register'][] = $r; | |
| 54 | + $this->words = $this->buildWords($tokens); | |
| 55 | + } | |
| 56 | +} |
| @@ -0,0 +1,41 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * MIPS(MIPS32/MIPS64、GAS) | |
| 6 | + */ | |
| 7 | +class AsmMips extends Assembly { | |
| 8 | + protected array $comment = ['#']; | |
| 9 | + protected ?array $multiline = ['/*', '*/']; | |
| 10 | + // 「$」はレジスター($t0、$31)なので16進数ではない | |
| 11 | + protected bool $dollarHex = false; | |
| 12 | + protected bool $percentBinary = false; | |
| 13 | + protected string $immediatePrefix = ''; | |
| 14 | + protected string $identifier = '\$\d+|[%$]?[.A-Za-z_][\w.$]*'; | |
| 15 | + // add.s → add、c.eq.d → c.eq → c | |
| 16 | + protected array $suffixes = ['/\.[a-z0-9]+$/', '/\.[a-z0-9]+$/']; | |
| 17 | + | |
| 18 | + protected array $tokens = [ | |
| 19 | + 'register' => [ | |
| 20 | + 'zero', 'at', 'v0', 'v1', 'a0', 'a1', 'a2', 'a3', 'k0', 'k1', 'gp', 'sp', 'fp', 'ra', 'hi', 'lo', 'pc', | |
| 21 | + ], | |
| 22 | + 'instruction' => [ | |
| 23 | + 'add', 'addu', 'addi', 'addiu', 'sub', 'subu', 'mult', 'multu', 'mul', 'madd', 'div', 'divu', 'mfhi', 'mflo', 'mthi', 'mtlo', | |
| 24 | + 'dadd', 'daddu', 'daddi', 'daddiu', 'dsub', 'dsubu', 'dmult', 'ddiv', | |
| 25 | + 'and', 'andi', 'or', 'ori', 'xor', 'xori', 'nor', 'sll', 'srl', 'sra', 'sllv', 'srlv', 'srav', 'dsll', 'dsrl', 'dsra', 'slt', 'sltu', 'slti', 'sltiu', | |
| 26 | + 'lui', 'lb', 'lbu', 'lh', 'lhu', 'lw', 'lwu', 'ld', 'lwl', 'lwr', 'll', 'sb', 'sh', 'sw', 'sd', 'swl', 'swr', 'sc', | |
| 27 | + 'beq', 'bne', 'bgez', 'bgtz', 'blez', 'bltz', 'bgezal', 'bltzal', 'j', 'jal', 'jr', 'jalr', 'syscall', 'break', 'eret', 'sync', 'nop', | |
| 28 | + 'mfc0', 'mtc0', 'mfc1', 'mtc1', 'lwc1', 'swc1', 'ldc1', 'sdc1', 'movn', 'movz', 'teq', 'ext', 'ins', 'seb', 'seh', 'clz', 'clo', | |
| 29 | + 'abs', 'cvt', 'c', 'bc1t', 'bc1f', 'mov', 'neg', 'sqrt', 'trunc', 'round', 'floor', 'ceil', | |
| 30 | + // 疑似命令 | |
| 31 | + 'li', 'la', 'move', 'b', 'bal', 'beqz', 'bnez', 'bge', 'bgt', 'ble', 'blt', 'not', 'negu', | |
| 32 | + ], | |
| 33 | + ]; | |
| 34 | + | |
| 35 | + public function prepare(string $code): void { | |
| 36 | + $tokens = $this->tokens; | |
| 37 | + $names = array_merge(self::numbered('t', 0, 9), self::numbered('s', 0, 8), self::numbered('f', 0, 31), self::numbered('', 0, 31)); | |
| 38 | + foreach ($names as $r) $tokens['register'][] = $r; | |
| 39 | + $this->words = $this->buildWords($tokens); | |
| 40 | + } | |
| 41 | +} |
| @@ -0,0 +1,50 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * POWER・PowerPC(GAS) | |
| 6 | + * GASではレジスターをただの数字で書く事が多い(ld 2, 24(12))ので、「r2」「%r2」の形だけを色付けする | |
| 7 | + */ | |
| 8 | +class AsmPower extends Assembly { | |
| 9 | + protected array $comment = ['#']; | |
| 10 | + protected ?array $multiline = ['/*', '*/']; | |
| 11 | + protected bool $dollarHex = false; | |
| 12 | + protected bool $percentBinary = false; | |
| 13 | + protected string $immediatePrefix = ''; | |
| 14 | + // add. → add(条件レジスターを更新する形) | |
| 15 | + protected array $suffixes = ['/\.$/']; | |
| 16 | + | |
| 17 | + protected array $tokens = [ | |
| 18 | + 'register' => [ | |
| 19 | + 'lr', 'ctr', 'xer', 'msr', 'cr', 'fpscr', 'vrsave', 'vscr', 'sp', 'toc', 'rtoc', | |
| 20 | + ], | |
| 21 | + 'instruction' => [ | |
| 22 | + 'li', 'lis', 'la', 'addi', 'addis', 'add', 'addc', 'adde', 'addze', 'addme', 'addic', 'subf', 'subfc', 'subfe', 'subfic', 'sub', 'subi', 'neg', | |
| 23 | + 'mullw', 'mulld', 'mulhw', 'mulhwu', 'mulhd', 'mulhdu', 'mulli', 'divw', 'divwu', 'divd', 'divdu', 'modsw', 'moduw', | |
| 24 | + 'and', 'andc', 'andi', 'andis', 'or', 'orc', 'ori', 'oris', 'xor', 'xori', 'xoris', 'nor', 'nand', 'eqv', 'not', 'mr', | |
| 25 | + 'slw', 'srw', 'sraw', 'srawi', 'sld', 'srd', 'srad', 'sradi', 'rlwinm', 'rlwimi', 'rlwnm', 'rldicl', 'rldicr', 'rldic', 'rldimi', | |
| 26 | + 'slwi', 'srwi', 'sldi', 'srdi', 'clrlwi', 'clrldi', 'extsb', 'extsh', 'extsw', 'cntlzw', 'cntlzd', 'popcntw', 'popcntd', | |
| 27 | + 'lbz', 'lbzu', 'lbzx', 'lhz', 'lhzu', 'lha', 'lhax', 'lwz', 'lwzu', 'lwzx', 'lwa', 'lwax', 'ld', 'ldu', 'ldx', 'lmw', 'lwarx', 'ldarx', | |
| 28 | + 'stb', 'stbu', 'stbx', 'sth', 'sthu', 'sthx', 'stw', 'stwu', 'stwx', 'std', 'stdu', 'stdx', 'stmw', 'stwcx', 'stdcx', | |
| 29 | + 'lfs', 'lfd', 'stfs', 'stfd', 'fadd', 'fadds', 'fsub', 'fmul', 'fmuls', 'fdiv', 'fmadd', 'fmr', 'fneg', 'fabs', 'fsqrt', 'fcmpu', 'fctiwz', 'fcfid', | |
| 30 | + 'cmpw', 'cmpwi', 'cmplw', 'cmplwi', 'cmpd', 'cmpdi', 'cmpld', 'cmpldi', 'cmp', 'cmpi', 'cmpl', 'cmpli', | |
| 31 | + 'b', 'ba', 'bl', 'bla', 'bc', 'bcl', 'blr', 'blrl', 'bctr', 'bctrl', 'beq', 'bne', 'blt', 'bgt', 'ble', 'bge', 'bso', 'bns', 'bdnz', 'bdz', | |
| 32 | + 'beqlr', 'bnelr', 'bltlr', 'bgtlr', 'blelr', 'bgelr', | |
| 33 | + 'mflr', 'mtlr', 'mfctr', 'mtctr', 'mfcr', 'mtcrf', 'mfxer', 'mtxer', 'mfspr', 'mtspr', 'mfmsr', 'mtmsr', | |
| 34 | + 'sc', 'nop', 'isync', 'sync', 'lwsync', 'eieio', 'dcbf', 'dcbst', 'icbi', 'trap', 'tw', 'twi', 'td', 'tdi', | |
| 35 | + 'lvx', 'stvx', 'vaddfp', 'vmaddfp', 'vperm', 'lxvd2x', 'stxvd2x', 'xvadddp', 'xvmuldp', | |
| 36 | + ], | |
| 37 | + 'keyword' => [ | |
| 38 | + // シンボル修飾子(msg@toc、x@ha) | |
| 39 | + 'ha', 'got', 'plt', 'tocbase', 'localentry', | |
| 40 | + ], | |
| 41 | + ]; | |
| 42 | + | |
| 43 | + public function prepare(string $code): void { | |
| 44 | + $tokens = $this->tokens; | |
| 45 | + foreach (['r', 'f', 'v'] as $p) foreach (self::numbered($p, 0, 31) as $r) $tokens['register'][] = $r; | |
| 46 | + foreach (self::numbered('vs', 0, 63) as $r) $tokens['register'][] = $r; | |
| 47 | + foreach (self::numbered('cr', 0, 7) as $r) $tokens['register'][] = $r; | |
| 48 | + $this->words = $this->buildWords($tokens); | |
| 49 | + } | |
| 50 | +} |
| @@ -0,0 +1,53 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * RISC-V(RV32/RV64、GAS) | |
| 6 | + */ | |
| 7 | +class AsmRiscv extends Assembly { | |
| 8 | + protected array $comment = ['#']; | |
| 9 | + protected ?array $multiline = ['/*', '*/']; | |
| 10 | + protected bool $dollarHex = false; | |
| 11 | + protected bool $percentBinary = false; | |
| 12 | + protected string $immediatePrefix = ''; | |
| 13 | + // fcvt.w.s → fcvt.w → fcvt、lr.w.aq → lr、c.addi → addi | |
| 14 | + protected array $suffixes = ['/\.[a-z0-9]+$/', '/\.[a-z0-9]+$/', '/^c\./']; | |
| 15 | + | |
| 16 | + protected array $tokens = [ | |
| 17 | + 'register' => [ | |
| 18 | + 'zero', 'ra', 'sp', 'gp', 'tp', 'fp', 'pc', | |
| 19 | + ], | |
| 20 | + 'instruction' => [ | |
| 21 | + // RV32I/RV64I | |
| 22 | + 'lui', 'auipc', 'jal', 'jalr', 'beq', 'bne', 'blt', 'bge', 'bltu', 'bgeu', 'lb', 'lh', 'lw', 'lbu', 'lhu', 'lwu', 'ld', 'sb', 'sh', 'sw', 'sd', | |
| 23 | + 'addi', 'slti', 'sltiu', 'xori', 'ori', 'andi', 'slli', 'srli', 'srai', 'add', 'sub', 'sll', 'slt', 'sltu', 'xor', 'srl', 'sra', 'or', 'and', | |
| 24 | + 'addiw', 'slliw', 'srliw', 'sraiw', 'addw', 'subw', 'sllw', 'srlw', 'sraw', 'fence', 'ecall', 'ebreak', | |
| 25 | + // M・A | |
| 26 | + 'mul', 'mulh', 'mulhsu', 'mulhu', 'div', 'divu', 'rem', 'remu', 'mulw', 'divw', 'divuw', 'remw', 'remuw', | |
| 27 | + 'lr', 'sc', 'amoswap', 'amoadd', 'amoxor', 'amoand', 'amoor', 'amomin', 'amomax', 'amominu', 'amomaxu', | |
| 28 | + // F・D | |
| 29 | + 'flw', 'fsw', 'fld', 'fsd', 'fadd', 'fsub', 'fmul', 'fdiv', 'fsqrt', 'fmin', 'fmax', 'fmadd', 'fmsub', 'fnmadd', 'fnmsub', | |
| 30 | + 'fsgnj', 'fsgnjn', 'fsgnjx', 'fcvt', 'fmv', 'feq', 'flt', 'fle', 'fclass', | |
| 31 | + // Zicsr・特権 | |
| 32 | + 'csrrw', 'csrrs', 'csrrc', 'csrrwi', 'csrrsi', 'csrrci', 'csrr', 'csrw', 'csrs', 'csrc', 'mret', 'sret', 'wfi', 'sfence', | |
| 33 | + // B・V(よく使う物) | |
| 34 | + 'andn', 'orn', 'xnor', 'clz', 'ctz', 'cpop', 'min', 'max', 'minu', 'maxu', 'rol', 'ror', 'rev8', 'sext', 'zext', | |
| 35 | + 'vsetvli', 'vsetivli', 'vsetvl', 'vle8', 'vle16', 'vle32', 'vle64', 'vse8', 'vse16', 'vse32', 'vse64', 'vadd', 'vsub', 'vmul', 'vfadd', 'vfmul', | |
| 36 | + 'vfmacc', 'vredsum', 'vfredusum', 'vmv', | |
| 37 | + // 疑似命令 | |
| 38 | + 'li', 'la', 'lla', 'mv', 'not', 'neg', 'negw', 'nop', 'j', 'jr', 'ret', 'call', 'tail', 'seqz', 'snez', 'sltz', 'sgtz', | |
| 39 | + 'beqz', 'bnez', 'blez', 'bgez', 'bltz', 'bgtz', 'bgt', 'ble', 'bgtu', 'bleu', 'fabs', 'fneg', | |
| 40 | + ], | |
| 41 | + ]; | |
| 42 | + | |
| 43 | + public function prepare(string $code): void { | |
| 44 | + $tokens = $this->tokens; | |
| 45 | + $names = array_merge( | |
| 46 | + self::numbered('x', 0, 31), self::numbered('f', 0, 31), self::numbered('v', 0, 31), | |
| 47 | + self::numbered('t', 0, 6), self::numbered('s', 0, 11), self::numbered('a', 0, 7), | |
| 48 | + self::numbered('ft', 0, 11), self::numbered('fs', 0, 11), self::numbered('fa', 0, 7), | |
| 49 | + ); | |
| 50 | + foreach ($names as $r) $tokens['register'][] = $r; | |
| 51 | + $this->words = $this->buildWords($tokens); | |
| 52 | + } | |
| 53 | +} |
| @@ -0,0 +1,92 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * x86-64(NASMとGASのAT&T記法の両方) | |
| 6 | + */ | |
| 7 | +class AsmX8664 extends Assembly { | |
| 8 | + protected array $comment = [';', '#']; | |
| 9 | + protected string $immediatePrefix = ''; | |
| 10 | + protected bool $percentBinary = false; | |
| 11 | + protected bool $percentMacros = true; | |
| 12 | + protected array $prefixes = ['rep', 'repe', 'repne', 'repz', 'repnz', 'lock', 'xacquire', 'xrelease', 'bnd', 'notrack', 'data16', 'addr32', 'rex', 'rex.w']; | |
| 13 | + | |
| 14 | + protected array $tokens = [ | |
| 15 | + 'register' => [ | |
| 16 | + 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi', 'rbp', 'rsp', | |
| 17 | + 'eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'ebp', 'esp', | |
| 18 | + 'ax', 'bx', 'cx', 'dx', 'si', 'di', 'bp', 'sp', | |
| 19 | + 'al', 'bl', 'cl', 'dl', 'ah', 'bh', 'ch', 'dh', 'sil', 'dil', 'bpl', 'spl', | |
| 20 | + 'rip', 'eip', 'ip', 'rflags', 'eflags', 'flags', | |
| 21 | + 'cs', 'ds', 'es', 'fs', 'gs', 'ss', 'st', | |
| 22 | + ], | |
| 23 | + 'instruction' => [ | |
| 24 | + // データ転送 | |
| 25 | + 'mov', 'movzx', 'movsx', 'movsxd', 'movabs', 'lea', 'xchg', 'cmpxchg', 'cmpxchg8b', 'cmpxchg16b', 'xadd', 'bswap', 'push', 'pop', | |
| 26 | + 'pushf', 'popf', 'pushfq', 'popfq', 'pusha', 'popa', 'cbw', 'cwde', 'cdqe', 'cwd', 'cdq', 'cqo', 'movbe', 'movnti', | |
| 27 | + // AT&T記法のサイズ付き | |
| 28 | + 'movb', 'movw', 'movl', 'movq', 'leaq', 'leal', 'addq', 'addl', 'subq', 'subl', 'pushq', 'popq', 'cmpq', 'cmpl', 'xorl', 'xorq', | |
| 29 | + 'andq', 'andl', 'orq', 'orl', 'incq', 'incl', 'decq', 'decl', 'imulq', 'imull', 'testq', 'testl', 'shlq', 'shrq', 'sarq', 'callq', 'retq', | |
| 30 | + 'movzbl', 'movzwl', 'movsbl', 'movswl', 'movslq', 'cltq', 'cqto', 'cltd', | |
| 31 | + // 算術・論理 | |
| 32 | + 'add', 'adc', 'sub', 'sbb', 'mul', 'imul', 'div', 'idiv', 'inc', 'dec', 'neg', 'cmp', 'test', 'and', 'or', 'xor', 'not', | |
| 33 | + 'shl', 'shr', 'sal', 'sar', 'rol', 'ror', 'rcl', 'rcr', 'shld', 'shrd', 'bt', 'bts', 'btr', 'btc', 'bsf', 'bsr', | |
| 34 | + 'lzcnt', 'tzcnt', 'popcnt', 'andn', 'bextr', 'blsi', 'blsmsk', 'blsr', 'bzhi', 'mulx', 'pdep', 'pext', 'rorx', 'sarx', 'shlx', 'shrx', | |
| 35 | + 'adcx', 'adox', 'crc32', | |
| 36 | + // 分岐 | |
| 37 | + 'jmp', 'call', 'ret', 'retf', 'iret', 'iretq', 'loop', 'loope', 'loopne', 'jcxz', 'jecxz', 'jrcxz', 'enter', 'leave', | |
| 38 | + 'int', 'int3', 'into', 'syscall', 'sysret', 'sysenter', 'sysexit', | |
| 39 | + // 文字列 | |
| 40 | + 'movs', 'movsb', 'movsw', 'movsq', 'stos', 'stosb', 'stosw', 'stosd', 'stosq', 'lods', 'lodsb', 'lodsw', 'lodsd', 'lodsq', | |
| 41 | + 'scas', 'scasb', 'scasw', 'scasd', 'scasq', 'cmps', 'cmpsb', 'cmpsw', 'cmpsq', | |
| 42 | + // フラグ・その他 | |
| 43 | + 'clc', 'stc', 'cmc', 'cld', 'std', 'cli', 'sti', 'lahf', 'sahf', 'nop', 'hlt', 'pause', 'ud2', 'cpuid', 'rdtsc', 'rdtscp', 'rdrand', 'rdseed', | |
| 44 | + 'xgetbv', 'lfence', 'sfence', 'mfence', 'prefetcht0', 'prefetcht1', 'prefetcht2', 'prefetchnta', 'clflush', 'endbr64', 'in', 'out', | |
| 45 | + // x87 | |
| 46 | + 'fld', 'fst', 'fstp', 'fild', 'fist', 'fistp', 'fadd', 'faddp', 'fsub', 'fsubp', 'fmul', 'fmulp', 'fdiv', 'fdivp', 'fsqrt', 'fsin', 'fcos', | |
| 47 | + 'fxch', 'fcomi', 'fcomip', 'fldz', 'fld1', 'fninit', 'fwait', | |
| 48 | + // SSE・AVX | |
| 49 | + 'movaps', 'movups', 'movapd', 'movupd', 'movss', 'movsd', 'movd', 'movdqa', 'movdqu', 'movhlps', 'movlhps', 'movmskps', | |
| 50 | + 'addss', 'addsd', 'addps', 'addpd', 'subss', 'subsd', 'subps', 'subpd', 'mulss', 'mulsd', 'mulps', 'mulpd', 'divss', 'divsd', 'divps', 'divpd', | |
| 51 | + 'sqrtss', 'sqrtsd', 'sqrtps', 'sqrtpd', 'rsqrtss', 'rsqrtps', 'rcpss', 'rcpps', 'minss', 'minsd', 'minps', 'minpd', 'maxss', 'maxsd', 'maxps', 'maxpd', | |
| 52 | + 'andps', 'andpd', 'andnps', 'andnpd', 'orps', 'orpd', 'xorps', 'xorpd', 'cmpps', 'cmpss', 'comiss', 'comisd', 'ucomiss', 'ucomisd', | |
| 53 | + 'shufps', 'shufpd', 'unpcklps', 'unpckhps', 'unpcklpd', 'unpckhpd', 'blendps', 'blendvps', 'dpps', 'roundss', 'roundps', | |
| 54 | + 'cvtsi2ss', 'cvtsi2sd', 'cvtss2si', 'cvtsd2si', 'cvttss2si', 'cvttsd2si', 'cvtss2sd', 'cvtsd2ss', 'cvtdq2ps', 'cvtps2dq', 'cvttps2dq', | |
| 55 | + 'paddb', 'paddw', 'paddd', 'paddq', 'psubb', 'psubw', 'psubd', 'psubq', 'pmullw', 'pmulld', 'pmuludq', 'pand', 'pandn', 'por', 'pxor', | |
| 56 | + 'pcmpeqb', 'pcmpeqd', 'pcmpgtd', 'pshufb', 'pshufd', 'pslld', 'psrld', 'psrad', 'psllq', 'psrlq', 'punpcklbw', 'punpckldq', 'packuswb', | |
| 57 | + 'pmovmskb', 'ptest', 'pminsd', 'pmaxsd', 'pextrd', 'pinsrd', 'pabsd', 'phaddd', | |
| 58 | + 'vmovaps', 'vmovups', 'vmovapd', 'vmovupd', 'vmovss', 'vmovsd', 'vmovdqa', 'vmovdqu', 'vmovdqa64', 'vmovdqu8', 'vmovdqu32', | |
| 59 | + 'vaddps', 'vaddpd', 'vaddss', 'vaddsd', 'vsubps', 'vsubpd', 'vmulps', 'vmulpd', 'vdivps', 'vdivpd', 'vsqrtps', 'vminps', 'vmaxps', | |
| 60 | + 'vandps', 'vorps', 'vxorps', 'vpxor', 'vpand', 'vpor', 'vpaddd', 'vpsubd', 'vpmulld', 'vpshufb', 'vpermd', 'vpermps', 'vperm2f128', | |
| 61 | + 'vbroadcastss', 'vbroadcastsd', 'vpbroadcastd', 'vinsertf128', 'vextractf128', 'vshufps', 'vblendvps', 'vgatherdps', 'vcvtps2ph', 'vcvtph2ps', | |
| 62 | + 'vfmadd132ps', 'vfmadd213ps', 'vfmadd231ps', 'vfmadd132ss', 'vfmadd213ss', 'vfmadd231ss', 'vfmadd231pd', 'vfnmadd231ps', 'vfmsub231ps', | |
| 63 | + 'vzeroupper', 'vzeroall', 'vptest', 'vcmpps', 'vhaddps', 'vdpps', 'vroundps', 'kmovw', 'kmovq', 'vpternlogd', 'vpcompressd', 'vpexpandd', | |
| 64 | + ], | |
| 65 | + 'keyword' => [ | |
| 66 | + // サイズ指定等(NASM) | |
| 67 | + 'byte', 'word', 'dword', 'qword', 'tword', 'oword', 'yword', 'zword', 'ptr', 'rel', 'abs', 'near', 'far', 'short', 'strict', 'wrt', 'seg', | |
| 68 | + 'rep', 'repe', 'repne', 'repz', 'repnz', 'lock', 'xacquire', 'xrelease', 'bnd', 'notrack', | |
| 69 | + ], | |
| 70 | + 'directive' => [ | |
| 71 | + // NASM | |
| 72 | + 'section', 'segment', 'global', 'extern', 'common', 'static', 'bits', 'default', 'db', 'dw', 'dd', 'dq', 'dt', 'do', 'dy', 'dz', | |
| 73 | + 'resb', 'resw', 'resd', 'resq', 'rest', 'reso', 'resy', 'resz', 'equ', 'times', 'incbin', 'align', 'alignb', 'struc', 'endstruc', | |
| 74 | + 'istruc', 'iend', 'at', 'org', 'cpu', 'use16', 'use32', 'use64', 'absolute', 'sectalign', | |
| 75 | + ], | |
| 76 | + ]; | |
| 77 | + | |
| 78 | + public function prepare(string $code): void { | |
| 79 | + $tokens = $this->tokens; | |
| 80 | + // r8〜r15、各サイズ | |
| 81 | + foreach (self::numbered('r', 8, 15) as $r) array_push($tokens['register'], $r, "{$r}d", "{$r}w", "{$r}b", "{$r}l"); | |
| 82 | + // 制御・デバッグ・ベクター・マスク・MMX・x87 | |
| 83 | + foreach ([['cr', 0, 15], ['dr', 0, 15], ['xmm', 0, 31], ['ymm', 0, 31], ['zmm', 0, 31], ['k', 0, 7], ['mm', 0, 7], ['st', 0, 7]] as [$p, $a, $b]) { | |
| 84 | + foreach (self::numbered($p, $a, $b) as $r) $tokens['register'][] = $r; | |
| 85 | + } | |
| 86 | + // 条件付き命令(je, cmovne, setg等) | |
| 87 | + $cc = ['o', 'no', 'b', 'nb', 'c', 'nc', 'ae', 'nae', 'z', 'nz', 'e', 'ne', 'be', 'nbe', 'a', 'na', 's', 'ns', 'p', 'np', 'pe', 'po', | |
| 88 | + 'l', 'nl', 'ge', 'nge', 'le', 'nle', 'g', 'ng']; | |
| 89 | + foreach ($cc as $c) array_push($tokens['instruction'], "j{$c}", "cmov{$c}", "set{$c}"); | |
| 90 | + $this->words = $this->buildWords($tokens); | |
| 91 | + } | |
| 92 | +} |
| @@ -0,0 +1,48 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Z80・GBZ80・eZ80 | |
| 6 | + */ | |
| 7 | +class AsmZ80 extends Assembly { | |
| 8 | + // 「af'」の様な裏レジスターも1つの単語にする(「%」は2進数なので接頭辞にしない) | |
| 9 | + protected string $identifier = '[.A-Za-z_][\w.$]*\'?'; | |
| 10 | + | |
| 11 | + protected array $tokens = [ | |
| 12 | + 'register' => [ | |
| 13 | + // 16ビット | |
| 14 | + "af'", "bc'", "de'", "hl'", 'af', 'bc', 'de', 'hl', 'ix', 'iy', 'ixh', 'ixl', 'iyh', 'iyl', 'sp', 'pc', | |
| 15 | + // 8ビット | |
| 16 | + 'a', 'b', 'c', 'd', 'e', 'f', 'h', 'l', 'i', 'r', | |
| 17 | + "a'", "b'", "c'", "d'", "e'", "f'", "h'", "l'", | |
| 18 | + // eZ80 | |
| 19 | + 'mb', | |
| 20 | + ], | |
| 21 | + 'instruction' => [ | |
| 22 | + // Z80 + GBZ80 + eZ80 | |
| 23 | + 'adc', 'add', 'and', 'bit', 'call', 'ccf', 'cp', 'cpl', 'daa', 'dec', 'di', 'djnz', 'ei', | |
| 24 | + 'halt', 'im', 'inc', 'in', 'jp', 'jr', 'ld', 'ldd', 'ldh', 'ldi', | |
| 25 | + 'nop', 'otir', 'otdr', 'or', 'pop', 'push', 'retn', 'reti', 'ret', 'res', | |
| 26 | + 'rst', 'rrc', 'rrca', 'rr', 'rra', 'rlc', 'rlca', 'rl', 'rla', 'sub', 'srl', 'sra', 'sla', 'set', 'scf', 'sbc', 'xor', | |
| 27 | + // Z80 + GBZ80のみ | |
| 28 | + 'sll', 'sl1', | |
| 29 | + // eZ80のみ | |
| 30 | + 'ind2', 'ind2r', 'indm', 'indmr', 'indr2', 'indrx', 'ini2', 'ini2r', 'inim', 'inimr', 'inirx', 'lea', 'mlt', | |
| 31 | + 'otd2r', 'otdm', 'otdmr', 'otdrx', 'oti2r', 'otim', 'otimr', 'otirx', 'out0', 'in0', 'outd2', 'outi2', | |
| 32 | + 'pea', 'rsmix', 'slp', 'stmix', 'tst', 'tstio', | |
| 33 | + // Z80のみ | |
| 34 | + 'cpdr', 'cpir', 'cpd', 'cpi', 'exx', 'ex', 'outd', 'inir', 'ini', 'indr', 'ind', 'lddr', 'ldir', | |
| 35 | + 'neg', 'outi', 'out', 'rld', 'rrd', | |
| 36 | + // GBZ80のみ | |
| 37 | + 'stop', 'swap', | |
| 38 | + ], | |
| 39 | + 'keyword' => [ | |
| 40 | + // 条件 | |
| 41 | + 'nz', 'z', 'nc', 'po', 'pe', 'p', 'm', | |
| 42 | + ], | |
| 43 | + 'directive' => [ | |
| 44 | + 'org', 'equ', 'db', 'dw', 'ds', 'defb', 'defw', 'defs', 'defm', 'dm', 'include', 'incbin', 'end', 'macro', 'endm', | |
| 45 | + 'section', 'rb', 'rw', 'if', 'else', 'endc', 'endif', 'rept', 'endr', 'export', 'assert', 'align', 'charmap', | |
| 46 | + ], | |
| 47 | + ]; | |
| 48 | +} |
| @@ -0,0 +1,127 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * アセンブリ言語の基本クラス | |
| 6 | + * | |
| 7 | + * 種類:instruction(命令)、register(レジスター)、directive(指令)、keyword(サイズ指定等)、label(ラベル) | |
| 8 | + * 行の最初の単語は、一覧に無くても命令として扱う(ラベルと指令を除く) | |
| 9 | + */ | |
| 10 | +abstract class Assembly extends Language { | |
| 11 | + protected array $comment = [';']; | |
| 12 | + protected ?array $multiline = null; | |
| 13 | + protected array $strings = ['"', "'"]; | |
| 14 | + protected bool $caseInsensitive = true; | |
| 15 | + protected bool $functionCalls = false; | |
| 16 | + protected string $digitSep = ''; | |
| 17 | + // 「%rax」(AT&T)や「$t0」(MIPS)の様な接頭辞付きも1つの単語にする | |
| 18 | + protected string $identifier = '[%$]?[.A-Za-z_][\w.$]*'; | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * @var string[] 命令・レジスターの接尾辞を外す正規表現(ARMの条件コード等) | |
| 22 | + * 順番に適用し、途中の結果も全部候補にする | |
| 23 | + */ | |
| 24 | + protected array $suffixes = []; | |
| 25 | + | |
| 26 | + /** @var string 数字の接頭辞(ARMの「#」、6502の「#」等) */ | |
| 27 | + protected string $immediatePrefix = '#?'; | |
| 28 | + | |
| 29 | + /** @var bool 「$FF」を16進数とするか(MIPSでは「$」はレジスター) */ | |
| 30 | + protected bool $dollarHex = true; | |
| 31 | + | |
| 32 | + /** @var bool 「%1010」を2進数とするか(x86では「%」はレジスター・マクロ) */ | |
| 33 | + protected bool $percentBinary = true; | |
| 34 | + | |
| 35 | + /** @var bool 「%define」等をNASMのマクロとして扱うか */ | |
| 36 | + protected bool $percentMacros = false; | |
| 37 | + | |
| 38 | + /** @var string[] 命令の前に付く接頭辞(rep、lock等)。次の単語も命令として扱う */ | |
| 39 | + protected array $prefixes = []; | |
| 40 | + | |
| 41 | + protected function numberPattern(): string { | |
| 42 | + $alts = [ | |
| 43 | + '0[xX][0-9A-Fa-f]+', | |
| 44 | + '0[bB][01]+', | |
| 45 | + '0[oO][0-7]+', | |
| 46 | + '[0-9][0-9A-Fa-f]*[hH](?![\w.])', | |
| 47 | + '\d+(?:\.\d+)?(?:[eE][+-]?\d+)?', | |
| 48 | + ]; | |
| 49 | + if ($this->dollarHex) $alts[] = '\$[0-9A-Fa-f]+(?![\w.])'; | |
| 50 | + if ($this->percentBinary) $alts[] = '%[01]+(?![\w.])'; | |
| 51 | + return $this->immediatePrefix.'-?(?:'.implode('|', $alts).')'; | |
| 52 | + } | |
| 53 | + | |
| 54 | + public function classify(string $word, array $ctx): ?string { | |
| 55 | + if ($word === '.' || $word === '$') return null; | |
| 56 | + $key = strtolower($word); | |
| 57 | + | |
| 58 | + // ラベル(「名前:」、又は「名前 db …」「名前 = …」の様な定義) | |
| 59 | + if ($ctx['next'] === ':' && ($ctx['first'] || $ctx['bol'])) return 'label'; | |
| 60 | + if ($ctx['first'] && ($ctx['next'] === '=' || $this->lookup(strtolower($ctx['nextWord'])) === 'directive')) return 'label'; | |
| 61 | + | |
| 62 | + $atOpcode = $ctx['first'] || in_array(strtolower($ctx['prevWord']), $this->prefixes, true); | |
| 63 | + $class = $this->lookup($key); | |
| 64 | + if ($class !== null) { | |
| 65 | + // 命令の位置以外の命令名はシンボルの可能性が高い(例:「jmp add」) | |
| 66 | + if ($class === 'instruction' && !$atOpcode) return null; | |
| 67 | + return $class; | |
| 68 | + } | |
| 69 | + | |
| 70 | + if ($this->percentMacros && $key[0] === '%' && strlen($key) > 1) return 'preprocessor'; | |
| 71 | + if ($key[0] === '.') { | |
| 72 | + // 「.section .text」の様に指令の行なら指令、「jne .done」の様に命令の行ならローカルラベルの参照 | |
| 73 | + if ($atOpcode) return 'directive'; | |
| 74 | + $head = strtolower($ctx['lineWord']); | |
| 75 | + return ($head !== '' && ($head[0] === '.' || $this->lookup($head) === 'directive')) ? 'directive' : null; | |
| 76 | + } | |
| 77 | + if ($atOpcode) return 'instruction'; | |
| 78 | + return null; | |
| 79 | + } | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * 単語表・接尾辞・接頭辞を返す(汎用のAsmがまとめる為) | |
| 83 | + * | |
| 84 | + * @return array [単語 => 種類, 接尾辞の正規表現, 接頭辞] | |
| 85 | + */ | |
| 86 | + public function getTables(): array { | |
| 87 | + return [$this->words, $this->suffixes, $this->prefixes]; | |
| 88 | + } | |
| 89 | + | |
| 90 | + /** | |
| 91 | + * 単語を一覧から探す(接頭辞「%」「$」と接尾辞を外した候補も試す) | |
| 92 | + * | |
| 93 | + * @param string $key 小文字の単語 | |
| 94 | + * @return string|null 種類 | |
| 95 | + */ | |
| 96 | + protected function lookup(string $key): ?string { | |
| 97 | + if ($key === '') return null; | |
| 98 | + $base = ltrim($key, '%$'); | |
| 99 | + foreach ($this->candidates($key) as $c) { | |
| 100 | + if (!isset($this->words[$c])) continue; | |
| 101 | + $class = $this->words[$c]; | |
| 102 | + // 接尾辞を外した候補は命令・指令だけ(「v0.4s」の様に「.」で区切った場合はレジスターも) | |
| 103 | + // そうしないと「hello」→「hel」+「lo」の様な誤判定が起きる | |
| 104 | + if ($c !== $key && $c !== $base && $class !== 'instruction' && $class !== 'directive' && !str_contains($key, '.')) continue; | |
| 105 | + return $class; | |
| 106 | + } | |
| 107 | + return null; | |
| 108 | + } | |
| 109 | + | |
| 110 | + /** | |
| 111 | + * 接頭辞・接尾辞を外した候補を作る | |
| 112 | + * | |
| 113 | + * @param string $key 小文字の単語 | |
| 114 | + * @return array 候補(元の単語が最初) | |
| 115 | + */ | |
| 116 | + protected function candidates(string $key): array { | |
| 117 | + $cands = [$key]; | |
| 118 | + if (strlen($key) > 1 && ($key[0] === '%' || $key[0] === '$')) $cands[] = substr($key, 1); | |
| 119 | + foreach ($this->suffixes as $re) { | |
| 120 | + foreach ($cands as $c) { | |
| 121 | + $s = preg_replace($re, '', $c); | |
| 122 | + if ($s !== null && $s !== '' && $s !== $c) $cands[] = $s; | |
| 123 | + } | |
| 124 | + } | |
| 125 | + return array_values(array_unique($cands)); | |
| 126 | + } | |
| 127 | +} |
| @@ -0,0 +1,272 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class C extends Language { | |
| 5 | + protected bool $preprocessor = true; | |
| 6 | + protected array $headerDirectives = ['include', 'include_next', 'embed']; | |
| 7 | + | |
| 8 | + protected array $tokens = [ | |
| 9 | + 'keyword' => [ | |
| 10 | + // C23 | |
| 11 | + 'alignas', 'alignof', 'bool', 'constexpr', 'false', 'nullptr', 'static_assert', 'thread_local', 'true', 'typeof', 'typeof_unqual', | |
| 12 | + '_BitInt', '_Decimal128', '_Decimal32', '_Decimal64', | |
| 13 | + // C11 | |
| 14 | + '_Alignas', '_Alignof', '_Atomic', '_Generic', '_Noreturn', '_Static_assert', '_Thread_local', | |
| 15 | + // C99 | |
| 16 | + 'inline', 'restrict', '_Bool', '_Complex', '_Imaginary', | |
| 17 | + // C89 | |
| 18 | + 'asm', 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extern', 'float', 'fortran', 'for', 'goto', 'if', 'inline', | |
| 19 | + 'int', 'long', 'noreturn', 'register', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', | |
| 20 | + // ISO | |
| 21 | + 'FILE', | |
| 22 | + ], | |
| 23 | + 'builtin' => [ | |
| 24 | + // ISO C | |
| 25 | + 'assert.h' => [ // C89 | |
| 26 | + 'assert', | |
| 27 | + ], | |
| 28 | + 'complex.h' => [ // C99 | |
| 29 | + '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', | |
| 30 | + 'cimag', 'cimagf', 'cimagl', 'clog', 'clogf', 'clogl', 'conj', 'conjf', 'conjl', 'cpow', 'cpowf', 'cpowl', 'cproj', 'cprojf', 'cprojl', 'creal', 'crealf', 'creall', | |
| 31 | + 'csqrt', 'csqrtf', 'csqrtl', 'csin', 'csinf', 'csinl', 'csinh', 'csinhf', 'csinhl', 'ctan', 'ctanf', 'ctanl', 'ctanh', 'ctanhf', 'ctanhl', | |
| 32 | + ], | |
| 33 | + 'ctype.h' => [ // C89 | |
| 34 | + 'isalnum', 'isalpha', 'isblank', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper', 'isxdigit', 'tolower', 'toupper', | |
| 35 | + ], | |
| 36 | + 'errno.h' => [ // C89 | |
| 37 | + 'errno', | |
| 38 | + ], | |
| 39 | + 'fenv.h' => [ // C99 | |
| 40 | + 'feclearexcept', 'fegetround', 'fegetenv', 'fegetexpectflag', 'feholdexcept', 'feraiseexcept', 'fesetround', 'fesetenv', 'fesetexpectflag', 'fetestexcept', | |
| 41 | + 'feupdateenv', | |
| 42 | + // __STDC_IEC_60559_DFP__ | |
| 43 | + 'fe_dec_getround', 'fe_dec_setround', | |
| 44 | + ], | |
| 45 | + 'inttypes.h' => [ // C99 | |
| 46 | + 'imaxabs', 'imaxdiv', 'strtoimax', 'strtoumax', 'wcstoimax', 'wcstoumax', | |
| 47 | + ], | |
| 48 | + 'locale.h' => [ // C89 | |
| 49 | + 'localeconv', 'setlocale', | |
| 50 | + ], | |
| 51 | + 'math.h' => [ // C89 | |
| 52 | + 'acos', 'acosf', 'acosl', 'acosh', 'acoshf', 'acoshl', 'asin', 'asinf', 'asinl', 'asinh', 'asinhf', 'asinhl', 'atan', 'atanf', 'atanl', 'atanh', 'atanhf', 'atanhl', | |
| 53 | + 'atan2', 'atan2f', 'atan2l', 'cbrt', 'cbrtf', 'cbrtl', 'ceil', 'ceilf', 'ceill', 'copysign', 'copysignf', 'copysignl', 'cos', 'cosf', 'cosl', 'erf', 'erff', 'erfl', | |
| 54 | + 'erfc', 'erfcf', 'erfcl', 'exp', 'expf', 'expl', 'exp2', 'exp2f', 'exp2l', 'expml', 'expmlf', 'expmll', 'fabs', 'fabsf', 'fabsl', 'fdim', 'fdimf', 'fdiml', | |
| 55 | + 'floor', 'floorf', 'floorl', 'fma', 'fmaf', 'fmal', 'fmax', 'fmaxf', 'fmaxl', 'fmin', 'fminf', 'fminl', 'fpclassify', 'frexp', 'frexpf', 'frexpl', | |
| 56 | + 'hypot', 'hypotf', 'hypotl', 'ilogb', 'ilogbf', 'ilogbl', 'isfinite', 'isgreater', 'isgreaterequal', 'isinf', 'isless', 'islessequal', 'islessgreater', 'isnan', | |
| 57 | + 'isnormal', 'isunordered', 'ldexp', 'ldexpf', 'ldexpl', 'lgamma', 'lgammaf', 'lgammal', 'log', 'logf', 'logl', 'log10', 'log10f', 'log10l', 'log2', 'log2f', 'log2l', | |
| 58 | + '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', | |
| 59 | + 'nexttoward', 'nexttowardf', 'nexttowardl', 'pow', 'powf', 'powl', 'remainder', 'remainderf', 'remainderl', 'remquo', 'remquof', 'remquol', 'rint', 'rintf', 'rintl', | |
| 60 | + 'round', 'roundf', 'roundl', 'scalbn', 'scalbnf', 'scalbnl', 'scalbln', 'scalblnf', 'scalblnl', 'signbit', 'sin', 'sinf', 'sinl', 'sinh', 'sinhf', 'sinhl', | |
| 61 | + 'sqrt', 'sqrtf', 'sqrtl', 'tan', 'tanf', 'tanl', 'tanh', 'tanhf', 'tanhl', 'tgamma', 'tgammaf', 'tgammal', 'trunc', 'truncf', 'truncl', | |
| 62 | + ], | |
| 63 | + 'setjmp.h' => [ // C89 | |
| 64 | + 'longjmp', 'setjmp', | |
| 65 | + ], | |
| 66 | + 'signal.h' => [ // C89 | |
| 67 | + 'raise', 'signal', | |
| 68 | + ], | |
| 69 | + 'stdarg.h' => [ // C89 | |
| 70 | + 'va_arg', 'va_copy', 'va_end', 'va_start', | |
| 71 | + ], | |
| 72 | + 'stdatomic.h' => [ // C11 | |
| 73 | + 'atomic_fetch_add', 'atomic_fetch_add_explicit', 'atomic_fetch_and', 'atomic_fetch_and_explicit', 'atomic_fetch_or', 'atomic_fetch_or_explicit', | |
| 74 | + 'atomic_fetch_sub', 'atomic_fetch_sub_explicit', 'atomic_fetch_xor', 'atomic_fetch_xor_explicit', 'atomic_flag', 'atomic_flag_clear', 'atomic_flag_clear_explicit', | |
| 75 | + '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', | |
| 76 | + 'atomic_store', 'atomic_store_explicit', 'atomic_thread_fence', 'kill_dependency', 'memory_order', | |
| 77 | + ], | |
| 78 | + 'stdbit.h' => [ // C23 | |
| 79 | + 'stdc_bit_ceil_uc', 'stdc_bit_ceil_us', 'stdc_bit_ceil_ui', 'stdc_bit_ceil_ul', 'stdc_bit_ceil_ull', 'stdc_bit_ceil', | |
| 80 | + 'stdc_bit_floor_uc', 'stdc_bit_floor_us', 'stdc_bit_floor_ui', 'stdc_bit_floor_ul', 'stdc_bit_floor_ull', 'stdc_bit_floor', | |
| 81 | + 'stdc_bit_width_uc', 'stdc_bit_width_us', 'stdc_bit_width_ui', 'stdc_bit_width_ul', 'stdc_bit_width_ull', 'stdc_bit_width', | |
| 82 | + 'stdc_count_ones_uc', 'stdc_count_ones_us', 'stdc_count_ones_ui', 'stdc_count_ones_ul', 'stdc_count_ones_ull', 'stdc_count_ones', | |
| 83 | + 'stdc_count_zeros_uc', 'stdc_count_zeros_us', 'stdc_count_zeros_ui', 'stdc_count_zeros_ul', 'stdc_count_zeros_ull', 'stdc_count_zeros', | |
| 84 | + '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', | |
| 85 | + 'stdc_first_leading_ones', | |
| 86 | + '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', | |
| 87 | + 'stdc_first_leading_zeros', | |
| 88 | + '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', | |
| 89 | + 'stdc_first_trailing_ones', | |
| 90 | + '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', | |
| 91 | + 'stdc_first_trailing_zeros', | |
| 92 | + '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', | |
| 93 | + 'stdc_leading_ones_uc', 'stdc_leading_ones_us', 'stdc_leading_ones_ui', 'stdc_leading_ones_ul', 'stdc_leading_ones_ull', 'stdc_leading_ones', | |
| 94 | + 'stdc_leading_zeros_uc', 'stdc_leading_zeros_us', 'stdc_leading_zeros_ui', 'stdc_leading_zeros_ul', 'stdc_leading_zeros_ull', 'stdc_leading_zeros', | |
| 95 | + 'stdc_trailing_ones_uc', 'stdc_trailing_ones_us', 'stdc_trailing_ones_ui', 'stdc_trailing_ones_ul', 'stdc_trailing_ones_ull', 'stdc_trailing_ones', | |
| 96 | + 'stdc_trailing_zeros_uc', 'stdc_trailing_zeros_us', 'stdc_trailing_zeros_ui', 'stdc_trailing_zeros_ul', 'stdc_trailing_zeros_ull', 'stdc_trailing_zeros', | |
| 97 | + 'stdc_rotate_left_uc', 'stdc_rotate_left_us', 'stdc_rotate_left_ui', 'stdc_rotate_left_ul', 'stdc_rotate_left_ull', 'stdc_rotate_left', | |
| 98 | + 'stdc_rotate_right_uc', 'stdc_rotate_right_us', 'stdc_rotate_right_ui', 'stdc_rotate_right_ul', 'stdc_rotate_right_ull', 'stdc_rotate_right', | |
| 99 | + ], | |
| 100 | + 'stdckdint.h' => [ // C23 | |
| 101 | + 'ckd_add', 'ckd_sub', 'ckd_mul', | |
| 102 | + ], | |
| 103 | + 'stdio.h' => [ // C89 | |
| 104 | + 'clearerr', 'fclose', 'feof', 'ferror', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen', 'fscanf', 'fseek', | |
| 105 | + 'fseekpos', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'perror', 'printf', 'putc', 'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', | |
| 106 | + 'setvbuf', 'snprintf', 'sprintf', 'sscanf', 'tmpfile', 'tmpnam', 'ungetc', 'vfprintf', 'vfscanf', 'vprintf', 'vscanf', 'vsnprintf', 'vsprintf', 'vsscanf', | |
| 107 | + // __STDC_WANT_LIB_EXT1__ | |
| 108 | + 'fopen_s', 'fprintf_s', 'freopen_s', 'fscanf_s', 'gets_s', 'printf_s', 'scanf_s', 'snprintf_s', 'sprintf_s', 'sscanf_s', 'tmpfile_s', 'tmpnam_s', | |
| 109 | + 'vfprintf_s', 'vfscanf_s', 'vprintf_s', 'vscanf_s', 'vsnprintf_s', 'vsprintf_s', 'vsscanf_s', | |
| 110 | + ], | |
| 111 | + 'stdlib.h' => [ // C89 | |
| 112 | + 'abort', 'abs', 'aligned_alloc', 'at_quick_exit', 'atexit', 'atof', 'atoi', 'atol', 'atoll', 'bsearch', 'calloc', 'div', 'exit', '_Exit', 'free', 'free_sized', | |
| 113 | + 'free_aligned_sized', 'labs', 'llabs', 'ldiv', 'lldiv', 'malloc', 'mblen', 'mbtowc', 'mbstowcs', 'memalignment', 'qsort', 'quick_exit', 'rand', 'realloc', 'srand', | |
| 114 | + 'strfromd', 'strfromf', 'strfroml', 'strtod', 'strtof', 'strtold', 'strtol', 'strtoll', 'strtoul', 'strtoull', 'system', 'wctomb', 'wcstombs', | |
| 115 | + // __STDC_IEC_60559_DFP__ | |
| 116 | + 'strfromd32', 'strfromd64', 'strfromd128', 'strtod32', 'strtod64', 'strtod128', | |
| 117 | + // __STDC_WANT_IEC_60559_TYPES_EXT__ | |
| 118 | + 'strfromdN', 'strfromdNx', 'strfromencfN', 'strfromencdecdN', 'strfromencbindN', 'strfromfN', 'strfromfNx', 'strtodN', 'strtodNx', 'strtofN', 'strtofNx', | |
| 119 | + 'strtoencfN', 'strtoencdecdN', 'strtoencbindN', | |
| 120 | + // __STDC_WANT_LIB_EXT1__ | |
| 121 | + 'abort_handler_s', 'bsearch_s', 'getenv_s', 'ignore_handler_s', 'mbstowcs_s', 'qsort_s', 'set_constraint_handler_s', 'wcstomb_s', 'wcstombs_s', | |
| 122 | + ], | |
| 123 | + 'stdmchar.h' => [ // C29 | |
| 124 | + 'stdc_mcnrtoc16n', 'stdc_mcnrtoc32n', 'stdc_mcnrtoc8n', 'stdc_mcnrtomcn', 'stdc_mcnrtomwcn', 'stdc_mcsnrtoc16sn', 'stdc_mcsnrtoc32sn', 'stdc_mcsnrtoc8sn', | |
| 125 | + 'stdc_mwcnrtomcn', 'stdc_mcsnrtomcsn', 'stdc_mwcnrtomwcn', 'stdc_mcsnrtomwcsn', 'stdc_mwcnrtoc16n', 'stdc_mwcnrtoc32n', 'stdc_mwcnrtoc8n', 'stdc_mwcsnrtoc16sn', | |
| 126 | + 'stdc_mwcsnrtoc32sn', 'stdc_mwcsnrtoc8sn', 'stdc_mwcsnrtomcsn', 'stdc_mwcsnrtomwcsn', 'stdc_c16nrtoc16n', 'stdc_c16nrtoc32n', 'stdc_c16nrtoc8n', 'stdc_c16nrtomcn', | |
| 127 | + 'stdc_c16nrtomwcn', 'stdc_c16snrtoc8sn', 'stdc_c16snrtoc16sn', 'stdc_c16snrtoc32sn', 'stdc_c16snrtomwcsn', 'stdc_c16snrtomcsn', 'stdc_c32nrtoc16n', 'stdc_c32nrtoc32n', | |
| 128 | + 'stdc_c32nrtoc8n', 'stdc_c32nrtomcn', 'stdc_c32nrtomwcn', 'stdc_c32snrtoc16sn', 'stdc_c32snrtoc32sn', 'stdc_c32snrtoc8sn', 'stdc_c32snrtomcsn', 'stdc_c32snrtomwcsn', | |
| 129 | + 'stdc_c8nrtoc16n', 'stdc_c8nrtoc32n', 'stdc_c8nrtoc8n', 'stdc_c8nrtomcn', 'stdc_c8nrtomwcn', 'stdc_c8snrtoc16sn', 'stdc_c8snrtoc32sn', 'stdc_c8snrtoc8sn', 'stdc_c8snrtomwcsn', 'stdc_c8snrtomcsn', | |
| 130 | + ], | |
| 131 | + 'string.h' => [ // C89 | |
| 132 | + 'memccpy', 'memchr', 'memcmp', 'memcpy', 'memcpy_s', 'memmove', 'memmove_s', 'memset', 'memset_explicit', 'memset_s', | |
| 133 | + 'strcat', 'strcat_s', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcpy_s', 'strcspn', 'strdup', 'strlen', 'strncat', 'strncat_s', 'strncmp', 'strncpy', 'strncpy_s', | |
| 134 | + 'strndup', 'strnlen_s', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok', 'strtok_s', 'strxfrm', | |
| 135 | + // __STD_WANT_LIB_EXT1__ | |
| 136 | + 'memcpy_s', 'memmove_s', 'memset_s', 'strcat_s', 'strcpy_s', 'strerror_s', 'strerrorlen_s', 'strncat_s', 'strncpy_s', 'strnlen_s', 'strtok_s', | |
| 137 | + ], | |
| 138 | + 'threads.h' => [ // C11 | |
| 139 | + 'call_once', 'cnd_broadcast', 'cnd_destroy', 'cnd_init', 'cnd_signal', 'cnd_timedwait', 'cnd_wait', | |
| 140 | + 'mtx_destroy', 'mtx_init', 'mtx_lock', 'mtx_timedlock', 'mtx_trylock', 'mtx_unlock', | |
| 141 | + 'thrd_create', 'thrd_current', 'thrd_detach', 'thrd_equal', 'thrd_exit', 'thrd_join', 'thrd_sleep', 'thrd_yield', 'tss_create', 'tss_delete', 'tss_get', 'tss_set', | |
| 142 | + ], | |
| 143 | + 'time.h' => [ // C89 | |
| 144 | + 'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'gmtime_r', 'localtime', 'localtime_r', 'mktime', | |
| 145 | + 'strftime', 'time', 'timegm', | |
| 146 | + // __STD_WANT_LIB_EXT1__ | |
| 147 | + 'asctime_s', 'ctime_s', 'gmtime_s', 'localtime_s', | |
| 148 | + ], | |
| 149 | + 'uchar.h' => [ // C11 | |
| 150 | + 'c8rtomb', 'c16rtomb', 'c32rtomb', 'mbrtoc8', 'mbrtoc16', 'mbrtoc32', | |
| 151 | + ], | |
| 152 | + 'wchar.h' => [ // C95 | |
| 153 | + 'wcscat', 'wcscat_s', 'wcschr', 'wcscmp', 'wcscpy', 'wcscpy_s', 'wcscroll', 'wcscroll', 'wcscspn', 'wcslen', 'wcsncpy', 'wcsncpy_s', 'wcsncat', 'wcsncat_s', | |
| 154 | + 'wcsncmp', 'wcsnlen_s', 'wcsrchr', 'wcspbrk', 'wcsstr', 'wcstok', 'wcstok_s', 'wcsxfrm', | |
| 155 | + 'wmemchr', 'wmemcmp', 'wmemcpy', 'wmemcpy_s', 'wmemmove', 'wmemmove_s', 'wmemset', | |
| 156 | + // __STDC_IEC_60559_DFP__ | |
| 157 | + 'wcstod32', 'wcstod64', 'wcstod128', | |
| 158 | + // __STDC_IEC_60559_TYPES__ | |
| 159 | + 'encptr', 'nptr', | |
| 160 | + // __STDC_WANT_LIB_EXT1__ | |
| 161 | + 'fwprintf_s', 'fwscanf_s', 'mvsrtowcs_s', 'snwprinf_s', 'swprintf_s', 'swscanf_s', 'vfwprintf_s', 'vfwscanf_s', 'vsnwprinf_s', 'vswprintf_s', 'vswscanf_s', | |
| 162 | + 'wcrtomb_s', 'wcscat_s', 'wcsncat_s', 'wcstok_s', 'wcsnlen_s', 'wcscpy_s', 'wcsncpy_s', 'wcsrtombs_s', 'wmemcpy_s', 'wmemmove_s', 'wprintf_s', 'wscanf_s', | |
| 163 | + ], | |
| 164 | + 'wctype.h' => [ // C95 | |
| 165 | + 'iswalnum', 'iswalpha', 'iswblank', 'iswcntrl', 'iswctype', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace', 'iswupper', 'iswxdigit', | |
| 166 | + 'towctrans', 'towlower', 'towupper', | |
| 167 | + 'wctrans', 'cwtype', | |
| 168 | + ], | |
| 169 | + ], | |
| 170 | + 'preprocessor' => [ | |
| 171 | + // C23 | |
| 172 | + '#elifdef', '#elifndef', '#embed', '#warning', '__has_include', '__has_embed', '__has_c_attribute', | |
| 173 | + // C99 | |
| 174 | + '#_Pragma', | |
| 175 | + // C89 | |
| 176 | + '#if', '#elif', '#else', '#endif', '#ifdef', '#ifndef', '#define', '#undef', '#include', '#line', '#error', '#pragma', '#defined', | |
| 177 | + ], | |
| 178 | + 'type' => [ | |
| 179 | + 'errno.h' => [ | |
| 180 | + 'errno_t', | |
| 181 | + ], | |
| 182 | + 'inttypes.h' => [ | |
| 183 | + 'intmax_t', 'uintmax_t', 'imaxdiv_t', | |
| 184 | + ], | |
| 185 | + 'iso646.h' => [ | |
| 186 | + 'and'/*&&*/, 'and_eq'/*&=*/, 'bitand'/*&*/, 'bitor'/*|*/, 'compl'/*~*/, 'not'/*!*/, 'not_eq'/*!=*/, 'or'/*||*/, 'or_eq',/*|=*/ 'xor'/*^*/, 'xor_eq'/*^=*/, | |
| 187 | + ], | |
| 188 | + 'setjmp.h' => [ | |
| 189 | + 'jmp_buf', | |
| 190 | + ], | |
| 191 | + 'signal.h' => [ | |
| 192 | + 'sig_atomic_t', | |
| 193 | + ], | |
| 194 | + 'stdatomic.h' => [ | |
| 195 | + 'atomic_bool', 'atomic_char', 'atomic_schar', 'atomic_uchar', 'atomic_short', 'atomic_ushort', 'atomic_int', 'atomic_uint', 'atomic_long', 'atomic_ulong', | |
| 196 | + 'atomic_llong', 'atomic_ullong', 'atomic_char8_t', 'atomic_char16_t', 'atomic_char32_t', 'atomic_wchar_t', 'atomic_int_least8_t', 'atomic_uint_least8_t', | |
| 197 | + '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', | |
| 198 | + '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', | |
| 199 | + 'atomic_intptr_t', 'atomic_uintptr_t', 'atomic_size_t', 'atomic_ptrdiff_t', 'atomic_intmax_t', 'atomic_uintmax_t', | |
| 200 | + ], | |
| 201 | + 'stddef.h' => [ | |
| 202 | + 'max_align_t', 'nullptr', 'ptrdiff_t', 'size_t', | |
| 203 | + ], | |
| 204 | + 'stdio.h' => [ | |
| 205 | + 'FILE', 'size_t', 'fpos_t', | |
| 206 | + ], | |
| 207 | + 'stdint.h' => [ | |
| 208 | + 'int8_t', 'int16_t', 'int32_t', 'int64_t', | |
| 209 | + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', | |
| 210 | + 'fpos_t', 'intptr_t', 'intmax_t', 'uintptr_t', 'uintmax_t', | |
| 211 | + 'int_least8_t', 'int_least16_t', 'int_least32_t', 'int_least64_t', | |
| 212 | + 'uint_least8_t', 'uint_least16_t', 'uint_least32_t', 'uint_least64_t', | |
| 213 | + 'int_fast8_t', 'int_fast16_t', 'int_fast32_t', 'int_fast64_t', | |
| 214 | + 'uint_fast8_t', 'uint_fast16_t', 'uint_fast32_t', 'uint_fast64_t', | |
| 215 | + ], | |
| 216 | + 'stdmchar.h' => [ | |
| 217 | + 'stdc_mcerr', | |
| 218 | + ], | |
| 219 | + 'stdarg.h' => [ | |
| 220 | + 'va_list', | |
| 221 | + ], | |
| 222 | + 'threads.h' => [ | |
| 223 | + 'cnd_t', | |
| 224 | + 'mtx_t', | |
| 225 | + 'thrd_t', | |
| 226 | + 'thrd_start_t', | |
| 227 | + 'tss_t', | |
| 228 | + 'tss_dtor_t', | |
| 229 | + ], | |
| 230 | + 'time.h' => [ | |
| 231 | + 'rsize_t', 'clock_t', 'time_t', | |
| 232 | + ], | |
| 233 | + 'uchar.h' => [ | |
| 234 | + 'char8_t', 'char16_t', 'char32_t', 'mbstate_t', | |
| 235 | + ], | |
| 236 | + 'wchar.h' => [ | |
| 237 | + 'wchar_t', 'wint_t', | |
| 238 | + ], | |
| 239 | + 'wctype.h' => [ | |
| 240 | + 'wctrans_t', 'wctype_t', | |
| 241 | + ], | |
| 242 | + ], | |
| 243 | + ]; | |
| 244 | + | |
| 245 | + /** @var array よく使う定数(ヘッダーに関係なく色を付ける) */ | |
| 246 | + protected array $constants = [ | |
| 247 | + 'NULL', 'EOF', 'stdin', 'stdout', 'stderr', 'EXIT_SUCCESS', 'EXIT_FAILURE', 'RAND_MAX', | |
| 248 | + 'CHAR_BIT', 'CHAR_MIN', 'CHAR_MAX', 'SCHAR_MIN', 'SCHAR_MAX', 'UCHAR_MAX', 'SHRT_MIN', 'SHRT_MAX', 'USHRT_MAX', | |
| 249 | + 'INT_MIN', 'INT_MAX', 'UINT_MAX', 'LONG_MIN', 'LONG_MAX', 'ULONG_MAX', 'LLONG_MIN', 'LLONG_MAX', 'ULLONG_MAX', | |
| 250 | + 'INT8_MIN', 'INT8_MAX', 'INT16_MIN', 'INT16_MAX', 'INT32_MIN', 'INT32_MAX', 'INT64_MIN', 'INT64_MAX', | |
| 251 | + 'UINT8_MAX', 'UINT16_MAX', 'UINT32_MAX', 'UINT64_MAX', 'SIZE_MAX', 'PTRDIFF_MIN', 'PTRDIFF_MAX', | |
| 252 | + 'FLT_MAX', 'FLT_MIN', 'FLT_EPSILON', 'DBL_MAX', 'DBL_MIN', 'DBL_EPSILON', 'HUGE_VAL', 'INFINITY', 'NAN', | |
| 253 | + 'M_PI', 'M_E', 'SEEK_SET', 'SEEK_CUR', 'SEEK_END', 'BUFSIZ', 'FILENAME_MAX', | |
| 254 | + '__FILE__', '__LINE__', '__DATE__', '__TIME__', '__func__', '__STDC__', '__STDC_VERSION__', | |
| 255 | + ]; | |
| 256 | + | |
| 257 | + /** | |
| 258 | + * #includeされたヘッダーの関数と型だけに色を付ける | |
| 259 | + * #includeが1つも無い場合(ブログの抜粋等)は全部に色を付ける | |
| 260 | + */ | |
| 261 | + public function prepare(string $code): void { | |
| 262 | + $tokens = $this->tokens; | |
| 263 | + preg_match_all('~^[ \t]*#[ \t]*include[ \t]*[<"]([^>"\n]+)[>"]~m', $code, $m); | |
| 264 | + if (!empty($m[1])) { | |
| 265 | + $inc = array_flip(array_map('trim', $m[1])); | |
| 266 | + foreach (['builtin', 'type'] as $k) { | |
| 267 | + if (isset($tokens[$k])) $tokens[$k] = array_intersect_key($tokens[$k], $inc); | |
| 268 | + } | |
| 269 | + } | |
| 270 | + $this->words = $this->buildWords(['constant' => $this->constants] + $tokens); | |
| 271 | + } | |
| 272 | +} |
| @@ -0,0 +1,45 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class CSharp extends Language { | |
| 5 | + protected bool $preprocessor = true; | |
| 6 | + protected string $digitSep = '_'; | |
| 7 | + // 「@class」の様な逐語的識別子 | |
| 8 | + protected string $identifier = '@?[\p{L}_][\p{L}\p{N}_]*'; | |
| 9 | + | |
| 10 | + protected array $rules = [ | |
| 11 | + // 生文字列 """…"""($"""…"""も) | |
| 12 | + ['re' => '\$*"""(?s:.*?)(?:"""|\z)', 'class' => 'string'], | |
| 13 | + // 逐語的文字列 @"…"(""でエスケープ、複数行可)、$@"…"、@$"…" | |
| 14 | + ['re' => '(?:\$@|@\$|@)"(?:[^"]|"")*"?', 'class' => 'string'], | |
| 15 | + // 補間文字列 $"…" | |
| 16 | + ['re' => '\$"(?:[^"\\\\\n]|\\\\.)*"?', 'class' => 'string'], | |
| 17 | + ]; | |
| 18 | + | |
| 19 | + protected array $tokens = [ | |
| 20 | + 'keyword' => [ | |
| 21 | + 'abstract', 'as', 'base', 'break', 'case', 'catch', 'checked', 'class', 'const', 'continue', 'default', 'delegate', 'do', 'else', | |
| 22 | + 'enum', 'event', 'explicit', 'extern', 'finally', 'fixed', 'for', 'foreach', 'goto', 'if', 'implicit', 'in', 'interface', 'internal', | |
| 23 | + 'is', 'lock', 'namespace', 'new', 'operator', 'out', 'override', 'params', 'private', 'protected', 'public', 'readonly', 'ref', | |
| 24 | + 'return', 'sealed', 'sizeof', 'stackalloc', 'static', 'struct', 'switch', 'this', 'throw', 'try', 'typeof', 'unchecked', 'unsafe', | |
| 25 | + 'using', 'virtual', 'volatile', 'while', | |
| 26 | + // 文脈依存(「value」「select」「file」等、変数名によく使う単語は除く) | |
| 27 | + 'alias', 'and', 'async', 'await', 'get', 'global', 'init', 'managed', 'nameof', 'not', 'notnull', 'or', 'partial', 'record', | |
| 28 | + 'required', 'scoped', 'set', 'unmanaged', 'var', 'when', 'where', 'with', 'yield', | |
| 29 | + ], | |
| 30 | + 'constant' => [ | |
| 31 | + 'true', 'false', 'null', | |
| 32 | + ], | |
| 33 | + 'type' => [ | |
| 34 | + 'bool', 'byte', 'sbyte', 'char', 'decimal', 'double', 'float', 'int', 'uint', 'nint', 'nuint', 'long', 'ulong', 'short', 'ushort', | |
| 35 | + 'object', 'string', 'void', 'dynamic', | |
| 36 | + 'Boolean', 'Byte', 'SByte', 'Char', 'Decimal', 'Double', 'Single', 'Int16', 'Int32', 'Int64', 'Int128', 'UInt16', 'UInt32', 'UInt64', | |
| 37 | + 'IntPtr', 'UIntPtr', 'Half', 'Object', 'String', 'Guid', 'DateTime', 'TimeSpan', 'Span', 'ReadOnlySpan', 'Memory', 'ReadOnlyMemory', | |
| 38 | + 'List', 'Dictionary', 'HashSet', 'Queue', 'Stack', 'LinkedList', 'SortedList', 'SortedDictionary', 'Array', 'Tuple', 'ValueTuple', | |
| 39 | + 'IEnumerable', 'IEnumerator', 'ICollection', 'IList', 'IDictionary', 'IReadOnlyList', 'IDisposable', 'IComparable', 'IEquatable', | |
| 40 | + 'Task', 'ValueTask', 'Action', 'Func', 'Predicate', 'EventHandler', 'Exception', 'ArgumentException', 'ArgumentNullException', | |
| 41 | + 'InvalidOperationException', 'NotImplementedException', 'Console', 'Math', 'MathF', 'Convert', 'Enumerable', 'StringBuilder', | |
| 42 | + 'Thread', 'CancellationToken', 'Stream', 'File', 'Path', 'Directory', 'Random', 'Vector2', 'Vector3', 'Vector4', 'Matrix4x4', 'Quaternion', | |
| 43 | + ], | |
| 44 | + ]; | |
| 45 | +} |
| @@ -0,0 +1,74 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class Cpp extends C { | |
| 5 | + protected array $headerDirectives = ['include', 'include_next', 'embed', 'import']; | |
| 6 | + | |
| 7 | + protected array $rules = [ | |
| 8 | + // 生文字列 R"区切り(…)区切り" | |
| 9 | + ['re' => '(?:u8|[uUL])?R"(?<rawdelim>[^()\\\\\s]{0,16})\((?s:.*?)\)\k<rawdelim>"', 'class' => 'string'], | |
| 10 | + // 属性 [[nodiscard]] | |
| 11 | + ['re' => '\[\[[^\]\n]*\]\]', 'class' => 'attribute'], | |
| 12 | + ]; | |
| 13 | + | |
| 14 | + /** @var array C++のトークン(Cの関数・型は prepare() で追加する) */ | |
| 15 | + protected array $cppTokens = [ | |
| 16 | + 'keyword' => [ | |
| 17 | + // C++26 | |
| 18 | + 'contract_assert', 'pre', 'post', 'trivially_relocatable_if_eligible', 'replaceable_if_eligible', | |
| 19 | + // C++20 | |
| 20 | + 'char8_t', 'concept', 'consteval', 'constinit', 'co_await', 'co_return', 'co_yield', 'import', 'module', 'requires', | |
| 21 | + // C++11 | |
| 22 | + 'alignas', 'alignof', 'char16_t', 'char32_t', 'constexpr', 'decltype', 'final', 'noexcept', 'nullptr', 'override', 'static_assert', 'thread_local', | |
| 23 | + // C++98 | |
| 24 | + 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'class', 'compl', 'const', 'const_cast', 'continue', | |
| 25 | + 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', | |
| 26 | + 'int', 'long', 'mutable', 'namespace', 'new', 'not', 'not_eq', 'operator', 'or', 'or_eq', 'private', 'protected', 'public', 'register', | |
| 27 | + 'reinterpret_cast', 'return', 'short', 'signed', 'sizeof', 'static', 'static_cast', 'struct', 'switch', 'template', 'this', 'throw', 'true', 'try', | |
| 28 | + 'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq', | |
| 29 | + // TMTS | |
| 30 | + 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'synchronized', 'transaction_safe', 'transaction_safe_dynamic', | |
| 31 | + ], | |
| 32 | + 'type' => [ | |
| 33 | + 'std', 'chrono', 'filesystem', 'ranges', 'views', 'this_thread', 'numbers', 'execution', 'pmr', | |
| 34 | + 'string', 'wstring', 'u8string', 'u16string', 'u32string', 'string_view', 'wstring_view', | |
| 35 | + 'vector', 'array', 'deque', 'list', 'forward_list', 'map', 'multimap', 'set', 'multiset', | |
| 36 | + 'unordered_map', 'unordered_multimap', 'unordered_set', 'unordered_multiset', 'flat_map', 'flat_set', | |
| 37 | + 'stack', 'queue', 'priority_queue', 'span', 'mdspan', 'bitset', 'valarray', | |
| 38 | + 'pair', 'tuple', 'optional', 'variant', 'any', 'expected', 'unexpected', 'monostate', 'nullopt_t', 'nullptr_t', 'byte', | |
| 39 | + 'unique_ptr', 'shared_ptr', 'weak_ptr', 'function', 'move_only_function', 'reference_wrapper', 'initializer_list', | |
| 40 | + 'thread', 'jthread', 'mutex', 'recursive_mutex', 'shared_mutex', 'lock_guard', 'unique_lock', 'scoped_lock', 'condition_variable', | |
| 41 | + 'atomic', 'atomic_flag', 'future', 'promise', 'stop_token', 'stop_source', | |
| 42 | + 'istream', 'ostream', 'iostream', 'ifstream', 'ofstream', 'fstream', 'stringstream', 'istringstream', 'ostringstream', | |
| 43 | + 'size_t', 'ptrdiff_t', 'intptr_t', 'uintptr_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', | |
| 44 | + 'float16_t', 'float32_t', 'float64_t', 'float128_t', 'bfloat16_t', 'max_align_t', | |
| 45 | + 'exception', 'runtime_error', 'logic_error', 'invalid_argument', 'out_of_range', 'bad_alloc', 'error_code', | |
| 46 | + 'iterator', 'const_iterator', 'reverse_iterator', 'allocator', 'hash', 'less', 'greater', 'equal_to', | |
| 47 | + 'duration', 'time_point', 'steady_clock', 'system_clock', 'high_resolution_clock', 'path', 'regex', 'complex', 'ratio', | |
| 48 | + ], | |
| 49 | + 'builtin' => [ | |
| 50 | + 'cout', 'cin', 'cerr', 'clog', 'wcout', 'wcin', 'endl', 'flush', 'ws', 'getline', 'print', 'println', 'format', | |
| 51 | + 'move', 'forward', 'swap', 'exchange', 'make_unique', 'make_shared', 'make_pair', 'make_tuple', 'make_optional', 'tie', 'get', | |
| 52 | + 'static_pointer_cast', 'dynamic_pointer_cast', 'reinterpret_pointer_cast', 'addressof', 'launder', 'bit_cast', 'to_underlying', | |
| 53 | + 'sort', 'stable_sort', 'find', 'find_if', 'count', 'count_if', 'copy', 'copy_if', 'fill', 'transform', 'accumulate', 'reduce', | |
| 54 | + 'for_each', 'remove', 'remove_if', 'reverse', 'unique', 'min', 'max', 'clamp', 'min_element', 'max_element', 'lower_bound', 'upper_bound', | |
| 55 | + 'all_of', 'any_of', 'none_of', 'iota', 'to_string', 'stoi', 'stol', 'stof', 'stod', 'invoke', 'apply', 'visit', 'holds_alternative', | |
| 56 | + 'declval', 'is_same_v', 'is_integral_v', 'is_floating_point_v', 'enable_if_t', 'conditional_t', 'decay_t', 'remove_reference_t', | |
| 57 | + 'numeric_limits', 'begin', 'end', 'cbegin', 'cend', 'rbegin', 'rend', 'size', 'ssize', 'empty', 'data', 'unreachable', | |
| 58 | + ], | |
| 59 | + ]; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * C++のトークンに、Cのライブラリ関数と型を加える(ヘッダーでは絞らない) | |
| 63 | + */ | |
| 64 | + public function prepare(string $code): void { | |
| 65 | + $tokens = $this->cppTokens; | |
| 66 | + foreach (['builtin', 'type'] as $k) { | |
| 67 | + foreach ($this->tokens[$k] ?? [] as $list) { | |
| 68 | + foreach ($list as $w) $tokens[$k][] = $w; | |
| 69 | + } | |
| 70 | + } | |
| 71 | + $tokens['preprocessor'] = $this->tokens['preprocessor'] ?? []; | |
| 72 | + $this->words = $this->buildWords(['constant' => $this->constants] + $tokens); | |
| 73 | + } | |
| 74 | +} |
| @@ -0,0 +1,100 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class Glsl extends Language { | |
| 5 | + protected bool $preprocessor = true; | |
| 6 | + protected array $headerDirectives = ['include']; | |
| 7 | + protected array $strings = ['"']; | |
| 8 | + protected string $digitSep = ''; | |
| 9 | + | |
| 10 | + protected array $tokens = [ | |
| 11 | + 'keyword' => [ | |
| 12 | + 'attribute', 'const', 'uniform', 'varying', 'buffer', 'shared', 'layout', 'centroid', 'flat', 'smooth', 'noperspective', | |
| 13 | + 'patch', 'sample', 'break', 'continue', 'do', 'for', 'while', 'switch', 'case', 'default', 'if', 'else', 'subroutine', | |
| 14 | + 'in', 'out', 'inout', 'invariant', 'precise', 'discard', 'return', 'struct', 'precision', 'highp', 'mediump', 'lowp', | |
| 15 | + 'coherent', 'volatile', 'restrict', 'readonly', 'writeonly', 'demote', 'terminateInvocation', 'rayPayloadEXT', 'hitAttributeEXT', | |
| 16 | + 'callableDataEXT', 'taskPayloadSharedEXT', 'perprimitiveEXT', | |
| 17 | + // layout()の中(「offset」「points」等、変数名にも使う単語は除く) | |
| 18 | + 'location', 'binding', 'set', 'push_constant', 'std140', 'std430', 'local_size_x', 'local_size_y', 'local_size_z', | |
| 19 | + 'input_attachment_index', 'constant_id', 'row_major', 'column_major', 'early_fragment_tests', 'max_vertices', | |
| 20 | + ], | |
| 21 | + 'constant' => [ | |
| 22 | + 'true', 'false', '__FILE__', '__LINE__', '__VERSION__', 'GL_ES', | |
| 23 | + ], | |
| 24 | + 'type' => [ | |
| 25 | + 'void', 'bool', 'int', 'uint', 'float', 'double', 'atomic_uint', 'genType', 'genIType', 'genUType', 'genBType', 'genDType', | |
| 26 | + 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'float16_t', 'float32_t', 'float64_t', | |
| 27 | + 'accelerationStructureEXT', 'rayQueryEXT', | |
| 28 | + 'subpassInput', 'subpassInputMS', 'isubpassInput', 'isubpassInputMS', 'usubpassInput', 'usubpassInputMS', 'sampler', 'samplerShadow', | |
| 29 | + ], | |
| 30 | + 'builtin' => [ | |
| 31 | + // 角度・三角関数 | |
| 32 | + 'radians', 'degrees', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh', | |
| 33 | + // 指数 | |
| 34 | + 'pow', 'exp', 'log', 'exp2', 'log2', 'sqrt', 'inversesqrt', | |
| 35 | + // 共通 | |
| 36 | + 'abs', 'sign', 'floor', 'trunc', 'round', 'roundEven', 'ceil', 'fract', 'mod', 'modf', 'min', 'max', 'clamp', 'mix', 'step', 'smoothstep', | |
| 37 | + 'isnan', 'isinf', 'floatBitsToInt', 'floatBitsToUint', 'intBitsToFloat', 'uintBitsToFloat', 'fma', 'frexp', 'ldexp', | |
| 38 | + 'packUnorm2x16', 'packSnorm2x16', 'packUnorm4x8', 'packSnorm4x8', 'unpackUnorm2x16', 'unpackSnorm2x16', 'unpackUnorm4x8', 'unpackSnorm4x8', | |
| 39 | + 'packHalf2x16', 'unpackHalf2x16', 'packDouble2x32', 'unpackDouble2x32', | |
| 40 | + // 幾何 | |
| 41 | + 'length', 'distance', 'dot', 'cross', 'normalize', 'faceforward', 'reflect', 'refract', 'ftransform', | |
| 42 | + // 行列・ベクトル比較 | |
| 43 | + 'matrixCompMult', 'outerProduct', 'transpose', 'determinant', 'inverse', | |
| 44 | + 'lessThan', 'lessThanEqual', 'greaterThan', 'greaterThanEqual', 'equal', 'notEqual', 'any', 'all', 'not', | |
| 45 | + // 整数 | |
| 46 | + 'uaddCarry', 'usubBorrow', 'umulExtended', 'imulExtended', 'bitfieldExtract', 'bitfieldInsert', 'bitfieldReverse', 'bitCount', 'findLSB', 'findMSB', | |
| 47 | + // テクスチャ | |
| 48 | + 'textureSize', 'textureQueryLod', 'textureQueryLevels', 'textureSamples', 'texture', 'textureProj', 'textureLod', 'textureOffset', | |
| 49 | + 'texelFetch', 'texelFetchOffset', 'textureProjOffset', 'textureLodOffset', 'textureProjLod', 'textureProjLodOffset', | |
| 50 | + 'textureGrad', 'textureGradOffset', 'textureProjGrad', 'textureProjGradOffset', 'textureGather', 'textureGatherOffset', 'textureGatherOffsets', | |
| 51 | + 'texture1D', 'texture1DProj', 'texture1DLod', 'texture1DProjLod', 'texture2D', 'texture2DProj', 'texture2DLod', 'texture2DProjLod', | |
| 52 | + 'texture3D', 'texture3DProj', 'texture3DLod', 'texture3DProjLod', 'textureCube', 'textureCubeLod', | |
| 53 | + 'shadow1D', 'shadow1DProj', 'shadow1DLod', 'shadow1DProjLod', 'shadow2D', 'shadow2DProj', 'shadow2DLod', 'shadow2DProjLod', | |
| 54 | + // 原子操作・画像 | |
| 55 | + 'atomicCounterIncrement', 'atomicCounterDecrement', 'atomicCounter', 'atomicAdd', 'atomicMin', 'atomicMax', 'atomicAnd', 'atomicOr', | |
| 56 | + 'atomicXor', 'atomicExchange', 'atomicCompSwap', 'imageSize', 'imageSamples', 'imageLoad', 'imageStore', 'imageAtomicAdd', | |
| 57 | + 'imageAtomicMin', 'imageAtomicMax', 'imageAtomicAnd', 'imageAtomicOr', 'imageAtomicXor', 'imageAtomicExchange', 'imageAtomicCompSwap', | |
| 58 | + // 微分・補間・ジオメトリ・同期 | |
| 59 | + 'dFdx', 'dFdy', 'dFdxFine', 'dFdyFine', 'dFdxCoarse', 'dFdyCoarse', 'fwidth', 'fwidthFine', 'fwidthCoarse', | |
| 60 | + 'interpolateAtCentroid', 'interpolateAtSample', 'interpolateAtOffset', 'EmitStreamVertex', 'EndStreamPrimitive', 'EmitVertex', 'EndPrimitive', | |
| 61 | + 'barrier', 'memoryBarrier', 'memoryBarrierAtomicCounter', 'memoryBarrierBuffer', 'memoryBarrierShared', 'memoryBarrierImage', 'groupMemoryBarrier', | |
| 62 | + 'subpassLoad', 'noise1', 'noise2', 'noise3', 'noise4', | |
| 63 | + // サブグループ・レイトレーシング | |
| 64 | + 'subgroupBarrier', 'subgroupElect', 'subgroupAll', 'subgroupAny', 'subgroupBroadcast', 'subgroupBroadcastFirst', 'subgroupBallot', | |
| 65 | + 'subgroupAdd', 'subgroupMul', 'subgroupMin', 'subgroupMax', 'subgroupShuffle', 'subgroupShuffleXor', | |
| 66 | + 'traceRayEXT', 'reportIntersectionEXT', 'executeCallableEXT', 'ignoreIntersectionEXT', 'terminateRayEXT', 'EmitMeshTasksEXT', 'SetMeshOutputsEXT', | |
| 67 | + ], | |
| 68 | + ]; | |
| 69 | + | |
| 70 | + public function prepare(string $code): void { | |
| 71 | + $tokens = $this->tokens; | |
| 72 | + // ベクトル・行列 | |
| 73 | + foreach (['vec', 'ivec', 'uvec', 'bvec', 'dvec', 'i8vec', 'u8vec', 'i16vec', 'u16vec', 'i64vec', 'u64vec', 'f16vec', 'f64vec'] as $v) { | |
| 74 | + for ($i = 2; $i <= 4; $i++) $tokens['type'][] = "{$v}{$i}"; | |
| 75 | + } | |
| 76 | + foreach (['mat', 'dmat', 'f16mat', 'f64mat'] as $m) { | |
| 77 | + for ($i = 2; $i <= 4; $i++) { | |
| 78 | + $tokens['type'][] = "{$m}{$i}"; | |
| 79 | + for ($j = 2; $j <= 4; $j++) $tokens['type'][] = "{$m}{$i}x{$j}"; | |
| 80 | + } | |
| 81 | + } | |
| 82 | + // サンプラー・テクスチャ・画像 | |
| 83 | + $dims = ['1D', '2D', '3D', 'Cube', '2DRect', '1DArray', '2DArray', 'CubeArray', 'Buffer', '2DMS', '2DMSArray']; | |
| 84 | + foreach (['', 'i', 'u'] as $p) { | |
| 85 | + foreach ($dims as $d) { | |
| 86 | + $tokens['type'][] = "{$p}sampler{$d}"; | |
| 87 | + $tokens['type'][] = "{$p}texture{$d}"; | |
| 88 | + $tokens['type'][] = "{$p}image{$d}"; | |
| 89 | + } | |
| 90 | + } | |
| 91 | + foreach (['1D', '2D', 'Cube', '2DRect', '1DArray', '2DArray', 'CubeArray'] as $d) $tokens['type'][] = "sampler{$d}Shadow"; | |
| 92 | + $this->words = $this->buildWords($tokens); | |
| 93 | + } | |
| 94 | + | |
| 95 | + public function classify(string $word, array $ctx): ?string { | |
| 96 | + // 組み込み変数(gl_Position, gl_GlobalInvocationID等) | |
| 97 | + if (str_starts_with($word, 'gl_')) return 'variable'; | |
| 98 | + return parent::classify($word, $ctx); | |
| 99 | + } | |
| 100 | +} |
| @@ -0,0 +1,28 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class Go extends Language { | |
| 5 | + protected string $digitSep = '_'; | |
| 6 | + | |
| 7 | + protected array $rules = [ | |
| 8 | + // 生文字列 `…`(複数行可) | |
| 9 | + ['re' => '`[^`]*`?', 'class' => 'string'], | |
| 10 | + ]; | |
| 11 | + | |
| 12 | + protected array $tokens = [ | |
| 13 | + 'keyword' => [ | |
| 14 | + 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else', 'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface', 'map', | |
| 15 | + 'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var', | |
| 16 | + ], | |
| 17 | + 'constant' => [ | |
| 18 | + 'true', 'false', 'iota', 'nil', | |
| 19 | + ], | |
| 20 | + 'type' => [ | |
| 21 | + 'uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64', 'float32', 'float64', 'complex64', 'complex128', | |
| 22 | + 'byte', 'rune', 'string', 'bool', 'uint', 'int', 'uintptr', 'error', 'any', 'comparable', | |
| 23 | + ], | |
| 24 | + 'builtin' => [ | |
| 25 | + 'append', 'cap', 'clear', 'close', 'complex', 'copy', 'delete', 'imag', 'len', 'make', 'max', 'min', 'new', 'panic', 'print', 'println', 'real', 'recover', | |
| 26 | + ], | |
| 27 | + ]; | |
| 28 | +} |
| @@ -0,0 +1,87 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class Hlsl extends Language { | |
| 5 | + protected bool $preprocessor = true; | |
| 6 | + protected array $headerDirectives = ['include']; | |
| 7 | + protected array $strings = ['"']; | |
| 8 | + protected string $digitSep = ''; | |
| 9 | + | |
| 10 | + protected array $tokens = [ | |
| 11 | + 'keyword' => [ | |
| 12 | + 'if', 'else', 'for', 'while', 'do', 'break', 'continue', 'discard', 'return', 'switch', 'case', 'default', | |
| 13 | + 'struct', 'class', 'interface', 'namespace', 'typedef', 'cbuffer', 'tbuffer', 'register', 'packoffset', 'static', 'const', | |
| 14 | + 'uniform', 'volatile', 'extern', 'inline', 'export', 'in', 'out', 'inout', 'groupshared', 'nointerpolation', 'linear', | |
| 15 | + 'centroid', 'noperspective', 'sample', 'precise', 'row_major', 'column_major', 'snorm', 'unorm', 'globallycoherent', | |
| 16 | + 'shared', 'template', 'typename', 'using', 'sizeof', 'this', 'enum', 'operator', 'payload', 'vertices', 'indices', 'primitives', | |
| 17 | + ], | |
| 18 | + 'constant' => [ | |
| 19 | + 'true', 'false', | |
| 20 | + ], | |
| 21 | + 'type' => [ | |
| 22 | + 'void', 'vector', 'matrix', 'string', | |
| 23 | + 'Texture1D', 'Texture1DArray', 'Texture2D', 'Texture2DArray', 'Texture2DMS', 'Texture2DMSArray', 'Texture3D', 'TextureCube', 'TextureCubeArray', | |
| 24 | + 'RWTexture1D', 'RWTexture1DArray', 'RWTexture2D', 'RWTexture2DArray', 'RWTexture3D', 'RWTexture2DMS', 'RWTexture2DMSArray', | |
| 25 | + 'RasterizerOrderedTexture1D', 'RasterizerOrderedTexture2D', 'RasterizerOrderedTexture3D', 'RasterizerOrderedBuffer', | |
| 26 | + 'FeedbackTexture2D', 'FeedbackTexture2DArray', | |
| 27 | + 'Buffer', 'RWBuffer', 'StructuredBuffer', 'RWStructuredBuffer', 'ByteAddressBuffer', 'RWByteAddressBuffer', | |
| 28 | + 'AppendStructuredBuffer', 'ConsumeStructuredBuffer', 'ConstantBuffer', 'TextureBuffer', | |
| 29 | + 'SamplerState', 'SamplerComparisonState', 'sampler', 'sampler1D', 'sampler2D', 'sampler3D', 'samplerCUBE', | |
| 30 | + 'RaytracingAccelerationStructure', 'RayDesc', 'RayQuery', 'BuiltInTriangleIntersectionAttributes', | |
| 31 | + 'InputPatch', 'OutputPatch', 'PointStream', 'LineStream', 'TriangleStream', | |
| 32 | + ], | |
| 33 | + 'builtin' => [ | |
| 34 | + 'abort', 'abs', 'acos', 'all', 'AllMemoryBarrier', 'AllMemoryBarrierWithGroupSync', 'any', 'asdouble', 'asfloat', 'asfloat16', 'asin', 'asint', | |
| 35 | + 'asint16', 'asuint', 'asuint16', 'atan', 'atan2', 'ceil', 'CheckAccessFullyMapped', 'clamp', 'clip', 'cos', 'cosh', 'countbits', 'cross', | |
| 36 | + 'D3DCOLORtoUBYTE4', 'ddx', 'ddx_coarse', 'ddx_fine', 'ddy', 'ddy_coarse', 'ddy_fine', 'degrees', 'determinant', 'DeviceMemoryBarrier', | |
| 37 | + 'DeviceMemoryBarrierWithGroupSync', 'distance', 'dot', 'dst', 'errorf', 'EvaluateAttributeAtCentroid', 'EvaluateAttributeAtSample', | |
| 38 | + 'EvaluateAttributeSnapped', 'exp', 'exp2', 'f16tof32', 'f32tof16', 'faceforward', 'firstbithigh', 'firstbitlow', 'floor', 'fma', 'fmod', | |
| 39 | + 'frac', 'frexp', 'fwidth', 'GetRenderTargetSampleCount', 'GetRenderTargetSamplePosition', 'GroupMemoryBarrier', 'GroupMemoryBarrierWithGroupSync', | |
| 40 | + 'InterlockedAdd', 'InterlockedAnd', 'InterlockedCompareExchange', 'InterlockedCompareStore', 'InterlockedExchange', 'InterlockedMax', | |
| 41 | + 'InterlockedMin', 'InterlockedOr', 'InterlockedXor', 'isfinite', 'isinf', 'isnan', 'ldexp', 'length', 'lerp', 'lit', 'log', 'log10', 'log2', | |
| 42 | + 'mad', 'max', 'min', 'modf', 'msad4', 'mul', 'noise', 'normalize', 'pow', 'printf', 'radians', 'rcp', 'reflect', 'refract', 'reversebits', | |
| 43 | + 'round', 'rsqrt', 'saturate', 'sign', 'sin', 'sincos', 'sinh', 'smoothstep', 'sqrt', 'step', 'tan', 'tanh', 'tex1D', 'tex2D', 'tex3D', 'texCUBE', | |
| 44 | + 'transpose', 'trunc', 'NonUniformResourceIndex', 'select', 'and', 'or', | |
| 45 | + 'WaveActiveAllEqual', 'WaveActiveAllTrue', 'WaveActiveAnyTrue', 'WaveActiveBallot', 'WaveActiveBitAnd', 'WaveActiveBitOr', 'WaveActiveBitXor', | |
| 46 | + 'WaveActiveCountBits', 'WaveActiveMax', 'WaveActiveMin', 'WaveActiveProduct', 'WaveActiveSum', 'WaveGetLaneCount', 'WaveGetLaneIndex', | |
| 47 | + 'WaveIsFirstLane', 'WavePrefixCountBits', 'WavePrefixProduct', 'WavePrefixSum', 'WaveReadLaneAt', 'WaveReadLaneFirst', | |
| 48 | + 'QuadReadAcrossDiagonal', 'QuadReadAcrossX', 'QuadReadAcrossY', 'QuadReadLaneAt', | |
| 49 | + 'TraceRay', 'ReportHit', 'IgnoreHit', 'AcceptHitAndEndSearch', 'CallShader', 'DispatchRaysIndex', 'DispatchRaysDimensions', | |
| 50 | + 'WorldRayOrigin', 'WorldRayDirection', 'ObjectRayOrigin', 'ObjectRayDirection', 'RayTMin', 'RayTCurrent', 'RayFlags', 'InstanceIndex', | |
| 51 | + 'InstanceID', 'GeometryIndex', 'PrimitiveIndex', 'HitKind', 'ObjectToWorld3x4', 'ObjectToWorld4x3', 'WorldToObject3x4', 'WorldToObject4x3', | |
| 52 | + 'SetMeshOutputCounts', 'DispatchMesh', | |
| 53 | + // リソースのメソッド | |
| 54 | + 'Sample', 'SampleLevel', 'SampleGrad', 'SampleBias', 'SampleCmp', 'SampleCmpLevel', 'SampleCmpLevelZero', 'Load', 'Store', 'GetDimensions', | |
| 55 | + 'Gather', 'GatherRed', 'GatherGreen', 'GatherBlue', 'GatherAlpha', 'GatherCmp', 'CalculateLevelOfDetail', 'Append', 'Consume', | |
| 56 | + 'IncrementCounter', 'DecrementCounter', 'Load2', 'Load3', 'Load4', 'Store2', 'Store3', 'Store4', | |
| 57 | + ], | |
| 58 | + ]; | |
| 59 | + | |
| 60 | + /** @var string[] 「[」の直後だけ属性として扱う名前(「loop」「branch」等は普通の単語でもある) */ | |
| 61 | + protected array $attributes = [ | |
| 62 | + 'numthreads', 'unroll', 'loop', 'branch', 'flatten', 'fastopt', 'allow_uav_condition', 'call', 'forcecase', 'earlydepthstencil', | |
| 63 | + 'maxvertexcount', 'domain', 'partitioning', 'outputtopology', 'outputcontrolpoints', 'patchconstantfunc', 'maxtessfactor', | |
| 64 | + 'instance', 'RootSignature', 'shader', 'WaveSize', 'NodeLaunch', 'NodeDispatchGrid', 'NodeMaxDispatchGrid', 'NumThreads', 'vk', | |
| 65 | + ]; | |
| 66 | + | |
| 67 | + /** @var string[] スカラー型(ベクトル・行列型を作る元) */ | |
| 68 | + protected array $scalars = [ | |
| 69 | + 'bool', 'int', 'uint', 'dword', 'half', 'float', 'double', 'min16float', 'min10float', 'min16int', 'min12int', 'min16uint', | |
| 70 | + 'int16_t', 'uint16_t', 'int32_t', 'uint32_t', 'int64_t', 'uint64_t', 'float16_t', 'float32_t', 'float64_t', | |
| 71 | + ]; | |
| 72 | + | |
| 73 | + public function prepare(string $code): void { | |
| 74 | + $tokens = $this->tokens; | |
| 75 | + foreach (self::vectorTypes($this->scalars) as $t) $tokens['type'][] = $t; | |
| 76 | + $this->words = $this->buildWords($tokens); | |
| 77 | + } | |
| 78 | + | |
| 79 | + public function classify(string $word, array $ctx): ?string { | |
| 80 | + // セマンティクス(: SV_Position, : TEXCOORD0) | |
| 81 | + if (str_starts_with($word, 'SV_')) return 'variable'; | |
| 82 | + if ($ctx['prev'] === ':' && preg_match('/^[A-Z][A-Z0-9_]*$/', $word)) return 'variable'; | |
| 83 | + // 属性([numthreads(8, 8, 1)]) | |
| 84 | + if ($ctx['prev'] === '[' && in_array($word, $this->attributes, true)) return 'attribute'; | |
| 85 | + return parent::classify($word, $ctx); | |
| 86 | + } | |
| 87 | +} |
| @@ -0,0 +1,155 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * 言語定義の基本クラス | |
| 6 | + * C系の言語の既定値を持つので、各言語は違う所だけを上書きする | |
| 7 | + */ | |
| 8 | +abstract class Language implements SyntaxInterface { | |
| 9 | + /** @var string[] 一行コメントの開始 */ | |
| 10 | + protected array $comment = ['//']; | |
| 11 | + /** @var array|null 複数行コメントの[開始, 終了] */ | |
| 12 | + protected ?array $multiline = ['/*', '*/']; | |
| 13 | + /** 複数行コメントを入れ子に出来るか(Swift等) */ | |
| 14 | + protected bool $nestedComments = false; | |
| 15 | + /** @var string[] 文字列の引用符(「\」でエスケープ) */ | |
| 16 | + protected array $strings = ['"', "'"]; | |
| 17 | + /** 行頭の「#」をプリプロセッサとして扱うか */ | |
| 18 | + protected bool $preprocessor = false; | |
| 19 | + /** @var string[] 次の「<…>」をファイル名とするプリプロセッサ指令 */ | |
| 20 | + protected array $headerDirectives = []; | |
| 21 | + /** 大文字と小文字を区別しないか */ | |
| 22 | + protected bool $caseInsensitive = false; | |
| 23 | + /** 「名前(」を関数として扱うか */ | |
| 24 | + protected bool $functionCalls = true; | |
| 25 | + /** 数字の区切り文字(C++の「'」、Go等の「_」) */ | |
| 26 | + protected string $digitSep = "'"; | |
| 27 | + /** 識別子の正規表現 */ | |
| 28 | + protected string $identifier = '[\p{L}_][\p{L}\p{N}_]*'; | |
| 29 | + /** @var array 追加の規則(既定の規則より先に確認する) */ | |
| 30 | + protected array $rules = []; | |
| 31 | + /** | |
| 32 | + * @var array 種類 => 単語一覧 | |
| 33 | + * 先に書いた種類が優先される | |
| 34 | + */ | |
| 35 | + protected array $tokens = []; | |
| 36 | + | |
| 37 | + /** @var array 単語 => 種類 */ | |
| 38 | + protected array $words = []; | |
| 39 | + | |
| 40 | + public function prepare(string $code): void { | |
| 41 | + $this->words = $this->buildWords($this->tokens); | |
| 42 | + } | |
| 43 | + | |
| 44 | + public function getRules(): array { | |
| 45 | + $r = $this->rules; | |
| 46 | + | |
| 47 | + if ($this->multiline && !$this->nestedComments) { | |
| 48 | + [$s, $e] = $this->multiline; | |
| 49 | + $r[] = ['re' => preg_quote($s, '~').'.*?(?:'.preg_quote($e, '~').'|\z)', 'class' => 'comment']; | |
| 50 | + } | |
| 51 | + | |
| 52 | + if (!empty($this->comment)) { | |
| 53 | + $alts = implode('|', array_map(fn($c) => preg_quote($c, '~'), $this->comment)); | |
| 54 | + $r[] = ['re' => "(?:{$alts})[^\\n]*", 'class' => 'comment']; | |
| 55 | + } | |
| 56 | + | |
| 57 | + foreach ($this->strings as $q) { | |
| 58 | + $q = preg_quote($q, '~'); | |
| 59 | + $r[] = ['re' => "{$q}(?:[^{$q}\\\\\\n]|\\\\.)*{$q}?", 'class' => 'string']; | |
| 60 | + } | |
| 61 | + | |
| 62 | + if ($this->preprocessor) { | |
| 63 | + $r[] = ['re' => '#[ \t]*[A-Za-z_]\w*', 'class' => 'preprocessor', 'bol' => true]; | |
| 64 | + } | |
| 65 | + | |
| 66 | + $r[] = ['re' => $this->numberPattern(), 'class' => 'number']; | |
| 67 | + $r[] = ['re' => $this->identifier, 'class' => 'ident']; | |
| 68 | + | |
| 69 | + return $r; | |
| 70 | + } | |
| 71 | + | |
| 72 | + public function classify(string $word, array $ctx): ?string { | |
| 73 | + $key = $this->caseInsensitive ? strtolower($word) : $word; | |
| 74 | + if (isset($this->words[$key])) return $this->words[$key]; | |
| 75 | + if ($this->functionCalls && $ctx['next'] === '(') return 'function'; | |
| 76 | + return null; | |
| 77 | + } | |
| 78 | + | |
| 79 | + public function getNestedComment(): ?array { | |
| 80 | + return $this->nestedComments ? $this->multiline : null; | |
| 81 | + } | |
| 82 | + | |
| 83 | + public function getHeaderDirectives(): array { | |
| 84 | + return $this->headerDirectives; | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 種類 => 単語一覧から、単語 => 種類の表を作る | |
| 89 | + * 一覧が「ヘッダー名 => 単語一覧」の場合はそのまま全部を使う(C::prepare()で絞る) | |
| 90 | + * | |
| 91 | + * @param array $tokens 種類 => 単語一覧 | |
| 92 | + * @return array 単語 => 種類 | |
| 93 | + */ | |
| 94 | + protected function buildWords(array $tokens): array { | |
| 95 | + $words = []; | |
| 96 | + foreach ($tokens as $class => $list) { | |
| 97 | + foreach ($list as $k => $w) { | |
| 98 | + foreach (is_array($w) ? $w : [$w] as $word) { | |
| 99 | + $key = $this->caseInsensitive ? strtolower($word) : $word; | |
| 100 | + if (!isset($words[$key])) $words[$key] = $class; | |
| 101 | + } | |
| 102 | + } | |
| 103 | + } | |
| 104 | + return $words; | |
| 105 | + } | |
| 106 | + | |
| 107 | + /** | |
| 108 | + * C系の数字(16進数・2進数・8進数・浮動小数点・接尾辞) | |
| 109 | + * | |
| 110 | + * @return string 正規表現 | |
| 111 | + */ | |
| 112 | + protected function numberPattern(): string { | |
| 113 | + $s = $this->digitSep === '' ? '' : preg_quote($this->digitSep, '~').'?'; | |
| 114 | + $d = "\\d(?:{$s}\\d)*"; | |
| 115 | + $x = "[0-9A-Fa-f](?:{$s}[0-9A-Fa-f])*"; | |
| 116 | + return "(?:0[xX]{$x}(?:\\.(?:{$x})?)?(?:[pP][+-]?\\d+)?" | |
| 117 | + ."|0[bB][01](?:{$s}[01])*" | |
| 118 | + ."|0[oO][0-7](?:{$s}[0-7])*" | |
| 119 | + ."|(?:{$d}(?:\\.(?!\\.)(?:{$d})?)?|\\.\\d(?:{$s}\\d)*)(?:[eE][+-]?\\d+)?)" | |
| 120 | + ."(?:[A-Za-z_]\\w*)?"; | |
| 121 | + } | |
| 122 | + | |
| 123 | + /** | |
| 124 | + * ベクトル・行列型を作る(float → float2…float4, float2x2…float4x4) | |
| 125 | + * | |
| 126 | + * @param array $bases 基本の型 | |
| 127 | + * @param bool $matrix 行列型も作るか | |
| 128 | + * @return array 型の一覧 | |
| 129 | + */ | |
| 130 | + protected static function vectorTypes(array $bases, bool $matrix = true): array { | |
| 131 | + $out = []; | |
| 132 | + foreach ($bases as $b) { | |
| 133 | + $out[] = $b; | |
| 134 | + for ($i = 1; $i <= 4; $i++) { | |
| 135 | + $out[] = "{$b}{$i}"; | |
| 136 | + if ($matrix) for ($j = 1; $j <= 4; $j++) $out[] = "{$b}{$i}x{$j}"; | |
| 137 | + } | |
| 138 | + } | |
| 139 | + return $out; | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * 「名前0」…「名前N」の一覧を作る(レジスター用) | |
| 144 | + * | |
| 145 | + * @param string|array $prefix 接頭辞 | |
| 146 | + * @param int $from 最初の番号 | |
| 147 | + * @param int $to 最後の番号 | |
| 148 | + * @return array | |
| 149 | + */ | |
| 150 | + protected static function numbered(string|array $prefix, int $from, int $to): array { | |
| 151 | + $out = []; | |
| 152 | + foreach ((array)$prefix as $p) for ($i = $from; $i <= $to; $i++) $out[] = "{$p}{$i}"; | |
| 153 | + return $out; | |
| 154 | + } | |
| 155 | +} |
| @@ -0,0 +1,62 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Objective-C(Cの上位互換) | |
| 6 | + */ | |
| 7 | +class ObjC extends C { | |
| 8 | + protected array $headerDirectives = ['include', 'include_next', 'import']; | |
| 9 | + | |
| 10 | + protected array $rules = [ | |
| 11 | + // NSString @"…" | |
| 12 | + ['re' => '@"(?:[^"\\\\\n]|\\\\.)*"?', 'class' => 'string'], | |
| 13 | + // @interface, @property, @autoreleasepool等 | |
| 14 | + ['re' => '@[A-Za-z_]\w*', 'class' => 'keyword'], | |
| 15 | + // リテラル @[…] @{…} @(…) | |
| 16 | + ['re' => '@(?=[\[{(])', 'class' => 'keyword'], | |
| 17 | + ]; | |
| 18 | + | |
| 19 | + /** @var array Cに加えるObjective-Cのトークン */ | |
| 20 | + protected array $objcTokens = [ | |
| 21 | + 'keyword' => [ | |
| 22 | + 'self', 'super', 'id', 'instancetype', 'Class', 'SEL', 'IMP', 'BOOL', 'in', 'out', 'inout', 'oneway', 'bycopy', 'byref', | |
| 23 | + 'nonatomic', 'atomic', 'strong', 'weak', 'copy', 'assign', 'retain', 'readonly', 'readwrite', 'getter', 'setter', | |
| 24 | + 'nullable', 'nonnull', 'null_resettable', '_Nullable', '_Nonnull', '_Null_unspecified', '__nullable', '__nonnull', | |
| 25 | + '__bridge', '__bridge_transfer', '__bridge_retained', '__block', '__weak', '__strong', '__unsafe_unretained', '__autoreleasing', '__kindof', | |
| 26 | + ], | |
| 27 | + 'constant' => [ | |
| 28 | + 'YES', 'NO', 'nil', 'Nil', 'NULL', | |
| 29 | + ], | |
| 30 | + 'type' => [ | |
| 31 | + 'NSObject', 'NSString', 'NSMutableString', 'NSArray', 'NSMutableArray', 'NSDictionary', 'NSMutableDictionary', 'NSSet', 'NSMutableSet', | |
| 32 | + 'NSNumber', 'NSValue', 'NSData', 'NSMutableData', 'NSDate', 'NSURL', 'NSError', 'NSException', 'NSInteger', 'NSUInteger', 'NSTimeInterval', | |
| 33 | + 'NSRange', 'NSZone', 'NSNotification', 'NSNotificationCenter', 'NSUserDefaults', 'NSFileManager', 'NSBundle', 'NSThread', 'NSTimer', | |
| 34 | + 'NSRunLoop', 'NSAutoreleasePool', 'NSProcessInfo', 'NSApplication', 'NSWindow', 'NSView', 'NSViewController', 'NSColor', 'NSImage', | |
| 35 | + 'UIApplication', 'UIView', 'UIViewController', 'UIWindow', 'UIColor', 'UIImage', 'UILabel', 'UIButton', | |
| 36 | + 'CGFloat', 'CGRect', 'CGPoint', 'CGSize', 'CGContextRef', 'CFStringRef', 'CFTypeRef', 'dispatch_queue_t', 'dispatch_block_t', | |
| 37 | + 'MTLDevice', 'MTLCommandQueue', 'MTLCommandBuffer', 'MTLRenderPipelineState', 'MTLBuffer', 'MTLTexture', | |
| 38 | + ], | |
| 39 | + 'builtin' => [ | |
| 40 | + 'NSLog', 'NSStringFromClass', 'NSClassFromString', 'NSStringFromSelector', 'NSSelectorFromString', 'NSMakeRange', 'NSApplicationMain', | |
| 41 | + 'UIApplicationMain', 'CGRectMake', 'CGPointMake', 'CGSizeMake', 'objc_msgSend', 'objc_getClass', 'class_getName', 'sel_registerName', | |
| 42 | + 'dispatch_async', 'dispatch_sync', 'dispatch_get_main_queue', 'dispatch_get_global_queue', 'dispatch_once', 'dispatch_after', | |
| 43 | + 'MTLCreateSystemDefaultDevice', 'CFRelease', 'CFRetain', | |
| 44 | + 'NS_ENUM', 'NS_OPTIONS', 'NS_ASSUME_NONNULL_BEGIN', 'NS_ASSUME_NONNULL_END', 'NS_DESIGNATED_INITIALIZER', 'NS_UNAVAILABLE', | |
| 45 | + ], | |
| 46 | + ]; | |
| 47 | + | |
| 48 | + public function prepare(string $code): void { | |
| 49 | + parent::prepare($code); | |
| 50 | + foreach ($this->buildWords($this->objcTokens) as $w => $class) { | |
| 51 | + if (!isset($this->words[$w])) $this->words[$w] = $class; | |
| 52 | + } | |
| 53 | + } | |
| 54 | + | |
| 55 | + public function classify(string $word, array $ctx): ?string { | |
| 56 | + $class = parent::classify($word, $ctx); | |
| 57 | + if ($class !== null) return $class; | |
| 58 | + // メッセージのセレクター部分([obj setValue:x forKey:y])。「Foo : NSObject」の様に空白がある場合は除く | |
| 59 | + if ($ctx['nextRaw'] === ':') return 'function'; | |
| 60 | + return null; | |
| 61 | + } | |
| 62 | +} |
| @@ -0,0 +1,58 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Slang(HLSLの上位互換) | |
| 6 | + */ | |
| 7 | +class Slang extends Hlsl { | |
| 8 | + protected array $rules = [ | |
| 9 | + // 属性 [[vk::binding(0, 0)]] | |
| 10 | + ['re' => '\[\[[^\]\n]*\]\]', 'class' => 'attribute'], | |
| 11 | + ]; | |
| 12 | + | |
| 13 | + /** @var array HLSLに加えるSlangのトークン */ | |
| 14 | + protected array $slangTokens = [ | |
| 15 | + 'keyword' => [ | |
| 16 | + 'import', 'module', 'implementing', '__include', '__exported', 'public', 'private', 'internal', 'extension', 'associatedtype', | |
| 17 | + 'where', 'let', 'var', 'func', 'property', 'get', 'set', '__init', '__subscript', 'This', 'typealias', 'throw', 'throws', | |
| 18 | + 'try', 'catch', 'defer', 'is', 'as', 'new', 'some', 'dyn', 'ref', '__ref', '__generic', '__target_switch', 'spirv_asm', | |
| 19 | + '__intrinsic_asm', 'constexpr', 'override', 'no_diff', 'fwd_diff', 'bwd_diff', 'alignof', 'countof', | |
| 20 | + ], | |
| 21 | + 'constant' => [ | |
| 22 | + 'none', 'nullptr', | |
| 23 | + ], | |
| 24 | + 'type' => [ | |
| 25 | + 'Ptr', 'Optional', 'Tuple', 'Array', 'DescriptorHandle', 'ParameterBlock', 'NativeString', 'String', 'int8_t', 'uint8_t', | |
| 26 | + 'Sampler1D', 'Sampler2D', 'Sampler3D', 'SamplerCube', 'Sampler1DArray', 'Sampler2DArray', 'SamplerCubeArray', 'Sampler2DShadow', | |
| 27 | + 'Sampler2DMS', 'Sampler2DMSArray', 'Texture2DShadow', 'TextureCubeShadow', 'RWTexture2DArray', 'Atomic', | |
| 28 | + 'IDifferentiable', 'IDifferentiablePtrType', 'DifferentialPair', 'IArithmetic', 'IFloat', 'IInteger', 'ILogical', 'IComparable', | |
| 29 | + 'IRangedValue', 'IArray', 'IRWArray', 'IBufferDataLayout', 'DefaultDataLayout', 'Std140DataLayout', 'Std430DataLayout', 'ScalarDataLayout', | |
| 30 | + ], | |
| 31 | + 'builtin' => [ | |
| 32 | + 'detach', 'diffPair', 'updatePrimal', 'updateDiff', 'getDiff', 'getPrimal', 'isDifferentialNull', | |
| 33 | + 'printf', 'bit_cast', 'reinterpret', 'createDynamicObject', 'getStringHash', 'static_assert', | |
| 34 | + ], | |
| 35 | + ]; | |
| 36 | + | |
| 37 | + protected array $attributes = [ | |
| 38 | + // HLSL | |
| 39 | + 'numthreads', 'unroll', 'loop', 'branch', 'flatten', 'fastopt', 'allow_uav_condition', 'call', 'forcecase', 'earlydepthstencil', | |
| 40 | + 'maxvertexcount', 'domain', 'partitioning', 'outputtopology', 'outputcontrolpoints', 'patchconstantfunc', 'maxtessfactor', | |
| 41 | + 'instance', 'RootSignature', 'shader', 'WaveSize', 'NumThreads', 'vk', | |
| 42 | + // Slang | |
| 43 | + 'Differentiable', 'ForwardDifferentiable', 'BackwardDifferentiable', 'BackwardDerivative', 'ForwardDerivative', 'PrimalSubstitute', | |
| 44 | + 'PreferRecompute', 'PreferCheckpoint', 'TreatAsDifferentiable', 'mutating', 'nonmutating', 'ForceInline', 'noinline', 'require', | |
| 45 | + 'SpecializationConstant', 'push_constant', 'Payload', 'raypayload', 'AutoPyBindCUDA', 'CudaKernel', 'TorchEntryPoint', | |
| 46 | + 'Format', 'OverloadRank', 'Deprecated', 'KnownBuiltin', 'anyValueSize', 'COM', 'Flags', 'UnscopedEnum', 'ForceUnroll', | |
| 47 | + ]; | |
| 48 | + | |
| 49 | + public function prepare(string $code): void { | |
| 50 | + $tokens = $this->tokens; | |
| 51 | + foreach ($this->slangTokens as $k => $list) { | |
| 52 | + foreach ($list as $w) $tokens[$k][] = $w; | |
| 53 | + } | |
| 54 | + foreach (self::vectorTypes($this->scalars) as $t) $tokens['type'][] = $t; | |
| 55 | + foreach (self::vectorTypes(['int8_t', 'uint8_t']) as $t) $tokens['type'][] = $t; | |
| 56 | + $this->words = $this->buildWords($tokens); | |
| 57 | + } | |
| 58 | +} |
| @@ -0,0 +1,55 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +class Swift extends Language { | |
| 5 | + protected bool $nestedComments = true; | |
| 6 | + protected array $strings = ['"']; | |
| 7 | + protected string $digitSep = '_'; | |
| 8 | + | |
| 9 | + protected array $rules = [ | |
| 10 | + // 生文字列 #"…"#(複数行の#"""…"""#も) | |
| 11 | + ['re' => '(?<rawhash>#+)"""(?s:.*?)"""\k<rawhash>|(?<rawhash2>#+)"(?:[^"\n]|"(?!\k<rawhash2>))*"\k<rawhash2>', 'class' => 'string'], | |
| 12 | + // 複数行の文字列 """…""" | |
| 13 | + ['re' => '"""(?s:.*?)(?:"""|\z)', 'class' => 'string'], | |
| 14 | + // コンパイラ指令(#if, #available, #selector等) | |
| 15 | + ['re' => '#[A-Za-z_]\w*', 'class' => 'preprocessor'], | |
| 16 | + // 属性(@main, @MainActor, @escaping等) | |
| 17 | + ['re' => '@[A-Za-z_]\w*', 'class' => 'attribute'], | |
| 18 | + // 予約語を名前にする `default` | |
| 19 | + ['re' => '`[^`\n]+`', 'class' => null], | |
| 20 | + ]; | |
| 21 | + | |
| 22 | + protected array $tokens = [ | |
| 23 | + 'keyword' => [ | |
| 24 | + // 宣言 | |
| 25 | + 'associatedtype', 'class', 'deinit', 'enum', 'extension', 'fileprivate', 'func', 'import', 'init', 'inout', 'internal', 'let', | |
| 26 | + 'open', 'operator', 'private', 'precedencegroup', 'protocol', 'public', 'rethrows', 'static', 'struct', 'subscript', 'typealias', | |
| 27 | + 'var', 'actor', 'macro', 'package', 'nonisolated', 'isolated', 'consuming', 'borrowing', 'sending', | |
| 28 | + // 文 | |
| 29 | + 'break', 'case', 'catch', 'continue', 'default', 'defer', 'do', 'else', 'fallthrough', 'for', 'guard', 'if', 'in', 'repeat', | |
| 30 | + 'return', 'throw', 'switch', 'where', 'while', | |
| 31 | + // 式・型 | |
| 32 | + 'as', 'await', 'is', 'self', 'Self', 'super', 'throws', 'try', 'async', 'some', 'any', 'Type', 'Protocol', | |
| 33 | + // 文脈依存 | |
| 34 | + 'convenience', 'dynamic', 'didSet', 'final', 'get', 'indirect', 'lazy', 'mutating', 'nonmutating', 'optional', 'override', | |
| 35 | + 'postfix', 'prefix', 'infix', 'required', 'set', 'unowned', 'weak', 'willSet', | |
| 36 | + ], | |
| 37 | + 'constant' => [ | |
| 38 | + 'true', 'false', 'nil', | |
| 39 | + ], | |
| 40 | + 'type' => [ | |
| 41 | + 'Int', 'Int8', 'Int16', 'Int32', 'Int64', 'Int128', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'UInt128', | |
| 42 | + 'Float', 'Float16', 'Float32', 'Float64', 'Float80', 'Double', 'CGFloat', 'Bool', 'String', 'Substring', 'Character', | |
| 43 | + 'Array', 'ContiguousArray', 'Dictionary', 'Set', 'Optional', 'Result', 'Range', 'ClosedRange', 'Void', 'Never', 'Any', 'AnyObject', | |
| 44 | + 'Error', 'Equatable', 'Hashable', 'Comparable', 'Codable', 'Encodable', 'Decodable', 'Identifiable', 'Sendable', 'Sequence', | |
| 45 | + 'Collection', 'IteratorProtocol', 'CustomStringConvertible', 'Numeric', 'BinaryInteger', 'FloatingPoint', 'SIMD2', 'SIMD3', 'SIMD4', | |
| 46 | + 'UnsafePointer', 'UnsafeMutablePointer', 'UnsafeRawPointer', 'UnsafeMutableRawPointer', 'UnsafeBufferPointer', 'OpaquePointer', | |
| 47 | + 'Data', 'Date', 'URL', 'UUID', 'NSObject', 'Task', 'MainActor', 'View', 'some', | |
| 48 | + ], | |
| 49 | + 'builtin' => [ | |
| 50 | + 'print', 'debugPrint', 'dump', 'assert', 'assertionFailure', 'precondition', 'preconditionFailure', 'fatalError', | |
| 51 | + 'min', 'max', 'abs', 'stride', 'zip', 'sequence', 'repeatElement', 'swap', 'type', 'withUnsafePointer', | |
| 52 | + 'withUnsafeMutablePointer', 'withUnsafeBytes', 'withExtendedLifetime', 'MemoryLayout', 'readLine', 'numericCast', | |
| 53 | + ], | |
| 54 | + ]; | |
| 55 | +} |
| @@ -0,0 +1,44 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Std\Lib\Syntax; | |
| 3 | + | |
| 4 | +interface SyntaxInterface { | |
| 5 | + /** | |
| 6 | + * コードを解析する前に呼ばれる(単語表の作成等) | |
| 7 | + * | |
| 8 | + * @param string $code ハイライトするコード全体 | |
| 9 | + * @return void | |
| 10 | + */ | |
| 11 | + public function prepare(string $code): void; | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * トークンの規則を優先順に返す | |
| 15 | + * 各規則:['re' => 正規表現(区切り文字なし), 'class' => 種類, 'bol' => 行頭のみか] | |
| 16 | + * 種類が「ident」の場合、classify()で分類する | |
| 17 | + * | |
| 18 | + * @return array 規則の配列 | |
| 19 | + */ | |
| 20 | + public function getRules(): array; | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * 識別子を分類する | |
| 24 | + * | |
| 25 | + * @param string $word 識別子 | |
| 26 | + * @param array $ctx 文脈(bol, first, prev, prevWord, lineWord, next, nextRaw, nextWord) | |
| 27 | + * @return string|null 種類、色を付けない場合はnull | |
| 28 | + */ | |
| 29 | + public function classify(string $word, array $ctx): ?string; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 入れ子可能な複数行コメントの[開始, 終了](Swift等) | |
| 33 | + * | |
| 34 | + * @return array|null | |
| 35 | + */ | |
| 36 | + public function getNestedComment(): ?array; | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * 次の「<…>」をファイル名として扱うプリプロセッサ指令(include等) | |
| 40 | + * | |
| 41 | + * @return array | |
| 42 | + */ | |
| 43 | + public function getHeaderDirectives(): array; | |
| 44 | +} |
| @@ -262,7 +262,105 @@ | ||
| 262 | 262 | |
| 263 | 263 | $test->assertEquals($expect, $actual); |
| 264 | 264 | }); |
| 265 | - | |
| 265 | + | |
| 266 | + $test->it('インライン数式はMathMLに交換出来るはず', function($test): void { | |
| 267 | + mkfile('長さは$$\lVert \mathbf{v} \rVert = \sqrt{x^2 + y^2}$$です。'); | |
| 268 | + | |
| 269 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 270 | + $body = $md->parse(); | |
| 271 | + | |
| 272 | + $test->assertStringContains('<math alttext=', $body); | |
| 273 | + $test->assertStringContains('<msqrt>', $body); | |
| 274 | + $test->assertStringContains('<mi>𝐯</mi>', $body); | |
| 275 | + $test->assertStringContains('<msup><mi>x</mi><mn>2</mn></msup>', $body); | |
| 276 | + $test->assertStringContains('です。', $body); | |
| 277 | + $test->assertEquals([], $md->getMathErrors()); | |
| 278 | + }); | |
| 279 | + | |
| 280 | + $test->it('LaTeXのバックスラッシュは消されないはず', function($test): void { | |
| 281 | + mkfile('$$\frac{1}{2} \cdot \theta$$の後に\*エスケープ\*'); | |
| 282 | + | |
| 283 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 284 | + $body = $md->parse(); | |
| 285 | + | |
| 286 | + $test->assertStringContains('<mfrac><mrow><mn>1</mn></mrow><mrow><mn>2</mn></mrow></mfrac>', $body); | |
| 287 | + $test->assertStringContains('<mo>⋅</mo><mi>θ</mi>', $body); | |
| 288 | + $test->assertStringContains('の後に*エスケープ*', $body); | |
| 289 | + }); | |
| 290 | + | |
| 291 | + $test->it('数式ブロックはブロック表示のMathMLに交換出来るはず', function($test): void { | |
| 292 | + mkfile("前の段落\n\n$$\n\\mathbf{M} =\n\\begin{bmatrix} a & b \\\\\nc & d \\end{bmatrix}\n$$\n\n\$\$x^2\$\$\n\n後の段落"); | |
| 293 | + | |
| 294 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 295 | + $body = $md->parse(); | |
| 296 | + | |
| 297 | + $test->assertEquals(2, substr_count($body, '<div class="math-display"><math display="block"')); | |
| 298 | + $test->assertEquals(2, substr_count($body, '<mtr>')); | |
| 299 | + $test->assertStringContains('<mo fence="true" stretchy="true" form="prefix">[</mo>', $body); | |
| 300 | + $test->assertStringContains('前の段落', $body); | |
| 301 | + $test->assertStringContains('後の段落', $body); | |
| 302 | + $test->assertEquals([], $md->getMathErrors()); | |
| 303 | + }); | |
| 304 | + | |
| 305 | + $test->it('未対応のLaTeXコマンドはエラーとして報告されるはず', function($test): void { | |
| 306 | + mkfile('$$\kerokero{x}$$'); | |
| 307 | + | |
| 308 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 309 | + $body = $md->parse(); | |
| 310 | + | |
| 311 | + $test->assertStringContains('<merror>', $body); | |
| 312 | + $test->assertEquals(['\kerokero'], $md->getMathErrors()); | |
| 313 | + }); | |
| 314 | + | |
| 315 | + $test->it('インラインコードの中身は変換されないはず', function($test): void { | |
| 316 | + mkfile('`a * b * c`と`my_var_name`と`\n`と`$$x$$`'); | |
| 317 | + | |
| 318 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 319 | + $body = $md->parse(); | |
| 320 | + | |
| 321 | + $test->assertStringContains('<code>a * b * c</code>', $body); | |
| 322 | + $test->assertStringContains('<code>my_var_name</code>', $body); | |
| 323 | + $test->assertStringContains('<code>\n</code>', $body); | |
| 324 | + $test->assertStringContains('<code>$$x$$</code>', $body); | |
| 325 | + }); | |
| 326 | + | |
| 327 | + $test->it('コードブロックの行末のバックスラッシュは残るはず', function($test): void { | |
| 328 | + mkfile("```txt\n /\\\n / \\\n```"); | |
| 329 | + | |
| 330 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 331 | + $body = $md->parse(); | |
| 332 | + | |
| 333 | + $test->assertStringContains(" /\\\n / \\</code></pre>", $body); | |
| 334 | + }); | |
| 335 | + | |
| 336 | + $test->it('コードタブはラジオボタンのタブに交換出来るはず', function($test): void { | |
| 337 | + mkfile(":::tabs\n```slang\nfloat3 v;\n```\n```glsl\nvec3 v;\n```\n```glsl GLSL 4.50\nvec3 w;\n```\n:::\n\n後の段落"); | |
| 338 | + | |
| 339 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 340 | + $body = $md->parse(); | |
| 341 | + | |
| 342 | + $test->assertStringContains('<div class="code-tabs">', $body); | |
| 343 | + $test->assertEquals(3, substr_count($body, '<input type="radio"')); | |
| 344 | + $test->assertEquals(1, substr_count($body, 'checked="checked"')); | |
| 345 | + $test->assertStringContains('>Slang</label>', $body); | |
| 346 | + $test->assertStringContains('>GLSL</label>', $body); | |
| 347 | + $test->assertStringContains('>GLSL 4.50</label>', $body); | |
| 348 | + $test->assertStringContains('<code class="language-slang hl"><span class="hl-type">float3</span> v;</code>', $body); | |
| 349 | + $test->assertStringContains('<code class="language-glsl hl"><span class="hl-type">vec3</span> v;</code>', $body); | |
| 350 | + $test->assertStringContains('後の段落', $body); | |
| 351 | + }); | |
| 352 | + | |
| 353 | + $test->it('対応している言語のコードブロックはハイライトされるはず', function($test): void { | |
| 354 | + mkfile("```c\nint x = 42; // <b>\n```\n\n```sh\necho \"<b>\"\n```"); | |
| 355 | + | |
| 356 | + $md = new Markdown('dietrannynigger', '/blog/'); | |
| 357 | + $body = $md->parse(); | |
| 358 | + | |
| 359 | + $test->assertStringContains('<code class="language-c hl"><span class="hl-keyword">int</span> x = <span class="hl-number">42</span>; <span class="hl-comment">// <b></span></code>', $body); | |
| 360 | + // 未対応の言語はエスケープだけ | |
| 361 | + $test->assertStringContains('<code class="language-sh">echo "<b>"</code>', $body); | |
| 362 | + }); | |
| 363 | + | |
| 266 | 364 | rmfile(); |
| 267 | 365 | }); |
| 268 | 366 |
| @@ -0,0 +1,118 @@ | ||
| 1 | +<?php | |
| 2 | +/************************************************************* | |
| 3 | +# 076 License | |
| 4 | + | |
| 5 | +Copyright (c) テクニカル諏訪子 | |
| 6 | + | |
| 7 | +Permission is hereby granted to any person obtaining a copy of the software | |
| 8 | +Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, | |
| 9 | +sublicense, and/or sell copies of the Software, subject to the following conditions: | |
| 10 | + | |
| 11 | + 1. **Origin Attribution**: | |
| 12 | + - You must not misrepresent the origin of the Software; you must not claim | |
| 13 | + you created the original Software. | |
| 14 | + - If the Software is used in a product, you must either: | |
| 15 | + a. Provide clear attribution in the product's documentation, user interface, | |
| 16 | + or other visible areas, **OR** | |
| 17 | + b. Pay the original developers a fee they specify in writing. | |
| 18 | + 2. **Usage Restriction**: | |
| 19 | + - The Software, or any derivative works, dependencies, or libraries | |
| 20 | + incorporating it, must not be used for censorship or to suppress freedom of | |
| 21 | + speech, expression, or creativity. Prohibited uses include, but are not | |
| 22 | + limited to: | |
| 23 | + - Censorship of so-called "hate speech", visuals, non-mainstream opinions, | |
| 24 | + ideas, or objective reality. | |
| 25 | + - Tools or systems designed to restrict access to information or | |
| 26 | + artistic works. | |
| 27 | + 3. **Notice Preservation**: | |
| 28 | + - This license and the above copyright notice must remain intact in all copies | |
| 29 | + of the source code. | |
| 30 | + | |
| 31 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
| 32 | +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A | |
| 33 | +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
| 34 | +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF | |
| 35 | +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE | |
| 36 | +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 37 | + */ | |
| 38 | +namespace Std\Test; | |
| 39 | + | |
| 40 | +require_once __DIR__.'/../../../autoload.php'; | |
| 41 | + | |
| 42 | +use Std\Lib\Tester; | |
| 43 | +use Std\Lib\Syntax; | |
| 44 | + | |
| 45 | +$test = new Tester([ | |
| 46 | + 'colorOutput' => true, | |
| 47 | + 'verboseOutput' => true | |
| 48 | +]); | |
| 49 | + | |
| 50 | +// タグを外してデコードすると元のコードに戻るか | |
| 51 | +function syntaxRoundtrip(string $lang, string $code): bool { | |
| 52 | + $html = (new Syntax($lang))->colorize($code); | |
| 53 | + return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8') === $code; | |
| 54 | +} | |
| 55 | + | |
| 56 | +$samples = [ | |
| 57 | + 'c' => "#include <stdio.h>\n/* a */ int main(void) { printf(\"%d\\n\", 0x1F); return 0; } // b", | |
| 58 | + 'cpp' => "auto s = R\"(raw \"x\")\"; [[nodiscard]] std::vector<int> v{1'000};", | |
| 59 | + 'glsl' => "layout(location = 0) out vec4 fragColor;\nvoid main() { fragColor = texture(tex, gl_FragCoord.xy); }", | |
| 60 | + 'go' => "func main() { s := `raw\nstring`; fmt.Println(len(s), nil) }", | |
| 61 | + 'hlsl' => "[numthreads(8, 8, 1)]\nvoid CSMain(uint3 id : SV_DispatchThreadID) { float4x4 m; }", | |
| 62 | + 'slang' => "[shader(\"compute\")]\nvoid main<T : IFloat>(uniform Ptr<T> p) { let x = detach(1.0); }", | |
| 63 | + 'swift' => "@main struct A { /* /* nested */ */ let s = #\"raw\"#; #if DEBUG\n print(\"\\(s)\") }", | |
| 64 | + 'objc' => "#import <Foundation/Foundation.h>\n@interface Foo : NSObject\n@end\nNSLog(@\"%@\", [obj valueForKey:@\"k\"]);", | |
| 65 | + 'csharp' => "var p = @\"C:\\dir\"; var m = $\"{p}\"; var r = \"\"\"\nraw\n\"\"\";", | |
| 66 | + 'asm' => "section .text\n_start: mov rax, 1 ; nasm\n mov x8, #64\n li a7, 93 # riscv", | |
| 67 | + 'asm-x8664' => "%define N 1\nrep movsb\nmovl \$1, %eax", | |
| 68 | + 'asm-aarch64' => "b.eq done\nadd v0.4s, v1.4s, v2.4s // neon", | |
| 69 | + 'asm-aarch32' => "addeq r0, r0, #1 @ arm", | |
| 70 | + 'asm-riscv' => "fcvt.w.s a0, fa0 # x", | |
| 71 | + 'asm-mips' => "addiu \$sp, \$sp, -8 # x", | |
| 72 | + 'asm-power' => "addi r3, r3, 1 # x", | |
| 73 | + 'asm-z80' => "ex af,af' ; x\nld a,'A'", | |
| 74 | + 'asm-6502' => "lda #\$FF ; x", | |
| 75 | + 'asm-68000' => "* comment\nmove.l #1,d0", | |
| 76 | +]; | |
| 77 | + | |
| 78 | +$test->describe('シンタックスハイライト', function($test) use ($samples): void { | |
| 79 | + $test->it('全ての言語で、タグを外すと元のコードに戻るはず', function($test) use ($samples): void { | |
| 80 | + foreach ($samples as $lang => $code) { | |
| 81 | + $test->assertTrue(syntaxRoundtrip($lang, $code), "{$lang}の往復に失敗"); | |
| 82 | + } | |
| 83 | + }); | |
| 84 | + | |
| 85 | + $test->it('HTMLをエスケープするはず', function($test): void { | |
| 86 | + $html = (new Syntax('c'))->colorize('"</code><script>" // <b>'); | |
| 87 | + $test->assertFalse(str_contains($html, '<script>')); | |
| 88 | + $test->assertStringContains('</code>', $html); | |
| 89 | + }); | |
| 90 | + | |
| 91 | + $test->it('トークンを正しく分類するはず', function($test): void { | |
| 92 | + $html = (new Syntax('c'))->colorize('int x = 42; // c'); | |
| 93 | + $test->assertStringContains('<span class="hl-keyword">int</span>', $html); | |
| 94 | + $test->assertStringContains('<span class="hl-number">42</span>', $html); | |
| 95 | + $test->assertStringContains('<span class="hl-comment">// c</span>', $html); | |
| 96 | + | |
| 97 | + $html = (new Syntax('hlsl'))->colorize('float4 p : SV_Position;'); | |
| 98 | + $test->assertStringContains('<span class="hl-variable">SV_Position</span>', $html); | |
| 99 | + | |
| 100 | + $html = (new Syntax('asm'))->colorize("loop:\n mov x8, #64"); | |
| 101 | + $test->assertStringContains('<span class="hl-label">loop</span>', $html); | |
| 102 | + $test->assertStringContains('<span class="hl-number">#64</span>', $html); | |
| 103 | + }); | |
| 104 | + | |
| 105 | + $test->it('文字列の中の数字やキーワードに色を付けないはず', function($test): void { | |
| 106 | + $html = (new Syntax('c'))->colorize('"int 42"'); | |
| 107 | + $test->assertEquals('<span class="hl-string">"int 42"</span>', $html); | |
| 108 | + }); | |
| 109 | + | |
| 110 | + $test->it('別名を解決するはず', function($test): void { | |
| 111 | + $test->assertEquals('cpp', Syntax::resolve('c++')); | |
| 112 | + $test->assertEquals('csharp', Syntax::resolve('C#')); | |
| 113 | + $test->assertEquals('asm-aarch64', Syntax::resolve('arm64')); | |
| 114 | + $test->assertFalse(Syntax::isSupported('sh')); | |
| 115 | + }); | |
| 116 | +}); | |
| 117 | + | |
| 118 | +$test->printSummary(); |
変更
util.php
| @@ -36,6 +36,7 @@ | ||
| 36 | 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | 37 | */ |
| 38 | 38 | if (!CURL_ENABLED) define('ACTIVITYPUB_ENABLED', false); |
| 39 | +foreach (['MAILER_ENABLED', 'OPENPROVIDER_ENABLED'] as $c) if (!defined($c)) define($c, false); | |
| 39 | 40 | |
| 40 | 41 | enum LogType { |
| 41 | 42 | case ActivityPub; |
| @@ -0,0 +1,7 @@ | ||
| 1 | +{@ include(common/header) @} | |
| 2 | + <h1 class="paragraph">test</h1> | |
| 3 | + <p class="paragraph"> | |
| 4 | + <pre><code>{{{ $cCode }}}</code></pre> | |
| 5 | + <pre><code>{{{ $asmCode }}}</code></pre> | |
| 6 | + </p> | |
| 7 | +{@ include(common/footer) @} |