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-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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-10_003
// ==/UserScript==
(function() {
  'use strict';

  const manual = `<ul>外部リンク:
  <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><b>UserScriptの説明:</b>
  <li>以下の入力欄に<code>bbs=poverty</code> のように入力すると、該当するログだけを表示します。</li>
  <li>カンマ(<code>,</code>) で区切ることで複数の条件が指定できます。</li>
  <li>URLのハッシュ(<code>#</code>)より後に条件を指定することでも機能します。</li>
  <li>大砲ログの<em>各セルをダブルクリック</em>して、その内容の<em>条件を追加</em>できます。</li>
  <li><em>列ヘッダーをクリック</em>で<em>ソートします。</li>
</ul>`;

  const DONGURI_LOG_CSS = `
    body {
      margin: 0;
      padding: 12px;
      display: block;
    }
    table { white-space: nowrap; }
    thead {
      position: sticky;
      top: 0;
      z-index: 1;
    }
    tfoot {
      position: sticky;
      bottom: 0;
      z-index: 1;
      background: #eee;
      color: #000;
    }
    tfoot td {
      padding: 0 0 .5rem 1.5rem;
    }
    tfoot p {
      margin: 0;
      padding: 0;
    }
    th:not([colspan]):hover, td:not([colspan]):hover { background: #ccc; }
    th:not([colspan]):active, td:not([colspan]):active { background: #ff9; }
    th, td { font-size: 15px; border: 1px solid; }
    th:not([colspan]) { position: relative; }
    th.sortOrder1::after { content: "▲"; }
    th.sortOrder-1::after { content: "▼"; }
    th[class^=sortOrder]::after {
      font-size: 0.5em;
      opacity: 0.5;
      vertical-align: super;
      position: absolute;
      top: 0;
      left: 50%;
      transform: translateX(-50%);
    }
    td a:visited { color: #808; }
    td a:hover { color: #000; }
    #myfilter {
      width: 100%;
    }
    .toggleDisplay {
      position: fixed;
      bottom: 10px;
      right: 30px;
      opacity: 0.7;
    }
  `;

  const READ_CGI_CSS = `
    .dongurihit:target {
      background: #fff;
      color: #000;
    }
    .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.pushState({
            scrollY: window.scrollY
          }, '');
          history.go(-1);
          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 = () => {
    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 = {};
    // Number of attempted requests to lastmodify.txt
    const attemptedXhrBBS = new Set();
    const completedXhrBBS = new Set();
    const columnSelector = {};
    const columns = {
      "order":"順",
      "term":"期",
      "date":"date(投稿時刻)",
      "bbs":"bbs",
      "bbsname":"bbs名",
      "key":"key",
      "id":"ハンターID",
      "hunter":"ハンター名",
      "target":"ターゲット",
      "subject":"subject"
    };
    Object.keys(columns).forEach((key, i) => {
      columnSelector[key] = `td:nth-of-type(${i + 1})`;
    });
    const columnKeys = Object.keys(columns);
    const columnValues = Object.values(columns);
    const originalTermSelector = 'td:nth-of-type(1)';
    const originalLogSelector = 'td:nth-of-type(2)';
    let completedRows = 0;

    const table = $('table');
    if (!table) {
      return false;
    }
    const thead = $('thead', table);
    const tbody = $('tbody', table);
    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');
      }
    });
    table.insertAdjacentElement('afterend', toggleDisplayContainer);

    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 appendCell = (tr, txt = null, id = null) => {
      const e = tr.appendChild(document.createElement(tr.parentElement.tagName === 'THEAD' ? 'th' : 'td'));
      if (txt !== null) {
        e.textContent = txt;
      }
      if (id !== null) {
        e.id = id;
      }
      return e;
    };

    if (!$('tr th:nth-of-type(1)', thead)) {
      return false;
    }
    // 順,期,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 => appendCell(tr, txt).setAttribute('scope', 'col'));

    table.insertAdjacentHTML('beforebegin', manual);
    const headers = $$('th', thead);
    const rows = $$('tr', tbody);

    let sortOrder = 1; // 1: 自然順, -1: 逆順
    let lastIndex = null;
    let lastSortOrder = null;
    const rsortKeys = ['term', 'date', 'key'];
    // 各列ヘッダーにクリックイベントを設定
    headers.forEach((header, index) => {
      header.addEventListener('click', (e) => {
        if (headers[lastIndex] && headers[lastIndex].classList) {
          headers[lastIndex].classList.remove(`sortOrder${lastSortOrder}`);
        }
        if (lastIndex !== index) {
          lastIndex = index;
          sortOrder = rsortKeys.indexOf(columnKeys[index]) === -1 ? 1 : -1;
        }

        lastSortOrder = sortOrder;
        // クリックされた列のインデックスに基づいてソート
        rows.sort((rowA, rowB) => {
          const cellA = rowA.cells[index].textContent;
          const cellB = rowB.cells[index].textContent;

          // テキストで自然順ソート
          return cellA.localeCompare(cellB, 'ja', {
            numeric: true
          }) * sortOrder;
        });
        header.classList.add(`sortOrder${sortOrder}`);

        // ソート順を反転
        sortOrder *= -1;

        // DocumentFragmentを作成
        const fragment = document.createDocumentFragment();
        // ソート済みの行をDocumentFragmentに追加
        rows.forEach(row => fragment.appendChild(row));
        // DocumentFragmentをtbodyに追加
        tbody.appendChild(fragment);
      });
    });

    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()}]`);
    };
    // Regular expression to detect and replace unwanted characters
    const replaceTextRecursively = (element) => {
      element.childNodes.forEach(node => {
        if (node.nodeType === Node.TEXT_NODE) {
          node.textContent = sanitizeText(node.textContent);
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          replaceTextRecursively(node);
        }
      });
    };

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

      // columns
      $(columnSelector.term, row).textContent = $(originalTermSelector, row).textContent;
      $(columnSelector.order, row).textContent = row.dataset.order;
      appendCell(row, addWeekdayToDatetime(date));
      appendCell(row, bbs);
      appendCell(row);
      appendCell(row, key);
      appendCell(row, row.dataset.id);
      appendCell(row, row.dataset.hunter);
      appendCell(row, row.dataset.target);
      appendCell(row);
    });

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

    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 rows = $$('tr', tbody);
      const total = rows.length;
      const value = val.trim();

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

      const criteria = value.split(filterSplitRegex).map(item => item.split('=')).reduce((acc, [key, val]) => {
        if (key && val) {
          acc[key.trim()] = noSanitizeKeys.indexOf(key) > -1 ? val.trim() : sanitize(val.trim());
        }
        return acc;
      }, {});

      rows.forEach(row => {
        const isVisible = Object.entries(criteria).every(([key, val]) => {
          if (key === 'ita') {
            key = 'bbs';
          }
          if (key === 'dat') {
            key = 'key';
          }
          if (!row.hasAttribute(`data-${key}`)) {
            return false;
          }

          if (equalValueKeys.indexOf(key) > -1) {
            return row.getAttribute(`data-${key}`) === val;
          } else if (includesValueKeys.indexOf(key) > -1) {
            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} 件 / ${total} 件中`;
    };

    // Insert the data of each BBS thread list
    const insertCells = (bbs) => {
      for (let obj of donguriLogBbsRows[bbs]) {
        ++completedRows;
        const { index, key } = obj;
        const row = initialRows[index];
        if (Object.hasOwn(row.dataset, 'subject') === true && 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);
      }
    };

    const insertCellsNotCount = (bbs) => {
      for (let obj of donguriLogBbsRows[bbs]) {
        const { index, key } = obj;
        if (Object.hasOwn(subjectList[bbs], key) === false) {
          continue;
        }
        const row = initialRows[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 = initialRows[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, 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', (e) => {
        location.hash = `#${input.value}`;
        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);
      completedXhrBBS.add(bbs);
      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 (Object.hasOwn(subjectList[bbs], key) === true) {
          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);
      completedXhrBBS.add(bbs);
      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');
      }
    };

    // 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`;
        attemptedXhrBBS.add(bbs);
        xhrGetDat(url, addBbsInfo);
      }
      table.addEventListener('dblclick', function(event) {
        event.preventDefault();
        if (!$('#myfilter')) {
          return;
        }
        const target = event.target;
        if (target.tagName === 'TD') {
          const index = Array.prototype.indexOf.call(target.parentNode.children, target);
          const txt = `${columnKeys[index]}=${target.textContent}`;
          location.hash += location.hash.length > 1 ? `,${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 (Object.hasOwn(donguriLogBbsRows, bbs) === false) {
            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}`);
})();