// $Id: imdbweaver.user.js 781 2014-06-26 23:26:56Z 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 whenever you visit imdb.com you will see extra functionality // Documentation here: http://refactoror.net/greasemonkey/imdbWeaver/doc.html // ----------------------------------------------------------------------------- // ==UserScript== // @name IMDb Weaver // @moniker iwvr // @namespace http://refactoror.net/ // @description Enhances the content of imdb pages by correlating information from related pages. // @version 0.3.8.2 // @author Chris Noe // @include imdb.com/* // @include *.imdb.com/* //--------- // @exclude *.imdb.com/images/* // @exclude *doubleclick* // @exclude *google_afc* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_log // @grant GM_xmlhttpRequest // @downloadURL https://update.greasyfork.icu/scripts/5058/IMDb%20Weaver.user.js // @updateURL https://update.greasyfork.icu/scripts/5058/IMDb%20Weaver.meta.js // ==/UserScript== var dm = new DomMonkey({ name : "IMDb Weaver" ,moniker : "iwvr" ,version : "0.3.8.2" }); // The values listed here are the first-time-use defaults // They have NO EFFECT once this script is installed. prefs.config({ "ajaxOperationLimit": 10 ,"all-topnav-isExpanded": true ,"highlightTitleTypes": true ,"firstMatchAccessKey": "A" ,"name-headshotMagnifier": true ,"name-ShowAge": true ,"name-ShowAgeAtTitleRelease": true ,"prefsMenuAccessKey": "P" ,"prefsMenuPosition": "BR" ,"prefsMenuVisible": true ,"removeAds": true ,"title-attributes": true ,"title-headshotMagnifier": true ,"title-headshotMagnification": 12 ,"title-ShowAges": true ,"title-StartResearch": true }); var titleHighlighers = { Khaki: ["(V)"] // direct to video, no theatrical release ,Lavender: ["(TV)", "TV series", "TV mini-series", "TV episode"] ,Pink: ["(VG)"] // video game }; // --------------- Page handlers --------------- tryCatch(dm.metadata["moniker"], function () { if (isStaticPageRequest()) { // a request from a DocumentContainer object log.debug("~~~~~ Static page request, no page processing: " + dm.xdoc.location.href); // TBD: need to add "ajaxstatic" param to all URLs to prevent page graph recursion dm.xdoc.foreachNode( "//a[contains(@href, 'imdb.com')]", function(a) { a.href = ajaxstaticUrl(a.href); log.debug("In " + dm.xdoc.location.href + " :: " + a.href); } ); return; } if (window != window.top) { log.debug("subordinate window, no page processing: " + document.location.href); return; // this is a subordinate window (iframe, etc) } if (dm.xdoc.location.href.match(/\/title\/tt\d+\/?/)) enhanceTitlePage(); else if (dm.xdoc.location.href.match(/\/name\/nm\d+\/?/)) enhanceNamePage(); else if (dm.xdoc.location.href.match(/\/find\?/)) enhanceFindPage(); else if (dm.xdoc.location.href.match(/\/updates\/history/)) enhanceUpdatePage(); else { log.info("IMDb generic page"); extendImdbDocument(dm.xdoc); enhanceImdbPage(dm.xdoc); } }); function isStaticPageRequest() { return dm.xdoc.location.href.match("ajaxstatic"); } var titleDoc; var nameDoc; // --------- Annoying intervening Ad pages --------- function expediteAdPage() { var i = location.href.indexOf("dest="); var redirUrl = "http:" + location.href.substring(i + 12); log.info("Ad page, skipping immediately to: " + redirUrl); // location.href = redirUrl; } // --------------- Title Page handler --------------- function enhanceTitlePage() { log.info("IMDb Title page"); titleDoc = extendImdbTitleDocument(dm.xdoc); enhanceImdbPage(dm.xdoc); dispatchFeature("removeAds", function() { titleDoc.removeAds(); }); // Gallery Magnifier // titleDoc.foreachNode("//div[@class='media_strip_thumbs']//img[contains(@src, '._V1._CR')]", function(img) { // // substitute the larger image // img.src = img.src.replace("_V1._CR75,0,300,300_SS90_.jpg", "_V1._SY400_SX600_.jpg"); // img.style.cssFloat = "none"; // var a = img.selectNode("ancestor::a[1]"); // var wrapper_div = a.wrapIn("div", {className: "iwvr_gallery_pic"} ); // wrapper_div.style.minWidth = img.clientWidth; // }); // titleDoc.addStyle( // "div.iwvr_gallery_pic:hover a img {\n" // + " height: auto;\n" // + " width: auto;\n" // + " position: absolute;\n" // + " margin-top: +100px;\n" // + " margin-left: -25px;\n" // + "}\n" // ); // createImageMagnifiers(titleDoc, "_V1._CR", /_CR[0-9,_]*/, "_SX600_SY400_"); dispatchFeature("title-headshotMagnifier", function() { var mag = prefs.get("title-headshotMagnification") || 12; var pixels = mag * 32; var fileSfx = "_V1._SX" + pixels + "_SY" + pixels + "_"; createImageMagnifiers(titleDoc, "_V1._SX23_SY30_", /_SX\d+_SY\d+_/, fileSfx); }); dispatchFeature("title-attributes", function() { addTitleAttrs(); }); // Display rating/runtime/language directly below title function addTitleAttrs() { var titleAttrs = new Array(); var cert = titleDoc.getCertification(); if (cert != null) { titleAttrs.push(cert); } var runtime = titleDoc.getRuntime(); if (runtime != null) { var rt = runtime + " min"; if (runtime > 60) { rt += " (" + formatHoursMinutes(runtime) + ")"; } titleAttrs.push(rt); } var lang = titleDoc.getLanguage(); if (lang != null) { titleAttrs.push(lang); } var languages = []; titleDoc.foreachNode("//a[contains(@href,'/language/')]", function(lang_a) { languages.push(lang_a.textContent); }); titleAttrs.push(languages.join("/")); var title_div = titleDoc.selectNodeNullable("//div[@id='tn15title']"); if (titleAttrs.length > 0) { title_div.appendChildText(titleAttrs.join(", ")); } } function formatHoursMinutes(min) { var m = "0" + min % 60; // (extra leading zero) var h = (min - m) / 60; return h + ":" + m.substring(m.length - 2); } addCastToolbar(); function addCastToolbar() { var toolbar_span = titleDoc.createXElement("span", {id: "iwvr_casttoolbar"} ); var quotedTitle = '"' + titleDoc.getTitle() + '"'; dispatchFeature("title-ShowAges", function() { var showCastAges_button = titleDoc.createXElement("button", {id: "iwvr_showCastAges"} ); with (showCastAges_button) { className = "iwvr_button"; textContent = "Show Ages"; title = "Show cast ages when " + quotedTitle + " was released in " + titleDoc.getTitleYear(); addEventListener('click', showCastAges, false); } with (toolbar_span) { appendChildTextNbsp(3); appendChild(showCastAges_button); } }); dispatchFeature("title-StartResearch", function() { var startResearch_button = titleDoc.createXElement("button", {id: "iwvr_startReasearch"} ); with (startResearch_button) { className = "iwvr_button"; addEventListener('click', startResearch, false); textContent = "Start Research..."; title = "Mine additional information about " + quotedTitle; } with (toolbar_span) { appendChildTextNbsp(3); appendChild(startResearch_button); } }); var cast_table = titleDoc.getCast_table(); if (cast_table != null) { cast_table.prependSibling(toolbar_span); } } function showCastAges(event) { // prevent second execution on same page event.target.removeEventListener('click', showCastAges, false); // process each cast member name var nodeCount = 0; var ajaxOperationLimit = prefs.get("ajaxOperationLimit"); titleDoc.foreachCastMember_tr ( "//td[@class='nm']" + titleDoc.CAST_NAMES_A + "[not(following-sibling::span[@id='iwvr_age'])]", function(a_name) { if (nodeCount < ajaxOperationLimit || ajaxOperationLimit == -1) { var titleDC = new DocumentContainer(); log.debug("%%%%% loadFromSameOrigin: " + a_name.href); titleDC.loadFromSameOrigin( // extract info from the cast member page a_name.href, function(doc) { var nameDoc = extendImdbNameDocument(doc); var titleYear = titleDoc.getTitleYear(); if (titleYear == null || titleYear == "") { age = "?"; } else { var age = nameDoc.getAge(titleYear); if (age == null) age = "?"; } // var dd_a = nameDoc.selectNodeNullable("//a[contains(@href, 'death_date=')]"); // if (dd_a) // dd = " " + dd_a.textContent; var age_span = titleDoc.createXElement("span", {id: "iwvr_age"} ); age_span.appendChildText(" (" + age + ")"); a_name.appendSibling(age_span); } ); } nodeCount++; } ) event.target.textContent = "Show More Ages"; event.target.addEventListener('click', showCastAges, false); } function startResearch() { if (titleDoc.selectNodeNullable("//div[@id='iwvr_researchItems_dialog']")) { return; // the dialog is already open } // make all names on page draggable titleDoc.foreachNode( titleDoc.CAST_NAMES_A, function(name_a) { name_a.className = "iwvr_researchable_item"; name_a.addEventListener('draggesture', addResearchItem, false); } ); function addResearchItem(event) { var original_item_a = event.target.parentNode; var dragged_item_a = original_item_a.cloneNode(true); var tip = titleDoc.selectNodeNullable("//*[@id='draggingTip']"); if (tip != null) tip.remove(); var ul = titleDoc.selectNode("//ul[@id='iwvr_research_itemlist']"); var li = titleDoc.createXElement("li"); li.appendChild(dragged_item_a); ul.appendChild(li); original_item_a.className = null; original_item_a.removeEventListener('draggesture', addResearchItem, false); }; // present the Research dialog var researchDialog = new DialogBox(titleDoc, "Research"); var researchDialog_div = researchDialog.createDialog( "iwvr_researchItems", "z-index: 999; position: fixed; bottom: 5px; right: 10px;", { OK: okResearch, Cancel: cancelResearch } ); researchDialog.main_td.style.padding = "6px"; with (researchDialog_div) { style.fontSize = "10pt"; style.fontFamily = "Arial, Helvetica, sans-serif"; researchDialog_div.style.overflow = "auto"; var sel = titleDoc.createSelect( "Research", { name: "researchmode" }, { tic: "Titles in common" }, "tic"); appendChild(sel); var scroll_div = titleDoc.createXElement("div"); with (scroll_div) { with (style) { border = "1px inset Black"; margin = 4; padding = 5; width = 200; height = 60; overflow = "auto"; } var items_ul = titleDoc.createXElement("ul", {id: "iwvr_research_itemlist"} ); with (items_ul.style) { marginTop = "0px"; paddingTop = "0px"; marginLeft = "0px"; paddingLeft = "1em"; } appendChild(items_ul); var tip_div = titleDoc.createXElement("div"); tip_div.id = "draggingTip"; with (tip_div.style) { color = "Gray"; width = "100%"; textAlign = "center"; height = "85%"; verticalAlign = "middle"; } tip_div.appendChildText("Drag Names Here"); appendChild(tip_div); } appendChild(scroll_div); var includes_div = titleDoc.createTopicDiv("Include"); includes_div.style.borderColor = "DarkGreen"; // includes_div.style.backgroundColor = "#66CC33"; with (includes_div.contentElement) { appendChild(titleDoc.createCheckbox( "Individual Episodes", {id: "include-episodes"}, false )); } appendChild(includes_div); var excludes_div = titleDoc.createTopicDiv("Exclude Categories"); excludes_div.style.borderColor = "#AA0000"; // excludes_div.style.backgroundColor = "#FF3300"; with (excludes_div.contentElement) { appendChild(titleDoc.createCheckbox( "Self", {id: "exclude-self", title: "Examples: Oprah, Letterman"}, true )); appendChildElement("br"); appendChild(titleDoc.createCheckbox( "Archive Footage", {id: "exclude-archive-footage"}, true )); } appendChild(excludes_div); } } function okResearch(doc) { var titleDoc = extendImdbTitleDocument(doc); // get URLs of selected research items var peopleUrls = new Array(); titleDoc.foreachNode("//ul[@id='iwvr_research_itemlist']//a", function(name_a) { peopleUrls.push(name_a.href); }); var includeEpisodes = titleDoc.selectNode("//input[@id='include-episodes']").checked; var omitCategories = new Array(); if (titleDoc.selectNode("//input[@id='exclude-self']").checked) omitCategories.push("self"); if (titleDoc.selectNode("//input[@id='exclude-archive-footage']").checked) omitCategories.push("archive"); endResearch(titleDoc); if (peopleUrls.length > 0) { correlatePeople(peopleUrls, includeEpisodes, omitCategories); } } function cancelResearch(doc) { var titleDoc = extendImdbTitleDocument(doc); endResearch(titleDoc); } function endResearch(titleDoc) { // remove class="iwvr_researchable_item" titleDoc.foreachNode( titleDoc.CAST_NAMES_A, function(name_a) { name_a.className = null; } ); } var DEFAULT_JOB_ABBREVS = { "A": "Actor", "A'": "Actress", "D": "Director", "P": "Producer", "PD": "Production Designer", "W": "Writer", "C": "Composer", "ST": "Soundtrack", "AD": "Art Department", "MC": "Miscellaneous Crew", "S": "Self", "Ar": "Archive Footage" }; // gather credits for specified people and // determine what titles they have in common function correlatePeople(urlList, includeEpisodes, omitCatList) { // foreach (var User in Users) // { //
[ActionSpan(User)]
// if (User.IsAnonymous) // { //
[RenderGravatar(User)]
//
[UserRepSpan(User)]
[UserFlairSpan(User)]
// } // else // { //
anonymous
// } // } var jobAbbrevs = new AbbreviationMap(DEFAULT_JOB_ABBREVS); var jointCredits_a = new Array(); var nameSet_a = new Array(); withDocuments( urlList, // gather credits from each person's page function(doc) { var nameDoc = extendImdbNameDocument(doc); var name_a = nameDoc.getName_a(); nameSet_a.push(name_a); jointCredits_a = jointCredits_a.concat( nameDoc.getCredits_a( {imdbName_a: name_a}, includeEpisodes, omitCatList) ); }, function(docList) { // sort combined credits (by url) sortBy(jointCredits_a, ["href"] ); sortBy(nameSet_a, ["textContent"] ); var creditsInCommon_a = new Array(); var soloCreditCount = 0; foreachGrouping(jointCredits_a, "href", function(creds_a) { if (creds_a.length < 2) { soloCreditCount++; return; // exclude where not in common with anybody else } var nameJobMap = new Array(); for (var c in creds_a) { var jobList = jobAbbrevs.registerList(creds_a[c].categoryList); nameJobMap[creds_a[c].propertySet.imdbName_a] = jobList; } var combo_a = creds_a[0].cloneNode(true); combo_a.nameJobMap = nameJobMap; creditsInCommon_a.push(combo_a); }); // sort combined credits (by title) sortBy(creditsInCommon_a, ["textContent"] ); // create information popup var resultsWindow = new DialogBox(titleDoc, "Research Results"); var resultsWindow_div = resultsWindow.createDialog( "iwvr_creditsInCommon", "z-index: 999; position: fixed; left: 15px; top: 20px;", { X: noop } ); resultsWindow.main_td.style.padding = "6px"; with (resultsWindow_div) { style.maxWidth = window.innerWidth - 70; style.maxHeight = window.innerHeight - 90; // style.overflow = "auto"; // construct the data table var i = 1; var table = titleDoc.createXElement("table", {className: "iwvr_results"} ); var caption = titleDoc.createXElement("caption"); caption.appendChildText("Credits in common", ["b"]); table.appendChild(caption); var cellAttrList = [ {className: "iwvr_results bulletcol"}, {className: "iwvr_results titlecol"} ].concat( dup(nameSet_a.length, {className: "iwvr_results datacol"} ) ); var thead = titleDoc.createXElement("thead"); thead.appendTableRow([null, "Title"].concat(nameSet_a), cellAttrList); table.appendChild(thead); var tbody = titleDoc.createXElement("tbody"); for (var c in creditsInCommon_a) { var rowSet = initArrayIndices(2 + nameSet_a.length); rowSet[0] = i + ")"; var credit_span = titleDoc.createXElement("span"); if (creditsInCommon_a[c].seriesTitle != null) { credit_span.appendChildText(creditsInCommon_a[c].seriesTitle); } credit_span.appendChild(creditsInCommon_a[c]); rowSet[1] = credit_span; for (var name_a in creditsInCommon_a[c].nameJobMap) { var jobList = creditsInCommon_a[c].nameJobMap[name_a]; var col = 2 + parseInt(arrayIndexOf(nameSet_a, name_a)); rowSet[col] = jobList.sort().join(", "); } tbody.appendTableRow(rowSet, cellAttrList); i++; } var legend_div = jobAbbrevs.toLegend("div", { className: "iwvr_results_legend" } ); tbody.appendTableRow( [ legend_div ], [ { colSpan: (2 + nameSet_a.length) } ] ); table.appendChild(tbody); appendChild(table); } } ); } } function dup(count, value) { var returnValue = new Array(); for (var i = 0; i < count; i++) { returnValue.push(value); } return returnValue; } // --------------- Name Page handler --------------- function enhanceNamePage() { log.info("IMDb Name page"); var nameDoc = extendImdbNameDocument(dm.xdoc); enhanceImdbPage(dm.xdoc); dispatchFeature("removeAds", function() { nameDoc.removeAds(); }); dispatchFeature("name-headshotMagnifier", function() { createImageMagnifiers(nameDoc, "_V1._SX32_", /_SX\d+_/, "_SX640_SY720_"); }); var birthInfo_div = nameDoc.selectNodeNullable( "//a[contains(@href, 'birth_year=')]/ancestor::div[1]"); var age = nameDoc.getAge(); var deathInfo_div = nameDoc.selectNodeNullable( "//a[contains(@href, 'death_date=')]/ancestor::div[1]"); var ageDeath = nameDoc.getAgeDeath(); dispatchFeature("highlightTitleTypes", function() { // first defeat the site's regular even/odd row highlighting nameDoc.foreachNode("//tbody[contains(@class, 'row-filmo-')]", function(tbody) { tbody.style.backgroundColor = "White"; }); // now add custom highlighting highlightTitleTypes(titleHighlighers); function highlightTitleTypes(hSpecs) { for (var color in hSpecs) { var matchStrs = hSpecs[color]; for (var i in matchStrs) { nameDoc.foreachNode([ "//text()[contains(., '" + matchStrs[i] + "')]//ancestor-or-self::tbody[1]", "//text()[contains(., '" + matchStrs[i] + "')]//ancestor-or-self::li[1]" ], function(item) { item.style.backgroundColor = color; }); } } } }); dispatchFeature("name-ShowAge", function() { if (birthInfo_div == null || age == null) { log.info("name-ShowAge: no birth year"); } else { birthInfo_div.appendChildElement("br"); birthInfo_div.appendChildText( ((nameDoc.getDeathDetails_div() != null) ? "would be " : "is ") + age + " years old" ); } if (deathInfo_div == null || ageDeath == null) { log.info("name-ShowAge: no death year"); } else { deathInfo_div.appendChildElement("br"); deathInfo_div.appendChildText( "at age " + ageDeath ); } }); dispatchFeature("name-ShowAgeAtTitleRelease", function() { // if we are arriving at a person's page from a specific title page if (dm.xdoc.referrer.match("imdb.com/title")) { // trim all but base title URL, (could be arriving from other detail pages) dm.xdoc.referrer.match(/(.*tt\d*)/); var referrerUrl = RegExp.$1; var nameDC = new DocumentContainer(); nameDC.loadFromSameOrigin( // use info from the referring title page referrerUrl, function(doc) { var titleDoc = extendImdbTitleDocument(doc); var titleYear = titleDoc.getTitleYear(); var ageThen; if (titleYear == null || titleYear == "") { ageThen = "?"; } else { ageThen = nameDoc.getAge(titleYear); if (ageThen == null) ageThen = "?"; } if (birthInfo_div == null || age == null) { log.info("name-ShowAgeAtTitleRelease: no birth year"); return; } birthInfo_div.appendChildElement("br"); var t; if (titleYear > (new Date()).getFullYear()) t = '(will be ' + ageThen + ' when "' + titleDoc.getTitle() + '" releases in ' + titleYear + ')' ; else t = '(was ' + ageThen + ' when "' + titleDoc.getTitle() + '" was released in ' + titleYear + ')' ; birthInfo_div.appendChildText(t); } ); } else { if (dm.xdoc.referrer == "") { log.warn("The name-ShowAgeAtTitleRelease option is enable" + " , but document.referrer is empty. REFERRER MAY BE DISABLED." ); } } }); } // --------------- Find Page handler --------------- function enhanceFindPage() { log.info("IMDb Find page"); var findDoc = extendImdbNameDocument(dm.xdoc); enhanceImdbPage(dm.xdoc); // assign access key to first matching item var accessKey = prefs.get("firstMatchAccessKey"); if (accessKey != null) { // first text link to "/rg/find-", that is inside a TD var firstMatch_a = findDoc.selectNodeNullable( "//td/a[not(img)][contains(@onclick, '/rg/find-')][1]"); if (firstMatch_a != null) { firstMatch_a.accessKey = accessKey.toUpperCase(); var itemNum_td = firstMatch_a.selectNodeNullable("preceding::td[1]"); if (itemNum_td != null) { var span = findDoc.createXElement("span"); span.appendChildText("[" + accessKey + "]"); firstMatch_a.href.match(/\/(\w+)\//); var linkType = RegExp.$1; // "title" or "name" span.title = "Alt-Shift-" + accessKey + " to go to this " + linkType; span.style.fontWeight = "bold"; itemNum_td.innerHTML = ""; itemNum_td.appendChild(span); } } } dispatchFeature("highlightTitleTypes", function() { highlightTitleTypes(titleHighlighers); function highlightTitleTypes(hSpecs) { for (var color in hSpecs) { var matchStrs = hSpecs[color]; for (var i in matchStrs) { findDoc.foreachNode("//a[contains(@onclick, '/rg/find-title')]/ancestor::table[1]//text()[contains(., '" + matchStrs[i] + "')]" + "//ancestor-or-self::td[1]", function(item) { item.style.backgroundColor = color; }); } } } }); // // Poster Magnifier // findDoc.foreachNode("//a[contains(@href, 'title-tiny')]/img", function(img) { // // substitute the larger image // img.src = img.src.replace(/(\d)t\.(jpg|png|gif)$/, "$1m.$2"); // img.className = "iwvr_headshot"; // img.height = null; // img.width = null; // }); // titleDoc.addStyle( // "img.iwvr_headshot { height: 32px; width: 22px; }\n" // + "td:hover img.iwvr_headshot {\n" // + " height: auto;\n" // + " width: auto;\n" // + " position: absolute;\n" // + " margin-top: -59px;\n" // + " margin-left: -125px;\n" // + "}\n" // ); } // --------------- Find Page handler --------------- function enhanceUpdatePage() { log.info("IMDb updates page"); // assign access key to first matching item var accessKey = prefs.get("firstMatchAccessKey"); if (accessKey) { var updateDoc = extendDocument(dm.xdoc); // first update item var firstMatch_a = updateDoc.selectNodeNullable("(//a[contains(@href, 'update?load=')])[1]"); if (firstMatch_a) { firstMatch_a.accessKey = accessKey.toUpperCase(); var item_td = firstMatch_a.selectNodeNullable("ancestor::td[1]"); if (item_td) { var span = updateDoc.createXElement("span"); span.appendChildText("[" + accessKey + "]"); span.style.fontWeight = "bold"; item_td.prependChild(span); } } } } // --------------- Find Page handler --------------- function enhanceImdbPage(imdbDoc) { log.info("enhanceImdbPage"); imdbDoc.removeAds(); var navbar_div = imdbDoc.selectNodeNullable("//div[@id='nb20']"); if (navbar_div != null) { navbar_div.makeCollapsible("all-topnav-isExpanded", true); } } // --------------- Title Page extensions --------------- function extendImdbTitleDocument(titleDoc) { if (titleDoc == null) return null; extendImdbDocument(titleDoc); titleDoc.removeAds_super = titleDoc.removeAds; titleDoc.removeAds = function() { this.removeAds_super(); titleDoc.foreachNode([ "//div[@id='tn15shopbox']" ,"//div[@id='tn15adrhs']" ,"//div[starts-with(@id, 'banner')]" ,"//div[@id='tn15tc']" // ,"//a[contains(href, NAVSTRIP)]" ], function(node) { node.remove(); log.debug("Removing ad element: <" + node.tagName + " id=" + node.id); } ); } titleDoc.getTitle = function() { var title; var poster_a = this.selectNodeNullable("//a[@name='poster']"); if (poster_a != null) { title = poster_a.title; } else { var title_span = this.selectNode("//title/text()"); title = title_span.textContent.replace(/\(\d\d\d\d\)/, ""); } return title.stripQuoteMarks().normalizeWhitespace(); }; titleDoc.getTitle_a = function() { var a = this.createXElement("a"); a.href = this.location.href; a.appendChildText(this.getTitle()); return a; } titleDoc.getTitleYear = function() { var year; var airDate = this.selectTextContent( "//*[text()='Original Air Date:']/following-sibling::*[1]/text()"); if (airDate != null) { airDate.match(/\d+ \w+ (\d\d\d\d)/); year = RegExp.$1; return year; } var year = this.selectTextContent( "//div[@id='tn15title']//a[contains(@href, '/year/')]/text()"); return year; }; titleDoc.getUserRating = function() { var userRating = this.selectTextContent( "//*[contains(text(), 'User Rating:')]/following-sibling::*[1]"); if (userRating == null) return null; userRating = userRating.split("/"); return userRating[0] / userRating[1]; }; titleDoc.getRuntime = function() { var runtime_text = this.selectNodeNullable( "//*[text()='Runtime:']/following-sibling::*[1]/text()"); if (runtime_text == null) { return null; } var runtime = runtime_text.textContent.match(/(\d+)/); return RegExp.$1; }; titleDoc.getCertification = function(country) { if (country == null) { country = "USA"; } var cert = this.selectTextContent( "//a[starts-with(@href, concat('/List?certificates=', '" + country + ":'))]"); return cert; }; titleDoc.getLanguage = function() { var lang_a = this.selectNodeNullable( "//a[starts-with(@href, '/Sections/Languages/')]"); if (lang_a == null) { return null; } return lang_a.textContent; }; titleDoc.CAST_TABLE = "//table[@class='cast']" ; titleDoc.CAST_NAMES_A = "//a[starts-with(@href, '/name/')]"; titleDoc.CHARACTER_NAME_A = "//a[@href='quotes']"; titleDoc.foreachCastMember_tr = function(relativeXpath, func) { this.foreachNode( titleDoc.CAST_TABLE + "//tr" + relativeXpath, func ); } titleDoc.getCast_table = function() { return this.selectNodeNullable(titleDoc.CAST_TABLE); }; log.debug( "extendImdbTitleDocument: " + "title='" + titleDoc.getTitle() + "'" + ", titleYear=" + titleDoc.getTitleYear() + ", userRating=" + titleDoc.getUserRating() ); return titleDoc; } // --------------- Title Page extensions --------------- function extendImdbNameDocument(nameDoc) { if (nameDoc == null) return null; extendImdbDocument(nameDoc); nameDoc.removeAds_super = nameDoc.removeAds; nameDoc.removeAds = function() { this.removeAds_super(); var ad_div = nameDoc.selectNodeNullable("//div[@id='tn15adrhs']"); if (ad_div != null) { ad_div.remove(); } } nameDoc.getName = function() { return this.selectTextContent("//title"); }; nameDoc.getName_a = function() { var a = this.createXElement("a"); a.href = this.location.href; a.appendChildText(this.getName()); return a; } nameDoc.getAge = function(refYear) { if (refYear == null) { refYear = (new Date()).getFullYear(); } var birthYear = this.getBirthYear(); if (birthYear == null) { return null; } var age = refYear - birthYear; if (isNaN(age)) { return "??"; } else { return age; } }; nameDoc.getBirthYear = function() { var birthYear_a = this.selectNodeNullable( "//a[contains(@href, 'birth_year=')]" ); if (birthYear_a == null) return null; else return birthYear_a.textContent; }; nameDoc.getBirthDetails_end = function() { var birthDetails_end = this.selectNodeNullable( "//a[contains(@href, 'birth_year=')]" + "/following::br[1]" ); return birthDetails_end; } nameDoc.getAgeDeath = function() { var deathYear = this.getDeathYear(); if (deathYear == null) { return null; } var ageDeath = deathYear - this.getBirthYear(); if (isNaN(ageDeath)) { return "??"; } else { return ageDeath; } }; nameDoc.getDeathYear = function() { var deathYear_a = this.selectNodeNullable( "//a[contains(@href, 'death_date=')]" ); if (deathYear_a == null) return null; else return deathYear_a.textContent; }; nameDoc.getDeathDetails_div = function() { return this.selectNodeNullable( "//*[contains(@href, 'death_date=')]" + "//ancestor::div[1]" ); } nameDoc.CREDIT_CATEGORIES_A = "//a[starts-with(@href, '/title/')]" + "/ancestor::div[@class='filmo']/descendant::a[@name][1]"; // Get all the credits on this page, grouped by title. // The list of job categories is attached to each "merged" title reference. nameDoc.getCredits_a = function(propSet, includeEpisodes, omitCatList) { var creditList_a = new Array(); nameDoc.foreachNode( // Director, Writer, Actor, etc nameDoc.CREDIT_CATEGORIES_A, function (category_a) { var catLabel = category_a.textContent.replace(/:/, ""); if (arrayIndexOf(omitCatList, category_a.name)) return; var matchCredits = "//a[@name='" + category_a.name + "']" + "/following::ol[1]/li/a[1]"; nameDoc.foreachNode( // each credit within a category matchCredits, function (credit_a) { credit_a.category = catLabel; if (propSet != null) { credit_a.propertySet = propSet; } creditList_a.push(credit_a); if (includeEpisodes) { credit_a.foreachNode( // each sub-credit within a credit "following-sibling::a", function (subCredit_a) { subCredit_a.category = catLabel; if (propSet != null) { subCredit_a.propertySet = propSet; } subCredit_a.seriesTitle = credit_a.textContent; creditList_a.push(subCredit_a); } ); } }); }); return mergeCreditCategories(creditList_a); // for each title combine multiple credits into a single object // with an array property that lists the category names function mergeCreditCategories(creditList_a) { sortBy(creditList_a, ["href"] ); var mergedCredits = new Array(); foreachGrouping(creditList_a, "href", function(creds_a) { var catList = new Array(); for (var i in creds_a) { catList.push(creds_a[i].category); } // reuse first element as a protoype var combined_a = creds_a[0].cloneNode(true); combined_a.textContent = combined_a.textContent.stripQuoteMarks(); combined_a.category = null; combined_a.categoryList = catList; combined_a.propertySet = creds_a[0].propertySet; mergedCredits.push(combined_a); }); return mergedCredits; } } log.debug( "extendImdbNameDocument: " + "name='" + nameDoc.getName() + "'" + ", birthYear='" + nameDoc.getBirthYear() + "'" + ", age=" + nameDoc.getAge() ); return nameDoc; } // --------------- IMDb Page extensions --------------- function extendImdbDocument(imdbDoc) { extendDocument(imdbDoc); addPrefsButton(); imdbDoc.removeAds = function() { log.debug("imdbDoc.removeAds"); this.foreachNode("//div[starts-with(@id, 'swf_')]", function(node) { node.remove(); log.debug("Removing ad element: <" + node.tagName + " id=" + node.id); } ); // log.debug("sweeping for DOUBLECLICK:"); // this.foreachNode("//img[contains(@src, 'doubleclick.net')]/ancestor::d[1]", function(node) { // node.remove(); // log.debug("Removing DOUBLECLICK ad div"); // } // ); this.removeFlashAds(); // experimental // this.foreachNode("//area[@alt='Learn more']", function(div) { // div.remove(); // log.debug("REMOVING NAVSTRIPE AD"); // } // ); // this.foreachNode("//a[contains(@href, '_NAVSTRIPE')]//ancestor::div[1]", function(div) { // div.remove(); // log.debug("REMOVING NAVSTRIPE AD"); // } // ); this.foreachNode( "//div[starts-with(@id, 'swf_')]", function(ad_div) { log.debug("REMOVING FLOATER AD"); ad_div.hideNode(); } ); // this.removeFloaterAds(); // imdbDoc.onAppears("//div[starts-with(@id, 'swf_')]", 500, function(ad_div) // { // log.debug("Removing floater ad"); // alert("Removing floater ad"); // ad_div.hide(); // }); } imdbDoc.removeFlashAds = function() { this.foreachNode( "//comment()[contains(., 'FLASH AD BEGINS')]", function(adbegin_cmt) { // log.info("COMMENT< '" + adbegin_cmt.textContent + "'"); // var adend_cmt = adbegin_cmt.selectNodeNullable( // "//following-sibling::comment()[contains(., 'FLASH AD ENDS')][1]"); // if (adend_cmt != null) { // log.info("COMMENT> '" + adend_cmt.textContent + "'"); // } var ad_div = adbegin_cmt.nextSibling.nextSibling; if (ad_div != null) { log.debug("Removing ad element: <" + ad_div.tagName + " id=" + ad_div.id); ad_div.parentNode.removeChild(ad_div); } } ); } // buttons imdbDoc.addStyle( ".iwvr_button {\n" + " font-size: 8pt;\n" + " font-family: Helvetica Narrow, sans-serif;\n" + "}\n" ); // research items imdbDoc.addStyle( "a.iwvr_researchable_item { background-color: Gold; }\n" + "a.iwvr_researchable_item:hover { cursor: crosshair; }\n" ); // research result grid styles imdbDoc.addStyle( ".iwvr_results {\n" + " padding: 3;\n" + " font-family: Arial Narrow, Helvetica Narrow, sans-serif;\n" + " font-size: small;\n" + "}\n" + "table.iwvr_results {\n" + " border-collapse: collapse;\n" + " font-family: Arial, Helvetica, sans-serif;\n" + "}\n" + "td.iwvr_results {\n" + " border: 1px solid Gray;\n" + " padding: 3;\n" + "}\n" + "div.iwvr_results_legend {\n" + " max-width: 100%;\n" + " text-align: center;\n" + "}\n" + "td.bulletcol { text-align: right; }\n" + "td.titlecol { text-align: left; }\n" + "td.datacol { text-align: center; }\n" ); return imdbDoc; } // --------------- helper functions --------------- function createImageMagnifiers(theDoc, imgUrlContains, imgUrlRegex, imgUrlReplacement) { // Headshot Magnifier theDoc.foreachNode("//img[contains(@src, '" + imgUrlContains + "')]", function(img) { if (img.src.indexOf("addtiny.gif") != -1) { return; // skip place-holders } // substitute the larger image img.src = img.src.replace(imgUrlRegex, imgUrlReplacement); img.className = "iwvr_headshot"; img.height = null; img.width = null; }); theDoc.addStyle( "img.iwvr_headshot { height: 30px; width: 23px; }\n" + "td:hover img.iwvr_headshot {\n" + " height: auto;\n" + " width: auto;\n" + " z-index: 999;\n" + " position: fixed;\n" + " top: 5%;\n" + " right: 5%;\n" + "}\n" ); // zoom visualization graphic // var zoom_img = dm.xdoc.createXElement("img"); // with (zoom_img.style) { // position = "absolute"; // top = "614px"; // left = "145px"; // } // var zoom_img_src = 'data:image/gif;base64,' + // 'R0lGODlhGACCAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/////' + // '/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + // 'AAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBm' + // 'mQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD/' + // '/zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNmzDNm/zOZADOZ' + // 'MzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYA' + // 'mWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ' + // '/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkz' + // 'M5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnM' + // 'mZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz' + // '/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/' + // 'M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9m' + // 'mf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP//' + // '/ywAAAAAGACCAAAI/wAfCBxIsKBBgggOKlSIIOHChwIbQoTY0OHEgxUvMsyosWBFix0jcgwpcmTIjyA7' + // 'okx5cSXJBy5JrmRJMabKmSdx3tTZcibNjTwn+pTY06fGoURrDi26VGnTh0iTLowqFSNVp0ihUv1Z8ipQ' + // 'r1a3Tt1adSDZsl3FGjyLli1NtyzhrpWLkK5ZuCDx5sVbl29atnf1/gUMU69Dw4cNDz67mGxjtYgfe41c' + // 'WHBlv5QzK75MV7Nlz5g3g+4suvRn06FPq07N2m1gu5YlP+XM+HVt24773sYNlnfWubt9zxYeVHdv47/D' + // 'HkdudOxy5sU9PoeOEmt04MOVX8e+XXpzodm/2i60bpI8WvHlzeesvv6jzPFMz2t1/z59/JeX8efHT/+l' + // '/aPygaefSAPCVOCBDwQEADs='; // zoom_img.src = zoom_img_src; // titleDoc.body.appendChild(zoom_img); // zoom_img.hide(); } // ==================== AbbreviationMap object ==================== function AbbreviationMap(initMap) { this.map = initMap; if (this.map == null) { this.map = new Array(); } this.register = function(value) { var abbrev = arrayIndexOf(this.map, value); if (abbrev != null) return abbrev; for (var len = 1; len < value.length; len++) { var abbrev = value.substring(0, len); if (this.map[abbrev] == null) { this.map[abbrev] = value; return abbrev; } } throw "Can't abbreviate: '" + value + "'"; } this.registerList = function(theList) { var abbrevList = new Array(); for (var i in theList) { abbrevList.push(this.register(theList[i])); } return abbrevList; } this.toLegend = function(elemType, attrMap) { // var dm.xdoc = extendDocument(document); var node = dm.xdoc.createXElement(elemType, attrMap); with (node) { for (var i in this.map) { appendChildText(i); appendChildText(':\u00A0"'); appendChildText(this.map[i]); appendChildText('"'); appendChildText(' - '); } } return node; } } // ==================== Preferences Dialog ==================== function addPrefsButton() { configurePrefsButton(function(prefsMgr, prefsDialog_div) { var table = dm.xdoc.createXElement("table"); prefsDialog_div.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) { var features_div = dm.xdoc.createTopicDiv("Enabled Features", td); appendChild(features_div); with (features_div.contentElement) { var genFeatures_div = dm.xdoc.createTopicDiv("All Pages", features_div); appendChild(genFeatures_div); with (genFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "highlightTitleTypes", "Highlight titles by type", "white: theatrical release / gold: direct to video / blue: TV / pink: video game" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "removeAds", "Remove advertising", "Remove advertising" )); } var titleFeatures_div = dm.xdoc.createTopicDiv("On Title Pages", features_div); appendChild(titleFeatures_div); with (titleFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "title-attributes", "Display title attributes", "Display rating/runtime/language directly below title" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "title-ShowAges", "[Show Ages] button", "Compute the ages of cast members" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "title-StartResearch", "[Start Research] button", "Open the Research dialog" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "title-headshotMagnifier", "Headshot Magnifier", "Hover mouse to magnify cast pictures" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "title-headshotMagnification", "Mag level", "Magnification factor", { size:2, maxLength: 2 } )).style.marginLeft = "28px"; } var nameFeatures_div = dm.xdoc.createTopicDiv("On Name Pages", features_div); appendChild(nameFeatures_div); with (nameFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "name-ShowAge", "Display age", "Display current age of the person" )); appendChildElement("br"); appendChild(prefsMgr.createPreferenceInput( "name-ShowAgeAtTitleRelease", "Display age at release", "Display age at time title was released" )); } var findFeatures_div = dm.xdoc.createTopicDiv("On Search Results", features_div); appendChild(findFeatures_div); with (findFeatures_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "firstMatchAccessKey", "Select first match", "Alt-Shift keyboard to navigate to first title matched", { size:1, maxLength: 1 } )); } } } var td = dm.xdoc.createXElement("td"); td.style.verticalAlign = "top"; tr.appendChild(td); with (td) { appendChild(prefsMgr.constructDockPrefsMenuSection(td)); appendChild(prefsMgr.constructAdvancedControlsSection(td)); var controls_div = dm.xdoc.createTopicDiv("Performance Controls", td); with (controls_div.contentElement) { appendChild(prefsMgr.createPreferenceInput( "ajaxOperationLimit", "Background threads", "Control how many simultaneous background operations are allowed" + ", (primarily affects Show Ages)", { size:1, maxLength: 2 } )); } appendChild(controls_div); } // 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/imdbWeaver/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() { if (getElapsed("sendAnonymousStatistics") < 2000) { log.debug("--------- SKIPPING COUNTER on rapid fire: " + dm.xdoc.location.href); return; } log.debug("--------- EMBEDDING COUNTER: " + dm.xdoc.location.href); // 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); // dm.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); if (resultNode.singleNodeValue == null) log.debug("null result for: " + xpath); 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