Skip to content

Instantly share code, notes, and snippets.

@yu-tang
Created March 18, 2012 04:01
Show Gist options
  • Save yu-tang/2068750 to your computer and use it in GitHub Desktop.
Save yu-tang/2068750 to your computer and use it in GitHub Desktop.
Scriptish / Greasemonkey User Script for moug

Scriptish / Greasemonkey User Script for moug

機能

  • コード ブロックをシンタックス ハイライト
  • twitter ユーザーはアバターを表示
  • レス番号を表示 (v0.3-)
  • キーボード ショートカット (v0.4-)
  • ユーザー設定画面 (v0.5-)

Syntax highlight Avatar Help dialog Preference command

キーボード ショートカット

スレッド本体のみに適用されます。スレッド一覧にはアサインがありません。

暫定実装のため、アサインは変更される可能性があります。

j 次の投稿へ移動

k 前の投稿へ移動

h 現在の投稿者の次の投稿へ移動

l 現在の投稿者の前の投稿へ移動

u 最初の投稿へ移動

n 最後の投稿へ移動

0-9 レス番号を指定して移動

※ 1秒以内に連続入力すれば、「23」のような二桁のレス番ジャンプも可能

? ショートカットのヘルプを表示 (v0.5-)

ユーザー設定画面

moug のサイトを表示している状態で、Scriptish または Greasemonkey のユーザスクリプトコマンド メニューから、ユーザー設定画面を表示し、特定の機能の有効 / 無効を切り替えることができます。

// ==UserScript==
// @id moug@yu-tang
// @name moug
// @namespace https://gist.github.com/2068750
// @author yu-tang https://github.com/yu-tang
// @version 0.5.3
// @description improve in user experiment at moug.
// @include http://www.moug.net/faq/view*
// @match http://www.moug.net/faq/view*
// @resource CSS https://s3.amazonaws.com/alexgorbatchev.com/pub/sh/3.0.83/styles/shCoreDefault.css
// @require https://s3.amazonaws.com/alexgorbatchev.com/pub/sh/3.0.83/scripts/shCore.js
// @require https://s3.amazonaws.com/alexgorbatchev.com/pub/sh/3.0.83/scripts/shBrushVb.js
// @require https://raw.github.com/odyniec/MonkeyConfig/master/monkeyconfig.js
// ==/UserScript==
/*
SyntaxHighlighter の使用について
================================
本来、SyntaxHighlighter の(作者さんによる)ホスティング先 URL は
下記が正しいです。
http://alexgorbatchev.com/pub/sh/current/
ただ、この URL をそのまま @require に使用すると、依存ファイルを DL できず、
ユーザースクリプトのインストールに失敗します。詳しくは追っていませんが、
おそらく Amazon S3 にリダイレクトされる関係ではないかと想像しています
(Scriptish と Greasemonkey で同様)。
上記の理由により、本ユーザースクリプトのメタデータでは、リダイレクト先の
URL の方を書いています。これならインストールに失敗しません。
当然、将来ホスト先や URL 構成が変わればインストールに失敗する事態が
予想されますので、ご注意ください。
また、作者さんはこのホスティングにより毎月 40$ ほどの出費になっている
そうです。余裕のある方は寄付をご検討ください。
*/
{ // スクリプト開始
/*
参考:
JavaScript のブロックスコープと名前空間 « Mozilla Developer Street (modest)
https://dev.mozilla.jp/2010/05/js-blockscope-and-namespace/
*/
//###################################################
// config (for user)
//###################################################
let cfg = new MonkeyConfig({
title: 'moug+ ユーザー設定',
menuCommand: true,
params: {
syntax_highlight: {
'type': 'checkbox',
'default': true,
'label': 'シンタックス ハイライト'
},
twitter_avatar: {
'type': 'checkbox',
'default': true,
'label': 'twitter アバター'
},
post_id: {
'type': 'checkbox',
'default': true,
'label': 'レス番号'
},
shortcut: {
'type': 'checkbox',
'default': true,
'label': 'キーボード ショートカット'
}
}
});
//###################################################
// Get route
//###################################################
let route = window.location.pathname.slice(5, -4); // "/faq/hoge.php" -> "hoge"
//###################################################
// GM_xpath function
//###################################################
if (GM_xpath === undefined) { // for Greasemonkey (not Scriptish)
var GM_xpath = function(arg) {
var nl = document.evaluate(
arg.path,
(arg.node || document),
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
if (arg.all) {
nl.length = nl.snapshotLength;
for (var i = 0, l = nl.snapshotLength; i < l; i++) {
nl[i] = nl.snapshotItem(i);
}
return nl;
} else {
return nl.snapshotItem(0);
}
};
}
//###################################################
// Syntax Highlight
//###################################################
let syntaxHighlight = function() {
// Gets code blocks.
var pre = document.getElementsByTagName('pre');
if (pre.length == 0) return; // コード ブロックが見当たらなければ、終了
// Add class to code blocks.
Array.forEach(pre, function(p){p.className += ' brush: vb;';});
// Add styles.
GM_addStyle(GM_getResourceText('CSS'));
/*
後から読み込まれるスタイルに背景白の!important指定があるため、
それよりさらに優先度を上げる目的で、子孫セレクタを重ねます。
もっとスマートなやり方があるかも…。
*/
GM_addStyle('\
div[class="syntaxhighlighter nogutter vb"]\
>table>tbody>tr>td.code>div.container>div.line\
{background-color: whiteSmoke !important;}'
);
// Default settings
SyntaxHighlighter.defaults['gutter'] = SyntaxHighlighter.defaults['toolbar'] = false;
// Highlight
SyntaxHighlighter.all();
};
//###################################################
// Avatar (via twitter)
//###################################################
let avatar = function(a) {
// Check for one-time execution.
if (Array.some(document.styleSheets, function(e){return e.cssRules[0].selectorText == 'img.avatar';})) return;
if (document.getElementsByClassName('avatar').length) return;
// Add styles.
GM_addStyle('img.avatar{height:24px;width:24px;margin-right:4px;}');
/*
画像の読み込みが完了する前に IMG 要素がレンダリングされるため、
ロードに時間がかかると (そして twitter の場合、しばしば
時間がかかる) 本来アバターが表示されるべき場所に、一時的とはいえ
代替イメージが表示されてしまう。これがかなり見栄えを損なう。
そのため、ここでは、何らかの理由でアバター画像を表示できない場合
(ローディング中を含む)、要素自体を非表示にする方針で対処する。
*/
GM_addStyle('\
img:-moz-any(:-moz-loading, :-moz-broken, \
:-moz-user-disabled, :-moz-suppressed)\
{display: none !important;}'
);
// define twitter screen names
var twitterScreenNames = {
/* moug handle : twitter screen name */
'YU-TANG' : 'yu_tang_',
'hatena' : 'hatena19',
'マコ ' : 'Mako_Misaki',
'kumatti' : 'kumatti1',
'きぬよ&あさみ': 'kinuasa',
'yamaV1.02β' : 'yamav102',
'月' : 'honda0510'
};
// Get list node.
var e = GM_xpath({
path: a.path,
all : true
});
/*
https://dev.twitter.com/docs/api/1/get/users/profile_image/:screen_name
https://api.twitter.com/1/users/profile_image?screen_name=twitterapi&size=bigger
bigger - 73px by 73px
normal - 48px by 48px
mini - 24px by 24px
original - undefined.
*/
for (var i = 0, l = e.length, s1, s2, tsn; i < l; ++i) {
s1 = e[i].textContent; // ex. "きぬよ&あさみ"
s1 = a.re.exec(s1);
if (tsn = twitterScreenNames[s1[2]]) {
s2 = e[i].innerHTML; // ex. "きぬよ&amp;あさみ"
s2 = a.re.exec(s2);
e[i].innerHTML =
( s2[1] || '' ) /* s2[1] が無い場合は undefined になるため */
+ '<img src="https://api.twitter.com/1/users/profile_image?screen_name='
+ tsn
+ '&size=mini" class="avatar" alt="" title="アバター" />'
+ '<span class="user-name">' + s2[2] + '</span>';
}
}
};
//###################################################
// Post Id
//###################################################
let postId = function() {
// Check for one-time execution.
if (document.getElementById('1')) return;
// Add styles.
GM_addStyle('span.postId{margin-left:1em;} span.postId:before{content:"#";}');
// Get list node.
var e = GM_xpath({
path: "//span[@class='small' and starts-with(., '投稿日時:')]",
all : true
});
for (var i = 0, l = e.length, span; i < l; ++i) {
span = document.createElement("span");
span.className = "postId small";
span.innerHTML = (i + 1).toString();
e[i].parentNode.appendChild(span);
}
/*
レス番の span 自体に id を付けると、#n で移動したときに
微妙に上が見切れるので、別途テーブルに id を付与する
*/
e = GM_xpath({
path: "//table[@class='response']",
all : true
});
for (i = 0, l = e.length; i < l; ++i) {
e[i].id = (i + 1).toString();
}
/*
# アンカー指定のURLだった場合、初期状態ではアンカーが存在しないので
JavaScript で移動させる。
アンカー指定の主なバリエーションは、下記の通り。
(1)NG: http://www.moug.net/faq/viewtopic.php#2?t=123456&highlight=hoge
(2)Ok: http://www.moug.net/faq/viewtopic.php?t=123456&highlight=hoge&#2
(3)Ok: http://www.moug.net/faq/viewtopic.php?t=123456#2&highlight=hoge
*/
var hash = window.location.hash;
if (hash != '') {
var re = /^#(\d+).*$/;
if (re.test(hash)) {
hash = hash.replace(re, '$1');
e = document.getElementById(hash);
if (e) e.scrollIntoView(true);
}
}
};
//###################################################
// Keyboard Shortcut
//###################################################
/*
kbsUtil - キーボード ショートカット ユーティリティ
==================================================
キーボード ショートカットに割り当てられた関数から参照されます。
moug のレイアウトは固定幅なので、要素の位置を毎回取得しなおすのは
無駄です。そのため、本オブジェクト内で初回に取得した位置情報を保持
して、二回目以降はその情報を使いまわすこととします。
もしキーボード ショートカットが一度も使用されなければ、要素の情報
自体を取得しません。
shortcut - ショートカット オブジェクト
======================================
フックしたいキーコード: 関数オブジェクト。
*/
switch (route) {
case "viewforum":
// ...
case "viewtopic":
var kbsUtil = new function(){
var _info = [];
var _char = '';
var _prevChar = '';
var _ensureInit = function(){
var bg = document.getElementById('cheatSheetBg');
if (bg && bg.style.display != 'none') {
bg.style.display = 'none';
}
if (_info.length > 0) return;
var tbl = GM_xpath({
path: "//table[@class='response']",
all : true
});
var re = /^投稿者:(?:&nbsp;|\s)(?:<img [^>]+>)?(.+)$/;
var rect = tbl[0].getBoundingClientRect();
var offset = rect.top - tbl[0].offsetTop;
for (var i = 0, l = tbl.length, poster, posterHTML; i < l; ++i) {
poster = GM_xpath({
path: ".//th[contains(@class,'messageHeaderR')]/div/span[@class='small']",
node: tbl[i]
});
posterHTML = poster.innerHTML.replace(re, '$1');
poster = poster.textContent.replace(re, '$1');
_info.push({
index: i,
element: tbl[i],
offsetTop: tbl[i].offsetTop,
top: Math.round(tbl[i].offsetTop + offset),
posterHTML: posterHTML,
poster: poster
});
}
};
/*
_ensureInit();
ここだと初期化のタイミングが早すぎてバグる。各ショートカット実行時に初期化すること。
*/
var _getScreenTop = function(){
return Math.round(_info[0].top - _info[0].element.getBoundingClientRect().top);
};
var _getCurrentPostInfo = function(){
var screenTopLine = _getScreenTop();
for (var i = 0, l = _info.length; i < l; ++i) {
if (_info[i].top > screenTopLine){
return i > 0 ? _info[--i] : _info[0];
}
}
// ここに来るのは、画面上端より下の要素が見つからなかった場合なので、末尾要素情報を返す
return _info[_info.length - 1];
};
this.next = function(){
_ensureInit();
var screenTopLine = _getScreenTop();
for (var i = 0, l = _info.length; i < l; ++i) {
if (_info[i].top > screenTopLine) return _info[i].element;
}
// ここに来るのは、画面上端より下の要素が見つからなかった場合なので、フッターを返す
return document.getElementById("footer");
};
this.prev = function(){
_ensureInit();
var screenTopLine = _getScreenTop();
for (var i = _info.length - 1, l = 0; i >= l; --i) {
if (_info[i].top < screenTopLine) return _info[i].element;
}
// ここに来るのは、画面上端より上の要素が見つからなかった場合なので、ヘッダーを返す
return document.getElementById("header");
};
this.nextSamePoster = function(){
_ensureInit();
var currentPostInfo = _getCurrentPostInfo();
var poster = currentPostInfo.poster;
for (var i = currentPostInfo.index + 1, l = _info.length; i < l; ++i) {
if (_info[i].poster == poster) return _info[i].element;
}
// ここに来るのは、見つからなかった場合なので、nullを返す
return null;
};
this.prevSamePoster = function(){
_ensureInit();
var currentPostInfo = _getCurrentPostInfo();
var poster = currentPostInfo.poster;
for (var i = currentPostInfo.index - 1, l = 0; i >= l; --i) {
if (_info[i].poster == poster) return _info[i].element;
}
// ここに来るのは、見つからなかった場合なので、nullを返す
return null;
};
this.first = function(){
_ensureInit();
return _info[0].element;
};
this.last = function(){
_ensureInit();
return _info[_info.length - 1].element;
};
this.showCheatSheet = function(){
const HEIGHT = 280, WIDTH = 200;
var bg = document.getElementById('cheatSheetBg');
if (bg) {
bg.style.display = (bg.style.display != 'none' ? 'none' : 'block');
} else {
bg = document.createElement('div');
bg.id = 'cheatSheetBg';
bg.setAttribute( 'style', '\
position: fixed;\
top: 0;\
left: 0;\
right: 0;\
bottom: 0;\
background: none repeat scroll 0 0 rgba(0, 0, 0, 0.5);\
' );
bg.setAttribute( 'onClick', 'this.style.display = "none";' );
document.body.appendChild(bg);
var cs = document.createElement('div');
cs.id = 'cheatSheet';
cs.innerHTML = '\
<p id="shortcut-title" style="text-align:center;">\
キーボード ショートカット</p><hr />\
<p><code class="shortcut-key">J</code>次の投稿</p>\
<p><code class="shortcut-key">K</code>前の投稿</p>\
<p><code class="shortcut-key">H</code>現在の投稿者の次の投稿</p>\
<p><code class="shortcut-key">L</code>現在の投稿者の前の投稿</p>\
<p><code class="shortcut-key">U</code>最初の投稿</p>\
<p><code class="shortcut-key">N</code>最後の投稿</p>\
<p><code class="shortcut-key">0</code>~<code class="shortcut-key">9</code>n 番目の投稿</p>\
<p style="margin-left:1em;font-size:x-small;">※ 連続入力で二桁番目の投稿指定</p>\
<p><code class="shortcut-key">?</code>ショートカットのヘルプ</p>\
';
cs.setAttribute( 'style', '\
position: fixed;\
width: ' + WIDTH + 'px;\
height: ' + HEIGHT + 'px;\
margin: auto;\
top: 0;\
right: 0;\
bottom: 0;\
left: 0;\
background: white;\
border-radius: 10px;\
padding: 10px;\
box-shadow: 2px 2px 10px #000;\
' );
bg.appendChild(cs);
}
};
this.__defineGetter__("char", function() { return _char; });
this.__defineSetter__("char",
function(c) {
[_prevChar, _char] = [_char, c];
if (c != '?') {
var bg = document.getElementById('cheatSheetBg');
if (bg && bg.style.display != 'none') bg.style.display = 'none';
}
}
);
this.__defineGetter__("prevChar", function() { return _prevChar; });
this.prevCharPressedTime = 0;
this.getPostsLength = function(){_ensureInit();return _info.length};
this.getPostInfo = function(index){_ensureInit();return _info[index]};
};
var shortcut = {
/* 0-9 : jump to post id */
'0': function(){
var jumpTo = kbsUtil.char;
var end = Date.now();
var elapsed = end - kbsUtil.prevCharPressedTime; // ミリ秒での時間
if (elapsed < 1000) { // 1 秒以内の場合
if (/^\d$/.test(kbsUtil.prevChar)){ // 数値の場合
var i = Number(kbsUtil.prevChar + kbsUtil.char);
var j = kbsUtil.getPostsLength();
if (i > 0 && i <= j){
jumpTo = i.toString();
}
}
}
if (jumpTo != '0') {
var e = kbsUtil.getPostInfo(Number(jumpTo) - 1);
if (e) e.element.scrollIntoView(true);
}
},
/* "j" : next post */
'j': function(){
kbsUtil.next().scrollIntoView(true);
},
/* "k" : prev post */
'k': function(){
kbsUtil.prev().scrollIntoView(true);
},
/* "h" : next post by same poster */
'h': function(){
var post = kbsUtil.nextSamePoster();
if (post) post.scrollIntoView(true);
},
/* "l" : prev post by same poster */
'l': function(){
var post = kbsUtil.prevSamePoster();
if (post) post.scrollIntoView(true);
},
/* "u" : first post */
'u': function(){
kbsUtil.first().scrollIntoView(true);
},
/* "n" : last post */
'n': function(){
kbsUtil.last().scrollIntoView(true);
},
/* "?" : show cheat sheet */
'?': function(){
kbsUtil.showCheatSheet();
}
};
for (let i = 49; i < 58; ++i) { shortcut[String.fromCharCode(i)] = shortcut['0'];} // '1'-'9' = '0'
}
let setKbShortcut = function() {
document.addEventListener("keypress", function(e) {
var pressed = String.fromCharCode(e.which); /* 大文字と小文字を意図的に揃えていないので、拾う側で注意 */
kbsUtil.char = pressed;
// フォーカスが検索テキストボックスに有った場合は、入力の邪魔をしない。
// また Alt キーや Ctrl キーが押下されていた場合も、邪魔をしない。
if ( e.target.tagName.toLowerCase() == 'input'
|| e.altKey
|| e.ctrlKey
|| e.metaKey )
return;
if (shortcut.hasOwnProperty(pressed)) {
shortcut[pressed]();
}
kbsUtil.prevCharPressedTime = Date.now();
}, true);
window.addEventListener("scroll", function(e) {
//GM_log("scroll event detected! "+window.pageXOffset+" "+window.pageYOffset);
}, true);
window.addEventListener("resize", function(e) {
//GM_log("resize event detected! "+window.innerWidth +" "+window.innerHeight );
}, true);
};
let putStyles4KbShortcut = function() {
GM_addStyle('div#cheatSheet>p{line-height: 2em;}');
GM_addStyle('p#shortcut-title{font-weight:bold;}');
GM_addStyle('code.shortcut-key{margin: auto 0.8em; \
-moz-appearance:button;padding:3px 7px;}');
};
//###################################################
// Main
//###################################################
switch (route) {
case "viewforum":
if (cfg.get('twitter_avatar')) {
avatar({
path: "//td[position()=3]/div | //td[position()=4]", // [トピック] 列 || [最新投稿者] 列
re: /(\S+:&nbsp;|\S+:\s)?(\S+)/
});
}
break;
case "viewtopic":
if (cfg.get('syntax_highlight'))
syntaxHighlight();
if (cfg.get('twitter_avatar')) {
avatar({
path: "//th[contains(@class,'messageHeaderR')]/div/span[@class='small']",
re: /^(投稿者:&nbsp;|投稿者:\s)(.+)$/
});
}
if (cfg.get('post_id'))
postId();
if (cfg.get('shortcut')) {
putStyles4KbShortcut();
setKbShortcut();
}
break;
//default: // search, profile, posting, ...
// @include が ".../view*" になっているため、他は入ってこないはず。
// 投稿時 (posting) は、アクセス頻度が少ないと思われるため、非対応とします。
}
} // スクリプト終了
@yu-tang
Copy link
Author

yu-tang commented Mar 27, 2012

@honda0510

5回ロードしても、スコープはそれぞれ別のようですけど、それでも気になりますか?

ああ、気になる理由をちゃんと書いてなかったですね。すみません。
サンドボックスに分離されるのは知っていたので、干渉とかは別に心配していませんでした。動作に支障があるとは思っていません。
単純に、jQuery をロードするだけでもコストがかかる(遅いとか重いとか)はずなので、そこが気分的に引っかかっているということです。
dotjs のように、フレームワーク自体が標準で jQuery を用意してくれている場合は、まだマシだと思うんですが、Greasemonkey のように jQuery と言っても単なる require ファイル扱いだと、何ら最適化はしてもらえないと思うんですよね。それが(たとえば)5 回、しかも別々の場所からロードってなると、ちょっとしたペナルティになったりしないかな…という感想です。
もっともローカルからのロードなので、ネットワーク越しではない分はるかにマシだとは思うのですが。

あと、自分は JavaScript あんまり触わっていなかったので、心理的に jQuery 依存度が低いのです。無くてもあんまり気にならないレベル。たぶん、Web 開発者だったらアウトだと思いますがw

あと、@ requireしなくても2回実行されるようです。

それは Greasemonkey 固有なのかもしれないですね。
自分が Scriptish でテストしたときは、1回だけでした。

@honda0510
Copy link

単純に、jQuery をロードするだけでもコストがかかる(遅いとか重いとか)はずなので、そこが気分的に引っかかっているということです。

なるほどです。

あと、自分は JavaScript あんまり触わっていなかったので、心理的に jQuery 依存度が低いのです。無くてもあんまり気にならないレベル。

jQueryは各ブラウザ毎の挙動の差異を吸収してくれるので楽なんですよね。

あと、@ requireしなくても2回実行されるようです。

それは Greasemonkey 固有なのかもしれないですね。
自分が Scriptish でテストしたときは、1回だけでした。

マジっすか。それは Scriptish に乗り換えたくなりますね。Scriptish、あとで試してみます。
あと、何回実行するかはページにもよるみたいです。https://www.google.co.jp/ では1回だけでした。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment