Created
June 11, 2015 12:52
-
-
Save dead-claudia/92a40db30a521413b684 to your computer and use it in GitHub Desktop.
Run other languages in the browser as first class citizens (uses MutationObserver with a CSS-based fallback)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This allows for compile-to-JS languages to run as first-class citizens | |
* in a browser. Very useful for development and smaller apps. It depends | |
* on MutationObserver, with a fallback to the deprecated DOM | |
* | |
* Usage: | |
* | |
* ```js | |
* installLanguage('text/coffee', CoffeeScript.compile, null, true); | |
* installLanguage('text/ls', require('livescript').run); | |
* ``` | |
*/ | |
window.installLanguage = (function (document) { | |
'use strict'; | |
var currentScript = document.currentScript; | |
var parentNode = currentScript.parentNode; | |
var generated = {}; | |
var currentId = 0; | |
var addListener = parentNode.addEventListener ? function (event, node, callback) { | |
node.addEventListener(event, callback, false); | |
} : function (event, node, callback) { | |
node.attachEvent(event, callback); | |
}; | |
var removeListener = parentNode.removeEventListener ? function (event, node, callback) { | |
node.removeEventListener(event, callback, false); | |
} : function (event, node, callback) { | |
node.detachEvent(event, callback); | |
}; | |
function insertNode(node) { | |
parentNode.insertBefore(node, currentScript); | |
} | |
function insert(node) { | |
var id = currentId++; | |
generated[id] = node; | |
var listener = function () { | |
delete generated[id]; | |
removeListener('load', node, listener); | |
listener = null; // to prevent memory leaks | |
}; | |
addListener('load', node, listener); | |
insertNode(node); | |
} | |
function forEach(xs, f) { | |
for (var i = 0; i < xs.length; i++) { | |
f(xs[i], i); | |
} | |
} | |
var observe = typeof MutationObserver !== 'undefined' ? function (mime, run, opts) { | |
var g = generated; | |
new MutationObserver(function (updates) { | |
forEach(updates, function (update) { | |
forEach(update.addedNodes, function (node) { | |
if (node.nodeType === 'SCRIPT' && | |
node.type === mime && | |
g.indexOf(node) === -1) { | |
run(node.text, opts); | |
} | |
}); | |
}); | |
}).observe(document, { | |
childList: true, | |
subtree: true | |
}); | |
} : (function () { | |
// http://www.happycode.info/create-css-classes-with-javascript/ | |
// Edited to DRY some of it, and to iterate in reverse instead of repeated | |
// assignment in loops | |
function createCSSSelectorRules(rules, selector) { | |
for (var i = 0, len = rules.length; i < len; i++) { | |
var current = rules[i]; | |
if (current.selectorText && | |
current.selectorText.toUpperCase() == selector.toUpperCase()) { | |
current.style.cssText = style; | |
return true; | |
} | |
} | |
} | |
function rule(selector, style) { | |
var styleSheets = document.styleSheets; | |
if (!styleSheets) { | |
return; | |
} | |
var len = styleSheets.length; | |
var styleSheet, mediaType; | |
selector = selector.toUpperCase(); | |
// Reverse iteration simplifies this algorithm tremendously, since the last enabled | |
// stylesheet is what we're getting. | |
for (var i = len; i >= 0; i--) { | |
var current = styleSheets[i]; | |
if (!current.disabled) { | |
var media = current.media; | |
mediaType = typeof media; | |
if (mediaType === 'string' && media === '' || media.indexOf('screen') !== -1) { | |
styleSheet = current; | |
} else if (mediaType === 'object') { | |
var mediaText = media.mediaText; | |
if (mediaText === '' || mediaText.indexOf('screen') !== -1) { | |
styleSheet = current; | |
} | |
} | |
if (typeof styleSheet === 'undefined') { | |
var elem = document.createElement('style'); | |
elem.type = 'text/css'; | |
insertNode(elem); | |
for (var i = len; i >= 0; i--) { | |
var current = styleSheets[i]; | |
if (!current.disabled) { | |
styleSheet = current; | |
break; | |
} | |
} | |
mediaType = typeof styleSheet.media; | |
break; | |
} | |
} | |
} | |
if (mediaType === 'string') { | |
if (!createCSSSelectorRules(styleSheet.rules, selector)) { | |
styleSheet.addRule(selector, style); | |
} | |
} else if (mediaType === 'object') { | |
var rules = styleSheet.cssRules; | |
if (!rules || !createCSSSelectorRules(rules, selector)) { | |
styleSheet.insertRule(selector + '{' + style + '}', rules ? rules.length : 0); | |
} | |
} | |
} | |
var prefixes = ['', '-moz-', '-webkit-', '-ms-', '-o-']; | |
var list = new Array(10); | |
for (var i = 0, j = 5, len = prefixes.length; i < len; i++, j++) { | |
var prefix = prefixes[i]; | |
rule('@' + prefix + 'keyframes nodeInserted', 'from{outline-color:#fff}to{outline-color:#000}'); | |
list[i] = prefix + 'animation-duration:0.01s'; | |
list[j] = prefix + 'animation-name:0.01s'; | |
} | |
var source = list.join(';'); | |
return function (mime, run, opts) { | |
function listener(event) { | |
if (event.animationName === 'nodeInserted') { | |
var node = event.target; | |
if (g.indexOf(node) === -1) { | |
run(node.text, opts); | |
} | |
} | |
} | |
addListener('animationstart', document, event); | |
addListener('MSAnimationStart', document, event); | |
addListener('webkitAnimationStart', document, event); | |
rule('script[type="' + mime.replace(/"/g, '\\"') + '"]', source); | |
}; | |
})(); | |
return function (mime, run, opts, compiles) { | |
if (opts != null) { | |
var oldRun = run; | |
run = function (code) { | |
oldRun(code); | |
}; | |
} | |
if (compiles) { | |
var compiler = run; | |
run = function (code, opts) { | |
var elem = document.createElement('script'); | |
elem.type = 'text/javascript'; | |
elem.text = compiler(code, opts); | |
insert(elem); | |
}; | |
} | |
observe(mime, run, opts); | |
}; | |
})(document); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment