Add extractor

This commit is contained in:
Nicholas Jitkoff 2022-01-29 11:40:35 -08:00
parent 6fb057cec6
commit 82ad60c7bb
6 changed files with 275 additions and 128 deletions

29
docs/extractrecipe.js Normal file
View file

@ -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);
})();
*/

View file

@ -5,6 +5,8 @@
<link id="favicon" rel=icon href=''>
<script src="/js/lzma/lzma-d-min.js"></script>
<script src="js/lzma/lzma_worker-min.js"></script>
<script src="/data.js"></script>
<script src="/index.js"></script>

View file

@ -1,109 +1,133 @@
var HEAD_TAGS = "PGJhc2UgdGFyZ2V0PSJfdG9wIj4K";
var HEAD_TAGS_EXTENDED =
"PG1ldGEgY2hhcnNldD0idXRmLTgiPjxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgiPjxiYXNlIHRhcmdldD0iX3RvcCI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj5ib2R5e21hcmdpbjowIGF1dG87cGFkZGluZzoxMnZtaW4gMTB2bWluO21heC13aWR0aDozNWVtO2xpbmUtaGVpZ2h0OjEuNWVtO2ZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCxzYW5zLXNlcmlmO3dvcmQtd3JhcDogYnJlYWstd29yZDt9PC9zdHlsZT4g";
(function () {
let dataRE = /^data:(?<type>[^;,]+)?(?<attrs>(?:;[^;,]+=[^;,]+)*)?(?:;(?<encoding>\w+64))?,(?<data>.*)$/
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:(?<type>[^;,]+)?(?<attrs>(?:;[^;,]+=[^;,]+)*)?(?:;(?<encoding>\w+64))?,(?<data>.*)$/
function setFavicon(favicon) {
document.getElementById("favicon").href = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><text y=".9em">'+ favicon + '</text></svg>'
}
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,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><text y=".9em">'+ favicon + '</text></svg>'
}
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 src="' + location.origin + '/render/recipe.js"></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).<br>Larger sites may require a different browser.<br><a href="http://reference.bitty.site">Learn more</a>';
}
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, `<script>var data = ${content}</script><script src="${script}"></script>`)
}
});
}
});
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();
}
};
})()

View file

@ -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;
}
}

View file

@ -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 = /(?<sign>-)?P(?:(?<years>[.,\d]+)Y)?(?:(?<months>[.,\d]+)M)?(?:(?<weeks>[.,\d]+)W)?(?:(?<days>[.,\d]+)D)?T(?:(?<hours>[.,\d]+)H)?(?:(?<minutes>[.,\d]+)M)?(?:(?<seconds>[.,\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
)
)
),
)
)

View file

@ -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)