| 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\Markdown; |
| 44 | |
| 45 | $test = new Tester([ |
| 46 | 'colorOutput' => true, |
| 47 | 'verboseOutput' => true |
| 48 | ]); |
| 49 | |
| 50 | function mkfile(string $body): void { |
| 51 | $post = fopen(ROOT."/blog/dietrannynigger.md", "w"); |
| 52 | fwrite($post, "title: 【】\n"); |
| 53 | fwrite($post, "uuid: ".uuid()."\n"); |
| 54 | fwrite($post, "author: 諏訪子\n"); |
| 55 | fwrite($post, "date: ".date("Y-m-d H:i:s")."\n"); |
| 56 | fwrite($post, "thumbnail: \n"); |
| 57 | fwrite($post, "thumborient: center\n"); |
| 58 | fwrite($post, "category: \n"); |
| 59 | fwrite($post, "----\n"); |
| 60 | fwrite($post, $body); |
| 61 | fclose($post); |
| 62 | } |
| 63 | |
| 64 | function rmfile(): void { |
| 65 | if (is_file(ROOT."/blog/dietrannynigger.md")) unlink(ROOT."/blog/dietrannynigger.md"); |
| 66 | } |
| 67 | |
| 68 | class Constants { |
| 69 | public const START = "<p> "; |
| 70 | public const END = " </p>"; |
| 71 | } |
| 72 | |
| 73 | $test->describe('マークダウン', function($test): void { |
| 74 | // サンプルの投稿が無いと、Markdownは404にリダイレクトしてexitするので、テスト全体が止まる。 |
| 75 | // 一時的な投稿もblog/に書くので、サイト(本番)では動かさない |
| 76 | if (!is_file(ROOT.'/blog/feature-test.md')) { |
| 77 | $test->skipAll('サンプルの投稿(blog/feature-test.md)が無い'); |
| 78 | } |
| 79 | |
| 80 | $test->it('メタデータをパーシング出来るはず', function($test): void { |
| 81 | $md = new Markdown('feature-test', '/blog/'); |
| 82 | $test->assertNotNull($md); |
| 83 | |
| 84 | $meta = $md->getMetadata(); |
| 85 | $test->assertNotNull($meta); |
| 86 | |
| 87 | $expect = new \stdClass; |
| 88 | $expect->title = '【マークダウン】関数'; |
| 89 | $expect->uuid = 'a8c04518-4181-4ec6-9ef0-3f88f23b84b6'; |
| 90 | $expect->author = '諏訪子'; |
| 91 | $expect->date = '2025-11-07 20:33:42'; |
| 92 | $expect->thumbnail = 'o_53803618dc1691.28179609-orig.jpg'; |
| 93 | $expect->thumborient = 'center'; |
| 94 | $expect->category = ['test', 'おちんちん', 'でっかいおっぱい']; |
| 95 | $expect->css = ['code', 'blockquote', 'table', 'blink']; |
| 96 | |
| 97 | $test->assertEquals($expect, $meta); |
| 98 | }); |
| 99 | |
| 100 | $test->it('横線はHTMLに交換出来るはず', function($test): void { |
| 101 | mkfile('----'); |
| 102 | |
| 103 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 104 | $test->assertNotNull($md); |
| 105 | |
| 106 | $body = $md->parse(); |
| 107 | $test->assertNotNull($body); |
| 108 | |
| 109 | $expect = "<hr />"; |
| 110 | $actual = str_replace("\n", '', trim($body)); |
| 111 | |
| 112 | $test->assertEquals($expect, $actual); |
| 113 | }); |
| 114 | |
| 115 | $test->it('ヘッダーはHTMLに交換出来るはず', function($test): void { |
| 116 | mkfile('# ケロケロ'); |
| 117 | |
| 118 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 119 | $test->assertNotNull($md); |
| 120 | |
| 121 | $body = $md->parse(); |
| 122 | $test->assertNotNull($body); |
| 123 | |
| 124 | $expect = "<h1 id=\"section-1\">ケロケロ</h1>"; |
| 125 | $actual = trim($body); |
| 126 | |
| 127 | $test->assertEquals($expect, $actual); |
| 128 | |
| 129 | // --------------- |
| 130 | |
| 131 | mkfile('## ケロケロ'); |
| 132 | |
| 133 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 134 | $test->assertNotNull($md); |
| 135 | |
| 136 | $body = $md->parse(); |
| 137 | $test->assertNotNull($body); |
| 138 | |
| 139 | $expect = "<h2 id=\"section-1\">ケロケロ</h2>"; |
| 140 | $actual = trim($body); |
| 141 | |
| 142 | $test->assertEquals($expect, $actual); |
| 143 | |
| 144 | // --------------- |
| 145 | |
| 146 | mkfile('### ケロケロ'); |
| 147 | |
| 148 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 149 | $test->assertNotNull($md); |
| 150 | |
| 151 | $body = $md->parse(); |
| 152 | $test->assertNotNull($body); |
| 153 | |
| 154 | $expect = "<h3 id=\"section-1\">ケロケロ</h3>"; |
| 155 | $actual = trim($body); |
| 156 | |
| 157 | $test->assertEquals($expect, $actual); |
| 158 | |
| 159 | // --------------- |
| 160 | |
| 161 | mkfile('#### ケロケロ'); |
| 162 | |
| 163 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 164 | $test->assertNotNull($md); |
| 165 | |
| 166 | $body = $md->parse(); |
| 167 | $test->assertNotNull($body); |
| 168 | |
| 169 | $expect = "<h4 id=\"section-1\">ケロケロ</h4>"; |
| 170 | $actual = trim($body); |
| 171 | |
| 172 | $test->assertEquals($expect, $actual); |
| 173 | |
| 174 | // --------------- |
| 175 | |
| 176 | mkfile('##### ケロケロ'); |
| 177 | |
| 178 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 179 | $test->assertNotNull($md); |
| 180 | |
| 181 | $body = $md->parse(); |
| 182 | $test->assertNotNull($body); |
| 183 | |
| 184 | $expect = "<h5 id=\"section-1\">ケロケロ</h5>"; |
| 185 | $actual = trim($body); |
| 186 | |
| 187 | $test->assertEquals($expect, $actual); |
| 188 | |
| 189 | // --------------- |
| 190 | |
| 191 | mkfile('###### ケロケロ'); |
| 192 | |
| 193 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 194 | $test->assertNotNull($md); |
| 195 | |
| 196 | $body = $md->parse(); |
| 197 | $test->assertNotNull($body); |
| 198 | |
| 199 | $expect = "<h6 id=\"section-1\">ケロケロ</h6>"; |
| 200 | $actual = trim($body); |
| 201 | |
| 202 | $test->assertEquals($expect, $actual); |
| 203 | }); |
| 204 | |
| 205 | $test->it('「//」で始まる行は無視されるはず', function($test): void { |
| 206 | mkfile("前の行\n// 秘密のメモ\n // 字下げしたメモ\n後の行"); |
| 207 | |
| 208 | $body = (new Markdown('dietrannynigger', '/blog/'))->parse(); |
| 209 | |
| 210 | $test->assertFalse(str_contains($body, '秘密のメモ')); |
| 211 | $test->assertFalse(str_contains($body, '字下げしたメモ')); |
| 212 | // 無い物として扱うので、段落は分かれない |
| 213 | $test->assertEquals(1, substr_count($body, '<p>')); |
| 214 | $test->assertStringContains('前の行後の行', $body); |
| 215 | }); |
| 216 | |
| 217 | $test->it('「/*」から「*/」までは無視されるはず', function($test): void { |
| 218 | mkfile("# 見出し\n/* 1行目\n\n# コメントの中の見出し\n最後の行 */\n本文\n/* 1行だけ */\n/* 後ろに文字 */ 残る\n\n- リスト"); |
| 219 | |
| 220 | $body = (new Markdown('dietrannynigger', '/blog/'))->parse(); |
| 221 | |
| 222 | $test->assertFalse(str_contains($body, '1行目')); |
| 223 | $test->assertFalse(str_contains($body, 'コメントの中の見出し')); |
| 224 | $test->assertFalse(str_contains($body, '最後の行')); |
| 225 | $test->assertFalse(str_contains($body, '1行だけ')); |
| 226 | $test->assertFalse(str_contains($body, '後ろに文字')); |
| 227 | $test->assertStringContains('本文', $body); |
| 228 | $test->assertStringContains('残る', $body); |
| 229 | $test->assertStringContains('<li>リスト</li>', $body); |
| 230 | $test->assertEquals(1, substr_count($body, '<h1')); |
| 231 | }); |
| 232 | |
| 233 | $test->it('閉じていない「/*」は最後までコメントのはず', function($test): void { |
| 234 | mkfile("見える\n/* ここから\n全部\n```c\nint x;\n```"); |
| 235 | |
| 236 | $body = (new Markdown('dietrannynigger', '/blog/'))->parse(); |
| 237 | |
| 238 | $test->assertStringContains('見える', $body); |
| 239 | $test->assertFalse(str_contains($body, '全部')); |
| 240 | $test->assertFalse(str_contains($body, '<pre>')); |
| 241 | }); |
| 242 | |
| 243 | $test->it('コードブロックの中のコメントは表示されるはず', function($test): void { |
| 244 | mkfile("```c\n// Cのコメント\n/* ブロック */\nint x;\n```\n\n```txt\n/* 開いたまま\n```\n\n後の段落"); |
| 245 | |
| 246 | $body = (new Markdown('dietrannynigger', '/blog/'))->parse(); |
| 247 | |
| 248 | $test->assertStringContains('<span class="hl-comment">// Cのコメント</span>', $body); |
| 249 | $test->assertStringContains('<span class="hl-comment">/* ブロック */</span>', $body); |
| 250 | $test->assertStringContains('/* 開いたまま', $body); |
| 251 | // コードブロックの中の「/*」はコメントを始めない |
| 252 | $test->assertStringContains('後の段落', $body); |
| 253 | }); |
| 254 | |
| 255 | $test->it('コメントはプレビュー用にも取り除けるはず', function($test): void { |
| 256 | $test->assertEquals("a\nb\n```\n// c\n```", Markdown::stripComments("a\r\n// x\r\n/* y\r\nz */\r\nb\r\n```\r\n// c\r\n```")); |
| 257 | }); |
| 258 | |
| 259 | $test->it('URLの「//」や行の途中の「/*」はコメントではないはず', function($test): void { |
| 260 | mkfile("https://076.moe/ の // と /* は行の途中\n次の行"); |
| 261 | |
| 262 | $body = (new Markdown('dietrannynigger', '/blog/'))->parse(); |
| 263 | |
| 264 | $test->assertStringContains('https://076.moe/ の // と /* は行の途中', $body); |
| 265 | $test->assertStringContains('次の行', $body); |
| 266 | }); |
| 267 | |
| 268 | $test->it('画像、音楽、動画はHTMLに交換出来るはず', function($test): void { |
| 269 | mkfile("\n |
| 270 | \n |
| 271 | \n |
| 272 | $[audio/ogg](https://ass.technicalsuwako.moe/砕月着信音.ogg)\n |
| 273 | #[video/mp4](https://ass.technicalsuwako.moe/フリーズ.mp4)"); |
| 274 | |
| 275 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 276 | $test->assertNotNull($md); |
| 277 | |
| 278 | $body = $md->parse(); |
| 279 | $test->assertNotNull($body); |
| 280 | |
| 281 | $expect = '<p><img style="width: 100%; " src="https://ass.technicalsuwako.moe/banner.png" alt="" /> </p> ' |
| 282 | . '<p><img style="width: 100%; " src="https://ass.technicalsuwako.moe/banner.png" alt="ケロケロ" /> </p> ' |
| 283 | . '<p><img style="width: 100%; max-width: 200px;" src="https://ass.technicalsuwako.moe/banner.png" alt="ケロケロ" /> </p> ' |
| 284 | . '<p><audio controls><source src="https://ass.technicalsuwako.moe/砕月着信音.ogg" type="audio/ogg" /></audio> </p> ' |
| 285 | . '<p><video style="max-width: 100%;" controls><source src="https://ass.technicalsuwako.moe/フリーズ.mp4" type="video/mp4" /></video> </p>'; |
| 286 | $actual = str_replace("\n", '', str_replace(' ', '', trim($body))); |
| 287 | |
| 288 | $test->assertEquals($expect, $actual); |
| 289 | }); |
| 290 | |
| 291 | $test->it('基本的なマークダウンはHTMLに交換出来るはず', function($test): void { |
| 292 | mkfile("**太文字**\n |
| 293 | *イタリック*\n |
| 294 | _下線_\n |
| 295 | ~取消線~\n |
| 296 | !:(5)明滅文字:!\n |
| 297 | ^(72)フォントサイズ^\n |
| 298 | %(ff00ff)フォントカラー%"); |
| 299 | |
| 300 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 301 | $test->assertNotNull($md); |
| 302 | |
| 303 | $body = $md->parse(); |
| 304 | $test->assertNotNull($body); |
| 305 | |
| 306 | $expect = '<p> <strong>太文字</strong> </p> ' |
| 307 | . '<p> <em>イタリック</em> </p> ' |
| 308 | . '<p> <u>下線</u> </p> ' |
| 309 | . '<p> <s>取消線</s> </p> ' |
| 310 | . '<p> <span class="blink" style="animation-duration: 5s;">明滅文字</span> </p> ' |
| 311 | . '<p> <span style="font-size: 72px;">フォントサイズ</span> </p> ' |
| 312 | . '<p> <span style="color: #ff00ff;">フォントカラー</span> </p>'; |
| 313 | $actual = str_replace("\n", '', trim($body)); |
| 314 | |
| 315 | $test->assertEquals($expect, $actual); |
| 316 | }); |
| 317 | |
| 318 | $test->it('複数マークダウンはHTMLに交換出来るはず', function($test): void { |
| 319 | mkfile("[^(500)<God>(すわこ)](https://techsuwako.jp/)^"); |
| 320 | |
| 321 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 322 | $test->assertNotNull($md); |
| 323 | |
| 324 | $body = $md->parse(); |
| 325 | $test->assertNotNull($body); |
| 326 | |
| 327 | $expect = Constants::START.'<a href="https://techsuwako.jp/"><span style="font-size: 500px;"><ruby>God<rp>(</rp><rt>すわこ</rt><rp>)</rp></ruby></a></span>'.Constants::END; |
| 328 | $actual = str_replace("\n", '', trim($body)); |
| 329 | |
| 330 | $test->assertEquals($expect, $actual); |
| 331 | }); |
| 332 | |
| 333 | $test->it('インライン数式はMathMLに交換出来るはず', function($test): void { |
| 334 | mkfile('長さは$$\lVert \mathbf{v} \rVert = \sqrt{x^2 + y^2}$$です。'); |
| 335 | |
| 336 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 337 | $body = $md->parse(); |
| 338 | |
| 339 | $test->assertStringContains('<math alttext=', $body); |
| 340 | $test->assertStringContains('<msqrt>', $body); |
| 341 | $test->assertStringContains('<mi>𝐯</mi>', $body); |
| 342 | $test->assertStringContains('<msup><mi>x</mi><mn>2</mn></msup>', $body); |
| 343 | $test->assertStringContains('です。', $body); |
| 344 | $test->assertEquals([], $md->getMathErrors()); |
| 345 | }); |
| 346 | |
| 347 | $test->it('LaTeXのバックスラッシュは消されないはず', function($test): void { |
| 348 | mkfile('$$\frac{1}{2} \cdot \theta$$の後に\*エスケープ\*'); |
| 349 | |
| 350 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 351 | $body = $md->parse(); |
| 352 | |
| 353 | $test->assertStringContains('<mfrac><mrow><mn>1</mn></mrow><mrow><mn>2</mn></mrow></mfrac>', $body); |
| 354 | $test->assertStringContains('<mo>⋅</mo><mi>θ</mi>', $body); |
| 355 | $test->assertStringContains('の後に*エスケープ*', $body); |
| 356 | }); |
| 357 | |
| 358 | $test->it('数式ブロックはブロック表示のMathMLに交換出来るはず', function($test): void { |
| 359 | mkfile("前の段落\n\n$$\n\\mathbf{M} =\n\\begin{bmatrix} a & b \\\\\nc & d \\end{bmatrix}\n$$\n\n\$\$x^2\$\$\n\n後の段落"); |
| 360 | |
| 361 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 362 | $body = $md->parse(); |
| 363 | |
| 364 | $test->assertEquals(2, substr_count($body, '<div class="math-display"><math display="block"')); |
| 365 | $test->assertEquals(2, substr_count($body, '<mtr>')); |
| 366 | $test->assertStringContains('<mo fence="true" stretchy="true" form="prefix">[</mo>', $body); |
| 367 | $test->assertStringContains('前の段落', $body); |
| 368 | $test->assertStringContains('後の段落', $body); |
| 369 | $test->assertEquals([], $md->getMathErrors()); |
| 370 | }); |
| 371 | |
| 372 | $test->it('未対応のLaTeXコマンドはエラーとして報告されるはず', function($test): void { |
| 373 | mkfile('$$\kerokero{x}$$'); |
| 374 | |
| 375 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 376 | $body = $md->parse(); |
| 377 | |
| 378 | $test->assertStringContains('<merror>', $body); |
| 379 | $test->assertEquals(['\kerokero'], $md->getMathErrors()); |
| 380 | }); |
| 381 | |
| 382 | $test->it('インラインコードの中身は変換されないはず', function($test): void { |
| 383 | mkfile('`a * b * c`と`my_var_name`と`\n`と`$$x$$`'); |
| 384 | |
| 385 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 386 | $body = $md->parse(); |
| 387 | |
| 388 | $test->assertStringContains('<code>a * b * c</code>', $body); |
| 389 | $test->assertStringContains('<code>my_var_name</code>', $body); |
| 390 | $test->assertStringContains('<code>\n</code>', $body); |
| 391 | $test->assertStringContains('<code>$$x$$</code>', $body); |
| 392 | }); |
| 393 | |
| 394 | $test->it('コードブロックの行末のバックスラッシュは残るはず', function($test): void { |
| 395 | mkfile("```txt\n /\\\n / \\\n```"); |
| 396 | |
| 397 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 398 | $body = $md->parse(); |
| 399 | |
| 400 | $test->assertStringContains(" /\\\n / \\</code></pre>", $body); |
| 401 | }); |
| 402 | |
| 403 | $test->it('コードタブはラジオボタンのタブに交換出来るはず', function($test): void { |
| 404 | mkfile(":::tabs\n```slang\nfloat3 v;\n```\n```glsl\nvec3 v;\n```\n```glsl GLSL 4.50\nvec3 w;\n```\n:::\n\n後の段落"); |
| 405 | |
| 406 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 407 | $body = $md->parse(); |
| 408 | |
| 409 | $test->assertStringContains('<div class="code-tabs">', $body); |
| 410 | $test->assertEquals(3, substr_count($body, '<input type="radio"')); |
| 411 | $test->assertEquals(1, substr_count($body, 'checked="checked"')); |
| 412 | $test->assertStringContains('>Slang</label>', $body); |
| 413 | $test->assertStringContains('>GLSL</label>', $body); |
| 414 | $test->assertStringContains('>GLSL 4.50</label>', $body); |
| 415 | $test->assertStringContains('<code class="language-slang hl"><span class="hl-type">float3</span> v;</code>', $body); |
| 416 | $test->assertStringContains('<code class="language-glsl hl"><span class="hl-type">vec3</span> v;</code>', $body); |
| 417 | $test->assertStringContains('後の段落', $body); |
| 418 | }); |
| 419 | |
| 420 | $test->it('対応している言語のコードブロックはハイライトされるはず', function($test): void { |
| 421 | mkfile("```c\nint x = 42; // <b>\n```\n\n```sh\necho \"<b>\"\n```"); |
| 422 | |
| 423 | $md = new Markdown('dietrannynigger', '/blog/'); |
| 424 | $body = $md->parse(); |
| 425 | |
| 426 | $test->assertStringContains('<code class="language-c hl"><span class="hl-keyword">int</span> x = <span class="hl-number">42</span>; <span class="hl-comment">// <b></span></code>', $body); |
| 427 | // 未対応の言語はエスケープだけ |
| 428 | $test->assertStringContains('<code class="language-sh">echo "<b>"</code>', $body); |
| 429 | }); |
| 430 | |
| 431 | $test->it('文字列から変換出来て、メタデータの区切りは無いはず', function($test): void { |
| 432 | $body = Markdown::fromString("# 題 <b>\n\n前\n\n----\n\n後\n")->parse(); |
| 433 | $test->assertStringContains('題 <b></h1>', $body); |
| 434 | $test->assertStringContains('前', $body); |
| 435 | $test->assertStringContains('<hr', $body); |
| 436 | $test->assertStringContains('後', $body); |
| 437 | }); |
| 438 | |
| 439 | rmfile(); |
| 440 | }); |
| 441 | |
| 442 | $test->printSummary(); |