Skip to content

Instantly share code, notes, and snippets.

@vmi
Created November 13, 2010 09:59
Show Gist options
  • Save vmi/675220 to your computer and use it in GitHub Desktop.
Save vmi/675220 to your computer and use it in GitHub Desktop.
Utilities for Arcadia
// ==UserScript==
// @name arcadia-reader
// @namespace arcadia
// @description Utilities for Arcadia
// @include http://www.mai-net.net/bbs/*
// @include http://mai-net.ath.cx/bbs/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_log
// ==/UserScript==
/*********************************************************************
Arcadia用の支援ツールです。
- ウォッチリストに登録することで、注目作品の更新状況を見易くします。
チェックボックスをクリックするとウォッチリストに登録されます。
- キルリストに登録することで、指定の作品を一覧から削除します。
チェックボックスを[Ctrl]+クリックするとキルリストに登録されます。
- キルパターンを登録することで、指定の正規表現にマッチする作品を一覧
から削除します。
[MENU]→[キルパターン] で表示されるテキストエリアに正規表現を記述
して[保存]を押してください。
*********************************************************************/
(function() {
//// 定数
var C_WATCH = '#CCFFCC';
var C_WATCH_NEW = '#00FF00';
var C_KILL = '#FFCCCC';
var NOW = new Date();
//// ユーティリティ関数
// ゼロパディング
function zp(n, c) {
n = n.toString();
var z = '';
for (var i = c - n.length; i > 0; --i)
z += '0';
return z + n;
}
// 'MM/DD hh:mm' を返す
function nowMDhm() {
var M = zp(NOW.getMonth() + 1, 2);
var D = zp(NOW.getDate(), 2);
var h = zp(NOW.getHours(), 2);
var m = zp(NOW.getMinutes(), 2);
return M + '/' + D + ' ' + h + ':' + m;
}
// 仮で年を補う(正しいとは限らない)
function appendYear(MDhm) {
var y;
if (nowMDhm() >= MDhm)
y = NOW.getFullYear();
else
y = NOW.getFullYear() - 1;
return y.toString() + '/' + MDhm;
}
// HTMLエスケープ
var escapeMap = { '<': '&lt;', '>': '&gt;', '"': '&quot;', '&': '&amp;' };
function escapeHTML(s) {
return s.replace(/[<>\"&]/g, function(c) { return escapeMap[c] || c; });
}
// ストレージへの書き込み
function setValue(key, value) {
GM_setValue(key, value);
}
// ストレージからの読み込み
function getValue(key) {
return GM_getValue(key);
}
// ストレージからの削除
function removeValue(key) {
GM_deleteValue(key);
}
// ストレージに格納されたキーの繰り返し
function eachByKeys(f) {
var keys = GM_listValues();
for (var i = 0; i < keys.length; i++)
f(keys[i]);
}
//// DOM操作関数
// エレメントの取得
function $(key, doc) {
if (!doc)
doc = document;
switch (key[0]) {
case '#':
return doc.getElementById(key.substring(1));
case '.':
return doc.getElementsByClassName(key.substring(1));
default:
return doc.getElementsByTagName(key);
}
}
// XPathによるエレメントの取得
function $x(xpath, doc) {
if (!doc)
doc = document;
var xpr = document.evaluate(xpath, doc, null, 7, null);
var xprLen = xpr.snapshotLength;
var result = [];
for (var i = 0; i < xprLen; i++)
result.push(xpr.snapshotItem(i));
return result;
}
// CSSを追加
function addCss(css) {
try {
var sss = document.styleSheets;
var ss = document.styleSheets[sss.length - 1];
ss.insertRule(css, ss.cssRules.length);
} catch (e) {
// JavaScript実行時にCSSのロードが完了していないと例外が発生する
GM_log(e);
var style = document.createElement('style');
style.type = 'text/css';
style.innerText = css;
$('head')[0].appendChild(style);
}
}
// テキストノードを生成
function text(s) {
return document.createTextNode(s);
}
// trにtdを追加
function addTd(tr, value, style) {
var td = document.createElement('td');
if (style) {
for (var key in style) {
td.style[key] = style[key];
}
}
if (typeof value == 'string')
td.appendChild(text(value));
else
td.appendChild(value);
tr.appendChild(td);
}
// tdのリストを文字列のリストに変換
function tds2texts(tds) {
var len = tds.length;
var result = [];
for (var i = 0; i < len; i++)
result.push(tds[i].textContent);
return result;
}
// フォントタグのsizeを削ってstyleに振り直す
function fixupFontSize() {
var fonts = $x('//font[@size]');
for (var i = 0; i < fonts.length; i++) {
var font = fonts[i];
switch (font.size) {
case '-2': font.style.fontSize = '80%'; break;
case '+1': case '4': font.style.fontSize = '120%'; break;
case '+2': font.style.fontSize = '150%'; break;
}
font.removeAttribute('size');
}
}
//// クラス定義
// リンク情報
var Link = function(url) {
this.url = url;
var [base, args] = url.replace(/#.*/, '').split('?', 2);
var bbs = null;
if (base)
bbs = base.match(/\/(\w+)\.php$/);
if (!bbs) {
this.type = 'sst_list';
return;
}
var pairs = args.split('&');
for (var i = 0; i < pairs.length; i++) {
var [key, value] = pairs[i].split('=', 2);
this[key] = value;
}
this.type = bbs[1] + '_' + this.act;
};
// 記事情報
var Article = function() {};
Article.prototype = {
'parseTr': function(tr, a) {
this.mark = '-';
var tds = $('td', tr);
switch (tds.length) {
case 7: // 全て: [0]元作品 [1]タイトル [2]投稿者 [3]記事 [4]感想 [5]PV [6]更新
[this.category, this.title, this.author, this.artCnt, this.impCnt, this.pv,
this.lastUpdated] = tds2texts(tds);
break;
case 6: // 個別: [0]タイトル [1]投稿者 [2]記事 [3]感想 [4]PV [5]更新
[this.title, this.author, this.artCnt, this.impCnt, this.pv,
this.lastUpdated] = tds2texts(tds);
var _;
[_, this.category] = $('center')[0].textContent.match(/^(.*?)(?:SS投稿掲示板)?$/);
if (this.category.length == 0)
this.category = '-';
GM_log('category: ' + this.category);
break;
case 4: // チラ裏: [0]タイトル [1]投稿者 [2]記事 [3]更新
[this.title, this.author, this.artCnt, this.lastUpdated] = tds2texts(tds);
this.category = 'チラシの裏';
this.artCnt = this.impCnt = this.pv = '-';
break;
}
this.href = a.href;
this.link = new Link(this.href);
this.id = this.link.all;
this.merge();
return this;
},
'parsePage': function() {
var _;
this.mark = '-';
// URL情報の設定
this.href = document.URL.replace(/&n=.*$/, '&n=0&count=1');
this.link = new Link(this.href);
this.id = this.link.all;
// カテゴリの設定
[_, this.category] = $('center')[0].textContent.match(/^(.*?)(?:SS投稿掲示板)?$/);
if (this.category.length == 0)
this.category = '-';
GM_log('category: ' + this.category);
// タイトル、投稿者、更新の設定
var trs = $x('id("table")/tbody/tr');
var [_, title, author, lastUpdated] = tds2texts($('td', trs[0]));
this.title = title;
[_, this.author] = author.match(/^\[(.+)\]$/);
for (var i = 0; i < trs.length; i++) {
var tds = $('td', trs[i]);
var updated = tds[tds.length - 1].textContent;
if (lastUpdated < updated)
lastUpdated = updated;
}
[_, this.lastUpdatedFull] = lastUpdated.match(/^\((.+)\)$/);
this.lastUpdated = this.lastUpdatedFull.substring(5);
this.merge();
return this;
},
'parseString': function(s) {
var list = s.split(/\t/);
[this.mark, this.category, this.title, this.author, this.lastUpdated,
this.href, this.lastUpdatedFull] = list;
if (!this.lastUpdatedFull)
this.lastUpdatedFull = appendYear(this.lastUpdated);
this.link = new Link(this.href);
this.id = this.link.all;
return this;
},
'toString': function() {
return [this.mark, this.category, this.title, this.author, this.lastUpdated,
this.href, this.lastUpdatedFull].join('\t');
},
'key': function() {
return 'a:' + this.id;
},
'save': function() {
setValue(this.key(), this.toString());
},
'remove': function() {
this.mark = '-';
removeValue(this.key());
},
'merge': function () {
var ls = getValue(this.key());
if (ls) {
var [mark, category, title, author, lastUpdated, href,
lastUpdatedFull] = ls.split(/\t/);
if (this.category == '-') {
this.category = category;
GM_log("fallback category: " + category);
}
if (this.title.match(/\.{6}$/)) {
this.title = title;
GM_log("fallback title: " + title);
}
if (this.author.match(/\.{6}$/)) {
this.author = author;
GM_log("fallback author: " + author);
}
if (!this.lastUpdatedFull) {
if (lastUpdatedFull &&
lastUpdatedFull >= lastUpdatedFull.substring(0, 5) + this.lastUpdated)
this.lastUpdatedFull = lastUpdatedFull;
else
this.lastUpdatedFull = appendYear(this.lastUpdated);
}
this.mark = mark;
this.isUpdated = (lastUpdated != this.lastUpdated);
this.save();
} else {
this.isUpdated = null;
}
}
};
//// 各種処理
//// イベント処理
// 記事のマップ
var articleMap = {};
// メニューの表示/非表示
function showMenu() {
var menuBody = $('#arMenuBody');
menuBody.style.display = (menuBody.style.display == 'none') ? '' : 'none';
}
// showList で表示しているリストの種類
var markedClass = null;
function shorten(s, n) {
var len = s.length;
var cnt = 0;
for (var i = 0; i < len; i++) {
if (s[i] < '\x7F')
cnt++;
else
cnt += 2;
if (cnt > 80)
return s.substring(0, i) + '......';
}
return s;
}
// 感想リンク
// http://www.mai-net.net/bbs/sst/sst.php?act=dump&cate=all&all=34592&n=0&count=1
// http://www.mai-net.net/bbs/sst/sst.php?act=impression&cate=all&no=34592&page=1
function commentLink(link) {
return link.replace(/([?&])(\w+)=(\w+)/g, function(a, s, k, v) {
switch (k) {
case "act":
v = "impression";
break;
case "all":
k = "no";
break;
case "n":
return "";
case "count":
k = "page";
v = "1";
break;
default:
break;
}
return s + k + "=" + v;
});
}
// ウォッチリスト/キルリストの表示
function showList(e) {
// キルパターンを非表示
$('#armKPEdit').style.display = 'none';
markedClass = this.id;
var mark = (markedClass == 'armWatch') ? 'w' : 'k';
// ストレージから記事情報を取得
var list = [];
eachByKeys(function(key) {
if (key.match(/^a:/)) {
var article = new Article().parseString(getValue(key));
if (article.mark == mark)
list.push(article);
}
});
// 日付順(降順)でソート
list.sort(function(a, b) {
var d = b.lastUpdatedFull.localeCompare(a.lastUpdatedFull);
return (d == 0) ? (b.id - a.id) : d;
});
// リストの生成
var listBody = $('#armListBody');
listBody.innerHTML = ''; // 既存のリスト消去
for (i = 0; i < list.length; i++) {
var article = list[i];
var tr = document.createElement('tr');
tr.className = markedClass;
tr.id = article.key();
var style = {'textAlign': 'center'};
addTd(tr, article.category, style);
var span = document.createElement('span');
var title = shorten(article.title, 80);
span.innerHTML = '<input type="checkbox"/> <a href="' + article.href + '" style="font-weight: bold">' + title + '</a> (<a href="' + commentLink(article.href) + '">感</a>)';
addTd(tr, span);
addTd(tr, article.author, style);
addTd(tr, article.lastUpdatedFull);
listBody.appendChild(tr);
}
// リストを表示
$('#armList').style.display = '';
}
// リストから要素を削除
function removeList(e) {
var listBody = $('#armListBody');
var trs = $('.' + markedClass, listBody);
for (var i = trs.length - 1; i >= 0; --i) {
var tr = trs[i];
var article = new Article().parseString(getValue(tr.id));
var input = $('input', tr)[0];
if (input.checked) {
var mainInput = $('#ar' + article.id);
if (mainInput) {
mainInput.checked = false;
mainInput.parentNode.parentNode.style.background = '';
}
article.remove();
listBody.removeChild(tr);
}
}
}
// キルパターンの表示
function showKillPattern(e) {
// リストを非表示
$('#armList').style.display = 'none';
var kp = getValue('killPattern') || '';
$('#armKPEditBody').value = kp;
// キルパターンを表示
$('#armKPEdit').style.display = '';
}
// キルパターンの保存
function saveKillPattern(e) {
var kp = $('#armKPEditBody').value.replace(/^\s+|\s+$/g, '');
if (kp.length > 0)
setValue('killPattern', kp);
else
removeValue('killPattern');
// キルパターンを非表示
$('#armKPEdit').style.display = 'none';
}
// メニューの分離
function separateMenu() {
var [m1, m2] = $x('//table[@class="brdr"][1]/tbody/tr[position()<=2]/td[1]');
if (!m1 || m1.textContent != 'MENU')
return;
// メニューのスタイルを設定
addCss('#arMenu {\
position: fixed;\
overflow-y: auto;\
top: 8px;\
left: 8px;\
border: 2px solid grey;\
}');
addCss('.armWatch { background: ' + C_WATCH + '; }');
addCss('.armKill { background: ' + C_KILL + '; }');
// メニューの生成
var menu = document.createElement('div');
menu.id = 'arMenu';
menu.style.maxHeight = Math.floor(window.innerHeight * 0.8) + 'px';
menu.innerHTML = '<table class="brdr" cellSpacing="1" cellPadding="3">\
<tr class="bga">\
<td colspan="2"><button id="arShowMenu">MENU</button></td>\
</tr>\
<tr id="arMenuBody" class="bgc" style="display:none" />\
</table>';
document.body.appendChild(menu);
// [MENU]セルを削除、元ページのメニューを移動
var menuBody = $('#arMenuBody');
m2.style.width = '14em';
m1.parentNode.removeChild(m1);
m2.parentNode.removeChild(m2);
menuBody.appendChild(m2);
// メニュー内操作画面の生成
var td = document.createElement('td');
td.style.verticalAlign = 'top';
td.style.minWidth = '14em';
menuBody.appendChild(td);
td.innerHTML = '\
<button id="armWatch">ウォッチリスト</button>\
<button id="armKill">キルリスト</button>\
<button id="armKillPattern">キルパターン</button><br/><br/>\
<div id="armList" style="display: none">\
<button onclick="document.getElementById(\'armList\').style.display=\'none\'">閉じる</button><br/><br/>\
<table class="brdr" cellSpacing="1" cellPadding="3" style="white-space: nowrap">\
<thead>\
<tr class="bga" style="text-align: center">\
<td>元作品</td><td>タイトル</td><td>投稿者</td><td>更新</td>\
</tr>\
</thead>\
<tbody id="armListBody"/>\
</table><br/>\
<button id="armRemove">削除</button>\
</div>\
<div id="armKPEdit" style="display: none">\
<button id="armKPSave">保存</button> <button onclick="document.getElementById(\'armKPEdit\').style.display=\'none\'">閉じる</button><br/><br/>\
<textarea cols="40" rows="25" id="armKPEditBody">\
</textarea>\
</div>\
';
// メニューの表示/非表示
$('#arShowMenu').addEventListener('click', showMenu, false);
// ウォッチリストの表示
$('#armWatch').addEventListener('click', showList, false);
// キルリストの表示
$('#armKill').addEventListener('click', showList, false);
// リストから要素を削除
$('#armRemove').addEventListener('click', removeList, false);
// キルパターンの表示
$('#armKillPattern').addEventListener('click', showKillPattern, false);
// キルパターンの保存
$('#armKPSave').addEventListener('click', saveKillPattern, false);
}
// チェックボックスを:
// - クリック ⇒ ウォッチリストに登録
// - (Shift or Ctrl)+クリック ⇒ キルリストに登録
function setMarkOnList(e) {
var tr = this.parentNode.parentNode;
var article = articleMap[this.id];
if (this.checked) {
if (e.ctrlKey || e.shiftKey) {
article.mark = 'k'; // kill
tr.style.background = C_KILL;
} else {
article.mark = 'w'; // watch
tr.style.background = C_WATCH;
}
article.save();
} else {
article.remove();
tr.style.background = '';
}
}
// 投稿掲示板
function sstList(thisPage) {
// タイトルを変更
document.title = $('center')[0].textContent;
// スタイルを修正
addCss('body, th, td { font-size: 16px; }');
addCss('.bgc { white-space: nowrap; }');
addCss('button, input { font-size: 80%; }');
fixupFontSize();
// メニューを分離
separateMenu();
// 一覧を操作
var trs = $('.bgc', $('.brdr')[0]);
var kpRe = getValue('killPattern');
if (kpRe) {
kpRe = new RegExp('(?:' + kpRe.replace(/\n/g, ')|(?:') + ')');
GM_log("Kill Pattern: " + kpRe);
}
for (var i = trs.length - 1; i >= 0; --i) {
var tr = trs[i];
var a = $('a', tr)[0];
var article = new Article().parseTr(tr, a);
var line = [article.category, article.title, article.author].join(':');
if (article.mark == 'k' || (kpRe && line.match(kpRe))) {
// キルリストに含まれている、もしくはキルパターンにマッチしたら削除
tr.parentNode.removeChild(tr);
GM_log("Killed: " + line);
continue;
}
articleMap['ar' + article.id] = article;
// チェックボックスを追加
var input = document.createElement('input');
input.type = 'checkbox';
input.id = 'ar' + article.id;
input.addEventListener('click', setMarkOnList, false);
var b = a.parentNode;
var td = b.parentNode;
td.insertBefore(input, b);
td.insertBefore(text(' '), b);
if (article.mark == 'w') { // ウォッチリストに含まれている
input.checked = true;
tr.style.background = article.isUpdated ? C_WATCH_NEW : C_WATCH;
}
// 感想リンクを追加
var comm = document.createElement('a');
var href = commentLink(a.href);
comm.href = href;
comm.text = "感";
comm.style.color = "black";
td.appendChild(text(' ('));
td.appendChild(comm);
td.appendChild(text(')'));
}
}
var currentArticle = null;
function setMarkOnArticle(e) {
if (this.checked) {
if (e.ctrlKey || e.shiftKey) {
currentArticle.mark = 'k'; // kill
this.parentNode.style.background = C_KILL;
} else {
currentArticle.mark = 'w'; // watch
this.parentNode.style.background = C_WATCH;
}
currentArticle.save();
} else {
currentArticle.remove();
this.parentNode.style.background = '';
}
}
// 投稿記事
function sstDump(thisPage) {
// スタイルを修正
fixupFontSize();
var naviMenu = $x('//td[@bgcolor="#a0a0ff"]/parent::tr');
for (var i = 0; i < naviMenu.length; i++)
naviMenu[i].style.whiteSpace = 'nowrap';
// ウォッチ/キル用チェックボックスを設定
currentArticle = new Article().parsePage();
var input = document.createElement('input');
input.type = 'checkbox';
input.addEventListener('click', setMarkOnArticle, false);
var titleTd = $(".bgb")[0];
titleTd.insertBefore(input, titleTd.firstChild);
switch (currentArticle.mark) {
case 'w':
titleTd.style.background = C_WATCH;
input.checked = true;
break;
case 'k':
titleTd.style.background = C_KILL;
input.checked = true;
break;
}
// 前ページ/次ページを補正する
var n = thisPage.n;
var prev = null, next = null;
var as = $x('id("table")//a');
var found = false;
for (var i = 0; i < as.length; i++) {
var a = as[i];
var link = new Link(a.href);
if (link.n == n) {
found = true;
if (i + 1 < as.length)
next = new Link(as[i + 1].href);
break;
}
prev = link;
}
if (found) {
var navis = $x('//*[@align="right"]/a');
for (i = 0; i < navis.length; i++) {
var navi = navis[i];
var parent = navi.parentNode;
switch (navi.textContent) {
case '前を表示する':
if (prev) {
// 前ページへのリンクを補正する
navi.href = navi.href.replace(/&n=\d+/, '&n=' + prev.n);
} else {
// 前ページへのリンクと区切り文字を削除する
var ns = navi.nextSibling;
if (ns && ns.nodeType == 3)
parent.removeChild(ns);
parent.removeChild(navi);
}
break;
case '次を表示する':
if (next) {
// 次ページへのリンクを補正する
navi.href = navi.href.replace(/&n=\d+/, '&n=' + next.n);
} else {
// 次ページへのリンクと区切り文字を削除する
var ps = navi.previousSibling;
if (ps && ps.nodeType == 3)
parent.removeChild(ps);
parent.removeChild(navi);
}
break;
}
}
}
}
// メイン
function main() {
var url = document.URL;
if (url.match(/mai-net\.ath\.cx/)) { // 旧URLならリダイレクト
url = url.replace(/mai-net\.ath\.cx/, 'www.mai-net.net');
GM_log('redirect to www.mai-net.net');
document.location.href = url;
return;
}
var thisPage = new Link(url);
switch (thisPage.type) {
case 'sst_list': // 投稿掲示板
sstList(thisPage);
break;
case 'sst_dump': // 投稿記事
sstDump(thisPage);
break;
case 'mainbbs_list': // メイン掲示板
break;
case 'mainbbs_dump': // メイン記事
break;
case 'sss_list': // 捜索掲示板
break;
case 'sss_dump': // 捜索記事
break;
}
}
GM_log('start.');
main();
GM_log('end.');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment