mirror of
https://github.com/arfct/itty-bitty.git
synced 2026-03-11 08:54:33 +00:00
Add recipe support
This commit is contained in:
parent
15829f5fa1
commit
a921dcd854
6 changed files with 269 additions and 11 deletions
|
|
@ -38,6 +38,7 @@ function decompressDataURI(dataURI, preamble, callback) {
|
|||
})
|
||||
})
|
||||
} else {
|
||||
if (preamble) dataURI = dataURI.replace(BASE64_MARKER, BASE64_MARKER + preamble)
|
||||
callback(dataURI)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
0
docs/index.src/data-min.js
vendored
0
docs/index.src/data-min.js
vendored
|
|
@ -2,12 +2,14 @@ var HEAD_TAGS = "PGJhc2UgdGFyZ2V0PSJfdG9wIj4K";
|
|||
var HEAD_TAGS_EXTENDED =
|
||||
"PG1ldGEgY2hhcnNldD0idXRmLTgiPjxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgiPjxiYXNlIHRhcmdldD0iX3RvcCI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj5ib2R5e21hcmdpbjowIGF1dG87cGFkZGluZzoxMnZtaW4gMTB2bWluO21heC13aWR0aDozNWVtO2xpbmUtaGVpZ2h0OjEuNWVtO2ZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCxzYW5zLXNlcmlmO3dvcmQtd3JhcDogYnJlYWstd29yZDt9PC9zdHlsZT4g";
|
||||
|
||||
let dataRE = /^data:(?<type>[^;,]+)?(?<attrs>(?:;[^;,]+=[^;,]+)*)?(?:;(?<encoding>\w+64))?,(?<data>.*)$/
|
||||
|
||||
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(`<script src="http://127.0.0.1:5000/render/recipe.js"></script> `);
|
||||
} 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
|
||||
|
|
|
|||
235
docs/render/recipe.js
Normal file
235
docs/render/recipe.js
Normal file
|
|
@ -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 = /(?<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.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();
|
||||
});
|
||||
20
recipe.py
Normal file
20
recipe.py
Normal file
|
|
@ -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)
|
||||
Loading…
Reference in a new issue