diff --git a/recipes/templates/version.html b/recipes/templates/version.html
index 886c80d..b52c676 100644
--- a/recipes/templates/version.html
+++ b/recipes/templates/version.html
@@ -17,20 +17,23 @@
This program comes with a copy of the GNU Affero General Public License
file at the root of this project.
{% endcomment %}
+{% load version_markup %}
{% block title %}{{ version.recipe.title }}{% if has_multiple_versions %} ({{ version.label }}){% endif %}{% endblock %}
{% block main %}
-
{{ version.recipe.title }}{% if has_multiple_versions %} ({{ version.label }}){% endif %}
{% if has_multiple_versions %}
Show all versions
{% endif %}
Edit recipe name
Add version
Edit Version
+
+ {{ version.recipe.title }}{% if has_multiple_versions %} ({{ version.label }}){% endif %}
{% for i in ingredients %}
- {{ i.text }}
{% endfor %}
-{{ version.body }}
+ {% if version.body %}{{ version.body|ndash_for_ranges|nbsp_for_amounts|unify_units|vulgar_fractions|to_sections }}{% endif %}
+
{% if version.img %}
{% endif %}
{% endblock %}
diff --git a/recipes/templatetags/__init__.py b/recipes/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/recipes/templatetags/version_markup.py b/recipes/templatetags/version_markup.py
new file mode 100644
index 0000000..10d5e97
--- /dev/null
+++ b/recipes/templatetags/version_markup.py
@@ -0,0 +1,175 @@
+"""
+Barn Web App - A collection of web-apps for my family's personal use,
+including a recipe database.
+Copyright © 2023 Benjamin Stadlbauer
+
+This program is free software: you can redistribute it and/or modify it
+under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or (at
+your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+General Public License for more details.
+
+This program comes with a copy of the GNU Affero General Public License
+file at the root of this project.
+"""
+
+from django import template
+import re
+from fractions import Fraction
+from django.template.defaultfilters import stringfilter
+from django.utils.html import conditional_escape, linebreaks
+from django.utils.safestring import mark_safe
+from django.utils.text import normalize_newlines
+
+register = template.Library()
+
+@register.filter(name='unify_units', is_safe=True, needs_autoescape=True)
+@stringfilter
+def unify_units_filter(value, autoescape=True):
+ if autoescape:
+ value = conditional_escape(value)
+
+ value = re.sub(r"\b[lL](iter)?\b", "Liter", value)
+ value = re.sub(r"\b[eE](ss|ß)?[lL]?(öffel)?\b", "Esslöffel", value)
+ value = re.sub(r"\b[tT](ee)?[lL]\.?(öffel)?\b", "Teelöffel", value)
+ value = re.sub(r"\b[dD]a?[gG]\b", "dag", value)
+ value = re.sub(r"\b([kK][iI][lL][oO]|[kK](ilo)?[gG]\.?(ramm)?)\b", "kg", value)
+ value = re.sub(r"(\b[Gg](rad)? ?|\u00B0 ?)?[Cc](elsius)?\b", "°C", value)
+ value = re.sub(r"\b[Mm](in)?\.?(uten)?\b", "min", value)
+ value = re.sub(r"\b[sS]\.?(ek)?\.?(unden)?\b", "Sekunden", value)
+
+ return mark_safe(value)
+
+@register.filter(name='vulgar_fractions', is_safe=True, needs_autoescape=True)
+@stringfilter
+def fractions_to_vuglar_fractions_filter(value, autoescape=True):
+ fraction_re = re.compile(r"(\d{1,2})( ?\/ ?)(\d{1,2})")
+ if autoescape:
+ value = conditional_escape(value)
+
+ match = fraction_re.search(value)
+
+ while (match):
+ f = Fraction(int(match.group(1)), int(match.group(3)))
+ fraction_string = ""
+
+ if 1 == f.numerator:
+ if 2 == f.denominator:
+ fraction_string = "½" # 1/2
+ elif 3 == f.denominator:
+ fraction_string = "⅓" # 1/3
+ elif 4 == f.denominator:
+ fraction_string = "¼" # 1/4
+ elif 5 == f.denominator:
+ fraction_string = "⅕" # 1/5
+ elif 6 == f.denominator:
+ fraction_string = "⅙" # 1/6
+ elif 7 == f.denominator:
+ fraction_string = "⅐" # 1/7
+ elif 8 == f.denominator:
+ fraction_string = "⅛" # 1/8
+ elif 9 == f.denominator:
+ fraction_string = "⅑" # 1/9
+ elif 10 == f.denominator:
+ fraction_string = "⅒" # 1/10
+ elif 2 == f.numerator:
+ if 3 == f.denominator:
+ fraction_string = "⅔" # 2/3
+ elif 5 == f.denominator:
+ fraction_string = "⅖" # 2/5
+ elif 3 == f.numerator:
+ if 4 == f.denominator:
+ fraction_string = "¾" # 3/4
+ elif 5 == f.denominator:
+ fraction_string = "⅗" # 3/5
+ elif 8 == f.denominator:
+ fraction_string = "⅜" # 3/8
+ elif 4 == f.numerator:
+ if 5 == f.denominator:
+ fraction_string = "⅘" # 4/5
+ elif 5 == f.numerator:
+ if 6 == f.denominator:
+ fraction_string = "⅚" # 5/6
+ elif 8 == f.denominator:
+ fraction_string = "⅝" # 5/8
+ elif 7 == f.numerator:
+ if 8 == f.denominator:
+ fraction_string = "⅞" # 7/8
+ if "" == fraction_string:
+ fraction_string = "%i⁄%i" % (f.numerator, f.denominator)
+
+ prefix = value[:match.start()]
+ suffix = value[match.end():]
+ value = prefix + fraction_string + suffix
+ match = fraction_re.search(value)
+
+ return mark_safe(value)
+
+@register.filter(name='nbsp_for_amounts', is_safe=True, needs_autoescape=True)
+@stringfilter
+def nbsp_for_amounts_filter(value, autoescape=True):
+ if autoescape:
+ value = conditional_escape(value)
+
+ return mark_safe(re.sub(r"(?<=\d) *(?=\b[^\d\W]+\b)", ' ', value)) # replaces optional spaces between digits and non-digit word characters with non-breaking spaces
+
+@register.filter(name='ndash_for_ranges', is_safe=True, needs_autoescape=True)
+@stringfilter
+def ndash_for_ranges_filter(value, autoescape=True):
+ if autoescape:
+ value = conditional_escape(value)
+
+ en_dash = '–' # word joiner (U+2060) en dash (U+2013) word joiner (U+2060)
+ return mark_safe(re.sub(r"(?<=\d) ?-+ ?(?=\d)", en_dash, value)) # replaces dashes between digits with en-dashes (and omits optional spaces and adds word joiner)
+
+def line_is_section_heading(text):
+ return len(text) < 30 and not re.search(r"\. *$", text)
+
+@register.filter(name='to_sections', is_safe=True, needs_autoescape=True)
+@stringfilter
+def body_to_sections_filter(value, autoescape=True):
+ if autoescape:
+ value = conditional_escape(value)
+
+ value = normalize_newlines(value)
+
+ section_objects = []
+ section_object = {
+ 'title': '',
+ 'body': '',
+ }
+
+ for line in re.split('\n{2,}', value):
+ if line_is_section_heading(line):
+ if section_object['body']:
+ section_objects += [section_object]
+
+ section_object = {
+ 'title': line,
+ 'body': '',
+ }
+ else:
+ if section_object['body']:
+ section_object['body'] += '\n\n' + line
+ else:
+ section_object['body'] = line
+
+ section_objects += [section_object]
+
+ sections = ''
+
+ for section in section_objects:
+ sections += '\n'
+
+ if section['title']:
+ sections += '%s
\n' % section['title']
+
+ sections += linebreaks(section['body'], autoescape=False)
+ sections += '\n\n'
+
+
+ return mark_safe(sections)