// $Id: tighttvgrid.user.js 543 2013-02-01 01:04:26Z Chris $ // ----------------------------------------------------------------------------- // This is a Greasemonkey user script. // To use it, first install Greasemonkey: http://www.greasespot.net/ // Then restart Firefox and revisit this script // From the Firefox menu select: Tools -> Install User Script // Accept the default configuration and install // Now when you visit any of the supported sites you will see extra functionality // Documentation here: http://refactoror.net/greasemonkey/TightTVGrid/doc.html // ----------------------------------------------------------------------------- // ==UserScript== // @name Tight TV Grid // @moniker ttg // @namespace http://refactoror.net/ // @description Operates on multiple TV listings services. Removes content surrounding the listing grid and adds an IMDb link in front of each program title. // @version 3.0.2.1 // @author Chris Noe // @include http://www.excite.com/tv/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_log // @grant GM_xmlhttpRequest // @downloadURL https://update.greasyfork.cloud/scripts/5059/Tight%20TV%20Grid.user.js // @updateURL https://update.greasyfork.cloud/scripts/5059/Tight%20TV%20Grid.meta.js // ==/UserScript== var dm = new DomMonkey({ name : "Tight TV Grid" ,moniker : "ttg" ,version : "3.0.2.1" }); // The values listed here are the first-time-use defaults // They have no effect once they are stored as mozilla preferences. prefs.config({ "controlBar-isExpanded": false ,"favoredTitles": "" ,"favoredTitlesColor": "#FF9900" ,"favoriteTitles": "" ,"favoriteTitlesColor": "#FFFF00" ,"fixedHeader": true ,"highlightFirstRun": true ,"ignoreTitles": "" ,"ignoreTitlesColor": "#996666" ,"insertImdbLinks": true ,"insertTvcomLinks": true ,"linksAlwaysOpenInNewTab": true ,"omitChannels": "" ,"prefsMenuAccessKey": "P" ,"prefsMenuPosition": "BR" ,"prefsMenuVisible": true ,"refreshMinute": 3 ,"removeGridAds": true ,"removeNonGridElements": true ,"searchTitleActor-isExpanded": false ,"showTimeMarker": true ,"tweakLayout": true }); // --------------- Page handlers --------------- tryCatch(dm.metadata["moniker"], function () { enhanceExciteListingPage(); }); function enhanceExciteListingPage() { if (dm.xdoc.location.href.match("grid.jsp") == null) { log.info("This is not the grid page, no processing..."); return; } var exciteDoc = extendListingDocument(dm.xdoc); if (exciteDoc == null) return null; if (exciteDoc.isEmpty()) { // failed page load re-try exciteDoc.schedulePageRefresh(1); return null; } else { // refresh on the upcoming hour exciteDoc.scheduleUpcomingHourRefresh(prefs.get("refreshMinute"), function() { window.location.reload(); }); } var programTypeMap = new Array(); exciteDoc.foreachNode( "//*[text()='Color Key']/following::table[1]//tr[position()!=1]/td", function(td) { programTypeMap[td.bgColor] = td.textContent; } ); dispatchFeature("removeNonGridElements", function() { // exciteDoc.isolateNode("//form[@name='gridform']/ancestor::center[1]"); // -- top of page // search bar exciteDoc.hideNodes("//select[@name='featuredguides']/ancestor::table[4]"); // nav bar exciteDoc.hideNodes("//a[contains(@href, 'entertainment.excite.com')]/ancestor::table[2]"); // title row exciteDoc.hideNodes("//a[contains(@href, 'tv/data.jsp')]/ancestor::table[1]"); // -- bottom of page // search title/actor exciteDoc.selectNode("//*[text()='Search by Title or Actor']/ancestor::p[1]") .makeCollapsible("searchTitleActor-isExpanded", true); // color key exciteDoc.hideNodes("//*[text()='Color Key']/ancestor::p[1]"); // ad exciteDoc.hideNodes("//div[@id='adFooter']/ancestor::table[1]"); // web search bar exciteDoc.hideNodes("//form[@name='footerSearch']/ancestor::table[3]"); // sitemap exciteDoc.hideNodes("//a[contains(@href, 'site_map/index.html')]/ancestor::table[1]"); // extra spacing exciteDoc.hideNodes("//br"); // channel-type section dividers exciteDoc.hideNodes("//*[@bgcolor='White']/ancestor::tr[1]"); exciteDoc.hideNodes("//*[text()='Basic Channels']/ancestor::tr[1]"); }); exciteDoc.selectNode("//form[@name='gridform']/ancestor::table[1]") .makeCollapsible("controlBar-isExpanded", true); dispatchFeature("fixedHeader", function() { // -EXPERIMENTAL- var grid_table = exciteDoc.selectNode( "//a[starts-with(@href, 'http://www.excite.com/tv/grid.jsp')][1]/ancestor::table[1]"); // make grid body scrollable exciteDoc.addStyle( "#grid_body tr td:last-child { padding-right: 18px; }\n" ); var tbody = grid_table.selectNode("descendant::tbody[1]"); tbody.id = "grid_body"; with (tbody.style) { height = "500px"; overflowX = "hidden"; overflowY = "auto"; } // move header row into thead element var header_tr = tbody.selectNode("descendant::tr[1]"); var thead = exciteDoc.createXElement("thead"); header_tr.remove(); thead.appendChild(header_tr); grid_table.prependChild(thead); }); var channelMatchers = prefs.getAsList("omitChannels", ";", ChannelMatcher); if (channelMatchers != null && channelMatchers != "") { // remove specified channels exciteDoc.foreachNode ( "//a[starts-with(@href, 'http://my.excite.com/tv/chan.jsp')]/text()", function(channelName_text) { var chanParts = channelName_text.textContent.split(" "); var isOmit = false; for (var c in channelMatchers) { for (var p in chanParts) { if (channelMatchers[c].match(chanParts[p])) { try { var chan_tr = channelName_text.selectNode("ancestor::tr[1]"); chan_tr.remove(); } catch(err) { log.info("Trying to delete '" + channelName_text.textContent + "', row previously deleted"); } break; } } } if (isOmit == true) { } } ); } // encapsulates a channel spec value (name/number/range) and requisite match() method function ChannelMatcher(chanSpec) { var i = chanSpec.toString().indexOf("-"); if (i > 0) { // range matcher this.lo = parseInt(chanSpec.substring(0, i)); this.hi = parseInt(chanSpec.substring(i + 1)); this.match = function(chan) { var f = (chan >= this.lo && chan <= this.hi); return f; } } else { // simple equality matcher this.chanSpec = chanSpec; this.match = function(chan) { var f = (chan == this.chanSpec); return f; } } } var favoriteTitles = prefs.getAsList("favoriteTitles", ";"); var favoredTitles = prefs.getAsList("favoredTitles", ";"); var ignoreTitles = prefs.getAsList("ignoreTitles", ";"); // process each title link in the grid var first_td = exciteDoc.selectNode("//a[starts-with(@href, 'http://www.excite.com/tv/grid.jsp')][1]/ancestor::td[1]"); var last_td; exciteDoc.foreachNode ( "//a[starts-with(@href, 'http://www.excite.com/tv/prog.jsp')]", function(programTitle_a) { var programAttrs = new Object(); programAttrs.title = doUnescape(programTitle_a.textContent.normalizeWhitespace()); var td = programTitle_a.selectNode("ancestor::td[1]"); programAttrs.programType = programTypeMap[td.bgColor]; var subTitle_i = programTitle_a.selectNodeNullable("following-sibling::i[1]"); if (subTitle_i != null) { programAttrs.subTitle = subTitle_i.textContent; } var attributes = programTitle_a.selectNodeNullable("following-sibling::text()"); if (attributes != null) { attributes = attributes.textContent.normalizeWhitespace(); // get New/Repeat indicator, if present if (attributes.substring(0, 1) == "(") { var i = attributes.indexOf(")"); if (i != -1) { programAttrs.new_repeat = attributes.substring(1, i); attributes = attributes.substring(i+1).trimWhitespace(); } } // get remaining attributes tokens = attributes.split(","); var t = 0; if (programAttrs.programType == "Movies") { programAttrs.isMovie = true; programAttrs.year = attributes.match(/\d\d\d\d/); programAttrs.runtime = attributes.match(/\d\d:\d\d/); } else { if (tokens[t]) programAttrs.subcat = tokens[t].trimWhitespace(); if (tokens[++t]) programAttrs.subsubcat = tokens[t].trimWhitespace(); } // if (programAttrs.programType == "Movies") { // var buf = ""; // for (var term in programAttrs) { // buf += term + "=" + programAttrs[term] + " "; // } // log.info(buf); // } } exciteDoc.insertLinks(programTitle_a, programAttrs); if (favoriteTitles != null && favoriteTitles.contains(programAttrs.title)) { programTitle_a.style.backgroundColor = prefs.get("favoriteTitlesColor"); } if (favoredTitles != null && favoredTitles.contains(programAttrs.title)) { programTitle_a.style.backgroundColor = prefs.get("favoredTitlesColor"); } if (ignoreTitles != null && ignoreTitles.contains(programAttrs.title)) { programTitle_a.style.color = prefs.get("ignoreTitlesColor"); } dispatchFeature("highlightFirstRun", function() { if (programAttrs.new_repeat == "New") { programTitle_a.style.fontWeight = "bold"; programTitle_a.style.fontSize = "120%"; } }); last_td = programTitle_a.selectNode("ancestor::td[1]"); } ); // (this has to be the last modification to the grid structure) dispatchFeature("showTimeMarker", function() { var gridLeftNav_a = exciteDoc.selectNode( "//a[starts-with(@href, 'http://www.excite.com/tv/grid.jsp')][1]"); var hour1_00_text = gridLeftNav_a.selectNode("following::text()[1]"); var gridStartDate = parseGridTime(hour1_00_text.textContent); var hour1_00_td = gridLeftNav_a.selectNode("ancestor::td[1]"); var hour1_30_td = hour1_00_td.selectNode("following::td[1]"); var ref_td = hour1_00_td; var rel_date = gridStartDate; if ( (new Date()).getMinutes() >= 30 ) { ref_td = hour1_30_td; rel_date.setMinutes(30); } var tm = new TimeMarker( ref_td, ref_td, ref_td, last_td, rel_date, 0.5 * HOUR ); exciteDoc.body.appendChild(tm); }); function parseGridTime(str) { var tim = str.trimWhitespace().split(" "); var hr_min = tim[0].split(":"); var meridiem = 0; if (tim[1] == "PM" && Number(hr_min[0]) < 12) meridiem = 12; var gridDate = new Date(); gridDate.setHours( (Number(hr_min[0]) + meridiem), hr_min[1], 00, 000); return gridDate; } } function extendListingDocument(doc) { if (doc == null) return null; addPrefsButton(); // Refresh this page on the upcoming hour, plus the specified number of minutes. // (Disabled if negative) doc.scheduleUpcomingHourRefresh = function(refreshMinute, func) { if (refreshMinute < 0) { log.info("Not configured for auto-refresh"); return; } var now = new Date(); var refreshTime = now.floor(HOUR).add(HOUR).add(refreshMinute * MINUTE); window.setTimeout(func, (refreshTime.getTime() - now.getTime()) ); log.info("Scheduled page refresh: " + refreshTime); } // Refresh this page in the the specified number of minutes. doc.schedulePageRefresh = function(refreshMinute, func) { var now = new Date(); var refreshTime = now.add(refreshMinute * MINUTE); window.setTimeout( function() { window.location.reload(); }, (refreshTime.getTime() - now.getTime()) ); log.info("Scheduled page refresh: " + refreshTime); } // insert external search link(s) in front of the specified node doc.insertLinks = function(base_node, programAttrs) { var theDoc = this; dispatchFeature("insertImdbLinks", function() { var imdbLink = createExternalLink("http://www.imdb.com/favicon.ico"); var IMDB_SEARCH_TT = "http://imdb.com/find?s=tt&q="; var IMDB_SEARCH_EP = "http://imdb.com/find?s=ep&q="; // var IMDB_SEARCH = "http://www.google.com/search?ie=UTF-8&oe=UTF-8&sourceid=navclient&gfns=1&q="; var year = ""; if (programAttrs.year != null) { year = " (" + programAttrs.year + ")"; } if (programAttrs.subTitle != null) { // episode search imdbSearchTerm = programAttrs.subTitle; imdbLink.title = "Search for episode " + imdbSearchTerm + " on imdb.com"; base_node.prependSibling(theDoc.createLink( imdbLink, IMDB_SEARCH_EP + doEscape(imdbSearchTerm) )); } else { // title search imdbSearchTerm = programAttrs.title; imdbLink.title = "Search for " + imdbSearchTerm + " on imdb.com"; base_node.prependSibling(theDoc.createLink( imdbLink, IMDB_SEARCH_TT + doEscape(imdbSearchTerm) )); } }); dispatchFeature("insertTvcomLinks", function() { var tvcomLink = createExternalLink("http://www.tv.com/favicon.ico"); var TVCOM_SEARCH = "http://www.tv.com/search.php?type=11&stype=all&tag=search;button&qs="; tvcomTerm = '"' + programAttrs.title + '"'; tvcomLink.title = "Search for " + tvcomTerm + " on tv.com"; if (programAttrs.isMovie != true) { base_node.prependSibling( theDoc.createLink(tvcomLink, TVCOM_SEARCH + tvcomTerm)); } }); function createExternalLink(url) { var img = document.createXElement('img', { src: url }); with (img.style) { border = 0; width = "16px"; height = "14px"; verticalAlign = "-25%"; } return img; } } doc.createLink = function(symbol, url) { var lookup_a = this.createXElement("a"); lookup_a.className = "lookup_a"; lookup_a.href = url; lookup_a.appendChild(symbol); if (prefs.get("linksAlwaysOpenInNewTab") == true) { lookup_a.target = "_blank"; } return lookup_a; } return doc; } // ==================== TimeMarker object ==================== // Install a real-time time indicator over the grid function TimeMarker(topRefNode, leftRefNode, rightRefNode, bottomRefNode, startTime, duration) { var topY = topRefNode.findPosY(); var bottomY = bottomRefNode.findPosY() + bottomRefNode.clientHeight; var heightY = bottomY - topY; var leftX = leftRefNode.findPosX(); var rightX = rightRefNode.findPosX() + rightRefNode.clientWidth; var widthX = rightX - leftX; var timemarker_div = document.createElement("div"); timemarker_div.id = "ttg_timemarker"; with (timemarker_div.style) { border = ".75px dashed red"; position = "absolute"; top = topY; height = heightY; zIndex = 99; } this.refresh = function() { var xleft = leftRefNode.findPosX(); var xright = rightRefNode.findPosX() + rightRefNode.clientWidth; var now = new Date(); var hourFrac = (now.getTime() - startTime.getTime()) / duration; var x = xleft + Math.floor(hourFrac * (xright - xleft)); timemarker_div.style.left = x; timemarker_div.title = formatGridTime(now); } this.refresh(); window.setInterval(this.refresh, 5000); window.addEventListener("resize", this.refresh, false); return timemarker_div; } function formatGridDate(gridtime) { var s = gridtime.toDateString().split(" "); return s[0] + ", " + s[1] + " " + s[2]; } function formatGridTime(gridtime) { var d = new Date(gridtime); var h = d.getHours(); var xm = "am"; if (h > 12) { h -= 12; xm = "pm"; } var m = "0" + d.getMinutes(); return h + ":" + m.substring(m.length - 2) + xm; } // ==================== Preferences Dialog ==================== function addPrefsButton() { configurePrefsButton(function(prefsMgr, prefsDialog_div) { var mainTabset = new TabSet(dm.xdoc, "ttg_mainTabset", ["General", "Highlighting"]); prefsDialog_div.appendChild(mainTabset.container_div); with (mainTabset.getTabContent_div("General")) { var table = dm.xdoc.createXElement("table"); appendChild(table); var tr = dm.xdoc.createXElement("tr"); table.appendChild(tr); var td = dm.xdoc.createXElement("td"); td.style.verticalAlign = "top"; tr.appendChild(td); with (td) { style.verticalAlign = "top"; var gridFeatures_div = dm.xdoc.createTopicDiv("Grid Layout", td); appendChild(gridFeatures_div); with (gridFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "removeNonGridElements", "Isolate listing grid", "Remove content surrounding the listing grid" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "fixedHeader", "Fixed header", "Grid content scroll independently of header" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "tweakLayout", "Tweak layout", "Adjust font styles, etc" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "showTimeMarker", "Show current time", "Indicate the current time as a vertical dashed line over the grid" )); var div = appendChildText("Remove Channels:", ["div"]); div.style.marginTop = "5px"; appendChild(prefsMgr.createPreferenceInput( "omitChannels", null, "Remove these channels from the grid", { size: 24 } )); } var linkFeatures_div = dm.xdoc.createTopicDiv("External Links", td); appendChild(linkFeatures_div); with (linkFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "insertImdbLinks", "Add imdb.com links", "Add an imdb.com search link in front of each program title" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "insertTvcomLinks", "Add tv.com links", "Add a tv.com search link in front of each program title" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "linksAlwaysOpenInNewTab", "Links open in a new tab", "" )).style.marginLeft = "16px"; } } var td = dm.xdoc.createXElement("td"); td.style.verticalAlign = "top"; tr.appendChild(td); with (td) { var miscFeatures_div = dm.xdoc.createTopicDiv("Miscellaneous", td); appendChild(miscFeatures_div); with (miscFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "removeGridAds", "Remove advertising", "Remove advertising" )); with (appendChildElement("div")) { style.margin = "2px"; } appendChild(prefsMgr.createPreferenceInput( "refreshMinute", "Auto-refresh minute", "Refresh the listing these many minutes after each hour", { size:1, maxLength: 2 } )); } appendChild(prefsMgr.constructDockPrefsMenuSection(td)); appendChild(prefsMgr.constructAdvancedControlsSection(td)); } } with (mainTabset.getTabContent_div("Highlighting")) { appendChild(prefsMgr.createPreferenceInput( "highlightFirstRun", "Emphasize first run programs", "Emphasize first run program titles (bold)" )); var highFeatures_div = dm.xdoc.createTopicDiv("Customize Program Titles", prefsDialog_div); appendChild(highFeatures_div); with (highFeatures_div.contentElement) { var tabset = new TabSet(dm.xdoc, "iwvr_highlightingTabset", ["Favorites", "Favored", "Ignored"]); appendChild(tabset.container_div); var tips = " (enter exact title spelling, separate multiple titles with ;)"; with (tabset.getTabContent_div("Favorites")) { appendChild(prefsMgr.createPreferenceInput( "favoriteTitles", null, "Highlight these titles" + tips, { rows: 6, cols: 25 } )); appendChild(prefsMgr.createPreferenceInput( "favoriteTitlesColor", "Color", "", { size:7 } )); } with (tabset.getTabContent_div("Favored")) { appendChild(prefsMgr.createPreferenceInput( "favoredTitles", null, "Highlight these titles, more subtly" + tips, { rows: 6, cols: 25 } )); appendChild(prefsMgr.createPreferenceInput( "favoredTitlesColor", "Color", "", { size:7 } )); } with (tabset.getTabContent_div("Ignored")) { appendChild(prefsMgr.createPreferenceInput( "ignoreTitles", null, "De-emphasize these titles" + tips, { rows: 6, cols: 25 } )); appendChild(prefsMgr.createPreferenceInput( "ignoreTitlesColor", "Color", "", { size:7 } )); } tabset.initialize(); } } mainTabset.initialize(); // Help link var docs_div = dm.xdoc.createXElement("div"); prefsDialog_div.appendChild(docs_div); with (docs_div) { appendChild(dm.xdoc.createHtmlLink( "http://refactoror.com/greasemonkey/TightTVGrid/doc.html#prefs", "Help" )); align = "center"; style.padding = "3px"; } }); } // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // =-=-=-=-=-=-=-=-=-=-= refactoror lib -=-=-=-=-=-=-=-=-=-=-= // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // common logic for the way I like to setup Preferences in my apps // Requires preferences: prefsMenuAccessKey, prefsMenuPosition, prefsMenuVisible, loggerLevel function configurePrefsButton(dialogConstructor) { // Preferences dialog GM_registerMenuCommand(dm.metadata["name"] + " Preferences...", openPrefsDialog); createPrefsButton(); // Prefs dialog function createPrefsButton() { var menuButton = dm.xdoc.createXElement("button", { textContent: "Prefs" }); setScreenPosition(menuButton, prefs.get("prefsMenuPosition")); if (prefs.get("prefsMenuVisible") == false) { menuButton.style.opacity = 0; // active but not visibile menuButton.style.zIndex = -1; // don't block other content } with (menuButton) { id = dm.metadata["moniker"] + "_prefs_menu_button"; title = dm.metadata["name"] + " Preferences"; style.fontSize = "9pt"; addEventListener('click', openPrefsDialog, false); // accessKey = getDeconflicted("prefsMenuAccessKey", "accessKey"); accessKey = prefs.get("prefsMenuAccessKey"); } if (dm.xdoc.body != null) { dm.xdoc.body.appendChild(menuButton); } } function getDeconflicted(prefsName, attrName) { var prefValue = prefs.get(prefsName); var node = xdoc.selectNodeNullable("//*[@" + attrName + "='" + prefValue + "']"); if (node != null) { log.warn("Conflict: <" + node.nodeName + "> element on this page is already using " + attrName + "=" + prefValue); prefValue = null; } return prefValue; } // Prefs dialog function openPrefsDialog(event) { var prefsMgr = new PreferencesManager( dm.xdoc, dm.metadata["moniker"] + "_prefs", dm.metadata["name"] + " Preferences", { OK: function okPrefs(doc) { prefsMgr.storePrefs(); }, Cancel: noop } ); var prefsDialog_div = prefsMgr.open(); if (prefsDialog_div == null) return; // the dialog is already open prefsMgr.constructDockPrefsMenuSection = function(contextNode) { var prefsDock_div = dm.xdoc.createTopicDiv("Dock [Prefs] Menu", contextNode); contextNode.style.verticalAlign = "top"; with (prefsDock_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "prefsMenuVisible", "Visible", "Prefs menu button visible on the screen" )); with (appendChild(prefsMgr.createScreenCornerPreference("prefsMenuPosition"))) { title = "Screen corner for [Prefs] menu button"; style.margin = "1px 0px 3px 20px"; } appendChild(prefsMgr.createPreferenceInput( "prefsMenuAccessKey", "Access Key", "Alt-Shift keyboard shortcut", { size:1, maxLength: 1 } )); } return prefsDock_div; } prefsMgr.constructAdvancedControlsSection = function(contextNode) { var controls_div = dm.xdoc.createTopicDiv("Advanced Controls", contextNode); with (controls_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "loggerLevel", "Logging Level", "Control level of information that appears in the Error Console", null, log.getLogLevelMap() )); } return controls_div; } dialogConstructor(prefsMgr, prefsDialog_div); } dispatchFeature("sendAnonymousStatistics", function() { var counter_img = document.createElement("img"); counter_img.id = "refactoror.net_counter"; counter_img.src = "http://refactoror.net/spacer.gif?" + dm.metadata["moniker"] + "ver=" + dm.metadata["version"] + "&od=" + GM_getValue("odometer") ; log.debug(counter_img.src + " :: location=" + document.location.href); xdoc.body.appendChild(counter_img); }); function getElapsed(name) { var prev_ms = parseInt(GM_getValue(name + "_ms", "0")); var now_ms = Number(new Date()); GM_setValue(name + "_ms", now_ms.toString()); return (now_ms - prev_ms); } } // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // =-=-=-=-=-=-=-=-=-=-=-= DOM Monkey -=-=-=-=-=-=-=-=-=-=-=-= // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* Parses the script headers into the metadata object. * Adds constants & utility methods to various javascript objects. * Initializes the Preferences object. * Initializes the logger object. */ function DomMonkey(metadata) { extendJavascriptObjects(); // DM objects provided on the context this.xdoc = extendDocument(document); this.metadata = metadata; // The values listed here are the first-time-use defaults // They have no effect once they are stored as mozilla preferences. prefs = new Preferences({ "loggerLevel": "WARN" ,"sendAnonymousStatistics": true }); log = new Logger(this.metadata["version"]); GM_setValue("odometer", GM_getValue("odometer", 0) + 1); } // ==================== DOM object extensions ==================== /** Extend the given document with methods * for querying and modifying the document object. */ function extendDocument(doc) { if (doc == null) return null; /** Determine if the current document is empty. */ doc.isEmpty = function() { return (this.body == null || this.body.childNodes.length == 0); }; /** Report number of nodes that matach the given xpath expression. */ doc.countNodes = function(xpath) { var n = 0; this.foreachNode(xpath, function(node) { n++; }); return n; }; /** Remove nodes that match the given xpath expression. */ doc.removeNodes = function(xpath) { this.foreachNode(xpath, function(node) { node.remove(); }); }; /** Hide nodes that match the given xpath expression. */ doc.hideNodes = function(xpath) { if (xpath instanceof Array) { for (var xp in xpath) { this.foreachNode(xp, function(node) { node.hide(); }); } } else { this.foreachNode(xpath, function(node) { node.hide(); }); } }; /** Make visible the nodes that match the given xpath expression. */ doc.showNodes = function(xpath) { this.foreachNode(xpath, function(node) { node.show(); }); }; /** Retrieve the value of the node that matches the given xpath expression. */ doc.selectValue = function(xpath, contextNode) { if (contextNode == null) contextNode = this; var result = this.evaluate(xpath, contextNode, null, XPathResult.ANY_TYPE, null); var resultVal; switch (result.resultType) { case result.STRING_TYPE: resultVal = result.stringValue; break; case result.NUMBER_TYPE: resultVal = result.numberValue; break; case result.BOOLEAN_TYPE: resultVal = result.booleanValue; break; default: log.error("Unhandled value type: " + result.resultType); } return resultVal; } /** Select the first node that matches the given xpath expression. * If none found, log warning and return null. */ doc.selectNode = function(xpath, contextNode) { var node = this.selectNodeNullable(xpath, contextNode); if (node == null) { // is it possible that the structure of this web page has changed? log.warn("XPath returned no elements: " + xpath + "\n" + genStackTrace(arguments.callee) ); } return node; } /** Select the first node that matches the given xpath expression. * If none found, return null. */ doc.selectNodeNullable = function(xpath, contextNode) { if (contextNode == null) contextNode = this; var resultNode = this.evaluate( xpath, contextNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return extendNode(resultNode.singleNodeValue); } /** Select all first nodes that match the given xpath expression. * If none found, return an empty Array. */ doc.selectNodes = function(xpath, contextNode) { var nodeList = new Array(); this.foreachNode(xpath, function(n) { nodeList.push(n); }, contextNode); return nodeList; } /** Select all nodes that match the given xpath expression. * If none found, return null. */ doc.selectNodeSet = function(xpath, contextNode) { if (contextNode == null) contextNode = this; var nodeSet = this.evaluate( xpath, contextNode, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); return nodeSet; } /** Iteratively execute the given func for each node that matches the given xpath expression. */ doc.foreachNode = function(xpath, func, contextNode) { if (contextNode == null) contextNode = this; // if array of xpath strings, call recursively if (xpath instanceof Array) { for (var i=0; i < xpath.length; i++) this.foreachNode(xpath[i], func, contextNode); return; } var nodeSet = contextNode.selectNodeSet(xpath, contextNode); var i = 0; var n = nodeSet.snapshotItem(i); while (n != null) { var result = func(extendNode(n)); if (result == false) { // dispatching func can abort the loop by returning false return; } n = nodeSet.snapshotItem(++i); } } /** Retrieve the text content of the node that matches the given xpath expression. */ doc.selectTextContent = function(xpath) { var node = this.selectNodeNullable(xpath, this); if (node == null) return null; return node.textContent.normalizeWhitespace(); }; /** Retrieve the text content of the node that matches the given xpath expression, * and apply the given regular expression to it, returning the portion that matches. */ doc.selectMatchTextContent = function(xpath, regex) { var text = this.selectTextContent(xpath); if (text == null) return null; return text.match(regex); }; /** Replace contents of contextNode (default: body), with specified node. * (The specified node is removed, then re-added to the emptied contextNode.) * The specified node is expected to be a descendent of the context node. * Otherwise the result is probably an error. * DOC-DEFAULT */ doc.isolateNode = function(xpath, contextNode) { if (contextNode == null) contextNode = this.body; extendNode(contextNode); var subjectNode = this.selectNode(xpath); if (subjectNode == null || subjectNode.parentNode == null) return; // gut the parent node (leave script elements alone) contextNode.foreachNode("child::*", function(node) { if (node.tagName != "SCRIPT" && node.tagName != "NOSCRIPT") { node.remove(); } }); // re-add the subject node var replacement_div = this.createElement("div"); replacement_div.id = "isolateNode:" + xpath; replacement_div.appendChild(subjectNode); contextNode.appendChild(replacement_div); return replacement_div; }; /** Add a