diff --git a/docs/data.js b/docs/data.js index 94fc4bb..1fc3320 100644 --- a/docs/data.js +++ b/docs/data.js @@ -38,6 +38,7 @@ function decompressDataURI(dataURI, preamble, callback) { }) }) } else { + if (preamble) dataURI = dataURI.replace(BASE64_MARKER, BASE64_MARKER + preamble) callback(dataURI) } } diff --git a/docs/index.html b/docs/index.html index a900249..16f22a0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,9 +1,8 @@ - + + + + diff --git a/docs/index.src/data-min.js b/docs/index.src/data-min.js deleted file mode 100644 index e69de29..0000000 diff --git a/docs/index.src/index.js b/docs/index.src/index.js index 48fbee7..b926a9c 100644 --- a/docs/index.src/index.js +++ b/docs/index.src/index.js @@ -2,12 +2,14 @@ var HEAD_TAGS = "PGJhc2UgdGFyZ2V0PSJfdG9wIj4K"; var HEAD_TAGS_EXTENDED = "PG1ldGEgY2hhcnNldD0idXRmLTgiPjxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgiPjxiYXNlIHRhcmdldD0iX3RvcCI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj5ib2R5e21hcmdpbjowIGF1dG87cGFkZGluZzoxMnZtaW4gMTB2bWluO21heC13aWR0aDozNWVtO2xpbmUtaGVpZ2h0OjEuNWVtO2ZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCxzYW5zLXNlcmlmO3dvcmQtd3JhcDogYnJlYWstd29yZDt9PC9zdHlsZT4g"; +let dataRE = /^data:(?[^;,]+)?(?(?:;[^;,]+=[^;,]+)*)?(?:;(?\w+64))?,(?.*)$/ + function dismiss() { if (document.getElementById("never").checked) window.localStorage.setItem('toasted', true); document.body.classList.remove("toasting") } -var validTypes = ["image/svg+xml"] +var validTypes = ["image/svg+xml", "application/ld+json"] window.onhashchange = window.onload = function() { var hash = window.location.hash.substring(1); @@ -49,11 +51,11 @@ window.onhashchange = window.onload = function() { } else if (hash.indexOf("data:text/plain;") == 0) { preamble = HEAD_TAGS_EXTENDED; } else { - let match = hash.match(/data:([^;]+)/) - let type = match[1]; - console.log("match", match, type) - - if (!validTypes.includes(type)) { + let match = hash.match(dataRE); + let type = match?.groups.type; + if (validTypes.includes(type)) { + preamble = btoa(` `); + } else { console.log("unknown type, rendering as download") let extension = title.split(".") document.querySelector("#dl-name").innerText = title; @@ -76,6 +78,7 @@ window.onhashchange = window.onload = function() { if (!dataURL) return; iframe.sandbox = "allow-downloads allow-scripts allow-forms allow-top-navigation allow-popups allow-modals allow-popups-to-escape-sandbox"; + dataURL = dataURL.replace("application/ld+json", "text/html"); if (download) { try { download.href = dataURL diff --git a/docs/render/recipe.js b/docs/render/recipe.js new file mode 100644 index 0000000..35574cd --- /dev/null +++ b/docs/render/recipe.js @@ -0,0 +1,235 @@ +FRACTION_MAP = { + '1/2': '\u00BD', '1/4': '\u00BC', '3/4': '\u00BE', '1/3': '\u2153', '2/3': '\u2154', '1/5': '\u2155', '2/5': '\u2156', '3/5': '\u2157', '4/5': '\u2158', '1/6': '\u2159', '5/6': '\u215A', '1/8': '\u215B', '3/8': '\u215C', '5/8': '\u215D', '7/8': '\u215E', + replace: function(string) { + return string.replace(/\d\/\d/g, function(a,b,c) { + return FRACTION_MAP[a]; + }) + } +} + +const m = (selector, ...args) => { + var attrs = (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]) && !(args[0] instanceof HTMLElement)) ? args.shift() : {}; + + let classes = selector.split("."); + if (classes.length > 0) selector = classes.shift(); + if (classes.length) attrs.className = classes.join(" ") + + let id = selector.split("#"); + if (id.length > 0) selector = id.shift(); + if (id.length) attrs.id = id[0]; + + var node = document.createElement(selector.length > 0 ? selector : "div"); + for (let prop in attrs) { + if (attrs.hasOwnProperty(prop) && attrs[prop] != undefined) { + if (prop.indexOf("data-") == 0) { + let dataProp = prop.substring(5).replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); + node.dataset[dataProp] = attrs[prop]; + } else { + node[prop] = attrs[prop]; + } + } + } + + const append = (child) => { + if (Array.isArray(child)) return child.forEach(append); + if (typeof child == "string") child = document.createTextNode(child); + if (child) node.appendChild(child); + }; + args.forEach(append); + + return node; +}; + +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.hours > 0 ) time.push(duration.hours + "h"); + if (duration.minutes > 0) time.push(duration.minutes + "m"); + time = time.join(" "); + } + return time; +} + +function markIngredient(e) { + e.target.classList.toggle("complete") +} +function highlightStep(e) { +// console.log("e", e.target) +// e.target.parent.children.forEach((i,el) => { +// el.classList.toggle("complete", ) +// } + e.target.classList.toggle("complete") + +} + +function render() { + let data = document.body.innerText; + document.body.innerText = ""; + delete document.documentElement.style.display; + let json = JSON.parse(data); + + if (json["@type"] != "Recipe") { + json = (json["@graph"] ?? json).find((item) => item["@type"] == "Recipe") + } + console.log("recipe", json) + + let image = json.image; + if (Array.isArray(image)) image = image.shift(); + image = image.url || image; + + document.body.appendChild( + m("article.recipe",{}, + m("img.publisher", {src:json.publisher?.image[0]?.url}), + m("img.thumbnail.noprint", {src:image}), + m("header", + m("h1", json.name), + m(".metadata", + json.nutrition?.calories ? m("div", (json.nutrition?.calories) + " calories") : null, + m("div", json.recipeYield), + m("div", formatTime(json.totalTime)), + json.author?.name ? m(".author", (json.author?.name)) : null, + "\u2605".repeat(json.aggregateRating?.ratingValue) + " " + parseFloat(json.aggregateRating?.ratingValue).toFixed(1) + " (" + json.aggregateRating?.ratingCount + ")", + // m(".rating", (json.aggregateRating?.ratingValue), (json.aggregateRating?.ratingCount)), + m("button.noprint", {onclick:() => window.print()},"print") + ), + m(".description", json.description), + + ), + m(".columns", + m(".ingredients", + json.recipeIngredient?.map(i => m("div.ingredient", {onclick:markIngredient}, FRACTION_MAP.replace(i))) + ), + m(".instructions", + json.recipeInstructions?.map(i => m("div.step", {onclick:highlightStep}, i.text)) + ) + ) + ) + ) + + document.head.appendChild(m("style", {}, ` + * { + font-family:-apple-system, BlinkMacSystemFont, sans-serif; + } + + img.thumbnail { + width:100%; + } + + img.publisher { + max-width: 12em; + margin-bottom:1em; + } + .recipe { + max-width:40em; + margin:auto; + line-height:133%; + } + + .columns { + display:flex; + gap:2em; + margin-bottom:4em; + align-content:center; + justify-content:center; + } + header { + gap:2em; + margin-bottom:2em; + } + h1 { + margin-top:2em; + + } + + .metadata { + align:right; + display:flex; + gap:1em; + margin-bottom:1em; + } + .metadata div:after { + content:"-"; + margin-left:1em; + } + .ingredients { + font-weight:600; + font-size:100%; + padding-top:1em; + flex:0 1 35%; + } + + .instructions { + white-space: pre-wrap; + line-height:125%; + font-size:100%; + flex: 0 1 65%; + } + + .ingredient { + padding: 0.625em 1em; + margin:0 -1em; + line-height:1.25em; + opacity:0.75; + cursor:pointer; + } + .ingredient.complete, + .step.complete { + text-decoration:line-through; + opacity:0.33; + } + .ingredient:hover { + opacity:1.0; + background-color:rgba(128,128,128,0.1) + } + + .step { + padding-top:2em; + max-width:40em; + + cursor:pointer; + } + .step:before { + content:""; + width:100px; + float:left; + margin-top:-1em; + height:0.5px; + background-color:currentColor; + opacity:0.54; + } + + button { + border:none; + background:none; + text-transform:uppercase; + } + + @media screen and (max-width: 480px) { + /* some CSS here */ + .columns { + flex-direction:column; + } + } + + @media (prefers-color-scheme: dark) { + body { + background-color:#222; + color:white; + } + } + @media print { + body { font-size:14px; } + .noprint { display:none; } + } + + + `)); + +} + +window.addEventListener('load', (event) => { + render(); +}); diff --git a/recipe.py b/recipe.py new file mode 100644 index 0000000..539c092 --- /dev/null +++ b/recipe.py @@ -0,0 +1,20 @@ +from recipe_scrapers import scrape_me +import json +import sys + +# give the url as a string, it can be url from any site listed below +scraper = scrape_me(sys.argv[1]) + +recipe = { + 'name': scraper.title(), + 'totalTime': scraper.total_time(), + 'recipeYield': scraper.yields(), + 'recipeIngredient': scraper.ingredients(), + 'recipeInstructions': [{'text': scraper.instructions()}], + 'image': [scraper.image()], + 'host': scraper.host(), + 'nutrition': scraper.nutrients() +} + +json_string = json.dumps(recipe) +print(json_string) \ No newline at end of file