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+
122 .fraction {
223 display: inline-block;
324 vertical-align: middle;
@@ -1,6 +1,6 @@
11 pre {
22 background: #232023;
3- color: fcfcfc;
3+ color: #fcfcfc;
44 padding: 1rem;
55 border-radius: 6px;
66 overflow: auto;
@@ -9,11 +9,28 @@
99 overflow-x: auto;
1010 }
1111
12-/*pre > code {
13- color: #fcfcfc;
14-}*/
12+pre > code.hl {
13+ color: #fcfcfc;
14+}
1515
1616 code {
1717 color: #b421f8;
1818 line-height: 1.45;
1919 }
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 @@
6060 $routes = [
6161 Route::add('GET', 'robots.txt', Home::class.'@robots'),
6262 Route::add('GET', 'blog/{page}', Home::class.'@article'),
63+ Route::add('GET', 'test', Page::class.'@test'),
6364 Route::add('GET', 'about', Page::class.'@about'),
6465 Route::add('GET', 'monero', Page::class.'@monero'),
6566 Route::add('GET', 'secret', Page::class.'@secret'),
@@ -179,6 +179,7 @@
179179 $tmpl->addCss('news-article');
180180 $tmpl->addCss('search');
181181 $tmpl->addCss('blogtype');
182+ $tmpl->addCss('code');
182183 $tmpl->render('article');
183184 exit();
184185
@@ -42,8 +42,42 @@
4242 use Std\Lib\Template;
4343 use Roles;
4444
45+use Std\Lib\Syntax; // TEST
46+
4547 class Page {
4648 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+ }
4781
4882 public function about(array $params): void {
4983 try {
@@ -121,7 +121,7 @@
121121 $ip = $_SERVER['REMOTE_ADDR'];
122122 $res = $this->purgeOldTokens($userData);
123123 if (!$res->isSuccess) return Result::Error($res->message);
124- $userData = $res->data;
124+ $userData = $res->data['user'];
125125 $token = bin2hex(random_bytes(64));
126126 $expire = time() + $this->tokenDuration;
127127 $newToken = [
@@ -164,7 +164,7 @@
164164 if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = [];
165165 $res = $this->purgeOldTokens($userData);
166166 if (!$res->isSuccess) return null;
167- $userData = $res->data;
167+ $userData = $res->data['user'];
168168 }
169169
170170 public function logout(?string $token = null): Result {
@@ -363,9 +363,9 @@
363363 if (!AUTH_ENABLED) return Result::Error('エラー:認証システムは無効です。');
364364 // 有効期限切れたトークンの削除
365365 $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+ }));
369369
370370 $path = $this->dataDir.$userData->id.'.'.$userData->username.'.json';
371371 $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -375,7 +375,7 @@
375375 return Result::Error('エラー:ユーザーデータの保存に失敗。');
376376 }
377377
378- return Result::Success('', $out);
378+ return Result::Success('', ['user' => $out]);
379379 }
380380
381381 private function getUserId(?string $username = null, ?string $token = null): int {
@@ -562,4 +562,4 @@
562562
563563 return Result::Success();
564564 }
565-};
565+};
@@ -97,6 +97,10 @@
9797 $this->fullInfo->type = $this->type;
9898 $this->fullInfo->size = $this->size;
9999 $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+ }
100104 }
101105
102106 ///////////////
@@ -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>&amp;</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 @@
4646 private array $codeBlockContent = [];
4747 private const METADATA_LINE = "----";
4848 private array $algebraicPlaceholder = [];
49+ private array $codePlaceholder = [];
50+ private array $mathErrors = [];
51+ private ?Latex $latex = null;
52+ private static int $tabGroupCount = 0;
4953
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+
5063 public function __construct(string $path, string $section, ?string $lang = null) {
5164 $this->html = [];
5265 if ($lang) $this->path = ROOT.$section.$lang.'/'.$path.'.md';
@@ -115,14 +128,44 @@
115128 $tableHeaders = [];
116129 $tableRows = [];
117130 $inTable = false;
118- $i = 0;
131+ $inMathBlock = false;
132+ $mathLines = [];
133+ $inTabs = false;
134+ $tabItems = [];
135+ $codeBlockLabel = '';
119136 $headerCount = 1;
137+ $this->mathErrors = [];
138+ $i = 0;
120139
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+
121166 foreach ($lines as $line) {
122167 $i++;
123- $line = rtrim($line, "\r\n");
124- $hasBR = str_ends_with($line, '\\');
125- $line = rtrim($line, "\\");
168+ $rawLine = rtrim($line, "\r\n");
126169
127170 // コメント
128171 // if (str_starts_with($line, '//')) {
@@ -132,30 +175,82 @@
132175 // continue;
133176 // }
134177
135- // コードブロック
136- if (preg_match('/^```(\w*)$/', $line, $matches)) {
178+ // コードブロック(```言語 ラベル)
179+ if (preg_match('/^```([\w\+\-#\.]*)(?:\s+(.*\S))?\s*$/u', $rawLine, $matches)) {
137180 if (!$this->inCodeBlock) {
138- if (!empty($currentParagraph)) {
139- $this->html[] = " <p>\n ".implode("", $currentParagraph)."\n </p>";
140- $currentParagraph = [];
141- }
181+ $flush();
142182 $this->inCodeBlock = true;
143183 $this->codeBlockLanguage = $matches[1];
184+ $codeBlockLabel = $matches[2] ?? '';
144185 continue;
145186 } 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+ }
147196 $this->inCodeBlock = false;
148197 $this->codeBlockLanguage = '';
149198 $this->codeBlockContent = [];
199+ $codeBlockLabel = '';
150200 continue;
151201 }
152202 }
153203
154204 if ($this->inCodeBlock) {
155- $this->codeBlockContent[] = $line;
205+ $this->codeBlockContent[] = $rawLine;
156206 continue;
157207 }
158208
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+
159254 // テーブルの処理
160255 if (preg_match('/^\|(.+)\|$/', $line)) {
161256 if (!empty($currentParagraph)) {
@@ -289,15 +384,27 @@
289384 }
290385 }
291386
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();
297395
298396 return implode("\n", $this->html);
299397 }
300398
399+ /**
400+ * 直前のparse()で見つかった、変換出来なかったLaTeXコマンド
401+ *
402+ * @return array エラーの配列
403+ */
404+ public function getMathErrors(): array {
405+ return $this->mathErrors;
406+ }
407+
301408 // 機能性メソッド
302409
303410 /**
@@ -308,8 +415,18 @@
308415 */
309416 private function parseInline(string $text): string {
310417 $this->algebraicPlaceholder = [];
418+ $this->codePlaceholder = [];
311419 $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
312420
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+
313430 $patterns = [
314431 // 数式
315432 '/(?<!\\\\)\$\$([^$]+)\$\$/u' => function($matches): string {
@@ -343,26 +460,32 @@
343460 '/(?<!\\\\)\[(.+?)\]\((.+?)\)/u' => '<a href="$2">$1</a>',
344461 // 振り仮名
345462 '/(?<!\\\\)&lt;(.+?)&gt;\((.+?)\)/u' => '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>',
346- // インラインコード
347- '/(?<!\\\\)`(.+?)`/u' => '<code>$1</code>',
348463 ];
349464
350465 foreach ($patterns as $pattern => $replacement) {
351466 if (is_callable($replacement)) {
352467 $text = preg_replace_callback($pattern, $replacement, $text);
353468 } else {
469+ // kys([$pattern, $replacement, $text]);
354470 $result = preg_replace($pattern, $replacement, $text);
471+ // kys($result);
355472 if ($result === null) continue;
473+ // kys($text);
356474 $text = $result;
357475 }
358476 }
359477
360- // プレースホルダーの数式に交換
478+ // エスケープ文字の\を消す(数式とコードを戻す前に)
479+ $text = preg_replace('/\\\\(.)/u', '$1', $text);
480+
481+ // プレースホルダーを数式とコードに交換
361482 foreach ($this->algebraicPlaceholder as $placeholder => $expr) {
362483 $text = str_replace($placeholder, $this->parseAlgebraic($expr), $text);
363484 }
364485
365- $text = preg_replace('/\\\\(.)/u', '$1', $text);
486+ foreach ($this->codePlaceholder as $placeholder => $code) {
487+ $text = str_replace($placeholder, $code, $text);
488+ }
366489
367490 return $text;
368491 }
@@ -371,92 +494,56 @@
371494 * 数式記法をパースする
372495 *
373496 * @param string $expression パースする数式
374- * @return string MathJax用にフォーマットされた数式
497+ * @param bool $display ブロック表示ならtrue
498+ * @return string MathMLとしてレンダリングされた数式
375499 */
376- private function parseAlgebraic(string $expression): string {
377- // HTMLエスケープ
500+ private function parseAlgebraic(string $expression, bool $display = false): string {
501+ // HTMLエスケープを戻す(Latexクラスが自分でエスケープする)
378502 $expression = htmlspecialchars_decode($expression, ENT_QUOTES);
379503
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;
395507
396- // ギリシャ文字を置換
397- foreach ($greekLetters as $text => $symbol) {
398- $expression = preg_replace("/\b$text\b/", $symbol, $expression);
399- }
508+ return $mathml;
509+ }
400510
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+ }
406520
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 '';
415529
416- foreach ($superscripts as $char) $converted .= $supMap[$char] ?? $char;
530+ $group = 'code-tabs-'.(++self::$tabGroupCount);
531+ $inputs = '';
532+ $panels = '';
417533
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']);
420540
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+ }
425545
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>";
460547 }
461548
462549 /**
@@ -477,8 +564,11 @@
477564 foreach ($items as $item) {
478565 $level = isset($item['level']) ? $item['level'] : $currentLevel;
479566
480- while ($currentLevel > $level)
567+ // ネストから戻る時はレベルを下げる(下げないと無限ループになる)
568+ while ($currentLevel > $level) {
481569 $html .= str_repeat(' ', $currentLevel)."</".array_pop($listStack).">\n";
570+ $currentLevel--;
571+ }
482572
483573 while ($currentLevel < $level) {
484574 $currentLevel++;
@@ -517,16 +607,29 @@
517607 * @return string HTMLのコードブロック
518608 */
519609 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);
522611 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>";
528612 }
529613
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+
530633 private function createBlockquote(array $content): string {
531634 return " <p>\n <blockquote>\n ".implode("<br />\n ", $content)."\n </blockquote>\n </p>";
532635 }
@@ -561,127 +664,9 @@
561664 $html .= " </tr>\n";
562665 }
563666 $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'] = [['/&lt;\/?[a-zA-Z0-9\-]+/', 'hl-tag']];
603- $rules['attr'] = [['/\s[a-zA-Z0-9\-]+(?==)/', 'hl-attr']];
604- $rules['value'] = [['/(?<==)"(?:\\.|[^"\\\n])*"(?=[>\s])/', 'hl-value']];
605667 }
606668
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>";
685670 return $html;
686671 }
687672 }
@@ -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 @@
262262
263263 $test->assertEquals($expect, $actual);
264264 });
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">// &lt;b&gt;</span></code>', $body);
360+ // 未対応の言語はエスケープだけ
361+ $test->assertStringContains('<code class="language-sh">echo &quot;&lt;b&gt;&quot;</code>', $body);
362+ });
363+
266364 rmfile();
267365 });
268366
@@ -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('&lt;/code&gt;', $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">&quot;int 42&quot;</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 @@
3636 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3737 */
3838 if (!CURL_ENABLED) define('ACTIVITYPUB_ENABLED', false);
39+foreach (['MAILER_ENABLED', 'OPENPROVIDER_ENABLED'] as $c) if (!defined($c)) define($c, false);
3940
4041 enum LogType {
4142 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) @}