Skip to content

Instantly share code, notes, and snippets.

@shiguruikai
Last active May 16, 2023 12:36
Show Gist options
  • Save shiguruikai/3888a1a9ef1e052b86e61a2719eeeffb to your computer and use it in GitHub Desktop.
Save shiguruikai/3888a1a9ef1e052b86e61a2719eeeffb to your computer and use it in GitHub Desktop.
Tampermonkey用の簡易5chブラウザ(NG設定、サムネイル表示)
// ==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