creamidea / Youdao Dictionary Enhancer

// ==UserScript==
// @name         Youdao Dictionary Enhancer
// @namespace    http://tampermonkey.net/
// @homepage     https://github.com/creamidea/YoudaoDictionaryEnhancer
// @version      1.2.3
// @description  Search words in Celerity
// @author       creamidea
// @match        http://*.youdao.com/*
// @require      http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js
// @require      http://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.js
// @resource     nprogress_css http://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css
// @resource     etymoline_css http://www.etymonline.com/style.css
// @resource     etymoline_font http://fonts.googleapis.com/css?family=Slabo+27px:400&lang=en
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_xmlhttpRequest
// @connect      www.etymonline.com
// @connect      www.google.com
// @updateURL    https://openuserjs.org/meta/creamidea/Youdao_Dictionary_Enhancer.meta.js
// ==/UserScript==

// changelog:
// version 1.2 add record of google define
// version 1.1 add translation function in etymoline area
// version 1.0 initial release

GM_addStyle(GM_getResourceText("nprogress_css"));
GM_addStyle('body{font-famile:"Hiragino Sans GB",STHeiti,"Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Micro Hei Mono","WenQuanYi Zen Hei","WenQuanYi Zen Hei Mono",LiGothicMed}');
GM_addStyle('.youdao-trans-icon {position: absolute;border-radius: 5px;padding: 3px; background-color: rgb(245, 245, 245);box-sizing: content-box;cursor: pointer;height: 18px;width: 18px;z-index: 2147483647;border: 1px solid rgb(220, 220, 220);color: rgb(51, 51, 51);}');
GM_addStyle('.etymoline .hint {text-align: center;font-size: 24px;margin: 24px 0;color: rebeccapurple;}');
GM_addStyle('#container{background: #f6f4ec;border-radius: 6px;box-shadow: 2px 2px 9px 1px gray;padding-left: 16px;padding-right: 16px;padding-bottom: 26px;margin-top: 16px;}');
GM_addStyle('.c-topbar-wrapper.setTop{top:0 !important;box-shadow: 2px 2px 4px #e5e5e5;}');
GM_addStyle('#phrsListTab h2.wordbook-js{overflow: visible;}');
GM_addStyle('.keyword{font-family: Georgia,"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; font-size: 39px;border-bottom: 2px gray dotted; line-height: 1;}');
GM_addStyle('#phrsListTab .trans-container>ul{font-size: 16px;} #phrsListTab .trans-container>ul>li{margin: 4px auto;}');
GM_addStyle('li .collinsMajorTrans{background: gainsboro !important;}');
GM_addStyle('.c-topbar-wrapper{box-shadow: 0 0 0 #fcfcfe;}');

(function () {
    'use strict';
    if (NProgress === undefined)
        NProgress = {
            set: function () { },
            start: function () { },
            inc: function () { },
            done: function () { },
            configure: function () { },
        };
    var ETYMONLINEHTTP = 'http://www.etymonline.com';
    var GOOGLEHTTP = 'https://www.google.com';
    var YOUDAOHTTP = $(location).attr('protocol') + '//' + $(location).attr('hostname');
    var $scontainer = $('#scontainer');
    var $query = $('#query');
    var $topImgAd = $('#topImgAd');
    var $webTrans = $('#webTrans');
    var injectEtymolineName = 'creamidea' + makeId(9); // make the name of injecting function is unique
    var openURLFunName = 'openurl' + makeId(9);
    var proxySelection = 'proxySelection' + makeId(9);
    var sltContainerName = 'selectionContainer' + makeId(9);
    var youdaoSearchButtonId = 'youdaoSearchButton' + makeId(9);
    var googleFrameId = 'googleResult' + makeId(9);

    // remove the ad
    $topImgAd.remove();
    $('#baidu-adv').remove();
    $('#follow').remove();

    // window global function. It is the callback in the iframe
    window[injectEtymolineName] = function (event) {
        // open the next page in the etymoline.com iframe
        event.preventDefault();
        var target = event.target;
        request(ETYMONLINEHTTP + target.attributes.href.value);
    };
    window[openURLFunName] = function (event) {
        event.preventDefault();
        location.href = event.target.attributes.href.value;
    };
    window[proxySelection] = function (event) {
        var sel = window.getSelection();
        var range = document.createRange();
        var targetSel = this.getSelection();
        var $sltContainer = $('#' + sltContainerName);
        var selectionText = encodeURIComponent(targetSel.toString());
        var $youdaoSearchButton = $('#' + youdaoSearchButtonId);
        if ($sltContainer.length === 0)
            $sltContainer = $('<div id=' + sltContainerName + ' />').appendTo('body');
        if ($youdaoSearchButton.length === 0)
            $youdaoSearchButton = $('<butotn id=' + youdaoSearchButtonId + ' class="youdao-trans-icon">').append('<a href="javascript: void(0)"><img src="http://shared.ydstatic.com/images/favicon.ico"></a>').appendTo('body');
        if (selectionText === "") {
            $youdaoSearchButton.css({ display: 'none' });
            return;
        }

        $sltContainer.css({ position: "absolute", zIndex: -1, top: "-1000px" }).text(selectionText);
        setTimeout(function () {
            $youdaoSearchButton.find('a').attr('href', YOUDAOHTTP + '/w/' + selectionText + '/')
                .end().css({ left: $('#' + injectEtymolineName).data('click-x'), top: $('#' + injectEtymolineName).data('click-y'), display: 'block' });
        }, 24);

        range.selectNode($sltContainer[0]);
        sel.removeAllRanges();
        sel.addRange(range);
        // range.setStart(targetSel.anchorNode, targetSel.anchorOffset);
        // range.setEnd(targetSel.focusNode, targetSel.focusOffset);
        // sel.removeAllRanges();
        // sel.empty();
        // sel.setBaseAndExtent(targetSel.anchorNode, targetSel.anchorOffset, targetSel.focusNode, targetSel.focusOffset);
        // window.getSelection().anchorNode.textContent.substring(this.getSelection().extentOffset, this.getSelection().anchorOffset);
    };

    // create the frame wrapper
    var $frameWrapper =
        $('<div id=' + injectEtymolineName + '-wrapper class="etymoline"/>').css({ border: 0, width: '100%' }).html(
            '<div class="hint">Etymoline.com ...</div>');
    // var $googleFrameWrapper =
    //    $('<div id=' + googleFrameId + '-wrapper class="google-frame"/>').css({ border: 0, width: '100%' }).html(
    //        '<div class="hint">google.com ...</div>');
    if ($webTrans.length === 0) return; // maybe no result :)

    $frameWrapper.insertBefore($webTrans);
    // $googleFrameWrapper.insertBefore($webTrans);

    // set NProgress
    NProgress.configure({ parent: '#' + injectEtymolineName + '-wrapper' });
    NProgress.set(0.7);
    NProgress.inc(0.2);

    // request the etymoline.com page
    var $phrsListTab = $('#phrsListTab');
    var queryWord;
    if ($phrsListTab.length > 0 && $phrsListTab.find('.keyword').length > 0)
        queryWord = $phrsListTab.find('.keyword').text();
    else
        queryWord = $query.val();
    setTimeout(function () { NProgress.start(); request(ETYMONLINEHTTP + '/index.php?term=' + encodeURIComponent(queryWord), etymolineHandler); }, 0);
    setTimeout(function () { request(GOOGLEHTTP + '/search?sclient=psy-ab&hl=en&fp=1&num=1&start=0&q=' + encodeURIComponent('define: ' + queryWord), googleHandler); }, 60);

    // set the default explanation
    $('#webTrans .tabs a').each(function (i, link) {
        if (link.innerText === '英英释义')
            link.click();
    });

    // from: http://stackoverflow.com/a/12444641/1925954
    var keys = {};
    function test_key(selkey) {
        var alias = {
            "Ctrl": 17,
            "Shift": 16,
            "/": 191,
            "a": 65,
            "e": 69
        };
        return keys[selkey] || keys[alias[selkey]];
    }
    function test_keys() {
        var i,
            keylist = arguments,
            status = true;
        for (i = 0; i < keylist.length; i++) {
            if (!test_key(keylist[i])) {
                // status = false;
                return false;
            }
        }
        return status;
    }
    function globalKeydown(event) {
        var keyCode = event.keyCode;
        keys[keyCode] = event.type === 'keydown';
        if (test_keys('Shift', 'e')) {
            if ($('.baav .voice-js')[0]) $('.baav .voice-js')[0].click();
            keys = {};
            return false;
        } else if (test_keys('Shift', 'a')) {
            if ($('.baav .voice-js')[1]) $('.baav .voice-js')[1].click();
            keys = {};
            return false;
        } else if (test_keys('Shift', '/')) {
            // ? => help
            toggleHelp();
            keys = {};
            return false;
        } else if (test_keys('/')) {
            $query.focus().select();
            keys = {};
            return false; // to avoid input '/' in inputbox.
        }
    }
    function globalKeyup(event) {
        var keyCode = event.keyCode;
        keys[keyCode] = false;
    }
    $(document).keydown(globalKeydown).keyup(globalKeyup);

    // adjust the youdao css
    // move top navigation
    $('#container').css({ width: "1000px" });
    $('#results').css({ width: "680px" });
    $('#ads').css({ width: "320px" });
    var transformToggle = [];
    $('#eTransform .tabs').children().each(function (index, elt) {
        transformToggle.push(elt.innerText);
    });
    $('#transformToggle').children().each(function (index, elt) {
        if (elt.id === 'wordGroup') return;
        var $clone = $(elt).clone();
        $clone.addClass('follow').removeClass('hide').css({ display: 'block' }).prepend('<p class="hd">' + transformToggle[index] + '</p>').next().css({ marginTop: "8px" });
        $clone.appendTo('#ads');
    });//.end().parent().remove();
    $('#doc>.c-topbar-wrapper').css({ height: "81px", top: "-42px" }).find('.c-subtopbar').remove();
    $scontainer.css({ marginTop: "42px" });

    function request(url, callback) {
        var xhr = new GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            // anonymous: true,
            onreadystatechange: function (resp) { onreadystatechange(resp, callback); }
        });
    }

    function onreadystatechange(resp, callback) {
        var readyState = resp.readyState;
        if (readyState === 0) {
            // Client has been created. open() not called yet.
        } else if (readyState === 1) {
            // open() has been called.
        } else if (readyState === 2) {
            // send() has been called, and headers and status are available.
        } else if (readyState === 3) {
            // Downloading; responseText holds partial data.
        } else if (readyState === 4) {
            switch (resp.status) {
                case 200:
                    // etymolineHandler(resp.responseText);
                    callback(resp.responseText);
                    break;
                default:
                    GM_log(['Ger Error: ', '\nState: ', resp.readyState, '\nMessage: ', resp.responseText].join(''));
            }
        }
    }

    function etymolineHandler(responseText) {
        var domParser = new DOMParser();
        var doc = domParser.parseFromString(responseText, 'text/html');
        var $dictionary = doc.querySelector('#dictionary');
        $dictionary.style.border = 0;
        $dictionary.style.marginBottom = 0;
        var $frame = $('#' + injectEtymolineName);
        if ($frame.length === 0) {
            $frame = $('<iframe id="' + injectEtymolineName + '" />').css({ border: 0, width: '100%', maxHeight: '240px' });
            $frameWrapper.append($frame);
            $frameWrapper.find('.hint').remove(); // remove the hint.
            $frame.contents().find("head")
                .append('<style>' + GM_getResourceText("etymoline_css") + '</style>')
                .append('<style>' + GM_getResourceText("etymoline_font") + '</style>')
                .append('<style>' +
                '.etymoline-footer {border: 0px;color: wheat;text-align: right;}' +
                '</style>');
            // $frame.contents().on('selectionchange', function () {debugger});
            $frame.contents()
                .on('selectionchange', parent[proxySelection])
                .on('mousemove', function (event) { $frame.data('click-x', $frame.offset().left + event.pageX); $frame.data('click-y', $frame.offset().top + event.pageY - 30); })
                .on('keydown', function (event) { globalKeydown(event); })
                .on('keyup', function (event) { globalKeyup(event); });
        }
        $frame.contents().find("body")
            .html($dictionary)
            .append('<footer class="etymoline-footer">From: <a href="http://www.etymonline.com/index.php" style="color: wheat;" target="_blank">The Online Etymology Dictionary</a></footer>');
        // Some fix
        $frame.ready(function () {
            $frame.css({ height: $frame.contents().find("html").height() });
            $frame.contents().find("body img").map(function (index, img) {
                // the resource path of dictionary png
                img.src = ETYMONLINEHTTP + '/graphics/dictionary.gif';
                return img;
            });
            $frame.contents().find("body a.dictionary").map(function (index, link) {
                // click the png
                link.target = '_blank';
                return link;
            });
            $frame.contents().find("body #dictionary dl a").not('.dictionary').map(function (index, link) {
                // click the word
                // link.onclick = parent[injectEtymolineName];
                var oLink = new URL(link.href);
                var term = oLink.search.slice(1).split('&').map(function (v) { var _v = v.split('='); return { key: _v[0], value: _v[1] }; }).filter(function (v) { if (v.key === 'term') return v; })[0];
                link.href = YOUDAOHTTP + '/w/' + term.value + '/';
                link.onclick = parent[openURLFunName];
                //link.href = link.href.replace(new RegExp(YOUDAOHTTP), ETYMONLINEHTTP);
                //link.target = '_blank';
                // link.href = 'javascript:void(0);';
                // link.style.cursor = 'default';
                return link;
            });
            $frame.contents().find("body .paging a").map(function (index, link) {
                // The code below is just for fun :P
                // var oLink = new URL(link.href);
                // var p = oLink.search.slice(1).split('&').map(function(v){var _v = v.split('=');return {key: _v[0], value: _v[1]}}).filter(function(v){if(v.key==='p')return v;})[0].value
                link.onclick = parent[injectEtymolineName];
                return link;
            });
        });
        NProgress.done();
    }

    function googleHandler(text) {
        // console.log(text);
        var googleDefinationHTML = parseJEAPI(text);
        var domParser = new DOMParser();
        var $doc = domParser.parseFromString(googleDefinationHTML, 'text/html');
        if (!$doc.querySelectorAll('.g.tpo.mod')) return; // has no defination

        var $googleFrame = $('#' + googleFrameId);
        if ($googleFrame.length === 0) $googleFrame = $('<iframe id=' + googleFrameId + ' />').css({ border: 0, width: '616px' }).appendTo('body');

        // trim doc
        // $doc.querySelector('.srg').remove();
        $doc.querySelector('.hd').remove();
        if ($doc.querySelector('hr')) $doc.querySelector('hr').remove();

        $googleFrame.contents().find('head').html($doc.head.innerHTML);
        $googleFrame.contents().find('body').html('<h1>被你发现了 XD</h1>' + $doc.body.innerHTML);
        $googleFrame.css({ height: $googleFrame.contents().height() });

        // after render the html, you can get these information.
        if ($googleFrame.contents().find('.vk_ans')[0] === undefined) return;
        var keyword = $googleFrame.contents().find('.vk_ans')[0].innerHTML;
        $('.keyword').html(keyword);

        // get the imags of "origin" and "use over time"
        $googleFrame.contents().find('.xpdxpnd img').closest('.xpdxpnd').each(function (index, elt) {
            var $elt = $(elt);
            $elt.find('img').attr('onload', '').end()
                .find('a').attr('onmousedown', '');
            if ($elt.find('.vk_sh.vk_gy').text().toUpperCase() === 'ORIGIN') {
                $elt.insertBefore('#webTrans');
            } else {
                //$elt.prependTo('#ads');
                $elt.appendTo('#ads');
            }
        });

        /*
        // get the changin of the speech
        var speechsContainer = [];
        var $phrsListTab = $('#phrsListTab');
        var $speechs = $googleFrame.contents().find('.lr_dct_sf_h');
        var $speechsDetail = $googleFrame.contents().find('.vk_gy');
        $speechs.each(function (index, $speech) {
            // speechsContainer.push([$speech, $speechsDetail[index]]);
            $phrsListTab.append($speech).append($speechsDetail[index]);
        });
        */
        return;
    }
    var googleJs = '!function(){window.google={},google.kHL="en",google.c={c:{a:!0}},google.time=function(){return(new Date).getTime()},google.timers={},google.startTick=function(o,e){var g=e&&google.timers[e].t?google.timers[e].t.start:google.time();google.timers[o]={t:{start:g},e:{},it:{},m:{}},(g=window.performance)&&g.now&&(google.timers[o].wsrt=Math.floor(g.now()))},google.tick=function(o,e,g){google.timers[o]||google.startTick(o),g=g||google.time(),e instanceof Array||(e=[e]);for(var t=0;t<e.length;++t)google.timers[o].t[e[t]]=g},google.afte=!0,google.aft=function(o){google.c.c.a&&google.afte&&google.tick("aft",o.id||o.src||o.name)}}();';
    function QS_kga(a) {
        QS_gda = a;
        QS_le(QS_7d, QS_9d) || google.dclc(QS_d(a, QS_7d, !0))
    }
    function QS_sga(a) {
        a = String(a);
        for (var b = ['"'], c = 0; c < a.length; c++) {
            var d = a.charAt(c), e = d.charCodeAt(0),
                f = c + 1, g;
            if (!(g = QS_kga[d])) {
                if (!(31 < e && 127 > e))
                    if (d in QS_jga)
                        d = QS_jga[d];
                    else if (d in QS_kga)
                        d = QS_jga[d] = QS_kga[d];
                    else {
                        g = d.charCodeAt(0);
                        if (31 < g && 127 > g)
                            e = d;
                        else {
                            if (256 > g) {
                                if (e = "\\x",
                                    16 > g || 256 < g)
                                    e += "0"
                            } else
                                e = "\\u",
                                    4096 > g && (e += "0");
                            e += g.toString(16).toUpperCase()
                        }
                        d = QS_jga[d] = e
                    }
                g = d
            }
            b[f] = g
        }
        b.push('"');
        return b.join("")
    }
    function parseJEAPI(a) {
        let e = a,
            f = [],
            g, l, m, n;

        for (g = l = 0; -1 != l && g >= l;) {
            l = e.indexOf("<script", g),
                -1 != l && (m = e.indexOf(">", l) + 1,
                    g = e.indexOf("\x3c/script>", m),
                    0 < m && g > m && f.push(e.substring(m, g)));
        }
        e = [];
        for (m = 0; m < f.length; ++m)
            g = f[m],
                g = g.replace(/location\.href/gi, QS_sga(l)),
                e.push(g);
        if (0 < e.length) {
            f = e.join(";");
            f = f.replace(/,"is":_loc/g, "");
            f = f.replace(/,"ss":_ss/g, "");
            f = f.replace(/,"fp":fp/g, "");
            f = f.replace(/,"r":dr/g, "");
            e = [];
            f = eval("var __r=[];var QS=function (){};QS.prototype.api=function(o){__r.push(o)};var je=new QS;" + f + ';__r;')
            for (let i = 0, max = f.length; i < max; i++) {
                if (f[i].i === 'search')
                    e.push(f[i].h)
                if (f[i].i === 'lfoot')
                    e.push(f[i].h)
            }
            return `<script>${googleJs}</script>` + e.join('')
        }
    }

    function makeId(len) {
        if (isNaN(parseInt(len))) len = 8;
        var text = "";
        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (var i = 0; i < len; i++)
            text += possible.charAt(Math.floor(Math.random() * possible.length));

        return text;
    }

    function toggleHelp() {
        console.log('Message for help. Comming soon...');
    }

})();