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,'
-}
-
-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,'
+ }
- 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)