diff --git a/docs/bitty.js b/docs/bitty.js index 11e81fb..c79349c 100644 --- a/docs/bitty.js +++ b/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, }; diff --git a/docs/index.html b/docs/index.html index ce6ed81..8134a78 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,6 +7,8 @@ + + diff --git a/docs/index.js b/docs/index.js index f87203a..62cb91d 100644 --- a/docs/index.js +++ b/docs/index.js @@ -1,8 +1,9 @@ import * as bitty from './bitty.js'; + window.bitty = bitty; + const HEAD_TAGS = () => btoa('\n'); const HEAD_TAGS_EXTENDED = () => btoa(` `); - // 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).
Larger sites may require a different browser.
Learn more'; } - 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 = `` - 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) { diff --git a/docs/render.js b/docs/render.js index 16d5bf5..18b5af1 100644 --- a/docs/render.js +++ b/docs/render.js @@ -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); diff --git a/docs/render/parse.js b/docs/render/parse.js index b13a633..48edd1f 100644 --- a/docs/render/parse.js +++ b/docs/render/parse.js @@ -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") -} \ No newline at end of file +} + + + +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}); + } +} +