diff --git a/.gitignore b/.gitignore index 01de35e..3d967d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +bin/* +build-state/* +data/distribution/* +!data/distribution/contents.lr + + # Created by https://www.gitignore.io/api/macos # Edit at https://www.gitignore.io/?templates=macos diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2c52dad --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/packages/force-update"] + path = src/packages/force-update + url = https://github.com/relikd/lektor-force-update-plugin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..86a6e87 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +PROJDIR := 'src' + +help: + @echo + @echo 'make clean - Removes all temporary server-build files (not ./bin)' + @echo 'make server - Start lektor server with live change updates' + @echo 'make build - Build deployable website into ./bin' + @echo + @echo 'make find-links - Search for cross reference between recipes' + @echo + +# Project build & clean + +clean: + @cd '$(PROJDIR)' && \ + temp_path="$$(lektor project-info --output-path)" && \ + if [[ -d "$$temp_path" ]]; then \ + echo "rm -rf $$temp_path"; rm -rf "$$temp_path"; \ + fi + +server: + @cd '$(PROJDIR)' && \ + (rm content/recipes; ln -s ../../data/development/ content/recipes) && \ + lektor server + +build: + @cd '$(PROJDIR)' && \ + (rm content/recipes; ln -s ../../data/distribution/ content/recipes) && \ + lektor build --output-path ../bin --buildstate-path ../build-state -f ENABLE_APPCACHE + +# Helper methods on all recipes + +find-links: + @echo + @cd '$(PROJDIR)/content/recipes' && \ + find */*.lr -exec grep --color=auto -i ".\.\./[^ ]*" -o {} + \ + || echo 'nothing found.' + @echo diff --git a/README.md b/README.md index 740a049..8807073 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,47 @@ Lektor recipes ============== -Generating a static site for recipes. -Small, fast, multi-language, indexed. +Static site generator for recipes; built upon [Lektor](https://github.com/lektor/lektor/). ![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/). +Features +------- + +- Responsive design (desktop, mobile, print) +- Mobile application (AppCache & app manifest) +- Offline cache (local storage [150 recipes ~ 3mb]) +- Multi-language (DE & EN, more can be added) +- Blazing fast (due to cache and minimal data usage) +- Indexed (group by time, ingredients, or tags) +- ~~static search~~ (**not yet**, but coming soon…) Install ------- -1. [Download](https://www.getlektor.com/) Lektor and follow the instructions. +1. Download [Lektor](https://www.getlektor.com/) 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.\** +3. Run `make server` to run a local server and preview the page. - -### 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. +4. For distribution run `make build` and add an [official deploy](https://www.getlektor.com/docs/deployment/). ### Modify Thanks to Lektor you have a simple content management system (see screenshot below). -Two things to note: +A few 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 (`:`) +2. You can add __ingredient groups__ if the line ends with a colon (`:`) + +3. The preferred __image size__ is `800x600`. Please scale all images down to save bandwidth. Try to keep an aspect ratio of 4:3 for the first image, or it will be cropped on the recipe overview page! All other images will be shown unmodified in whatever aspect ratio is provided (individual recipe page) + +4. __AppCache__ is disabled during development. However, you can pass `-f ENABLE_APPCACHE` to any `lektor` command to enable it. The makefile does this by default for the `build` target. Also, see [Lektor docs](https://www.getlektor.com/docs/) and [jinja2 template](https://jinja.palletsprojects.com/en/2.10.x/templates/) documentation. diff --git a/data/development/brownies-raw/contents+de.lr b/data/development/brownies-raw/contents+de.lr new file mode 100644 index 0000000..45a7a5f --- /dev/null +++ b/data/development/brownies-raw/contents+de.lr @@ -0,0 +1,19 @@ +yield: 20×20cm Backform +--- +ingredients: + +2 Tassen Walnüsse +1 Tasse Kakao +1/4 TL Salz +2 1/2 Tassen Datteln, weich, Medjool + +Optional: +1 Tasse Mandeln, grob gehackt +--- +directions: + +Walnüsse im Mixer fein hacken. Kakao und Salz hinzugeben und miteinander vermixen. + +Datteln nach und nach einzeln hinzugeben, während der Mixer weiter läuft. Die Mixtur sollte bröckelig sein aber beim Zusammendrücken leicht zusammenhalten (wenn es nicht zusammenhält, noch mehr Datteln hinzugeben) + +In einer Schüssel die Mandeln mit der Mixtur vermischen und in eine Kuchenform pressen. Bis zum Servieren im Kühlschrank, luftdicht verschlossen halten. diff --git a/data/development/brownies-raw/contents+en.lr b/data/development/brownies-raw/contents+en.lr new file mode 100644 index 0000000..4a483c0 --- /dev/null +++ b/data/development/brownies-raw/contents+en.lr @@ -0,0 +1,19 @@ +yield: 8×8 square pan +--- +ingredients: + +2 cups walnuts +1 cup cocoa powder +1/4 tsp salt +2 1/2 cups dates, soft, medjool + +Optional: +1 cup almonds, roughly chopped +--- +directions: + +Blend walnuts on high until finely ground. Add the cocoa and salt. Pulse to combine. + +Add the dates one at a time through the feed tube of the food processor while it is running. What you should end up with is a mix that appears rather like cake crumbs. But when pressed, will easily stick together (if the mixture does not hold together well, add more dates). + +In a large bowl, combine the walnut-cocoa mix with the chopped almonds. Press into a lined cake pan or mold. Place in the fridge until ready to serve. Store in an airtight container. diff --git a/data/development/brownies-raw/contents.lr b/data/development/brownies-raw/contents.lr new file mode 100644 index 0000000..8adbf36 --- /dev/null +++ b/data/development/brownies-raw/contents.lr @@ -0,0 +1,13 @@ +name: Brownies, Raw +--- +tags: cake, glutenfree, raw, sweet +--- +time: 15 +--- +difficulty: easy +--- +rating: 4 +--- +source: https://mynewroots.org/site/2011/04/the-raw-brownie-2/ +--- +date: 2013-09-01 diff --git a/data/development/brownies-raw/image.jpg b/data/development/brownies-raw/image.jpg new file mode 100644 index 0000000..c67302e Binary files /dev/null and b/data/development/brownies-raw/image.jpg differ diff --git a/src/content/recipes/contents.lr b/data/development/contents.lr similarity index 100% rename from src/content/recipes/contents.lr rename to data/development/contents.lr diff --git a/data/development/gluten-free-flour/contents+de.lr b/data/development/gluten-free-flour/contents+de.lr new file mode 100644 index 0000000..dabeb0c --- /dev/null +++ b/data/development/gluten-free-flour/contents+de.lr @@ -0,0 +1,133 @@ +name: Glutenfreies Mehl +--- +yield: 9 Tassen +--- +ingredients: + +6 Tassen Mehl +3 Tassen Stärke +--- +directions: + +1) Mindestens 1–2 Stärken (leichtes Mehl) und 1 mittleres Mehl mischen. Je nach Geschmack und Textur noch ein weiteres Mehl (mittel oder schwer) hinzufügen. + +2) Mehl mit einem Löffel umfüllen und mit einem Messer glattstreichen. + +3) In einen luftdichten Behälter geben und gut schütteln. + +4) An einem dunklen, trockenen Ort oder im Kühlschrank aufbewahren. + + +### Existierende Mischungen + +#### [Cara's All-Purpose Mischung](https://forkandbeans.com/2013/02/13/the-best-gluten-free-flour-blend/) +- 3 Tassen __Braunes Reismehl__, extrafein gemahlen +- 3 Tassen __Sorghummehl__, extrafein gemahlen +- 1.5 Tassen __Pfeilwurzmehl__ +- 1.5 Tassen __Kartoffelstärke__ + +Für die [Leichte Mischung](https://forkandbeans.com/2015/11/18/gluten-free-flour-blend-for-cakes/) (perfekt für Kuchen, Cupcakes, Muffins, etc.) – Sorghum ersetzen durch Weißes Reismehl. + + +#### [Sarah's All-Purpose Mischung](https://sarahbakesgfree.com/2012/05/sarahs-gluten-free-flour-blend.html) +- 4 Tassen __Braunes Reismehl__ +- 2 Tassen __Weißes Reismehl__ +- 2 Tassen __Kartoffelstärke__ +- 1/2 Tasse __Tapiokastärke__ +- 1/2 Tasse __Maisstärke__ +- 5 TL __Xanthan__ + + +### Mischung selbst zusammen stellen + +#### Leichte Mehle: +Das sind alle Stärken — ein Muss bei der Herstellung von glutenfreien Mehlen. + +
+
Pfeilwurzmehl
+
+Diese leicht verdauliche Stärke wird aus einer Kombination mehrerer Wurzeln gewonnen. Es kann als gesündere Alternative für Maisstärke, zur Verdickung von Soßen verwendet werden. +
+
Maisstärke
+
+Aus Mais gemahlen, ist diese Stärke ein hervorragendes Binde- und Verdickungsmittel. Maisstärke erzeugt bspw. bei Brot eine großartige Kruste. +
+
Kartoffelstärke
+
+Ausgezeichnete Stärke um Backwaren mit Feuchtigkeit zu versorgen. Bitte beachte, dass Kartoffelstärke und Kartoffelmehl zwei verschiedene Dinge sind – Etikett sorgfältig lesen. +
+
Tapiokastärke / Tapiokamehl
+
+Aus der Maniokwurzel extrahiert und gebleicht, wird Tapiokastärke typischerweise als Verdickungsmittel in Rezepten verwendet. Dies ist eine aromatisierte Stärke. Sie verleiht dem Backgut eine gewisse Leichtigkeit in der Textur. +
+
+ + +#### Mittlere Mehle: +Obwohl diese Mehle an sich nahrhaft sind, sind sie bei der Verwendung in einer Rezeptur etwas leichter und stabiler. Sie können in Verbindung mit einer Stärke auch einzeln verwendet werden. + +
+
Favabohne / Ackerbohne
+
+Die Favabohne kommt typischerweise in einer Mischung aus Kichererbsenmehl vor. Sie lässt den Teig schön aufgehen, hat aber einen ausgeprägten Geschmack. +
+
Kichererbse
+
+Einer der besten glutenfreien Mehle, der einzige Haken ist ein sehr ausgeprägter Bohnengeschmack. Lässt die Backwaren außergewöhnlich gut aufgehen. Kann einzeln mit Stärke verwendet werden. +
+
Hirse
+
+Mit einem trockenen und leicht nussigen Geschmack ist Hirsemehl ein überwiegend stärkehaltiges Getreide. Der Proteingehalt gleicht dem von Vollkornmehl. +
+
Hafer
+
+Dieses Mehl, das direkt aus Hafer gemahlen wird, ist reich an Vitaminen und Ballaststoffen. Es ergibt einen schönen, gleichmäßigen Geschmack und lässt die Backwaren gut aufgehen. Kann einzeln oder in Kombination mit anderen Mehlen verwendet werden. +
+
Quinoa
+
+Trotz seines sehr ausgeprägten Geschmacks ist Quinoa ein weiteres großartiges Mehl. Es kann einzeln oder in Kombination mit anderen Mehlen verwendet werden. Es ist nahrhaft, enthält viele Mineralien und Vitamine, und produziert eine hervorragende Textur. +
+
Sorghum
+
+Sorghum neigt dazu, die Textur und Leichtigkeit von Weizenmehl nachzuahmen und verleiht den Backwaren eine gewisse Zartheit. Aus diesem Grund ist es eines meiner Lieblingsmehle. +
+
Weißer Reis
+
+Berühmt dafür, eine kiesige Textur zu erzeugen. Bei diesem Mehl ist es wichtig, einen sehr feinen Mahlgrad zu wählen. Es ist ein sehr verbreitetes Mehl, das aus gutem Grund in vielen Mischungen verwendet wird. Es ist leicht und liefert hervorragende Ergebnisse. Hinweis: Süßer Reis unterscheidet sich vom weißem Reis und sollte eher wie eine Stärke (und in kleineren Mengen) verwendet werden. +
+
+ + +#### Schwere Mehle: +Das sind dichtere und nahrhaftere Mehle. Sie werden selten einzeln verwendet, sondern in Kombination mit mittleren Mehlen. + +
+
Mandel
+
+Mit einem schönen Kick an Proteinen ist Mandelmehl eine gute Möglichkeit, deinen Backwaren einen butterigen Geschmack zu verleihen. Es ist ein gutes Bindemittel und verleiht Feuchtigkeit (besonders wenn du keine Eier verwendest). Für Paleo-Diät sind Nuss- und Kokosmehle eine Korn-freie Alternative. +
+
Amaranth
+
+Aus den Samen der Amaranthpflanze gewonnen, ist dieses gemahlene Mehl dichter und nährstoffreicher als die meisten Mehle. Es kann in Backwaren die wenig aufgehen einzeln verwendet werden, oder in Kombination mit einer Mischung aus mittleren Mehlen. +
+
Brauner Reis
+
+Braunes Reismehl ist sehr vergleichbar mit Vollkornmehl. Es hat viele Nährstoffe und verleiht deinem Rezept eine gute Struktur. Idealerweise sollte es superfein gemahlen sein – sonst entsteht eine kiesige Textur. Es kann mit einer Stärke einzeln verwendet werden oder in Kombination mit anderen mittleren Mehl(en). +
+
Buchweizen
+
+Lass dich sich nicht von dem Namen täuschen, Buchweizen wird nicht aus Weizen gewonnen, sondern aus einer Frucht. Dieses Mehl kann deinem Backgut einen schönen braunen Farbton verleihen, ist voll von Nährstoffen und Dichte. Es muss in Kombination mit einer Stärke und einem mittlerem Mehl verwendet werden, insbesondere für stark aufgehende Rezepte. +
+
Kokosnuss
+
+Kokosnuss absorbiert sehr viel Flüssigkeit im Rezept. Deshalb nur in kleineren Mengen (ca. 1/4 Tasse) und in Kombination mit anderen Mehlen verwenden. +
+
Mais
+
+Ein herzhaftes, dichtes Mehl. Maismehl kann der Mehlmischung eine schöne Textur verleihen, ähnlich wie bei einem Maisbrot. +
+
Teff
+
+Teff ist ein einzigartiges, aromatisiertes Vollkorn Mehl, das in äthiopischen Lebensmitteln verwendet wird. Es ist leicht, erzeugt aber gleichzeitig eine dichte Textur, so dass es am besten in kleineren Mengen (wie Kokosmehl) und in Kombination mit anderen Mehlen verwendet wird. +
+
diff --git a/data/development/gluten-free-flour/contents+en.lr b/data/development/gluten-free-flour/contents+en.lr new file mode 100644 index 0000000..72f705e --- /dev/null +++ b/data/development/gluten-free-flour/contents+en.lr @@ -0,0 +1,134 @@ +name: Gluten-Free Flour +--- +yield: 9 cups +--- +ingredients: + +6 cups flour +3 cups starch +--- +directions: + +1) You'll need 1–2 starches (light flour) and at least 1 medium flour. Based on the prefered flavor and texture, you can mix in another flour (medium or heavy). + +2) Spoon out flours with a spoon and level off with a knife. + +3) Place into an air-tight container and shake well. + +4) Keep stored in a dark, dry place or in the refrigerator. + + +### Existing blends + +#### [Cara's All-Purpose Blend](https://forkandbeans.com/2013/02/13/the-best-gluten-free-flour-blend/) +- 3 cups brown rice flour, superfine ground +- 3 cups sorghum flour, superfine ground +- 1.5 cups arrowroot powder +- 1.5 cups potato starch + + +For the [Light Blend](https://forkandbeans.com/2015/11/18/gluten-free-flour-blend-for-cakes/) (perfect for cakes, cupcakes, muffins, etc.) – replace sorghum with white rice flour. + + +#### [Sarah's All-Purpose Blend](https://sarahbakesgfree.com/2012/05/sarahs-gluten-free-flour-blend.html) +- 4 cups brown rice flour +- 2 cups white rice flour +- 2 cups potato starch +- 1/2 cup tapioca flour +- 1/2 cup corn starch +- 5 tsp xanthan gum + + +### Create your own blend + +#### Light based flours: +These are all of the starches — a must when creating a blend of gluten free flours. + +
+
Arrowroot Powder
+
+This easy-to-digest starch is extracted from a combination of several plant rootstocks. It can be used as a healthier sub for cornstarch in thickening up sauces and gravies. +
+
Corn Starch
+
+Ground from corn, this starch makes for a great binder and thickening agent. Check for a great non GMO brand because cornstarch can add a great outer crust for your breads. +
+
Potato Starch
+
+This is my go-to starch for it's ability to add moisture into baked goods. Please note that potato starch and potato flour are two different things – read the label carefully. +
+
Tapioca Starch / Tapioca Flour
+
+Extracted and bleached from the cassava root, tapioca starch is typically used as a thickening agent in recipes. This is a flavored starch to use in a blend for its ability to bring a certain lightness in texture to the baked good. +
+
+ + +#### Medium based flours: +Though nutritious in their own right, these flours are a bit lighter when used in a recipe and are more stable to be used alone paired with a starch. + +
+
Fava Bean
+
+Fava bean can typically be found with a mix of garbanzo bean flour. It yields a really nice rise but has a distinct flavor. +
+
Garbanzo Bean / Chickpea
+
+One of the best result-producing gluten-free flours, the only catch is its a very distinct bean flavor. The rise in your baked goods will be exceptional and can be used alone with a starch. +
+
Millet
+
+With a dry and slighty nutty flavor, millet flour is a predominantly starchy grain with a protein content that is similar to whole wheat flour. +
+
Oat
+
+Ground straight from oats, this flour is rich in vitamins and fiber. It yields a nice even flavor and a great rise to your baked goods. Can be used alone or in a combination of other flours. +
+
Quinoa
+
+Despite it's very distinct flavor, quinoa is another great medium-based flour that can work alone or in combination to other flours. It's nutritious, dense with minerals and vitamins, and produces a great texture. +
+
Sorghum
+
+Sorghum tends to mimic the texture and lightness of wheat flour and will give your baked goods a certain tenderness. For this reason, it's one of my go-to flours in my flour blend. +
+
White Rice
+
+Notorious for yielding a gritty texture, it's important to get the finest ground for this flour. It's a very common flour used in blends for good reason, it's light and gives great results. Note: Sweet Rice is different from White Rice and should be used more like a starch and in smaller amounts. +
+
+ + +#### Heavy based flours: +These are the more dense and nutritious flours that are rarely used alone and will need to be used in tandem with another medium-based flour. + +
+
Almond
+
+With a nice kick of protein, nut meal is a great way to give your baked goods a buttery flavor. It works best if you add a little into your mix (especially if you do not use eggs) to yield a nice binding result and overall moisture into your recipe. If you are Paleo, nut and coconut flours are you to-go grain-free flours. +
+
Amaranth
+
+Derived from the seeds of the amaranth plant, this stone ground flour is denser and more nutritious than most flours. It can be used alone in low rise baked goods or in combination with a mix of medium flours. +
+
Brown Rice
+
+Brown rice flour is very comparable to whole wheat flour, with its dense nutrition and great structure it lends to your recipe. It's best if you use it in superfine ground form – this way you will avoid that gritty texture. It can be used alone with a starch or in combination with another medium flour(s). +
+
Buckwheat
+
+Don't be fooled by its name, buckwheat is not derived by wheat but rather a fruit. This flour can give your baked good a nice brown hue, full of nutrition and density. It needs to be used in combination with a starch and medium flour, especially for higher rising recipes. +
+
Coconut
+
+Coconut has a great way of absorbing the liquid in a recipe, which is why it needs to be used in smaller amounts (think 1/4 cup) and in combo with other flours. +
+
Corn
+
+A hearty, dense flour, corn flour can add a nice texture to your flour blend, similar to a corn bread toothsome feel. +
+
Teff
+
+A unique flavored whole grain, Teff is a very common flour used in Ethiopian food. It's light but creates a dense texture at the same time so it's best if used in smaller amounts (like coconut flour) and in combination with other flours. +
+
diff --git a/data/development/gluten-free-flour/contents.lr b/data/development/gluten-free-flour/contents.lr new file mode 100644 index 0000000..c909951 --- /dev/null +++ b/data/development/gluten-free-flour/contents.lr @@ -0,0 +1,9 @@ +tags: bread, glutenfree, ingredient +--- +time: 5 +--- +difficulty: easy +--- +source: https://forkandbeans.com/2013/12/30/guide-gluten-free-flours/ +--- +date: 2013-08-09 diff --git a/data/development/gluten-free-flour/image.jpg b/data/development/gluten-free-flour/image.jpg new file mode 100644 index 0000000..b0b39f9 Binary files /dev/null and b/data/development/gluten-free-flour/image.jpg differ diff --git a/src/content/recipes/vanilla-cut-out-cookies/contents+de.lr b/data/development/vanilla-cut-out-cookies/contents+de.lr similarity index 60% rename from src/content/recipes/vanilla-cut-out-cookies/contents+de.lr rename to data/development/vanilla-cut-out-cookies/contents+de.lr index 89536ef..f07fc67 100644 --- a/src/content/recipes/vanilla-cut-out-cookies/contents+de.lr +++ b/data/development/vanilla-cut-out-cookies/contents+de.lr @@ -1,7 +1,5 @@ name: Vanille Ausstech-Kekse --- -yield: 26-28 Kekse ---- ingredients: 1/2 Tasse Margarine, oder Kokos Öl @@ -9,8 +7,8 @@ ingredients: 1/2 TL Vanille Extrakt 1/4 TL Vanilleshote 1 Prise Salz -2 1/4 Tassen Mehl, glutenfrei +2 1/4 Tassen Mehl, glutenfrei, @../gluten-free-flour/ --- directions: -No translation yet. Click the flag (🇱🇷) at the bottom. \ No newline at end of file +No translation yet. Click the flag (🇱🇷) at the bottom. diff --git a/data/development/vanilla-cut-out-cookies/contents+en.lr b/data/development/vanilla-cut-out-cookies/contents+en.lr new file mode 100644 index 0000000..6350aa7 --- /dev/null +++ b/data/development/vanilla-cut-out-cookies/contents+en.lr @@ -0,0 +1,24 @@ +name: Vanilla cut-out 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 +1 dash salt +2 1/4 cups flour, gluten-free, @../gluten-free-flour/ +--- +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 it's 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 it's a little sticky, add a little more flour (try 1-2 Tbsp); if it's 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 won't 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 don't 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 diff --git a/src/content/recipes/vanilla-cut-out-cookies/contents.lr b/data/development/vanilla-cut-out-cookies/contents.lr similarity index 50% rename from src/content/recipes/vanilla-cut-out-cookies/contents.lr rename to data/development/vanilla-cut-out-cookies/contents.lr index 88f63d2..f0b312c 100644 --- a/src/content/recipes/vanilla-cut-out-cookies/contents.lr +++ b/data/development/vanilla-cut-out-cookies/contents.lr @@ -6,6 +6,8 @@ rating: 4 --- difficulty: easy --- -source: https://www.unconventionalbaker.com/recipes/gluten-free-vegan-vanilla-cut-out-cookies/ +yield: 26-28 +--- +source: https://unconventionalbaker.com/recipes/gluten-free-vegan-vanilla-cut-out-cookies/ --- date: 2019-05-15 diff --git a/src/content/recipes/vanilla-cut-out-cookies/image.jpg b/data/development/vanilla-cut-out-cookies/image.jpg similarity index 100% rename from src/content/recipes/vanilla-cut-out-cookies/image.jpg rename to data/development/vanilla-cut-out-cookies/image.jpg diff --git a/src/content/recipes/vanilla-cut-out-cookies/image2.jpg b/data/development/vanilla-cut-out-cookies/image2.jpg similarity index 100% rename from src/content/recipes/vanilla-cut-out-cookies/image2.jpg rename to data/development/vanilla-cut-out-cookies/image2.jpg diff --git a/src/content/recipes/vanilla-cut-out-cookies/image3.jpg b/data/development/vanilla-cut-out-cookies/image3.jpg similarity index 100% rename from src/content/recipes/vanilla-cut-out-cookies/image3.jpg rename to data/development/vanilla-cut-out-cookies/image3.jpg diff --git a/data/distribution/contents.lr b/data/distribution/contents.lr new file mode 100644 index 0000000..dfb26d8 --- /dev/null +++ b/data/distribution/contents.lr @@ -0,0 +1 @@ +_model: recipes \ No newline at end of file diff --git a/data/export-yummy.py b/data/export-yummy.py new file mode 100644 index 0000000..9062f90 --- /dev/null +++ b/data/export-yummy.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +import sqlite3 +import os +import sys +from datetime import datetime + +''' +Usage: python3 generate-alternates.py '…/YummySoup.library/Database.SQL' + +You may have to adjust `mapTag`, `clearUTF()`, and `slugify()` below. +Output is generated in the current folder under `yummysoup-exported`. +''' + +# check if input param is SQL database file +try: + inputPath = os.path.abspath(sys.argv[1]) + if not os.path.isfile(inputPath) or not inputPath.upper().endswith('SQL'): + raise Exception() + base = os.path.dirname(inputPath) + print('connecting...') + db = sqlite3.connect(inputPath) +except Exception: + print() + print(f'usage: {os.path.basename(sys.argv[0])} "path/to/db.SQL"') + print('(e.g., "…/YummySoup! Librarys.library/Library Database.SQL")') + print() + exit() + +# create output export dir if necessary +_out = os.path.abspath('./yummysoup-exported/') +if not os.path.exists(_out): + os.mkdir(_out) + +# map old tags to new one. Should be all available tags in YummySoup! +# right hand side must be lower case string or None +mapTag = { + '': None, + 'Weihnachten': 'xmas', + 'Wurst': None, + 'Dressing': 'dressing', + 'Soße': 'sauce', + 'Hauptspeise': 'main-dish', + 'Süßes': 'sweet', + 'Zutat': 'ingredient', + 'Raw': 'raw', + 'Aufstrich': 'spread', + 'Brot': 'bread', + 'Kuchen': 'cake', + 'Kekse': 'cookies', + 'trocken': None, + 'Salat': 'salad', + 'Drink': 'drinks', + 'Riegel': None, + 'Schokolade': 'chocolate', + 'Dip': 'dip', + 'fruchtig': None, + 'Glutenfrei': 'glutenfree' +} + + +def ttoint(txt): + i, n = txt.split(' ') if txt else (0, 'M') + return int(i) * [1, 60, 1440]['MST'.index(n[0])] + + +# def matchTime(time): +# if time in [0, 25, 135, 165, 300]: +# return [None, 30, 150, 150, 360][[0, 25, 135, 165, 300].index(time)] +# prev = 99999 +# val = time +# for x in [5, 10, 15, 20, 30, 45, 60, 75, 90, 105, +# 120, 150, 180, 240, 360, 480, 720, 1440]: +# diff = abs(time - x) +# if diff < prev: +# prev = diff +# val = x +# elif diff == prev: +# print(time) +# return val + + +def clearUTF(txt): + return txt.replace('\\U00df', 'ß').replace('\\U00f1', 'ñ')\ + .replace('\\U00c4', 'Ä').replace('\\U00e4', 'ä')\ + .replace('\\U00d6', 'Ö').replace('\\U00f6', 'ö')\ + .replace('\\U00dc', 'Ü').replace('\\U00fc', 'ü') + + +def slugify(txt): + return txt.lower().replace(' ', '-').replace(':', '').replace('ß', 'ss')\ + .replace('(', '').replace(')', '').replace(',', '').replace('ê', 'e')\ + .replace('ä', 'ae').replace('ü', 'ue').replace('ö', 'oe').strip('-') + + +def formatIngredient(info): + try: + if info['isG'] in ['YES', '1']: + return '\n' + info['nam'] + except KeyError: + pass + txt = info['nam'].replace(',', ' ') + if info['mea']: + txt = '{} {}'.format(info['mea'], txt) + if info['qua']: + txt = '{} {}'.format(info['qua'], txt) + if info['met']: + txt = '{}, {}'.format(txt, info['met']) + return txt + + +def ingredientToStr(txt): + res = '' + for ing in clearUTF(txt).split('},'): + ing = ing.strip('{()} \n') + info = {'qua': '', 'mea': '', 'nam': '', 'met': ''} + for prop in ing.split(';'): + if not prop: + continue + k, v = [x.strip('\n "') for x in prop.split('=')] + info[k[:3]] = v + res += '\n' + formatIngredient(info) + return res + + +def directionsToStr(txt): + return txt.replace('', '').replace(' ', '')\ + .replace('', '').replace('
', '').replace('', '__').\ + replace('', '__').replace('', '_').replace('', '_')\ + .replace('℃', '°C').replace(' °C', '°C')\ + .replace('½', '1/2').replace('¼', '1/4').replace('⅛', '1/8')\ + .replace('⅓', '1/3').replace('⅔', '2/3').replace('¾', '3/4') + + +def prnt(key, val, inline=True): + return '' if not val else '{}:{}{}\n---\n'.format( + key, ' ' if inline else '\n\n', str(val).strip()) + + +def export(slug, content, img): + output = os.path.join(_out, slug) + for i in range(10): + folder = output + if i > 0: + folder += '-%d' % i + if not os.path.isdir(folder): + output = folder + break + os.mkdir(output) + with open(os.path.join(output, 'contents.lr'), 'w') as f: + f.write(txt.strip().rstrip('-')) + + for i in range(1, 10): + src = img % i + dest = os.path.join(output, f'image{"" if i == 1 else i}.jpg') + if not os.path.isfile(src): + break + with open(src, 'rb') as a, open(dest, 'wb') as b: + b.write(a.read()) + + +print('exporting...') +for row in db.cursor().execute('''SELECT * FROM ZRECIPES'''): + difficulty, rating, date, img = row[4], row[7], row[9], row[10] + duration, tags, name, yields = row[12:15], row[15], row[17], row[21] + notes, directions, source, ingredients = row[23], row[25], row[26], row[27] + + # preprocess + date = datetime.fromtimestamp(date + 978307200).strftime('%Y-%m-%d') + img = os.path.join(base, 'Images', img + '-Image%d.jpg') + duration = sum([ttoint(x) for x in duration]) # matchTime() + tags = ', '.join(sorted([mapTag[x] for x in tags.split(',') if mapTag[x]])) + slug = slugify(name) + if yields: + y = yields.split(' ') + if len(y) == 3 and y[1].endswith('form'): + yields = '{} {}'.format(y[2], y[1]) + + txt = '' + txt += prnt('name', name) + txt += prnt('tags', tags) + txt += prnt('time', duration) + txt += prnt('difficulty', [None, 'easy', 'medium', 'hard'][difficulty]) + txt += prnt('rating', rating) + txt += prnt('yield', yields) + txt += prnt('ingredients', ingredientToStr(ingredients), False) + desc = directionsToStr(directions) + if notes: + desc = '{}\n\n__Notes:__ {}'.format(desc.strip(), notes) + txt += prnt('directions', desc, False) + txt += prnt('source', source) + txt += prnt('date', date) + + export(slug, txt, img) + +db.close() +print('done.') diff --git a/data/generate-alternates.py b/data/generate-alternates.py new file mode 100644 index 0000000..46f5e5c --- /dev/null +++ b/data/generate-alternates.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import os +import sys + +''' +Usage: python3 generate-alternates.py development/* + +Input is a recipe folder. +Will take the `contents.lr` and extract `contents+de.lr` and `contents+en.lr`. +The content will be identical but its easier to edit this way. +No necessary redundant data fields. +''' + + +def prnt(key, val, inline=True): + return '' if not val else '{}:{}{}\n---\n'.format( + key, ' ' if inline else '\n\n', str(val).strip()) + + +def splitContent(path): + mode = 1 + idx = 0 + with open(os.path.join(path, 'contents.lr'), 'r') as fin: + tmp = ['', ''] + for line in fin: + if mode == 1: + tag = line.split(':')[0] + if tag in ['name', 'yield', 'ingredients', 'directions']: + idx = 1 + else: + idx = 0 + tmp[idx] += line + mode = 2 + else: + tmp[idx] += line + if line == '---\n': + mode = 1 + tmp[1] = tmp[1][:-4] + return tmp + + +def writeSplit(path): + de_file = os.path.join(path, 'contents+de.lr') + if not os.path.isdir(path) or os.path.exists(de_file): + return + print(path) + content = splitContent(path) + if not content[1]: + return + with open(de_file, 'w') as f: + f.write(content[1]) + with open(os.path.join(path, 'contents+en.lr'), 'w') as f: + f.write(content[1]) + with open(os.path.join(path, 'contents.lr'), 'w') as f: + f.write(content[0]) + + +for x in sys.argv[1:]: + writeSplit(x) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..58717e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lektor diff --git a/src/assets/app.webmanifest b/src/assets/app.webmanifest new file mode 100644 index 0000000..a221dd4 --- /dev/null +++ b/src/assets/app.webmanifest @@ -0,0 +1,14 @@ +{ + "name": "42 recipes", + "display": "standalone", + "background_color" : "#EAE9E7", + "theme_color": "#DC3A59", + "scope": ".", + "icons": [{ + "src": "img/icon-180.png", + "sizes": "180x180" + },{ + "src": "img/icon-196.png", + "sizes": "196x196" + }] +} \ No newline at end of file diff --git a/src/assets/favicon.ico b/src/assets/favicon.ico new file mode 100644 index 0000000..6d60f35 Binary files /dev/null and b/src/assets/favicon.ico differ diff --git a/src/assets/img/favicon.svg b/src/assets/img/favicon.svg new file mode 100644 index 0000000..1e60ee0 --- /dev/null +++ b/src/assets/img/favicon.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/assets/img/icon-180.png b/src/assets/img/icon-180.png new file mode 100644 index 0000000..c329a81 Binary files /dev/null and b/src/assets/img/icon-180.png differ diff --git a/src/assets/img/icon-196.png b/src/assets/img/icon-196.png new file mode 100644 index 0000000..378dfdf Binary files /dev/null and b/src/assets/img/icon-196.png differ diff --git a/src/assets/img/icon-32.png b/src/assets/img/icon-32.png new file mode 100644 index 0000000..82c2acd Binary files /dev/null and b/src/assets/img/icon-32.png differ diff --git a/src/assets/img/icon-glutenfree.svg b/src/assets/img/icon-glutenfree.svg new file mode 100644 index 0000000..a1d35bd --- /dev/null +++ b/src/assets/img/icon-glutenfree.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/icon-glutenfree_old.svg b/src/assets/img/icon-glutenfree_old.svg new file mode 100644 index 0000000..95cba9a --- /dev/null +++ b/src/assets/img/icon-glutenfree_old.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/icon-raw.svg b/src/assets/img/icon-raw.svg new file mode 100644 index 0000000..b007071 --- /dev/null +++ b/src/assets/img/icon-raw.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/src/assets/img/icon-vegan.svg b/src/assets/img/icon-vegan.svg new file mode 100644 index 0000000..655a32a --- /dev/null +++ b/src/assets/img/icon-vegan.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/assets/img/icon-yield.svg b/src/assets/img/icon-yield.svg new file mode 100644 index 0000000..c07a0ec --- /dev/null +++ b/src/assets/img/icon-yield.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/src/root/index.html b/src/assets/index.html similarity index 100% rename from src/root/index.html rename to src/assets/index.html diff --git a/src/assets/static/col2.js b/src/assets/static/col2.js deleted file mode 100644 index fdb1b84..0000000 --- a/src/assets/static/col2.js +++ /dev/null @@ -1,10 +0,0 @@ -(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); - } -})(); \ No newline at end of file diff --git a/src/assets/static/lozad.min.js b/src/assets/static/lozad.min.js deleted file mode 100644 index 625e656..0000000 --- a/src/assets/static/lozad.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! 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{ready()};} + function update(){ready(); cache.swapCache(); window.location.reload()} + function busy(){document.getElementById('cache-status').style='background:darkorange'} + function ready(){document.getElementById('cache-status').style='background:forestgreen'} + function failed(){document.getElementById('cache-status').style='background:red'} + } +} +/*! 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 * { - background-color: #FFF; + background: #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; +.tags a:hover, .tags .active, a:hover .recipe-tile, .recipe-tile .hover .time { + background: 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) { +@media(max-width: 32em) { .cluster dd { margin-left: 0 } .cluster dd a { white-space: unset } } @@ -77,25 +91,31 @@ header .tags { max-width: 600px; margin: 0 auto } /* * Grid overview */ -.pagination { text-align: center; margin-top: 1em; } +.pagination { text-align: center; margin-top: 1em } .recipe-tile { - background-color: var(--cBg2); - color: var(--cTxt); + background: 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; +.recipe-tile .placeholder { font: bold 25px/150px 'Courier New', monospace; + background: #777; color: var(--cBg2); } -a:hover .recipe-tile img { mix-blend-mode: overlay } -.recipe-tile p { height: 2.6em; margin: 0.3em 10px; overflow-y: auto } +.recipe-tile p { height: 2.5em; margin: 0.3em 10px; overflow-y: auto } +.recipe-tile img, .recipe-tile .overlay { display: block; width: 200px; height: 150px } +.recipe-tile .overlay { position: absolute } +.recipe-tile .icon-bar { position: absolute; bottom: 3px; right: 3px } +.recipe-tile .icon-bar i.icon { margin-left: 2px; font-size: 28px } +a:hover .recipe-tile .hover { display: block; background: #0006 } +.recipe-tile .hover { display: none; height: 100% } +.recipe-tile .hover .time { + position: relative; top: -1.25em; + font: bold 1.1em/1.3em monospace; +} +/* snap to column grid */ .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); */ @@ -107,9 +127,9 @@ a:hover .recipe-tile img { mix-blend-mode: overlay } @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 } + .recipe-tile .overlay { display: none } + .recipe-tile, a:hover .recipe-tile, .recipe-tile .placeholder { + background: #FFF; color: #000 } } /* @@ -120,16 +140,19 @@ a:hover .recipe-tile img { mix-blend-mode: overlay } .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 #ingredients { float: left; margin: 0 30px 15px 0; max-width: 300px } +.recipe #ingredients a { line-height: 1em } .recipe #directions ul { list-style-type: circle } +.recipe #directions dl dt { color: var(--cRed2); font-weight: bold } +.recipe #directions dl dd { margin-bottom: 1em } /* Colored, 3-part, difficulty bar */ -.difficulty.easy > div:nth-child(1) { background-color: #3C3 } +.difficulty.easy > div:nth-child(1) { background: #3C3 } .difficulty.medium > div:nth-child(1), -.difficulty.medium > div:nth-child(2) { background-color: #FC3 } +.difficulty.medium > div:nth-child(2) { background: #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.hard > div:nth-child(3) { background: #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 { @@ -138,16 +161,26 @@ a:hover .recipe-tile img { mix-blend-mode: overlay } 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 screen and (max-width: 50em) { + .recipe h1 { margin-bottom: 0 } + .recipe #img-carousel { height: calc(75vw - 2*50px) } + .recipe #img-carousel img { height: 100%; max-width: 100%; padding: 0 } + .recipe #metrics { float: unset; max-width: max-content; margin: .5em auto 2em } + .recipe #metrics > *:not(:first-child) { margin-right: -100vw; max-width: 50vw } +} +@media screen and (max-width: 40em), print and (orientation: portrait) { + .recipe #ingredients { float: unset; max-width: 100% } +} +@media screen and (max-width: 32em) { + .recipe #img-carousel { padding: 0 10px; height: calc(75vw - 2*10px) } } -@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 { + h1 { margin-top:0 } + #source, #rating, .difficulty, #img-carousel { 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 } -} +}*/ diff --git a/src/configs/force-update.ini b/src/configs/force-update.ini new file mode 100644 index 0000000..4f70c63 --- /dev/null +++ b/src/configs/force-update.ini @@ -0,0 +1,2 @@ +enabled = yes +endswith = .appcache \ No newline at end of file diff --git a/src/content/app.appcache/contents.lr b/src/content/app.appcache/contents.lr new file mode 100644 index 0000000..443cbcb --- /dev/null +++ b/src/content/app.appcache/contents.lr @@ -0,0 +1,3 @@ +_template: cache.manifest +--- +_model: none \ No newline at end of file diff --git a/src/content/groupby/time/contents.lr b/src/content/groupby/time/contents.lr index 400ed5e..af7fc61 100644 --- a/src/content/groupby/time/contents.lr +++ b/src/content/groupby/time/contents.lr @@ -10,4 +10,4 @@ xdata: 120 180 360 -9999 +1440 diff --git a/src/content/recipes/vanilla-cut-out-cookies/contents+en.lr b/src/content/recipes/vanilla-cut-out-cookies/contents+en.lr deleted file mode 100644 index 13ffb5c..0000000 --- a/src/content/recipes/vanilla-cut-out-cookies/contents+en.lr +++ /dev/null @@ -1,26 +0,0 @@ -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 it’s 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 it’s a little sticky, add a little more flour (try 1-2 tbsp); if it’s 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 won’t 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 don’t 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 diff --git a/src/content/settings/contents+de.lr b/src/content/settings/contents+de.lr index 0b1faad..9884bf6 100644 --- a/src/content/settings/contents+de.lr +++ b/src/content/settings/contents+de.lr @@ -1 +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 \ No newline at end of file +measures: EL TL kg g L dl cl ml cm Msp Prise Tasse Tassen Dose Dosen kleine große Bund Packung Packungen Scheibe Scheiben Schuss Stängel Tropfen Tube \ No newline at end of file diff --git a/src/content/settings/contents+en.lr b/src/content/settings/contents+en.lr index effd13d..561b317 100644 --- a/src/content/settings/contents+en.lr +++ b/src/content/settings/contents+en.lr @@ -1 +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 \ No newline at end of file +measures: kg g L dl cl ml oz lb pt qt cm inch tsp tbsp c cup cups pkg pkgs drop drops tube dash dashes ounce ounces small medium large box can tin pinch pinches clove cloves stick sticks bunch handful splash splashes stem stems slice slices \ No newline at end of file diff --git a/src/content/settings/contents.lr b/src/content/settings/contents.lr index 524aabb..41b7486 100644 --- a/src/content/settings/contents.lr +++ b/src/content/settings/contents.lr @@ -5,5 +5,3 @@ _hidden: yes replace_frac: yes --- replace_temp: yes ---- -show_empty_tags: no diff --git a/src/content/tags/bread/contents+de.lr b/src/content/tags/bread/contents+de.lr index b52cbce..8e279cc 100644 --- a/src/content/tags/bread/contents+de.lr +++ b/src/content/tags/bread/contents+de.lr @@ -1 +1 @@ -name: Brot +name: Brot \ No newline at end of file diff --git a/src/content/tags/bread/contents.lr b/src/content/tags/bread/contents.lr index 8a1905a..30e079e 100644 --- a/src/content/tags/bread/contents.lr +++ b/src/content/tags/bread/contents.lr @@ -1 +1 @@ -name: Bread +name: Bread \ No newline at end of file diff --git a/src/content/tags/cake/contents+de.lr b/src/content/tags/cake/contents+de.lr index 1fdb80c..7f48c06 100644 --- a/src/content/tags/cake/contents+de.lr +++ b/src/content/tags/cake/contents+de.lr @@ -1 +1 @@ -name: Kuchen +name: Kuchen \ No newline at end of file diff --git a/src/content/tags/cake/contents.lr b/src/content/tags/cake/contents.lr index 2283244..dee45d9 100644 --- a/src/content/tags/cake/contents.lr +++ b/src/content/tags/cake/contents.lr @@ -1 +1 @@ -name: Cake +name: Cake \ No newline at end of file diff --git a/src/content/tags/cheese/contents+de.lr b/src/content/tags/cheese/contents+de.lr new file mode 100644 index 0000000..b94f5c0 --- /dev/null +++ b/src/content/tags/cheese/contents+de.lr @@ -0,0 +1 @@ +name: Käse \ No newline at end of file diff --git a/src/content/tags/cheese/contents.lr b/src/content/tags/cheese/contents.lr new file mode 100644 index 0000000..eb81fe7 --- /dev/null +++ b/src/content/tags/cheese/contents.lr @@ -0,0 +1 @@ +name: Cheese \ No newline at end of file diff --git a/src/content/tags/chocolate/contents+de.lr b/src/content/tags/chocolate/contents+de.lr deleted file mode 100644 index 94cdb88..0000000 --- a/src/content/tags/chocolate/contents+de.lr +++ /dev/null @@ -1 +0,0 @@ -name: Schokolade diff --git a/src/content/tags/chocolate/contents.lr b/src/content/tags/chocolate/contents.lr deleted file mode 100644 index f7aa370..0000000 --- a/src/content/tags/chocolate/contents.lr +++ /dev/null @@ -1 +0,0 @@ -name: Chocolate diff --git a/src/content/tags/cookies/contents+de.lr b/src/content/tags/cookies/contents+de.lr index 3079952..28d4fb7 100644 --- a/src/content/tags/cookies/contents+de.lr +++ b/src/content/tags/cookies/contents+de.lr @@ -1 +1 @@ -name: Kekse +name: Keks \ No newline at end of file diff --git a/src/content/tags/cookies/contents.lr b/src/content/tags/cookies/contents.lr index 0613a7e..fbe56c2 100644 --- a/src/content/tags/cookies/contents.lr +++ b/src/content/tags/cookies/contents.lr @@ -1 +1 @@ -name: Cookies +name: Cookie \ No newline at end of file diff --git a/src/content/tags/crust/contents+de.lr b/src/content/tags/crust/contents+de.lr new file mode 100644 index 0000000..9ff66ad --- /dev/null +++ b/src/content/tags/crust/contents+de.lr @@ -0,0 +1 @@ +name: Kuchenboden \ No newline at end of file diff --git a/src/content/tags/crust/contents.lr b/src/content/tags/crust/contents.lr new file mode 100644 index 0000000..a0c0603 --- /dev/null +++ b/src/content/tags/crust/contents.lr @@ -0,0 +1 @@ +name: Crust \ No newline at end of file diff --git a/src/content/tags/dip/contents.lr b/src/content/tags/dip/contents.lr index 0145d3f..1a151c1 100644 --- a/src/content/tags/dip/contents.lr +++ b/src/content/tags/dip/contents.lr @@ -1 +1 @@ -name: Dip +name: Dip \ No newline at end of file diff --git a/src/content/tags/dressing/contents.lr b/src/content/tags/dressing/contents.lr deleted file mode 100644 index d662d31..0000000 --- a/src/content/tags/dressing/contents.lr +++ /dev/null @@ -1 +0,0 @@ -name: Dressing diff --git a/src/content/tags/drinks/contents.lr b/src/content/tags/drinks/contents.lr index bd24de0..e82d3eb 100644 --- a/src/content/tags/drinks/contents.lr +++ b/src/content/tags/drinks/contents.lr @@ -1 +1 @@ -name: Drinks +name: Drink \ No newline at end of file diff --git a/src/content/tags/glutenfree/contents+de.lr b/src/content/tags/glutenfree/contents+de.lr index 986bd67..d257448 100644 --- a/src/content/tags/glutenfree/contents+de.lr +++ b/src/content/tags/glutenfree/contents+de.lr @@ -1 +1 @@ -name: Glutenfrei +name: Glutenfrei \ No newline at end of file diff --git a/src/content/tags/glutenfree/contents.lr b/src/content/tags/glutenfree/contents.lr index af204d2..2124649 100644 --- a/src/content/tags/glutenfree/contents.lr +++ b/src/content/tags/glutenfree/contents.lr @@ -1 +1 @@ -name: Glutenfree +name: Gluten-free \ No newline at end of file diff --git a/src/content/tags/ingredient/contents+de.lr b/src/content/tags/ingredient/contents+de.lr index b1cb8a3..e8ccb11 100644 --- a/src/content/tags/ingredient/contents+de.lr +++ b/src/content/tags/ingredient/contents+de.lr @@ -1 +1 @@ -name: Zutat +name: Zutat \ No newline at end of file diff --git a/src/content/tags/ingredient/contents.lr b/src/content/tags/ingredient/contents.lr index edc91cf..f87d25b 100644 --- a/src/content/tags/ingredient/contents.lr +++ b/src/content/tags/ingredient/contents.lr @@ -1 +1 @@ -name: Ingredient +name: Ingredient \ No newline at end of file diff --git a/src/content/tags/main-dish/contents+de.lr b/src/content/tags/main-dish/contents+de.lr index b83fc3d..4463ce5 100644 --- a/src/content/tags/main-dish/contents+de.lr +++ b/src/content/tags/main-dish/contents+de.lr @@ -1 +1 @@ -name: Hauptspeise +name: Hauptspeise \ No newline at end of file diff --git a/src/content/tags/main-dish/contents.lr b/src/content/tags/main-dish/contents.lr index b83ec71..1eae988 100644 --- a/src/content/tags/main-dish/contents.lr +++ b/src/content/tags/main-dish/contents.lr @@ -1 +1 @@ -name: Main dish +name: Main dish \ No newline at end of file diff --git a/src/content/tags/muffins/contents.lr b/src/content/tags/muffins/contents.lr new file mode 100644 index 0000000..bcd6755 --- /dev/null +++ b/src/content/tags/muffins/contents.lr @@ -0,0 +1 @@ +name: Muffin \ No newline at end of file diff --git a/src/content/tags/raw/contents.lr b/src/content/tags/raw/contents.lr index 434f901..7f2aba1 100644 --- a/src/content/tags/raw/contents.lr +++ b/src/content/tags/raw/contents.lr @@ -1 +1 @@ -name: Raw +name: Raw \ No newline at end of file diff --git a/src/content/tags/salad/contents+de.lr b/src/content/tags/salad/contents+de.lr index a6b710c..38e3537 100644 --- a/src/content/tags/salad/contents+de.lr +++ b/src/content/tags/salad/contents+de.lr @@ -1 +1 @@ -name: Salat +name: Salat \ No newline at end of file diff --git a/src/content/tags/salad/contents.lr b/src/content/tags/salad/contents.lr index f0a1475..0c260de 100644 --- a/src/content/tags/salad/contents.lr +++ b/src/content/tags/salad/contents.lr @@ -1 +1 @@ -name: Salad +name: Salad \ No newline at end of file diff --git a/src/content/tags/sauce/contents+de.lr b/src/content/tags/sauce/contents+de.lr deleted file mode 100644 index fdacb8b..0000000 --- a/src/content/tags/sauce/contents+de.lr +++ /dev/null @@ -1 +0,0 @@ -name: Soße diff --git a/src/content/tags/sauce/contents.lr b/src/content/tags/sauce/contents.lr deleted file mode 100644 index 728d057..0000000 --- a/src/content/tags/sauce/contents.lr +++ /dev/null @@ -1 +0,0 @@ -name: Sauce diff --git a/src/content/tags/savory/contents+de.lr b/src/content/tags/savory/contents+de.lr new file mode 100644 index 0000000..5bbdb84 --- /dev/null +++ b/src/content/tags/savory/contents+de.lr @@ -0,0 +1 @@ +name: Herzhaft \ No newline at end of file diff --git a/src/content/tags/savory/contents.lr b/src/content/tags/savory/contents.lr new file mode 100644 index 0000000..1ba3d21 --- /dev/null +++ b/src/content/tags/savory/contents.lr @@ -0,0 +1 @@ +name: Savory \ No newline at end of file diff --git a/src/content/tags/spread/contents+de.lr b/src/content/tags/spread/contents+de.lr index ca6fce9..92ef13f 100644 --- a/src/content/tags/spread/contents+de.lr +++ b/src/content/tags/spread/contents+de.lr @@ -1 +1 @@ -name: Aufstrich +name: Aufstrich \ No newline at end of file diff --git a/src/content/tags/spread/contents.lr b/src/content/tags/spread/contents.lr index 79b76e6..ce45cad 100644 --- a/src/content/tags/spread/contents.lr +++ b/src/content/tags/spread/contents.lr @@ -1 +1 @@ -name: Spread +name: Spread \ No newline at end of file diff --git a/src/content/tags/sweet/contents+de.lr b/src/content/tags/sweet/contents+de.lr index 7394311..429097b 100644 --- a/src/content/tags/sweet/contents+de.lr +++ b/src/content/tags/sweet/contents+de.lr @@ -1 +1 @@ -name: Süßes +name: Süß \ No newline at end of file diff --git a/src/content/tags/sweet/contents.lr b/src/content/tags/sweet/contents.lr index 48bbf33..c8b4e6f 100644 --- a/src/content/tags/sweet/contents.lr +++ b/src/content/tags/sweet/contents.lr @@ -1 +1 @@ -name: Sweet +name: Sweet \ No newline at end of file diff --git a/src/content/tags/xmas/contents+de.lr b/src/content/tags/xmas/contents+de.lr index 28bcedd..c8d7691 100644 --- a/src/content/tags/xmas/contents+de.lr +++ b/src/content/tags/xmas/contents+de.lr @@ -1 +1 @@ -name: Weihnachten +name: Weihnachten \ No newline at end of file diff --git a/src/content/tags/xmas/contents.lr b/src/content/tags/xmas/contents.lr index 2c0c422..23f0fdf 100644 --- a/src/content/tags/xmas/contents.lr +++ b/src/content/tags/xmas/contents.lr @@ -1 +1 @@ -name: Xmas +name: Xmas \ No newline at end of file diff --git a/src/databags/i18n+de.ini b/src/databags/i18n+de.ini index d98cd63..6edb6c6 100644 --- a/src/databags/i18n+de.ini +++ b/src/databags/i18n+de.ini @@ -1,11 +1,5 @@ [duration] label = Zeit -day = Tag -days = Tage -hour = Std -hours = Std -min = Min -mins = Min [yield] label = Menge @@ -17,6 +11,9 @@ easy = Einfach medium = Mittel hard = Schwer +[ingredients] +recipeLink = ⤳Rezept + [title] latest = Zuletzt hinzugefügt all_recipes = Alle Rezepte diff --git a/src/databags/i18n+en.ini b/src/databags/i18n+en.ini index fad9e5e..c7ce99b 100644 --- a/src/databags/i18n+en.ini +++ b/src/databags/i18n+en.ini @@ -1,11 +1,5 @@ [duration] label = Time -day = day -days = days -hour = hour -hours = hours -min = minutes -mins = min [yield] label = Yield @@ -17,6 +11,9 @@ easy = Easy medium = Medium hard = Hard +[ingredients] +recipeLink = ⤳recipe + [title] latest = Latest recipes all_recipes = All recipes diff --git a/src/models/recipe.ini b/src/models/recipe.ini index 6bf8e21..0663e0a 100644 --- a/src/models/recipe.ini +++ b/src/models/recipe.ini @@ -21,9 +21,8 @@ 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 +type = integer +addon_label = min [fields.difficulty] label = Difficulty diff --git a/src/models/settings.ini b/src/models/settings.ini index 00d9ca8..0c663ec 100644 --- a/src/models/settings.ini +++ b/src/models/settings.ini @@ -12,10 +12,10 @@ enabled = no [fields.measures] label = Measures -description = Comma separated list +description = Space 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 +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 @@ -26,9 +26,3 @@ type = boolean 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 diff --git a/src/packages/force-update b/src/packages/force-update new file mode 160000 index 0000000..2a1c729 --- /dev/null +++ b/src/packages/force-update @@ -0,0 +1 @@ +Subproject commit 2a1c729fce7052f54f75285f34252ca007145a07 diff --git a/src/packages/helper/lektor_helper.py b/src/packages/helper/lektor_helper.py index 85b5562..6c2d0db 100644 --- a/src/packages/helper/lektor_helper.py +++ b/src/packages/helper/lektor_helper.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from lektor.pluginsystem import Plugin +from lektor.pluginsystem import Plugin, get_plugin from lektor.databags import Databags +from markupsafe import Markup +from datetime import datetime import unicodedata -import os -import shutil # ------- # Sorting @@ -39,20 +39,17 @@ def noUmlaut(text): 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] + i = ['1/2', '1/3', '2/3', '1/4', '3/4', '1/8'].index(x) + res += [u'½', u'⅓', u'⅔', u'¼', u'¾', u'⅛'][i] except ValueError: - res += ' ' + x + if x in u'-–—': + res += u' - ' + else: + res += ' ' + x return res.lstrip() @@ -77,24 +74,6 @@ def updateSet_if(dic, parent, parentkey, value): 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 @@ -105,13 +84,14 @@ def splitIngredientLine(line): indices = [0, len(line)] for i, char in enumerate(line): if char.isspace(): - capture = False - indices[state] = i - state += 1 + if capture: + capture = False + indices[state] = i + state += 1 continue elif capture: continue - elif state == 1 and char in '0123456789-.,': + elif state == 1 and char in u'0123456789-–—.,': state -= 1 elif state > 1: break @@ -127,6 +107,9 @@ def parseIngredientLine(line, measureList=[], rep_frac=False): measure = line[idx[0]:idx[1]].lstrip() if measure.lower() in measureList: name = line[idx[1]:].lstrip() + # if name.startswith('of '): + # measure += ' of' + # name = name[3:] else: measure = '' name = line[idx[0]:].lstrip() @@ -136,19 +119,18 @@ def parseIngredientLine(line, measureList=[], rep_frac=False): 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)) +def replace_atref_urls(text, label=None): + if '@' not in text: + return text + result = list() + for x in text.split(): + if x[0] == '@': + x = x[1:] + result.append(u'{}'.format(x, label or x)) + else: + result.append(x) + return Markup(' '.join(result)) # ---------------- # Main entry point @@ -157,68 +139,56 @@ def groupByMergeCluster(dic, arr=[30, 60, 120], reverse=False): class HelperPlugin(Plugin): name = u'Helper' description = u'Some helper methods, filters, and templates.' - alt = None - availableTags = set() + buildTime = None + settings = dict() + translations = dict() # ----------- # Event hooks # ----------- - def on_before_build_all(self, builder, **extra): - # display only tags that contain at least one recipe + def processCLI(self, extra_flags): + useCache = bool(extra_flags.get('ENABLE_APPCACHE')) + plugin = get_plugin('force-update', self.env) + if plugin.enabled and not useCache: + plugin.enabled = False + print('AppCache: ' + ('ENABLED' if useCache else 'DISABLED')) + self.env.jinja_env.globals['ENABLE_APPCACHE'] = useCache + + def processSettings(self): + bag = Databags(self.env) pad = self.env.new_pad() - for r in pad.query('recipes'): - self.availableTags.update(r['tags']) + for alt in self.env.load_config().iter_alternatives(): + set = pad.get('settings', alt=alt) + self.translations[alt] = bag.lookup('i18n+' + alt) + self.settings[alt] = { + 'measures': set['measures'].lower().split(), + 'replFrac': set['replace_frac'] + } - 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_before_build_all(self, builder, **extra): + build_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print('Build time: ' + build_time) + self.env.jinja_env.globals['DATE_NOW'] = build_time + # update project settings once per build + self.processCLI(getattr(builder, 'extra_flags')) + self.processSettings() - def on_process_template_context(self, context, **extra): - self.alt = context['alt'] + # def on_process_template_context(self, context, **extra): + # pass def on_setup_env(self, **extra): - # self.env.load_config().iter_alternatives() - # pad = self.env.new_pad() - # pad.query('groupby', alt=alt) + def localizeDic(alt, partA, partB=None): + if alt not in self.translations: + raise RuntimeError( + 'localize() expects first parameter to be an alternate') + if partB is None: + partA, partB = partA.split('.', 1) + return self.translations[alt][partA][partB] - 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'] + def ingredientsForRecipe(recipe, alt='en'): + meaList = self.settings[alt]['measures'] + repFrac = self.settings[alt]['replFrac'] for line in recipe['ingredients']: line = line.strip() @@ -229,23 +199,21 @@ class HelperPlugin(Plugin): else: yield parseIngredientLine(line, meaList, repFrac) - def groupByAttribute(recipeList, attribute): + def groupByAttribute(recipeList, attribute, alt='en'): groups = dict() for recipe in recipeList: if attribute == 'ingredients': - for ing in ingredientsForRecipe(recipe): + for ing in ingredientsForRecipe(recipe, alt): 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['replaceAtRefURLs'] = replace_atref_urls 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 diff --git a/src/packages/time-duration/.gitignore b/src/packages/time-duration/.gitignore new file mode 100644 index 0000000..463960b --- /dev/null +++ b/src/packages/time-duration/.gitignore @@ -0,0 +1,5 @@ +dist +build +*.pyc +*.pyo +*.egg-info diff --git a/src/packages/time-duration/README.md b/src/packages/time-duration/README.md new file mode 100644 index 0000000..2a3deb4 --- /dev/null +++ b/src/packages/time-duration/README.md @@ -0,0 +1,4 @@ +# Time Duration + +This plugin converts integer numbers to a human readable duration. +E.g. 90 -> 1 hour 30 minutes \ No newline at end of file diff --git a/src/packages/time-duration/lektor_time_duration.py b/src/packages/time-duration/lektor_time_duration.py new file mode 100644 index 0000000..e77197c --- /dev/null +++ b/src/packages/time-duration/lektor_time_duration.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from lektor.pluginsystem import Plugin + +durationLocale = { + 'de': {'day': 'Tag', 'hour': 'Std', 'min': 'Min', + 'days': 'Tage', 'hours': 'Std', 'mins': 'Min'}, + 'en': {'day': 'day', 'hour': 'hour', 'min': 'min', + 'days': 'days', 'hours': 'hours', 'mins': 'min'} +} + +# ----------- +# Single Time + + +def pluralize(n, single, multi): + if n == 0: + return '' + return u'{} {}'.format(n, single if n == 1 else multi) + + +def to_duration(time, alt='en'): + time = int(time) if time else 0 + if (time <= 0): + return '' + days = time // (60 * 24) + time -= days * (60 * 24) + L = durationLocale[alt] + return ' '.join([ + pluralize(days, L['day'], L['days']), + pluralize(time // 60, L['hour'], L['hours']), + pluralize(time % 60, L['min'], L['mins'])]).strip() + +# ------------ +# Time Cluster + + +def to_time_in_cluster(time, cluster, alt='en'): + for idx, x in enumerate(cluster): + x = int(x) + if x == time: + if idx == 0: + timeB = to_duration(time, alt) + return '<' + timeB + else: + timeA = to_duration(cluster[idx - 1], alt) + timeB = to_duration(time - 1, alt) + return u'{} – {}'.format(timeA, timeB) + else: + return '>' + to_duration(cluster[-1], alt) + + +def find_in_cluster(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 + else: + key = clusterList[-1] + 1 + return key + + +def group_by_time_cluster(dic, arr=[30, 60, 120], reverse=False): + arr = sorted([int(x) for x in arr]) + groups = dict() + for key, recipes in dic: + key = find_in_cluster(key, arr) + if key == 0 and not reverse: + key = '' + try: + groups[key] + except KeyError: + groups[key] = set() + groups[key].update(recipes) + return sorted(groups.items(), reverse=bool(reverse)) + + +class TimeDurationPlugin(Plugin): + name = u'Time Duration' + description = u'Convert int to duration. E.g., 90 -> "1hr 30min".' + + def on_setup_env(self, **extra): + self.env.jinja_env.filters['duration'] = to_duration + self.env.jinja_env.filters['durationCluster'] = to_time_in_cluster + self.env.jinja_env.filters['groupTimeCluster'] = group_by_time_cluster diff --git a/src/packages/time-duration/setup.py b/src/packages/time-duration/setup.py new file mode 100644 index 0000000..ab52a49 --- /dev/null +++ b/src/packages/time-duration/setup.py @@ -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.*)') + +with open('lektor_time_duration.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-time-duration', + packages=find_packages(), + py_modules=['lektor_time_duration'], + # url='[link to your repository]', + version='0.1', + classifiers=[ + 'Framework :: Lektor', + 'Environment :: Plugins', + ], + entry_points={ + 'lektor.plugins': [ + 'time-duration = lektor_time_duration:TimeDurationPlugin', + ] + } +) diff --git a/src/templates/cache.manifest b/src/templates/cache.manifest new file mode 100644 index 0000000..05dc3a5 --- /dev/null +++ b/src/templates/cache.manifest @@ -0,0 +1,54 @@ +CACHE MANIFEST +# Date build: {{ DATE_NOW }} + +{%- macro _print_(items) -%} +{%- for item in items -%} + {{ item|replace('../', '', 1) }} +{% endfor -%} +{%- endmacro -%} + +{%- macro _add_(list, item) -%} +{{- list.append(item|url) or pass -}} +{%- endmacro -%} + +{%- set root = site.get('/', this.alt) -%} + +{%- set assetList = [] -%} +{%- for asset in root.pad.asset_root.children recursive -%} + {%- if asset.__class__.__name__ != 'Directory' -%} + {{- _add_(assetList, asset) -}} + {%- endif -%} + {{- loop(asset.children) -}} +{%- endfor -%} + +{%- set cacheList = [] -%} +{{- _add_(cacheList, root) -}} +{%- for x in root.children if x != this recursive -%} + {{- _add_(cacheList, x) -}} + + {%- set pg = x.datamodel.pagination_config -%} + {%- if pg.enabled -%} + {%- for page in range(2, pg.count_pages(x) + 1) -%} + {{- _add_(cacheList, pg.get_record_for_page(x, page)) -}} + {%- endfor -%} + {%- endif -%} + + {% set img = x.attachments.images|sort(attribute='record_label')|first -%} + {%- if img -%} + {{- _add_(cacheList, img.thumbnail(200, 150, 'crop')) -}} + {%- endif -%} + + {%- if x.datamodel.has_own_children -%} + {{- loop(x.children) -}} + {%- endif -%} +{%- endfor -%} + +{#- Generate cache file index #} +# static +{{ _print_(assetList) -}} +# index +{{ _print_(cacheList) -}} + +{#- All other requests are forwarded #} +NETWORK: +* \ No newline at end of file diff --git a/src/templates/cluster.html b/src/templates/cluster.html index 96dd315..2d365d8 100644 --- a/src/templates/cluster.html +++ b/src/templates/cluster.html @@ -8,10 +8,10 @@ {%- set sortType = this.xdata + [''] -%} {%- endif -%} - {%- set all = site.query('/recipes', this.alt) | groupByAttribute(this.group_key) | groupSort(sortType, this.reverse_order) -%} + {%- set all = site.query('/recipes', this.alt) | groupByAttribute(this.group_key, this.alt) | groupSort(sortType, this.reverse_order) -%} {%- if this.group_key == 'time' -%} - {%- set all = all | groupMergeCluster(this.xdata, this.reverse_order) -%} + {%- set all = all | groupTimeCluster(this.xdata, this.reverse_order) -%} {%- endif -%}

{{ this.name }}

@@ -22,9 +22,9 @@ {%- elif not attrib -%} {{ this.null_fallback }} {%- elif this.group_key == 'time' -%} - {{ attrib | duration(this.xdata) }} + {{ attrib | durationCluster(this.xdata, this.alt) }} {%- elif this.group_key == 'difficulty' -%} - {{ localize('difficulty', attrib) }} + {{ localize(this.alt, 'difficulty', attrib) }} {%- else -%} {{ attrib }} {%- endif -%} diff --git a/src/templates/layout.html b/src/templates/layout.html index 233aff8..8b74a4b 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -1,31 +1,39 @@ - - - - - -{% block title %}Welcome{% endblock %} · recipe lekture - +{%- if ENABLE_APPCACHE %} + +{% endif -%} + + + + + + + + + + {% block title %}Welcome{% endblock %} · recipe lekture + + {#- ontouchstart="" #}
+ {%- if ENABLE_APPCACHE %} + + {%- endif %}
- {%- 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 -%} {{ tag.name }} - {%- endif -%} {%- endfor %}
diff --git a/src/templates/macros/recipes.html b/src/templates/macros/recipes.html index 95d4be3..46bb165 100644 --- a/src/templates/macros/recipes.html +++ b/src/templates/macros/recipes.html @@ -5,13 +5,24 @@ {%- set img = recipe.attachments.images|sort(attribute='record_label')|first -%} {#--#}
-
- {%- if img -%} - - {%- else -%} - No Image - {%- endif -%} + + {#- overlay on hover and always-visible icons #} +
+
{{ recipe.time|duration(recipe.alt) }}
+
+ {%- if 'raw' in recipe.tags -%}{%- endif -%} + {%- if 'glutenfree' in recipe.tags -%}{%- endif -%} +
+ + {#- show image or placeholder text #} + {% if img -%} + + {%- else -%} +
No Image
+ {%- endif -%} + + {#- recipe title #}

{{ recipe.name }}

{#--#}
diff --git a/src/templates/recipe.html b/src/templates/recipe.html index da24632..0aecc1f 100644 --- a/src/templates/recipe.html +++ b/src/templates/recipe.html @@ -2,37 +2,42 @@ {% block title %}{{ this.name }}{% endblock %} {% block body %}
+ {% if this.source -%}
- ⤳ {{ this.source.host }} + {%- if this.source.host -%} + ⤳ {{ this.source.host }} + {%- else -%} + ⤳ {{ this.source }} + {%- endif -%}
{% endif %}

{{ this.name }}

- +
{{ this.rating|rating }}
{%- if this.difficulty %} - {{ localize('difficulty', this.difficulty) }} + {{ localize(this.alt, 'difficulty', this.difficulty) }} {%- else %} - {{ localize('difficulty._unset') }} + {{ localize(this.alt, 'difficulty._unset') }} {%- endif %}
-
{{ localize('duration.label') }}: {{ this.time|duration if this.time else '—' }}
-
{{ localize('yield.label') }}: {{ this.yield if this.yield else '—' }}
+
{{ localize(this.alt, 'duration.label') }}: {{ this.time|duration(this.alt) if this.time else '—' }}
+
{{ localize(this.alt, 'yield.label') }}: {{ this.yield or '—' }}
-

{{ localize('title.ingredients') }}:

+

{{ localize(this.alt, 'title.ingredients') }}:

    - {%- for ing in this|enumIngredients %} + {%- for ing in this|enumIngredients(this.alt) %} {%- if ing['group'] %}
  • {{ ing['group'] }}
  • {%- else %} @@ -41,7 +46,7 @@ {%- if ing['measure'] %}{{ ing['measure'] }} {% endif -%} {{ ing['name'] }} {%- if ing['note'] -%} - {{ ', ' ~ ing['note'] }} + {{ ', ' ~ ing['note'] | replaceAtRefURLs(label=localize(this.alt, 'ingredients.recipeLink')) }} {%- endif -%} {%- endif %} @@ -50,9 +55,9 @@
-

{{ localize('title.directions') }}:

+

{{ localize(this.alt, 'title.directions') }}:

{% if site.get('settings', alt=this.alt)['replace_temp'] -%} - {{ this.directions|string|replace('°C', '℃')|replace('°F', '℉')|markdown }} + {{ this.directions.html|replace('°C', '℃')|replace('°F', '℉') }} {% else -%} {{ this.directions }} {% endif -%} diff --git a/src/templates/recipes.html b/src/templates/recipes.html index 0e4bbd7..5184f4f 100644 --- a/src/templates/recipes.html +++ b/src/templates/recipes.html @@ -1,9 +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 title %}{{ localize(this.alt, 'title.recipes') }}{% endblock %} {% block body %} -

{{ localize('title.recipes') }}

+

{{ localize(this.alt, 'title.recipes') }}

{{ render_recipe_list(this.pagination.items) }} {{ render_pagination_all(this.pagination) }} {% endblock %} \ No newline at end of file diff --git a/src/templates/root.html b/src/templates/root.html index 9d9cca5..04150b1 100644 --- a/src/templates/root.html +++ b/src/templates/root.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% from "macros/recipes.html" import render_recipe_list %} {% block body %} -

{{ localize('title.latest') }}

+

{{ localize(this.alt, 'title.latest') }}

{{ render_recipe_list(site.query('recipes', this.alt) | sort(attribute='date', reverse=True), limit=6) }}