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

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Created by https://www.gitignore.io/api/macos
# Edit at https://www.gitignore.io/?templates=macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# End of https://www.gitignore.io/api/macos

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2019 Oleg Geier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
Lektor recipes
==============
Generating a static site for recipes.
Small, fast, multi-language, indexed.
![screenshot](img1.jpg)
Styling is optimized for desktop, mobile, and print output.
At some point I may add search filters and offline archives for mobile devices.
This project is built upon [Lektor](https://github.com/lektor/lektor/).
Install
-------
1. [Download](https://www.getlektor.com/) Lektor and follow the instructions.
2. Clone this repository and change to the `src` directory.
3. Run `lektor server` to run a local server and preview the page. **Note:** Open http://127.0.0.1:5000/en/ instead of the default `/` path.\**
### Deploy
You need to add a deployment setting to the project file.
Either apply something from the [official docs](https://www.getlektor.com/docs/deployment/),
or run a custom rsync command:
```
rsync -rclzv --delete --exclude=.* SRC DST
```
\** You don't have to worry about the redirect.
The `root/index.html` is copied to the destination.
Instead, you could also delete `root/` and change the project file.
Set `url_prefix` to `/` for one of the alternates.
### Modify
Thanks to Lektor you have a simple content management system (see screenshot below).
Two things to note:
1. Measurements have to be added manually to settings. Don't forget to __pluralize__ (c, cup, cups, etc.)
2. You can __group ingredients__ if the line ends with a colon (`:`)
Also, see [Lektor docs](https://www.getlektor.com/docs/) and [jinja2 template](https://jinja.palletsprojects.com/en/2.10.x/templates/) documentation.
![screenshot](img2.jpg)
![screenshot](img3.jpg)

BIN
img1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
img2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
img3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

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 %}