diff --git a/docs/extractrecipe.js b/docs/extractrecipe.js new file mode 100644 index 0000000..8d32e50 --- /dev/null +++ b/docs/extractrecipe.js @@ -0,0 +1,29 @@ +javascript:((itty_bitty_recipes) => { +let l=document.querySelector('script[type="application/ld+json"]'); +if (!l) return alert("No Recipe element found in HTML") +let ldjson = l.innerText.trim(); +let json = JSON.parse(ldjson); +if (Array.isArray(json)) json = json.find((o)=>o['@type']=="Recipe") +console.log(json) +var blob = new Blob([JSON.stringify(json)],{type : 'application/ld+json;charset=utf-8'}); +var a = new FileReader(); +a.onload = function(e) { + let url = itty_bitty_recipes + '/#/' + e.target.result; + console.log("URL", url.length, url) + open(url); +}; +a.readAsDataURL(blob); +})('http://localhost') + + +/* +javascript:(function(){ + let id="ittybitty"; + if (document.getElementById(id)) return; + var l = document.createElement('script'); + l.setAttribute('type','text/javascript'); + l.setAttribute('src','http://localhost/extractrecipe.js'); + l.id=id; + document.head.appendChild(l); +})(); +*/ \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 767682e..f74c0e3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,6 +5,8 @@ + + diff --git a/docs/index.js b/docs/index.js index ff1ce3a..f5a5b8c 100644 --- a/docs/index.js +++ b/docs/index.js @@ -1,109 +1,133 @@ -var HEAD_TAGS = "PGJhc2UgdGFyZ2V0PSJfdG9wIj4K"; -var HEAD_TAGS_EXTENDED = - "PG1ldGEgY2hhcnNldD0idXRmLTgiPjxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgiPjxiYXNlIHRhcmdldD0iX3RvcCI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj5ib2R5e21hcmdpbjowIGF1dG87cGFkZGluZzoxMnZtaW4gMTB2bWluO21heC13aWR0aDozNWVtO2xpbmUtaGVpZ2h0OjEuNWVtO2ZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCxzYW5zLXNlcmlmO3dvcmQtd3JhcDogYnJlYWstd29yZDt9PC9zdHlsZT4g"; +(function () { -let dataRE = /^data:(?[^;,]+)?(?(?:;[^;,]+=[^;,]+)*)?(?:;(?\w+64))?,(?.*)$/ + const HEAD_TAGS = "PGJhc2UgdGFyZ2V0PSJfdG9wIj4K"; + const HEAD_TAGS_EXTENDED = "PG1ldGEgY2hhcnNldD0idXRmLTgiPjxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgiPjxiYXNlIHRhcmdldD0iX3RvcCI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj5ib2R5e21hcmdpbjowIGF1dG87cGFkZGluZzoxMnZtaW4gMTB2bWluO21heC13aWR0aDozNWVtO2xpbmUtaGVpZ2h0OjEuNWVtO2ZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCxzYW5zLXNlcmlmO3dvcmQtd3JhcDogYnJlYWstd29yZDt9PC9zdHlsZT4g"; -function dismiss() { - if (document.getElementById("never").checked) window.localStorage.setItem('toasted', true); - document.body.classList.remove("toasting") -} + let dataUrlRE = /^data:(?[^;,]+)?(?(?:;[^;,]+=[^;,]+)*)?(?:;(?\w+64))?,(?.*)$/ -function setFavicon(favicon) { - document.getElementById("favicon").href = 'data:image/svg+xml,'+ favicon + '' -} - -var validTypes = ["image/svg+xml", "application/ld+json"] -window.addEventListener("message", (e) => { - if (e.origin == 'null') { - if (e.data.title) document.title = e.data.title; - if (e.data.favicon) setFavicon(e.data.favicon); - } -}, false); - -window.onhashchange = window.onload = function() { - var hash = window.location.hash.substring(1); - if (window.location.search) { - console.log("window.location.search.substring(1)", window.location.search.substring(1)) - window.history.replaceState(null, null, window.location.search.substring(1) + "#" + hash) + function dismiss() { + if (document.getElementById("never").checked) window.localStorage.setItem('toasted', true); + document.body.classList.remove("toasting") } - if (hash.length < 3) { - location.href = "/edit"; - } else { - // if (!window.localStorage.getItem('toasted')) document.body.classList.add("toasting"); + function setFavicon(favicon) { + document.getElementById("favicon").href = 'data:image/svg+xml,'+ favicon + '' + } - var iframe = document.getElementById("iframe"); - var link = document.getElementById("edit"); - var preamble = undefined; - var renderMode = "data"; + const renderers = {"application/ld+json": "recipe"} - var slashIndex = hash.indexOf("/"); - var title = hash.substring(0, slashIndex); - document.title = title.length - ? decodeURIComponent(title.replace(/_/g, " ")) - : location.hostname; + window.addEventListener("message", function(e) { + console.log("Message:", e.origin, e.data) + // if (e.origin == 'null' || e.origin = ) { + if (e.data.title) document.title = e.data.title; + if (e.data.favicon) setFavicon(e.data.favicon); + 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); - hash = hash.substring(slashIndex + 1); - var editable = hash.charAt(0) == "?"; - if (editable) { - hash = hash.substring(1); + // window.onhashchange = + window.onload = function() { + if (window.location.search) { // Redirect search to path (coming out of server opengraph forwarding) + console.log("window.location.search.substring(1)", window.location.search.substring(1)) + window.history.replaceState(null, null, window.location.search.substring(1) + "#" + fragment) } - if (hash.indexOf("data:") != 0) { - var colon = hash.indexOf(":"); - if ( colon > 0 && colon < 15) { - document.body.classList.remove("toasting"); - return window.location.replace(hash); - } - var compressed = true; - preamble = HEAD_TAGS_EXTENDED; - hash = - "data:text/html;charset=utf-8;" + - (compressed ? "bxze64," : "base64,") + - hash; - } else if (hash.indexOf("data:text/html;") == 0) { - preamble = HEAD_TAGS; - } else if (hash.indexOf("data:text/plain;") == 0) { - preamble = HEAD_TAGS_EXTENDED; - } else if (hash.indexOf("data:text/") == 0) { - } else if (hash.indexOf("data:image/") == 0) { - } else { - let match = hash.match(dataRE); - let type = match?.groups.type; - if (type == "application/ld+json") { - let script = '' - script = script + " ".repeat(3 - (script.length % 3)) - preamble = btoa(script); - renderMode = "frame"; - } else if (!validTypes.includes(type)) { - console.log("unknown type, rendering as download") - let extension = title.split(".") - document.querySelector("#dl-name").innerText = title; - if (extension.length > 1) document.querySelector("#dl-image").innerText = extension.pop(); - renderMode = "download"; - } - } - link.onclick = function() { - location.href = "/edit" + location.hash; - }; var isIE = navigator.userAgent.match(/rv:11/); var isEdge = navigator.userAgent.match(/Edge\//); var isWatch = (window.outerWidth < 200); + + var fragment = window.location.hash.substring(1); + if (fragment.length < 3) { + return location.href = "/edit"; + } + + // if (!window.localStorage.getItem('toasted')) document.body.classList.add("toasting"); + + var iframe = document.getElementById("iframe"); + var link = document.getElementById("edit"); + var dataPrefix = undefined; + var renderMode = "data"; + + var slashIndex = fragment.indexOf("/"); + var title = fragment.substring(0, slashIndex); + document.title = title.length + ? decodeURIComponent(title.replace(/_/g, " ")) + : location.hostname; + + fragment = fragment.substring(slashIndex + 1); + var editable = fragment.charAt(0) == "?"; + if (editable) { + fragment = fragment.substring(1); + } + if (fragment.startsWith("data:")) { + let match = fragment.match(dataUrlRE); + let attrs = new URLSearchParams(match.groups.attrs?.substring(1).replace(/;/g, "&")); + + if (fragment.indexOf("data:text/html;") == 0) { + dataPrefix = HEAD_TAGS; + } else if (fragment.indexOf("data:text/plain;") == 0) { + dataPrefix = HEAD_TAGS_EXTENDED; + renderMode = "frame"; + } else if (fragment.indexOf("data:text/") == 0) { + } else if (fragment.indexOf("data:image/") == 0) { + } else { + let type = match?.groups.type; + const renderer = attrs.get("render") || renderers[type]; + if (renderer) { + var script = renderer; + if (script.indexOf("/") == -1) script = location.origin + '/render/' + script + '.js' + renderMode = "script"; + } else { + console.log("unknown type, rendering as download") + let extension = title.split(".") + document.querySelector("#dl-name").innerText = title; + if (extension.length > 1) document.querySelector("#dl-image").innerText = extension.pop(); + renderMode = "download"; + } + } + + if (match?.groups.encoding == "base64") { + compressDataURI(fragment, function(compressedFragment) { + console.log("Compressing long url", fragment.length, compressedFragment.length) + window.location.hash = window.location.hash.replace(fragment, compressedFragment); + }) + + } + } else { + var colon = fragment.indexOf(":"); + if ( colon > 0 && colon < 15) { + document.body.classList.remove("toasting"); + return window.location.replace(fragment); + } + var compressed = true; + dataPrefix = HEAD_TAGS_EXTENDED; + fragment = + "data:text/html;charset=utf-8;" + + (compressed ? "bxze64," : "base64,") + + fragment; + } + + link.onclick = function() { + location.href = "/edit" + location.hash; + }; + if ((isEdge || isIE) && location.href.length == 2083) { document.getElementById("warning").innerHTML = 'Edge only supports shorter URLs (maximum 2083 bytes).
Larger sites may require a different browser.
Learn more'; } - decompressDataURI(hash, preamble, function(dataURL) { + decompressDataURI(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 (renderMode == "script") if (isIE && renderMode == "data") renderMode = "frame"; + let contentTarget = iframe.contentWindow.document; if (isWatch) { - renderMode = "rewrite"; + contentTarget = document; } console.log("Rendering via", renderMode) - dataURL = dataURL.replace("application/ld+json", "text/plain"); + // dataURL = dataURL.replace("application/ld+json", "text/plain"); if (renderMode == "download") { try { let dl = document.querySelector("#download"); @@ -116,25 +140,26 @@ window.onhashchange = window.onload = function() { } } else if (renderMode == "data") { iframe.src = dataURL; - } else if (renderMode == "url") { - location.href = dataURL; - } else if (renderMode == "frame") { + } else { dataToString(dataURL, function(content) { - var doc = iframe.contentWindow.document; - doc.open(); - doc.write(content); - doc.close(); - }); - } else if (renderMode == "rewrite") { - dataToString(dataURL, function(content) { - document.open(); - document.write(content); - document.close(); + if (renderMode == "frame") { + writeDocContent(contentTarget, content) + } else if (renderMode == "script") { + writeDocContent(contentTarget, ``) + } }); } }); var link = document.getElementById("edit"); link.href = "/edit" + location.hash; link.style.display = editable ? "block" : "none"; + + }; + + function writeDocContent(doc, content) { + doc.open(); + doc.write(content); + doc.close(); } -}; + +})() \ No newline at end of file diff --git a/docs/render/recipe.css b/docs/render/recipe.css index 670317f..dc89a1e 100644 --- a/docs/render/recipe.css +++ b/docs/render/recipe.css @@ -1,5 +1,5 @@ * { - font-family: -apple-system, BlinkMacSystemFont, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; } body { @@ -12,12 +12,15 @@ body { background-color: gray; background-size: cover; background-position: center; + margin-bottom:2em; } img.publisher { - max-width: 12em; - float: right; - margin-top: -0.5em; + max-width: 5em; + /* margin-top: -0.5em; */ + /* transform:scale(0.7); */ + + float:right; } .recipe { @@ -37,10 +40,15 @@ img.publisher { header { gap: 2em; margin-bottom: 2em; +/* + padding-left:15em; */ } h1 { - margin-top: 2em; + margin-top: 0.3em; + margin-bottom:0.3em; + line-height:125%; + font-weight:500; } .metadata { @@ -77,6 +85,21 @@ h1 { line-height: 1.25em; opacity: 0.75; cursor: pointer; + padding-left:3em; +} + +.ingredients .yield { + margin-bottom:2em; + font-weight:bold; + text-transform: uppercase; + + margin-left:2em; +} +.ingredients .quantity { + float:left; + margin-left:-2.5em; + width:2em; + text-align:center; } .ingredient.complete, @@ -105,6 +128,13 @@ h1 { background-color: currentColor; opacity: 0.54; } +hr { + /* width: 100px; + float: left; + height: 0.5px; + background-color: currentColor; + opacity: 0.54; */ +} a.action { border: none; @@ -112,14 +142,14 @@ a.action { text-transform: uppercase; text-decoration: none; color: currentColor; + opacity:0.4; cursor: pointer; - border-radius: 1em; - line-height: 2em; - padding: 0 1em; } a.action:hover { - background-color: rgba(128, 128, 128, 0.1) + opacity:1.0; + + text-decoration: underline; } @media screen and (max-width: 480px) { @@ -134,6 +164,11 @@ a.action:hover { } .ingredient { margin: 0; + margin-left:2em; + margin-bottom:0.67em; + } + .ingredients .yield { + margin-left:3em; } } diff --git a/docs/render/recipe.js b/docs/render/recipe.js index 3c41ae4..2ad02ec 100644 --- a/docs/render/recipe.js +++ b/docs/render/recipe.js @@ -1,9 +1,11 @@ let script = document.currentScript; +console.log("data!", data); + FRACTION_MAP = { - '1/2': '\u00BD', '1/4': '\u00BC', + '1/2': '\u00BD', '3/4': '\u00BE', '1/3': '\u2153', '2/3': '\u2154', @@ -24,6 +26,16 @@ FRACTION_MAP = { } } +function getStringProperty(stringOrArray, prop) { + if (Array.isArray(stringOrArray)) { + stringOrArray = stringOrArray.pop(); + } + if (prop) { + stringOrArray = stringOrArray[prop] + } + return stringOrArray; +} + const m = (selector, ...args) => { var attrs = (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]) && !(args[0] instanceof HTMLElement)) ? args.shift() : {}; @@ -57,12 +69,23 @@ const m = (selector, ...args) => { return node; }; +function clean(html) { + let doc = new DOMParser().parseFromString(html, 'text/html'); + return doc.body.textContent || ""; +} + function formatTime(time) { const timeRE = /(?-)?P(?:(?[.,\d]+)Y)?(?:(?[.,\d]+)M)?(?:(?[.,\d]+)W)?(?:(?[.,\d]+)D)?T(?:(?[.,\d]+)H)?(?:(?[.,\d]+)M)?(?:(?[.,\d]+)S)?/ let duration = time.match(timeRE).groups; console.log("match", duration) if (duration) { time = []; + + if (duration.minutes > 60) { + duration.hours = Math.floor(duration.minutes / 60); + duration.minutes = duration.minutes % 60 + } + if (duration.hours > 0) time.push(duration.hours + "h"); if (duration.minutes > 0) time.push(duration.minutes + "m"); time = time.join(" "); @@ -83,12 +106,25 @@ function highlightStep(e) { } +const ingredientMatch = /^(?:A )?([\/0-9\u00BC-\u00BE\u2153-\u215E\u2009]+) ?(.*)/ +function ingredientEl(string, terms) { + //console.log("terms", terms); + + let match = FRACTION_MAP.replace(clean(string)).match(ingredientMatch); + //console.log("match",match ) + if (match) { + let els = match[2].split(/\s/); + els = els.map(s => terms?.has(s) ? m("i", s + " ") : s + " ") + return [m("span.quantity", match[1]), els] + } + if (string == "-") return m("hr"); + return [m("span.quantity", ""), (string)]; +} + function render() { - let data = document.body.innerHTML; - document.body.innerText = ""; delete document.documentElement.style.display; try { - var json = JSON.parse(data); + var json = data; //JSON.parse(data); if (json["@type"] != "Recipe") { json = (json["@graph"] ?? json).find((item) => item["@type"] == "Recipe") } @@ -100,15 +136,25 @@ function render() { if (!json) return; let image = json.image; if (Array.isArray(image)) image = image.shift(); - image = image.url || image; + image = image?.url || image; instructions = json.recipeInstructions; if (Array.isArray(instructions)) { if (Array.isArray(instructions[0])) instructions = instructions.flat() } else { instructions = [{ text: instructions }]; } + parent.postMessage({title:json.name, favicon:"🍳"}, "*"); +console.log("post message") + instructions = instructions.map(i => i.text || i); + let text = instructions.join(" "); + var terms = new Set(text.split(/\s/).filter(s => s.length > 2)); + instructions = instructions.map(i => m("div.step", { onclick: highlightStep }, i)) + + let rating = json.aggregateRating; + + document.body.appendChild( m("article.recipe", {}, image ? m(".thumbnail.noprint", { style: "background-image:url(" + image + ");" }) : null, @@ -118,30 +164,39 @@ function render() { m("h1", json.name), m(".metadata", // json.nutrition?.calories ? m("div", (json.nutrition?.calories) + (parseFloat(json.nutrition?.calories) != NaN ? " calories" : "")) : null, - json.totalTime ? m("div", formatTime(json.totalTime)) : undefined, - m("div", json.recipeYield), - json.author?.name ? m(".author", (json.author?.name)) : null, - (rating = json.aggregateRating) ? [ - parseFloat(rating.ratingValue).toFixed(1), - "\u2606".repeat(1), - rating.ratingCount ? " (" + rating.ratingCount + ")" : null - ].join(" ") : null, + // m(".rating", (json.aggregateRating?.ratingValue), (json.aggregateRating?.ratingCount)), - m("a.action.noprint", { onclick: () => window.print() }, "print"), - m("a.action.noprint", { href: json.mainEntityOfPage }, "link"), ), - m(".description", json.description), + m(".description", + clean(json.description), + json.author?.name ? m("span.author", (" —⁠" + json.author?.name)) : null, + + m("p"), + + (rating && rating.ratingCount != 0) ? [ + " • ", + "\u2606".repeat(rating.ratingValue), " ", + parseFloat(rating.ratingValue).toFixed(1), " ", + rating.ratingCount ? m("span.count", `(${rating.ratingCount.toString()})`) : null + ] : null, + " • ", m("a.action.noprint", { onclick: () => window.print() }, "print"), + " • ", m("a.action.noprint", { href: json.mainEntityOfPage || json.url, target:"_blank"}, "link"), + + ), ), m(".columns", m(".ingredients", - json.recipeIngredient?.map(i => m("div.ingredient", { onclick: markIngredient }, FRACTION_MAP.replace(i))) + m("div.yield", ingredientEl(getStringProperty(json.recipeYield))), + + json.recipeIngredient?.map(i => m("div.ingredient", { onclick: markIngredient }, ingredientEl(i, terms))) ), m(".instructions", - instructions.map(i => m("div.step", { onclick: highlightStep }, i.text)) + json.totalTime ? m("div", formatTime(json.totalTime)) : undefined, + instructions ) - ) + ), ) ) diff --git a/docs/v1/data.js b/docs/v1/data.js index 94fc4bb..844d6c5 100644 --- a/docs/v1/data.js +++ b/docs/v1/data.js @@ -31,10 +31,10 @@ function decompressDataURI(dataURI, preamble, callback) { var base64Index = dataURI.indexOf(LZMA64_MARKER); if (base64Index > 0) { var base64 = dataURI.substring(base64Index + LZMA64_MARKER.length); - zipToString(base64, function(result) { - stringToData(result, function(data) { - if (!data) return callback(undefined); - callback(dataURI.substring(0, base64Index) + BASE64_MARKER + (preamble || '') + data.split(',')[1]) + zipToString(base64, function(string) { + stringToData(string, function(data) { + if (!data) return callback(); + callback(dataURI.substring(0, base64Index) + BASE64_MARKER + (preamble || '') + data.split(',')[1], string) }) }) } else { @@ -43,6 +43,7 @@ function decompressDataURI(dataURI, preamble, callback) { } function zipToString(data, callback) { + let data = data.replace("-",""); var array = base64ToByteArray(data); LZMA.decompress(array, function(result, error) { if (!(typeof result === 'string')) result = new Uint8Array(result)