// ==UserScript== // @name Homestar Subtitles Pre-Alpha // @namespace http://www.hrwiki.org/ // @description Subtitles for Homestar Runner // @include http://homestarrunner.com/* // @include http://www.homestarrunner.com/* // @include http://podstar.homestarrunner.com/* // @include http://videlectrix.com/* // @include http://www.videlectrix.com/* // @include http://hrwiki.org/mirror/* // @include http://www.hrwiki.org/mirror/* // @include https://secure.homestarrunner.com/heythanks.html* // ==/UserScript== var subtitleOptions = { "colors": true, // distinguish characters by colors "captions": true, // include sound effects "lang": "en", "debug": false } var characters = { "sfx": { "color": "#FFF", "name": {"en": ""} } }; var transcript = new Array() var flash = false; var subtitleholder = false; var currentsubtitles = false; var nosubtitles = document.createComment(""); var currentsubtitles = nosubtitles; var filename = location.pathname.toLowerCase(); var i = filename.lastIndexOf('/'); if (i >= 0) filename = filename.substr(i + 1); i = filename.lastIndexOf('.'); if (i >= 0) filename = filename.substr(0,i); GM_log("Loading character list..."); GM_xmlhttpRequest({ method: "GET", url: "http://www.hrwiki.org/index.php/HRWiki:Subtitles/Characters?action=raw", onload: charactersLoaded }); function charactersLoaded(results) { var parser = new unsafeWindow.DOMParser(); var text = results.responseText; GM_log(text); text = text.replace(/^\s+/g, ""); var doc = parser.parseFromString(text, "application/xml"); var speakers = doc.getElementsByTagName("speaker"); for (var i = 0; i < speakers.length; i++) { var speakername = speakers[i].getAttribute("id"); characters[speakername] = {"color": speakers[i].getAttribute("color"), "name": {"en": ""}}; var names = speakers[i].getElementsByTagName("name"); for (var j = 0; j < names.length; j++) { var lang = names[j].getAttribute("xml:lang"); characters[speakername].name[lang] = names[j].firstChild.nodeValue; } } if (subtitleOptions["debug"]) { for (i in characters) { var p = document.createElement('p'); p.style.color = characters[i].color; p.appendChild(document.createTextNode(characters[i].name.en)); document.body.appendChild(p); } } GM_log("Loading transcript..."); GM_xmlhttpRequest({ method: "GET", url: "http://www.hrwiki.org/index.php?title=HRWiki:Subtitles/" + subtitleOptions["lang"] + "/" + escape(filename) + "&action=raw", onload: function(x) { transcriptLoaded(x, false); } }); } function transcriptLoaded(results, redirected) { GM_log("All loaded."); var parser = new unsafeWindow.DOMParser(); var text = results.responseText; GM_log(text); text = text.replace(/^\s+/g, ""); // check for redirects var matches = text.match(/^#REDIRECT\s*\[\[(.*)\]\]/i); if (matches) { if (redirected) // trap double-redirects return; text = matches[1]; if (matches = text.match(/^(.*)\|/)) text = matches[1]; if (matches = text.match(/^(.*)\#/)) text = matches[1]; text = text.replace(/^\s+|\s+$/g, ''); text = text.replace(/ /, '_'); GM_xmlhttpRequest({ method: "GET", url: "http://www.hrwiki.org/index.php?title=" + escape(text) + "&action=raw", onload: function(x) { transcriptLoaded(x, true); } }); return; } if (text == "") // text = "No transcript found"; return; var doc = parser.parseFromString(text, "application/xml"); // set some defaults if (!doc.documentElement.getAttribute("xml:lang")) doc.documentElement.setAttribute("xml:lang", "en"); if (!doc.documentElement.getAttribute("dir")) doc.documentElement.setAttribute("dir", "ltr"); // inherit languages to all subnodes inheritLanguages(doc.documentElement); // now parse the lines into divs and get start and end frames var lines = doc.getElementsByTagName("line"); for (var i = 0; i < lines.length; i++) { var line = new Object(); line.start = lines[i].getAttribute("start"); line.end = lines[i].getAttribute("end"); line.text = importNodes(lines[i]); transcript.push(line); if (subtitleOptions["debug"]) { document.body.appendChild(document.createTextNode("\xA0\xA0\xA0\xA0Start: " + line.start + " End: " + line.end)); document.body.appendChild(line.text); } } if (subtitleOptions["debug"]) document.body.style.color="white"; GM_log("Setting up subtitles..."); setupSubtitles(); } function inheritLanguages(node) { for (var i = node.firstChild; i; i = i.nextSibling) { if (i.nodeType == i.ELEMENT_NODE) { if (!i.getAttribute("xml:lang")) i.setAttribute("xml:lang", node.getAttribute("xml:lang")); if (!i.getAttribute("dir")) i.setAttribute("dir", node.getAttribute("dir")); inheritLanguages(i); } } } // replacement for document.importNode() that actually works properly // turning eg into an HTML span element, rather than an unknown element // that happens to be called "span". function importNodes(node) { var name = node.nodeName.toLowerCase(); if (characters[name]) { node.setAttribute("speaker", name); name = "speaker"; } if (name == "line" || name == "speaker") { // format the speaker appropriately as a div var speaker = node.getAttribute("speaker"); if (!subtitleOptions["captions"] && speaker == "sfx") return document.createComment(""); // return nothing newNode = document.createElement("div"); var char = characters[speaker]; if (!char) char = {"color": "#FFF", "name": {"en": ""}}; if (subtitleOptions["colors"]) newNode.style.color = char.color; if (node.getAttribute("voiceover")) newNode.style.fontStyle = "italic"; if (node.getAttribute("volume")) newNode.style.fontSize = (node.getAttribute("volume") * 100) + "%"; newNode.lang = node.getAttribute("xml:lang"); newNode.dir = node.getAttribute("dir"); var hasSpeakerChildren = false; for (var i = node.firstChild; i; i = i.nextSibling) { if (i.nodeType == i.ELEMENT_NODE) { newNode.appendChild(importNodes(i)); var a = i.nodeName.toLowerCase(); if (i == "line" || i == "speaker" || characters[i]) hasSpeakerChildren = true; } else if (i.nodeType == i.TEXT_NODE || i.nodeType == i.CDATA_SECTION_NODE) newNode.appendChild(document.importNode(i, true)); } if (!hasSpeakerChildren) { // this is a normal text node - do some extra text stuff if (speaker == 'sfx' || speaker == 'thecheat' || speaker == 'pompom') { newNode.insertBefore(document.createTextNode('('), newNode.firstChild); newNode.appendChild(document.createTextNode(')')) newNode.style.fontStyle = "italic"; } if (!subtitleOptions["colors"]) { // find the language with the longest prefix match // fall back to "en" if none found var bestmatch = "en"; var langbits = node.getAttribute("xml:lang").split("-"); for (i = langbits.length; i >= 1; i--) { var lang = langbits.slice(0, i).join("-"); if (char.name[lang]) { bestmatch = lang; break; } } if (char.name[bestmatch] != '') newNode.insertBefore(document.createTextNode(char.name[bestmatch] + ": "), newNode.firstChild); } } return newNode; } else { // check element blacklist if (name == "script" || name == "style" || name == "object" || name == "param" || name == "embed" || name == "a" || name == "img" || name == "applet" || name == "map" || name == "frame" || name == "iframe" || name == "meta" || name == "link" || name == "form" || name == "input") return document.createComment(""); // return nothing var newNode = document.createElement(name); // copy across attributes for (var i = 0; i < node.attributes.length; i++) { name = node.attributes[i].nodeName.toLowerCase(); // check attribute blacklist // javascript, and anything that might load stuff from offsite if (name != "href" && name != "src" && name.substring(0, 2) != "on") { if (name == "style") { // regex taken from MediaWiki Sanitizer.php if (!node.attributes[i].nodeValue.match(/(expression|tps*:\/\/|url\\s*\().*/i)) newNode.setAttribute("style", node.attributes[i].nodeValue); } else newNode.setAttribute(node.attributes[i].nodeName, node.attributes[i].nodeValue); } } // copy across children for (var i = node.firstChild; i; i = i.nextSibling) { if (i.nodeType == i.ELEMENT_NODE) newNode.appendChild(importNodes(i)); else if (i.nodeType == i.TEXT_NODE || i.nodeType == i.CDATA_SECTION_NODE) newNode.appendChild(document.importNode(i, true)); } return newNode; } document.createComment(""); // fallthrough } function setupSubtitles() { subtitleholder = document.createElement('div'); subtitleholder.style.fontSize = "25px"; subtitleholder.style.lineHeight = "25px"; subtitleholder.style.color = "white"; subtitleholder.style.backgroundColor = "black"; subtitleholder.style.height = "100px"; subtitleholder.style.marginTop = "10px"; subtitleholder.style.textAlign = "center"; document.body.appendChild(subtitleholder); subtitleholder.appendChild(nosubtitles); flash = document.getElementsByTagName("embed")[0].wrappedJSObject; var oldopen = unsafeWindow.open; unsafeWindow.open = function(a,b,c) { c = c.split(/,/); var opts = {}; for (var i = 0; i < c.length; i++) { var d = c[i].split(/=/); opts[d[0]] = d[1]; } opts["width"] = window.screen.width * 0.8; opts["height"] = window.screen.height * 0.8; opts["resizeable"] = "yes"; c = ""; for (var i in opts) { c += i + "=" + opts[i] + ","; } return oldopen(a,b,c); } GM_log("Starting refresh loop..."); setTimeout(refreshSubtitles, 100); } function setSubtitles(node) { if (!node) node = nosubtitles; if (currentsubtitles != node) { subtitleholder.replaceChild(node, subtitleholder.firstChild); currentsubtitles = node; } } function refreshSubtitles() { var frame = false; if (flash) frame = flash.CurrentFrame; if (typeof(frame) == "function") frame = frame(); if (typeof(frame) == "number" && frame >= 0) { // change from native 0-based to more friendly 1-based frame++; // binary search to find the right transcript line var first = 0; var last = transcript.length; while(first < (last - 1)) { var mid = (first + last) >> 1; if (frame >= transcript[mid].start) first = mid; else last = mid; } // should we actually show the line? if(transcript[first].start <= frame && transcript[first].end >= frame) setSubtitles(transcript[first].text); else setSubtitles(false); } setTimeout(refreshSubtitles, 100); }