dict:
+ """
+ Retrieve the width and height for a given image type from the app state.
+
+ Args:
+ request (Request): FastAPI request object.
+ image_type (str): The type of the image (e.g., 'thumbnails').
+
+ Returns:
+ dict: A dictionary with 'width' and 'height'.
+ """
+
+ image_sizes = self.app.state.IMAGE_SIZES
+ if image_type not in image_sizes:
+ raise ValueError( f"Invalid image type: {image_type}. Must be one of {list( image_sizes.keys() )}" )
+
+ return image_sizes[image_type]
+
+ def _add_routes(self):
+ self.router.add_api_route(
+ "/image/{category}/{type}/{filename}",
+ self.get_image,
+ methods=["GET"],
+ response_class=FileResponse,
+ )
+
+ async def get_image(self, category: str, type: str, filename: str):
+ """
+ Retrieve an image file from the specified category and type.
+ """
+ file_path = self._resolve_path(category, type, filename)
+ return FileResponse(file_path)
+
+ def validate_image(self, file_path:FileHandler=None, width:int=None, height:int=None, overwrite = True ) -> bool:
+ if not os.path.exists( file_path.dest_filename ):
+ with Image.open( file_path.src_file ) as img:
+ print(file_path.src_file)
+ self._resize_image( img, file_path, width, height )
+ return True
+
+ with Image.open( file_path.dest_filename ) as img:
+ if img.width != width or img.height != height:
+ if overwrite:
+ self._resize_image( img, file_path, width, height )
+ return False
+ return True
+
+ def _resize_image(self, img: Image.Image, file_path: FileHandler, width: int, height: int):
+ resized_img = img.resize( (width, height), Image.Resampling.LANCZOS )
+ os.makedirs(file_path.dest_path,exist_ok = True)
+ resized_img.save( file_path.dest_filename )
+
+
+ async def get_image(self, category: str, type: str, filename: str):
+ file_path = self._resolve_path( category, type, filename )
+ return FileResponse( file_path )
+
+ def image_tag(self, category: str, image_type: str, filename: str, alt: str = "", width: int = None,
+ height: int = None) -> str:
+ """
+ Generate an HTML
tag with default sizes if dimensions are not provided.
+ """
+ # Use default sizes if none are provided
+ default_size = self.get_image_size( image_type)
+ width = width or default_size.get( "width" )
+ height = height or default_size.get( "height" )
+
+
+
+ #file_path = self._resolve_path( category, image_type, filename )
+ file_path = FileHandler(category = category,image_type = image_type,filename = filename)
+ self.validate_image( file_path, width = width,height=height, overwrite = True )
+
+ tag = f'
"
+
+ return tag
\ No newline at end of file
diff --git a/app/services/markdown_processor.py b/app/services/markdown_processor.py
index 71af55f..c1c6927 100644
--- a/app/services/markdown_processor.py
+++ b/app/services/markdown_processor.py
@@ -1,8 +1,7 @@
import os
-
from bs4 import BeautifulSoup
-
-from app.services.markdown_render import render_markdown_with_jinja # Your custom renderer
+from fastapi import FastAPI
+from app.services.markdown_render import MarkdownRenderer
from jinja2 import Environment, FileSystemLoader
@@ -12,7 +11,7 @@ class MarkdownProcessor:
'index.html' per category directory using a custom rendering engine.
"""
- def __init__(self, input_dir: str, templates_dir: str):
+ def __init__(self, input_dir: str, templates_dir: str,app:FastAPI=None):
"""
Initialize the MarkdownProcessor.
@@ -22,6 +21,8 @@ class MarkdownProcessor:
"""
self.input_dir = input_dir
self.env = Environment(loader=FileSystemLoader(templates_dir))
+ self.app = app
+
def _process_markdown_files_in_directory(self, directory_path: str) -> list:
"""
@@ -33,15 +34,21 @@ class MarkdownProcessor:
Returns:
list: A list of processed sections containing metadata and rendered content.
"""
+ from pathlib import Path
+
sections = []
+
+
+
for file in sorted(os.listdir(directory_path)):
if file.endswith(".md"):
file_path = os.path.join(directory_path, file)
with open(file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
-
+ markdown_render = MarkdownRenderer(file_path=file_path,app=self.app)
# Process Markdown and Jinja2
- rendered_content, metadata = render_markdown_with_jinja(markdown_content)
+
+ rendered_content, metadata = markdown_render.render_markdown_with_jinja ( markdown_content )
# Append the section to the list
sections.append({
diff --git a/app/services/markdown_render.py b/app/services/markdown_render.py
index 495364a..8c6d491 100644
--- a/app/services/markdown_render.py
+++ b/app/services/markdown_render.py
@@ -1,123 +1,168 @@
+from pathlib import Path
+import sys
import markdown
+from fastapi import FastAPI
from jinja2 import Environment, DictLoader
-from .image_controller import ImageHandler
-# Define Jinja2 custom functions
-def img_left_overlay(src):
- """Render an image with overlay."""
- return f'''
-
-

-
Overlay Text
-
- '''
+from markupsafe import Markup
+from .image_service import ImageService
-def box(title, content):
- """Render a box component."""
- return f'''
-
- '''
-
-def note(content):
- """Render a note component."""
- return f'''
-
- '''
-def link_to(title, url):
- """Render a box component."""
- return f'''
- {title}
- '''
-
-def warning(content):
- """Render a warning component."""
- return f'''
-
- '''
+class MarkdownRenderer:
+ def __init__(self, file_path: str = None, app: FastAPI=None):
+ """
+ Initialize the MarkdownRenderer with a Jinja2 environment and custom functions.
+ """
+ self.app = app
+ self.image_service = ImageService(self.app)
+ self.jinja_env = self._create_jinja_environment()
+ self.file_path = file_path
-def slider(options, images):
- """Render a slider using the provided HTML structure."""
- import uuid
- modal_id = uuid.uuid4().hex.upper()[0:6] # Lets create some uniq modals
- width = options.get("width", 500)
- height = options.get("height", 375)
+ def _create_jinja_environment(self) -> Environment:
+ """
+ Create and configure the Jinja2 environment with custom functions.
- html_content = []
- html_content.append('' )
- html = '\n'.join( html_content )
+ env.globals.update({
+ "img_left_overlay": self.img_left_overlay,
+ "box": self.box,
+ "note": self.note,
+ "warning": self.warning,
+ "link_to": self.link_to,
+ "slider": self.slider,
+ "image": self.get_image, # Add image handler function
+ })
+ return env
- return html
+ def img_left_overlay(self, src: str) -> str:
+ """Render an image with overlay."""
+ return f'''
+
+

+
Overlay Text
+
+ '''
+
+ def box(self, title: str, content: str) -> str:
+ """Render a box component."""
+ return f'''
+
+ '''
+
+ def note(self, content: str) -> str:
+ """Render a note component."""
+ return f'''
+
+ '''
+
+ def link_to(self, title: str, url: str) -> str:
+ """Render a link component."""
+ return f'''
+ {title}
+ '''
+
+ def warning(self, content: str) -> str:
+ """Render a warning component."""
+ return f'''
+
+ '''
+
+ def slider(self, options: dict, images: list) -> str:
+ """Render a slider component."""
+ import uuid
+ modal_id = uuid.uuid4().hex.upper()[0:6]
-def create_jinja_environment():
- """Create and configure the Jinja2 environment."""
- env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
- image_handler = ImageHandler(base_dir="static/images")
+ html_content = []
+ html_content.append('')
+ return '\n'.join(html_content)
- })
- return env
+ def _get_category(self):
+ if isinstance(self.file_path, str):
+ this_path = Path(self.file_path)
+ return this_path.parent.name
-def render_markdown_with_jinja(markdown_content: str):
- """
- Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
+ return True
- Args:
- markdown_content (str): Raw Markdown content.
- Returns:
- tuple: Rendered HTML content and metadata as a dictionary.
- """
- # Step 1: Convert Markdown to HTML and extract metadata
- md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
- intermediate_html = md.convert(markdown_content)
- metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
+ def get_image(self, image_type: str, filename: str, alt: str = "", width: int = None, height: int = None) -> Markup:
+ """
+ Generate a dynamic HTML
tag for an image using ImageService's image_tag method.
+ """
+ valid_types = ['thumbnails', 'large', 'small', 'original']
+ if image_type not in valid_types:
+ sys.tracebacklimit = 0
+ raise ValueError( f"Invalid image type: {image_type}. Must be one of {valid_types}" )
- # Step 2: Pass the resulting HTML with Jinja2 custom tags through Jinja2
- env = create_jinja_environment()
+ tag = self.image_service.image_tag(
+ category=self._get_category(),
+ image_type=image_type,
+ filename=filename,
+ alt=alt,
+ width=width,
+ height=height
+ )
+ my_tag = Markup(tag)
+ print(my_tag)
+ return my_tag
- template = env.get_template("base_template")
- final_html = template.render(content=intermediate_html)
- # Step 3: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
- final_output = env.from_string(final_html).render()
- return final_output, metadata
+
+ def render_markdown_with_jinja(self, markdown_content: str):
+ """
+ Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
+
+ Args:
+ markdown_content (str): Raw Markdown content.
+
+ Returns:
+ tuple: Rendered HTML content and metadata as a dictionary.
+ """
+
+ md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
+ intermediate_html = md.convert(markdown_content)
+ metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
+
+ # Step 3: Pass the resulting HTML with Jinja2 custom tags through Jinja2
+ template = self.jinja_env.get_template("base_template")
+ final_html = template.render(content=intermediate_html)
+
+ # Step 4: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
+ final_output = self.jinja_env.from_string(final_html).render()
+
+ return final_output, metadata
\ No newline at end of file
diff --git a/app/services/metadata_processor.py b/app/services/metadata_processor.py
index cfe923b..de8a0e0 100644
--- a/app/services/metadata_processor.py
+++ b/app/services/metadata_processor.py
@@ -3,6 +3,8 @@ import markdown
import json
from typing import List, Dict
+from fastapi import FastAPI
+
class MetadataProcessor:
"""
@@ -10,7 +12,7 @@ class MetadataProcessor:
and generate a structured JSON file.
"""
- def __init__(self, input_dir: str, output_file: str):
+ def __init__(self, input_dir: str, output_file: str,app:FastAPI=None):
"""
Initialize the MetadataProcessor.
diff --git a/data.old/bolig/index.html b/data.old/bolig/index.html
deleted file mode 100644
index 6298baa..0000000
--- a/data.old/bolig/index.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
Opsumering: Lidt omkring job situationen og hvordan det fungere
-
-
-
- Untitled
-
-
Bolig Bolig Bolig Bolig - Hvor skal sengen placeres
-
Nu bliver det spænde!
-
-
-
Dette er stadig en test side
-
-
-
{img-left-overlay: images/my-cat.png}
-
-
-
-
-
diff --git a/data.old/bolig/lidt_omkring_boliger.md b/data.old/bolig/lidt_omkring_boliger.md
deleted file mode 100644
index f7e66c8..0000000
--- a/data.old/bolig/lidt_omkring_boliger.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Generalt
-description: Hvem, hvad og hvor
-author: Henrik Jess
-date: ons 11 dec 22:16:13 CET 2024
-summary: Lidt omkring job situationen og hvordan det fungere
-favorite: true
-image: images/pic07.jpg
----
-
-# Bolig Bolig Bolig Bolig - Hvor skal sengen placeres
-
-Nu bliver det spænde!
-
-{{ note("Dette er stadig en test side") }}
-
-{img-left-overlay: images/my-cat.png}
diff --git a/data.old/job/det_ved_jeg_om_job.md b/data.old/job/det_ved_jeg_om_job.md
deleted file mode 100644
index 5a09157..0000000
--- a/data.old/job/det_ved_jeg_om_job.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: Job
-description: Hvem, hvad og hvor
-author: Henrik Jess
-date: ons 11 dec 22:16:13 CET 2024
-summary: Lidt omkring job situationen og hvordan det fungere
-favorite: true
-image: images/pic04.jpg
----
-
-# Lidt mere info om job
-
-Der skal langt mere tekst her
-
-
-
-
-
-{{ note("Husk alpha side") }}
\ No newline at end of file
diff --git a/data.old/job/index.html b/data.old/job/index.html
deleted file mode 100644
index 9e7fd3c..0000000
--- a/data.old/job/index.html
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
Opsumering: Lidt omkring job situationen og hvordan det fungere
-
-
-
- Untitled
-
-
Lidt mere info om job
-
Der skal langt mere tekst her
-
-
-
-
-
-
-
-
-
-
-
Opsumering: Lidt omkring job situationen og hvordan det fungere
-
-
-
- Untitled
-
-
Overskrift 1
-
Overskrift 2
-
Overskrift 3
-
Overskrift4
-
Here is a custom box:
-
-
-
Important Title
-
This is the content inside the box.
-
-
-
Here is a note:
-
-
-
This is a note for the readers.
-
-
-
-
-
-
-
diff --git a/data.old/job/job1.md b/data.old/job/job1.md
deleted file mode 100644
index 4a35e4d..0000000
--- a/data.old/job/job1.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-name: Job Job Job Job Job
-description: Hvem, hvad og hvor
-author: Henrik Jess
-date: ons 11 dec 22:16:13 CET 2024
-summary: Lidt omkring job situationen og hvordan det fungere
-favorite: true
-image: images/pic04.jpg
----
-
-
-# Overskrift 1
-## Overskrift 2
-### Overskrift 3
-#### Overskrift4
-Here is a custom box:
-
-{{ box("Important Title", "This is the content inside the box.") }}
-
-Here is a note:
-
-{{ note("This is a note for the readers.") }}
diff --git a/data.old/kontor/index.html b/data.old/kontor/index.html
deleted file mode 100644
index 555e8e8..0000000
--- a/data.old/kontor/index.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
Opsumering: Lad os snakke kontor fælleskaber
-
-
-
- Untitled
-
-
Kontorfællesskab!
-
Der skal langt mere tekst her
-
-
-
-
-
-
-
-
diff --git a/data.old/kontor/lidt_om_kontore.md b/data.old/kontor/lidt_om_kontore.md
deleted file mode 100644
index 7b9695e..0000000
--- a/data.old/kontor/lidt_om_kontore.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: Kontor
-description: Kontorfælleskaber osv
-author: Henrik Jess
-date: today
-summary: Lad os snakke kontor fælleskaber
-favorite: false
-image: images/pic05.jpg
----
-
-# Kontorfællesskab!
-
-Der skal langt mere tekst her
-
-
-{{ note("Husk alpha side") }}
\ No newline at end of file
diff --git a/data.old/skat/index.html b/data.old/skat/index.html
deleted file mode 100644
index 0db8b94..0000000
--- a/data.old/skat/index.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
Opsumering: Jeg er langt fra expert, men her er lidt hvad jeg har indsamlet omkring skat
-
-
-
- Untitled
-
-
Skat! - Det skal jo også være sjovt og leve
-
dette er mere tekst omkring skat
-
-
-
-
-
diff --git a/data.old/skat/portugal_vs_dansk_skat.md b/data.old/skat/portugal_vs_dansk_skat.md
deleted file mode 100644
index a6db716..0000000
--- a/data.old/skat/portugal_vs_dansk_skat.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-name: Skat
-description: SKAT,SKAT, SKAT - Det skal betales den slags
-author: Henrik Jess
-date: today
-summary: Jeg er langt fra expert, men her er lidt hvad jeg har indsamlet omkring skat
-favorite: false
-image: images/pic07.jpg
----
-
-# Skat! - Det skal jo også være sjovt og leve
-
-dette er mere tekst omkring skat
\ No newline at end of file
diff --git a/data.old/skole/index.html b/data.old/skole/index.html
deleted file mode 100644
index c5aa2c0..0000000
--- a/data.old/skole/index.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
Opsumering: Nørj det er lidt spændende..
-
-
-
- Untitled
-
-
Skole start!
-
dette er mere tekst omkring skole
-
Skole efter lidt tid
-
HA!
-
-
-
-
-
diff --git a/data.old/skole/skole.md b/data.old/skole/skole.md
deleted file mode 100644
index b56fb23..0000000
--- a/data.old/skole/skole.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Skole
-description: Skole, ny kultur og menesker
-author: Erika Nielsen
-date: today
-summary: Nørj det er lidt spændende..
-favorite: false
-image: images/pic07.jpg
----
-
-# Skole start!
-
-dette er mere tekst omkring skole
-
-# Skole efter lidt tid
-
-HA!
diff --git a/data/Kultur/0200_kultur_unikt.md b/data/Kultur/0200_kultur_unikt.md
index 4a3d521..f19ce24 100644
--- a/data/Kultur/0200_kultur_unikt.md
+++ b/data/Kultur/0200_kultur_unikt.md
@@ -69,7 +69,6 @@ Lissabon føles på mange måder som København – med fokus på kultur, kvalit
Samtidig er mange oplevelser billigere end i København, og her er en atmosfære, der er svær at finde nordpå: Lissabon har en unik blanding af tradition og modernitet, som gør den til en by, der bliver ved med at fascinere.
-{{ image("pic11.jpg", 200, 150, "thumbnail", "Example Image") }}
### Lidt billeder
diff --git a/data/_frontpage/frontpage.md b/data/_frontpage/frontpage.md
index b23acef..00b38b8 100644
--- a/data/_frontpage/frontpage.md
+++ b/data/_frontpage/frontpage.md
@@ -1,7 +1,8 @@
# Min Drøm om Portugal
## En Frisk Start for Henrik og Erika
-{{ image("pic09.jpg", 475, 100, "", "Example Image") }}
+
+{{ image('thumbnails', 'pic11.jpg', alt='Mit fantatiske billed') }}
---
@@ -14,3 +15,4 @@ For Erika er det også en chance for at opleve en ny kultur, møde nye mennesker
Denne hjemmeside er en samling af mine tanker, forberedelser og erfaringer på vejen mod at gøre drømmen til virkelighed. Måske overvejer du selv en lignende rejse, eller måske er du bare nysgerrig på, hvordan det er at tage springet. Her deler jeg min rejse – fra de første spæde idéer til de praktiske skridt mod et nyt liv i Portugal. For mig og for Erika.
---
+
diff --git a/depricated/convert_markdown_to_html.py b/depricated/convert_markdown_to_html.py
deleted file mode 100644
index f05ea83..0000000
--- a/depricated/convert_markdown_to_html.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-from app.services.markdown_render import render_markdown_with_jinja
-
-def process_markdown_files(input_dir: str, output_dir: str):
- """
- Recursively process all Markdown files in the input directory,
- render them to HTML, and save them to the output directory.
- """
- for root, _, files in os.walk(input_dir):
- for file in files:
- if file.endswith(".md"):
- input_file_path = os.path.join(root, file)
-
- # Determine output file path (convert .md to .html)
- relative_path = os.path.relpath(input_file_path, input_dir)
- output_file_path = os.path.join(output_dir, os.path.splitext(relative_path)[0] + ".html")
-
- # Ensure the output directory exists
- os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
-
- # Read Markdown content
- with open(input_file_path, "r", encoding="utf-8") as md_file:
- markdown_content = md_file.read()
-
- # Render Markdown with Jinja2
- print(f"Processing: {input_file_path} -> {output_file_path}")
- rendered_html = render_markdown_with_jinja(markdown_content)
-
- # Write the rendered HTML to the output file
- with open(output_file_path, "w", encoding="utf-8") as html_file:
- html_file.write(rendered_html)
-
- print("Markdown processing complete!")
-
-if __name__ == "__main__":
- # Input directory containing Markdown files
- input_directory = "./data"
-
- # Output directory where HTML files will be stored
- output_directory = "./data"
-
- # Start the processing
- process_markdown_files(input_directory, output_directory)
diff --git a/depricated/markdown_service.py b/depricated/markdown_service.py
deleted file mode 100644
index b2f4fc9..0000000
--- a/depricated/markdown_service.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import os
-from app.services.markdown_render import render_markdown_with_jinja
-
-def process_markdown_files(input_dir: str, output_dir: str):
- """
- Recursively process all Markdown files in the input directory,
- render them to HTML, and save them in the output directory.
- """
- for root, _, files in os.walk(input_dir):
- for file in files:
- if file.endswith(".md"):
- input_file_path = os.path.join(root, file)
- relative_path = os.path.relpath(input_file_path, input_dir)
- output_file_path = os.path.join(output_dir, os.path.splitext(relative_path)[0] + ".html")
-
- os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
-
- with open(input_file_path, "r", encoding="utf-8") as md_file:
- markdown_content = md_file.read()
-
- print(f"Processing: {input_file_path} -> {output_file_path}")
- rendered_html = render_markdown_with_jinja(markdown_content)
-
- with open(output_file_path, "w", encoding="utf-8") as html_file:
- html_file.write(rendered_html)
diff --git a/depricated/mock_data.json b/depricated/mock_data.json
deleted file mode 100644
index 59881d5..0000000
--- a/depricated/mock_data.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "categories": [
- {"name": "SKAT", "path": "skat", "author": "Henrik"},
- {"name": "Skole", "path": "skole", "author": "Erika"},
- {"name": "Bolig", "path": "bolig", "author": "Henrik"},
- {"name": "Job", "path": "job", "author": "Henrik"}
- ],
- "favorites": [
- {"name": "SKAT", "image": "images/pic07.jpg", "description": "Favorit Kategori"},
- {"name": "Skole", "image": "images/pic08.jpg", "description": "Skole information"},
- {"name": "Bolig", "image": "images/pic09.jpg", "description": "Bolig detaljer"}
- ]
-}
\ No newline at end of file
diff --git a/depricated/test_markdown_render.py b/depricated/test_markdown_render.py
deleted file mode 100644
index d71869a..0000000
--- a/depricated/test_markdown_render.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from app.services.markdown_render import MarkdownRenderer
-
-# Initialize MarkdownRenderer
-renderer = MarkdownRenderer()
-
-# Test Markdown input
-markdown_content = """
-{img-left-overlay: src=my-cat.png}
-{box: title=Test Box, content=This is a test.}
-{note: content=This is a note.}
-{warning: content=Be careful!}
-"""
-
-# Render to HTML
-print("Rendering Markdown...")
-html_output = renderer.render(markdown_content)
-print("Rendered HTML:")
-print(html_output)
\ No newline at end of file
diff --git a/fonts/open-sans-v40-latin.zip b/fonts/open-sans-v40-latin.zip
deleted file mode 100644
index eaa737f..0000000
Binary files a/fonts/open-sans-v40-latin.zip and /dev/null differ
diff --git a/fonts/roboto-slab-v34-latin.zip b/fonts/roboto-slab-v34-latin.zip
deleted file mode 100644
index 71f78b9..0000000
Binary files a/fonts/roboto-slab-v34-latin.zip and /dev/null differ
diff --git a/templates/navigation.html b/templates/navigation.html
index b37ef0f..f4f32a5 100644
--- a/templates/navigation.html
+++ b/templates/navigation.html
@@ -26,6 +26,7 @@
{% for favorite in data.favorites %}
+
{{ favorite.path }}: {{ favorite.description }}