レポジトリ種類: Mercurial

<?php
namespace Site\Controller;

use Site\Controller\BlogPost;
use Site\Controller\Mods;
use Site\Lib\Markdown;
use Site\Lib\Template;

class Home extends BlogPost {
  use Mods;

  private array $searchKeywords = [];

  //------------------------------------------
  // ページ
  //------------------------------------------

  /**
   * ブログ投稿ページ
   *
   * @param array $params ページ番号等
   * @return void
   */
  public function show(array $params): void {
    try {
      $page = isset($_GET['page']) ? $_GET['page'] : 1;
      $postsPerPage = 20;

      $tmpl = new Template('/');
      $pagetit = 'トップページ';

      $description = 'テクニカル諏訪子ちゃんの個人ブログ';

      $posts = $this->getPosts();
      if (!is_array($posts)) $posts = [];

      // 検索機能が使用されている場合
      if (isset($_GET['q']) && !empty($_GET['q'])) {
        $this->searchKeywords = array_map('trim', explode(',', $_GET['q']));
        $posts = $this->searchPosts($this->searchKeywords, $posts);
        $pagetit = '検索結果: ' . htmlspecialchars($_GET['q']);

        // 検索結果にキーワードをハイライト
        $posts = $this->highlightKeywords($posts);
      }

      // ページネーション
      $totalPosts = count($posts);
      $totalPages = ceil($totalPosts / $postsPerPage);
      $page = min($page, $totalPages);
      $currentPosts = array_slice(
        $posts, 
        ($page - 1) * $postsPerPage, 
        $postsPerPage
      );

      $tmpl->assign('currentPage', $page);
      $tmpl->assign('totalPages', $totalPages);
      $tmpl->assign('posts', $currentPosts);
      $tmpl->assign('pagetit', $pagetit);
      $tmpl->assign('curPage', 'blog');
      $tmpl->assign('custCss', false);
      $tmpl->assign('sns', $this->getSns());
      $tmpl->assign('support', $this->getSupport());
      $tmpl->assign('menu', $this->getMenu());
      $tmpl->assign('description', $description);
      $tmpl->assign('searchActive', !empty($this->searchKeywords));

      $tmpl->addCss('news');
      $tmpl->addCss('search');
      $tmpl->addCss('pagination');
      $tmpl->render('home');
    } catch (\Exception $e) {
      throw new \Exception($e->getMessage());
    }
  }

  /**
   * ブログ投稿ページ
   *
   * @param array $params マークダウンファイル等
   * @return void
   */
  public function article(array $params): void {
    $page = '';
    if (isset($params['page'])) $page = $params['page'];

    try {
      $tmpl = new Template('/');
      $md = new Markdown($page);

      $meta = $md->getMetadata();
      $pagetit = $meta->title;
      $article = $md->parse();
      $description = 'テクニカル諏訪子ちゃんの個人ブログ';

      // 検索からの遷移の場合、記事内のキーワードをハイライト
      if (isset($_GET['q']) && !empty($_GET['q'])) {
        $keywords = array_map('trim', explode(',', $_GET['q']));
        $article = $this->highlightTextContent($article, $keywords);
        $meta->title = $this->highlightTextContent($meta->title, $keywords);
      }

      $tmpl->assign('pagetit', $pagetit);
      $tmpl->assign('curPage', 'blog');
      $tmpl->assign('custCss', false);
      $tmpl->assign('sns', $this->getSns());
      $tmpl->assign('support', $this->getSupport());
      $tmpl->assign('menu', $this->getMenu());
      $tmpl->assign('article', $article);
      $tmpl->assign('meta', $meta);
      $tmpl->assign('description', $description);

      if (isset($meta->css) && !empty($meta->css)) {
        foreach ($meta->css as $v) {
          $tmpl->addCss($v);
        }
      }

      $tmpl->addCss('news-article');
      $tmpl->addCss('search');
      $tmpl->render('article');
    } catch (\Exception $e) {
      throw new \Exception($e->getMessage());
    }
  }

  //------------------------------------------
  // 機能性
  //------------------------------------------

  /**
   * キーワードに基づいて投稿を検索する
   * 
   * @param array $keywords 検索キーワードの配列
   * @param array $posts 検索対象の投稿記事の配列
   * @return array 検索条件に一致する投稿記事の配列
   */
  private function searchPosts(array $keywords, array $posts): array {
    if (empty($keywords) || empty($posts)) {
      return $posts;
    }

    $foundPosts = [];
    $path = ROOT.'/blog/';

    foreach ($posts as $post) {
      $matched = false;
      
      // タイトルで検索
      foreach ($keywords as $keyword) {
        $keyword = trim($keyword);
        if (empty($keyword)) continue;
        
        // タイトル内でキーワードが見つかった場合
        if (mb_stripos($post['title'], $keyword) !== false) {
          $foundPosts[] = $post;
          $matched = true;
          break;
        }
      }
      
      // すでにマッチしていれば次の記事へ
      if ($matched) continue;
      
      // 記事の本文をチェック
      $slug = $post['slug'];
      $filePath = $path.$slug.'.md';
      
      if (file_exists($filePath)) {
        $content = file_get_contents($filePath);
        $parts = explode('----', $content, 2);
        if (count($parts) > 1) {
          $articleBody = trim($parts[1]);
          
          foreach ($keywords as $keyword) {
            $keyword = trim($keyword);
            if (empty($keyword)) continue;
            
            // 本文内でキーワードが見つかった場合
            if (mb_stripos($articleBody, $keyword) !== false) {
              $foundPosts[] = $post;
              break;
            }
          }
        }
      }
    }
    
    return $foundPosts;
  }

  /**
   * 検索結果の投稿内のキーワードをハイライトする
   * 
   * @param array $posts 検索結果の投稿配列
   * @return array ハイライト処理後の投稿配列
   */
  private function highlightKeywords(array $posts): array {
    if (empty($this->searchKeywords) || empty($posts)) {
      return $posts;
    }
    
    foreach ($posts as &$post) {
      // タイトルのハイライト
      if (!empty($post['title'])) {
        $post['title'] =
          $this->highlightTextContent($post['title'], $this->searchKeywords);
      }
      
      // プレビューのハイライト
      if (!empty($post['preview'])) {
        $post['preview'] =
          $this->highlightTextContent($post['preview'], $this->searchKeywords);
      }
    }
    
    return $posts;
  }
  
  /**
   * テキスト内のキーワードをハイライトする
   * 
   * @param string $text ハイライト対象のテキスト
   * @param array $keywords ハイライトするキーワード配列
   * @return string ハイライト処理後のテキスト
   */
  private function highlightTextContent(string $text, array $keywords): string {
    if (empty($keywords) || empty($text)) {
      return $text;
    }
    
    $highlightedText = $text;
    
    foreach ($keywords as $keyword) {
      $keyword = trim($keyword);
      if (empty($keyword)) continue;
      
      // キーワードを大文字小文字を区別せずに置換
      $highlightedText = preg_replace(
        '/('.preg_quote($keyword, '/').')/iu',
        '<span class="search-highlight">$1</span>',
        $highlightedText
      );
    }
    
    return $highlightedText;
  }
}