// ==UserScript== | |
// @name CL&U Hacks | |
// @namespace stackexchange | |
// @description CL&U Hacks (derived from JL&U Hacks) | |
// @include http://*chinese.stackexchange.com/* | |
// ==/UserScript== | |
$(function () { | |
var DEBUG_MODE = false; // disable this before release! | |
//ruby mode regex | |
var replaces = new RegExp( | |
"(?:[|[\\[\u3010])([^\\]]+)[\\]\u3011][{\uff5b]([^}]+)[}\uff5d]|([\u4e00-\ufeed\u3003\u3004\u3005\u3006\u3007\u3012]+)\\s*[A-z\\u0100-\\u017F\\u0180-\\u024F\\u3100-\\u312F\\u31A0-\\u31BF0-9{\u3010\uff5b]([.\u3001\u3002\uff0d-\uff0f\uff61\uff1c\uff1e\uff08\uff09\\(\\)\u226a\u226b\uff1b;\uff1a:\uff01!\uff1d=\u2261\u2260\u2252\uff04\uffe5\uff1f\\?\uff06\uff03#\uff20@\u201c\u2018\u201d\u2019]+)[}\u3011\uff5d]", "g"), | |
cache = []; | |
var ruby = { | |
start: function () { | |
this.addEditHelp(); | |
this.addMenu(); | |
this.addOptionsBox(); | |
this.addStyles(); | |
if (this.mode == "uDisableRubyEngine") { | |
// don't run if ruby disabled altogether | |
return; | |
} | |
this.parse(); | |
setInterval($.proxy(this.parse, this), 400); | |
}, | |
addCSS: function (css) { | |
var head = document.getElementsByTagName('head')[0], | |
style = document.createElement('style'); | |
style.type = 'text/css'; | |
if (style.styleSheet){ | |
style.styleSheet.cssText = css; | |
} else { | |
style.appendChild(document.createTextNode(css)); | |
} | |
head.appendChild(style); | |
}, | |
htmlEscape: function (i) { | |
return i.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | |
}, | |
loadMode: function () { | |
try { | |
var mode = localStorage["uRubyMode"+(DEBUG_MODE ? "_dbg" : "")], | |
allowedModes = $("input[name=uRubyMode]").map(function() { | |
return this.id; | |
}); | |
// Only use the value in localStorage if it's a recognized value | |
return ($.inArray(mode, allowedModes) !== -1) ? mode : "uAccentedPinyin"; | |
} catch(e) { | |
return "uAccentedPinyin"; | |
}; | |
}, | |
saveMode: function () { | |
try { | |
localStorage["uRubyMode"+(DEBUG_MODE ? "_dbg" : "")] = $('input[name=uRubyMode]:checked', '#uRubyModeForm').attr("id"); | |
location.reload(); | |
} catch(e) { | |
alert("Options can only be changed when localStorage is available. Please check your browser settings."); | |
}; | |
}, | |
addEditHelp: function() { | |
// Add to the editor help at http://japanese.stackexchange.com/questions/ask etc | |
// but only for the Japanese stack exchange, as it probably isn't as relevant for anime.se | |
if (location.href.indexOf('chinese.stackexchange') != -1) { | |
$('<p><span class="dingus">►</span> pinyin ruby <code class="noRuby">拼音【pin1yin1】</code>, <code class="noRuby">拼音{pin1yin1}</code> or <code class="noRuby">[拼音]{pin1yin1}</code></p>').insertBefore("#how-to-format p.ar"); | |
} | |
}, | |
addMenu: function () { | |
//add menu | |
if (!$("#upopup").length) { | |
var addTo = $("#footer-menu .top-footer-links,.footer-links"); // .footer-links for mobile | |
addTo.prepend( | |
'<a id="upopuphyperlink">ruby options</a><div id="upopup" style="color:black;position:absolute;'+ | |
'display:none;background-color:#fff;border:1px solid #ccc;margin-top:3px;padding:5px;z-index:500;'+ | |
'box-shadow:1px 1px 2px rgba(0,0,0,0.2);"/>' | |
); | |
$('#upopuphyperlink').click(function() { | |
$('#upopup').toggle(); | |
$("#upopup").css('top', $('#upopuphyperlink').offset().top - $("#upopup").height() - 10); | |
$("#upopup").css('left', $('#upopuphyperlink').offset().left); | |
}); | |
}; | |
}, | |
addOptionsBox: function () { | |
//add options box | |
$("#upopup").html(""); | |
$("#upopup").append( | |
'<div>' + | |
'<h4>ruby mode</h4>' + | |
'<form id="uRubyModeForm">'+ | |
'<input type="radio" name="uRubyMode" id="uDisableRubyEngine"><label for="uDisableRubyEngine"> don\'t convert pinyin ruby markup (leave as-is)</label></input><br>' + | |
'<input type="radio" name="uRubyMode" id="uDisableRuby"><label for="uDisableRuby"> don\'t show any pinyin ruby</label></input><br>' + | |
'<input type="radio" name="uRubyMode" id="uAccentedPinyin"><label for="uAccentedPinyin"> pinyin ruby using tone accents (default)</label></input><br>' + | |
'<input type="radio" name="uRubyMode" id="uNumberedPinyin"><label for="uNumberedPinyin"> pinyin ruby using tone numbers</label></input><br>' + | |
'<input type="radio" name="uRubyMode" id="uMouseOver"><label for="uMouseOver"> hide pinyin texts, only show when mousing over hanzi</label></input><br>' + | |
'</form>'+ | |
'<input type="button" id="usave" value="save and reload"/> <input type="button" onclick="$(\'#upopup\').hide()" value="close"/>' + | |
'</div>' | |
).find("div").css( | |
{ 'color': '#000', 'textAlign': 'left', 'lineHeight': '1.9em' } | |
).find("a").css( | |
{ 'color': 'blue', 'margin': 0 } | |
); | |
//restore previous settings/register save settings event etc | |
this.mode = this.loadMode(); | |
$('#'+this.mode).prop('checked', true); | |
$("#usave").click($.proxy(this.saveMode, this)); | |
}, | |
addStyles: function() { | |
var alignCSS = "rt,ruby,rb{text-align:center;ruby-align:center;}"; | |
if (!/IE|Chrome|AppleWebKit|Safari/.test(navigator.userAgent)) { | |
this.nonNativeRuby = true; | |
this.addCSS( | |
'ruby{display:inline-table;text-indent:0;white-space:nowrap;margin:0;padding:0;line-height:1em;border:none;vertical-align:text-bottom;}' + | |
'rb{display:table-row-group;line-height:1em;height:1em;font-size:1em;border:none;margin:0;padding:0;white-space:nowrap; overflow: hidden;}' + | |
'rt{display:table-header-group;font-size:0.75em;line-height:1em;height:1em;white-space:nowrap;border:none;margin:0;padding:0;overflow:hidden;}' + | |
alignCSS | |
); | |
} else { | |
//add different css for WebKit browsers (Chrome/Safari/Opera 15) | |
//& IE (as they support ruby natively) | |
this.addCSS( | |
'ruby{line-height:1;height:1em;}' + | |
'rb{line-height:1;}' + | |
'rt{font-size:0.7em;line-height:1.1;}' + | |
alignCSS | |
); | |
}; | |
this.addCSS( | |
'span.hiddenruby:hover,span.hiddenruby-rp:hover{background:#CCFFCC;}'+ | |
'span.hiddenruby,span.hiddenruby-rp{cursor:default;border-bottom:1px dashed gray;}'+ | |
'span.tone-h{border-top:1px solid red;}'+ | |
'span.tone-l-change{border:solid red;border-width:0 0 1px 1px;}'+ | |
'span.tone-l{border-bottom:1px solid red;}'+ | |
'span.tone-h-change{border:solid red;border-width:1px 0 0 1px;}'+ | |
'.ushadow,#upop,.upop{-webkit-box-shadow: 0px 0px 7px rgba(50, 50, 50, 0.2);-moz-box-shadow: 0px 0px 7px rgba(50, 50, 50, 0.2);box-shadow: 0px 0px 7px rgba(50, 50, 50, 0.2);}'+ | |
'#upop,.upop{padding:2px 5px;font-size:1.3em;background:#FFFFF0;border-radius:3px;border:1px solid #ccc;white-space:nowrap;}' | |
); | |
}, | |
parse: function () { | |
//a.question-hyperlink h2 is for hyperlinks on mobile | |
//div.excerpt is for browsing through question summaries | |
$("span,code:not(.noRuby),p,li,b,i,em,strong,a,div.excerpt,a.question-hyperlink h2").contents() | |
.filter(function () { | |
return ( | |
this.nodeType == 3 || this.nodeType == 1 | |
) && /[\u3000-\ufeed]/.test(this.data); | |
}) | |
.each(function () { | |
for (var i = 0; i < cache.length; i++) { | |
if (cache[i] == this && true) { | |
return; | |
} | |
} | |
var data_escaped = ruby.htmlEscape(this.data), | |
data = data_escaped.replace(replaces, function ($0, $1, $2, $3, $4) { | |
var hanzi = $1 || $3, | |
rb = $2 || $4; | |
//console.log(hanzi+' '+rb); | |
if (ruby.mode == "uMouseOver") { | |
return '<span class="hiddenruby-rp" title="' + rb + '">' + hanzi + '</span>'; | |
} | |
else if (ruby.mode == "uDisableRuby") { | |
return hanzi; | |
} | |
return ruby.rubyize(hanzi, rb); | |
}); | |
if (data != data_escaped) { | |
$(this).replaceWith(data); | |
} | |
cache.push(this); | |
}); | |
if (this.mode == 'uMouseOver') { | |
this.replaceTitleAttrs(); | |
} | |
if (this.nonNativeRuby && /Opera|Firefox/.test(navigator.userAgent)) { | |
$('.ruby-rp').each(function() { | |
// Using px values for vertical-align is more accurate, but isn't supported in all browsers. | |
// For this reason, it'll only be used for Firefox and Opera with the Presto engine | |
// (as they worked when I tested them). Unfortunately, this still has rounding issues | |
// when in tandem with the browsers' rendering, so it can be out by a pixel or so. | |
var amount = Math.round($(this).height()/2.0)+1; | |
if (amount) { | |
$(this).removeClass('ruby-rp'); | |
$(this).css('verticalAlign', amount+'px'); | |
}; | |
}); | |
} | |
}, | |
rubyize: function (hanzi, pinyin) { | |
function getRuby(hanzi, pinyin) { | |
return '<ruby class="ruby-rp"><rb>' + hanzi + '</rb><rt>' + pinyin + '</rt></ruby>'; | |
} | |
var pinyins = []; | |
function onReplace(a, pinyin, tone) { | |
// Split each pinyin "part" using the numbered tones or spaces | |
var p = (pinyin||'') + (tone||''); | |
if (p) { | |
if (ruby.mode == 'uAccentedPinyin') { | |
// convert numbered pinyin to accented pinyin | |
p = decode_pinyin(p); | |
} | |
pinyins.push(p); | |
} | |
//console.log(pinyin+' '+tone) | |
return ''; | |
} | |
var t = pinyin.replace(/([^0-5 ]*)([0-5 ]*)/g, onReplace), | |
o = ''; | |
if (hanzi.length == pinyins.length) { | |
for (var i=0; i<Math.min(hanzi.length, pinyins.length); i++) { | |
o += getRuby(hanzi.charAt(i), pinyins[i]); | |
} | |
} | |
else { | |
o = getRuby(hanzi, pinyins.join('')); | |
} | |
return o; | |
}, | |
replaceTitleAttrs: function () { | |
// add faster popup windows as "title=" takes some time to appear | |
// "title=" also usually isn't usable on tablets | |
$('.hiddenruby-rp').each(function (e) { | |
// go through the "rp", aka "reprocess" classes | |
$(this).removeClass('hiddenruby-rp').addClass('hiddenruby'); | |
var title = String(this.title), | |
upop; | |
this.title = ''; | |
$(this).mouseover(function(e) { | |
if (upop) { | |
// just to be sure... | |
upop.remove(); | |
upop = null; | |
} | |
$(document.body).append('<div id="upop" class="upop">'+ruby.htmlEscape(title)+'</div>'); | |
upop = $("#upop"); | |
upop.attr("id", ""); | |
upop.css("position", "absolute"); | |
ruby.updateUPopPos(upop, e); | |
}).mousemove(function(e) { | |
if (!upop) { | |
return; | |
} | |
ruby.updateUPopPos(upop, e); | |
}).mouseout(function() { | |
if (!upop) { | |
return; | |
} | |
upop.remove(); | |
upop = null; | |
}); | |
}); | |
}, | |
updateUPopPos: function (upop, e) { | |
var setTo = { | |
top: e.pageY+20, | |
left: e.pageX+20, | |
right: 'auto', | |
maxWidth: $(window).width() | |
}; | |
if ((setTo.left+12+upop.width()) > $(window).width()) { | |
// Especially in the mobile website, | |
// make sure popup isn't offscreen | |
setTo.left = 'auto'; | |
setTo.right = 0; | |
}; | |
upop.css(setTo); | |
} | |
} | |
// from http://stackoverflow.com/questions/8200349/convert-numbered-pinyin-to-pinyin-with-tone-marks | |
var PinyinToneMark = { | |
0: "aoeiuv\u00fc", | |
1: "\u0101\u014d\u0113\u012b\u016b\u01d6\u01d6", | |
2: "\u00e1\u00f3\u00e9\u00ed\u00fa\u01d8\u01d8", | |
3: "\u01ce\u01d2\u011b\u01d0\u01d4\u01da\u01da", | |
4: "\u00e0\u00f2\u00e8\u00ec\u00f9\u01dc\u01dc" | |
} | |
function decode_pinyin(s) { | |
s = s.toLowerCase(); | |
var r = "", | |
t = ""; | |
for (var i=0; i<s.length; i++) { | |
var c = s.charAt(i); | |
if (/[a-z]/.test(c)) { | |
t += c; | |
} | |
else if (c === ':') { | |
if (t.charAt(t.length-1) === 'u') { | |
t = t.slice(0, -1) + "\u00fc"; | |
} | |
else { | |
t += c; | |
} | |
} | |
else { | |
if (c in PinyinToneMark || c === '5') { | |
var tone = parseInt(c) % 5; | |
if (tone !== 0) { | |
var m = /[aoeiuv\u00fc]+/g.exec(t); | |
if (!m) { | |
t += c; | |
} | |
else if (m[0].length == 1) { | |
var start = m.index, | |
end = start+m[0].length; | |
t = t.slice(0, start) + PinyinToneMark[tone][PinyinToneMark[0].indexOf(m[0])] + t.slice(end); | |
} | |
else { | |
if (t.indexOf('a') !== -1) { | |
t = t.replace(/a/g, PinyinToneMark[tone][0]); | |
} | |
else if (t.indexOf('o') !== -1) { | |
t = t.replace(/o/g, PinyinToneMark[tone][1]); | |
} | |
else if (t.indexOf('e') !== -1) { | |
t = t.replace(/e/g, PinyinToneMark[tone][2]); | |
} | |
else if (/.*ui$/.test(t)) { | |
t = t.replace(/i/g, PinyinToneMark[tone][3]); | |
} | |
else if (/.*iu$/.test(t)) { | |
t = t.replace(/u/g, PinyinToneMark[tone][4]); | |
} | |
else { | |
t += "!"; | |
} | |
} | |
} | |
} | |
r += t; | |
t = ""; | |
} | |
} | |
r += t; | |
return r; | |
} | |
ruby.start(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment