Skip to content

Instantly share code, notes, and snippets.

@rnikitin
Created August 6, 2012 16:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rnikitin/3276431 to your computer and use it in GitHub Desktop.
Save rnikitin/3276431 to your computer and use it in GitHub Desktop.
$(document).ready(function() {
$('.gist').each(function(i) {
writeCapture.html(this, '<script src="'+$(this).text()+'.js"></script>');
});
});
/**
* writeCapture.js v1.0.5
*
* @author noah <noah.sloan@gmail.com>
*
*/
(function($,global) {
var doc = global.document;
function doEvil(code) {
var div = doc.createElement('div');
doc.body.insertBefore(div,null);
$.replaceWith(div,'<script type="text/javascript">'+code+'</script>');
}
// ensure we have our support functions
$ = $ || (function(jQuery) {
/**
* @name writeCaptureSupport
*
* The support functions writeCapture needs.
*/
return {
/**
* Takes an options parameter that must support the following:
* {
* url: url,
* type: 'GET', // all requests are GET
* dataType: "script", // it this is set to script, script tag injection is expected, otherwise, treat as plain text
* async: true/false, // local scripts are loaded synchronously by default
* success: callback(text,status), // must not pass a truthy 3rd parameter
* error: callback(xhr,status,error) // must pass truthy 3rd parameter to indicate error
* }
*/
ajax: jQuery.ajax,
/**
* @param {String Element} selector an Element or selector
* @return {Element} the first element matching selector
*/
$: function(s) { return jQuery(s)[0]; },
/**
* @param {String jQuery Element} selector the element to replace.
* writeCapture only needs the first matched element to be replaced.
* @param {String} content the content to replace
* the matched element with. script tags must be evaluated/loaded
* and executed if present.
*/
replaceWith: function(selector,content) {
// jQuery 1.4? has a bug in replaceWith so we can't use it directly
var el = jQuery(selector)[0];
var next = el.nextSibling, parent = el.parentNode;
jQuery(el).remove();
if ( next ) {
jQuery(next).before( content );
} else {
jQuery(parent).append( content );
}
},
onLoad: function(fn) {
jQuery(fn);
},
copyAttrs: function(src,dest) {
var el = jQuery(dest), attrs = src.attributes;
for (var i = 0, len = attrs.length; i < len; i++) {
if(attrs[i] && attrs[i].value) {
try {
el.attr(attrs[i].name,attrs[i].value);
} catch(e) { }
}
}
}
};
})(global.jQuery);
$.copyAttrs = $.copyAttrs || function() {};
$.onLoad = $.onLoad || function() {
throw "error: autoAsync cannot be used without jQuery " +
"or defining writeCaptureSupport.onLoad";
};
// utilities
function each(array,fn) {
for(var i =0, len = array.length; i < len; i++) {
if( fn(array[i]) === false) return;
}
}
function isFunction(o) {
return Object.prototype.toString.call(o) === "[object Function]";
}
function isString(o) {
return Object.prototype.toString.call(o) === "[object String]";
}
function slice(array,start,end) {
return Array.prototype.slice.call(array,start || 0,end || array && array.length);
}
function any(array,fn) {
var result = false;
each(array,check);
function check(it) {
return !(result = fn(it));
}
return result;
}
function SubQ(parent) {
this._queue = [];
this._children = [];
this._parent = parent;
if(parent) parent._addChild(this);
}
SubQ.prototype = {
_addChild: function(q) {
this._children.push(q);
},
push: function (task) {
this._queue.push(task);
this._bubble('_doRun');
},
pause: function() {
this._bubble('_doPause');
},
resume: function() {
this._bubble('_doResume');
},
_bubble: function(name) {
var root = this;
while(!root[name]) {
root = root._parent;
}
return root[name]();
},
_next: function() {
if(any(this._children,runNext)) return true;
function runNext(c) {
return c._next();
}
var task = this._queue.shift();
if(task) {
task();
}
return !!task;
}
};
/**
* Provides a task queue for ensuring that scripts are run in order.
*
* The only public methods are push, pause and resume.
*/
function Q(parent) {
if(parent) {
return new SubQ(parent);
}
SubQ.call(this);
this.paused = 0;
}
Q.prototype = (function() {
function f() {}
f.prototype = SubQ.prototype;
return new f();
})();
Q.prototype._doRun = function() {
if(!this.running) {
this.running = true;
try {
// just in case there is a bug, always resume
// if paused is less than 1
while(this.paused < 1 && this._next()){}
} finally {
this.running = false;
}
}
};
Q.prototype._doPause= function() {
this.paused++;
};
Q.prototype._doResume = function() {
this.paused--;
this._doRun();
};
// TODO unit tests...
function MockDocument() { }
MockDocument.prototype = {
_html: '',
open: function( ) {
this._opened = true;
if(this._delegate) {
this._delegate.open();
}
},
write: function(s) {
if(this._closed) return;
this._written = true;
if(this._delegate) {
this._delegate.write(s);
} else {
this._html += s;
}
},
writeln: function(s) {
this.write(s + '\n');
},
close: function( ) {
this._closed = true;
if(this._delegate) {
this._delegate.close();
}
},
copyTo: function(d) {
this._delegate = d;
d.foobar = true;
if(this._opened) {
d.open();
}
if(this._written) {
d.write(this._html);
}
if(this._closed) {
d.close();
}
}
};
// test for IE 6/7 issue (issue 6) that prevents us from using call
var canCall = (function() {
var f = { f: doc.getElementById };
try {
f.f.call(doc,'abc');
return true;
} catch(e) {
return false;
}
})();
function unProxy(elements) {
each(elements,function(it) {
var real = doc.getElementById(it.id);
if(!real) {
logError('<proxyGetElementById - finish>',
'no element in writen markup with id ' + it.id);
return;
}
each(it.el.childNodes,function(it) {
real.appendChild(it);
});
if(real.contentWindow) {
// TODO why is the setTimeout necessary?
global.setTimeout(function() {
it.el.contentWindow.document.
copyTo(real.contentWindow.document);
},1);
}
$.copyAttrs(it.el,real);
});
}
function getOption(name,options) {
if(options && options[name] === false) {
return false;
}
return options && options[name] || self[name];
}
function capture(context,options) {
var tempEls = [],
proxy = getOption('proxyGetElementById',options),
forceLast = getOption('forceLastScriptTag',options),
writeOnGet = getOption('writeOnGetElementById',options),
immediate = getOption('immediateWrites', options),
state = {
write: doc.write,
writeln: doc.writeln,
finish: function() {},
out: ''
};
context.state = state;
doc.write = immediate ? immediateWrite : replacementWrite;
doc.writeln = immediate ? immediateWriteln : replacementWriteln;
if(proxy || writeOnGet) {
state.getEl = doc.getElementById;
doc.getElementById = getEl;
if(writeOnGet) {
findEl = writeThenGet;
} else {
findEl = makeTemp;
state.finish = function() {
unProxy(tempEls);
};
}
}
if(forceLast) {
state.getByTag = doc.getElementsByTagName;
doc.getElementsByTagName = function(name) {
var result = slice(canCall ? state.getByTag.call(doc,name) :
state.getByTag(name));
if(name === 'script') {
result.push( $.$(context.target) );
}
return result;
};
var f = state.finish;
state.finish = function() {
f();
doc.getElementsByTagName = state.getByTag;
};
}
function replacementWrite(s) {
state.out += s;
}
function replacementWriteln(s) {
state.out += s + '\n';
}
function immediateWrite(s) {
var target = $.$(context.target);
var div = doc.createElement('div');
target.parentNode.insertBefore(div,target);
$.replaceWith(div,sanitize(s));
}
function immediateWriteln(s) {
var target = $.$(context.target);
var div = doc.createElement('div');
target.parentNode.insertBefore(div,target);
$.replaceWith(div,sanitize(s) + '\n');
}
function makeTemp(id) {
var t = doc.createElement('div');
tempEls.push({id:id,el:t});
// mock contentWindow in case it's supposed to be an iframe
t.contentWindow = { document: new MockDocument() };
return t;
}
function writeThenGet(id) {
var target = $.$(context.target);
var div = doc.createElement('div');
target.parentNode.insertBefore(div,target);
$.replaceWith(div,state.out);
state.out = '';
return canCall ? state.getEl.call(doc,id) :
state.getEl(id);
}
function getEl(id) {
var result = canCall ? state.getEl.call(doc,id) :
state.getEl(id);
return result || findEl(id);
}
return state;
}
function uncapture(state) {
doc.write = state.write;
doc.writeln = state.writeln;
if(state.getEl) {
doc.getElementById = state.getEl;
}
return state.out;
}
function clean(code) {
// IE will execute inline scripts with <!-- (uncommented) on the first
// line, but will not eval() them happily
return code && code.replace(/^\s*<!(\[CDATA\[|--)/,'').replace(/(\]\]|--)>\s*$/,'');
}
function ignore() {}
function doLog(code,error) {
console.error("Error",error,"executing code:",code);
}
var logError = isFunction(global.console && console.error) ?
doLog : ignore;
function captureWrite(code,context,options) {
var state = capture(context,options);
try {
doEvil(clean(code));
} catch(e) {
logError(code,e);
} finally {
uncapture(state);
}
return state;
}
// copied from jQuery
function isXDomain(src) {
var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec(src);
return parts && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host );
}
function attrPattern(name) {
return new RegExp('[\\s\\r\\n]'+name+'[\\s\\r\\n]*=[\\s\\r\\n]*(?:(["\'])([\\s\\S]*?)\\1|([^\\s>]+))','i');
}
function matchAttr(name) {
var regex = attrPattern(name);
return function(tag) {
var match = regex.exec(tag) || [];
return match[2] || match[3];
};
}
var SCRIPT_TAGS = /(<script[^>]*>)([\s\S]*?)<\/script>/ig,
SCRIPT_2 = /<script[^>]*\/>/ig,
SRC_REGEX = attrPattern('src'),
SRC_ATTR = matchAttr('src'),
TYPE_ATTR = matchAttr('type'),
LANG_ATTR = matchAttr('language'),
GLOBAL = "__document_write_ajax_callbacks__",
DIV_PREFIX = "__document_write_ajax_div-",
TEMPLATE = "window['"+GLOBAL+"']['%d']();",
callbacks = global[GLOBAL] = {},
TEMPLATE_TAG = '<script type="text/javascript">' + TEMPLATE + '</script>',
global_id = 0;
function nextId() {
return (++global_id).toString();
}
function normalizeOptions(options,callback) {
var done;
if(isFunction(options)) {
done = options;
options = null;
}
options = options || {};
done = done || options && options.done;
options.done = callback ? function() {
callback(done);
} : done;
return options;
}
// The global Q synchronizes all sanitize operations.
// The only time this synchronization is really necessary is when two or
// more consecutive sanitize operations make async requests. e.g.,
// sanitize call A requests foo, then sanitize B is called and bar is
// requested. document.write was replaced by B, so if A returns first, the
// content will be captured by B, then when B returns, document.write will
// be the original document.write, probably messing up the page. At the
// very least, A will get nothing and B will get the wrong content.
var GLOBAL_Q = new Q();
var debug = [];
var logDebug = window._debugWriteCapture ? function() {} :
function (type,src,data) {
debug.push({type:type,src:src,data:data});
};
var logString = window._debugWriteCapture ? function() {} :
function () {
debug.push(arguments);
};
function newCallback(fn) {
var id = nextId();
callbacks[id] = function() {
fn();
delete callbacks[id];
};
return id;
}
function newCallbackTag(fn) {
return TEMPLATE_TAG.replace(/%d/,newCallback(fn));
}
/**
* Sanitize the given HTML so that the scripts will execute with a modified
* document.write that will capture the output and append it in the
* appropriate location.
*
* @param {String} html
* @param {Object Function} [options]
* @param {Function} [options.done] Called when all the scripts in the
* sanitized HTML have run.
* @param {boolean} [options.asyncAll] If true, scripts loaded from the
* same domain will be loaded asynchronously. This can improve UI
* responsiveness, but will delay completion of the scripts and may
* cause problems with some scripts, so it defaults to false.
*/
function sanitize(html,options,parentQ,parentContext) {
// each HTML fragment has it's own queue
var queue = parentQ && new Q(parentQ) || GLOBAL_Q;
options = normalizeOptions(options);
var done = getOption('done',options);
var doneHtml = '';
var fixUrls = getOption('fixUrls',options);
if(!isFunction(fixUrls)) {
fixUrls = function(src) { return src; };
}
// if a done callback is passed, append a script to call it
if(isFunction(done)) {
// no need to proxy the call to done, so we can append this to the
// filtered HTML
doneHtml = newCallbackTag(function() {
queue.push(done);
});
}
// for each tag, generate a function to load and eval the code and queue
// themselves
return html.replace(SCRIPT_TAGS,proxyTag).replace(SCRIPT_2,proxyBodyless) + doneHtml;
function proxyBodyless(tag) {
// hack in a bodyless tag...
return proxyTag(tag,tag.substring(0,tag.length-2)+'>','');
}
function proxyTag(element,openTag,code) {
var src = SRC_ATTR(openTag),
type = TYPE_ATTR(openTag) || '',
lang = LANG_ATTR(openTag) || '',
isJs = (!type && !lang) || // no type or lang assumes JS
type.toLowerCase().indexOf('javascript') !== -1 ||
lang.toLowerCase().indexOf('javascript') !== -1;
logDebug('replace',src,element);
if(!isJs) {
return element;
}
var id = newCallback(queueScript), divId = DIV_PREFIX + id,
run, context = { target: '#' + divId, parent: parentContext };
function queueScript() {
queue.push(run);
}
if(src) {
// fix for the inline script that writes a script tag with encoded
// ampersands hack (more comon than you'd think)
src = fixUrls(src);
openTag = openTag.replace(SRC_REGEX,'');
if(isXDomain(src)) {
// will load async via script tag injection (eval()'d on
// it's own)
run = loadXDomain;
} else {
// can be loaded then eval()d
if(getOption('asyncAll',options)) {
run = loadAsync();
} else {
run = loadSync;
}
}
} else {
// just eval code and be done
run = runInline;
}
function runInline() {
captureHtml(code);
}
function loadSync() {
$.ajax({
url: src,
type: 'GET',
dataType: 'text',
async: false,
success: function(html) {
captureHtml(html);
}
});
}
function logAjaxError(xhr,status,error) {
logError("<XHR for "+src+">",error);
queue.resume();
}
function setupResume() {
return newCallbackTag(function() {
queue.resume();
});
}
function loadAsync() {
var ready, scriptText;
function captureAndResume(script,status) {
if(!ready) {
// loaded before queue run, cache text
scriptText = script;
return;
}
try {
captureHtml(script, setupResume());
} catch(e) {
logError(script,e);
}
}
// start loading the text
$.ajax({
url: src,
type: 'GET',
dataType: 'text',
async: true,
success: captureAndResume,
error: logAjaxError
});
return function() {
ready = true;
if(scriptText) {
// already loaded, so don't pause the queue and don't resume!
captureHtml(scriptText);
} else {
queue.pause();
}
};
}
function loadXDomain(cb) {
var state = capture(context,options);
queue.pause(); // pause the queue while the script loads
logDebug('pause',src);
doXDomainLoad(context.target,src,captureAndResume);
function captureAndResume(xhr,st,error) {
logDebug('out', src, state.out);
html(uncapture(state),
newCallbackTag(state.finish) + setupResume());
logDebug('resume',src);
}
}
function captureHtml(script, cb) {
var state = captureWrite(script,context,options);
cb = newCallbackTag(state.finish) + (cb || '');
html(state.out,cb);
}
function safeOpts(options) {
var copy = {};
for(var i in options) {
if(options.hasOwnProperty(i)) {
copy[i] = options[i];
}
}
delete copy.done;
return copy;
}
function html(markup,cb) {
$.replaceWith(context.target,sanitize(markup,safeOpts(options),queue,context) + (cb || ''));
}
return '<div style="display: none" id="'+divId+'"></div>' + openTag +
TEMPLATE.replace(/%d/,id) + '</script>';
}
}
function doXDomainLoad(target,url,success) {
// TODO what about scripts that fail to load? bad url, etc.?
var script = document.createElement("script");
script.src = url;
target = $.$(target);
var done = false, parent = target.parentNode;
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function(){
if ( !done && (!this.readyState ||
this.readyState == "loaded" || this.readyState == "complete") ) {
done = true;
success();
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
parent.removeChild( script );
}
};
parent.insertBefore(script,target);
}
/**
* Sanitizes all the given fragments and calls action with the HTML.
* The next fragment is not started until the previous fragment
* has executed completely.
*
* @param {Array} fragments array of objects like this:
* {
* html: '<p>My html with a <script...',
* action: function(safeHtml,frag) { doSomethingToInject(safeHtml); },
* options: {} // optional, see #sanitize
* }
* Where frag is the object.
*
* @param {Function} [done] Optional. Called when all fragments are done.
*/
function sanitizeSerial(fragments,done) {
// create a queue for these fragments and make it the parent of each
// sanitize call
var queue = GLOBAL_Q;
each(fragments, function (f) {
queue.push(run);
function run() {
f.action(sanitize(f.html,f.options,queue),f);
}
});
if(done) {
queue.push(done);
}
}
function findLastChild(el) {
var n = el;
while(n && n.nodeType === 1) {
el = n;
n = n.lastChild;
// last child may not be an element
while(n && n.nodeType !== 1) {
n = n.previousSibling;
}
}
return el;
}
/**
* Experimental - automatically captures document.write calls and
* defers them untill after page load.
* @param {Function} [done] optional callback for when all the
* captured content has been loaded.
*/
function autoCapture(done) {
var write = doc.write,
writeln = doc.writeln,
currentScript,
autoQ = [];
doc.writeln = function(s) {
doc.write(s+'\n');
};
var state;
doc.write = function(s) {
var scriptEl = findLastChild(doc.body);
if(scriptEl !== currentScript) {
currentScript = scriptEl;
autoQ.push(state = {
el: scriptEl,
out: []
});
}
state.out.push(s);
};
$.onLoad(function() {
// for each script, append a div immediately after it,
// then replace the div with the sanitized output
var el, div, out, safe, doneFn;
done = normalizeOptions(done);
doneFn = done.done;
done.done = function() {
doc.write = write;
doc.writeln = writeln;
if(doneFn) doneFn();
};
for(var i = 0, len = autoQ.length; i < len; i++ ) {
el = autoQ[i].el;
div = doc.createElement('div');
el.parentNode.insertBefore( div, el.nextSibling );
out = autoQ[i].out.join('');
// only the last snippet gets passed the callback
safe = len - i === 1 ? sanitize(out,done) : sanitize(out);
$.replaceWith(div,safe);
}
});
}
function extsrc(cb) {
var scripts = document.getElementsByTagName('script'),
s,o, html, q, ext, async, doneCount = 0,
done = cb ? newCallbackTag(function() {
if(++doneCount >= exts.length) {
cb();
}
}) : '',
exts = [];
for(var i = 0, len = scripts.length; i < len; i++) {
s = scripts[i];
ext = s.getAttribute('extsrc');
async = s.getAttribute('asyncsrc');
if(ext || async) {
exts.push({ext:ext,async:async,s:s});
}
}
for(i = 0, len = exts.length; i < len; i++) {
o = exts[i];
if(o.ext) {
html = '<script type="text/javascript" src="'+o.ext+'"> </script>';
$.replaceWith(o.s,sanitize(html) + done);
} else if(o.async) {
html = '<script type="text/javascript" src="'+o.async+'"> </script>';
$.replaceWith(o.s,sanitize(html,{asyncAll:true}, new Q()) + done);
}
}
}
var name = 'writeCapture';
var self = global[name] = {
_original: global[name],
support: $,
/**
*/
fixUrls: function(src) {
return src.replace(/&amp;/g,'&');
},
noConflict: function() {
global[name] = this._original;
return this;
},
debug: debug,
/**
* Enables a fun little hack that replaces document.getElementById and
* creates temporary elements for the calling code to use.
*/
proxyGetElementById: false,
// this is only for testing, please don't use these
_forTest: {
Q: Q,
GLOBAL_Q: GLOBAL_Q,
$: $,
matchAttr: matchAttr,
slice: slice,
capture: capture,
uncapture: uncapture,
captureWrite: captureWrite
},
replaceWith: function(selector,content,options) {
$.replaceWith(selector,sanitize(content,options));
},
html: function(selector,content,options) {
var el = $.$(selector);
el.innerHTML ='<span/>';
$.replaceWith(el.firstChild,sanitize(content,options));
},
load: function(selector,url,options) {
$.ajax({
url: url,
dataType: 'text',
type: "GET",
success: function(content) {
self.html(selector,content,options);
}
});
},
extsrc: extsrc,
autoAsync: autoCapture,
sanitize: sanitize,
sanitizeSerial: sanitizeSerial
};
})(this.writeCaptureSupport,this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment