refactor: full rewrite of app logic (better, faster, type-safe)
This commit is contained in:
@@ -1,9 +1,3 @@
|
|||||||
sort_key: 10
|
sort_key: 10
|
||||||
---
|
---
|
||||||
group_key: difficulty
|
group_key: difficulty
|
||||||
---
|
|
||||||
xdata:
|
|
||||||
|
|
||||||
easy
|
|
||||||
medium
|
|
||||||
hard
|
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ sort_key: 15
|
|||||||
group_key: rating
|
group_key: rating
|
||||||
---
|
---
|
||||||
reverse_order: yes
|
reverse_order: yes
|
||||||
|
---
|
||||||
|
null_fallback: ☆☆☆
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
sort_key: 20
|
sort_key: 20
|
||||||
---
|
---
|
||||||
group_key: time
|
group_key: time
|
||||||
---
|
|
||||||
xdata:
|
|
||||||
|
|
||||||
15
|
|
||||||
30
|
|
||||||
60
|
|
||||||
120
|
|
||||||
180
|
|
||||||
360
|
|
||||||
1440
|
|
||||||
|
|||||||
@@ -5,3 +5,22 @@ _hidden: yes
|
|||||||
replace_frac: yes
|
replace_frac: yes
|
||||||
---
|
---
|
||||||
replace_temp: yes
|
replace_temp: yes
|
||||||
|
---
|
||||||
|
replace_frac_map:
|
||||||
|
|
||||||
|
1/2 ½
|
||||||
|
1/3 ⅓
|
||||||
|
2/3 ⅔
|
||||||
|
1/4 ¼
|
||||||
|
3/4 ¾
|
||||||
|
1/8 ⅛
|
||||||
|
---
|
||||||
|
duration_cluster:
|
||||||
|
|
||||||
|
15
|
||||||
|
30
|
||||||
|
60
|
||||||
|
120
|
||||||
|
180
|
||||||
|
360
|
||||||
|
1440
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
[duration]
|
[duration]
|
||||||
label = Zeit
|
label = Zeit
|
||||||
|
day = Tag
|
||||||
|
days = Tage
|
||||||
|
hour = Std
|
||||||
|
hours = Std
|
||||||
|
min = Min
|
||||||
|
mins = Min
|
||||||
|
|
||||||
[yield]
|
[yield]
|
||||||
label = Menge
|
label = Menge
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
[duration]
|
[duration]
|
||||||
label = Time
|
label = Time
|
||||||
|
day = day
|
||||||
|
days = days
|
||||||
|
hour = hour
|
||||||
|
hours = hours
|
||||||
|
min = min
|
||||||
|
mins = min
|
||||||
|
|
||||||
[yield]
|
[yield]
|
||||||
label = Yield
|
label = Yield
|
||||||
|
|||||||
@@ -39,9 +39,3 @@ label = Reverse Sort
|
|||||||
width = 1/5
|
width = 1/5
|
||||||
type = boolean
|
type = boolean
|
||||||
alts_enabled = false
|
alts_enabled = false
|
||||||
|
|
||||||
[fields.xdata]
|
|
||||||
label = Extended Data
|
|
||||||
description = Used for ordinal sort order or merging integer cluster
|
|
||||||
width = 1/2
|
|
||||||
type = strings
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type = string
|
|||||||
label = Ingredients / Zutaten
|
label = Ingredients / Zutaten
|
||||||
description = 42 g Ingredient, Notes (add additional measures in settings)
|
description = 42 g Ingredient, Notes (add additional measures in settings)
|
||||||
width = 2/3
|
width = 2/3
|
||||||
type = strings
|
type = ingredientslist
|
||||||
|
|
||||||
[fields.tags]
|
[fields.tags]
|
||||||
label = Tags / Kategorie
|
label = Tags / Kategorie
|
||||||
|
|||||||
@@ -28,3 +28,19 @@ label = Replace 1/2 with ½, ⅔, etc.
|
|||||||
width = 1/5
|
width = 1/5
|
||||||
type = boolean
|
type = boolean
|
||||||
alts_enabled = false
|
alts_enabled = false
|
||||||
|
|
||||||
|
[fields.replace_frac_map]
|
||||||
|
label = Ingredient replacements for numbers and fractions
|
||||||
|
description = "Space separated list (default: 1/2 ½ 1/3 ⅓ 2/3 ⅔ 1/4 ¼ 3/4 ¾ 1/8 ⅛)"
|
||||||
|
width = 2/5
|
||||||
|
type = text
|
||||||
|
default = 1/2 ½ 1/3 ⅓ 2/3 ⅔ 1/4 ¼ 3/4 ¾ 1/8 ⅛
|
||||||
|
alts_enabled = false
|
||||||
|
|
||||||
|
[fields.duration_cluster]
|
||||||
|
label = Time duration splits
|
||||||
|
description = "list of int ('10 30' creates three clusters: 0-9, 10-29, >30)"
|
||||||
|
width = 2/5
|
||||||
|
type = text
|
||||||
|
default = 15, 30, 60, 120, 180, 360, 1440
|
||||||
|
alts_enabled = false
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Helper
|
|
||||||
|
|
||||||
Just some python functions that are necessary for the project.
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from lektor.pluginsystem import Plugin, get_plugin
|
|
||||||
from lektor.databags import Databags
|
|
||||||
from markupsafe import Markup
|
|
||||||
from datetime import datetime
|
|
||||||
import unicodedata
|
|
||||||
import lektor_html_to_tex as tex
|
|
||||||
|
|
||||||
# -------
|
|
||||||
# Sorting
|
|
||||||
|
|
||||||
|
|
||||||
def sorted_images(obj, attr='record_label'):
|
|
||||||
return sorted(obj.attachments.images, key=lambda x: getattr(x, attr))
|
|
||||||
|
|
||||||
|
|
||||||
def title_image(self, attr='record_label', small=False):
|
|
||||||
img = (sorted_images(self, attr) or [None])[0]
|
|
||||||
if img and small:
|
|
||||||
img = img.thumbnail(200, 150, mode='crop')
|
|
||||||
return img
|
|
||||||
|
|
||||||
|
|
||||||
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 replaceFractions(txt):
|
|
||||||
res = ' '
|
|
||||||
for c in u'-–—':
|
|
||||||
if c in txt:
|
|
||||||
txt = txt.replace(c, ' – ')
|
|
||||||
for x in txt.split():
|
|
||||||
if x == '':
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
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:
|
|
||||||
if x == '–':
|
|
||||||
res += '–'
|
|
||||||
elif res[-1:] == '–':
|
|
||||||
res += x
|
|
||||||
else:
|
|
||||||
res += ' ' + x
|
|
||||||
return res.lstrip(' ')
|
|
||||||
|
|
||||||
|
|
||||||
def numFillWithText(num, fill=u'★', empty=u'☆', total=3):
|
|
||||||
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)
|
|
||||||
|
|
||||||
# --------------------
|
|
||||||
# Ingredient splitting
|
|
||||||
|
|
||||||
|
|
||||||
def splitIngredientLine(line):
|
|
||||||
state = 1
|
|
||||||
capture = False
|
|
||||||
indices = [0, len(line)]
|
|
||||||
for i, char in enumerate(line):
|
|
||||||
if char.isspace():
|
|
||||||
if capture:
|
|
||||||
capture = False
|
|
||||||
indices[state] = i
|
|
||||||
state += 1
|
|
||||||
continue
|
|
||||||
elif capture:
|
|
||||||
continue
|
|
||||||
elif state == 1 and char in u'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()
|
|
||||||
# if name.startswith('of '):
|
|
||||||
# measure += ' of'
|
|
||||||
# name = name[3:]
|
|
||||||
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}
|
|
||||||
|
|
||||||
|
|
||||||
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'<a href="{}">{}</a>'.format(x, label or x))
|
|
||||||
else:
|
|
||||||
result.append(x)
|
|
||||||
return Markup(' '.join(result))
|
|
||||||
|
|
||||||
# ----------------
|
|
||||||
# Main entry point
|
|
||||||
|
|
||||||
|
|
||||||
class HelperPlugin(Plugin):
|
|
||||||
name = u'Helper'
|
|
||||||
description = u'Some helper methods, filters, and templates.'
|
|
||||||
buildTime = None
|
|
||||||
settings = dict()
|
|
||||||
translations = dict()
|
|
||||||
|
|
||||||
# -----------
|
|
||||||
# Event hooks
|
|
||||||
# -----------
|
|
||||||
|
|
||||||
def on_before_build_all(self, builder, **extra):
|
|
||||||
# update project settings once per build
|
|
||||||
bag = Databags(self.env)
|
|
||||||
pad = self.env.new_pad()
|
|
||||||
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_process_template_context(self, context, **extra):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
def on_setup_env(self, **extra):
|
|
||||||
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 ingredientsForRecipe(recipe, alt='en', mode='raw'):
|
|
||||||
meaList = self.settings[alt]['measures']
|
|
||||||
repFrac = self.settings[alt]['replFrac']
|
|
||||||
|
|
||||||
for line in recipe['ingredients']:
|
|
||||||
line = line.strip()
|
|
||||||
if mode == 'tex':
|
|
||||||
line = tex.raw_text_to_tex(line)
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
elif line.endswith(':'):
|
|
||||||
yield {'group': line}
|
|
||||||
else:
|
|
||||||
yield parseIngredientLine(line, meaList, repFrac)
|
|
||||||
|
|
||||||
def groupByAttribute(recipeList, attribute, alt='en'):
|
|
||||||
groups = dict()
|
|
||||||
for recipe in recipeList:
|
|
||||||
if attribute == 'ingredients':
|
|
||||||
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['sorted_images'] = sorted_images
|
|
||||||
self.env.jinja_env.filters['title_image'] = title_image
|
|
||||||
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.globals['localize'] = localizeDic
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
5
src/packages/html-to-tex/.gitignore
vendored
5
src/packages/html-to-tex/.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
dist
|
|
||||||
build
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.egg-info
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# character set translations for LaTex special chars
|
|
||||||
s?>.?>?g
|
|
||||||
s?<.?<?g
|
|
||||||
s?\\?\\backslash ?g
|
|
||||||
s?{?\\{?g
|
|
||||||
s?}?\\}?g
|
|
||||||
s?%?\\%?g
|
|
||||||
s?\$?\\$?g
|
|
||||||
s?&?\\&?g
|
|
||||||
s?#?\\#?g
|
|
||||||
s?_?\\_?g
|
|
||||||
s?~?\\~?g
|
|
||||||
s?\^?\\^?g
|
|
||||||
s? ?~?g
|
|
||||||
# Paragraph borders
|
|
||||||
s?<p>?\\par ?g
|
|
||||||
s?</p>??g
|
|
||||||
# Headings
|
|
||||||
s?<title>\([^<]*\)</title>?\\section*{\1}?g
|
|
||||||
s?<hn>?\\part{?g
|
|
||||||
s?</hn>?}?g
|
|
||||||
s?<h1>?\\section*{?g
|
|
||||||
s?</h[0-9]>?}?g
|
|
||||||
s?<h2>?\\subsection*{?g
|
|
||||||
s?<h3>?\\subsubsection*{?g
|
|
||||||
s?<h4>?\\paragraph*{?g
|
|
||||||
s?<h5>?\\paragraph*{?g
|
|
||||||
s?<h6>?\\subparagraph*{?g
|
|
||||||
# UL is itemize
|
|
||||||
s?<ul>?\\begin{itemize}?g
|
|
||||||
s?</ul>?\\end{itemize}?g
|
|
||||||
s?<ol>?\\begin{enumerate}?g
|
|
||||||
s?</ol>?\\end{enumerate}?g
|
|
||||||
s?<li>?\\item ?g
|
|
||||||
s?</li>??g
|
|
||||||
# DL is description
|
|
||||||
s?<dl>?\\begin{description}?g
|
|
||||||
s?</dl>?\\end{description}?g
|
|
||||||
# closing delimiter for DT is first < or end of line which ever comes first NO
|
|
||||||
#s?<dt>\([^<]*\)<?\\item[\1]<?g
|
|
||||||
#s?<dt>\([^<]*\)$?\\item[\1]?g
|
|
||||||
#s?<dd>??g
|
|
||||||
#s?<dt>?\\item[?g
|
|
||||||
#s?<dd>?]?g
|
|
||||||
s?<dt>\([^<]*\)</dt>?\\item[\1]?g
|
|
||||||
s?<dd>??g
|
|
||||||
s?</dd>??g
|
|
||||||
# Italics
|
|
||||||
s?<it>\([^<]*\)</it>?{\\it \1}?g
|
|
||||||
s?<em>\([^<]*\)</em>?{\\it \1}?g
|
|
||||||
s?<b>\([^<]*\)</b>?{\\bf \1}?g
|
|
||||||
s?<strong>\([^<]*\)</strong>?{\\bf \1}?g
|
|
||||||
# recipe specific
|
|
||||||
s?<a href="../\([^"/]*\)/*">\([^<]*\)</a>?\\recipelink{\1}{\2}?g
|
|
||||||
# Get rid of Anchors
|
|
||||||
s?<a href="\(http[^"]*\)">\([^<]*\)</a>?\\external{\1}{\2}?g
|
|
||||||
s?<a[^>]*>??g
|
|
||||||
s?</a>??g
|
|
||||||
# quotes (replace after href)
|
|
||||||
s?\([[:space:]]\)"\([^[:space:]]\)?\1``\2?g
|
|
||||||
s?"?''?g
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from lektor.pluginsystem import Plugin # , get_plugin
|
|
||||||
import mistune
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import subprocess as shell
|
|
||||||
import lektor_helper as helper
|
|
||||||
import lektor_time_duration as timedur
|
|
||||||
|
|
||||||
my_dir = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
f_sed_a = os.path.join(my_dir, 'html2latex.sed')
|
|
||||||
|
|
||||||
|
|
||||||
def sed_repl_tex(f_sed, content):
|
|
||||||
with shell.Popen(('echo', content), stdout=shell.PIPE) as ps:
|
|
||||||
o = shell.check_output(['sed', '-f', f_sed], stdin=ps.stdout)
|
|
||||||
ps.wait()
|
|
||||||
return o.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def html_to_tex(html):
|
|
||||||
return sed_repl_tex(f_sed_a, html)
|
|
||||||
|
|
||||||
|
|
||||||
def raw_text_to_tex(text):
|
|
||||||
text = text.replace('\\', '\\backslash ').replace(' ', '~')
|
|
||||||
for c in '}{%$&#_~^':
|
|
||||||
if c in text:
|
|
||||||
text = text.replace(c, '\\' + c)
|
|
||||||
return text
|
|
||||||
# return sed_repl_tex(f_sed_b, text)
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeToTex(object):
|
|
||||||
def __init__(self, enumerator):
|
|
||||||
super(RecipeToTex, self).__init__()
|
|
||||||
bases = tuple([]) + (mistune.Renderer,)
|
|
||||||
renderer_cls = type('renderer_cls', bases, {})
|
|
||||||
renderer = renderer_cls(escape=False)
|
|
||||||
self.mdparser = mistune.Markdown(renderer, escape=False)
|
|
||||||
self.enumerator = enumerator
|
|
||||||
|
|
||||||
def make(self, recipe, alt):
|
|
||||||
ingredients = ''
|
|
||||||
for x in self.enumerator(recipe, alt, mode='tex'):
|
|
||||||
ingredients += '\n' + self.process_ingredient(x, alt)
|
|
||||||
ingredients = ingredients[1:] or '\\item '
|
|
||||||
self.mdparser.renderer.record = recipe
|
|
||||||
instructions_html = self.mdparser(recipe['directions'].source)
|
|
||||||
instructions = sed_repl_tex(f_sed_a, instructions_html)
|
|
||||||
return self.process_recipe(recipe, alt, ingredients, instructions)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def process_recipe(self, recipe, alt, ingredients, instructions):
|
|
||||||
img = helper.title_image(recipe)
|
|
||||||
if img:
|
|
||||||
img = img.path[:-4] + '@200x150_crop' + img.path[-4:]
|
|
||||||
duration = recipe['time']
|
|
||||||
host = recipe['source'].host
|
|
||||||
time = timedur.to_duration(duration, alt) if duration else ''
|
|
||||||
srcUrl = raw_text_to_tex(str(recipe['source'])) or ''
|
|
||||||
srcHost = raw_text_to_tex(host) if host else ''
|
|
||||||
return f'''
|
|
||||||
\\newrecipe{{{recipe['_slug']}}}{{{raw_text_to_tex(recipe['name'])}}}
|
|
||||||
\\meta{{{time}}}{{{recipe['yield'] or ''}}}
|
|
||||||
\\footer{{{srcUrl}}}{{{srcHost}}}
|
|
||||||
|
|
||||||
\\begin{{ingredients}}{{{img or ''}}}
|
|
||||||
{ingredients}
|
|
||||||
\\end{{ingredients}}
|
|
||||||
|
|
||||||
{instructions}
|
|
||||||
'''
|
|
||||||
|
|
||||||
def process_ingredient(self, ing, alt):
|
|
||||||
grp = ing.get('group')
|
|
||||||
if grp:
|
|
||||||
return f'\\ingGroup{{{grp}}}'
|
|
||||||
|
|
||||||
ret = ''
|
|
||||||
val = ing['value']
|
|
||||||
meas = ing['measure']
|
|
||||||
note = ing['note']
|
|
||||||
ret += '\\item'
|
|
||||||
if val or meas:
|
|
||||||
sep = '~' if val and meas else ''
|
|
||||||
ret += '[{}{}{}]'.format(val or '', sep, meas or '')
|
|
||||||
ret += f' \\ingName{{{ ing["name"] }}}' # keep space in front
|
|
||||||
if note:
|
|
||||||
ret += '\\ingDetail{'
|
|
||||||
for prt in note.split():
|
|
||||||
if prt.startswith('@../'):
|
|
||||||
ret += f' \\pagelink{{{ prt[4:].rstrip("/") }}}'
|
|
||||||
else:
|
|
||||||
ret += ' ' + prt
|
|
||||||
ret += '}'
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class HtmlToTex(Plugin):
|
|
||||||
name = u'HTML to TEX converter'
|
|
||||||
description = u'Will convert html formatted text to (la)tex format.'
|
|
||||||
|
|
||||||
def on_after_prune(self, builder, **extra):
|
|
||||||
maketex = bool(builder.extra_flags.get('ENABLE_PDF_EXPORT'))
|
|
||||||
print('PDF Export: ' + ('ENABLED' if maketex else 'DISABLED'))
|
|
||||||
if not maketex:
|
|
||||||
return
|
|
||||||
|
|
||||||
dest_dir = my_dir
|
|
||||||
for x in range(3):
|
|
||||||
dest_dir = os.path.dirname(dest_dir)
|
|
||||||
dest_dir = os.path.join(dest_dir, 'extras', 'pdf-export')
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
print('PDF Export: generate tex files')
|
|
||||||
with open(os.path.join(dest_dir, 'dyn-builddir.tex'), 'w') as f:
|
|
||||||
# Export current build dir (for image search)
|
|
||||||
f.write('\\def\\builddir{' + builder.destination_path + '}')
|
|
||||||
parser = RecipeToTex(self.env.jinja_env.filters['enumIngredients'])
|
|
||||||
for alt in self.env.load_config().list_alternatives():
|
|
||||||
tex = ''
|
|
||||||
for recipe in builder.pad.get('/recipes', alt=alt).children:
|
|
||||||
tex += parser.make(recipe, alt)
|
|
||||||
fname = os.path.join(dest_dir, f'dyn-recipes-{alt}.tex')
|
|
||||||
with open(fname, 'w') as f:
|
|
||||||
f.write(tex)
|
|
||||||
print('PDF Export: done in %.2f sec' % (time.time() - start_time))
|
|
||||||
|
|
||||||
def on_setup_env(self, **extra):
|
|
||||||
self.env.jinja_env.filters['raw_text_to_tex'] = raw_text_to_tex
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='lektor-html-to-tex',
|
|
||||||
py_modules=['lektor_html-to-tex'],
|
|
||||||
version='1.0',
|
|
||||||
entry_points={
|
|
||||||
'lektor.plugins': [
|
|
||||||
'html-to-tex = lektor_html_to_tex:HtmlToTex',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
1
src/packages/main/lektor_main/__init__.py
Normal file
1
src/packages/main/lektor_main/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .plugin import MainPlugin # noqa: F401
|
||||||
66
src/packages/main/lektor_main/durationcluster.py
Normal file
66
src/packages/main/lektor_main/durationcluster.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import List, Optional, Dict, Union
|
||||||
|
|
||||||
|
|
||||||
|
def int_to_cluster(time: Union[str, int, None], clusters: List[int]) \
|
||||||
|
-> Optional[int]:
|
||||||
|
'''
|
||||||
|
Choose cluster where time >= X and time < X+1
|
||||||
|
Example: `[30, 60, 120]` -> (0-29, 30-59, 60-119, 120+)
|
||||||
|
Return values are: 36 -> 60, 12 -> 30, 120 -> 121.
|
||||||
|
'''
|
||||||
|
time = int(time) if time else 0
|
||||||
|
if not time:
|
||||||
|
return None
|
||||||
|
for x in clusters:
|
||||||
|
if x > time:
|
||||||
|
return x
|
||||||
|
return clusters[-1] + 1
|
||||||
|
|
||||||
|
|
||||||
|
def cluster_as_str(
|
||||||
|
time: Union[str, int, None],
|
||||||
|
clusters: List[int],
|
||||||
|
translations: Dict[str, str],
|
||||||
|
) -> Optional[str]:
|
||||||
|
''' Return descriptive duration range; 30 -> "15 min – 29 min". '''
|
||||||
|
time = int(time) if time else 0
|
||||||
|
if not time:
|
||||||
|
return None
|
||||||
|
for idx, x in enumerate(clusters):
|
||||||
|
if x == time:
|
||||||
|
if idx == 0:
|
||||||
|
return '<' + human_readable_duration(time, translations)
|
||||||
|
timeA = human_readable_duration(clusters[idx - 1], translations)
|
||||||
|
timeB = human_readable_duration(time - 1, translations)
|
||||||
|
return u'{} – {}'.format(timeA, timeB)
|
||||||
|
return '>' + human_readable_duration(clusters[-1], translations)
|
||||||
|
|
||||||
|
|
||||||
|
def human_readable_duration(
|
||||||
|
time: Union[str, int, None], translations: Dict[str, str]
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Take an arbitrary int and return readable duration string.
|
||||||
|
For example, 16 -> "16 mins", 121 -> "2 hours 1 min"
|
||||||
|
'''
|
||||||
|
time = int(time) if time else 0
|
||||||
|
if (time <= 0):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
days = time // (60 * 24)
|
||||||
|
time -= days * (60 * 24)
|
||||||
|
hours = time // 60
|
||||||
|
mins = time - hours * 60
|
||||||
|
ret = ''
|
||||||
|
if days:
|
||||||
|
ret += f'{days} {translations["day" if days == 1 else "days"]}'
|
||||||
|
if hours:
|
||||||
|
if ret:
|
||||||
|
ret += ' '
|
||||||
|
ret += f'{hours} {translations["hour" if hours == 1 else "hours"]}'
|
||||||
|
if mins:
|
||||||
|
if ret:
|
||||||
|
ret += ' '
|
||||||
|
ret += f'{mins} {translations["min" if mins == 1 else "mins"]}'
|
||||||
|
return ret
|
||||||
115
src/packages/main/lektor_main/ingredients.py
Normal file
115
src/packages/main/lektor_main/ingredients.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from lektor.types import Type
|
||||||
|
from typing import TYPE_CHECKING, List, Optional, Any
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from lektor.db import Record
|
||||||
|
from lektor.types.base import RawValue
|
||||||
|
from .utils import replaceFractions
|
||||||
|
from .settings import IngredientConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientEntry:
|
||||||
|
@property
|
||||||
|
def isGroup(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def isIngredient(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientGroup(IngredientEntry):
|
||||||
|
@property
|
||||||
|
def isGroup(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, line: str) -> None:
|
||||||
|
self.name = line
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return '<IngredientGroup name="{}">'.format(self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class Ingredient(IngredientEntry):
|
||||||
|
@property
|
||||||
|
def isIngredient(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, line: str, conf: IngredientConfig) -> None:
|
||||||
|
idx = Ingredient.split_raw(line)
|
||||||
|
# parse quantity
|
||||||
|
self.quantity = line[:idx[0]]
|
||||||
|
if conf.frac_map:
|
||||||
|
self.quantity = replaceFractions(self.quantity, conf.frac_map)
|
||||||
|
# parse unit
|
||||||
|
unit = line[idx[0]:idx[1]].lstrip()
|
||||||
|
if unit in conf.units:
|
||||||
|
name = line[idx[1]:].lstrip()
|
||||||
|
else:
|
||||||
|
unit, name = '', line[idx[0]:].lstrip()
|
||||||
|
self.unit = unit
|
||||||
|
# parse ingredient name + note
|
||||||
|
note = ''
|
||||||
|
name_note = name.split(',', 1)
|
||||||
|
if len(name_note) > 1:
|
||||||
|
name, note = [x.strip() for x in name_note]
|
||||||
|
self.name = name
|
||||||
|
self.note = note
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_raw(line: str) -> List[int]:
|
||||||
|
state = 1
|
||||||
|
capture = False
|
||||||
|
indices = [0, len(line)]
|
||||||
|
for i, char in enumerate(line):
|
||||||
|
if char.isspace():
|
||||||
|
if capture:
|
||||||
|
capture = False
|
||||||
|
indices[state] = i
|
||||||
|
state += 1
|
||||||
|
continue
|
||||||
|
elif capture:
|
||||||
|
continue
|
||||||
|
elif state == 1 and char in u'0123456789-–—.,':
|
||||||
|
state -= 1
|
||||||
|
elif state > 1:
|
||||||
|
break
|
||||||
|
capture = True
|
||||||
|
return indices
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return '<Ingredient "{}" qty="{}" unit="{}" note="{}">'.format(
|
||||||
|
self.name, self.quantity, self.unit, self.note)
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientsDescriptor:
|
||||||
|
def __init__(self, raw: Optional[str]) -> None:
|
||||||
|
self.raw = raw
|
||||||
|
|
||||||
|
def parse(self, raw: str, record: 'Record') -> List[IngredientEntry]:
|
||||||
|
conf = IngredientConfig.of(record)
|
||||||
|
ret = [] # type: List[IngredientEntry]
|
||||||
|
for line in raw.splitlines(True): # we need to strip anyway
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
if line.endswith(':'):
|
||||||
|
ret.append(IngredientGroup(line.rstrip(':')))
|
||||||
|
else:
|
||||||
|
ret.append(Ingredient(line, conf))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __get__(self, record: 'Record', _: Any = None) -> Any:
|
||||||
|
if record is None:
|
||||||
|
return self
|
||||||
|
if not self.raw:
|
||||||
|
return []
|
||||||
|
data = self.parse(self.raw, record)
|
||||||
|
del self.raw
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientsListType(Type):
|
||||||
|
widget = 'multiline-text'
|
||||||
|
|
||||||
|
def value_from_raw(self, raw: 'RawValue') -> IngredientsDescriptor:
|
||||||
|
return IngredientsDescriptor(raw.value or None)
|
||||||
302
src/packages/main/lektor_main/latex.py
Normal file
302
src/packages/main/lektor_main/latex.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from lektor.build_programs import BuildProgram # subclass
|
||||||
|
from lektor.reporter import reporter # build, verbosity
|
||||||
|
from lektor.sourceobj import VirtualSourceObject # subclass
|
||||||
|
import click
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil # which, copyfile, rmtree
|
||||||
|
import subprocess as shell # run, DEVNULL
|
||||||
|
from weakref import ref
|
||||||
|
from typing import TYPE_CHECKING, List, Tuple, Generator
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from lektor.builder import Artifact, Builder, BuildState
|
||||||
|
from lektor.db import Record
|
||||||
|
from lektor.environment import Environment
|
||||||
|
from .utils import lookup_template_path
|
||||||
|
|
||||||
|
VPATH = 'LatexPDF'
|
||||||
|
TEXER = 'lualatex'
|
||||||
|
PDF_OUT_DIR = 'out'
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# Sed re-format html as latex
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
HTML_TO_LATEX_RULES = [
|
||||||
|
# character set translations for LaTex special chars
|
||||||
|
(r'>.', r'>'),
|
||||||
|
(r'<.', r'<'),
|
||||||
|
(r'\\', r'\\backslash '),
|
||||||
|
(r'{', r'\\{'),
|
||||||
|
(r'}', r'\\}'),
|
||||||
|
(r'%', r'\\%'),
|
||||||
|
(r'\$', r'\\$'),
|
||||||
|
(r'&', r'\\&'),
|
||||||
|
(r'#', r'\\#'),
|
||||||
|
(r'_', r'\\_'),
|
||||||
|
(r'~', r'\\~'),
|
||||||
|
(r'\^', r'\\^'),
|
||||||
|
(r' ', r'~'),
|
||||||
|
# Paragraph borders
|
||||||
|
(r'<p>', r'\\par '),
|
||||||
|
(r'</p>', r''),
|
||||||
|
# Headings
|
||||||
|
(r'<title>([^<]*)</title>', r'\\section*{\1}'),
|
||||||
|
(r'<hn>', r'\\part{'),
|
||||||
|
(r'</hn>', r'}'),
|
||||||
|
(r'<h1>', r'\\section*{'),
|
||||||
|
(r'</h[0-9]>', r'}'),
|
||||||
|
(r'<h2>', r'\\subsection*{'),
|
||||||
|
(r'<h3>', r'\\subsubsection*{'),
|
||||||
|
(r'<h4>', r'\\paragraph*{'),
|
||||||
|
(r'<h5>', r'\\paragraph*{'),
|
||||||
|
(r'<h6>', r'\\subparagraph*{'),
|
||||||
|
# UL is itemize
|
||||||
|
(r'<ul>', r'\\begin{itemize}'),
|
||||||
|
(r'</ul>', r'\\end{itemize}'),
|
||||||
|
(r'<ol>', r'\\begin{enumerate}'),
|
||||||
|
(r'</ol>', r'\\end{enumerate}'),
|
||||||
|
(r'<li>', r'\\item '),
|
||||||
|
(r'</li>', r''),
|
||||||
|
# DL is description
|
||||||
|
(r'<dl>', r'\\begin{description}'),
|
||||||
|
(r'</dl>', r'\\end{description}'),
|
||||||
|
# closing delimiter for DT is first < or end of line which ever comes first
|
||||||
|
# (r'<dt>([^<]*)<', r'\\item[\1]<'),
|
||||||
|
# (r'<dt>([^<]*)$', r'\\item[\1]'),
|
||||||
|
# (r'<dd>', r''),
|
||||||
|
# (r'<dt>', r'\\item['),
|
||||||
|
# (r'<dd>', r']'),
|
||||||
|
(r'<dt>([^<]*)</dt>', r'\\item[\1]'),
|
||||||
|
(r'<dd>', r''),
|
||||||
|
(r'</dd>', r''),
|
||||||
|
# Italics
|
||||||
|
(r'<it>([^<]*)</it>', r'{\\it \1}'),
|
||||||
|
(r'<em>([^<]*)</em>', r'{\\it \1}'),
|
||||||
|
(r'<b>([^<]*)</b>', r'{\\bf \1}'),
|
||||||
|
(r'<strong>([^<]*)</strong>', r'{\\bf \1}'),
|
||||||
|
# recipe specific
|
||||||
|
(r'<a href="\.\./([^"/]*)/*">([^<]*)</a>', r'\\recipelink{\1}{\2}'),
|
||||||
|
# Get rid of Anchors
|
||||||
|
(r'<a href="(http[^"]*)">([^<]*)</a>', r'\\external{\1}{\2}'),
|
||||||
|
(r'<a[^>]*>', r''),
|
||||||
|
(r'</a>', r''),
|
||||||
|
# quotes (replace after href)
|
||||||
|
(r'(\s)"([^\s])', r'\1``\2'),
|
||||||
|
(r'"', r"''"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __test__() -> None:
|
||||||
|
TEST_DATA = '''
|
||||||
|
Hello > is < for
|
||||||
|
chars: " " & # _ ~ ^ % $ { }
|
||||||
|
\\.
|
||||||
|
|
||||||
|
<p>paragraph</p>
|
||||||
|
|
||||||
|
<title>this is a title</title>
|
||||||
|
<hn>this is a part</hn>
|
||||||
|
<h1>this is a section</h1>
|
||||||
|
<h2>this is a subsection</h2>
|
||||||
|
<h3>this is a subsubsection</h3>
|
||||||
|
<h4>this is a paragraph</h4>
|
||||||
|
<h5>this is a paragraph</h5>
|
||||||
|
<h6>this is a subparagraph</h6>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>unordered one</li>
|
||||||
|
<li>unordered two</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>ordered one</li>
|
||||||
|
<li>ordered two</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>definition title</dt>
|
||||||
|
<dd>definition value</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<it>this is it</it>
|
||||||
|
<em>this is em</em>
|
||||||
|
<b>this is b</b>
|
||||||
|
<strong>this is strong</strong>
|
||||||
|
|
||||||
|
<a href="http://example.org">external anchor</a>
|
||||||
|
<a href="../recipe">recipe anchor</a>
|
||||||
|
<a href="language">other anchor</a>
|
||||||
|
|
||||||
|
between "some" text
|
||||||
|
|
||||||
|
<p class="name">test</p>
|
||||||
|
'''
|
||||||
|
print(Sed.apply(HTML_TO_LATEX_RULES, TEST_DATA))
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
class Sed:
|
||||||
|
def __init__(self, rules: List[Tuple[str, str]]) -> None:
|
||||||
|
self._rules = [(re.compile(pattern), sub) for pattern, sub in rules]
|
||||||
|
|
||||||
|
def replace(self, data: str) -> str:
|
||||||
|
ret = data
|
||||||
|
for regx, repl in self._rules:
|
||||||
|
ret = regx.sub(repl, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply(rules: List[Tuple[str, str]], data: str) -> str:
|
||||||
|
ret = data
|
||||||
|
for pattern, repl in rules:
|
||||||
|
ret = re.sub(pattern, repl, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def html_to_tex(html: str) -> str:
|
||||||
|
return Sed.apply(HTML_TO_LATEX_RULES, html)
|
||||||
|
|
||||||
|
|
||||||
|
def raw_text_to_tex(text: str) -> str:
|
||||||
|
if not text:
|
||||||
|
return ''
|
||||||
|
text = text.replace('\\', '\\backslash ')
|
||||||
|
for c in '}{%$&#_~^':
|
||||||
|
text = text.replace(c, '\\' + c)
|
||||||
|
return text.replace(' ', '~')
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# Helper methods
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
def _report_updated(msg: str) -> None:
|
||||||
|
click.echo('{} {}'.format(click.style('U', fg='green'), msg))
|
||||||
|
|
||||||
|
|
||||||
|
def _report_error(msg: str) -> None:
|
||||||
|
click.echo('{} {}'.format(click.style('E', fg='red'), msg))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# PDF Build Program & Source
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
class TexSources:
|
||||||
|
enabled: bool = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def registerBuilder(env: 'Environment', enabled: bool) -> None:
|
||||||
|
TexSources.enabled = enabled
|
||||||
|
env.add_build_program(PdfSource, PdfBuildProgram)
|
||||||
|
|
||||||
|
@env.virtualpathresolver(VPATH)
|
||||||
|
def resolvePDF(record: 'Record', pieces: List[str]) \
|
||||||
|
-> PdfSource:
|
||||||
|
return PdfSource(record)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add(builder: 'Builder', record: 'Record') -> None:
|
||||||
|
if TexSources.enabled:
|
||||||
|
try:
|
||||||
|
refs = builder.__tex_files # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
refs = list()
|
||||||
|
builder.__tex_files = refs # type: ignore[attr-defined]
|
||||||
|
refs.append(ref(record))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build(builder: 'Builder') -> None:
|
||||||
|
if not TexSources.enabled:
|
||||||
|
print(' * PDF Export: DISABLED')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sources = builder.__tex_files # type: ignore[attr-defined]
|
||||||
|
del builder.__tex_files # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
sources = []
|
||||||
|
|
||||||
|
if sources:
|
||||||
|
msg = f'PDF builder ({TEXER})'
|
||||||
|
with reporter.build(msg, builder): # type: ignore[attr-defined]
|
||||||
|
for rec_ref in sources:
|
||||||
|
builder.build(PdfSource(rec_ref()))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# PDF Build Program & Source
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
class PdfBuildProgram(BuildProgram):
|
||||||
|
source: 'PdfSource'
|
||||||
|
|
||||||
|
def produce_artifacts(self) -> None:
|
||||||
|
self.declare_artifact(
|
||||||
|
self.source.url_path,
|
||||||
|
sources=list(self.source.iter_source_filenames()))
|
||||||
|
|
||||||
|
def build_artifact(self, artifact: 'Artifact') -> None:
|
||||||
|
self.source.build(self.build_state)
|
||||||
|
|
||||||
|
|
||||||
|
class PdfSource(VirtualSourceObject):
|
||||||
|
@property
|
||||||
|
def path(self) -> str: # type: ignore[override]
|
||||||
|
return self.record.path + '@' + VPATH # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_path(self) -> str: # type: ignore[override]
|
||||||
|
return self.record.url_path[:-4] + '.pdf' # type:ignore[no-any-return]
|
||||||
|
|
||||||
|
def iter_source_filenames(self) -> Generator[str, None, None]:
|
||||||
|
template = lookup_template_path(self.record['_template'], self.pad.env)
|
||||||
|
if template:
|
||||||
|
yield template
|
||||||
|
yield from self.record.iter_source_filenames()
|
||||||
|
|
||||||
|
def build(self, build_state: 'BuildState') -> None:
|
||||||
|
cmd_tex = shutil.which(TEXER)
|
||||||
|
if not cmd_tex:
|
||||||
|
_report_error(f'Skip PDF export. {TEXER} not found.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# filename / path variables
|
||||||
|
tex_src = build_state.get_destination_filename(self.record.url_path)
|
||||||
|
pdf_dest = build_state.get_destination_filename(self.url_path)
|
||||||
|
build_dir = build_state.builder.destination_path
|
||||||
|
tex_root = os.path.join(build_state.env.root_path, '_tex-to-pdf')
|
||||||
|
tmp_dir = os.path.join(tex_root, PDF_OUT_DIR)
|
||||||
|
pdf_src = os.path.join(tmp_dir, os.path.basename(tex_src)[:-3] + 'pdf')
|
||||||
|
|
||||||
|
# create temporary output directory
|
||||||
|
os.makedirs(tmp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# store build destination to resolve image paths in setup.tex
|
||||||
|
with open(os.path.join(tmp_dir, 'builddir.tex'), 'w') as fp:
|
||||||
|
fp.write('\\def\\buildDir{' + build_dir + '}')
|
||||||
|
|
||||||
|
# run lualatex
|
||||||
|
silent = reporter.verbosity == 0 # type: ignore[attr-defined]
|
||||||
|
for i in range(1, 3):
|
||||||
|
if i > 1:
|
||||||
|
_report_updated(self.url_path.lstrip('/') + f' [{i}/2]')
|
||||||
|
p = shell.run([
|
||||||
|
cmd_tex, # lualatex
|
||||||
|
'--halt-on-error',
|
||||||
|
'--output-directory', tmp_dir,
|
||||||
|
tex_src # tex file
|
||||||
|
],
|
||||||
|
cwd=tex_root, # change work dir so lualatex can find setup.tex
|
||||||
|
stdout=shell.DEVNULL if silent else None, # dont spam console
|
||||||
|
input=b'') # auto-reply to stdin on error
|
||||||
|
|
||||||
|
if p.returncode == 0:
|
||||||
|
shutil.copyfile(pdf_src, pdf_dest)
|
||||||
|
else:
|
||||||
|
_report_error(f'{TEXER} returned error code {p.returncode}')
|
||||||
|
break
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
147
src/packages/main/lektor_main/plugin.py
Normal file
147
src/packages/main/lektor_main/plugin.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from lektor.databags import Databags
|
||||||
|
from lektor.db import Page # isinstance
|
||||||
|
from lektor.pluginsystem import Plugin
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, Any, List, Dict, Tuple, Set, Iterator
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from lektor.builder import Builder
|
||||||
|
from lektor.db import Record
|
||||||
|
|
||||||
|
from .durationcluster import (
|
||||||
|
int_to_cluster, cluster_as_str, human_readable_duration
|
||||||
|
)
|
||||||
|
from .ingredients import IngredientsListType
|
||||||
|
from .latex import TexSources, raw_text_to_tex, html_to_tex
|
||||||
|
from .settings import Settings
|
||||||
|
from .utils import (
|
||||||
|
fillupText, replace_atref_urls, sorted_images, title_image, noUmlauts
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MainPlugin(Plugin):
|
||||||
|
name = 'Main Plugin'
|
||||||
|
description = 'Code snippets for recipe lekture.'
|
||||||
|
|
||||||
|
def on_setup_env(self, **extra: Any) -> None:
|
||||||
|
# Register custom field type
|
||||||
|
self.env.add_type(IngredientsListType)
|
||||||
|
# Add jinja filter & globals
|
||||||
|
self.env.jinja_env.filters.update({
|
||||||
|
'asDuration': self.as_duration, # needs alt
|
||||||
|
'asRating': self.as_rating,
|
||||||
|
'replaceAtRefURLs': replace_atref_urls,
|
||||||
|
'performGroupBy': self.performGroupBy,
|
||||||
|
|
||||||
|
'sorted_images': sorted_images,
|
||||||
|
'title_image': title_image,
|
||||||
|
|
||||||
|
'latexStr': raw_text_to_tex,
|
||||||
|
'latexHtml': html_to_tex,
|
||||||
|
})
|
||||||
|
# self.env.jinja_env.globals.update({
|
||||||
|
# 'str': str,
|
||||||
|
# 'dir': dir,
|
||||||
|
# 'type': type,
|
||||||
|
# 'len': len,
|
||||||
|
# 'now': datetime.now
|
||||||
|
# })
|
||||||
|
# Latex -> PDF Build program
|
||||||
|
make_pdf = extra.get('extra_flags', {}).get('ENABLE_PDF_EXPORT', False)
|
||||||
|
TexSources.registerBuilder(self.env, enabled=make_pdf)
|
||||||
|
|
||||||
|
def on_after_build(
|
||||||
|
self, builder: 'Builder', source: 'Record', **extra: Any
|
||||||
|
) -> None:
|
||||||
|
if not isinstance(source, Page):
|
||||||
|
return # ignore Asset, Directory, etc.
|
||||||
|
if source.path.endswith('.tex'): # type: ignore[attr-defined]
|
||||||
|
TexSources.add(builder, source)
|
||||||
|
|
||||||
|
def on_after_build_all(self, builder: 'Builder', **extra: Any) -> None:
|
||||||
|
# must run after all sources are built
|
||||||
|
# or else latex fails because it cannot find referenced images
|
||||||
|
TexSources.build(builder)
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Duration #
|
||||||
|
##############
|
||||||
|
|
||||||
|
def _i18n(self, alt: str, key: str) -> Dict[str, str]:
|
||||||
|
# used for dependency tracking
|
||||||
|
return Databags(self.env).lookup( # type: ignore[no-any-return]
|
||||||
|
f'i18n+{alt}.{key}')
|
||||||
|
|
||||||
|
def as_duration(self, time: int, alt: str) -> str:
|
||||||
|
return human_readable_duration(time, self._i18n(alt, 'duration'))
|
||||||
|
|
||||||
|
def as_rating(self, x: str) -> str:
|
||||||
|
return fillupText(x, u'☆', u'★', 3)
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# Group by filter #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
def performGroupBy(
|
||||||
|
self,
|
||||||
|
recipes: List['Page'],
|
||||||
|
attribute: str,
|
||||||
|
reverse: bool = False,
|
||||||
|
alt: str = 'en',
|
||||||
|
) -> Iterator[Tuple[str, Set['Page']]]:
|
||||||
|
# Pre-Processing
|
||||||
|
if attribute == 'time':
|
||||||
|
time_clusters = Settings.duration_cluster()
|
||||||
|
time_translations = self._i18n(alt, 'duration')
|
||||||
|
elif attribute == 'difficulty':
|
||||||
|
difficulty_translations = self._i18n(alt, 'difficulty')
|
||||||
|
|
||||||
|
# Grouping
|
||||||
|
ret = dict() # type: Dict[Any, Set[Page]]
|
||||||
|
for recipe in recipes:
|
||||||
|
try:
|
||||||
|
data = recipe[attribute] or None
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
if attribute == 'time':
|
||||||
|
data = [int_to_cluster(data, time_clusters)]
|
||||||
|
elif attribute == 'ingredients' and data:
|
||||||
|
data = [x.name for x in data or [] if x.isIngredient]
|
||||||
|
else:
|
||||||
|
data = [data]
|
||||||
|
for x in data:
|
||||||
|
if x not in ret:
|
||||||
|
ret[x] = set()
|
||||||
|
ret[x].add(recipe)
|
||||||
|
|
||||||
|
# Sorting
|
||||||
|
reverse = bool(reverse)
|
||||||
|
if attribute == 'difficulty':
|
||||||
|
order = ['easy', 'medium', 'hard']
|
||||||
|
none_diff = -1 if reverse else 99
|
||||||
|
|
||||||
|
def _fn(x: Tuple) -> Any:
|
||||||
|
return order.index(x[0]) if x[0] in order else none_diff
|
||||||
|
elif attribute == 'ingredients': # sort by: str
|
||||||
|
none_ingr = 'aaaa' if reverse else 'zzzz'
|
||||||
|
|
||||||
|
def _fn(x: Tuple) -> Any:
|
||||||
|
return noUmlauts(x[0]).lower() if x[0] else none_ingr
|
||||||
|
else: # sort by: int
|
||||||
|
none_int = 0 if reverse else 999999999
|
||||||
|
|
||||||
|
def _fn(x: Tuple) -> Any:
|
||||||
|
return int(x[0]) if x[0] else none_int
|
||||||
|
|
||||||
|
result = sorted(ret.items(), reverse=bool(reverse), key=_fn)
|
||||||
|
|
||||||
|
# Post-Processing
|
||||||
|
for group, recipe_list in result:
|
||||||
|
if attribute == 'time':
|
||||||
|
group = cluster_as_str(group, time_clusters, time_translations)
|
||||||
|
elif attribute == 'rating':
|
||||||
|
group = self.as_rating(group)
|
||||||
|
elif attribute == 'difficulty':
|
||||||
|
group = difficulty_translations.get(group)
|
||||||
|
|
||||||
|
yield group, recipe_list
|
||||||
54
src/packages/main/lektor_main/settings.py
Normal file
54
src/packages/main/lektor_main/settings.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from lektor.context import get_ctx
|
||||||
|
from typing import TYPE_CHECKING, List, Dict, Optional, NamedTuple
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from lektor.db import Pad, Page, Record
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientConfig(NamedTuple):
|
||||||
|
units: List[str]
|
||||||
|
frac_map: Dict[str, str]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def of(record: 'Record') -> 'IngredientConfig':
|
||||||
|
return Settings.ingredient_config(record.pad, record.alt)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
@staticmethod
|
||||||
|
def load(pad: Optional['Pad'] = None, alt: Optional[str] = None) -> 'Page':
|
||||||
|
if not pad:
|
||||||
|
ctx = get_ctx()
|
||||||
|
if not ctx:
|
||||||
|
raise RuntimeError('Should never happen, missing context.')
|
||||||
|
pad = ctx.pad
|
||||||
|
if not alt:
|
||||||
|
alt = ctx.source.alt
|
||||||
|
assert(pad is not None)
|
||||||
|
# used for dependency tracking
|
||||||
|
return pad.get('/settings', alt=alt) # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ingredient_config(pad: 'Pad', alt: str) -> IngredientConfig:
|
||||||
|
set = Settings.load(pad, alt)
|
||||||
|
mea = set['measures'].split()
|
||||||
|
frac = {}
|
||||||
|
if set['replace_frac']:
|
||||||
|
it = iter(set['replace_frac_map'].split())
|
||||||
|
frac = dict(zip(it, it))
|
||||||
|
return IngredientConfig(mea, frac)
|
||||||
|
|
||||||
|
# def ingredient_config_old(record: 'Record') -> IngredientConfig:
|
||||||
|
# plugin = record.pad.env.plugins['main'] # type: MainPlugin
|
||||||
|
# cfg = plugin.get_config() # used for dependency tracking
|
||||||
|
# mea = cfg.get('measures.' + alt, '').split()
|
||||||
|
# frac = {}
|
||||||
|
# if cfg.get_bool('general.replace_frac'):
|
||||||
|
# frac = cfg.section_as_dict('replace_frac')
|
||||||
|
# return IngredientConfig(mea, frac)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def duration_cluster() -> List[int]:
|
||||||
|
# split() without args takes care of double spaces and trims each item
|
||||||
|
splits = Settings.load()['duration_cluster'].replace(',', ' ').split()
|
||||||
|
return [int(x) for x in splits]
|
||||||
85
src/packages/main/lektor_main/utils.py
Normal file
85
src/packages/main/lektor_main/utils.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from jinja2.loaders import split_template_path # lookup_template_name()
|
||||||
|
from markupsafe import Markup
|
||||||
|
import os
|
||||||
|
import unicodedata
|
||||||
|
from typing import TYPE_CHECKING, Dict, Union, Optional, List
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from lektor.db import Page, Image
|
||||||
|
from lektor.environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
def fillupText(
|
||||||
|
numerator: Union[str, int, None],
|
||||||
|
empty: str = u'☆',
|
||||||
|
filled: str = u'★',
|
||||||
|
total: int = 3
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Create a progress-bar-like string from int or number string.
|
||||||
|
0 -> "☆☆☆", 1 -> "★☆☆", 2 -> "★★☆", 3 -> "★★★", etc.
|
||||||
|
'''
|
||||||
|
x = int(numerator) if numerator else 0
|
||||||
|
return filled * x + empty * (total - x)
|
||||||
|
|
||||||
|
|
||||||
|
def replaceFractions(txt: str, repl_map: Dict[str, str]) -> str:
|
||||||
|
''' Replace `1 1/2 - 3/4` with `1½–¾`, etc. '''
|
||||||
|
res = ' '
|
||||||
|
for c in u'-–—':
|
||||||
|
txt = txt.replace(c, ' – ')
|
||||||
|
# NOTE: `split(' ')` can contain empty values but `split()` does not!
|
||||||
|
for x in txt.split():
|
||||||
|
if x == '–':
|
||||||
|
res += x
|
||||||
|
else:
|
||||||
|
res += repl_map.get(x) or (x if res[-1] == '–' else ' ' + x)
|
||||||
|
return res.lstrip(' ')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_atref_urls(text: str, label: Optional[str] = None) -> str:
|
||||||
|
''' Replace `@../recipe/` with `<a href="../recipe/">label</a>` '''
|
||||||
|
if '@' not in text:
|
||||||
|
return text
|
||||||
|
result = list()
|
||||||
|
for x in text.split():
|
||||||
|
if x[0] == '@':
|
||||||
|
x = x[1:]
|
||||||
|
result.append(u'<a href="{}">{}</a>'.format(x, label or x))
|
||||||
|
else:
|
||||||
|
result.append(x)
|
||||||
|
return Markup(' '.join(result))
|
||||||
|
|
||||||
|
|
||||||
|
def sorted_images(obj: 'Page', attr: str = 'record_label') -> List['Image']:
|
||||||
|
return sorted(obj.attachments.images,
|
||||||
|
key=lambda x: getattr(x, attr)) # type:ignore[no-any-return]
|
||||||
|
|
||||||
|
|
||||||
|
def title_image(obj: 'Page', attr: str = 'record_label', small: bool = False) \
|
||||||
|
-> Optional['Image']:
|
||||||
|
imgs = sorted_images(obj, attr)
|
||||||
|
img = imgs[0] if imgs else None
|
||||||
|
if img and small:
|
||||||
|
img = img.thumbnail(200, 150, mode='crop')
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def noUmlauts(text: str) -> str:
|
||||||
|
# try:
|
||||||
|
# data = unicode(text, 'utf-8')
|
||||||
|
# except (TypeError, NameError):
|
||||||
|
# pass
|
||||||
|
text = unicodedata.normalize('NFD', text)
|
||||||
|
data = text.encode('ascii', 'ignore')
|
||||||
|
text = data.decode('utf-8')
|
||||||
|
return str(text)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_template_path(name: str, env: 'Environment') -> Optional[str]:
|
||||||
|
pieces = split_template_path(name)
|
||||||
|
for base in env.jinja_env.loader.searchpath:
|
||||||
|
path = os.path.join(base, *pieces)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
21
src/packages/main/setup.py
Normal file
21
src/packages/main/setup.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='lektor-main',
|
||||||
|
packages=['lektor_main'],
|
||||||
|
entry_points={
|
||||||
|
'lektor.plugins': [
|
||||||
|
'main = lektor_main:MainPlugin',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
author='relikd',
|
||||||
|
version='0.1',
|
||||||
|
description='Main code for this repository',
|
||||||
|
license='MIT',
|
||||||
|
python_requires='>=3.6',
|
||||||
|
keywords=['lektor', 'plugin'],
|
||||||
|
classifiers=[
|
||||||
|
'Environment :: Plugins',
|
||||||
|
'Framework :: Lektor',
|
||||||
|
],
|
||||||
|
)
|
||||||
5
src/packages/time-duration/.gitignore
vendored
5
src/packages/time-duration/.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
dist
|
|
||||||
build
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.egg-info
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Time Duration
|
|
||||||
|
|
||||||
This plugin converts integer numbers to a human readable duration.
|
|
||||||
E.g. 90 -> 1 hour 30 minutes
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# -*- 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),
|
|
||||||
key=lambda x: x[0] if x[0] != '' else 999999999)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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_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',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,33 +1,10 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}{{ this.name }}{% endblock %}
|
{% block title %}{{ this.name }}{% endblock %}
|
||||||
{% block body %}
|
{% 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, this.alt) | groupSort(sortType, this.reverse_order) -%}
|
|
||||||
|
|
||||||
{%- if this.group_key == 'time' -%}
|
|
||||||
{%- set all = all | groupTimeCluster(this.xdata, this.reverse_order) -%}
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
<h1>{{ this.name }}</h1>
|
<h1>{{ this.name }}</h1>
|
||||||
<dl class="cluster">
|
<dl class="cluster">
|
||||||
{%- for attrib, recipes in all -%}
|
{%- for group_name, recipes in site.query('/recipes', this.alt) | performGroupBy(this.group_key, this.reverse_order, this.alt) -%}
|
||||||
<dt>{%- if this.group_key == 'rating' -%}
|
<dt>{{ group_name or this.null_fallback }}</dt>
|
||||||
{{ attrib | rating }}
|
|
||||||
{%- elif not attrib -%}
|
|
||||||
{{ this.null_fallback }}
|
|
||||||
{%- elif this.group_key == 'time' -%}
|
|
||||||
{{ attrib | durationCluster(this.xdata, this.alt) }}
|
|
||||||
{%- elif this.group_key == 'difficulty' -%}
|
|
||||||
{{ bag('i18n+' + this.alt, 'difficulty', attrib) }}
|
|
||||||
{%- else -%}
|
|
||||||
{{ attrib }}
|
|
||||||
{%- endif -%}</dt>
|
|
||||||
<dd>
|
<dd>
|
||||||
{%- set pipe = joiner(' | ') -%}
|
{%- set pipe = joiner(' | ') -%}
|
||||||
{%- for recipe in recipes | sort(attribute='name') -%}
|
{%- for recipe in recipes | sort(attribute='name') -%}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
{#- overlay on hover and always-visible icons #}
|
{#- overlay on hover and always-visible icons #}
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<div class="hover"><div class="time">{{ recipe.time|duration(recipe.alt) }}</div></div>
|
<div class="hover"><div class="time">{{ recipe.time | asDuration(recipe.alt) }}</div></div>
|
||||||
<div class="icon-bar">
|
<div class="icon-bar">
|
||||||
{%- if 'raw' in recipe.tags -%}<i class="icon raw"></i>{%- endif -%}
|
{%- if 'raw' in recipe.tags -%}<i class="icon raw"></i>{%- endif -%}
|
||||||
{%- if 'glutenfree' in recipe.tags -%}<i class="icon gf"></i>{%- endif -%}
|
{%- if 'glutenfree' in recipe.tags -%}<i class="icon gf"></i>{%- endif -%}
|
||||||
|
|||||||
@@ -21,29 +21,29 @@
|
|||||||
<h1>{{ this.name }}</h1>
|
<h1>{{ this.name }}</h1>
|
||||||
|
|
||||||
<section id="metrics" class="small">
|
<section id="metrics" class="small">
|
||||||
<div id="rating" class="xlarge">{{ this.rating|rating }}</div>
|
<div id="rating" class="xlarge">{{ this.rating | asRating }}</div>
|
||||||
<div class="difficulty {{this.difficulty}}">
|
<div class="difficulty {{this.difficulty}}">
|
||||||
<div></div><div></div><div></div>
|
<div></div><div></div><div></div>
|
||||||
<span {% if not this.difficulty %}class="small"{%- endif %}>{{
|
<span {% if not this.difficulty %}class="small"{%- endif %}>{{
|
||||||
bag('i18n+' + this.alt, 'difficulty', this.difficulty or '_unset') }}</span>
|
bag('i18n+' + this.alt, 'difficulty', this.difficulty or '_unset') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ bag('i18n+' + this.alt, 'duration.label') }}: {{ this.time|duration(this.alt) if this.time else '—' }}</div>
|
<div>{{ bag('i18n+' + this.alt, 'duration.label') }}: {{ this.time | asDuration(this.alt) or '—' }}</div>
|
||||||
<div>{{ bag('i18n+' + this.alt, 'yield.label') }}: {{ this.yield or '—' }}</div>
|
<div>{{ bag('i18n+' + this.alt, 'yield.label') }}: {{ this.yield or '—' }}</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="ingredients">
|
<section id="ingredients">
|
||||||
<h2>{{ bag('i18n+' + this.alt, 'title.ingredients') }}:</h2>
|
<h2>{{ bag('i18n+' + this.alt, 'title.ingredients') }}:</h2>
|
||||||
<ul class="no-bullets li-lg-space">
|
<ul class="no-bullets li-lg-space">
|
||||||
{%- for ing in this|enumIngredients(this.alt) %}
|
{%- for ing in this.ingredients %}
|
||||||
{%- if ing['group'] %}
|
{%- if ing.isGroup %}
|
||||||
<li class="dark-red bold mrgTopMd">{{ ing['group'] }}</li>
|
<li class="dark-red bold mrgTopMd">{{ ing.name }}:</li>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<li>
|
<li>
|
||||||
{%- if ing['value'] %}{{ ing['value'] }} {% endif -%}
|
{%- if ing.quantity %}{{ ing.quantity }} {% endif -%}
|
||||||
{%- if ing['measure'] %}{{ ing['measure'] }} {% endif -%}
|
{%- if ing.unit %}{{ ing.unit }} {% endif -%}
|
||||||
<span class="light-red">{{ ing['name'] }}</span>
|
<span class="light-red">{{ ing.name }}</span>
|
||||||
{%- if ing['note'] -%}
|
{%- if ing.note -%}
|
||||||
<span class="small italic">{{ ', ' ~ ing['note'] | replaceAtRefURLs(label=bag('i18n+' + this.alt, 'ingredients.recipeLink')) }}</span>
|
<span class="small italic">{{ ', ' ~ ing.note | replaceAtRefURLs(label=bag('i18n+' + this.alt, 'ingredients.recipeLink')) }}</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</li>
|
</li>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user