feeles-ide
Version:
The hackable and serializable IDE to make learning material
186 lines (157 loc) • 3.94 kB
JavaScript
const a_href = false;
const img_src = false;
const audio_src = false;
const script_src = false;
const xhr_url = false;
// Hyper link
if (a_href) {
const hrefLoader = (node, href, set) => {
set(`javascript: feeles.replace('${href}');`);
};
interruptSetter(HTMLAnchorElement, 'href', hrefLoader);
}
// Image source
if (img_src) {
interruptSetter(HTMLImageElement, 'src', resourceLoader);
}
// Audio source
if (audio_src) {
interruptSetter(HTMLAudioElement, 'src', resourceLoader);
}
// Script source
if (script_src) {
interruptSetter(HTMLScriptElement, 'src', resourceLoader);
}
// XHR open()
if (xhr_url) {
interruptXHR(XMLHttpRequest);
}
/**
* @param node: HTMLElement
* @param src: String
* @param set: Function
*/
function resourceLoader(node, src, set) {
if (!isSameOrigin(src)) {
set(src);
return;
}
// If relative path:
feeles.fetch(getFeelesName(src))
.then(response => response.blob())
.then((blob) => {
const url = URL.createObjectURL(blob);
const revokeHandler = () => {
node.removeEventListener('load', revokeHandler);
node.removeEventListener('error', revokeHandler);
URL.revokeObjectURL(url);
};
node.addEventListener('load', revokeHandler);
node.addEventListener('error', revokeHandler);
set(url);
});
}
/**
* @param constructor: HTMLElement
* @param attr: String
* @param delegate: Function(
* node: HTMLElement,
* value: any,
* set: Function
* )
*/
function interruptSetter(constructor, attr, delegate) {
const proto = constructor.prototype;
const desc = Object.getOwnPropertyDescriptor(proto, attr);
Object.defineProperty(proto, attr, {
set: function(value) {
delegate(this, value, desc.set.bind(this));
}
});
}
/**
* @param constructor: XMLHttpRequest
* @param attr: String
* @param delegate: Function(
* node: HTMLElement,
* value: any,
* set: Function
* )
*/
function interruptXHR(constructor) {
const {
open,
send
} = constructor.prototype;
Object.defineProperty(constructor.prototype, 'open', {
value: interruptOpen,
});
function interruptOpen(_method, _url, _async = true, _user = '', _password = '') {
if (_async === false) {
throw new Error('feeles.XMLHttpRequest does not support synchronization requests.');
}
if (!isSameOrigin(_url)) {
open.call(this, _method, _url, _async, _user, _password);
return;
}
this.send = function(...sendArgs) {
feeles.fetch(getFeelesName(_url))
.then((response) => response.blob())
.then((blob) => {
const url = URL.createObjectURL(blob);
const revokeHandler = () => {
this.removeEventListener('load', revokeHandler);
this.removeEventListener('error', revokeHandler);
this.removeEventListener('abort', revokeHandler);
URL.revokeObjectURL(url);
};
this.addEventListener('load', revokeHandler);
this.addEventListener('error', revokeHandler);
this.addEventListener('abort', revokeHandler);
open.call(this, _method, url, _async, _user, _password);
send.apply(this, sendArgs);
});
};
}
}
const currentOrigin = getOrigin('');
const baseURL = (() => {
const a = document.createElement('a');
a.href = '';
if (!a.href) {
return '';
}
// If a.origin === "null" (e.g. Open in Blob URL), a.pathname doesn't work.
if (a.origin === "null") {
return 'http://fake.origin/';
}
const index = a.href.lastIndexOf('/');
return a.href.substr(0, index + 1);
})();
/**
* @param url: String
* @return String
*/
function getFeelesName(url) {
if (baseURL && typeof URL === 'function') {
const fullPath = new URL(url, baseURL).href;
return fullPath.substr(baseURL.length);
}
return url;
}
/**
* @param url: String
* @return Boolean
*/
function isSameOrigin(url) {
return getOrigin(url) === currentOrigin;
}
/**
* @param url: String
* @return String
*/
function getOrigin(url) {
const a = document.createElement('a');
a.href = url;
return a.origin;
}