6
\$\begingroup\$

This Greasemonkey script adds links to tags on StackExchange sites which allow for activating, deactivating and ignoring favorite and related tags by using UI components.

Since JavaScript is not my favourite language I'd be glad to hear what can be done more elegant, more beautiful, more performant, etc. (also by using jQuery, for instance).

// ==UserScript==
// @name            SEQTAIL - StackExchange Questions' TAgs Inline Links
// @author          Gerold 'Geri' Broser
// @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl-3.0.html>
// @namespace       igb
// @description     Adds links to all tags that allow for activating, deactivating and ignoring favorite and related tags by UI components rather than by editing the search field.
// @description:de  Fügt zu allen Tags Links hinzu, die es erlauben Favorite und Related Tags über UI-Konponenten zu aktivieren oder zu deaktivieren, anstelle das Suchfeld zu bearbeiten.
// @include         http://stackoverflow.com/questions*
// @include         http://stackoverflow.com/unanswered*
// @include         http://codereview.stackexchange.com/questions*
// @version         16.4.7
// @icon            http://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico
// @run-at          document-idle
// @tested-with     Firefox 45.0.1, Greasemonkey 3.7; Chrome 49.0.2623.110, TamperMonkey 4.0.10
// ==/UserScript==

(function() { 'use strict';

console.debug("BEGIN SEQTAIL...");

function qouteRegexSpecialCharsIn(string) {
    return string.replace(/([\[\]\(\)\{\}\|\?\+\-\*\^\$\\\.\!\=])/g, "\\$1");
    }

// Adds links to all tags that allow to active, deactivate and ignore favorite and related tags
// by selecting rather than by editing the search field.
function addLinksTo(tags) {

    for (var n = 0; n < tags.snapshotLength; n++) {
        var tag = tags.snapshotItem(n);
        var tagName = tag.innerHTML.replace(/<img.+>/g, "");

        // 'ignore' tag
        if (search !== "" // if search field is empty (an ignored tag cannot exist on its own in search field)...
            && search != "[" + tagName + "]" // ...and tag not the only active (see above)...
                && search.indexOf("-[" + tagName + "]") < 0) { // ...and not already ignored
            var i = document.createElement('a');
            var rgx = new RegExp("\\[" + qouteRegexSpecialCharsIn(tagName) + "\\]\\+*", "g"); // to remove tag from link below if it is active
            i.href = "/questions/tagged/" +
                       search.replace(rgx, "").trim().replace(/ +/g, "+") + (search == "" ? "" : "+") + "-[" + tagName + "]";
            i.title = "ignore questions tagged '" + tagName + "'";
            i.style = "background-color: #e6e6e6; padding: 0 0.4em;";
            i.innerHTML = '!';
            tag.parentNode.insertBefore(i, tag.nextSibling);
            }

         // 'activate' and 'deactivate' tag
        var ad = document.createElement('a');
        if (search.indexOf("[" + tagName + "]") < 0 // if tag not active...
            || search.indexOf("-[" + tagName + "]") >= 0) { // ...or ignored
            rgx = new RegExp("-\\[" + qouteRegexSpecialCharsIn(tagName) + "\\]", "g"); // to remove tag from link below if it is ignored
            ad.href = "/questions/tagged/" +
                        search.replace(rgx, "").trim().replace(/ +/g, "+") + (search == "" ? "" : "+") + "[" + tagName + "]";
            ad.title = "add '" + tagName + "' to active tags";
            ad.style = "background-color: #e6ffe6; padding: 0 0.3em;";
            //ad.innerHTML = '+'; // if using the regular '+' sign it is added to the tag name by SO after editing Favorite Tags,
                                  // since '+' is a valid character in tag names
            //const AND = "&#2227;" // ∧ ... logical AND is not displayed properly in FF
            ad.innerHTML = "&uarr;";
            }
        else {
            rgx = new RegExp("\\[" + qouteRegexSpecialCharsIn(tagName) + "\\]", "g"); // to remove tag from link below if it is active
            ad.href = "/questions/tagged/" +
                        search.trim().replace(rgx, "").trim().replace(/ +/g, "+");
            ad.title = "remove '" + tagName + "' from active tags";
            ad.style = "background-color: #ffe6e6; padding: 0 0.3em;";
            //ad.innerHTML = '&minus;';
            ad.innerHTML = "&darr;"; // since up arrow is used at 'add' above, use down arrow here for the sake of consistency
            }
        tag.parentNode.insertBefore(ad, tag.nextSibling);
        } // for(tags)
    } // addLinksTo(...)

// Adds link to tags section that allows to deactivate all tags.
function addAllLinkTo(tagsSection) {
    if (search === "" ) // if search field is empty
        return;
    var da = document.createElement('a');
    da.href = "/questions";
    da.title = "remove all from active tags";
    da.style = "background-color: #ffe6e6; padding: 0 0.3em;";
    da.innerHTML = '&times;';
    tagsSection.appendChild(da);
    } // addAllLinkTo(...)

var search = document.getElementById('search').firstElementChild.value;

// active tags
var aTag = document.evaluate("//div[@class='tagged']", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
             .snapshotItem(0);
if (aTag !== null) {
    var aTags = document.evaluate("//div[@class='tagged']/a[not(starts-with(., 'about'))]",
                aTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    addLinksTo(aTags);
    addAllLinkTo(aTag);
    }

// interesting tags
var iTag = document.getElementById('interestingTags');
if (iTag !== null) {
    var iTags = document.evaluate("//div[@id='interestingTags']/a", iTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    addLinksTo(iTags);
    addAllLinkTo(iTag);
    }

// related tags
var rTag = document.evaluate("//div[contains(@class, 'js-gps-related-tags')]",
             document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
               .snapshotItem(0);
if (rTag !== null) {
    var rTags = document.evaluate( "//div[contains(@class, 'js-gps-related-tags')]/div/a",
                rTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    addLinksTo(rTags);
    }

//unanswered tags
var uTag = document.evaluate("//div[./h4[@id='h-related-tags']]",
             document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
               .snapshotItem(0);
if (uTag !== null) {
    var uTags = document.evaluate( "//div[./h4[@id='h-related-tags']]/div/a",
                uTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    addLinksTo(uTags);
    }

// Adds a section with links primarily for removing related tags which are no more in the Related Tags list once active.
function addDeactivateSection(parentElement) {
    var active = search.split(" ");
    if (active[0] === "" ) // if search field is empty (first element is an empty string after split())
        return;
    var deactivate = document.createElement('div');
    deactivate.style = "margin-bottom: 1.5em;";
    var h3 = document.createElement('h3');
    h3.style = "margin-top: 1em; font-weight: normal;";
    h3.innerHTML = "Remove from active tags";
    deactivate.appendChild(h3);
    for (var i = 0; i < active.length; i++) {
        var r = document.createElement('a');
        var rgx = "\-*" + qouteRegexSpecialCharsIn(active[i])/*.replace(/\[/g, "\\\[").replace(/\]/g, "\\\]")*/;
        r.href = "/questions/tagged/" +
                             search.replace(new RegExp(rgx, "g"), "").trim().replace(/ +/g, "+");
        r.title = "remove '" + active[i].replace(/[\[\]]/g, "") + "' from active tags";
        r.style = "font-size: 12px; background-color: #ffe6e6; margin: 0.2em; padding: 0.2em 0.4em;";
        r.innerHTML = active[i].replace(/[\-\[\]]/g, "");
        if (active[i].startsWith("-")) {
            var n = document.createElement('span');
            n.innerHTML = "not";
            deactivate.appendChild(n);
            }
        deactivate.appendChild(r);
        } // for(active tags)
    parentElement.parentNode.parentNode.insertBefore(deactivate, parentElement.parentNode.nextSibling);
    addAllLinkTo(deactivate);
    } // addDeactivateSection(...)

addDeactivateSection(iTag);

console.debug("END SEQTAIL.");

})(); // use strict

Tweaked SE UI

\$\endgroup\$
5
\$\begingroup\$

Since Stack Exchange already uses jQuery, I think it's a good idea to write userscripts using jQuery too. However, rewriting the whole code to use jQuery instead of native APIs would require a lot of work, so I will just comment on the code you wrote.


(function() { 'use strict';

I would put the 'use strict' statement in the next line.


You're using many semicolons. JavaScript has a nice feature called automation semicolon insertion, which means you almost don't need to use semicolons at all, with just a few exceptions. From npm style guide:

Don't use them [semicolons] except in four situations:

  • for (;;) loops. They're actually required.
  • null loops like: while (something) ; (But you'd better have a good reason for doing that.)
  • case 'foo': doSomething(); break
  • In front of a leading ( or [ at the start of the line. This prevents the expression from being interpreted as a function call or property access, respectively.

Note that it's only a matter of style. You don't have to follow it if you don't like it, but personally I think that if something is not required, there's no reason to use it.


You don't use quotes or apostrophes for string literals consistently. For example:

'use strict'
console.debug("BEGIN StackExchange Questions...")
document.getElementById('search')

Decide if you want to write string using either apostrophes or double quotes and use it consistently in the whole code. Personally, I prefer apostrophes, because they require to press only one key, whereas to insert a double quote character you have to hold Shift too.

And for example if you choose apostrophes and you want to make a string which contains apostrophes, feel free to use double quotes in this specific situation, so you don't have to escape the apostrophes with a backslash.


You're using document.evaluate() to select DOM elements. I don't really see any reason why it could be better than document.querySelector(), which uses CSS-style selectors. And unfortunately, it's not supported by IE at all. I think you should support at least IE 11, because still many people use it.


ECMAScript 6 has a lot of great features, and most of them are already supported in major browsers, but you aren't using any of them. For example, in for loop, you could use let instead of var, to limit the scope of the variable to the loop. This loop:

for (var n = 0; n < tags.snapshotLength; n++)

could be changed to:

for (let n = 0; n < tags.snapshotLength; n++)

Read more about let keyword on MDN docs.


I see you're using strict comparison operators (=== and !==) almost everywhere, but in one place I think you forgot one equal sign:

&& search != "[" + tagName + "]"

var i = document.createElement('a');

Try to use some more meaningful identifiers, for example ignoreTagLink.


i.style = "background-color: #e6e6e6; padding: 0 0.4em;";

Instead of using inline styles, I recommend you adding a class to the element, and define CSS rules for that class, like that:

var $style = document.createElement("style")
$style.textContent = `
.someClass {
  background-color: #e6e6e6;
  padding: 0 0.4em;
}
`
document.head.appendChild($style)

You're repeating some code — don't do this, it's a bad practice. Try to follow DRY (don't repeat yourself) principle.


Some lines of code are too lengthy, like this one:

    var iTags = document.evaluate("//div[@id='interestingTags']/a", iTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

You could split it into multiple lines like that:

    var iTags = document.evaluate(
      "//div[@id='interestingTags']/a",
      iTag,
      null,
      XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
      null
    );

That's all issues I see for now, if I find some more, I'll edit this answer.

\$\endgroup\$
  • 3
    \$\begingroup\$ Welcome to Code Review! Good job on your first answer. \$\endgroup\$ – SirPython Apr 8 '16 at 21:45
  • \$\begingroup\$ Thanks for the reply! Just a quick first note: I try to use quotes consistently. Double if it is a "real" string (presented to the user), single if it is internal (for referencing etc.). But, you're right. Sometimes I don't follow this personal rule – inadvertently. \$\endgroup\$ – Gerold Broser Apr 8 '16 at 22:42
  • \$\begingroup\$ "because they require to press only one key" – unfortunately not on my German kbd. "you aren't using any of them" – as I mentioned: JS is not my favourite (it's Java, and now it's also clear where all the ';' s come from :) – but that's why I'm here. :) I'm using strict comparison since Tampermonkey's editor gave me according warnings (and it didn't give me warnings on those lines). You're right, I thought of inserting a <style> already. The long evaluate lines are to be replaced with querySelector() anyway. Thanks again for this great answer! I learned quite a lot from it. \$\endgroup\$ – Gerold Broser Apr 8 '16 at 23:31
  • \$\begingroup\$ I also wonder why let isn't mentioned on THE WORLD'S LARGEST WEB DEVELOPER SITE. \$\endgroup\$ – Gerold Broser Apr 9 '16 at 0:14
  • 1
    \$\begingroup\$ @GeroldBroser w3schools has a lot of obsolete information, and it isn't the largest web developer site. \$\endgroup\$ – Michał Perłakowski Apr 9 '16 at 11:09
4
\$\begingroup\$

Style

Your indentation is incorrect in a few places.

At the start:

(function() { 'use strict';

console.debug("BEGIN StackExchange Questions...");

You're starting a block, so you need to indent all its children correctly. That includes 'use strict'

function qouteRegexSpecialCharsIn(string) {
    return string.replace(/([\[\]\(\)\{\}\|\?\+\-\*\^\$\\\.\!\=])/g, "\\$1");
    }

Here your close brace is on the new line, but it doesn't belong there. Also you misspelled quote. Your title could use a refactor anyway.

I'm seeing a lot of whitespace where it doesn't belong:

document.evaluate( "//div[contains(@class, 'js-gps-related-tags')]/div/a",
            rTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

and:

if (active[0] === "" )
    return;

Your naming is a bit confusing:

addLinksTo

Add links to what?

qouteRegexSpecialCharsIn

Quoting regex special characters in what?

rTag, aTag, iTag, uTag

This is very confusing. While I'm sure each of the letters have relevance to what the variables contain, you're effectively trading like 7 letters for readability:

interestingTag

XPath and document.evaluate

I nearly forgot entirely about XPath, thanks for bringing back the nightmares.

In all seriousness though, CSS selectors have been a core feature of JavaScript for a very long time. You should use them instead.

Instead of your eight parameter document.evaluate, you can just pass through a simple CSS selector ('div#interestingTags a')

document.evaluate("//div[@id='interestingTags']/a", iTag, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    addLinksTo(iTags);

into:

document.querySelectorAll('div#interestingTags a');

and for the ones where you find elements by Id:

documnet.getElementById('theIdToSelect');

UserScript headers

If you're hosting this script on GitHub or somewhere public, you can include @downloadURL and @updateURL headers so Tamper/Greasemonkey will automatically update your userscript when you push out a new version.


Regex

When you find yourself writing a regex like this:

/([\[\]\(\)\{\}\|\?\+\-\*\^\$\\\.\!\=])/g

You should question what it is you're trying to do. You shouldn't be coding to the "cleverest" way, because when you come back to try to maintain it in two weeks, or in production code for others, it's gonna be really hard to maintain.

You don't want a call from the new guy two weeks after you're finished while you're sipping mojitos in Barbados. Try debugging Regex over the phone. Not fun.

Anyway, you probably should avoid using regexes for everything (they're all throughout your code), they overcomplicate your code, and make it really really hard to read.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the reply! Where do you think that the closing brace belongs to? If one reads the whole function header addLinksTo(tags) and call addLinksTo(rTags) it's clear "where to" and "in what" respectively, isn't it? rTag, aTag, iTag, uTag refer to related, active, interesting, unanswered as mentioned in comments directly above. And since these are solely used in the immediate context of the next few lines... But you're right. I usually prefer good descriptive names as well. What do you suggest for extracting strings concisely instead of Regex? String's functions? \$\endgroup\$ – Gerold Broser Apr 8 '16 at 23:06
  • \$\begingroup\$ Perhaps string functions would be better @GeroldBroser, they may even be more performant. The closing brace belongs on the same level of indentation as the function declaration \$\endgroup\$ – Quill Apr 8 '16 at 23:34
  • \$\begingroup\$ More performant? For sure? More or less deeply nested and chained substr[ing]()s and [last]indexOf()s are harder to read for me. I don`t consider them being concise. There are various indent styles. I usually use Whitesmiths. If it is to save vertical space: Ratliff. Or the one that has been agreed on in a project I'm working on together with others. I always consider "Choose one, than stick to it." for each of my projects. \$\endgroup\$ – Gerold Broser Apr 9 '16 at 0:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.