// ==UserScript==
// @name:ja 5ch.net どんぐり大砲ログのスレタイ収集&フィルタリング
// @name 5ch.net Donguri Cannon Log Analyzer
// @namespace https://greasyfork.org/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
// @noframes
// @version 2025.05.03.001
// @downloadURL https://update.greasyfork.icu/scripts/496742/5chnet%20Donguri%20Cannon%20Log%20Analyzer.user.js
// @updateURL https://update.greasyfork.icu/scripts/496742/5chnet%20Donguri%20Cannon%20Log%20Analyzer.meta.js
// ==/UserScript==
(function() {
'use strict';
const toggleDisplayText = 'Toggle Table';
const manual = String.raw`
どんぐり大砲を撃たれたレスのスレッドタイトルを取得し表示をします。スレッドタイトルをクリックすると撃たれたレスにジャンプします。各メタデータでフィルタリング、ソート可能にします。
フィルターとソートについて。
テーブル下部のにbbs=newsplus
のように入力すると該当するログだけを表示します。
カンマ(,
) で区切ることで複数の条件が指定できます。
URLのハッシュ(#
)より後に条件を指定することでも機能します。
大砲ログの各セルをダブルクリックして、その内容の条件を追加できます(再度ダブルクリックで削除)。
列ヘッダーをクリックでソートします。
itest.5ch.net表示だとレスにジャンプするのに遅延が発生するためPC表示させるためのCookieを設定します。itest.5ch.net表示させたい場合はitest.5ch.netトップページの設定から「常にPC版で表示」をOFFに変えてください。
右側の${toggleDisplayText} ボタンを押すと元のテーブルと切り替えます。
`;
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;
}
:is(thead, tbody) tr :is(th, td):nth-of-type(-n+9) {
width: auto;
}
:is(thead, tbody) tr :is(th, td):nth-last-of-type(-n+1) {
width: 100%;
}
table {
content-visibility: auto;
contain-intrinsic-size: 40000px;
}
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 :is(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;
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;
}
:is(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;
}
:is(.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 = '