facta
Version:
Sudoku-like integer factoring game
145 lines (125 loc) • 4.74 kB
JavaScript
;
{
const XSS = "Possible XSS attack warning. Possible object forgery attempt detected. Codes do not match.";
const OBJ = "Object properties don't work here.";
const MARKER = hid => {detail: `Insertion point market not found for ${hid}`, hid};
const HID = hid => {detail: `Node or handlers not found for recorded hid ${hid}`, hid};
const LAST_ATTR_NAME = /\s+([\w-]+)\s*=\s*"?\s*$/;
const NEW_TAG = /<\w+/g;
const currentKey = Math.random()+'';
const VOID_ELEMENTS = new Set(["area","base","br","col","command","embed","hr","img","input","keygen",
"link","menuitem","meta","param","source","track","wbr"]);
Object.assign(self,{R,render});
function R (parts, ...vals) {
const handlers = {};
let hid, lastNewTagIndex, lastTagName, str = '';
parts = [...parts];
vals = vals.map(parseValue);
while (parts.length > 1) {
let part = parts.shift();
let attrNameMatches = part.match(LAST_ATTR_NAME);
let newTagMatches = part.match(NEW_TAG);
let val = vals.shift();
if (newTagMatches) {
if ( handlers[hid] ) str = markInsertionPoint({str,lastNewTagIndex,lastTagName,hid});
hid = `hid_${Math.random()}`.replace('.','');
const lastTag = newTagMatches[newTagMatches.length-1];
lastNewTagIndex = part.indexOf(lastTag) + str.length;
lastTagName = lastTag.slice(1);
}
if (typeof val === 'function') {
const attrName = attrNameMatches && attrNameMatches.length > 1
? attrNameMatches[1].replace(/^on/,'').toLowerCase()
: false,
newPart = part.replace(attrNameMatches[0], '');
str += attrName ? newPart : part;
if ( !Array.isArray(handlers[hid]) ) handlers[hid] = [];
handlers[hid].push({eventName: attrName, handler: val});
} else if (!!val && !!val.handlers && !!val.str) {
Object.assign(handlers,val.handlers);
str += part;
val = val.str;
if (attrNameMatches) val = `"${val}"`;
str += val;
} else {
str += part;
str += attrNameMatches ? `"${safe(val)}"` : safe(val);
}
}
if ( handlers[hid] ) str = markInsertionPoint({str,lastNewTagIndex,lastTagName,hid});
str += parts.shift();
const o = {str,handlers};
o.code = sign(o,currentKey);
return o;
}
function render (r, root, {replace: replace = false} = {}) {
r = parseR(r);
const {str,handlers} = r;
if (replace) {
root.insertAdjacentHTML('beforeBegin', str);
root.remove();
} else {
root.innerHTML = '';
root.insertAdjacentHTML('afterBegin', str);
}
Object.entries(handlers).forEach(
([hid,nodeHandlers]) => addHandlersToMarkedInsertionPoint({hid,nodeHandlers}));
}
function join (rs) {
const H = {},
str = rs.map(({str,handlers,code}) => (
verify({str,handlers,code},currentKey),Object.assign(H,handlers),str)).join('\n');
if (str) {
const o = {str,handlers:H};
o.code = sign(o,currentKey);
return o;
}
}
function parseValue(v) {
if (Array.isArray(v) && v.every(item => !!item.handlers && !!item.str)) {
return join(v) || '';
} else if (typeof v === 'object' && !!v) {
if (!!v.str && !!v.handlers) {
return verify(v,currentKey) && v;
}
throw {error: OBJ, value: v};
} else return v === null || v === undefined ? '' : v;
}
function parseR(r) {
if (Array.isArray(r) && r.every(val => !!val.str && !!val.handlers)) r = join(r);
verify(r,currentKey);
return r;
}
function markInsertionPoint({str,lastNewTagIndex,lastTagName,hid}) {
const before = str.slice(0,lastNewTagIndex);
const after = str.slice(lastNewTagIndex);
return before +
`<${lastTagName} id=${hid}>` +
(isVoid(lastTagName) ? '' : `</${lastTagName}>`) +
after;
}
function addHandlersToMarkedInsertionPoint({hid,nodeHandlers}) {
const hidNode = document.getElementById(hid);
let node;
if (!!hidNode) {
node = hidNode.nextElementSibling;
hidNode.remove();
} else throw {error: MARKER(hid)}
if (!!node && !!nodeHandlers) {
nodeHandlers.forEach(({eventName,handler}) => node.addEventListener(eventName,handler));
} else throw {error: HID(hid)}
}
function safe (v) {
return String(v).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''');
}
function sign ({str,handlers},key) {
return key;
}
function verify ({str,handlers,code},key) {
if (sign({str,handlers},key) === code) return true;
throw {error: XSS};
}
function isVoid(tag) {
return VOID_ELEMENTS.has(tag.toLowerCase().trim());
}
}