// ==UserScript==
// @name:ja 5ch.net どんぐり大砲ログのスレタイ収集&フィルタリング
// @name 5ch.net Donguri Cannon Log Analyzer
// @namespace https://gf.qytechs.cn/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
// @version 2025.03.21.001
// ==/UserScript==
(function() {
'use strict';
const toggleDisplayText = 'Toggle Table';
const manual = String.raw`<h2 class="userscript-title">リンク</h2>
<div class="external-link">
<p><a rel="noreferrer noopener" href="https://web.archive.org/web/20240711172430/https://nanjya.net/donguri/" target="_blank">5chどんぐりシステムの備忘録</a> どんぐりシステムに関する詳細。</p>
<p><a rel="noreferrer noopener" href="https://donguridepot.stars.ne.jp/" target="_blank">どんぐりランキング置き場</a> 大砲ログの過去ログ検索やログ統計など。</p>
<p><a rel="noreferrer noopener" href="https://kes.5ch.net/donguri/" target="_blank">どんぐり板</a> どんぐり</p>
</div>
<h2 class="userscript-title">UserScriptの説明</h2>
<div class="userscript-manual">
<p class="userscript-manual-summary">どんぐり大砲を撃たれたレスのスレッドタイトルを取得し表示をします。スレッドタイトルをクリックすると撃たれたレスにジャンプします。各メタデータでフィルタリング、ソート可能にします。</p>
<div class="userscript-manual-section">
フィルターとソートについて。
<p>テーブル下部の<label for="myfilter">入力欄</label>に<code>bbs=newsplus</code> のように入力すると該当するログだけを表示します。</p>
<p>カンマ(<code>,</code>) で区切ることで複数の条件が指定できます。</p>
<p>URLのハッシュ(<code>#</code>)より後に条件を指定することでも機能します。</p>
<p>大砲ログの<b>各セルをダブルクリック</b>して、その内容の条件を追加できます(再度ダブルクリックで削除)。</p>
<p><b>列ヘッダーをクリック</b>でソートします。</p>
</div>
<p>itest.5ch.net表示だとレスにジャンプするのに遅延が発生するため<strong>PC表示させるためのCookieを設定します。</strong>itest.5ch.net表示させたい場合は<a rel="noreferrer noopener" href="https://itest.5ch.net/" target="_blank">itest.5ch.netトップページ</a>の設定から「常にPC版で表示」をOFFに変えてください。</p>
<p>右側の<b>${toggleDisplayText}</b> ボタンを押すと元のテーブルと切り替えます。</p>
</div>`;
const DONGURI_LOG_CSS = String.raw`
: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;
}
:where(thead, tbody) tr :where(th, td):nth-of-type(-n+9) {
width: auto;
}
:where(thead, tbody) tr :where(th, 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 :where(th:first-of-type, 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;
}
:where(th, 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);
}
.userscript-title {
font-size: .8rem;
line-height: 1;
margin: 0;
padding: 0;
}
:where(.external-link, .userscript-manual) {
font-size: .8rem;
margin: .4rem 0 .4rem 0;
& p, & div {
padding: 0;
margin: 0 0 0 1.5rem;
display: list-item;
list-style-type: disc;
list-style-position: inside;
}
}
code {
color: var(--acorn-code-color);
}
.toggleDisplay {
position: fixed;
top: 40%;
right: 30px;
opacity: 0.8;
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 = String.raw`
.dongurihit:target, .dongurihit:target * {
background: #fff;
color: #000;
}
`;
// Helper functions
const $ = (selector, context = document) => context.querySelector(selector);
const $$ = (selector, context = document) => Array.from(context.querySelectorAll(selector));
const addStyle = (css) => {
const style = document.createElement('style');
style.textContent = css;
document.head.append(style);
};
// Scroll and highlight the relevant post in read.cgi
const readCgiJump = () => {
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;
}
const isMatchingPost = post.id === hashIsNumber || (dateymd && dateElement.textContent.includes(dateymd) && dateElement.textContent.includes(datehms));
if (!isMatchingPost) {
return;
}
post.classList.add('dongurihit');
if (post.id && location.hash !== `#${post.id}`) {
history.replaceState(null, '', location.href.slice(0, -location.hash.length));
location.hash = `#${post.id}`;
history.replaceState(null, '', location.href);
return;
}
const observer = new IntersectionObserver(entries => {
waitForTabToBecomeActive().then(() => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setTimeout(() => post.classList.remove('dongurihit'), 1500);
}
});
});
});
observer.observe(post);
return;
});
};
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.origin}${location.pathname}`);
// $('html').toggleAttribute('hidden');
document.cookie = '5chClassic=on; domain=.5ch.net; path=/; SameSite=Lax;';
$('body').removeAttribute('style');
$('header').removeAttribute('style');
addStyle(DONGURI_LOG_CSS);
// Storage for bbs list and post titles list
const bbsOriginList = new Map();
const bbsNameList = new Map();
// post titles
const subjectList = new Map();
// Index list of tbody tr selectors for each BBS
const donguriLogBbsRows = new Map();
// Thread keys for each BBS in the table
const donguriLogBbsKeys = new Map();
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;
}
const thead = $('thead', table);
let tbody = $('tbody', table);
$$('th', thead).forEach(header => {
header.removeAttribute('style');
});
const originalTable = table.cloneNode(true);
originalTable.className = 'originalLog';
// Create a element to toggle the display between the original table and the UserScript-generated table
const toggleDisplay = document.createElement('div');
toggleDisplay.textContent = toggleDisplayText;
toggleDisplay.className = 'toggleDisplay';
// Switch between original and UserScript display depending on table state
toggleDisplay.addEventListener('click', () => {
if ($('table.originalLog') && $('table.originalLog').hasAttribute('hidden') === false) {
$('table.originalLog').toggleAttribute('hidden', true);
table.removeAttribute('hidden');
} else {
table.toggleAttribute('hidden', true);
if (!$('table.originalLog')) {
table.after(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) => {
const e = tr.appendChild(document.createElement('th'));
e.textContent = txt;
return e;
};
const appendTdCell = (tr, txt = '') => {
const e = tr.appendChild(document.createElement('td'));
if (txt !== '') {
e.textContent = txt;
}
return e;
};
if (!$('tr th:nth-of-type(1)', thead)) {
return;
}
const colgroup = document.createElement('colgroup');
colgroup.span = String(columnKeys.length);
thead.before(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 = document.createElement('p');
additionalManual.textContent = `各メタデータ名: ${JSON.stringify(columns).replace(/"/g,'').replace(/,/g,', ')}`;
$('.userscript-manual-section:first-of-type').after(additionalManual);
table.before(toggleDisplay);
const sanitizeText = (content) => {
return content.replace(/[\u0000-\u001F\u007F\u200E\u200F\u202A\u202B\u202C\u202D\u202E]/gu, 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(new RegExp('/', '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.append(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 (!donguriLogBbsRows.has(bbs)) {
donguriLogBbsRows.set(bbs, new Map());
}
if (!donguriLogBbsKeys.has(bbs)) {
donguriLogBbsKeys.set(bbs, new Set());
}
donguriLogBbsRows.get(bbs).set(i, key);
donguriLogBbsKeys.get(bbs).add(key);
newRow.dataset.order = i + 1;
newRow.dataset.term = sanitize($(originalTermSelector, row).textContent);
newRow.dataset.date = date;
newRow.dataset.bbs = bbs;
newRow.dataset.key = key;
newRow.dataset.log = 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.append(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;
}
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;
*/
const cellA = rowA.getAttribute(`data-${columnKeys[index]}`);
const cellB = rowB.getAttribute(`data-${columnKeys[index]}`);
// 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.append(row));
// Append the DocumentFragment to tbody
tbody.append(fragment);
table.classList.remove('progress');
});
});
const isEqualMaps = (map1, map2) => {
if (map1.size !== map2.size) {
return false;
}
for (let [key, val] of map1) {
if (!map2.has(key) || map2.get(key) !== val) {
return false;
}
}
return true;
};
const str2Regex = (str) => {
try {
const match = str.match(new RegExp('^/(.*)/([a-z]*)$'));
if (match.length) {
return new RegExp(match[1], match[2]);
}
} catch (e) {
return false;
}
return false;
};
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(/\s*,\s*/).map(item => item.split('=')).reduce((acc, [key, val]) => {
if (!columnKeys.includes(key) || !val) {
return acc;
}
const regexResult = str2Regex(val);
if (typeof(regexResult) === 'object') {
acc.set(key, regexResult);
return acc;
}
acc.set(key, noSanitizeKeys.includes(key) ? val : sanitize(val));
return acc;
}, new Map());
if (isEqualMaps(lastFilteringCriteria, criteria)) {
return;
}
lastFilteringCriteria = criteria;
if (criteria.size === 0) {
rows.forEach(row => row.removeAttribute('hidden'));
$('#myfilterResult').textContent = `${rowCount} 件 / ${rowCount} 件中`;
return;
}
rows.forEach(row => {
const isVisible = Array.from(criteria.entries()).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.toggleAttribute('hidden', true);
}
});
$('#myfilterResult').textContent = `${count} 件 / ${rowCount} 件中`;
};
// Insert the data of each BBS thread list
const insertCells = (bbs) => {
completedRows += donguriLogBbsRows.get(bbs).size;
for (let [index, key] of donguriLogBbsRows.get(bbs)) {
const row = rows[index];
if ('subject' in row.dataset && row.dataset.subject.length) {
continue;
}
const {
date,
origin
} = row.dataset;
const subject = subjectList.get(bbs).get(key) || "???";
row.dataset.subject = subject;
const anchor = document.createElement('a');
anchor.href = `${origin}/test/read.cgi/${bbs}/${key}/?v=pc#date=${date}`;
anchor.setAttribute('rel', 'noreferrer noopener');
anchor.target = `${bbs}_${key}`;
anchor.textContent = subject;
$(columnSelector.subject, row).append(anchor);
}
// After inserting all cells
if (completedRows === rowCount) {
filterRows($('#myfilter').value);
console.timeEnd(`${location.origin}${location.pathname}`);
// $('html').toggleAttribute('hidden');
}
};
const insertCellsNotCount = (bbs) => {
for (let [index, key] of donguriLogBbsRows.get(bbs)) {
if (!subjectList.get(bbs).has(key)) {
continue;
}
const row = rows[index];
const {
date,
origin
} = row.dataset;
const subject = subjectList.get(bbs).get(key);
row.dataset.subject = subject;
const anchor = document.createElement('a');
anchor.href = `${origin}/test/read.cgi/${bbs}/${key}/?v=pc#date=${date}`;
anchor.setAttribute('rel', 'noreferrer noopener');
anchor.target = `${bbs}_${key}`;
anchor.textContent = subject;
$(columnSelector.subject, row).append(anchor);
}
};
const insertBbsnameCells = (bbs) => {
for (let index of donguriLogBbsRows.get(bbs).keys()) {
const row = rows[index];
const origin = bbsOriginList.has(bbs) ? bbsOriginList.get(bbs) : "https://origin";
const bbsName = bbsNameList.has(bbs) ? bbsNameList.get(bbs) : "???";
row.dataset.origin = origin;
row.dataset.bbsname = bbsName;
$(columnSelector.bbsname, row).textContent = bbsName;
}
};
// Initialize the filter input and its functionalities
const tfootHtml = String.raw`
<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>
`;
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));
console.time('filterRows');
filterRows(input.value);
console.timeEnd('filterRows');
});
const escapeRegExp = (string) => string.replace(/[.\/*+?^${}()|[\]\\]/g, '\\$&');
tbody.addEventListener('dblclick', function(event) {
event.preventDefault();
const target = event.target;
if (!$('#myfilter') || target.tagName !== 'TD') {
return;
}
let targetTxt = target.textContent.trim();
if (targetTxt.includes(',')) {
targetTxt = `/^${escapeRegExp(targetTxt).replace(/,/g, '\\x2c')}$/`;
} else if (str2Regex(targetTxt) !== false) {
targetTxt = `/^${escapeRegExp(targetTxt)}$/`;
}
const index = Array.prototype.indexOf.call(target.parentNode.children, target);
const txt = `${columnKeys[index]}=${targetTxt}`;
const re = new RegExp(`(^|,)${escapeRegExp(txt)}(?:,|$)`);
const input = $('#myfilter');
if (re.test(input.value)) {
const newHash = input.value.replace(re,"$1");
location.hash = encodeURIComponent(newHash.endsWith(',') ? newHash.slice(0, -1) : newHash);
if (!newHash.length) {
history.replaceState(null, '', location.pathname);
}
} else if (input.value.length > 1 && input.value.endsWith(',') === false) {
location.hash += `,${txt}`;
} else {
location.hash += txt;
}
});
// GM_xmlhttpRequest wrapper to handle HTTP Get requests
const xhrGetDat = (url, loadFunc, errorFunc, mime = 'text/plain; charset=shift_jis', type = 'text') => {
console.time(url);
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: type,
timeout: 3600 * 1000,
overrideMimeType: mime,
onload: response => loadFunc(response),
onerror: response => errorFunc(response)
});
};
const errorFunc = (response) => {
console.timeEnd(response.finalUrl);
console.error(response);
};
const ignoreErrorFunc = (response) => {
console.timeEnd(response.finalUrl);
console.error(response);
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);
if (!subjectList.has(bbs)) {
subjectList.set(bbs, new Map());
}
insertCells(bbs);
};
const isSubsetOf = (superset, subset) => {
return [...subset].every(value => superset.has(value));
};
const getDifferenceSet = (set1, set2) => {
return new Set([...set1].filter(value => !set2.has(value)));
};
const parser = new DOMParser();
// Process post titles line to update subjectList and modify the table-cells
const addBbsPastInfo = (response) => {
console.timeEnd(response.finalUrl);
const url = response.finalUrl;
const urlLength = url.length;
const afterBbsPath = '/kako/';
const afterBbsPathLength = afterBbsPath.length;
const beforeBbsSlashIndex = url.lastIndexOf('/', urlLength - afterBbsPathLength - 1);
const bbs = url.substring(beforeBbsSlashIndex + 1, urlLength - afterBbsPathLength);
if (response.status !== 200) {
console.error(response);
insertCells(bbs);
return;
}
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 (subjectList.get(bbs).has(key)) {
return;
}
subjectList.get(bbs).set(key, subject);
});
if (!isSubsetOf(new Set(subjectList.get(bbs).keys()), donguriLogBbsKeys.get(bbs))) {
console.info('Subject not found. {"bbs":"%s","key":"%s"}', bbs, Array.from(getDifferenceSet(donguriLogBbsKeys.get(bbs), new Set(subjectList.get(bbs).keys()))));
}
insertCells(bbs);
};
// Process post titles line to update subjectList and modify the table-cells
const addBbsInfo = (response) => {
console.timeEnd(response.finalUrl);
const url = response.finalUrl;
const urlLength = url.length;
const afterBbsPath = '/lastmodify.txt';
const afterBbsPathLength = afterBbsPath.length;
const beforeBbsSlashIndex = url.lastIndexOf('/', urlLength - afterBbsPathLength - 1);
const bbs = url.substring(beforeBbsSlashIndex + 1, urlLength - afterBbsPathLength);
subjectList.set(bbs, new Map());
if (response.status !== 200) {
console.error(response);
insertCells(bbs);
return;
}
const lastmodify = response.responseText.trim();
let lines = lastmodify.split(/[\r\n]+/);
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let [keyDat, subject] = line.split(/\s*<>\s*/, 2);
let lastDotIndex = keyDat.lastIndexOf('.');
if (lastDotIndex === -1) {
continue;
}
let key = keyDat.substring(0, lastDotIndex);
if (!donguriLogBbsKeys.get(bbs).has(key)) {
continue;
}
if (/&#?[a-zA-Z0-9]+;?/.test(subject)) {
subject = parser.parseFromString(subject, 'text/html').documentElement.textContent;
}
subjectList.get(bbs).set(key, subject);
}
// All subjects corresponding to the keys in the cell were confirmed
if (isSubsetOf(new Set(subjectList.get(bbs).keys()), donguriLogBbsKeys.get(bbs))) {
insertCells(bbs);
} else {
insertCellsNotCount(bbs);
// Check past log
xhrGetDat(new URL("./kako/", url), addBbsPastInfo, ignoreErrorFunc, '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 donguriLogBbsRows.keys()) {
const url = `${bbsOriginList.get(bbs)}/${bbs}/lastmodify.txt`;
xhrGetDat(url, addBbsInfo, ignoreErrorFunc);
}
};
// 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;
}
const domainIndex = '.5ch.net/';
const domainIndexLength = domainIndex.length;
let bbsCount = 0;
outerBlock: for (const menu_list of response.response.menu_list) {
for (const category_content of menu_list.category_content) {
const bbs = category_content.directory_name;
if (!donguriLogBbsRows.has(bbs) || bbsOriginList.has(bbs)) {
continue;
}
const bbsUrl = category_content.url;
const index = bbsUrl.indexOf(domainIndex);
if (index === -1) {
continue;
}
const origin = bbsUrl.substring(0, index + domainIndexLength - 1);
bbsOriginList.set(bbs, origin);
bbsNameList.set(bbs, category_content.board_name);
insertBbsnameCells(bbs);
if (donguriLogBbsKeys.size === ++bbsCount) {
break outerBlock;
}
}
}
if (bbsOriginList.size === 0) {
console.error('No boards found.');
return;
}
xhrBbsInfoFromDonguriRows();
};
// Initial data fetch from bbsmenu
xhrGetDat('https://menu.5ch.net/bbsmenu.json', bbsmenuFunc, errorFunc, 'application/json; charset=utf-8', 'json');
};
const processMap = {
donguriLog: {
regex: new RegExp(String.raw`^https?://donguri\.5ch\.net/cannonlogs$`),
handler: donguriFilter
},
readCgi: {
regex: new RegExp(String.raw`^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}`);
})();