ミラーる

This commit is contained in:
2025-11-07 22:50:22 +09:00
parent 438c7d8aef
commit b1ef7f218e
19 changed files with 2511 additions and 2511 deletions

View File

@@ -1,188 +1,188 @@
# Little Beast # Little Beast
シンプル、実践的、アンチ・ブロート シンプル、実践的、アンチ・ブロート
## Little Beast とは? ## Little Beast とは?
Little Beast は PHP 8.3 以上向けのフレームワークで、076.moeゲーム開発会社と technicalsuwako.moe社長のブログ向けに作られました。\ Little Beast は PHP 8.3 以上向けのフレームワークで、076.moeゲーム開発会社と technicalsuwako.moe社長のブログ向けに作られました。\
メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\ メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\
各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\ 各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\
全てのモジュールはゼロから書かれており、データベースは一切必要ありません。 全てのモジュールはゼロから書かれており、データベースは一切必要ありません。
Little Beast はテクニカル諏訪子がゲーム開発に完全復帰する前の最後の Web プロジェクトです。 Little Beast はテクニカル諏訪子がゲーム開発に完全復帰する前の最後の Web プロジェクトです。
## Little Beast が「ではない」物 ## Little Beast が「ではない」物
* 汎用フレームワーク * 汎用フレームワーク
* 使い憎い * 使い憎い
* Web 開発者向け * Web 開発者向け
* 万人向け * 万人向け
* インストールが面倒 * インストールが面倒
* 教条的 * 教条的
* 民主的に運営される * 民主的に運営される
## Little Beast が「持っていない」物 ## Little Beast が「持っていない」物
* データベース(全てファイルベース) * データベース(全てファイルベース)
* 依存関係 * 依存関係
* パッケージマネージャー * パッケージマネージャー
* Docker/Kubernetes/Vagrant/Nix 等のコンテナ * Docker/Kubernetes/Vagrant/Nix 等のコンテナ
* JavaScript * JavaScript
* ブロートウェア * ブロートウェア
* ORM * ORM
* 認証/認可システム * 認証/認可システム
* キューやバックグラウンドジョブ * キューやバックグラウンドジョブ
* クラウド/サーバーレス統合 * クラウド/サーバーレス統合
* スキャフォールディング * スキャフォールディング
* コード生成 * コード生成
* 抽象化レイヤー * 抽象化レイヤー
* 不要なファイル * 不要なファイル
* DEI、行動規範、その他の差別的慣行 * DEI、行動規範、その他の差別的慣行
## 独自機能 ## 独自機能
* データベース不要 * データベース不要
* Composer や PEAR 不要 * Composer や PEAR 不要
* サーバーオーバーヘッドゼロ * サーバーオーバーヘッドゼロ
* コンテナ不要 * コンテナ不要
* Maron テンプレートエンジン * Maron テンプレートエンジン
* カスタム Markdown * カスタム Markdown
* ActivityPub * ActivityPub
* 多言語対応 * 多言語対応
* マルチブログ対応 * マルチブログ対応
* 100% 綺麗で正しい HTML5 と CSS3 * 100% 綺麗で正しい HTML5 と CSS3
* 100% 正しい PHP * 100% 正しい PHP
* SEO フレンドリー * SEO フレンドリー
* モジュラー CSS * モジュラー CSS
* Atom フィード * Atom フィード
* 組み込みテストスイート * 組み込みテストスイート
## インストール方法 ## インストール方法
```sh ```sh
cd /var/www/htdocs cd /var/www/htdocs
git clone https://github.com/TechnicalSuwako/LittleBeast.git . git clone https://github.com/TechnicalSuwako/LittleBeast.git .
mv config/config.sample.php config/config.php mv config/config.sample.php config/config.php
``` ```
HTTP サーバーを設定して `/public` をルートとして `php-fpm` で実行して下さい。\ HTTP サーバーを設定して `/public` をルートとして `php-fpm` で実行して下さい。\
それだけです! それだけです!
### OpenBSD サーバー ### OpenBSD サーバー
```sh ```sh
pkg_add php-8.4.14 php-gmp-8.4.14 pkg_add php-8.4.14 php-gmp-8.4.14
rcctl enable php84_fpm httpd relayd rcctl enable php84_fpm httpd relayd
rcctl start php84_fpm httpd relayd rcctl start php84_fpm httpd relayd
``` ```
#### httpd #### httpd
``` ```
server "technicalsuwako.moe" { server "technicalsuwako.moe" {
listen on * tls port 8443 listen on * tls port 8443
gzip-static gzip-static
tls { tls {
certificate "/etc/ssl/technicalsuwako.moe.crt" certificate "/etc/ssl/technicalsuwako.moe.crt"
key "/etc/ssl/private/technicalsuwako.moe.key" key "/etc/ssl/private/technicalsuwako.moe.key"
} }
root "/htdocs/technicalsuwako.moe/www/public" root "/htdocs/technicalsuwako.moe/www/public"
directory index "index.php" directory index "index.php"
location "/.well-known/acme-challenge/*" { location "/.well-known/acme-challenge/*" {
root "/acme" root "/acme"
request strip 2 request strip 2
} }
location "/*.php" { location "/*.php" {
fastcgi socket "/run/php-fpm.sock" fastcgi socket "/run/php-fpm.sock"
} }
location "/*.php[/?]*" { location "/*.php[/?]*" {
fastcgi socket "/run/php-fpm.sock" fastcgi socket "/run/php-fpm.sock"
} }
location "/" { location "/" {
directory index "index.php" directory index "index.php"
} }
location match "/blog/" { location match "/blog/" {
request rewrite "/index.php" request rewrite "/index.php"
} }
location match "/about" { location match "/about" {
request rewrite "/index.php" request rewrite "/index.php"
} }
location match "/monero" { location match "/monero" {
request rewrite "/index.php" request rewrite "/index.php"
} }
location match "/secret" { location match "/secret" {
request rewrite "/index.php" request rewrite "/index.php"
} }
location match "/ap/" { location match "/ap/" {
request rewrite "/index.php" request rewrite "/index.php"
} }
location "/.well-known/webfinger" { location "/.well-known/webfinger" {
request rewrite "/index.php" request rewrite "/index.php"
} }
location "/blog.atom" { location "/blog.atom" {
request rewrite "/index.php" request rewrite "/index.php"
} }
} }
server "www.technicalsuwako.moe" { server "www.technicalsuwako.moe" {
listen on * tls port 8443 listen on * tls port 8443
gzip-static gzip-static
tls { tls {
certificate "/etc/ssl/technicalsuwako.moe.crt" certificate "/etc/ssl/technicalsuwako.moe.crt"
key "/etc/ssl/private/technicalsuwako.moe.key" key "/etc/ssl/private/technicalsuwako.moe.key"
} }
block return 301 "https://technicalsuwako.moe$REQUEST_URI" block return 301 "https://technicalsuwako.moe$REQUEST_URI"
} }
``` ```
#### relayd #### relayd
``` ```
relayd_addr="0.0.0.0" relayd_addr="0.0.0.0"
router_addr="192.168.10.106" router_addr="192.168.10.106"
table <httpd> { $router_addr } table <httpd> { $router_addr }
http protocol reverse { http protocol reverse {
tcp { nodelay, sack, socket buffer 65536, backlog 100 } tcp { nodelay, sack, socket buffer 65536, backlog 100 }
tls ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256" tls ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
tls keypair "technicalsuwako.moe" tls keypair "technicalsuwako.moe"
return error return error
match request header append "X-Forwarded-For" value "$REMOTE_ADDR" match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
match request header append "X-Forwarded-Port" value "$REMOTE_PORT" match request header append "X-Forwarded-Port" value "$REMOTE_PORT"
#match response header set "Referrer-Policy" value "same-origin" #match response header set "Referrer-Policy" value "same-origin"
match response header set "X-Frame-Options" value "deny" match response header set "X-Frame-Options" value "deny"
match response header set "X-Content-Type-Options" value "nosniff" match response header set "X-Content-Type-Options" value "nosniff"
match response header set "Referrer-Policy" value "strict-origin-when-cross-origin" match response header set "Referrer-Policy" value "strict-origin-when-cross-origin"
match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload" match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
match response header set "Cross-Origin-Opener-Policy" value "same-origin" match response header set "Cross-Origin-Opener-Policy" value "same-origin"
match response header set "Content-Security-Policy" value "img-src 'self' https://*.076.moe http://*.076.moe https://*.technicalsuwako.moe http://*.technicalsuwako.moe; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none';" match response header set "Content-Security-Policy" value "img-src 'self' https://*.076.moe http://*.076.moe https://*.technicalsuwako.moe http://*.technicalsuwako.moe; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none';"
match response header append "Permissions-Policy" value "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=()" match response header append "Permissions-Policy" value "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=()"
match request header "Accept-Encoding" value "gzip" tag "gzip" match request header "Accept-Encoding" value "gzip" tag "gzip"
match response header "Content-Type" value "text/*" tag "compress" match response header "Content-Type" value "text/*" tag "compress"
match response header "Content-Type" value "text/html" tag "charset=UTF-8" match response header "Content-Type" value "text/html" tag "charset=UTF-8"
match response header "Content-Type" value "application/javascript" tag "compress" match response header "Content-Type" value "application/javascript" tag "compress"
match response header "Content-Type" value "application/json" tag "compress" match response header "Content-Type" value "application/json" tag "compress"
pass request quick header "Host" value "technicalsuwako.moe" forward to <httpd> pass request quick header "Host" value "technicalsuwako.moe" forward to <httpd>
pass pass
} }
relay www_tls { relay www_tls {
listen on $relayd_addr port 443 tls listen on $relayd_addr port 443 tls
protocol reverse protocol reverse
# Default # Default
forward to <httpd> port 8443 check tcp forward to <httpd> port 8443 check tcp
} }
relay www_www { relay www_www {
listen on $relayd_addr port 80 listen on $relayd_addr port 80
protocol reverse protocol reverse
# Default # Default
forward to <httpd> port 8080 check tcp forward to <httpd> port 8080 check tcp
} }
``` ```
## 必要な PHP モジュール ## 必要な PHP モジュール
* php_gmp * php_gmp

View File

@@ -1,22 +1,22 @@
title: テーブルの例 title: テーブルの例
uuid: 684d234e-f0d6-4da0-8f38-71c7105262d5 uuid: 684d234e-f0d6-4da0-8f38-71c7105262d5
author: Little-san author: Little-san
date: 2025-04-11 08:50:57 date: 2025-04-11 08:50:57
category: test,css category: test,css
css: table css: table
---- ----
テーブルの例だ。 テーブルの例だ。
複数CSSファイルを含むには、コンマで分けて下さい。 複数CSSファイルを含むには、コンマで分けて下さい。
例えば: `css: table,search` 例えば: `css: table,search`
## プログラミング言語のランキング ## プログラミング言語のランキング
| プログラミング言語 | ランキング | 理由 | | プログラミング言語 | ランキング | 理由 |
|-------|-----|----| |-------|-----|----|
| C | ★★★★★ | どこでも使える | | C | ★★★★★ | どこでも使える |
| C++ | ★★★★★ | たーのし~ | | C++ | ★★★★★ | たーのし~ |
| PHP | ★★★★☆ | Little Beastをで作ったから | | PHP | ★★★★☆ | Little Beastをで作ったから |
| Go | ★★★☆☆ | 言語は問題ないけど、会社は親DEI・・・ | | Go | ★★★☆☆ | 言語は問題ないけど、会社は親DEI・・・ |
| Ruby | ★☆☆☆☆ | 遅過ぎる | | Ruby | ★☆☆☆☆ | 遅過ぎる |
| Javascript | ★☆☆☆☆ | 🤡 | | Javascript | ★☆☆☆☆ | 🤡 |
| Rust | ★☆☆☆☆ | ゲイ!! | | Rust | ★☆☆☆☆ | ゲイ!! |
| Zig | ★★★☆☆ | 良いけど、未だ開発中 | | Zig | ★★★☆☆ | 良いけど、未だ開発中 |

View File

@@ -1,20 +1,20 @@
.fraction { .fraction {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
position: relative; position: relative;
margin: 0 0.2em; margin: 0 0.2em;
} }
.fraction .numerator, .fraction .denominator { .fraction .numerator, .fraction .denominator {
display: block; display: block;
font-size: 0.8em; font-size: 0.8em;
} }
.fraction .numerator { .fraction .numerator {
border-bottom: 1px solid #fcfcfc; border-bottom: 1px solid #fcfcfc;
} }
.algebraic { .algebraic {
font-family: 'Times New Roman', serif; font-family: 'Times New Roman', serif;
} }

View File

@@ -1,9 +1,9 @@
.blink { .blink {
animation: blinker 1s linear infinite; animation: blinker 1s linear infinite;
} }
@keyframes blinker { @keyframes blinker {
50% { 50% {
opacity: 0; opacity: 0;
} }
} }

View File

@@ -1,8 +1,8 @@
blockquote { blockquote {
background: #121012; background: #121012;
border: 2px solid #f545f5; border: 2px solid #f545f5;
border-radius: 2px; border-radius: 2px;
border-left: 12px solid #c016c6; border-left: 12px solid #c016c6;
margin: 1.5em 10px; margin: 1.5em 10px;
padding: 0.5em 10px; padding: 0.5em 10px;
} }

View File

@@ -1,36 +1,36 @@
.blog-type > p { .blog-type > p {
font-weight: bolder; font-weight: bolder;
} }
a.blog-type-btn { a.blog-type-btn {
background-color: #550f75; background-color: #550f75;
color: #120f12; color: #120f12;
text-decoration: none; text-decoration: none;
border: 1px solid #fcfcfc; border: 1px solid #fcfcfc;
border-radius: 4px; border-radius: 4px;
padding: 4px; padding: 4px;
margin: 4px; margin: 4px;
transition: background-color 0.9s; transition: background-color 0.9s;
} }
a.blog-type-btn.active { a.blog-type-btn.active {
background-color: #c016c6; background-color: #c016c6;
} }
hr.blog-type-line { hr.blog-type-line {
border: 3px dotted #c016c6; border: 3px dotted #c016c6;
} }
a.blog-type-btn:hover { a.blog-type-btn:hover {
background-color: #ea79d8; background-color: #ea79d8;
} }
a.blog-type-btn.active:ae6bdb { a.blog-type-btn.active:ae6bdb {
background-color: #ae6bdb; background-color: #ae6bdb;
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
a.blog-type-btn { a.blog-type-btn {
display: block; display: block;
} }
} }

View File

@@ -1,40 +1,40 @@
.diff-table { .diff-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-family: monospace; font-family: monospace;
color: #fcfcfc; color: #fcfcfc;
} }
.diff-table td { .diff-table td {
border: 1px solid #bcb4bc; border: 1px solid #bcb4bc;
padding: 5px; padding: 5px;
vertical-align: top; vertical-align: top;
} }
.diff-header th { .diff-header th {
border: 1px solid #bcb4bc; border: 1px solid #bcb4bc;
} }
.line-number { .line-number {
width: 50px; width: 50px;
text-align: right; text-align: right;
color: #c016c6; color: #c016c6;
} }
.removed { .removed {
background-color: #fa9faa; background-color: #fa9faa;
color: #b61729; color: #b61729;
} }
.added { .added {
background-color: #88ecc1; background-color: #88ecc1;
color: #2c980c; color: #2c980c;
} }
.context { .context {
background-color: #232320; background-color: #232320;
} }
.empty { .empty {
background-color: #746c75; background-color: #746c75;
} }

View File

@@ -1,16 +1,16 @@
hr { hr {
border: 1px solid #ea79d8; border: 1px solid #ea79d8;
} }
.pager { .pager {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.prev { .prev {
text-align: left; text-align: left;
} }
.next { .next {
text-align: right; text-align: right;
} }

View File

@@ -1,94 +1,94 @@
<?php <?php
namespace Site\Controller; namespace Site\Controller;
use Site\Controller\BlogPost; use Site\Controller\BlogPost;
use Site\Lib\Markdown; use Site\Lib\Markdown;
class Atom extends BlogPost { class Atom extends BlogPost {
private string $domain = 'technicalsuwako.moe'; private string $domain = 'technicalsuwako.moe';
/** /**
* 最新の5記事のAtomフィードを生成する * 最新の5記事のAtomフィードを生成する
* *
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function feed(array $params): void { public function feed(array $params): void {
try { try {
// 最新の投稿を取得 // 最新の投稿を取得
$posts = $this->getPosts('/blog/'); $posts = $this->getPosts('/blog/');
// 最新の5件に制限 // 最新の5件に制限
$posts = array_slice($posts, 0, 5); $posts = array_slice($posts, 0, 5);
// サイトのドメインを取得 // サイトのドメインを取得
$domain = $_SERVER['HTTP_HOST']; $domain = $_SERVER['HTTP_HOST'];
$baseUrl = 'https://'.$domain; $baseUrl = 'https://'.$domain;
// 現在の日時RFC3339形式 // 現在の日時RFC3339形式
$published = date('c'); $published = date('c');
// XMLヘッダーとコンテンツタイプを設定 // XMLヘッダーとコンテンツタイプを設定
header('Content-Type: application/atom+xml; charset=utf-8'); header('Content-Type: application/atom+xml; charset=utf-8');
// Atomフィードの開始部分 // Atomフィードの開始部分
echo '<?xml version="1.0" encoding="utf-8"?>'."\n"; echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
echo '<feed xmlns="http://www.w3.org/2005/Atom">'."\n"; echo '<feed xmlns="http://www.w3.org/2005/Atom">'."\n";
// フィードの基本情報 // フィードの基本情報
echo ' <title>'.SITEINFO['title'].'</title>'."\n"; echo ' <title>'.SITEINFO['title'].'</title>'."\n";
echo ' <link href="'.$baseUrl.'" />'."\n"; echo ' <link href="'.$baseUrl.'" />'."\n";
echo ' <link href="'.$baseUrl.'/blog.atom" rel="self" />'."\n"; echo ' <link href="'.$baseUrl.'/blog.atom" rel="self" />'."\n";
echo ' <id>'.$baseUrl.'/</id>'."\n"; echo ' <id>'.$baseUrl.'/</id>'."\n";
echo ' <published>'.$published.'</published>'."\n"; echo ' <published>'.$published.'</published>'."\n";
echo ' <updated>'.$published.'</updated>'."\n"; echo ' <updated>'.$published.'</updated>'."\n";
echo ' <author>'."\n"; echo ' <author>'."\n";
echo ' <name>'.SITEINFO['title'].'</name>'."\n"; echo ' <name>'.SITEINFO['title'].'</name>'."\n";
echo ' </author>'."\n"; echo ' </author>'."\n";
// 各エントリー(記事) // 各エントリー(記事)
foreach ($posts as $post) { foreach ($posts as $post) {
// 記事の本文を取得(プレーンテキスト) // 記事の本文を取得(プレーンテキスト)
$path = ROOT.'/blog/'.$post['slug'].'.md'; $path = ROOT.'/blog/'.$post['slug'].'.md';
$content = ''; $content = '';
$postPublished = date('c', strtotime($post['date'])); $postPublished = date('c', strtotime($post['date']));
if (file_exists($path)) { if (file_exists($path)) {
$fileContent = file_get_contents($path); $fileContent = file_get_contents($path);
$parts = explode('----', $fileContent, 2); $parts = explode('----', $fileContent, 2);
if (count($parts) > 1) { if (count($parts) > 1) {
// 本文をHTMLとして準備 // 本文をHTMLとして準備
$md = new Markdown($post['slug'], '/blog/'); $md = new Markdown($post['slug'], '/blog/');
$content = $md->parse(); $content = $md->parse();
// HTMLタグを取り除かないようにCDATAで囲む // HTMLタグを取り除かないようにCDATAで囲む
$content = '<![CDATA['.$content.']]>'; $content = '<![CDATA['.$content.']]>';
} }
} }
echo ' <entry>'."\n"; echo ' <entry>'."\n";
echo ' <title>'.htmlspecialchars($post['title']).'</title>'."\n"; echo ' <title>'.htmlspecialchars($post['title']).'</title>'."\n";
echo ' <link href="'.$baseUrl.'/blog/'.$post['slug'].'" />'."\n"; echo ' <link href="'.$baseUrl.'/blog/'.$post['slug'].'" />'."\n";
echo ' <id>'.$baseUrl.'/blog/'.$post['slug'].'</id>'."\n"; echo ' <id>'.$baseUrl.'/blog/'.$post['slug'].'</id>'."\n";
echo ' <published>'.$postPublished.'</published>'."\n"; echo ' <published>'.$postPublished.'</published>'."\n";
// カテゴリ(タグ) // カテゴリ(タグ)
if (isset($post['category']) && is_array($post['category'])) { if (isset($post['category']) && is_array($post['category'])) {
foreach ($post['category'] as $category) { foreach ($post['category'] as $category) {
echo ' <category term="'.htmlspecialchars($category).'" />'."\n"; echo ' <category term="'.htmlspecialchars($category).'" />'."\n";
} }
} }
// 本文(要約または全文) // 本文(要約または全文)
echo ' <content type="html">'.$content.'</content>'."\n"; echo ' <content type="html">'.$content.'</content>'."\n";
echo ' </entry>'."\n"; echo ' </entry>'."\n";
} }
// フィードの終了 // フィードの終了
echo '</feed>'; echo '</feed>';
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フィードの作成に失敗: '.$e->getMessage(); echo 'フィードの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
} }

View File

@@ -1,66 +1,66 @@
<?php <?php
namespace Site\Controller; namespace Site\Controller;
class BlogPost { class BlogPost {
/** /**
* ブログ投稿を取得する * ブログ投稿を取得する
* *
* @return array 投稿の配列 * @return array 投稿の配列
*/ */
public function getPosts(string $section): array { public function getPosts(string $section): array {
$path = ROOT.$section; $path = ROOT.$section;
$posts = []; $posts = [];
if (!is_dir($path)) return $posts; if (!is_dir($path)) return $posts;
$files = glob($path.'/*.md'); $files = glob($path.'/*.md');
foreach ($files as $file) { foreach ($files as $file) {
$content = file_get_contents($file); $content = file_get_contents($file);
$parts = explode('----', $content, 2); $parts = explode('----', $content, 2);
if (count($parts) != 2) continue; if (count($parts) != 2) continue;
$metadata = []; $metadata = [];
$meta = explode("\n", trim($parts[0])); $meta = explode("\n", trim($parts[0]));
foreach ($meta as $line) { foreach ($meta as $line) {
$line = trim($line); $line = trim($line);
if (empty($line)) continue; if (empty($line)) continue;
$colonPos = strpos($line, ':'); $colonPos = strpos($line, ':');
if ($colonPos === false) continue; if ($colonPos === false) continue;
$key = trim(substr($line, 0, $colonPos)); $key = trim(substr($line, 0, $colonPos));
$value = trim(substr($line, $colonPos + 1)); $value = trim(substr($line, $colonPos + 1));
$value = trim($value, '"\''); $value = trim($value, '"\'');
if ($key == 'category') { if ($key == 'category') {
$metadata[$key] = array_map('trim', explode(',', $value)); $metadata[$key] = array_map('trim', explode(',', $value));
} else { } else {
$metadata[$key] = $value; $metadata[$key] = $value;
} }
} }
$articleBody = trim($parts[1]); $articleBody = trim($parts[1]);
$preview = mb_substr(strip_tags($articleBody), 0, 50).'...'; $preview = mb_substr(strip_tags($articleBody), 0, 50).'...';
$slug = basename($file, '.md'); $slug = basename($file, '.md');
$posts[] = [ $posts[] = [
'title' => $metadata['title'] ?? '', 'title' => $metadata['title'] ?? '',
'date' => $metadata['date'] ?? '', 'date' => $metadata['date'] ?? '',
'thumbnail' => $metadata['thumbnail'] ?? '', 'thumbnail' => $metadata['thumbnail'] ?? '',
'thumborient' => $metadata['thumborient'] ?? '', 'thumborient' => $metadata['thumborient'] ?? '',
'category' => $metadata['category'] ?? [], 'category' => $metadata['category'] ?? [],
'uuid' => $metadata['uuid'] ?? '', 'uuid' => $metadata['uuid'] ?? '',
'preview' => $preview, 'preview' => $preview,
'slug' => $slug, 'slug' => $slug,
]; ];
} }
// 日付でソート(新しい順) // 日付でソート(新しい順)
usort($posts, function($a, $b) { usort($posts, function($a, $b) {
return strtotime($b['date']) - strtotime($a['date']); return strtotime($b['date']) - strtotime($a['date']);
}); });
return $posts; return $posts;
} }
} }

View File

@@ -1,151 +1,151 @@
<?php <?php
namespace Site\Controller; namespace Site\Controller;
use Site\Controller\BlogPost; use Site\Controller\BlogPost;
use Site\Controller\Mods; use Site\Controller\Mods;
use Site\Lib\Activitypub; use Site\Lib\Activitypub;
class Fediverse extends BlogPost { class Fediverse extends BlogPost {
use Mods; use Mods;
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apfinger(array $params): void { public function apfinger(array $params): void {
try { try {
header('Content-Type: application/jrd+json'); header('Content-Type: application/jrd+json');
$ap = new Activitypub(); $ap = new Activitypub();
echo $ap->getWebfinger(); echo $ap->getWebfinger();
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apactor(array $params): void { public function apactor(array $params): void {
try { try {
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
$ap = new Activitypub(); $ap = new Activitypub();
echo $ap->getActor(); echo $ap->getActor();
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apinbox(array $params): void { public function apinbox(array $params): void {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('HTTP/1.1 405 Method Not Allowed'); header('HTTP/1.1 405 Method Not Allowed');
header('Allow: POST'); header('Allow: POST');
exit; exit;
} }
$input = file_get_contents('php://input'); $input = file_get_contents('php://input');
$activity = json_decode($input, true); $activity = json_decode($input, true);
if (!$activity || !isset($activity['type'])) { if (!$activity || !isset($activity['type'])) {
header('HTTP/1.1 400 Bad Request'); header('HTTP/1.1 400 Bad Request');
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
echo json_encode(['error' => '不正なアクティビティ']); echo json_encode(['error' => '不正なアクティビティ']);
exit; exit;
} }
logger(\LogType::ActivityPub, "受付に入れた:".json_encode($activity)); logger(\LogType::ActivityPub, "受付に入れた:".json_encode($activity));
try { try {
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
$ap = new Activitypub(); $ap = new Activitypub();
$ap->postInbox($activity); $ap->postInbox($activity);
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apactivity(array $params): void { public function apactivity(array $params): void {
$uuid = ''; $uuid = '';
if (isset($params['uuid'])) $uuid = $params['uuid']; if (isset($params['uuid'])) $uuid = $params['uuid'];
try { try {
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
$posts = $this->getPosts('/blog/'); $posts = $this->getPosts('/blog/');
$ap = new Activitypub($posts); $ap = new Activitypub($posts);
echo $ap->getActivity($uuid); echo $ap->getActivity($uuid);
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apoutbox(array $params): void { public function apoutbox(array $params): void {
try { try {
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
$posts = $this->getPosts('/blog/'); $posts = $this->getPosts('/blog/');
$ap = new Activitypub($posts); $ap = new Activitypub($posts);
echo $ap->getOutbox(); echo $ap->getOutbox();
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apfollowers(array $params): void { public function apfollowers(array $params): void {
try { try {
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
$ap = new Activitypub(); $ap = new Activitypub();
echo $ap->getFollowers(); echo $ap->getFollowers();
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
/** /**
* @param array $params パラメータ配列 * @param array $params パラメータ配列
* @return void * @return void
*/ */
public function apfollowing(array $params): void { public function apfollowing(array $params): void {
try { try {
header('Content-Type: application/activity+json'); header('Content-Type: application/activity+json');
$ap = new Activitypub(); $ap = new Activitypub();
echo $ap->getFollowing(); echo $ap->getFollowing();
exit; exit;
} catch (\Exception $e) { } catch (\Exception $e) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo 'フェディバースの作成に失敗: '.$e->getMessage(); echo 'フェディバースの作成に失敗: '.$e->getMessage();
exit; exit;
} }
} }
} }

View File

@@ -1,130 +1,130 @@
<?php <?php
namespace Site\Lib; namespace Site\Lib;
class DiffViewer { class DiffViewer {
private $diffContent; private $diffContent;
public function __construct(string $filePath) { public function __construct(string $filePath) {
if (!file_exists($filePath)) { if (!file_exists($filePath)) {
throw new \Exception("Diff file not found: $filePath"); throw new \Exception("Diff file not found: $filePath");
} }
$this->diffContent = file_get_contents($filePath); $this->diffContent = file_get_contents($filePath);
} }
public function displaySideBySide(): string { public function displaySideBySide(): string {
$lines = explode("\n", $this->diffContent); $lines = explode("\n", $this->diffContent);
$fileDiffs = []; $fileDiffs = [];
$currentFile = null; $currentFile = null;
$hunk = []; $hunk = [];
$lineNumbers = ['left' => 0, 'right' => 0]; $lineNumbers = ['left' => 0, 'right' => 0];
$currentLeftLines = []; $currentLeftLines = [];
$currentRightLines = []; $currentRightLines = [];
foreach ($lines as $line) { foreach ($lines as $line) {
// ファイルヘッダーの処理 // ファイルヘッダーの処理
if (preg_match('/^---\s+(.+)/', $line, $matches)) { if (preg_match('/^---\s+(.+)/', $line, $matches)) {
// ファイルを処理する場合、データを保存する // ファイルを処理する場合、データを保存する
if ($currentFile !== null) { if ($currentFile !== null) {
$this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers);
$fileDiffs[$currentFile] = [ $fileDiffs[$currentFile] = [
'leftLines' => $currentLeftLines, 'leftLines' => $currentLeftLines,
'rightLines' => $currentRightLines 'rightLines' => $currentRightLines
]; ];
$hunk = []; $hunk = [];
$currentLeftLines = []; $currentLeftLines = [];
$currentRightLines = []; $currentRightLines = [];
$lineNumbers = ['left' => 0, 'right' => 0]; $lineNumbers = ['left' => 0, 'right' => 0];
} }
$currentFile = $matches[1]; $currentFile = $matches[1];
continue; continue;
} }
if (preg_match('/^\+\+\+\s+(.+)/', $line)) { if (preg_match('/^\+\+\+\s+(.+)/', $line)) {
continue; continue;
} }
// ハンクヘッダーの処理 (例:@@ -10,6 +10,7 @@) // ハンクヘッダーの処理 (例:@@ -10,6 +10,7 @@)
if (preg_match('/^@@\s+-(\d+),\d+\s+\+(\d+),\d+\s+@@/', $line, $matches)) { if (preg_match('/^@@\s+-(\d+),\d+\s+\+(\d+),\d+\s+@@/', $line, $matches)) {
$this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers);
$hunk = []; $hunk = [];
$lineNumbers['left'] = (int)$matches[1]; $lineNumbers['left'] = (int)$matches[1];
$lineNumbers['right'] = (int)$matches[2]; $lineNumbers['right'] = (int)$matches[2];
continue; continue;
} }
// ハンクでの行列の集まり // ハンクでの行列の集まり
if (substr($line, 0, 1) === '-' || substr($line, 0, 1) === '+' || substr($line, 0, 1) === ' ') { if (substr($line, 0, 1) === '-' || substr($line, 0, 1) === '+' || substr($line, 0, 1) === ' ') {
$hunk[] = $line; $hunk[] = $line;
} }
} }
// 最後のハンク・ファイルの処理 // 最後のハンク・ファイルの処理
if ($currentFile !== null) { if ($currentFile !== null) {
$this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers);
$fileDiffs[$currentFile] = [ $fileDiffs[$currentFile] = [
'leftLines' => $currentLeftLines, 'leftLines' => $currentLeftLines,
'rightLines' => $currentRightLines 'rightLines' => $currentRightLines
]; ];
} }
// 各ファイルにHTMLの出力の作成 // 各ファイルにHTMLの出力の作成
$html = ''; $html = '';
foreach ($fileDiffs as $fileName => $diff) { foreach ($fileDiffs as $fileName => $diff) {
$html .= "<h2>ファイル: ".htmlspecialchars($fileName)."</h2>\n"; $html .= "<h2>ファイル: ".htmlspecialchars($fileName)."</h2>\n";
$html .= $this->generateHtml($diff['leftLines'], $diff['rightLines']); $html .= $this->generateHtml($diff['leftLines'], $diff['rightLines']);
} }
return $html; return $html;
} }
private function processHunk(array $hunk, array &$leftLines, array &$rightLines, array &$lineNumbers): void { private function processHunk(array $hunk, array &$leftLines, array &$rightLines, array &$lineNumbers): void {
foreach ($hunk as $line) { foreach ($hunk as $line) {
$prefix = substr($line, 0, 1); $prefix = substr($line, 0, 1);
$content = substr($line, 1); $content = substr($line, 1);
if ($prefix === '-') { if ($prefix === '-') {
$leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'removed', 'line' => $lineNumbers['left']]; $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'removed', 'line' => $lineNumbers['left']];
$lineNumbers['left']++; $lineNumbers['left']++;
} elseif ($prefix === '+') { } elseif ($prefix === '+') {
$rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'added', 'line' => $lineNumbers['right']]; $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'added', 'line' => $lineNumbers['right']];
$lineNumbers['right']++; $lineNumbers['right']++;
} elseif ($prefix === ' ') { } elseif ($prefix === ' ') {
// 両側のコンテキストは同じ行列があるかの確認 // 両側のコンテキストは同じ行列があるかの確認
while ($lineNumbers['left'] < $lineNumbers['right']) { while ($lineNumbers['left'] < $lineNumbers['right']) {
$leftLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['left']]; $leftLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['left']];
$lineNumbers['left']++; $lineNumbers['left']++;
} }
while ($lineNumbers['right'] < $lineNumbers['left']) { while ($lineNumbers['right'] < $lineNumbers['left']) {
$rightLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['right']]; $rightLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['right']];
$lineNumbers['right']++; $lineNumbers['right']++;
} }
$leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['left']]; $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['left']];
$rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['right']]; $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['right']];
$lineNumbers['left']++; $lineNumbers['left']++;
$lineNumbers['right']++; $lineNumbers['right']++;
} }
} }
} }
private function generateHtml(array $leftLines, array $rightLines): string { private function generateHtml(array $leftLines, array $rightLines): string {
$html = '<table class="diff-table">'; $html = '<table class="diff-table">';
$html .= '<tr class="diff-header"><th colspan="2">前</th><th colspan="2">新</th></tr>'; $html .= '<tr class="diff-header"><th colspan="2">前</th><th colspan="2">新</th></tr>';
$maxLines = max(count($leftLines), count($rightLines)); $maxLines = max(count($leftLines), count($rightLines));
for ($i = 0; $i < $maxLines; $i++) { for ($i = 0; $i < $maxLines; $i++) {
$left = isset($leftLines[$i]) ? $leftLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; $left = isset($leftLines[$i]) ? $leftLines[$i] : ['content' => '', 'type' => 'empty', 'line' => ''];
$right = isset($rightLines[$i]) ? $rightLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; $right = isset($rightLines[$i]) ? $rightLines[$i] : ['content' => '', 'type' => 'empty', 'line' => ''];
$html .= '<tr>'; $html .= '<tr>';
// 左(変更前) // 左(変更前)
$html .= '<td class="line-number">' . ($left['line'] ?: '&nbsp;') . '</td>'; $html .= '<td class="line-number">' . ($left['line'] ?: '&nbsp;') . '</td>';
$html .= '<td class="' . $left['type'] . '">' . ($left['content'] ?: '&nbsp;') . '</td>'; $html .= '<td class="' . $left['type'] . '">' . ($left['content'] ?: '&nbsp;') . '</td>';
// 右(変更後) // 右(変更後)
$html .= '<td class="line-number">' . ($right['line'] ?: '&nbsp;') . '</td>'; $html .= '<td class="line-number">' . ($right['line'] ?: '&nbsp;') . '</td>';
$html .= '<td class="' . $right['type'] . '">' . ($right['content'] ?: '&nbsp;') . '</td>'; $html .= '<td class="' . $right['type'] . '">' . ($right['content'] ?: '&nbsp;') . '</td>';
$html .= '</tr>'; $html .= '</tr>';
} }
$html .= '</table>'; $html .= '</table>';
return $html; return $html;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,64 @@
<?php <?php
namespace Site\Test; namespace Site\Test;
require_once __DIR__.'/../../../autoload.php'; require_once __DIR__.'/../../../autoload.php';
use Site\Lib\Tester; use Site\Lib\Tester;
use Site\Lib\Mysql; use Site\Lib\Mysql;
$test = new Tester([ $test = new Tester([
'colorOutput' => true, 'colorOutput' => true,
'verboseOutput' => true 'verboseOutput' => true
]); ]);
$test->describe('パケットのデバッグ', function($test): void { $test->describe('パケットのデバッグ', function($test): void {
try { try {
$db = new Mysql(); $db = new Mysql();
$db->setDebug(true); $db->setDebug(true);
$db->connect(); $db->connect();
$result = $db->query('SELECT * FROM user WHERE id = 1'); $result = $db->query('SELECT * FROM user WHERE id = 1');
foreach ($result['rows'] as $row) { foreach ($result['rows'] as $row) {
echo "ユーザー名: ".$row['nickname']."\n"; echo "ユーザー名: ".$row['nickname']."\n";
} }
$db->savePacketLogToFile('mysql_log.txt'); $db->savePacketLogToFile('mysql_log.txt');
$db->close(); $db->close();
} catch (\Exception $e) { } catch (\Exception $e) {
echo 'エラー: '.$e->getMessage()."\n"; echo 'エラー: '.$e->getMessage()."\n";
} }
}); });
$test->describe('プリペアドステートメント', function($test): void { $test->describe('プリペアドステートメント', function($test): void {
try { try {
$db = new Mysql(); $db = new Mysql();
$db->connect(); $db->connect();
// データの入り // データの入り
$stmt = $db->prepare('INSERT INTO users (name, age) VALUES (?, ?)'); $stmt = $db->prepare('INSERT INTO users (name, age) VALUES (?, ?)');
$test->assertTrue($stmt); $test->assertTrue($stmt);
$db->execute($stmt, ['山田太郎', 25]); $db->execute($stmt, ['山田太郎', 25]);
// TODO: assert // TODO: assert
$close = $db->demolish($stmt); $close = $db->demolish($stmt);
$this->assertTrue($close); $this->assertTrue($close);
// データの受け取り // データの受け取り
$stmt = $db->prepare('SELECT * FROM users WHERE age > ?'); $stmt = $db->prepare('SELECT * FROM users WHERE age > ?');
$test->assertTrue($stmt); $test->assertTrue($stmt);
$res = $db->execute($stmt, [20]); $res = $db->execute($stmt, [20]);
// TODO: assert // TODO: assert
print_r($res); print_r($res);
$close = $db->demolish($stmt); $close = $db->demolish($stmt);
$this->assertTrue($close); $this->assertTrue($close);
$db->close(); $db->close();
} catch (\Exception $e) { } catch (\Exception $e) {
echo 'エラー: '.$e->getMessage()."\n"; echo 'エラー: '.$e->getMessage()."\n";
} }
}); });

View File

@@ -1,23 +1,23 @@
<article class="news-card"> <article class="news-card">
{@ if (isset($post['thumbnail']) && $post['thumbnail'] != '') @} {@ if (isset($post['thumbnail']) && $post['thumbnail'] != '') @}
<div class="news-image"> <div class="news-image">
<a href="/{{ $section }}/{{ $post['slug'] }}"> <a href="/{{ $section }}/{{ $post['slug'] }}">
<img src="/static/article/{{ $post['thumbnail'] }}" alt="{{ $post['title'] }}" loading="lazy" /> <img src="/static/article/{{ $post['thumbnail'] }}" alt="{{ $post['title'] }}" loading="lazy" />
</a> </a>
</div> </div>
{@ endif @} {@ endif @}
<div class="news-content"> <div class="news-content">
<div class="news-meta"> <div class="news-meta">
<span class="news-date">{{ $post['date'] }}</span> <span class="news-date">{{ $post['date'] }}</span>
{# {@ if (isset($post['category']) && is_array($post['category'])) @} #} {# {@ if (isset($post['category']) && is_array($post['category'])) @} #}
{@ foreach ($post['category'] as $cat) @} {@ foreach ($post['category'] as $cat) @}
<span class="news-category">{{ $cat }}</span> <span class="news-category">{{ $cat }}</span>
{@ endforeach @} {@ endforeach @}
{# {@ endif @} #} {# {@ endif @} #}
</div> </div>
<h2 class="news-title"> <h2 class="news-title">
<a href="/{{ $section }}/{{ $post['slug'] }}{{{ isset($_GET['q']) ? '?q='.urlencode($_GET['q']) : '' }}}">{{{ $post['title'] }}}</a> <a href="/{{ $section }}/{{ $post['slug'] }}{{{ isset($_GET['q']) ? '?q='.urlencode($_GET['q']) : '' }}}">{{{ $post['title'] }}}</a>
</h2> </h2>
<p class="news-preview">{{{ $post['preview'] }}}</p> <p class="news-preview">{{{ $post['preview'] }}}</p>
</div> </div>
</article> </article>

View File

@@ -1,71 +1,71 @@
{@ if (isset($totalPages) && $totalPages > 1) @} {@ if (isset($totalPages) && $totalPages > 1) @}
<div class="pagination"> <div class="pagination">
{# 検索クエリがある場合はページネーションリンクに含める #} {# 検索クエリがある場合はページネーションリンクに含める #}
{$ $queryParams = [] $} {$ $queryParams = [] $}
{@ if (isset($_GET['q']) && !empty($_GET['q'])) @} {@ if (isset($_GET['q']) && !empty($_GET['q'])) @}
{$ $queryParams['q'] = $_GET['q'] $} {$ $queryParams['q'] = $_GET['q'] $}
{@ endif @} {@ endif @}
{# 前のページへのリンク #} {# 前のページへのリンク #}
{@ if (isset($currentPage) && $currentPage > 1) @} {@ if (isset($currentPage) && $currentPage > 1) @}
{$ $prevParams = $queryParams $} {$ $prevParams = $queryParams $}
{$ $prevParams['page'] = $currentPage - 1 $} {$ $prevParams['page'] = $currentPage - 1 $}
{$ $prevQueryString = http_build_query($prevParams) $} {$ $prevQueryString = http_build_query($prevParams) $}
<a href="?{{ $prevQueryString }}" class="page-link">&laquo; 前</a> <a href="?{{ $prevQueryString }}" class="page-link">&laquo; 前</a>
{@ endif @} {@ endif @}
{# 表示するページ番号の範囲を計算(モバイル対応の為) #} {# 表示するページ番号の範囲を計算(モバイル対応の為) #}
{# 最大表示ページ数 #} {# 最大表示ページ数 #}
{$ $rangeSize = 2 $} {$ $rangeSize = 2 $}
{$ $startPage = max(1, $currentPage - floor($rangeSize / 2)) $} {$ $startPage = max(1, $currentPage - floor($rangeSize / 2)) $}
{$ $endPage = min($totalPages, $startPage + $rangeSize - 1) $} {$ $endPage = min($totalPages, $startPage + $rangeSize - 1) $}
{# 範囲の調整 #} {# 範囲の調整 #}
{@ if ($endPage - $startPage + 1 < $rangeSize && $startPage > 1) @} {@ if ($endPage - $startPage + 1 < $rangeSize && $startPage > 1) @}
{$ $startPage = max(1, $endPage - $rangeSize + 1) $} {$ $startPage = max(1, $endPage - $rangeSize + 1) $}
{@ endif @} {@ endif @}
{# 最初のページへのリンク(多数のページがある場合) #} {# 最初のページへのリンク(多数のページがある場合) #}
{@ if ($startPage > 1) @} {@ if ($startPage > 1) @}
{$ $firstParams = $queryParams $} {$ $firstParams = $queryParams $}
{$ $firstParams['page'] = 1 $} {$ $firstParams['page'] = 1 $}
{$ $firstQueryString = http_build_query($firstParams) $} {$ $firstQueryString = http_build_query($firstParams) $}
<a href="?{{ $firstQueryString }}" class="page-link">1</a> <a href="?{{ $firstQueryString }}" class="page-link">1</a>
{@ if ($startPage > 2) @} {@ if ($startPage > 2) @}
<span class="page-ellipsis">...</span> <span class="page-ellipsis">...</span>
{@ endif @} {@ endif @}
{@ endif @} {@ endif @}
{@ for ($i = $startPage; $i <= $endPage; $i++) @} {@ for ($i = $startPage; $i <= $endPage; $i++) @}
{$ $pageParams = $queryParams $} {$ $pageParams = $queryParams $}
{$ $pageParams['page'] = $i $} {$ $pageParams['page'] = $i $}
{$ $pageQueryString = http_build_query($pageParams) $} {$ $pageQueryString = http_build_query($pageParams) $}
{@ if ($i == $currentPage) @} {@ if ($i == $currentPage) @}
<span class="page-current" aria-current="page">{{ $i }}</span> <span class="page-current" aria-current="page">{{ $i }}</span>
{@ else @} {@ else @}
<a href="?{{ $pageQueryString }}" class="page-link">{{ $i }}</a> <a href="?{{ $pageQueryString }}" class="page-link">{{ $i }}</a>
{@ endif @} {@ endif @}
{@ endfor @} {@ endfor @}
{@ if ($endPage < $totalPages) @} {@ if ($endPage < $totalPages) @}
{# 最後のページへのリンク(多数のページがある場合) #} {# 最後のページへのリンク(多数のページがある場合) #}
{$ $lastParams = $queryParams $} {$ $lastParams = $queryParams $}
{$ $lastParams['page'] = $totalPages $} {$ $lastParams['page'] = $totalPages $}
{$ $lastQueryString = http_build_query($lastParams) $} {$ $lastQueryString = http_build_query($lastParams) $}
{@ if ($endPage < $totalPages - 1) @} {@ if ($endPage < $totalPages - 1) @}
<span class="page-ellipsis">...</span> <span class="page-ellipsis">...</span>
{@ endif @} {@ endif @}
<a href="?{{ $lastQueryString }}" class="page-link">{{ $totalPages }}</a> <a href="?{{ $lastQueryString }}" class="page-link">{{ $totalPages }}</a>
{@ endif @} {@ endif @}
{# 次のページへのリンク #} {# 次のページへのリンク #}
{@ if (isset($currentPage) && $currentPage < $totalPages) @} {@ if (isset($currentPage) && $currentPage < $totalPages) @}
{$ $nextParams = $queryParams $} {$ $nextParams = $queryParams $}
{$ $nextParams['page'] = $currentPage + 1 $} {$ $nextParams['page'] = $currentPage + 1 $}
{$ $nextQueryString = http_build_query($nextParams) $} {$ $nextQueryString = http_build_query($nextParams) $}
<a href="?{{ $nextQueryString }}" class="page-link">次 &raquo;</a> <a href="?{{ $nextQueryString }}" class="page-link">次 &raquo;</a>
{@ endif @} {@ endif @}
</div> </div>
{@ endif @} {@ endif @}

View File

@@ -1,6 +1,6 @@
<div class="search-form"> <div class="search-form">
<form action="/{{ $curblog != 'gd' ? $section : '' }}" method="GET"> <form action="/{{ $curblog != 'gd' ? $section : '' }}" method="GET">
<input type="text" name="q" value="{{{ isset($_GET['q']) ? htmlspecialchars($_GET['q']) : '' }}}" placeholder="キーワードを入力して下さい" /> <input type="text" name="q" value="{{{ isset($_GET['q']) ? htmlspecialchars($_GET['q']) : '' }}}" placeholder="キーワードを入力して下さい" />
<input type="submit" value="検索" /> <input type="submit" value="検索" />
</form> </form>
</div> </div>

View File

@@ -1,15 +1,15 @@
{@ include(common/header) @} {@ include(common/header) @}
<h1 class="paragraph">Moneroで支援♡</h1> <h1 class="paragraph">Moneroで支援♡</h1>
<p class="paragraph"> <p class="paragraph">
欲しければ、モネロXMRでご支援お願い申し上げます。 欲しければ、モネロXMRでご支援お願い申し上げます。
</p> </p>
<p> <p>
<img src="https://ass.technicalsuwako.moe/keroxmr.png" alt="" /> <img src="https://ass.technicalsuwako.moe/keroxmr.png" alt="" />
<img src="https://ass.technicalsuwako.moe/xmr-qr.png" alt="" /> <img src="https://ass.technicalsuwako.moe/xmr-qr.png" alt="" />
</p> </p>
<p class="paragraph"> <p class="paragraph">
<pre><code>88daW9ANXGVg9zHe6tzHSpQjxHN6JPFDz9wvZBecL1BfTFwmkuLYm9xRsLUt1WAVGPQ6h5pZX6nyu9zXFwE5efSz1gtE1oz</code></pre> <pre><code>88daW9ANXGVg9zHe6tzHSpQjxHN6JPFDz9wvZBecL1BfTFwmkuLYm9xRsLUt1WAVGPQ6h5pZX6nyu9zXFwE5efSz1gtE1oz</code></pre>
</p> </p>
{@ include(common/footer) @} {@ include(common/footer) @}

View File

@@ -1,6 +1,6 @@
{@ include(common/header) @} {@ include(common/header) @}
<h1 class="paragraph">秘密のページ</h1> <h1 class="paragraph">秘密のページ</h1>
<p class="paragraph"> <p class="paragraph">
内緒だね~ 内緒だね~
</p> </p>
{@ include(common/footer) @} {@ include(common/footer) @}