This commit is contained in:
relikd
2019-11-06 01:08:23 +01:00
commit 57351a5e12
87 changed files with 1219 additions and 0 deletions

10
src/assets/static/col2.js Normal file
View File

@@ -0,0 +1,10 @@
(function(){// show at least 2 columns on mobile devices
var viewport = document.head.querySelector("meta[name=viewport]");
if (viewport && screen.width < 485) {
document.head.removeChild(viewport);
var x = document.createElement("meta");
x.setAttribute("name", "viewport");
x.setAttribute("content", "width=485");
document.head.appendChild(x);
}
})();

9
src/assets/static/lozad.min.js vendored Normal file
View File

@@ -0,0 +1,9 @@
/*! lozad.js - v1.9.0 - 2019-02-09
* https://github.com/ApoorvSaxena/lozad.js
* Copyright (c) 2019 Apoorv Saxena; Licensed MIT */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.lozad=e()}(this,function(){"use strict";var g=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var o in r)Object.prototype.hasOwnProperty.call(r,o)&&(t[o]=r[o])}return t},n="undefined"!=typeof document&&document.documentMode,l={rootMargin:"0px",threshold:0,load:function(t){if("picture"===t.nodeName.toLowerCase()){var e=document.createElement("img");n&&t.getAttribute("data-iesrc")&&(e.src=t.getAttribute("data-iesrc")),t.getAttribute("data-alt")&&(e.alt=t.getAttribute("data-alt")),t.appendChild(e)}if("video"===t.nodeName.toLowerCase()&&!t.getAttribute("data-src")&&t.children){for(var r=t.children,o=void 0,a=0;a<=r.length-1;a++)(o=r[a].getAttribute("data-src"))&&(r[a].src=o);t.load()}t.getAttribute("data-src")&&(t.src=t.getAttribute("data-src")),t.getAttribute("data-srcset")&&t.setAttribute("srcset",t.getAttribute("data-srcset")),t.getAttribute("data-background-image")&&(t.style.backgroundImage="url('"+t.getAttribute("data-background-image")+"')"),t.getAttribute("data-toggle-class")&&t.classList.toggle(t.getAttribute("data-toggle-class"))},loaded:function(){}};
/**
* Detect IE browser
* @const {boolean}
* @private
*/function f(t){t.setAttribute("data-loaded",!0)}var b=function(t){return"true"===t.getAttribute("data-loaded")};return function(){var r,o,a=0<arguments.length&&void 0!==arguments[0]?arguments[0]:".lozad",t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},e=g({},l,t),n=e.root,i=e.rootMargin,d=e.threshold,c=e.load,u=e.loaded,s=void 0;return window.IntersectionObserver&&(s=new IntersectionObserver((r=c,o=u,function(t,e){t.forEach(function(t){(0<t.intersectionRatio||t.isIntersecting)&&(e.unobserve(t.target),b(t.target)||(r(t.target),f(t.target),o(t.target)))})}),{root:n,rootMargin:i,threshold:d})),{observe:function(){for(var t=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:document;return t instanceof Element?[t]:t instanceof NodeList?t:e.querySelectorAll(t)}(a,n),e=0;e<t.length;e++)b(t[e])||(s?s.observe(t[e]):(c(t[e]),f(t[e]),u(t[e])))},triggerLoad:function(t){b(t)||(c(t),f(t),u(t))},observer:s}}});

153
src/assets/static/style.css Normal file
View File

@@ -0,0 +1,153 @@
:root { --cTxt: #222;
--cBg1: #FAF9F7; --cBg2: #EAE9E7; --cBg3: #9A9997;
--cRed1: #DC3A59; --cRed2: #AA203A; --cRed3: #EE6A84;
}
.center { text-align: center }
.small { font-size: 0.8em }
.large { font-size: 1.3em }
.xlarge { font-size: 1.6em }
.italic { font-style: italic }
.bold { font-weight: bold }
.dark-red { color: var(--cRed2) }
.light-red { color: var(--cRed3) }
.mrgTopMd { margin-top: 0.7em }
.v-scroll { overflow-x: scroll; white-space: nowrap; -webkit-overflow-scrolling: touch }
ul.no-bullets { padding: unset; margin: unset; list-style: none }
ul.li-lg-space li { padding-bottom: 0.3em }
/*
* General
*/
a { color: var(--cRed2); text-decoration: none }
a:hover { color: var(--cRed1) }
body {
font-family: 'Verdana', sans-serif;
background-color: var(--cBg3);
color: var(--cTxt);
margin: unset;
}
header #logo { font-size: 42px; display: block; margin-bottom: 15px }
header a { color: var(--cTxt) }
header, h1 { text-align: center }
header, footer, .page {
background: var(--cBg2);
max-width: 1060px;
margin: 0 auto;
padding: 20px 30px;
}
.page { background: var(--cBg1) }
nav ul { padding: unset }
nav ul li { display: inline-block; margin: 0.1em 0.5em }
nav ul li a.active { text-decoration: overline }
footer table { margin: -10px 0 }
@media(max-width: 485px) { body { font-size: 1.4em } }
@media print {
header, footer { display: none }
body, .page { background-color: #FFF }
}
/*
* Components
*/
.tags { display: flex; flex-wrap: wrap; justify-content: center }
.tags > * {
background-color: #FFF;
border: 1px solid var(--cRed1);
border-radius: 0.3em;
padding: 0.3em 0.5em;
margin: 0.2em;
}
.tags a:hover, .tags .active, a:hover .recipe-tile {
background-color: var(--cRed1);
color: #FFF;
}
header .tags { max-width: 600px; margin: 0 auto }
.cluster dt { margin-top: 0.7em; font-size: 1.6em }
.cluster dd { margin-top: 0.4em }
.cluster dd a { white-space: nowrap }
@media(max-width: 500px) {
.cluster dd { margin-left: 0 }
.cluster dd a { white-space: unset }
}
/*
* Grid overview
*/
.pagination { text-align: center; margin-top: 1em; }
.recipe-tile {
background-color: var(--cBg2);
color: var(--cTxt);
display: inline-block;
vertical-align: top;
margin: 6px;
width: 200px;
text-align: center;
}
.recipe-tile .img-placeholder {
background-color: #777;
color: var(--cBg2);
width: 200px;
height: 150px;
font: bold 25px/150px 'Courier New', monospace;
}
a:hover .recipe-tile img { mix-blend-mode: overlay }
.recipe-tile p { height: 2.6em; margin: 0.3em 10px; overflow-y: auto }
.tile-grid { width: fit-content; max-width: 1060px; margin: 0 auto }
.latest .tile-grid { max-width: 636px }
/* max-width = prev + 2*30; width = x * (200 + 2*6); */
@media screen and (max-width: 1120px) { .tile-grid { max-width: 848px } }
@media screen and (max-width: 908px) { .tile-grid { max-width: 636px } }
@media screen and (max-width: 696px) { .tile-grid { max-width: 424px !important } }
@media screen and (max-width: 484px) { .tile-grid { max-width: 212px !important } }
@media print and (orientation: portrait) { .tile-grid { width: 636px } }
@media print and (orientation: landscape) { .tile-grid { width: 1060px } }
@media print {
a:hover .recipe-tile img { mix-blend-mode: unset !important }
.recipe-tile, .recipe-tile .img-placeholder, a:hover .recipe-tile {
background-color: #FFF; color: #000 }
}
/*
* Individual recipe
*/
.recipe h2 { font-size: 0.8em; margin: 0 0 1em 0 }
.recipe #img-carousel { padding: 0 50px; margin: 0 -30px }
.recipe #source { margin-left: -1em; margin-bottom: -1.5em }
.recipe #metrics { float: right; margin: 0 0 15px 25px; max-width: 180px }
.recipe #metrics > * { text-indent: -20px; margin-left: 20px; padding-top: 0.3em }
.recipe #ingredients { float: left; margin: 0 25px 15px 0; max-width: 300px }
.recipe #directions ul { list-style-type: circle }
/* Colored, 3-part, difficulty bar */
.difficulty.easy > div:nth-child(1) { background-color: #3C3 }
.difficulty.medium > div:nth-child(1),
.difficulty.medium > div:nth-child(2) { background-color: #FC3 }
.difficulty.hard > div:nth-child(1),
.difficulty.hard > div:nth-child(2),
.difficulty.hard > div:nth-child(3) { background-color: #F30 }
.difficulty > * { vertical-align: middle; }
.difficulty > div:nth-child(1) { border-radius: 50% 0 0 50% }
.difficulty > div:nth-child(3) { border-radius: 0 50% 50% 0 }
.difficulty > div {
display: inline-block;
width: 1em; height: 1em;
border: 1px solid #555;
}
@media screen and (max-width: 800px) {
.recipe #img-carousel img { height: auto; width: 100%; padding: 0 }
.recipe #metrics { float: unset; max-width: fit-content; margin: 20px auto }
}
@media(max-width: 600px) { .recipe #ingredients { float: unset; max-width: 100% } }
@media(max-width: 500px) { .recipe #img-carousel { padding: 0 10px } }
@media print { #source, #rating, .difficulty { display: none } }
@media print and (orientation: landscape) { #img-carousel img { display: none } }
@media print and (orientation: portrait) {
#img-carousel img:not(:first-child) { display: none }
.recipe #metrics { float: unset; padding-bottom: 1em }
}

1
src/content/contents.lr Normal file
View File

@@ -0,0 +1 @@
_model: root

View File

@@ -0,0 +1,5 @@
_model: clusters
---
_template: querylist.html
---
_slug: recipes/by

View File

@@ -0,0 +1 @@
name: Aufwand

View File

@@ -0,0 +1 @@
name: Difficulty

View File

@@ -0,0 +1,9 @@
sort_key: 10
---
group_key: difficulty
---
xdata:
easy
medium
hard

View File

@@ -0,0 +1 @@
name: Zutaten

View File

@@ -0,0 +1 @@
name: Ingredients

View File

@@ -0,0 +1,3 @@
sort_key: 5
---
group_key: ingredients

View File

@@ -0,0 +1 @@
name: Bewertung

View File

@@ -0,0 +1 @@
name: Rating

View File

@@ -0,0 +1,5 @@
sort_key: 15
---
group_key: rating
---
reverse_order: yes

View File

@@ -0,0 +1 @@
name: Zeit

View File

@@ -0,0 +1 @@
name: Time

View File

@@ -0,0 +1,13 @@
sort_key: 20
---
group_key: time
---
xdata:
15
30
60
120
180
360
9999

View File

@@ -0,0 +1 @@
_model: recipes

View File

@@ -0,0 +1,16 @@
name: Vanille Ausstech-Kekse
---
yield: 26-28 Kekse
---
ingredients:
1/2 Tasse Margarine, oder Kokos Öl
1/2 Tasse Ahornsirup
1/2 TL Vanille Extrakt
1/4 TL Vanilleshote
1 Prise Salz
2 1/4 Tassen Mehl, glutenfrei
---
directions:
No translation yet. Click the flag (🇱🇷) at the bottom.

View File

@@ -0,0 +1,26 @@
name: Vanilla cut-out cookies
---
yield: 26-28 cookies
---
ingredients:
1/2 cup non-dairy butter, or coconut oil
1/2 cup maple syrup
1/2 tsp vanilla extract
1/4 tsp vanilla bean
dash salt
2 1/4 cups gluten-free flour blend
---
directions:
1) Preheat oven to 350°F. Line 2 cookie sheets with parchment paper. Prepare a rolling area with two additional sheets of parchment paper for that, and have your cookie cutter(s) handy.
2) Place butter in a large mixing bowl and whip it with a mixer until its creamy. Add sweetener, vanilla extract and bean, and salt and mix once again to combine. Add in flour and use a wooden spoon to mix. Then get in there with your hands and mix everything together by working the dough until you can shape it into a ball {note: as depending on the flour mix you use there may be a slight variance, know that the consistency of the dough should not be sticky but should press together when pinched — be sure to knead it really well first for some time — if its a little sticky, add a little more flour (try 1-2 tbsp); if its a little dry add a little more sweetener (try 1 tbsp)}. Shape the dough into 2 balls and then flatten each into a disk.
3) Roll out one of the dough balls between two sheets of parchment paper to ¼” thickness {or thinner or thicker depending on how you want your cookies to turn out}. Use a cookie cutter to cut out the cookies. Carefully transfer to a prepared cookie sheets, spacing them ½” apart {they wont spread as they bake}. Gather up any dough scraps and repeat until all dough is used up. Repeat the process with the second dough ball.
4) Bake in a pre-heated oven for approximately 11-13 minutes, until the edges just begin to become golden. Remove from oven and place on a cooling rack. {Note: cookies will harden a little within minutes of cooling, so dont overbake}. Allow the cookies to cool for 10 minutes and enjoy!
__Note:__
You can make your own glutenfree flour blend by combining:
1 cup brown rice flour, ¾ cup tapioca starch, ½ cup sweet rice flour, ½ tsp guar gum

View File

@@ -0,0 +1,11 @@
tags: cookies, sweet, xmas, glutenfree
---
time: 30
---
rating: 4
---
difficulty: easy
---
source: https://www.unconventionalbaker.com/recipes/gluten-free-vegan-vanilla-cut-out-cookies/
---
date: 2019-05-15

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1 @@
measures: EL, TL, kg, g, L, dl, cl, ml, cm, Msp, Prise, Tasse, Tassen, Dose, Dosen, kleine, Bund, Packung, Packungen, Scheibe, Scheiben, Schuss, Stängel, Tropfen, Tube

View File

@@ -0,0 +1 @@
measures: kg, g, L, dl, cl, ml, oz, lb, pt, qt, cm, tsp, tbsp, c, cup, cups, pkg, pck, drop, drops, tube, dash, dashes, ounce, ounces, small, medium, large, box, can, pinch, tin, clove, cloves

View File

@@ -0,0 +1,9 @@
_model: settings
---
_hidden: yes
---
replace_frac: yes
---
replace_temp: yes
---
show_empty_tags: no

View File

@@ -0,0 +1 @@
name: Brot

View File

@@ -0,0 +1 @@
name: Bread

View File

@@ -0,0 +1 @@
name: Kuchen

View File

@@ -0,0 +1 @@
name: Cake

View File

@@ -0,0 +1 @@
name: Schokolade

View File

@@ -0,0 +1 @@
name: Chocolate

View File

@@ -0,0 +1,5 @@
_model: tags
---
_slug: recipes/tags
---
_template: querylist.html

View File

@@ -0,0 +1 @@
name: Kekse

View File

@@ -0,0 +1 @@
name: Cookies

View File

@@ -0,0 +1 @@
name: Dip

View File

@@ -0,0 +1 @@
name: Dressing

View File

@@ -0,0 +1 @@
name: Drinks

View File

@@ -0,0 +1 @@
name: Glutenfrei

View File

@@ -0,0 +1 @@
name: Glutenfree

View File

@@ -0,0 +1 @@
name: Zutat

View File

@@ -0,0 +1 @@
name: Ingredient

View File

@@ -0,0 +1 @@
name: Hauptspeise

View File

@@ -0,0 +1 @@
name: Main dish

View File

@@ -0,0 +1 @@
name: Raw

View File

@@ -0,0 +1 @@
name: Salat

View File

@@ -0,0 +1 @@
name: Salad

View File

@@ -0,0 +1 @@
name: Soße

View File

@@ -0,0 +1 @@
name: Sauce

View File

@@ -0,0 +1 @@
name: Aufstrich

View File

@@ -0,0 +1 @@
name: Spread

View File

@@ -0,0 +1 @@
name: Süßes

View File

@@ -0,0 +1 @@
name: Sweet

View File

@@ -0,0 +1 @@
name: Weihnachten

View File

@@ -0,0 +1 @@
name: Xmas

25
src/databags/i18n+de.ini Normal file
View File

@@ -0,0 +1,25 @@
[duration]
label = Zeit
day = Tag
days = Tage
hour = Std
hours = Std
min = Min
mins = Min
[yield]
label = Menge
[difficulty]
label = Aufwand
_unset = Aufwand unklar
easy = Einfach
medium = Mittel
hard = Schwer
[title]
latest = Zuletzt hinzugefügt
all_recipes = Alle Rezepte
recipes = Rezepte
ingredients = Zutaten
directions = Zubereitung

25
src/databags/i18n+en.ini Normal file
View File

@@ -0,0 +1,25 @@
[duration]
label = Time
day = day
days = days
hour = hour
hours = hours
min = minutes
mins = min
[yield]
label = Yield
[difficulty]
label = Difficulty
_unset = Difficulty not set
easy = Easy
medium = Medium
hard = Hard
[title]
latest = Latest recipes
all_recipes = All recipes
recipes = Recipes
ingredients = Ingredients
directions = Directions

44
src/models/cluster.ini Normal file
View File

@@ -0,0 +1,44 @@
[model]
name = Cluster
label = {{ this.name }}
hidden = yes
protected = yes
[children]
enabled = no
[attachments]
enabled = no
[fields.name]
label = Name
width = 1/2
type = string
[fields.group_key]
label = Grouping Attribute
width = 1/3
type = string
[fields.sort_key]
label = Sort order
width = 1/5
default = 0
type = sort_key
[fields.null_fallback]
label = Null Fallback
width = 1/3
type = string
default = ???
[fields.reverse_order]
label = Reverse Sort
width = 1/5
type = boolean
[fields.xdata]
label = Extended Data
description = Used for ordinal sort order or merging integer cluster
width = 1/2
type = strings

12
src/models/clusters.ini Normal file
View File

@@ -0,0 +1,12 @@
[model]
name = Cluster
label = Cluster
hidden = yes
protected = yes
[children]
model = cluster
order_by = sort_key
[attachments]
enabled = no

67
src/models/recipe.ini Normal file
View File

@@ -0,0 +1,67 @@
[model]
name = Recipe
label = {{ this._id }}
hidden = yes
[children]
enabled = no
[fields.name]
label = Name
width = 2/3
type = string
size = large
[fields.date]
label = Date / Datum
width = 1/3
type = date
size = large
[fields.time]
label = Time / Zeit
width = 1/8
type = select
choices = 5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120, 150, 180, 240, 360, 480, 720, 1440
choice_labels = 5m, 10m, 15m, 20m, 30m, 45m, 1h, 1h 15m, 1h 30m, 1h 45m, 2h, 2h 30m, 3h, 4h, 6h, 8h, 12h, 24h
[fields.difficulty]
label = Difficulty
width = 1/8
type = select
choices = easy, medium, hard
choice_labels = Easy, Medium, Hard
[fields.rating]
label = Rating
width = 1/8
type = select
choices = 1, 2, 3, 4, 5
choice_labels = ★☆☆☆☆, ★★☆☆☆, ★★★☆☆, ★★★★☆, ★★★★★
[fields.yield]
label = Yield / Menge
width = 1/2
type = string
[fields.ingredients]
label = Ingredients / Zutaten
description = 42 g Ingredient, Notes (add additional measures in settings)
width = 2/3
type = strings
[fields.tags]
label = Tags / Kategorie
width = 1/3
type = checkboxes
source = site.query('/tags', alt)
[fields.directions]
label = Directions / Zubereitung
description = Markdown formatting applies: ### Header, __bold__, _italic_
type = markdown
[fields.source]
label = Source / Quelle
type = url
size = small

16
src/models/recipes.ini Normal file
View File

@@ -0,0 +1,16 @@
[model]
name = Recipes
label = Recipes
hidden = yes
protected = yes
[attachments]
enabled = no
[children]
model = recipe
order_by = name
[pagination]
enabled = yes
per_page = 60

6
src/models/root.ini Normal file
View File

@@ -0,0 +1,6 @@
[model]
name = Root Page
label = root
[attachments]
enabled = no

34
src/models/settings.ini Normal file
View File

@@ -0,0 +1,34 @@
[model]
name = Settings
label = Settings
hidden = yes
protected = yes
[children]
enabled = no
[attachments]
enabled = no
[fields.measures]
label = Measures
description = Comma separated list
width = 3/5
type = text
default = kg, g, L, dl, cl, ml, oz, lb, pt, qt, cm, tsp, tbsp, c, cup, cups, pkg, pck
[fields.replace_frac]
label = Replace 1/2 with ½, ⅔, et.c
width = 1/5
type = boolean
[fields.replace_temp]
label = Replace °C/°F with ℃/℉
width = 1/5
type = boolean
[fields.show_empty_tags]
label = Show empty tags
description = Even if no recipes exist in that category
width = 1/4
type = boolean

18
src/models/tag.ini Normal file
View File

@@ -0,0 +1,18 @@
[model]
name = Tag
label = {{ this.name }}
hidden = yes
[attachments]
enabled = no
[children]
replaced_with = site.query('/recipes', alt).filter(F.tags.contains(this))
[pagination]
enabled = yes
per_page = 60
[fields.name]
label = Name
type = string

12
src/models/tags.ini Normal file
View File

@@ -0,0 +1,12 @@
[model]
name = Tags
label = Tags
hidden = yes
protected = yes
[children]
model = tag
order_by = name
[attachments]
enabled = no

5
src/packages/helper/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
dist
build
*.pyc
*.pyo
*.egg-info

View File

@@ -0,0 +1,3 @@
# Helper
Just some python functions that are necessary for the project.

View File

@@ -0,0 +1,251 @@
# -*- coding: utf-8 -*-
from lektor.pluginsystem import Plugin
from lektor.databags import Databags
import unicodedata
import os
import shutil
# -------
# Sorting
def sortKeyInt(x):
return int(x[0]) if x[0] else 0
def sortKeyStr(x):
return noUmlaut(x[0]).lower()
def groupByDictSort(dic, sorter=None, reverse=False):
if type(sorter) == list: # sort by pre-defined, ordered list
return sorted(dic, reverse=bool(reverse), key=lambda x:
sorter.index(x[0]) if x[0] in sorter else 0)
fn = sortKeyInt if sorter == 'int' else sortKeyStr
return sorted(dic, reverse=bool(reverse), key=fn)
# -----------------------
# Pure text manupulations
def noUmlaut(text):
try:
text = unicode(text, 'utf-8')
except (TypeError, NameError):
pass
text = unicodedata.normalize('NFD', text)
text = text.encode('ascii', 'ignore')
text = text.decode("utf-8")
return str(text)
def pluralize(n, single, multi):
if n == 0:
return ''
return u'{} {}'.format(n, single if n == 1 else multi)
def replaceFractions(txt):
res = ''
for x in txt.split():
try:
i = ['1/2', '1/3', '2/3', '1/4', '3/4', '1/8', '-'].index(x)
res += [u'½', u'', u'', u'¼', u'¾', u'', u' - '][i]
except ValueError:
res += ' ' + x
return res.lstrip()
def numFillWithText(num, fill=u'', empty=u'', total=5):
num = int(num) if num else 0
return fill * num + empty * (total - num)
# ------------------
# Array manipulation
def updateSet_if(dic, parent, parentkey, value):
try:
key = parent[parentkey]
except KeyError:
return
if not key:
key = ''
try:
dic[key]
except KeyError:
dic[key] = set()
dic[key].add(value)
def updateSet_addMultiple(dic, key, others):
try:
dic[key]
except KeyError:
dic[key] = set()
dic[key].update(others)
def findCluster(key, clusterList=[30, 60, 120]):
key = int(key) if key else 0
if key > 0:
for cluster in clusterList:
if key < cluster:
key = cluster
break
return key
# --------------------
# Ingredient splitting
def splitIngredientLine(line):
state = 1
capture = False
indices = [0, len(line)]
for i, char in enumerate(line):
if char.isspace():
capture = False
indices[state] = i
state += 1
continue
elif capture:
continue
elif state == 1 and char in '0123456789-.,':
state -= 1
elif state > 1:
break
capture = True
return indices
def parseIngredientLine(line, measureList=[], rep_frac=False):
idx = splitIngredientLine(line)
val = line[:idx[0]]
if rep_frac:
val = replaceFractions(val)
measure = line[idx[0]:idx[1]].lstrip()
if measure.lower() in measureList:
name = line[idx[1]:].lstrip()
else:
measure = ''
name = line[idx[0]:].lstrip()
note = ''
name_note = name.split(',', 1)
if len(name_note) > 1:
name, note = [x.strip() for x in name_note]
return {'value': val, 'measure': measure, 'name': name, 'note': note}
# --------------------
# Other Helper methods
def groupByMergeCluster(dic, arr=[30, 60, 120], reverse=False):
arr = sorted([int(x) for x in arr])
groups = dict()
for key, recipes in dic:
key = findCluster(key, arr)
if key == 0 and not reverse:
key = ''
updateSet_addMultiple(groups, key, recipes)
return sorted(groups.items(), reverse=bool(reverse))
# ----------------
# Main entry point
class HelperPlugin(Plugin):
name = u'Helper'
description = u'Some helper methods, filters, and templates.'
alt = None
availableTags = set()
# -----------
# Event hooks
# -----------
def on_before_build_all(self, builder, **extra):
# display only tags that contain at least one recipe
pad = self.env.new_pad()
for r in pad.query('recipes'):
self.availableTags.update(r['tags'])
def on_after_prune(self, builder, **extra):
# redirect to /en/
for file in ['index.html']:
src_f = os.path.join(self.env.root_path, 'root', file)
if os.path.exists(src_f):
dst_f = os.path.join(builder.destination_path, file)
with open(dst_f, 'wb') as df:
with open(src_f, 'rb') as sf:
shutil.copyfileobj(sf, df)
def on_process_template_context(self, context, **extra):
self.alt = context['alt']
def on_setup_env(self, **extra):
# self.env.load_config().iter_alternatives()
# pad = self.env.new_pad()
# pad.query('groupby', alt=alt)
def localizeDic(key, subkey=None):
bag = Databags(self.env).lookup('i18n+{}.{}'.format(self.alt, key))
return bag[subkey] if subkey else bag
def to_duration(time, cluster=None):
time = int(time) if time else 0
if (time <= 0):
return ''
# Calls itself without cluster argument
if cluster:
cluster = [int(x) for x in cluster]
idx = cluster.index(time)
if idx == 0:
return '<' + to_duration(time)
timeA = to_duration(cluster[idx - 1])
if idx + 1 >= len(cluster):
return '>' + timeA
else:
return u'{} {}'.format(timeA, to_duration(time))
days = time // (60 * 24)
time -= days * (60 * 24)
L = localizeDic('duration')
return ' '.join([
pluralize(days, L['day'], L['days']),
pluralize(time // 60, L['hour'], L['hours']),
pluralize(time % 60, L['min'], L['mins'])]).strip()
def ingredientsForRecipe(recipe):
set = self.env.new_pad().get('settings', alt=self.alt)
meaList = [x.strip() for x in set['measures'].lower().split(',')]
repFrac = set['replace_frac']
for line in recipe['ingredients']:
line = line.strip()
if not line:
continue
elif line.endswith(':'):
yield {'group': line}
else:
yield parseIngredientLine(line, meaList, repFrac)
def groupByAttribute(recipeList, attribute):
groups = dict()
for recipe in recipeList:
if attribute == 'ingredients':
for ing in ingredientsForRecipe(recipe):
updateSet_if(groups, ing, 'name', recipe)
else:
updateSet_if(groups, recipe, attribute, recipe)
# groups[undefinedKey].update(groups.pop('_undefined'))
return groups.items()
self.env.jinja_env.filters['duration'] = to_duration
self.env.jinja_env.filters['rating'] = numFillWithText
self.env.jinja_env.filters['replaceFractions'] = replaceFractions
self.env.jinja_env.filters['enumIngredients'] = ingredientsForRecipe
self.env.jinja_env.filters['groupByAttribute'] = groupByAttribute
self.env.jinja_env.filters['groupSort'] = groupByDictSort
self.env.jinja_env.filters['groupMergeCluster'] = groupByMergeCluster
self.env.jinja_env.globals['localize'] = localizeDic
self.env.jinja_env.globals['availableTags'] = self.availableTags

View File

@@ -0,0 +1,38 @@
import ast
import io
import re
from setuptools import setup, find_packages
with io.open('README.md', 'rt', encoding="utf8") as f:
readme = f.read()
_description_re = re.compile(r'description\s+=\s+(?P<description>.*)')
with open('lektor_helper.py', 'rb') as f:
description = str(ast.literal_eval(_description_re.search(
f.read().decode('utf-8')).group(1)))
setup(
author=u'relikd',
author_email='oleg@relikd.de',
description=description,
keywords='Lektor plugin',
license='MIT',
long_description=readme,
long_description_content_type='text/markdown',
name='lektor-helper',
packages=find_packages(),
py_modules=['lektor_helper'],
# url='[link to your repository]',
version='0.1',
classifiers=[
'Framework :: Lektor',
'Environment :: Plugins',
],
entry_points={
'lektor.plugins': [
'helper = lektor_helper:HelperPlugin',
]
}
)

14
src/recipes.lektorproject Normal file
View File

@@ -0,0 +1,14 @@
[project]
name = recipe lekture
url_style = relative
[alternatives.en]
name = English
primary = yes
url_prefix = /en/
locale = en_US
[alternatives.de]
name = German
url_prefix = /de/
locale = de_DE

1
src/root/index.html Normal file
View File

@@ -0,0 +1 @@
<meta http-equiv="refresh" content="0; URL='en/'" />

View File

@@ -0,0 +1,39 @@
{% extends "layout.html" %}
{% block title %}{{ this.name }}{% endblock %}
{% block body %}
{%- if this.group_key in ['rating', 'time'] -%}
{%- set sortType = 'int' -%}
{%- elif this.xdata -%}
{%- set sortType = this.xdata + [''] -%}
{%- endif -%}
{%- set all = site.query('/recipes', this.alt) | groupByAttribute(this.group_key) | groupSort(sortType, this.reverse_order) -%}
{%- if this.group_key == 'time' -%}
{%- set all = all | groupMergeCluster(this.xdata, this.reverse_order) -%}
{%- endif -%}
<h1>{{ this.name }}</h1>
<dl class="cluster">
{%- for attrib, recipes in all -%}
<dt>{%- if this.group_key == 'rating' -%}
{{ attrib | rating }}
{%- elif not attrib -%}
{{ this.null_fallback }}
{%- elif this.group_key == 'time' -%}
{{ attrib | duration(this.xdata) }}
{%- elif this.group_key == 'difficulty' -%}
{{ localize('difficulty', attrib) }}
{%- else -%}
{{ attrib }}
{%- endif -%}</dt>
<dd>
{%- set pipe = joiner(' | ') -%}
{%- for recipe in recipes | sort(attribute='name') -%}
{{ pipe() }}<a href="{{ recipe|url }}">{{ recipe.name }}</a>
{%- endfor -%}
</dd>
{%- endfor %}
</dl>
{% endblock %}

48
src/templates/layout.html Normal file
View File

@@ -0,0 +1,48 @@
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=0.75">
<script type="text/javascript" src="{{ '/static/col2.js'|url }}"></script>
<script type="text/javascript" src="{{ '/static/lozad.min.js'|url }}"></script>
<link rel="stylesheet" href="{{ '/static/style.css'|url }}">
<title>{% block title %}Welcome{% endblock %} · recipe lekture</title>
<body>
<header>
<a id="logo" href="{{ site.get('/', alt=this.alt)|url }}">recipe lekture</a>
<nav>
<ul>
{%- set allRecipes = site.get('recipes', this.alt) %}
<li><a {% if this == allRecipes %}class="active"{% endif %} href="{{ allRecipes|url }}">{{ localize('title.all_recipes') }}</a></li>
{%- for navpage in site.query('/groupby', this.alt) %}
<li><a {% if this.is_child_of(navpage) %}class="active"{% endif %} href="{{ navpage|url }}">{{ navpage.name }}</a></li>
{%- endfor %}
</ul>
</nav>
<div class="tags small">
{%- set allowEmptyTags = site.get('settings', alt=this.alt)['show_empty_tags'] -%}
{%- for tag in site.query('/tags', this.alt) %}
{%- if allowEmptyTags or tag._id in availableTags -%}
<a {%
if this.is_child_of(tag) or (this.tags and tag._id in this.tags) -%}
class="active"
{%- endif %} href="{{ tag|url }}">{{ tag.name }}</a>
{%- endif -%}
{%- endfor %}
</div>
</header>
<div class="page">
{% block body %}{% endblock %}
</div>
<footer>{#--#}
<table width="100%">{#--#}
<td>Build with <a href="https://www.getlektor.com/">Lektor</a>, template by <a href="https://github.com/relikd/lektor-recipes">relikd</a>.</td>{#--#}
<td class="xlarge" width="1em">
{%- if this.alt == 'de' -%}
<a href="{{ '.'|url(alt='en') }}" title="zur englischen Seite wechseln">🇱🇷</a>
{%- else -%}
<a href="{{ '.'|url(alt='de') }}" title="switch to german page">🇩🇪</a>
{%- endif -%}
</td>{#--#}
</table>{#--#}
</footer>
<script type="text/javascript">const observer = lozad(); observer.observe();</script>
</body>

View File

@@ -0,0 +1,33 @@
{%- macro render_pagination_all(pagination) -%}
{%- if pagination.pages > 1 -%}
<div class=pagination>
{%- for page in pagination.iter_pages() %}
{% if page -%}
{%- if page != pagination.page -%}
<a href="{{ pagination.for_page(page)|url }}">{{ page }}</a>
{%- else -%}
<strong>{{ page }}</strong>
{%- endif -%}
{%- else -%}
<span class=ellipsis>...</span>
{%- endif -%}
{%- endfor %}
</div>
{%- endif -%}
{%- endmacro -%}
{% macro render_pagination_prev_next(pagination) %}
<div class="pagination">
{% if pagination.has_prev %}
<a href="{{ pagination.prev|url }}">&laquo; Previous</a>
{% else %}
<span class="disabled">&laquo; Previous</span>
{% endif %}
| {{ pagination.page }} |
{% if pagination.has_next %}
<a href="{{ pagination.next|url }}">Next &raquo;</a>
{% else %}
<span class="disabled">Next &raquo;</span>
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,21 @@
{%- macro render_recipe_list(recipes, limit=0) -%}
<div class="tile-grid">
{%- for recipe in recipes -%}
{%- if limit == 0 or loop.index <= limit -%}
{%- set img = recipe.attachments.images|sort(attribute='record_label')|first -%}
<a href="{{ recipe|url }}">{#--#}
<div class="recipe-tile">
<div class="img-placeholder">
{%- if img -%}
<img class="lozad" width="200" height="150" data-src="{{ img.thumbnail(200, 150)|url }}" />
{%- else -%}
No Image
{%- endif -%}
</div>
<p>{{ recipe.name }}</p>
</div>{#--#}
</a>
{%- endif -%}
{%- endfor %}
</div>
{%- endmacro -%}

View File

@@ -0,0 +1,10 @@
{% extends "layout.html" %}
{% block title %}{{ this.datamodel.name }}{% endblock %}
{% block body %}
<h1>{{ this.datamodel.name }}</h1>
<ul class="li-lg-space">
{%- for cluster in this.children %}
<li><a href="{{ cluster|url }}">{{ cluster.name }}</a></li>
{%- endfor %}
</ul>
{% endblock %}

63
src/templates/recipe.html Normal file
View File

@@ -0,0 +1,63 @@
{% extends "layout.html" %}
{% block title %}{{ this.name }}{% endblock %}
{% block body %}
<article class="recipe">
<section id="img-carousel" class="v-scroll center">
{%- for img in this.attachments.images|sort(attribute='record_label') %}
<img src="{{ img|url }}" height="400px">
{%- endfor %}
</section>
{% if this.source -%}
<div id="source" class="small center">
<a href="{{ this.source }}">⤳ {{ this.source.host }}</a>
</div>
{% endif %}
<h1>{{ this.name }}</h1>
<section id="metrics" class="small">
<div id="rating" class="xlarge">{{ this.rating|rating }}</div>
<div class="difficulty {{this.difficulty}}">
<div></div><div></div><div></div>
{%- if this.difficulty %}
<span>{{ localize('difficulty', this.difficulty) }}</span>
{%- else %}
<span class="small">{{ localize('difficulty._unset') }}</span>
{%- endif %}
</div>
<div>{{ localize('duration.label') }}: {{ this.time|duration if this.time else '—' }}</div>
<div>{{ localize('yield.label') }}: {{ this.yield if this.yield else '—' }}</div>
</section>
<section id="ingredients">
<h2>{{ localize('title.ingredients') }}:</h2>
<ul class="no-bullets li-lg-space">
{%- for ing in this|enumIngredients %}
{%- if ing['group'] %}
<li class="dark-red bold mrgTopMd">{{ ing['group'] }}</li>
{%- else %}
<li>
{%- if ing['value'] %}{{ ing['value'] }} {% endif -%}
{%- if ing['measure'] %}{{ ing['measure'] }} {% endif -%}
<span class="light-red">{{ ing['name'] }}</span>
{%- if ing['note'] -%}
<span class="small italic">{{ ', ' ~ ing['note'] }}</span>
{%- endif -%}
</li>
{%- endif %}
{%- endfor %}
</ul>
</section>
<section id="directions">
<h2>{{ localize('title.directions') }}:</h2>
{% if site.get('settings', alt=this.alt)['replace_temp'] -%}
{{ this.directions|string|replace('°C', '℃')|replace('°F', '℉')|markdown }}
{% else -%}
{{ this.directions }}
{% endif -%}
</section>
<div style="clear: both;"></div>
</article>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% from "macros/recipes.html" import render_recipe_list %}
{% from "macros/pagination.html" import render_pagination_all %}
{% block title %}{{ localize('title.recipes') }}{% endblock %}
{% block body %}
<h1>{{ localize('title.recipes') }}</h1>
{{ render_recipe_list(this.pagination.items) }}
{{ render_pagination_all(this.pagination) }}
{% endblock %}

8
src/templates/root.html Normal file
View File

@@ -0,0 +1,8 @@
{% extends "layout.html" %}
{% from "macros/recipes.html" import render_recipe_list %}
{% block body %}
<h1>{{ localize('title.latest') }}</h1>
<div class="latest">
{{ render_recipe_list(site.query('recipes', this.alt) | sort(attribute='date', reverse=True), limit=6) }}
</div>
{% endblock %}

9
src/templates/tag.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% from "macros/recipes.html" import render_recipe_list %}
{% from "macros/pagination.html" import render_pagination_all %}
{% block title %}{{ this.name }}{% endblock %}
{% block body %}
<h1>Tag: {{ this.name }}</h1>
{{ render_recipe_list(this.pagination.items) }}
{{ render_pagination_all(this.pagination) }}
{% endblock %}