New rendering mechanism

This commit is contained in:
Nicholas Jitkoff 2022-06-08 22:53:37 -07:00
parent b0b1376d20
commit 25e5017203
5 changed files with 304 additions and 58 deletions

View file

@ -16,7 +16,146 @@ const dataUrlRE =
// Fragment characters: A-Z a-z 0-9 + / =
// ? : @ - . _ ~ ! $ & ' ( ) * , ; and kinda (#)
class DataURL {
constructor(url) {
let match = url.match(dataUrlRE);
let info = match.groups;
Object.assign(this, info);
this.params = info.params ? JSON.parse('{"' + decodeURI(info.params?.substring(1)).replace(/"/g, '\\"').replace(/;/g, '","').replace(/=/g,'":"') + '"}') : {};
if (this.encoding) this.data = this.data.replace(/=/g,"");
}
get href() {
let urlString = "data:";
if (this.mediatype) urlString += this.mediatype
if (this.params) Object.entries(this.params).forEach( e => urlString += `;${e[0]}=${e[1]}`)
if (this.encoding) urlString += ";" + this.encoding
urlString += "," + (this.dataPrefix || '') + this.data;
return urlString;
}
get format() {
return this.params.format || this.encoding;
}
decompress = async () => {
// Decrypt if needed
if (this.params.cipher) {
console.log(this.params.cipher, decryptData)
this.data = await decryptData(this.params.cipher, this.data);
}
// Decompress if needed
if (this.format != "base64") {
let bytes = base64ToByteArray(this.data);
this.rawData = await decompressData(bytes, this.format)
this.data = await dataToBase64(this.rawData);
delete this.params.format
this.encoding = BASE64_MARKER
}
return this;
}
compress = async (format = LZMA_MARKER) => {
console.log("data", this.data.length, {data:this.data})
let rawData = await base64ToByteArray(this.data);
let compressedData = await compressData(rawData, format);
var base64String = dataToBase64(compressedData);
this.data = base64String;
this.params.format = format;
return this;
}
}
async function testCompression(rawData) {
let gz = await compressData(rawData, GZIP_MARKER);
console.log(gz.length, typeof gz, dataToBase64(gz).length);
let xz = await compressData(rawData, LZMA_MARKER);
console.log(xz.length, typeof xz, dataToBase64(xz).length);
let ungz = await decompressData(gz, GZIP_MARKER);
let unxz = await decompressData(xz, LZMA_MARKER);
console.log("unzip", ungz == rawData, unxz==rawData,{ungz, unxz,rawData,
raw: byteArrayToString(rawData).substring(684),
ungzs: byteArrayToString(ungz).substring(684),
unxzs: byteArrayToString(unxz).substring(684)
}, (byteArrayToString(ungz)) == byteArrayToString(unxz))
}
async function compressData(data, encoding = GZIP_MARKER, callback) {
console.log("compressing with", encoding)
if (encoding == GZIP_MARKER) {
return import("/js/gzip/pako.js").then((module) => {
console.log({gzdata:data})
return pako.deflate(data, {level:"9"});
});
} else if (encoding == BROT_MARKER) {
} else if (encoding == LZMA_MARKER) {
return new Promise(function(resolve, reject) {
console.log({xz:data})
LZMA.compress(data, 9, function(result, error) {
if (error) reject(error);
resolve(result);
});
});
}
}
function stringToByteArray(string) {
return new TextEncoder().encode(string);
return Uint8Array.from(string, c => c.charCodeAt(0));
}
function byteArrayToString(bytes) {
return new TextDecoder().decode(bytes);
return String.fromCharCode.apply(null, new Uint8Array(bytes));
}
async function decompressData(data, encoding, callback) {
if (encoding == GZIP_MARKER) {
return import("/js/gzip/pako.js").then((module) => {
let byteArray = pako.inflate(data);
return byteArray;
});
} else if (encoding == BROT_MARKER) {
return import("/js/brotli/decode.js").then((module) => {
return module.BrotliDecode(data);
});
} else if (encoding == LZMA_MARKER || encoding == LZMA64_MARKER) {
return new Promise(function(resolve, reject) {
LZMA.decompress(data, (result, error) => {
if (error) reject(error);
resolve(stringToByteArray(result));
});
});
}
}
async function decryptData(cipher, base64) {
return new Promise((resolve, reject) => {
loadScript("/js/crypto-js.min.js", () => {
console.log("decrypting", cipher)
let pass = prompt("This page is encrypted. What's the passcode?");
if (!pass) resolve(base64);
let decrypted = CryptoJS[cipher.toUpperCase()].decrypt(base64, pass);
return resolve(CryptoJS.enc.Base64.stringify(decrypted));
})
})
}
function infoForDataURL(url) {
return new DataURL(url);
let match = url.match(dataUrlRE);
let info = match.groups;
// info.params = new URLSearchParams(info?.groups.attrs?.substring(1).replace(/;/g, "&"));
@ -29,6 +168,11 @@ var LZMA64_MARKER = 'bxze64';
var GZIP64_MARKER = 'gzip64';
var BROT64_MARKER = 'brot64';
var BASE_MARKER = 'bs';
var LZMA_MARKER = 'xz';
var GZIP_MARKER = 'gz';
var BROT_MARKER = 'br';
function compressDataURL(dataURL, callback) {
var base64Index = dataURL.indexOf(BASE64_MARKER);
var base64 = dataURL.substring(base64Index + BASE64_MARKER.length + 1);
@ -38,15 +182,19 @@ function compressDataURL(dataURL, callback) {
}
function base64ToByteArray(base64) {
var raw = window.atob(base64);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for(let i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
// function base64ToByteArray(base64) {
// var raw = window.atob(base64);
// var rawLength = raw.length;
// var array = new Uint8Array(new ArrayBuffer(rawLength));
// for(let i = 0; i < rawLength; i++) {
// array[i] = raw.charCodeAt(i);
// }
// return array;
// }
function loadScript(src, callback) {
let script = el("script", { src });
script.addEventListener('load', function(e) {
@ -63,8 +211,6 @@ function escapeStringForIMessage(str) {
return str;
}
// import * as CryptoJS from "";
function decryptBase64(cipher, base64, callback) {
if (!cipher) return callback(base64);
@ -104,11 +250,13 @@ function decompressDataURL(dataURL, preamble, callback) {
function compressString(string, encoding = LZMA64_MARKER, callback) {
if (encoding == LZMA64_MARKER) {
LZMA.compress(string, 9, function(result, error) {
if (error) console.error(error);
var base64String = window.btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return callback(base64String);
});
loadScript("/js/lzma/lzma_worker-min.js", () => {
LZMA.compress(string, 9, function(result, error) {
if (error) console.error(error);
var base64String = window.btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return callback(base64String);
});
})
} else if (encoding == BROT64_MARKER) {
// import("/js/brotli/decode.js").then((module) => {
// console.log("module", module)
@ -143,6 +291,47 @@ function decompressString(data, encoding, callback) {
}
}
// echo -n 'hello world' | brotli | base64
// DwWAaGVsbG8gd29ybGQD
// echo -n 'hello world' | gzip -9 | base64
// H4sIAPfMmGICA8tIzcnJVyjPL8pJAQCFEUoNCwAAAA==
// echo -n 'hello world' | lzma -9 | base64
// XQAAAAT//////////wA0GUnujekXiTozYAX3z2T/+3ggAA==
function dataToBase64(data) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(data)));
}
function dataToBase64FR(data) {
return new Promise((resolve, reject) => {
if (!data || !data.length) return resolve("");
var fr = new FileReader();
fr.onload = () => resolve(fr.result.split(',')[1]);
fr.onerror = reject;
fr.readAsDataURL(new Blob([data], {encoding:"UTF-8",type:"text/html;charset=UTF-8"}));
})
}
function dataURLToString(durl) {
return fetch(durl)
.then(r => r.blob())
.then(blob => {
return new Promise((resolve, reject) => {
var fr = new FileReader();
fr.onload = () => resolve(fr.result)
fr.onerror = reject;
fr.readAsText(blob);
})
})
}
function stringToData(string, callback) {
if (!string.length) return callback("");
var a = new FileReader();
@ -151,7 +340,7 @@ function stringToData(string, callback) {
}
function dataToString(data, callback) {
newDataURLtoBlob(data).then(blob => {
return newDataURLtoBlob(data).then(blob => {
var reader = new FileReader();
reader.onload = function(e) { callback(reader.result) }
reader.readAsText(blob);
@ -178,6 +367,7 @@ function dataURLtoBlob(dataURL) {
export {
DataURL,
infoForDataURL,
stringToData,
dataToString,
@ -189,4 +379,8 @@ export {
LZMA64_MARKER,
GZIP64_MARKER,
BROT64_MARKER,
BASE_MARKER,
LZMA_MARKER,
GZIP_MARKER,
BROT_MARKER,
};

View file

@ -7,6 +7,8 @@
<link id="favicon" rel="icon" href='/favicon.svg'>
<link rel="stylesheet" href="/index.css">
<script src="/js/lzma/lzma-d-min.js"></script>
<script src="/js/lzma/lzma_worker-min.js"></script>
<script src="/index.js" type="module"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-WHNPBD4T42"></script>
<script nomodule> location.href = "/v1/" + location.hash </script>

View file

@ -1,8 +1,9 @@
import * as bitty from './bitty.js';
window.bitty = bitty;
const HEAD_TAGS = () => btoa('<base target="_top">\n');
const HEAD_TAGS_EXTENDED = () => btoa(`<meta charset="utf-8"><meta name="viewport" content="width=device-width"><base target="_top"><style type="text/css">body{margin:0 auto;padding:12vmin 10vmin;max-width:35em;line-height:1.5em;font-family:-apple-system,BlinkMacSystemFont,sans-serif;word-wrap:break-word;}@media(prefers-color-scheme: dark){body{color:white;background-color:#111;}}</style> `);
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker
@ -40,7 +41,7 @@
"text/rawhtml": {script:"parse"},
"javascript": {script:"bookmarklet"},
"web3": {script:"web3", mode:"frame"},
}
}
window.addEventListener("message", function(e) {
console.debug("Message:", e.origin, e.data)
@ -53,15 +54,27 @@
if (e.data.image) path.push("i/" + encodeURIComponent(btoa(e.data.image)));
window.location.pathname = path.join('/');
}
if (e.data.replaceURL) {
window.history.replaceState(null, null, e.data.replaceURL);
renderContent();
if (e.data.compressURL) {
let durl = new bitty.DataURL(e.data.replaceURL);
durl.compress().then(arg => {
window.history.replaceState(null, null, "/#/" + arg.href);
renderContent();
})
} else {
window.history.replaceState(null, null, e.data.replaceURL);
renderContent();
}
}
if (e.data.setStorage) document.localStorage.setItem(contentHash, e.data.set);
if (e.data.getStorage) document.getElementById("iframe").postMessage(document.localStorage.getItem(contentHash), e.origin)
}, false);
function renderContent() {
async function renderContent() {
var fragment = window.location.hash.substring(1);
if (window.location.search) { // Redirect search to path (coming out of server opengraph forwarding)
@ -138,14 +151,6 @@
renderMode = "script";
}
// if (info?.encoding == "base64" && !info.params?.encode) {
// bitty.compressDataURL(fragment, function(compressedFragment) {
// console.log("Compressing long url", fragment.length, compressedFragment.length)
// window.location.hash = window.location.hash.replace(fragment, compressedFragment);
// window.location.reload();
// console.log("Reloading")
// })
// }
} else {
var colon = fragment.indexOf(":");
if ( colon > 0 && colon < 15) {
@ -155,15 +160,15 @@
let renderer = renderers[scheme];
if (renderer) {
return renderContentWithScript(renderer.script, title, info, fragment, fragment);
return renderContentWithScript(false, renderer.script, title, info, fragment, fragment);
}
return window.location.replace(fragment);
}
var compressed = true;
dataPrefix = HEAD_TAGS_EXTENDED();
let encoding = !compressed ? "base64," : (fragment.startsWith("XQA") ? bitty.LZMA64_MARKER : bitty.GZIP64_MARKER);
fragment = "data:text/html;charset=utf-8;" + encoding + "," + fragment;
let encoding = !compressed ? "base64," : (fragment.startsWith("XQA") ? bitty.LZMA_MARKER : bitty.GZIP_MARKER);
fragment = "data:text/html;charset=utf-8;format=" + encoding + ";base64," + fragment;
}
@ -173,16 +178,25 @@
'Edge only supports shorter URLs (maximum 2083 bytes).<br>Larger sites may require a different browser.<br><a href="http://reference.bitty.site">Learn more</a>';
}
bitty.decompressDataURL(fragment, dataPrefix, function(dataURL, dataContent) {
let durl = new bitty.DataURL(fragment);
await durl.decompress();
durl.dataPrefix = dataPrefix;
let dataURL = durl.href;
let dataContent = durl.rawData;
// bitty.decompressDataURL(fragment, dataPrefix, function(dataURL, dataContent)
{
if (!dataURL) return;
iframe.sandbox = "allow-downloads allow-scripts allow-forms allow-top-navigation allow-popups allow-modals allow-popups-to-escape-sandbox";
if (isIE && renderMode == "data") renderMode = "frame";
let contentTarget// = iframe.contentWindow.document;
if (isWatch) {
console.log("Rendering for watch")
contentTarget = document;
}
console.log("Rendering mode: " + "\x1B[1m" + renderMode)
console.log("Rendering mode: " + "\x1B[1m" + renderMode, durl)
// dataURL = dataURL.replace("application/ld+json", "text/plain");
if (renderMode == "download") {
try {
@ -208,11 +222,11 @@
if (renderMode == "frame") {
writeDocContent(contentTarget, content)
} else if (renderMode == "script") {
renderContentWithScript(script, title, info, content, dataURL);
renderContentWithScript(contentTarget == document, script, title, info, content, dataURL);
}
});
}
});
}
let recordHistory = false
if (recordHistory) recordToHistory(title, type, description, window.location);
@ -222,16 +236,29 @@
window.addEventListener('load',renderContent);
// window.addEventListener('hashchange',renderContent);
const SCRIPT_LOADER = `<!doctype html><meta charset=utf-8><script src="${location.origin}/render.js"></script>`
function renderContentWithScript(script, title, info, body, url) {
function renderContentWithScript(overwrite, script, title, info, body, url) {
if (script.indexOf("/") == -1) script = location.origin + '/render/' + script + '.js'
iframe.onload = (() => {
iframe.contentWindow.postMessage({script, url, title, info, body}, "*");
delete iframe.onload
});
// writeDocContent(iframe.contentWindow.document, SCRIPT_LOADER)
// iframe.srcdoc = SCRIPT_LOADER;
iframe.src = "data:text/html," + SCRIPT_LOADER;
if (overwrite) {
let scriptEl = document.createElement("script")
scriptEl.src = "/render.js"
scriptEl.addEventListener('load', function(e) {
console.log("Loaded script", scriptEl.src);
renderScriptContent({script, url, title, info, body}, "*");
});
document.head.appendChild(scriptEl);
} else {
iframe.onload = (() => {
iframe.contentWindow.postMessage({script, url, title, info, body}, "*");
delete iframe.onload
});
// writeDocContent(iframe.contentWindow.document, SCRIPT_LOADER)
// iframe.srcdoc = SCRIPT_LOADER;
iframe.src = "data:text/html," + SCRIPT_LOADER;
}
}
function writeDocContent(doc, content) {

View file

@ -27,12 +27,15 @@ function loadSyle(href) {
document.head.appendChild(el("link", { type: "text/css", rel: "stylesheet", href}));
}
window.addEventListener("message", function(e) {
var base = el('base', {href: e.data.script});
function renderScriptContent(data, origin) {
var base = el('base', {href: data});
document.head.appendChild(base);
window.params = data;
window.params.origin = origin;
console.log("Rendering with", data.script, data)
loadScript(data.script);
}
window.params = e.data;
window.params.origin = e.origin;
console.log("Rendering with", e.data.script, e.data)
loadScript(e.data.script);
window.addEventListener("message", function(e) {
renderScriptContent(e.data, e.origin);
}, false);

View file

@ -2,21 +2,41 @@
parent.postMessage({title:"Parsing Content..."}, "*");
document.write("PARSING CONTENT…")
document.body.appendChild(document.createTextNode("• • •"))
const parser = new DOMParser();
const doc = parser.parseFromString(params.body, "text/html");
let ldjson = doc.querySelector('script[type="application/ld+json"]')
if (ldjson) {
ldjson = ldjson.innerText.trim();
let f = new FileReader();
f.onload = function(e) {
//top.location.href = ( '/#/' + e.target.result);
parent.postMessage({replaceURL:'/#/' + e.target.result}, "*");
};
f.readAsDataURL(new Blob([ldjson],{type : 'application/ld+json;charset=utf-8'}));
cleanRecipe(ldjson)
} else {
alert("No recipe found")
}
}
function cleanRecipe(ldjson) {
try {
var json = JSON.parse(ldjson.innerText); //JSON.parse(data);
if (json["@type"] != "Recipe") {
json = (json["@graph"] ?? json).find((item) => item["@type"] == "Recipe")
}
delete json.review;
delete json.video;
let f = new FileReader();
f.onload = function(e) {
parent.postMessage({replaceURL:e.target.result, compressURL:"true"}, "*");
};
f.readAsDataURL(new Blob([JSON.stringify(json)],{type : 'application/ld+json;charset=utf-8'}));
} catch (e) {
console.debug("Data", e, {ldjson});
}
}