// ==UserScript==
// @name MediaTriggerWords
// @namespace http://www.fourmilab.ch/webtools/greasemonkey/MediaTriggerWords/
// @description Highlights mainstream media trigger words in text
// @include *
// @version 1.0
// @homepage http://www.fourmilab.ch/webtools/greasemonkey/

/*
		     MediaTriggerWords.user.js
	      by John Walker  -- http://www.fourmilab.ch/

    This Firefox Greasemonkey (http://www.greasespot.net/) user script
    (also usable with other browsers with a compatible user script
    facility) allows you to highlight words or phrases in the body
    text of HTML/XHTML documents with any number of user-defined
    styles.

    You can add any words you wish highlighted to the "defwords"
    calls below.  These are regular expressions, and can be coded
    to recognise variant forms and spellings.

    By default, the script is applied to all sites.  If you'd like to
    restrict it specific sites (for example, one or more legacy media
    outlets), use the Tools/Greasemonkey/Manage User Scripts menu
    item and edit the Included Pages and Excluded Pages accordingly.

*/

/*  This script is derived from the Profanity Filter:
	http://userscripts.org/scripts/show/7286
    which in turn is based upon the Jmaxxz Vulgar Word Blocker
	http://userscripts.org/scripts/show/2287
    and the "Dumb Quotes" script:
	http://diveintogreasemonkey.org/casestudy/dumbquotes.html
    in Mark Pilgrim's "Dive into Greasemonkey":
	http://diveintogreasemonkey.org/
*/

// ==/UserScript==

(function() {

    var patterns = [], classes = [];

    /*	The following define the classes of words.  If the first
	character of the specification is "=", the match will be
	case-sensitive, otherwise it will be case-insensitive.
	The specification is a regular expression, and should
	contain metacharacters to handle variant spellings and
	plurals.  Any grouping within these patterns *must* be done
	with a (?: ... ) specification to avoid messing up the
	capture from the text string.

	You may add additional categories as you wish, but be sure to
	declare their rendering in the style definition below.  */

    //	Rendering styles for our various word classes

    addGlobalStyle('span.love { background-color: #D0D0FF; } ' +
		   'span.hate { background-color: #FFD0D0; } ' +
		   'span.kill { background-color: #FFFFB0; } ');

    //	MSM "love" words

    defwords([
		"Alternatives?",
		"Collaborative",
		"Collectives?",
		"Communit(?:y|ies)",
		"Commons",
		"Consensual",
		"Consensus(?:es)?",
		"Cooperatives?",
		"Creative",
		"=democratic",
		"Dissents?",
		"Diverse",
		"Diversit(?:y|ies)",
		"Environmental",
		"Equitable",
		"Expert",
		"Foundations?",
		"Free",
		"Green",
		"Independents?",
		"Innovative",
		"Mainline",
		"Mainstream",
		"Mavericks?",
		"Open",
		"Organic",
		"Participatory",
		"People's",
		"Popular",
		"Pragmatic",
		"Progressives?",
		"Protests?",
		"Public",
		"Renewables?",
		"Social",
		"Solar",
		"Sustainable",
	     ],
	"love");

    //	MSM "hate" words

    defwords([
		"Arrogant",
		"Christians?",
		"Competitive",
		"Conservatives?",
		"Controversial",
		"Corporate",
		"Corporations?",
		"Decenc(?:y|ies)",
		"Divisive",
		"Elites?",
		"Enterprises?",
		"Evangelicals?",
		"Extremists?",
		"Faiths?",
		"Fundamentalists?",
		"Individuals?",
		"Markets?",
		"Morals?",
		"Nuclear",
		"Partisans?",
		"Private",
		"Propert(?:y|ies)",
		"Reactionar(?:y|ies)",
		"Religious",
		"Responsible",
		"Right-wing",
		"Traditional",
	     ],
	"hate");

    //	The spectrum of killers

    defwords([
		"Freedom fighters?",
		"Activists?",
		"Dissidents?",
		"Militants?",
		"Insurgents?",
		"Rebels?",
		"Guerr?ill?as?",
		"Terrorists?",
	     ],
	"kill");

    //	Add one or more words to the dictionary with a specified class

    function defwords(words, class) {
	for (var i = 0; i < words.length; i++) {
	    var w = words[i].replace(/^=/, "");
	    patterns.push(new RegExp("([^a-zA-Z])(" + w + ")([^a-zA-Z])",
		words[i].match(/^=/) ? "g" : "gi"));
	    classes.push(class);
	}
    }

    //	Quote HTML metacharacters in body text

    function quoteHTML(s) {
	s = s.replace(/&/g, "&amp;");
	s = s.replace(/</g, "&lt;");
	s = s.replace(/>/g, "&gt;");
	return s;
    }

    //	Add one or more CSS style rules to the document

    function addGlobalStyle(css) {
	var head, style;
	head = document.getElementsByTagName('head')[0];
	if (!head) {
	    return;
	}
	style = document.createElement('style');
	style.type = 'text/css';
	style.innerHTML = css;
	head.appendChild(style);
    }

    //	Apply highlighting replacements to a text sequence

    var curpat;     	    // Hidden argument to repmatch()
    var changes;    	    // Number of changes made by repmatch()

    function repmatch(matched, before, word, after) {
	changes++;
	return before + '<span class="' + classes[curpat] + '">' + word + '</span>' + after;
    }

    function highlight(s) {
	s = " " + s;
	for (curpat = 0; curpat < patterns.length; curpat++) {
	    s = s.replace(patterns[curpat],
		    repmatch);
	}
	return s.substring(1);
    }

    //	We only modify HTML/XHTML documents
    if (document.contentType &&
    	(!(document.contentType.match(/html/i)))) {
    	return;
    }

    // Highlight words in body copy

    var textnodes = document.evaluate("//body//text()", document, null,
	    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

    for (var i = 0; i < textnodes.snapshotLength; i++) {
	var node = textnodes.snapshotItem(i);
	/* Test whether this text node appears within a
	   <style>, <script>, or <textarea> container.
	   If so, it is not actual body text and must
	   be left alone to avoid wrecking the page. */
	if (node.parentNode.tagName != "STYLE" &&
	    node.parentNode.tagName != "TEXTAREA" &&
	    node.parentNode.tagName != "SCRIPT") {
	    /* Many documents have large numbers of empty text nodes.
	       By testing for them, we avoid running all of our
	       regular expressions over a target which they can't
	       possibly match. */
	    if (!(node.data.match(/^\s*$/))) {
		var s = " " + node.data + " ";
		changes = 0;
		var d = highlight(quoteHTML(s));
		if (changes > 0) {
		    var rep = document.createElement("span");
		    rep.innerHTML = d.substring(1, d.length - 1);
		    node.parentNode.replaceChild(rep, node);
		}
	    }
	}
    }

})();
