mirror of
https://github.com/arfct/itty-bitty.git
synced 2026-03-11 08:54:33 +00:00
New rendering mechanism
This commit is contained in:
parent
b0b1376d20
commit
25e5017203
5 changed files with 304 additions and 58 deletions
224
docs/bitty.js
224
docs/bitty.js
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue