diff --git a/Makefile b/Makefile index b34d795..0d6f4c9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ PROJDIR := src +TEXER := lualatex help: @echo @@ -11,7 +12,7 @@ help: @echo ' server - Start lektor server with live change updates' @echo ' build - Build deployable website into ./bin' @echo ' deploy - Custom rsync command to sync ./bin to remote server' - @echo + @echo ' pdf - Generate PDF from tex (after build)' @echo @echo ' find-links - Search for cross reference between recipes' @echo @@ -42,15 +43,14 @@ clean-all: clean plugins # Build server: - @cd '$(PROJDIR)' && lektor server + @cd '$(PROJDIR)' && lektor server # -f ENABLE_PDF_EXPORT build: @cd '$(PROJDIR)' && \ - lektor build --output-path ../bin --buildstate-path ../build-state -f ENABLE_APPCACHE + lektor build --output-path ../bin --buildstate-path ../build-state -f ENABLE_APPCACHE -f ENABLE_PDF_EXPORT @echo @echo 'Checking dead links ...' @python3 extras/find-dead-links.py - @echo deploy: @echo @@ -59,6 +59,24 @@ deploy: @echo # --dry-run rsync -rclzv --exclude=.lektor --exclude=.DS_Store --delete bin/ vps:/srv/http/recipe-lekture +pdf: + @echo + @echo 'Generating PDF from tex source ...' + @echo 'Check if $(TEXER) exists' + @which $(TEXER) + @cd extras/pdf-export/ && \ + SECONDS=0; \ + for i in 1 2; do \ + for alt in de en; do \ + fname="pdf-$${alt}.tex"; \ + echo "$$ $(TEXER) $${fname} [$${i}]"; \ + $(TEXER) $${fname} > /dev/null; \ + done; \ + done; \ + echo "done. finished after $${SECONDS}s." + rm -rf extras/pdf-export/*.{aux,log,out,toc} + mv extras/pdf-export/pdf-*.pdf bin/static + # Helper methods on all recipes find-links: diff --git a/extras/pdf-export/.gitignore b/extras/pdf-export/.gitignore index 3e60473..b5b215c 100644 --- a/extras/pdf-export/.gitignore +++ b/extras/pdf-export/.gitignore @@ -1,3 +1,7 @@ +/*.aux +/*.log +/*.out +/*.toc +/dyn-*.tex /pdf-de.pdf -/pdf-en.pdf -/setup-builddir.tex \ No newline at end of file +/pdf-en.pdf \ No newline at end of file diff --git a/extras/pdf-export/pdf-de.tex b/extras/pdf-export/pdf-de.tex index 36173ed..bb59a30 100644 --- a/extras/pdf-export/pdf-de.tex +++ b/extras/pdf-export/pdf-de.tex @@ -1,7 +1,7 @@ \documentclass[a4paper,12pt,twoside]{article} \usepackage[ngerman]{babel} \include{setup} -\include{setup-builddir} % load \def\builddir{} +\include{dyn-builddir} % load \def\builddir{} \def\tIngredients{Zutaten} \def\tRecipe{Rezept} @@ -9,6 +9,5 @@ \begin{document} \makefrontmatter -\graphicspath{{\builddir/de/}} -\include{\builddir/de/pdf-export} +\include{dyn-recipes-de} \end{document} \ No newline at end of file diff --git a/extras/pdf-export/pdf-en.tex b/extras/pdf-export/pdf-en.tex index 251cae7..17572ad 100644 --- a/extras/pdf-export/pdf-en.tex +++ b/extras/pdf-export/pdf-en.tex @@ -1,7 +1,7 @@ \documentclass[letter,12pt,twoside]{article} \usepackage[english]{babel} \include{setup} -\include{setup-builddir} % load \def\builddir{} +\include{dyn-builddir} % load \def\builddir{} \def\tIngredients{Ingredients} \def\tRecipe{recipe} @@ -9,6 +9,5 @@ \begin{document} \makefrontmatter -\graphicspath{{\builddir/en/}} -\include{\builddir/en/pdf-export} +\include{dyn-recipes-en} \end{document} \ No newline at end of file diff --git a/extras/pdf-export/setup.tex b/extras/pdf-export/setup.tex index 6ce927f..5845bef 100644 --- a/extras/pdf-export/setup.tex +++ b/extras/pdf-export/setup.tex @@ -9,45 +9,20 @@ \usepackage{tocloft} % \cftbeforesecskip \usepackage{ragged2e} % \RaggedRight \usepackage{fontspec} % \setmainfont, \setsansfont, \setmonofont +\usepackage{microtype} +\usepackage{xurl} +\PassOptionsToPackage{hyphens}{url} +\usepackage[hidelinks]{hyperref} + + +%------------------------------------------- +% Defines & Geometry +%------------------------------------------- +\graphicspath{{\builddir/en/}} \definecolor{red2}{HTML}{AA203A} \definecolor{red3}{HTML}{EE6A84} -\usepackage{xurl} -\PassOptionsToPackage{hyphens}{url} -\usepackage{hyperref} -\renewcommand{\UrlFont}{\normalfont} -%% Use this if the font is _not_ installed on the system -%% but it requires the download of the additional fonts package -%% \setmonofont[Path=fonts/]{FiraSans-Light} -\setmainfont[ - Path = fonts/, - BoldFont = Crimson-Bold, - ItalicFont = Crimson-Italic, - BoldItalicFont = Crimson-BoldItalic -]{Crimson-Roman} -\setsansfont[ - Path = fonts/, - Scale = 0.95, - BoldFont = FiraSans-Medium, - ItalicFont = FiraSans-LightItalic, - BoldItalicFont = FiraSans-MediumItalic -]{FiraSans-Regular} - -%% Use this if the font is installed on the system -%% this does not require additional fonts package -% \setmainfont[ -% BoldFont = {Fira Sans Medium}, -% ItalicFont = {Fira Sans Light Italic}, -% BoldItalicFont = {Fira Sans Medium Italic} -% ]{Fira Sans Regular} -% \setmainfont[ -% BoldFont = {Crimson Bold}, -% ItalicFont = {Crimson Italic}, -% BoldItalicFont = {Crimson Bold Italic} -% ]{Crimson Roman} - -% Defines & Geometry \def\marginwidth{60mm} \def\marginsep{8.2mm} \def\marginwoverflow{67mm} % -1.2mm for makebox @@ -66,56 +41,117 @@ includemp, % showframe } -% \renewcommand{\footnotesep}{2.5ex} -\renewcommand{\footnoterule}{ - \kern -2pt - \hrule width \textwidth height .2pt - \kern 1.8pt} -% spacing before & after + + +%------------------------------------------- +% Penalties +%------------------------------------------- +\doublehyphendemerits=10000 % No consecutive line hyphens +\brokenpenalty=10000 % No broken words across columns/pages +\widowpenalty=9999 % Almost no widows at bottom of page +\clubpenalty=9999 % Almost no orphans at top of page +\interfootnotelinepenalty=9999 % Almost never break footnotes + + +%------------------------------------------- +% Fonts +%------------------------------------------- +\renewcommand{\UrlFont}{\normalfont} +%% Use this if the font is _not_ installed on the system +%% but it requires the download of the additional fonts package +%% \setmonofont[Path=fonts/]{FiraSans-Light} +\setmainfont[ + Path = fonts/, + BoldFont = {Crimson-Bold}, + ItalicFont = {Crimson-Italic}, + BoldItalicFont = {Crimson-BoldItalic} +]{Crimson-Roman} +\setsansfont[ + Path = fonts/, + Scale = 0.95, + BoldFont = {FiraSans-Medium}, + ItalicFont = {FiraSans-LightItalic}, + BoldItalicFont = {FiraSans-MediumItalic} +]{FiraSans-Regular} + +%% Use this if the font is installed on the system +%% this does not require additional fonts package +% \setmainfont[ +% BoldFont = {Fira Sans Medium}, +% ItalicFont = {Fira Sans Light Italic}, +% BoldItalicFont = {Fira Sans Medium Italic} +% ]{Fira Sans Regular} +% \setmainfont[ +% BoldFont = {Crimson Bold}, +% ItalicFont = {Crimson Italic}, +% BoldItalicFont = {Crimson Bold Italic} +% ]{Crimson Roman} + + +%------------------------------------------- +% Spacing in sections +%------------------------------------------- \makeatletter -% footer -\renewcommand\@makefntext[1]{\noindent\makebox[0.5em][l]{\@makefnmark}#1} -% subsubsection \renewcommand{\subsubsection}{% \@startsection{subsubsection}{3}% {\z@}{-2ex \@plus -1ex \@minus -1ex}{.2ex \@plus 1ex \@minus .2ex}% {\normalfont\large\bfseries}% } -% paragraph \renewcommand{\paragraph}{% \@startsection{paragraph}{4}% - {\z@}{1.5ex \@plus 1ex \@minus .2ex}{-.75em}% + {\z@}{1.5ex \@plus 1ex \@minus .2ex}{-.75em \@plus -1em \@minus -.5em}% {\normalfont\normalsize\bfseries}% } +% footer +\renewcommand\@makefntext[1]{\noindent\makebox[0.5em][l]{\@makefnmark}#1} \makeatother -% adjust description environment -\setlist[description,enumerate,itemize]{% +% \titleformat*{\section}{\LARGE\bfseries} # titlesec +\setlength{\cftbeforesecskip}{.5ex} % spacing in TOC + + +%------------------------------------------- +% Spacing in enumerations +%------------------------------------------- +\setlist[description]{% topsep=0.7\baselineskip, itemsep=0pt, + % labelsep=*, % itemindent=-2em, % listparindent=-2em, - % leftmargin=0em, font={\sffamily} } -\setlist[enumerate,itemize]{leftmargin=1em,topsep=0\baselineskip,parsep=0pt} -\widowpenalty10000 -\clubpenalty10000 +\setlist[enumerate,itemize]{ + topsep=0\baselineskip, + leftmargin=1em, + itemsep=1ex, + parsep=0pt +} + + +%------------------------------------------- +% Section & footnote numbering +%------------------------------------------- % disable section numbering \setcounter{secnumdepth}{0} \setcounter{tocdepth}{1} -% \titleformat*{\section}{\LARGE\bfseries} # titlesec -\setlength{\cftbeforesecskip}{0.2em} % spacing in TOC - +% \renewcommand{\footnotesep}{2.5ex} +\renewcommand{\footnoterule}{ + \kern -2pt + \hrule width \textwidth height .2pt + \kern 1.8pt} % Footer page numbering \renewpagestyle{plain}{% \setfoot[{\makebox[-\marginwoverflow][r]{\thepage}}][][]{}{}{{\makebox[-\marginwoverflow][l]{\thepage}}} } \pagestyle{plain} -% \graphicspath{{../src/content/}} +%------------------------------------------- +% Title page +%------------------------------------------- \newcommand{\makefrontmatter}{ \begin{titlepage} + \setcounter{page}{0} % sets the number 0 on the first page in preview \newgeometry{top=2.5cm,bottom=2.5cm,left=4cm,right=4cm} \centering \vspace*{.06\textheight} @@ -134,7 +170,10 @@ \setcounter{page}{1} } + +%------------------------------------------- % Custom commands +%------------------------------------------- \newcommand{\meta}[2]{% \vspace{-0.75em}% ~\includegraphics[height=2ex]{misc/icon-time}\enspace\ifx\relax#1\relax—\else#1\fi% @@ -146,8 +185,7 @@ \ifx\relax#1\relax\else{% \enlargethispage{5.75ex} \let\thefootnote\relax\footnotetext{% - \hspace{-.6em} Source: % - \ifx\relax#2\relax#1\else\href{#1}{#2}\fi% + \hspace{-.6em} Source: \ifx\relax#2\relax#1\else\href{#1}{#2}\fi% }\fi% } } @@ -156,7 +194,7 @@ \newcommand{\ingGroup}[1]{\vspace{.2ex}\item{\bfseries\color{red2}\hspace{-1em} #1}} \newcommand{\ingName}[1]{{\color{red3}#1}} \newcommand{\ingDetail}[1]{\emph{\footnotesize,#1}} -\newcommand{\pagelink}[1]{\tRecipe \tPagePrefix\,{\color{red2}\pageref{#1}}} +\newcommand{\pagelink}[1]{\tRecipe{} \tPagePrefix\,{\color{red2}\pageref{#1}}} \newcommand{\recipelink}[2]{#2 (\tPagePrefix\,\pageref{#1})} \newcommand{\external}[2]{#2\footnote{\href{#1}{#1}}} % \newcommand{\external}[2]{#2\footnote{\url{#1}}} diff --git a/src/content/pdf-export.tex/contents.lr b/src/content/pdf-export.tex/contents.lr deleted file mode 100644 index 8fc6658..0000000 --- a/src/content/pdf-export.tex/contents.lr +++ /dev/null @@ -1,3 +0,0 @@ -_template: makepdf.tex ---- -_model: none \ No newline at end of file diff --git a/src/packages/helper/lektor_helper.py b/src/packages/helper/lektor_helper.py index 8845e7f..45c3e96 100644 --- a/src/packages/helper/lektor_helper.py +++ b/src/packages/helper/lektor_helper.py @@ -210,7 +210,9 @@ class HelperPlugin(Plugin): repFrac = self.settings[alt]['replFrac'] for line in recipe['ingredients']: - line = tex.raw_text_to_tex(line).strip() + line = line.strip() + if mode == 'tex': + line = tex.raw_text_to_tex(line) if not line: continue elif line.endswith(':'): diff --git a/src/packages/html-to-tex/html2latex.sed b/src/packages/html-to-tex/html2latex.sed index d9832f0..c682de0 100644 --- a/src/packages/html-to-tex/html2latex.sed +++ b/src/packages/html-to-tex/html2latex.sed @@ -50,13 +50,12 @@ s?\([^<]*\)?{\\it \1}?g s?\([^<]*\)?{\\it \1}?g s?\([^<]*\)?{\\bf \1}?g s?\([^<]*\)?{\\bf \1}?g -# old unused, because lektor generated internally other urls then output -# s?[^<]*?\\recipelink{\1}?g # recipe specific -s?\([^<]*\)?\\recipelink{\1}{\2}?g +s?\([^<]*\)?\\recipelink{\1}{\2}?g # Get rid of Anchors s?\([^<]*\)?\\external{\1}{\2}?g s?]*>??g s???g -# after href replace +# quotes (replace after href) +s?\([[:space:]]\)"\([^[:space:]]\)?\1``\2?g s?"?''?g diff --git a/src/packages/html-to-tex/lektor_html_to_tex.py b/src/packages/html-to-tex/lektor_html_to_tex.py index df6bd12..c557887 100644 --- a/src/packages/html-to-tex/lektor_html_to_tex.py +++ b/src/packages/html-to-tex/lektor_html_to_tex.py @@ -1,21 +1,25 @@ # -*- coding: utf-8 -*- from lektor.pluginsystem import Plugin # , get_plugin +import mistune import os -import subprocess +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_content(f_sed, content): - with subprocess.Popen(('echo', content), stdout=subprocess.PIPE) as ps: - o = subprocess.check_output(['sed', '-f', f_sed], stdin=ps.stdout) +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_content(f_sed_a, html) + return sed_repl_tex(f_sed_a, html) def raw_text_to_tex(text): @@ -24,23 +28,104 @@ def raw_text_to_tex(text): if c in text: text = text.replace(c, '\\' + c) return text - # return sed_repl_content(f_sed_b, 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_before_build_all(self, builder, **extra): - # export current build dir - dest_file = my_dir + 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_file = os.path.dirname(dest_file) - dest_file = os.path.join(dest_file, 'extras', 'pdf-export', - 'setup-builddir.tex') - with open(dest_file, 'w') as f: + 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['html_to_tex'] = html_to_tex self.env.jinja_env.filters['raw_text_to_tex'] = raw_text_to_tex diff --git a/src/templates/makepdf.tex b/src/templates/makepdf.tex deleted file mode 100644 index 3a752b7..0000000 --- a/src/templates/makepdf.tex +++ /dev/null @@ -1,31 +0,0 @@ -{%- set recipe_label = localize(this.alt, 'ingredients.recipeLink') %} - -{%- for recipe in site.get('/recipes', this.alt).children %} -\newrecipe{ {{- recipe._slug }}}{ {{- recipe.name | raw_text_to_tex }}} -\meta{ {{- recipe.time|duration(this.alt) if recipe.time else '' }}}{ {{- recipe.yield or '' }}} -\footer{ {{- recipe.source | string | raw_text_to_tex or '' }}}{ {{- recipe.source.host | raw_text_to_tex if recipe.source.host else '' }}} - {%- set img = recipe | title_image(small=True) %} - -\begin{ingredients}{ {%- if img %}{{ img|url }}{% else %}{% endif %}} - {%- for ing in recipe|enumIngredients(this.alt, mode='tex') %} - {%- if ing['group'] %} -\ingGroup{ {{- ing['group'] | raw_text_to_tex }}} - {%- else %} -\item - {%- if ing['value'] or ing['measure'] -%} - [{%- if ing['value'] %}{{ ing['value'] }}{% endif -%} - {%- if ing['measure'] %}~{{ ing['measure'] }}{% endif -%}] - {%- endif %} \ingName{ {{- ing['name'] -}}} - {%- if ing['note'] %}\ingDetail{ - {%- for prt in ing['note'].split() -%} - {%- if prt.startswith('@../') %} \pagelink{ {{- prt[4:-1] -}} } - {%- else %} {{ prt -}} - {%- endif -%} - {%- endfor -%}} - {%- endif -%} - {%- endif -%} - {%- endfor %} -\end{ingredients} - -{{ recipe.directions.html | html_to_tex }} -{%- endfor %}