Greasy Fork

Greasy Fork is available in English.

5ch.net donguri Hit Response Getter

Fetches and filters hit responses from donguri 5ch boards

当前为 2024-06-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        5ch.net donguri Hit Response Getter
// @namespace   http://greasyfork.icu/users/1310758
// @description Fetches and filters hit responses from donguri 5ch boards
// @match       *://donguri.5ch.net/cannonlogs
// @match       *://*.5ch.net/test/read.cgi/*/*
// @connect     5ch.net
// @license     MIT License
// @author      pachimonta
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @version     2024-06-14_003
// ==/UserScript==
(function() {
  'use strict';

  const manual = `<ul class="external-link">外部リンク:
  <li><a href="https://nanjya.net/donguri/" target="_blank">5chどんぐりシステムの備忘録</a> どんぐりシステムに関する詳細。</li>
  <li><a href="http://dongurirank.starfree.jp/" target="_blank">どんぐりランキング置き場</a> 大砲ログの過去ログ検索やログ統計など。</li>
</ul>
<ul class="userscript-manual"><b>UserScriptの説明:</b>
  <li>テーブル下部の<label for="myfilter">入力欄</label>に<code>bbs=poverty</code> のように入力すると、該当するログだけを表示します。</li>
  <li>カンマ(<code>,</code>) で区切ることで複数の条件が指定できます。</li>
  <li>URLのハッシュ(<code>#</code>)より後に条件を指定することでも機能します。</li>
  <li>大砲ログの各セルをダブルクリックして、その内容の条件を追加できます。</li>
  <li>列ヘッダーをクリックでソートします。</li>
</ul>`;

  const DONGURI_LOG_CSS = `
    :root {
      --acorn-background: #f6f6f6;
      --acorn-color: #000;
      --acorn-header-background: #f5f7ff;
      --acorn-header-color: #000;
      --myfilter-placeholder-shown-background: #fff;
      --myfilter-background: #cff;
      --myfilter-color: #000;
      --acorn-tfoot-background: #eee;
      --acorn-tfoot-color: #000;
      --acorn-td-hover-background: #ccc;
      --acorn-td-active-background: #ff9;
      --acorn-td-a-visited: #808;
      --acorn-td-a-hover: #000;
      --acorn-th-background: #f5f7ff;
      --acorn-td-background: #fff;
      --acorn-a-color: #0d47a1;
      --acorn-td-border-left-color: #ccc;
      --acorn-td-likely-hit-background: #ffe4e1;
      --acorn-code-color: #d81b60;
    }
    @media (prefers-color-scheme: dark) {
      :root {
        --acorn-background: #000;
        --acorn-color: #eee;
        --acorn-header-background: #2b2b2b;
        --acorn-header-color: #fff;
        --myfilter-placeholder-shown-background: #000;
        --myfilter-background: #033;
        --myfilter-color: #fff;
        --acorn-tfoot-background: #222;
        --acorn-tfoot-color: #fff;
        --acorn-td-hover-background: #777;
        --acorn-td-active-background: #bb3;
        --acorn-td-a-visited: #c3c;
        --acorn-td-a-hover: #ccc;
        --acorn-th-background: #2b2b2b;
        --acorn-td-background: #000;
        --acorn-a-color: #ffb300;
        --acorn-td-border-left-color: #333;
        --acorn-td-likely-hit-background: #002b3e;
        --acorn-code-color: #f06292
      }
    }
    body {
      margin: 0;
      padding: 8px;
      display: block;
      background: var(--acorn-background) !important;
      color: var(--acorn-color);
    }
    header {
      background: var(--acorn-header-background) !important;
      color: var(--acorn-header-color) !important;
    }
    a { color: var(--acorn-a-color); }
    table {
      border-collapse: separate;
      border-spacing: 0;
      white-space: nowrap;
      table-layout: fixed;
    }
    thead tr th:nth-of-type(-n+9), tbody tr td:nth-of-type(-n+9)  {
      width: auto;
    }
    thead tr th:nth-last-of-type(-n+1), tbody tr td:nth-last-of-type(-n+1) {
      width: 100%;
    }
    table, th, td {
      border: 1px solid var(--acorn-color);
      font-size: 15px;
    }
    thead {
      position: sticky;
      top: 0;
      z-index: 1;
    }
    tfoot {
      position: sticky;
      bottom: 0;
      z-index: 2;
      background: var(--acorn-tfoot-background);
      color: var(--acorn-tfoot-color);
    }
    tfoot p {
      margin: 0;
      padding: 0;
    }
    tr th:first-of-type, tr td:first-of-type {
      border-left-width: 0.33rem;
      border-left-color: var(--acorn-td-border-left-color);
    }
    th { background: var(--acorn-th-background) !important; }
    th:hover, td:not([colspan]):hover { background: var(--acorn-td-hover-background); }
    th:active, td:not([colspan]):active { background: var(--acorn-td-active-background); }
    tbody td { background: var(--acorn-td-background); }
    td.likely-hit { background: var(--acorn-td-likely-hit-background); }
    th { position: relative; }
    th.sortOrder1::after { content: "▲"; }
    th.sortOrder-1::after { content: "▼"; }
    th[class^=sortOrder]::after {
      font-size: 0.5rem;
      opacity: 0.5;
      vertical-align: super;
      position: absolute;
      top: 0;
      left: 50%;
      transform: translateX(-50%);
    }
    td a:visited { color: var(--acorn-td-a-visited); }
    td a:hover { color: var(--acorn-td-a-hover); }
    th, td {
      border-top-width: 0;
      border-left-width: 0;
    }
    th:last-child, td:last-child {
      border-right-width: 0;
    }
    tr:last-child td {
      border-bottom-width: 0;
    }
    label {
      display: inline-block;
      text-decoration: underline;
      cursor: pointer;
    }
    label:hover {
      text-decoration: none;
    }
    #myfilter {
      width: calc(100vw - 4rem);
      background: var(--myfilter-background);
      color: var(--myfilter-color);
    }
    #myfilter:placeholder-shown {
      background: var(--myfilter-placeholder-shown-background);
    }
    ul.external-link, ul.userscript-manual {
      font-size: .8rem;
    }
    code {
       color: var(--acorn-code-color);
    }
    .toggleDisplay {
      position: fixed;
      top: 40%;
      right: 30px;
      opacity: 0.7;
      background: var(--acorn-tfoot-background);
      font-size: .8rem;
      z-index: 2;
    }
    .toggleDisplay:hover {
      background: var(--acorn-td-hover-background);
      opacity: inherit;
    }
    .progress {
      cursor: progress;
    }
  `;

  const READ_CGI_CSS = `
    .dongurihit:target, .dongurihit:target * {
      background: #fff;
      color: #000;
    }
  `;

  // Helper functions
  const $ = (selector, context = document) => context.querySelector(selector);
  const $$ = (selector, context = document) => [...context.querySelectorAll(selector)];

  // Scroll and highlight the relevant post in read.cgi
  const readCgiJump = () => {
    GM_addStyle(READ_CGI_CSS);
    const waitForTabToBecomeActive = () => {
      return new Promise((resolve) => {
        if (document.visibilityState === 'visible') {
          resolve();
        } else {
          const handleVisibilityChange = () => {
            if (document.visibilityState === 'visible') {
              document.removeEventListener('visibilitychange', handleVisibilityChange);
              resolve();
            }
          };
          document.addEventListener('visibilitychange', handleVisibilityChange);
        }
      });
    };
    const scrollActive = () => {
      const hashIsNumber = location.hash.match(/^#(\d+)$/) ? location.hash.substring(1) : null;
      const [dateymd, datehms] = (location.hash.match(/#date=([^&=]{10})([^&=]+)/) || [null, null, null]).slice(1);

      if (!hashIsNumber && !dateymd) {
        return;
      }

      $$('.date').some(dateElement => {
        const post = dateElement.closest('.post');
        if (!post) {
          return false;
        }

        const isMatchingPost = post.id === hashIsNumber || (dateymd && dateElement.textContent.includes(dateymd) && dateElement.textContent.includes(datehms));
        if (!isMatchingPost) {
          return false;
        }

        post.classList.add('dongurihit');
        if (post.id && location.hash !== `#${post.id}`) {
          location.hash = `#${post.id}`;
          history.replaceState(null, '', location.href);
          return true;
        }

        const observer = new IntersectionObserver(entries => {
          waitForTabToBecomeActive().then(() => {
            entries.forEach(entry => {
              if (entry.isIntersecting) {
                setTimeout(() => post.classList.remove('dongurihit'), 1500);
              }
            });
          });
        });
        observer.observe(post);
        return true;
      });
    };

    if (!window.donguriInitialized) {
      window.addEventListener('hashchange', scrollActive);
      window.donguriInitialized = true;
    }
    const scrollToElementWhenActive = () => {
      waitForTabToBecomeActive().then(() => {
        scrollActive();
      });
    };
    scrollToElementWhenActive();
    return;
  };

  // Filter Acorn Cannon Logs
  const donguriFilter = () => {
    console.time(location.href);
    $('body').removeAttribute('style');
    $('header').removeAttribute('style');
    GM_addStyle(DONGURI_LOG_CSS);

    // Storage for bbs list and post titles list
    const bbsOriginList = {};
    const bbsNameList = {};
    // post titles
    const subjectList = {};
    // Index list of tbody tr selectors for each BBS
    const donguriLogBbsRows = {};

    const columnSelector = {};
    const columns = {
      "order":"順",
      "term":"期",
      "date":"date(投稿時刻)",
      "bbs":"bbs",
      "bbsname":"bbs名",
      "key":"key",
      "id":"ハンターID",
      "hunter":"ハンター名",
      "target":"ターゲット",
      "subject":"subject"
    };
    const columnKeys = Object.keys(columns);
    const columnValues = Object.values(columns);
    columnKeys.forEach((key, i) => {
      columnSelector[key] = `td:nth-of-type(${i + 1})`;
    });
    const originalTermSelector = 'td:nth-of-type(1)';
    const originalLogSelector = 'td:nth-of-type(2)';
    let completedRows = 0;

    let lastFilteringCriteria = {};

    const table = $('table');
    if (!table) {
      return false;
    }
    const thead = $('thead', table);
    let tbody = $('tbody', table);
    $$('th', thead).forEach(header => {
      header.removeAttribute('style');
    });
    const originalTable = Object.assign(table.cloneNode(true), {
      className: 'originalLog'
    });

    // Create a checkbox to toggle the display between the original table and the UserScript-generated table
    const toggleDisplayCheckbox = Object.assign(document.createElement('input'), {
      type: 'checkbox',
      checked: 'checked',
      id: 'toggleDisplay'
    });
    const toggleDisplayLabel = Object.assign(document.createElement('label'), {
      htmlFor: 'toggleDisplay',
      textContent: 'Toggle Table'
    });
    const toggleDisplayContainer = Object.assign(document.createElement('div'), {
      className: 'toggleDisplay'
    });
    toggleDisplayContainer.append(toggleDisplayCheckbox, toggleDisplayLabel);

    // Switch between original and UserScript display depending on checkbox state
    toggleDisplayCheckbox.addEventListener('change', (event) => {
      if (event.target.checked) {
        // Change display to UserScript
        $('table.originalLog').setAttribute('hidden', 'hidden');
        table.removeAttribute('hidden');
      } else {
        // Change to original display
        table.setAttribute('hidden', 'hidden');
        if (!$('table.originalLog')) {
          table.insertAdjacentElement('afterend', originalTable);
        }
        $('table.originalLog').removeAttribute('hidden');
      }
    });

    const addWeekdayToDatetime = (datetimeStr) => {
      const firstColonIndex = datetimeStr.indexOf(':');
      const splitIndex = firstColonIndex - 2;
      const datePart = datetimeStr.slice(0, splitIndex);
      const timePart = datetimeStr.slice(splitIndex);
      const [year, month, day] = datePart.split('/').map(Number);
      const date = new Date(year, month - 1, day);
      const weekdays = ['日', '月', '火', '水', '木', '金', '土'];
      const weekday = weekdays[date.getDay()];
      return `${datePart}(${weekday}) ${timePart}`;
    };

    const appendThCell = (tr, txt = null) => {
      const e = tr.appendChild(document.createElement('th'));
      if (txt !== null) {
        e.textContent = txt;
      }
      return e;
    };
    const appendTdCell = (tr, txt = null) => {
      const e = tr.appendChild(document.createElement('td'));
      if (txt !== null) {
        e.textContent = txt;
      }
      return e;
    };
    if (!$('tr th:nth-of-type(1)', thead)) {
      return false;
    }
    const colgroup = Object.assign(document.createElement('colgroup'), {
      span: columnKeys.length.toString()
    });
    thead.insertAdjacentElement('beforebegin', colgroup);
    // 順,期,date(投稿時刻),bbs,bbs名,key,ハンターID,ハンター名,ターゲット,subject
    // order,term,date,bbs,bbsname,key,id,hunter,target,subject
    const tr = $('tr:nth-of-type(1)', thead);
    columnValues.slice(0, 2).forEach((txt, i) => {
      const th = $(`th:nth-of-type(${i + 1})`, tr);
      th.textContent = txt;
      th.setAttribute('scope', 'col');
      th.removeAttribute('style');
    });
    columnValues.slice(2).forEach(txt => appendThCell(tr, txt).setAttribute('scope', 'col'));

    table.insertAdjacentHTML('beforebegin', manual);
    const additionalManual = Object.assign(document.createElement('li'), {
      textContent: `各パラメータ名: ${JSON.stringify(columns).replace(/"/g,'').replace(/,/g,', ')}`
    });
    $('.userscript-manual li:first-of-type').insertAdjacentElement('afterend', additionalManual);
    table.insertAdjacentElement('beforebegin', toggleDisplayContainer);

    const rloRegex = /[\x00-\x1F\x7F\u200E\u200F\u202A\u202B\u202C\u202D\u202E]/g;

    const sanitizeText = (content) => {
      return content.replace(rloRegex, match => `[U+${match.codePointAt(0).toString(16).toUpperCase()}]`);
    };

    // date format "2024/06/1110:37:32.24"
    const japanDate2UnixTimeStr = (jpdate) => {
      const lastDashIndex = jpdate.lastIndexOf('/');
      return Date.parse(jpdate.replace(/\//g,'-').slice(0,lastDashIndex + 3) + 'T' + jpdate.slice(lastDashIndex + 3) + '+09:00').toString().substring(0,10);
    };

    const sanitize = (value) => value.replace(/[^a-zA-Z0-9_:/.\-]/g, '');

    let rows = $$('tr', tbody);
    // Number of 'tbody tr' selectors
    const rowCount = rows.length;
    const userLogRegex = /^(.*)?さん\[([a-f0-9]{8})\]は(.*(?:\[[a-f0-9]{4}\*\*\*\*\])?)?さんを(?:[撃打]っ|外し)た$/u;
    console.time('initialRows');
    const fragment = document.createDocumentFragment();
    const tbodyFragment = document.createElement('tbody');
    fragment.appendChild(tbodyFragment);
    // Expand each cell in the tbody
    rows.forEach((row, i) => {
      const newRow = document.createElement('tr');
      newRow.innerHTML = '<td></td>'.repeat(2);
      const log = sanitizeText($(originalLogSelector, row).textContent.trim());
      const verticalPos = log.lastIndexOf('|');
      const [bbs, key, date] = log.slice(verticalPos + 2).split(' ', 3);
      if (!(bbs in donguriLogBbsRows)) {
        donguriLogBbsRows[bbs] = [{index:i,key:key}];
      } else {
        donguriLogBbsRows[bbs].push({index:i,key:key});
      }
      newRow.dataset.order = i + 1;
      newRow.dataset.term = sanitize($(originalTermSelector, row).textContent);
      Object.assign(newRow.dataset, {
        date,
        bbs,
        key,
        log
      });
      [newRow.dataset.hunter, newRow.dataset.id, newRow.dataset.target] = log.slice(0, verticalPos - 1).match(userLogRegex).slice(1, 4);

      // columns
      $(columnSelector.term, newRow).textContent = $(originalTermSelector, row).textContent;
      $(columnSelector.order, newRow).textContent = newRow.dataset.order;
      appendTdCell(newRow, addWeekdayToDatetime(date));
      appendTdCell(newRow, bbs);
      appendTdCell(newRow);
      appendTdCell(newRow, key);
      appendTdCell(newRow, newRow.dataset.id);
      appendTdCell(newRow, newRow.dataset.hunter);
      appendTdCell(newRow, newRow.dataset.target);
      appendTdCell(newRow);
      if (japanDate2UnixTimeStr(date) === key) {
        $(columnSelector.subject, newRow).classList.add('likely-hit');
      }
      tbodyFragment.appendChild(newRow);
    });
    table.replaceChild(fragment, tbody);
    console.timeEnd('initialRows');
    tbody = tbodyFragment;
    rows = $$('tr', tbody);
    const headers = $$('th', thead);

    let sortOrder = -1; // 1: Ascending order, -1: Descending order
    let lastIndex = null;
    const rsortKeys = ['term', 'date', 'key'];
    // Set click event for each column header
    headers.forEach((header, index) => {
      header.addEventListener('click', () => {
        if (table.classList.contains('progress') || completedRows !== rowCount) {
          return false;
        }
        table.classList.add('progress');
        if (lastIndex !== null) {
          headers[lastIndex].classList.remove(`sortOrder${sortOrder}`);
        }
        // Reverse the sort order
        sortOrder *= -1;
        if (lastIndex !== index) {
          lastIndex = index;
          sortOrder = !rsortKeys.includes(columnKeys[index]) ? 1 : -1;
        }
        header.classList.add(`sortOrder${sortOrder}`);
        // Sort based on the index of the clicked column
        rows.sort((rowA, rowB) => {
          const cellA = rowA.cells[index].textContent;
          const cellB = rowB.cells[index].textContent;

          // Natural order sort by text
          return cellA.localeCompare(cellB, 'ja', {
            numeric: true
          }) * sortOrder;
        });

        // Create a DocumentFragment
        const fragment = document.createDocumentFragment();
        // Add sorted rows to the DocumentFragment
        rows.forEach(row => fragment.appendChild(row));
        // Append the DocumentFragment to tbody
        tbody.appendChild(fragment);
        table.classList.remove('progress');
      });
    });
    const isEqual = (obj1, obj2) => {
      // true if both are the same object or both are null
      if (obj1 === obj2) { return true; }
      // If either is null, false
      if (obj1 === null || obj2 === null) { return false; }
      // false if types are different
      if (typeof obj1 !== typeof obj2) { return false; }
      // Comparison for primitive types
      if (typeof obj1 !== 'object') { return obj1 === obj2; }

      // From here, it is guaranteed that both obj1 and obj2 are objects
      const keys1 = Object.keys(obj1);
      const keys2 = Object.keys(obj2);
      if (keys1.length !== keys2.length) { return false; }

      // Sorting keys to improve comparison efficiency
      keys1.sort();
      keys2.sort();

      // Loop to compare keys
      for (let i = 0; i < keys1.length; i++) {
        if (keys1[i] !== keys2[i]) { return false; }
        if (!isEqual(obj1[keys1[i]], obj2[keys2[i]])) { return false; }
      }

      return true;
    };
    const str2Regex = (str) => {
      try {
        const match = str.match(/^\/(.*)\/([a-z]*)$/);
        if (match.length) {
          return new RegExp(match[1], match[2]);
        }
      } catch (e) {
        return false;
      }
      return false;
    };

    const filterSplitRegex = /\s*,\s*/;
    const noSanitizeKeys = ['log','bbsname','hunter','target','subject'];
    const equalValueKeys = ['term','bbs'];
    const includesValueKeys = ['log','bbsname','subject','date'];

    // Update elements visibility based on filtering criteria
    const filterRows = (val = '') => {
      let count = 0;
      const value = val.trim();

      if (value.length === 0) {
        rows.forEach(row => row.removeAttribute('hidden'));
        $('#myfilterResult').textContent = `${rowCount} 件 / ${rowCount} 件中`;
        lastFilteringCriteria = {};
        return;
      }

      const criteria = value.split(filterSplitRegex).map(item => item.split('=')).reduce((acc, [key, val]) => {
        if (!columnKeys.includes(key) || !val) { return acc; }
        const regexResult = str2Regex(val);
        if (typeof(regexResult) === 'object') {
          acc[key] = regexResult;
          return acc;
        }
        acc[key] = noSanitizeKeys.includes(key) ? val : sanitize(val);
        return acc;
      }, {});

      if (isEqual(lastFilteringCriteria,criteria)) {
        return;
      }
      lastFilteringCriteria = criteria;

      if (criteria.length === 0) {
        rows.forEach(row => row.removeAttribute('hidden'));
        $('#myfilterResult').textContent = `${rowCount} 件 / ${rowCount} 件中`;
        return;
      }

      rows.forEach(row => {
        const isVisible = Object.entries(criteria).every(([key, val]) => {
          if (typeof(val) === 'object') {
            return val.test(row.getAttribute(`data-${key}`));
          } else if (equalValueKeys.includes(key)) {
            return row.getAttribute(`data-${key}`) === val;
          } else if (includesValueKeys.includes(key)) {
            return row.getAttribute(`data-${key}`).includes(val);
          } else {
            return row.getAttribute(`data-${key}`).indexOf(val) === 0;
          }
        });

        if (isVisible) {
          count++;
          row.removeAttribute('hidden');
        } else {
          row.setAttribute('hidden', 'hidden');
        }
      });

      $('#myfilterResult').textContent = `${count} 件 / ${rowCount} 件中`;
    };

    // Insert the data of each BBS thread list
    const insertCells = (bbs) => {
      completedRows += donguriLogBbsRows[bbs].length;
      for (let obj of donguriLogBbsRows[bbs]) {
        const { index, key } = obj;
        const row = rows[index];
        if ('subject' in row.dataset && row.dataset.subject.length) {
          continue;
        }
        const { date, origin } = row.dataset;
        const subject = subjectList[bbs][key] || "???";
        Object.assign(row.dataset, { subject });
        const anchor = Object.assign(document.createElement('a'), {
          href: `${origin}/test/read.cgi/${bbs}/${key}/?v=pc#date=${date}`,
          target: '_blank',
          textContent: subject
        });
        $(columnSelector.subject, row).appendChild(anchor);
      }
      // After inserting all cells
      if (completedRows === rowCount) {
        filterRows($('#myfilter').value);
        console.timeEnd(location.href);
      }
    };

    const insertCellsNotCount = (bbs) => {
      for (let obj of donguriLogBbsRows[bbs]) {
        const { index, key } = obj;
        if (!(key in subjectList[bbs])) {
          continue;
        }
        const row = rows[index];
        const { date, origin } = row.dataset;
        const subject = subjectList[bbs][key];
        Object.assign(row.dataset, { subject });
        const anchor = Object.assign(document.createElement('a'), {
          href: `${origin}/test/read.cgi/${bbs}/${key}/?v=pc#date=${date}`,
          target: '_blank',
          textContent: subject
        });
        $(columnSelector.subject, row).appendChild(anchor);
      }
    };

    const insertBbsnameCells = (bbs) => {
      for (let obj of donguriLogBbsRows[bbs]) {
        const { index } = obj;
        const row = rows[index];
        const origin = bbsOriginList[bbs] || "https://origin";
        const bbsName = bbsNameList[bbs] || "???";
        Object.assign(row.dataset, {
          origin,
          bbsname: bbsName
        });
        $(columnSelector.bbsname, row).textContent = bbsName;
      }
    };

    const tfootHtml = `
      <tfoot>
        <tr>
          <td colspan="${columnKeys.length}">
            <p id="myfilterResult"></p>
            <input
              type="text"
              size="40"
              id="myfilter"
              placeholder="Filter (e.g., bbs=av, key=1711038453, date=06/01(土) 01:55, id=ac351e30, subject=/\p{EPres}/v, log=abesoriさん[97a65812])"
            >
          </td>
        </tr>
      </tfoot>
    `;

    // Initialize the filter input and its functionalities
    const createFilterInput = () => {
      table.insertAdjacentHTML('beforeend', tfootHtml);

      const input = $('#myfilter');

      input.addEventListener('input', () => {
        if (input.value.length) {
          location.hash = `#${input.value}`;
        } else {
          // Prevent page navigation in the case of "#" only
          history.replaceState(null, '', location.pathname);
          filterRows();
        }
        return;
      });

      if (location.hash) {
        input.value = decodeURIComponent(location.hash.substring(1));
      }

      window.addEventListener('hashchange', () => {
        input.value = decodeURIComponent(location.hash.substring(1));
        filterRows(input.value);
      });
    };

    // GM_xmlhttpRequest wrapper to handle HTTP Get requests
    const xhrGetDat = (url, loadFunc, mime = 'text/plain; charset=shift_jis') => {
      console.time(url);
      GM_xmlhttpRequest({
        method: 'GET',
        url: url,
        timeout: 3600 * 1000,
        overrideMimeType: mime,
        onload: response => loadFunc(response),
        onerror: error => console.error('An error occurred during the request:', error)
      });
    };

    const arrayContainsArray = (superset, subset) => {
      return subset.every(value => superset.includes(value));
    };
    const arrayDifference = (array1, array2) => {
      return array1.filter(value => !array2.includes(value));
    };

    const parser = new DOMParser();
    const htmlEntityRegex = /&#?[a-zA-Z0-9]+;?/;
    const crlfRegex = /[\r\n]+/;
    const logSplitRegex = /\s*<>\s*/;

    // Process post titles line to update subjectList and modify the table-cells
    const addBbsPastInfo = (response) => {
      console.timeEnd(response.finalUrl);
      if (response.status !== 200) {
        console.error('Failed to load data. Status code:', response.status);
        return false;
      }

      const url = response.finalUrl;
      const pathname = new URL(url).pathname;
      const slashIndex = pathname.indexOf('/');
      const secondSlashIndex = pathname.indexOf('/', slashIndex+1);
      const bbs = pathname.substring(slashIndex+1,secondSlashIndex);
      const html = parser.parseFromString(response.responseText, 'text/html').documentElement;
      $$('[class="main_odd"],[class="main_even"]', html).forEach(p => {
        let [key, subject] = [ $('.filename', p).textContent, $('.title', p).textContent ];
        if (key.includes('.')) { key = key.substring(0, key.lastIndexOf('.')); }
        if (key in subjectList[bbs]) {
          return;
        }
        subjectList[bbs][key] = subject;
      });
      if (arrayContainsArray(Object.keys(subjectList[bbs]), [...new Set(donguriLogBbsRows[bbs].map(item => item.key))]) === false) {
        console.info('Subject not found. {"bbs":"%s","key":"%s"}', bbs, arrayDifference([...new Set(donguriLogBbsRows[bbs].map(item => item.key))], Object.keys(subjectList[bbs])));
      }
      insertCells(bbs);
    };

    // Process post titles line to update subjectList and modify the table-cells
    const addBbsInfo = (response) => {
      console.timeEnd(response.finalUrl);
      if (response.status !== 200) {
        console.error('Failed to load data. Status code:', response.status);
        return false;
      }

      const url = response.finalUrl;
      const lastSlashIndex = url.lastIndexOf('/');
      const secondLastSlashIndex = url.lastIndexOf('/', lastSlashIndex - 1);
      const bbs = url.substring(secondLastSlashIndex + 1, lastSlashIndex);

      const lastmodify = response.responseText;
      subjectList[bbs] = {};
      lastmodify.split(crlfRegex).forEach(line => {
        let [key, subject] = line.split(logSplitRegex, 2);
        if (key.includes('.')) { key = key.substring(0, key.lastIndexOf('.')); }
        if (htmlEntityRegex.test(subject)) {
          subject = parser.parseFromString(subject, 'text/html').documentElement.textContent;
        }
        subjectList[bbs][key] = subject;
      });
      // All subjects corresponding to the keys in the cell were confirmed
      if (arrayContainsArray(Object.keys(subjectList[bbs]), [...new Set(donguriLogBbsRows[bbs].map(item => item.key))])) {
        insertCells(bbs);
      } else {
        insertCellsNotCount(bbs);
        // Check past log
        xhrGetDat(new URL("./kako/", url), addBbsPastInfo, 'text/plain; charset=utf-8');
      }
    };

    const escapeRegExp = (string) => {
      // A pattern for escaping regular expression metacharacters
      const metaCharacters = /[.*+?^${}()|[\]\\]/g;

      // Escaping metacharacters
      return string.replace(metaCharacters, match => '\\' + match);
    };

    // Function to process post titles by XHRing lastmodify.txt from the BBS list in the donguri log table
    const xhrBbsInfoFromDonguriRows = () => {
      for (let bbs of Object.keys(donguriLogBbsRows)) {
        const url = `${bbsOriginList[bbs]}/${bbs}/lastmodify.txt`;
        xhrGetDat(url, addBbsInfo);
      }
      tbody.addEventListener('dblclick', function(event) {
        event.preventDefault();
        const target = event.target;
        if (!$('#myfilter') || target.tagName !== 'TD') {
          return;
        }
        let targetTxt = target.textContent.trim();
        if (str2Regex(targetTxt) !== false) {
          targetTxt = escapeRegExp(targetTxt);
        }
        const index = Array.prototype.indexOf.call(target.parentNode.children, target);
        const txt = `${columnKeys[index]}=${targetTxt}`;
        location.hash += (location.hash.length > 1 && location.hash.endsWith(',') === false) ? `,${txt}` : txt;
      });
    };

    const bbsLinkRegex = /\.5ch\.net\/([a-zA-Z0-9_-]+)\/$/;

    // Function to process the bbsmenu response
    const bbsmenuFunc = (response) => {
      console.timeEnd(response.finalUrl);
      if (response.status !== 200) {
        console.error('Failed to fetch bbsmenu. Status code:', response.status);
        return false;
      }
      const html = parser.parseFromString(response.responseText, 'text/html').documentElement;
      for (let bbsLink of $$('a[href*=".5ch.net/"]', html)) {
        const match = bbsLink.href.match(bbsLinkRegex);
        if (match) {
          const bbs = match[1];
          if (!(bbs in donguriLogBbsRows)) {
            continue;
          }
          bbsOriginList[bbs] = new URL(bbsLink.href).origin;
          bbsNameList[bbs] = bbsLink.textContent.trim();
        }
      }
      if (Object.keys(bbsOriginList).length === 0) {
        console.error('No boards found.');
        return;
      }
      for (let bbs of Object.keys(donguriLogBbsRows)) {
        insertBbsnameCells(bbs);
      }
      xhrBbsInfoFromDonguriRows();
    };
    createFilterInput();
    // Initial data fetch from bbsmenu
    xhrGetDat('https://menu.5ch.net/bbsmenu.html', bbsmenuFunc);
  };

  const processMap = {
    donguriLog: {
      regex: /^https?:\/\/donguri\.5ch\.net\/cannonlogs$/,
      handler: donguriFilter
    },
    readCgi: {
      regex: /^https?:\/\/[a-z0-9]+\.5ch\.net\/test\/read\.cgi\/\w+\/\d+.*$/,
      handler: readCgiJump
    }
  };
  const processBasedOnUrl = (url) => {
    for (const key in processMap) {
      if (processMap[key].regex.test(url)) {
        processMap[key].handler();
        break;
      }
    }
  };
  processBasedOnUrl(`${location.origin}${location.pathname}`);
})();