Skip to content

Instantly share code, notes, and snippets.

@cyphr
Created November 20, 2013 12:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cyphr/7562487 to your computer and use it in GitHub Desktop.
Save cyphr/7562487 to your computer and use it in GitHub Desktop.
// ==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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
},
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&nbsp;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