Last active
May 16, 2023 12:36
-
-
Save shiguruikai/3888a1a9ef1e052b86e61a2719eeeffb to your computer and use it in GitHub Desktop.
Tampermonkey用の簡易5chブラウザ(NG設定、サムネイル表示)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name 5ch Browser | |
// @namespace http://tampermonkey.net/ | |
// @version 1.0.0 | |
// @description 簡易5chブラウザ(NG設定、サムネイル表示) | |
// @author shiguruikai | |
// @match *://*.5ch.net/test/* | |
// @match *://*.5ch.sc/test/* | |
// @match *://*.bbspink.com/test/* | |
// @grant none | |
// ==/UserScript== | |
(() => { | |
'use strict'; | |
/** | |
* NG IP | |
* @type {string[]} | |
*/ | |
const NG_IP = []; | |
/** | |
* NG 名前 | |
* @type {RegExp[]} | |
*/ | |
const NG_NAMES = []; | |
/** | |
* NG ID | |
* @type {RegExp[]} | |
*/ | |
const NG_ID = []; | |
/** | |
* NG ワード | |
* @type {RegExp[]} | |
*/ | |
const NG_WORDS = []; | |
/** 透明あぼーんにするかどうか */ | |
const USE_HIDDEN_ABONE = true; | |
/** サムネイルにモザイクをかけるかどうか */ | |
const USE_THUMBNAIL_MOSAIC = true; | |
/** モザイクサムネイルの鮮明度(モザイクの横ピクセル数) */ | |
const THUMBNAIL_MOSAIC_WIDTH_PX = 50; | |
/** サムネイルの最大幅 */ | |
const THUMBNAIL_STYLE_MAX_WIDTH = '512px'; | |
/** サムネイルの最大高さ */ | |
const THUMBNAIL_STYLE_MAX_HEIGHT = '768px'; | |
/** サムネイルを表示する画像の拡張子 */ | |
const IMAGE_EXTS = ['apng', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'tiff', 'webp']; | |
const style = document.createElement('style'); | |
style.innerHTML = ` | |
div.message a.image { | |
display: inline-block; | |
margin: 8px 0; | |
vertical-align: top; | |
} | |
div.message a.image + a.image { | |
margin-left: 4px | |
} | |
`; | |
document.head.append(style); | |
/** | |
* @param {string} expression | |
* @param {Element} element | |
* @returns {Element} | |
*/ | |
const getElementByXPath = (expression, element = document) => { | |
return document.evaluate(expression, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; | |
}; | |
/** | |
* @param {string} expression | |
* @param {Element} element | |
* @returns {Element[]} | |
*/ | |
const getElementsByXPath = (expression, element = document) => { | |
const elements = []; | |
const x = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); | |
for (let i = 0, l = x.snapshotLength; i < l; i++) { | |
elements.push(x.snapshotItem(i)); | |
} | |
return elements; | |
}; | |
/** | |
* @param {string} src | |
* @returns {Promise<HTMLImageElement>} | |
*/ | |
const loadImage = (src) => { | |
return new Promise((resolve, reject) => { | |
const img = new Image(); | |
img.onload = () => resolve(img); | |
img.onerror = (e) => reject(e); | |
img.src = src; | |
}); | |
}; | |
/** | |
* @param {HTMLImageElement} image | |
* @returns {Promise<boolean>} | |
*/ | |
const isImgurRemovedPNG = async (image) => { | |
if (image.width == 161 && image.height == 81 && image.src.includes('://i.imgur.com/')) { | |
return (await fetch(image.src, { method: 'HEAD' })).url === 'https://i.imgur.com/removed.png'; | |
} else { | |
return false; | |
} | |
}; | |
/** | |
* h抜きリンクをハイパーリンクに変える。 | |
* @param {Element} element | |
*/ | |
const autoLink = (element) => { | |
const regex = /\bttps?:\/\/\S+/gi; | |
const replacer = (match, index, string) => { | |
return `<a href="h${match}" target="_blank" rel="noopener">h${match}</a>`; | |
}; | |
element.innerHTML = element.innerHTML.replace(regex, replacer); | |
}; | |
/** | |
* @param {string} fileName | |
* @returns {Element} | |
*/ | |
const getExtension = (fileName) => { | |
return fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2); | |
}; | |
/** | |
* @param {Element} element | |
*/ | |
const isImageLink = (element) => { | |
if (!element.href) return false; | |
return IMAGE_EXTS.includes(getExtension(element.href)); | |
}; | |
const postElements = getElementsByXPath("/html/body/div/div[@class='thread']/div[@class='post']"); | |
const observer = new IntersectionObserver( | |
(entries, observer) => { | |
entries | |
.filter((entry) => entry.isIntersecting) | |
.forEach((entry) => { | |
const postElement = entry.target; | |
// 監視を解除 | |
observer.unobserve(postElement); | |
const number = Number(postElement.id); | |
const nameElement = getElementByXPath("./div[@class='meta']/span[@class='name']", postElement); | |
const uidElement = getElementByXPath("./div[@class='meta']/span[@class='uid']", postElement); | |
const messageElement = getElementByXPath("./div[@class='message']", postElement); | |
if (!number || !nameElement || !uidElement || !messageElement) return; | |
// ワッチョイIPアドレス | |
const ipAdress = nameElement.textContent.match(/\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]\)$/); | |
// 最初の三文字"ID:"を取り除いたID | |
const uid = uidElement.textContent.substring(3); | |
// あぼーんするかどうか | |
// ただし、レス番号が1の場合はあぼーんしない | |
const abone = | |
number !== 1 && | |
(NG_NAMES.some((n) => n.test(nameElement.textContent)) || | |
(ipAdress && NG_IP.some((n) => n.includes(ipAdress[1]))) || | |
NG_ID.some((n) => n === uid) || | |
NG_WORDS.some((n) => n.test(messageElement.textContent))); | |
if (abone) { | |
if (USE_HIDDEN_ABONE) { | |
const nextNode = postElement.nextSibling; | |
if (nextNode && nextNode.nodeName.toLowerCase() === 'br') { | |
nextNode.remove(); | |
} | |
postElement.remove(); | |
} else { | |
nameElement.replaceWith(document.createTextNode('あぼーん ')); | |
postElement.removeChild(messageElement); | |
} | |
} else { | |
autoLink(messageElement); | |
messageElement.querySelectorAll('a').forEach((a) => { | |
// リンクを直リンクにする | |
if (/^https?:\/\/[^/]+\/\?/.test(a.getAttribute('href'))) { | |
a.href = a.textContent; | |
} | |
// rel="noopener"を設定 | |
if (a.target === '_blank') { | |
a.rel = 'noopener'; | |
} | |
// 画像リンクの場合、サムネイルを追加する | |
if (isImageLink(a)) { | |
// 公式のサムネイルを削除する | |
getElementsByXPath(".//div[@div='thumb5ch']", a).forEach((it) => it.remove()); | |
getElementsByXPath(".//div[@div='thumbBBS']", a).forEach((it) => it.remove()); | |
loadImage(a.href) | |
.then(async (img) => { | |
// Imgurの削除済みの画像の場合、リンクアドレスの直前に「(削除済み)」を表示する | |
if (await isImgurRemovedPNG(img)) { | |
a.insertAdjacentText('afterbegin', '(削除済み) '); | |
return; | |
} | |
// サムネイルの作成 | |
const thumbnail = USE_THUMBNAIL_MOSAIC | |
? (() => { | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
// キャンバスサイズを画像に合わせる | |
canvas.width = img.width; | |
canvas.height = img.height; | |
// 縮尺サイズ | |
const w = THUMBNAIL_MOSAIC_WIDTH_PX; | |
const h = (THUMBNAIL_MOSAIC_WIDTH_PX * img.height) / img.width; | |
// 元の画像を縮尺してキャンバスに描画する | |
ctx.drawImage(img, 0, 0, w, h); | |
// アンチエイリアスを無効にする | |
ctx.msImageSmoothingEnabled = false; | |
ctx.mozImageSmoothingEnabled = false; | |
ctx.webkitImageSmoothingEnabled = false; | |
ctx.imageSmoothingEnabled = false; | |
// 縮尺した画像を元のサイズに拡大して描画する | |
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height); | |
return canvas; | |
})() | |
: img; | |
// サムネイルのスタイル | |
thumbnail.style.maxWidth = THUMBNAIL_STYLE_MAX_WIDTH; | |
thumbnail.style.maxHeight = THUMBNAIL_STYLE_MAX_HEIGHT; | |
// サムネイルをリンクアドレスの上に追加する | |
a.insertAdjacentElement('afterbegin', document.createElement('br')); | |
a.insertAdjacentElement('afterbegin', thumbnail); | |
// 画像リンクが連続する場合、間の改行を削除する。改行を削除すると、画像が横に並んで表示される。 | |
if ( | |
a.nextSibling && | |
a.nextSibling.textContent.trim() === '' && | |
a.nextElementSibling && | |
a.nextElementSibling.tagName.toLowerCase() === 'br' && | |
a.nextElementSibling.nextSibling && | |
a.nextElementSibling.nextSibling.textContent.trim() === '' && | |
a.nextElementSibling.nextElementSibling && | |
isImageLink(a.nextElementSibling.nextElementSibling) | |
) { | |
a.parentElement.removeChild(a.nextElementSibling); | |
} | |
}) | |
.catch(() => { | |
a.insertAdjacentText('afterbegin', '(リンク切れ) '); | |
}); | |
} | |
}); | |
} | |
}); | |
}, | |
{ | |
root: null, | |
rootMargin: '50%', | |
threshold: 0.0, | |
} | |
); | |
// 全てのレス要素を監視 | |
postElements.forEach((n) => { | |
observer.observe(n); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment