From 97218ce11f4f5b1cae1f89547b446def8d45d32c Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Tue, 9 Aug 2022 00:23:00 +0200
Subject: [PATCH] did a lot of stuff

---
 config.json                                  |  17 +-
 fgs/__main__.py                              |   4 +-
 fgs/datatypes.py                             | 175 +++++++++++++++----
 fgs/generator.py                             |  13 +-
 fgs/pandoc.py                                |  64 ++++---
 fgs/reader.py                                |  20 +--
 fgs/writer.py                                |  26 +--
 theme/templates/base.html                    |   2 +-
 theme/templates/macros/cards.html            |  12 +-
 theme/templates/macros/content_renderer.html |  18 +-
 theme/templates/macros/getters.html          |  28 +--
 theme/templates/macros/link.html             | 115 +++---------
 theme/templates/macros/nav.html              |  35 ++--
 theme/templates/macros/renderers.html        |  38 +---
 theme/templates/page.html                    |  30 ++--
 theme/templates/tag.html                     |   4 +-
 16 files changed, 293 insertions(+), 308 deletions(-)

diff --git a/config.json b/config.json
index c450581..5b82865 100644
--- a/config.json
+++ b/config.json
@@ -3,24 +3,21 @@
 	"static_directories": [
 		"images"
 	],
+	"homepage_slug": "index",
 	"theme": {
 		"default_tag_color": "hsl(289, 43%, 56%)",
-		"homepage_slug": "index",
 		"static_dir": "theme",
 		"css_file": "main.css",
-		"default_template": "page.html",
-		"link_target": {
-			"internal": "_self",
-			"external": "_blank"
-		}
+		"default_template": "page.html"
+	},
+	"link_target": {
+		"internal": "_self",
+		"external": "_blank"
 	},
 	"default_status": "published",
-	"toc_depth": 3,
 	"pandoc": {
 		"base": "markdown",
-		"args": [
-			"--table-of-contents"
-		],
+		"args": [],
 		"extensions": [
 			"+abbreviations",
 			"+all_symbols_escapable",
diff --git a/fgs/__main__.py b/fgs/__main__.py
index 266388e..501fed3 100644
--- a/fgs/__main__.py
+++ b/fgs/__main__.py
@@ -31,6 +31,7 @@ def main():
     factories['author'] = datatypes.AuthorFactory(config, factories)
     factories['tag'] = datatypes.TagFactory(config, factories)
     factories['link'] = datatypes.LinkFactory(config, factories)
+    factories['config'] = datatypes.LocalizedConfigFactory(config, factories)
 
     mdreader = reader.MarkdownReader(config, factories)
     pages = []
@@ -59,6 +60,8 @@ def main():
 
 
 
+
+
 def read_static_dir(directory, static_files, subpath = []):
     print("static_dir: " + directory);
     for filename in os.listdir(directory):
@@ -86,7 +89,6 @@ def parse_dir(directory, pages, mdreader, subpath = []):
 
 
 
-    
 
 if __name__ == '__main__':
     main()
diff --git a/fgs/datatypes.py b/fgs/datatypes.py
index 71605d7..22783e3 100644
--- a/fgs/datatypes.py
+++ b/fgs/datatypes.py
@@ -1,6 +1,44 @@
 import datetime
 from dateutil import parser as dtparser
 
+class DictConverter:
+
+    def __init__(self, factories):
+        self.factories = factories
+
+    def convert(self, item, lang, key = None):
+        if isinstance(item, dict):
+            res = {}
+            for k in item.keys():
+                res[k] = self.convert(item[k], lang, k)
+            return res
+        elif isinstance(item, list):
+            res = []
+            for i in item:
+                res.append(self.convert(i, lang))
+            return res
+        elif isinstance(item, str) and key == "link":
+            return self.factories['link'].get_by_raw(item, lang)
+        elif isinstance(item, str) and key == "tag":
+            return self.factories['tag'].get(item, lang)
+        #elif isinstance(item, str) and key == "slug":
+        #    return self.factories['page'].get(item, lang)
+        else:
+            return item
+
+
+
+class LocalizedConfigFactory(DictConverter):
+    def __init__(self, config, factories):
+        super().__init__(factories)
+        self.config = config
+        self.store = {}
+
+    def get(self, lang):
+        if lang not in self.store:
+            self.store[lang] = self.convert(self.config, lang, None)
+        return self.store[lang]
+
 class PageFactory:
     def __init__(self, config, factories):
         self.config = config
@@ -14,13 +52,13 @@ class PageFactory:
 
     def get(self, slug, lang):
         if not self.has(slug, lang):
-            self.store[lang][slug] = Page(slug, lang, self.config, self.factories)
+            self.store[lang][slug] = Page(slug, lang, self.factories)
         return self.store[lang][slug];
 class Page:
-    def __init__(self, slug, lang, config, factories):
+    def __init__(self, slug, lang, factories):
         self.slug = slug
         self.lang = lang
-        self._config = config
+        self._config = None
         self._factories = factories
         self.initialized = False
 
@@ -29,18 +67,19 @@ class Page:
         self.filename =  filename
         self.subpath =  subpath
         self.raw =  raw
-        self._metadata =  metadata
+        dictconverter = DictConverter(self._factories)
+        self._metadata =  dictconverter.convert(metadata, self.lang)
         self.content =  content
         self.title =  title
         self.category = self._factories['tag'].get(category, self.lang)
-        self.date_created =  Date(date_created, self._config)
-        self.date_modified =  Date(date_modified, self._config)
+        self.date_created =  Date(date_created, self.config)
+        self.date_modified =  Date(date_modified, self.config)
         self.status =  status
 
         # authors
-        self.authors = []
+        self.authors = set()
         for local_part, domain, name in authors:
-            self.authors.append(self._factories['author'].get(local_part + '@' + domain, name))
+            self.authors.add(self._factories['author'].get(local_part + '@' + domain, name))
         if last_modification_author:
             self.last_modification_author = self._factories['author'].get(last_modification_author[0] + '@' + last_modification_author[1], last_modification_author[2])
         else:
@@ -55,9 +94,15 @@ class Page:
         #self.url = self.lang + '/' + self.category.name + '/' + self.slug + ".html"
         self.link = self._factories['link'].get_by_type("slug", self.slug, self.lang)
 
+    def get_config(self):
+        if not self._config:
+            self._config = self._factories['config'].get(self.lang)
+        return self._config
+    config = property(get_config, None, None)
+
 
     def get_metadata(self):
-        deflang = self._config['lang']['default']
+        deflang = self.config['lang']['default']
         if self.lang == deflang:
             return self._metadata
         defpage = self._factories['page'].get(self.slug, deflang)
@@ -67,12 +112,14 @@ class Page:
     metadata = property(get_metadata, None, None)
 
     def get_tags(self):
+        if not self.initialized:
+            raise Exception("Page not initialized.")
         res = []
         if 'tags' in self.metadata:
             rawtags = self.tags_str_to_list(self.metadata['tags'])
             for t in rawtags:
                 if (t != self.category.name):
-                    tag = factories['tag'].get(t, self.lang)
+                    tag = self._factories['tag'].get(t, self.lang)
                     tag.reg_page(self)
                     res.append(tag)
         res.append(self.category) # the category is also a default tag
@@ -96,17 +143,17 @@ class Page:
             else:
                 res['prio']= 0
 
-            start = Date("min", self._config)
+            start = Date("min", self.config)
             if 'start' in lrel:
-                start = Date(lrel['start'], self._config)
+                start = Date(lrel['start'], self.config)
             res['start'] = start
 
-            end = Date("max", self._config)
+            end = Date("max", self.config)
             if 'end' in lrel:
-                end = Date(lrel['end'], self._config)
+                end = Date(lrel['end'], self.config)
             res['end'] = end
 
-            build_date = Date("now", self._config)
+            build_date = Date("now", self.config)
             if (build_date > end):
                 res['was_relevant'] = True
             elif (build_date < start):
@@ -120,7 +167,7 @@ class Page:
         if 'template' in self.metadata:
             return self.metadata['template']
         else:
-            return self._config['theme']['default_template']
+            return self.config['theme']['default_template']
     template = property(get_template, None, None)
 
 
@@ -154,6 +201,8 @@ class TagFactory:
     def get(self, name, lang):
         if not isinstance(name, str):
             raise Exception("name is not a string", name, type(name))
+        if lang == None:
+            raise Exception("cannot get None lang for tag", name)
         if name not in self.store[lang]:
             self.store[lang][name] = Tag(name, lang, self.factories)
         return self.store[lang][name];
@@ -163,12 +212,19 @@ class Tag:
     def __init__(self, name, lang, factories):
         self.name = name
         self.lang = lang
+        self._config = None
         self.pages = set()
         self.is_category = False
         self._link = None
         self._factories = factories
 
 
+    def get_config(self):
+        if not self._config:
+            self._config = self._factories['config'].get(self.lang)
+        return self._config
+    config = property(get_config, None, None)
+
     def get_link(self):
         if not self._link:
             self._link = self._factories['link'].get_by_type("tag", self.name, self.lang)
@@ -183,10 +239,10 @@ class Tag:
     page = property(get_page, None, None)
 
     def get_color(self):
-        if 'color' in self.page.metadata:
+        if self.page and 'color' in self.page.metadata:
             return self.page.metadata['color']
         else:
-            return self.config['default_tag_color']
+            return self.config['theme']['default_tag_color']
     color = property(get_color, None, None)
 
     def get_title(self):
@@ -263,16 +319,24 @@ class LinkFactory:
         self.factories = factories
         self.store = {}
     def get_by_raw(self, rawurl, deflang):
-        if rawurl not in self.store:
-            self.store[rawurl] = Link(rawurl, self.factories, deflang)
-        return self.store[rawurl];
+        link = Link(rawurl, self.factories, deflang) # this is dirty
+        lang = link.lang
+        if lang == None:
+            raise Exception("Link lang is None", lang, deflang, link.raw)
+        if lang not in self.store:
+            self.store[lang] = {}
+        if rawurl not in self.store[lang]:
+            self.store[lang][rawurl] = link
+        return self.store[lang][rawurl];
     def get_by_type(self, reftype, refid, reflang):
         rawurl = reftype + ':' + refid + ':' + reflang
         return self.get_by_raw(rawurl=rawurl, deflang=reflang)
 class Link:
     def __init__(self, rawurl, factories, deflang):
         self.raw = rawurl
+        self._factories = factories
         self.is_external = False
+        self.lang = deflang
 
         urlasplit = rawurl.split('#')
 
@@ -283,13 +347,13 @@ class Link:
         urlwoa = urlasplit[0] # url without anchor
         if len(urlwoa) == 0:
             # rawurl: '#some-section'
-            self.url = ""
+            self._url = ""
             self.type = "local"
         else:
             components = urlwoa.split(':')
             if len(components) == 1:
                 # rawurl: 'lageplan.uni-goettingen.de'
-                self.url = "https://" + urlwoa
+                self._url = "https://" + urlwoa
                 self.is_external = True
             else:
                 reftype = components[0]
@@ -299,33 +363,74 @@ class Link:
                     reflang = components[2]
                 if reftype == "slug":
                     # rawurl: 'slug:some-page:de'
-                    self.page = factories['page'].get(refid, reflang)
+                    self._page = None
                     # TODO handle link/alias pages
-                    self.path = '/'.join([self.page.lang, self.page.category.name, self.page.slug + '.html'])
-                    self.url = self.path
+                    self._url = None
                     self.type = reftype
+                    self.lang = reflang
+                    self.refid = refid
                 elif reftype == "tag":
                     # rawurl: 'tag:some-tag:de'
-                    self.tag = factories['tag'].get(refid, reflang)
-                    self.path = '/'.join([self.tag.lang, 'tag', self.tag.name + '.html'])
-                    self.url = self.path
+                    self._tag = None
+                    self._url = None
                     self.type = reftype
+                    self.lang = reflang
+                    self.refid = refid
                 else:
                     # rawurl: 'https://example.com'
-                    self.url = urlwoa
+                    self._url = urlwoa
                     self.is_external = True
 
-        self.urlwithanchor = self.url
-        if self.anchor:
-            self.urlwithanchor = self.url + "#" + self.anchor
+    def get_page(self):
+        if not self._page:
+            self._page = self._factories['page'].get(self.refid, self.lang)
+        return self._page
+    page = property(get_page, None, None)
 
+    def get_tag(self):
+        if not self._tag:
+            self._tag = self._factories['tag'].get(self.refid, self.lang)
+        return self._tag
+    tag = property(get_tag, None, None)
+
+    def get_category(self):
+        if self.type == "slug":
+            return self.page.category
+        elif self.type == "tag":
+            if self.tag.is_category:
+                return self.tag
+            else:
+                raise Exception("category can only be called on category tags")
+        else:
+            raise Exception("category is not allowed to be called here")
+    category = property(get_category, None, None)
+
+    def get_path(self):
+        if self.type == "slug":
+            return '/'.join([self.page.lang, self.page.category.name, self.page.slug + '.html'])
+        elif self.type == "tag":
+            return '/'.join([self.tag.lang, 'tag', self.tag.name + '.html'])
+        else:
+            raise Exception("path is not allowed to be called here", self.type)
+    path = property(get_path, None, None)
+
+    def get_url(self):
+        if self._url == None:
+            self._url = self.path
+        return self._url
+    url = property(get_url, None, None)
+
+    def get_urlwithanchor(self):
+        if self.anchor:
+            return self.url + "#" + self.anchor
+        else:
+            return self.url
+    urlwithanchor = property(get_urlwithanchor, None, None)
 
 
 
 
 
-    
-        
 
 
 
diff --git a/fgs/generator.py b/fgs/generator.py
index 6115044..3a97380 100644
--- a/fgs/generator.py
+++ b/fgs/generator.py
@@ -76,8 +76,8 @@ class Generator:
 
 
     def generate_homepage(self, writer, lang, path):
-            page = self.context['pages'][lang][self.config['theme']['homepage_slug']]
-            writer.write_template(page.template, path , lang, {'page': page})
+            page = self.context['pages'][lang][self.config['homepage_slug']]
+            writer.write_template(page.template, path , lang, {'page': page}, page.config)
 
 
     def generate_output(self, writer):
@@ -88,18 +88,15 @@ class Generator:
         for lang in self.config['lang']['supported']:
             # all pages
             for page in self.context['pages'][lang].values():
-                writer.write_template(page.template, page.link.path, lang, {'page': page})
+                writer.write_template(page.template, page.link.path, lang, {'page': page}, page.config)
 
             # all tags
             for tag in self.context['tags'][lang].values():
-                writer.write_template('tag.html', tag.link.path, lang, {'tag': tag})
+                writer.write_template('tag.html', tag.link.path, lang, {'tag': tag}, tag.config)
 
             # homepages for languages
             self.generate_homepage(writer, lang, lang + "/index.html")
-        
-            
+
         # homepage
         self.generate_homepage(writer, self.config['lang']['default'], "index.html")
-            
-        
 
diff --git a/fgs/pandoc.py b/fgs/pandoc.py
index d3ebc20..5218451 100644
--- a/fgs/pandoc.py
+++ b/fgs/pandoc.py
@@ -4,7 +4,7 @@ import subprocess
 import json
 import sys
 
-def run_pandoc(source, base="markdown", extensions=[], extra_args=[]):
+def run_pandoc(source, factories, lang, base="markdown", extensions=[], extra_args=[]):
     to = "json"
     ext_str = ""
     if isinstance(extensions, list):
@@ -42,7 +42,7 @@ def run_pandoc(source, base="markdown", extensions=[], extra_args=[]):
     blocks = []
     for raw_block in raw_blocks:
         #print('raw_block: ', type(raw_block), raw_block)
-        block = parse_from_register(block_parsing_register, raw_block)
+        block = parse_from_register(factories, lang, block_parsing_register, raw_block)
         if block != None:
             blocks.append(block)
 
@@ -53,11 +53,11 @@ def run_pandoc(source, base="markdown", extensions=[], extra_args=[]):
     # TODO TOC
     #contentmetadata["toc"] = build_toc(n["toc_list"].copy())
 
-    blocks = json.loads(json.dumps(blocks, cls=ElementEncoder)) # Reduce to 'simple' dict, which can be converted to JSON in Jinja2.
+    #blocks = json.loads(json.dumps(blocks, cls=ElementEncoder)) # Reduce to 'simple' dict, which can be converted to JSON in Jinja2.
 
     return (blocks, contentmetadata)
 
-def parse_from_register(reg: dict, h: dict):
+def parse_from_register(factories, lang, reg: dict, h: dict):
     t = h['t'] # pandoc type
     if t not in reg:
         raise Exception("pandoc type not in register", t, h)
@@ -75,35 +75,38 @@ def parse_from_register(reg: dict, h: dict):
             return None
 
         handler = entry['handler']
-        res = handler(entry['etype'])
+        res = handler(factories, lang, entry['etype'])
     else:
         handler = entry
-        res = handler()
+        res = handler(factories, lang)
 
     res.parse(c)
 
     return res
 
 
-class ElementEncoder(json.JSONEncoder):
-    def default(self, obj):
-        if isinstance(obj, Element):
-            res = {}
-            for key, name in obj.export.items():
-                res[name] = getattr(obj, key)
-            return res
-        return super().default(obj)
+#class ElementEncoder(json.JSONEncoder):
+#    def default(self, obj):
+#        if isinstance(obj, Element):
+#            res = {}
+#            for key, name in obj.export.items():
+#                res[name] = getattr(obj, key)
+#            return res
+#        return super().default(obj)
 
 class Element():
-    def __init__(self, etype = None):
+    def __init__(self, factories, lang, etype = None):
+        self.factories = factories
+        self.lang = lang
         if etype != None:
             self.etype = etype
         self.children = []
         self.export = {}
 
-        self.export_key('etype', 'type')
-        self.export_key('eclass', 'class')
-        self.export_key('children')
+        #self.type = etype
+        #self.export_key('etype', 'type')
+        #self.export_key('eclass', 'class')
+        #self.export_key('children')
 
     def addChild(self, child):
         if child != None:
@@ -113,19 +116,14 @@ class Element():
         raise Exception("parse_internal not overridden: ", self)
 
     def parse(self, pandocraw):
-        prevkeys = dir(self)
+        #prevkeys = dir(self)
         self.parse_internal(pandocraw)
-        afterkeys = dir(self)
-        for key in afterkeys:
-            if key not in prevkeys:
-                self.export_key(key)
+        #afterkeys = dir(self)
+        #for key in afterkeys:
+        #    if key not in prevkeys:
+        #        self.export_key(key)
 
 
-    def export_key(self, key, name=None):
-        if name == None:
-            name = key
-        self.export[key] = name
-
 
 
     def parse_blocks(self, raw_blocks):
@@ -148,11 +146,11 @@ class Element():
         return res
 
     def parse_block(self, raw_block):
-        res = parse_from_register(block_parsing_register, raw_block)
+        res = parse_from_register(self.factories, self.lang, block_parsing_register, raw_block)
         self.addChild(res)
         return res
     def parse_inline(self, raw_inline):
-        res = parse_from_register(inline_parsing_register, raw_inline)
+        res = parse_from_register(self.factories, self.lang, inline_parsing_register, raw_inline)
         self.addChild(res)
         return res
 
@@ -185,7 +183,8 @@ class Element():
 
     def parse_target(self, raw_target): # For URLs
         res = {}
-        res['url'] = self.parse_text(raw_target[0])
+        rawurl = self.parse_text(raw_target[0])
+        res['link'] = self.factories['link'].get_by_raw(rawurl, self.lang)
         res['title'] = self.parse_text(raw_target[1])
         return res
 
@@ -336,9 +335,6 @@ class InlineString(Inline): # Text
         # TODO handle abbreviations
 
 class InlineSimple(Inline): # [Inline]
-    def __init__(self, etype):
-        self.etype = etype
-        super().__init__()
 
     def parse_internal(self, pandocraw):
         self.content = self.parse_inlines(pandocraw)
diff --git a/fgs/reader.py b/fgs/reader.py
index a800024..1168bfc 100644
--- a/fgs/reader.py
+++ b/fgs/reader.py
@@ -33,16 +33,6 @@ class MarkdownReader:
 
         category_name = self.get_category_name(metadata, subpath)
 
-        # content
-        content, contentmetadata = pandoc.run_pandoc(source=rawcontent, base=self.config['pandoc']['base'], extensions=self.config['pandoc']['extensions'])
-        metadata.update(contentmetadata) # merge content specific metadata into metadata
-        #print(content)
-
-        # title
-        if 'title' not in metadata:
-            raise Exception("File is missing title in metadata: ", path, subpath)
-        title = metadata['title']
-
         # slug and lang
         pathlist = path.split('/')
         filename = pathlist[-1]
@@ -71,6 +61,16 @@ class MarkdownReader:
         #print("slug: ", slug)
         #print("lang: ", lang)
 
+        # content
+        content, contentmetadata = pandoc.run_pandoc(factories=self.factories, lang=lang, source=rawcontent, base=self.config['pandoc']['base'], extensions=self.config['pandoc']['extensions'])
+        metadata.update(contentmetadata) # merge content specific metadata into metadata
+        #print(content)
+
+        # title
+        if 'title' not in metadata:
+            raise Exception("File is missing title in metadata: ", path, subpath)
+        title = metadata['title']
+
         # date_created and date_modified
         date_modified = datetime.now()
         date_created = datetime.now()
diff --git a/fgs/writer.py b/fgs/writer.py
index 5c12877..ebeb32c 100644
--- a/fgs/writer.py
+++ b/fgs/writer.py
@@ -18,7 +18,7 @@ class Writer:
 
         print("templates: ", self.env.list_templates())
 
-    def write_template(self, template, path, lang, extra_context):
+    def write_template(self, template, path, lang, extra_context, localized_config):
         tmpl = self.env.get_template(template)
 
         pathsplit = path.split('/')
@@ -31,25 +31,25 @@ class Writer:
             for i in range(count):
                 siteurl = siteurl + '/..'
         else:
-            siteurl = config['siteurl']
+            siteurl = self.config['siteurl']
 
         # render template
         context = {}
         context.update(self.context)
-        context["config"] = self.config
-        context["theme"] = self.config['theme']
+        context["config"] = localized_config
+        context["theme"] = localized_config['theme']
         context["template"] = template
-        context["t"] = self.config["lang"] # translate
+        context["t"] = localized_config["lang"] # translate
         context["l"] = lang # current language
         context["path"] = path
         context["siteurl"] = siteurl
         context.update(extra_context)
-        print("template: ", template)
-        print("path: ", path)
-        print("lang: ", lang)
-        print("context: ", context)
-        print("context keys: ", context.keys())
-        print("tags:", type(context['tags']), context['tags'])
+        #print("template: ", template)
+        #print("path: ", path)
+        #print("lang: ", lang)
+        #print("context: ", context)
+        #print("context keys: ", context.keys())
+        #print("tags:", type(context['tags']), context['tags'])
         out = tmpl.render(context)
 
         # write file
@@ -59,8 +59,8 @@ class Writer:
         # write to file
         fullpath = self.output_dir + '/' + path
         directory = os.path.dirname(fullpath)
-        print("fullpath: ", fullpath)
-        print("dir: ", directory)
+        print("writing file: ", fullpath)
+        #print("dir: ", directory)
         os.makedirs(directory, exist_ok=True)
         with open(fullpath, mode) as f:
             f.write(out)
diff --git a/theme/templates/base.html b/theme/templates/base.html
index 52ee81c..99f993c 100644
--- a/theme/templates/base.html
+++ b/theme/templates/base.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 
 {%- import 'macros/getters.html' as get with context -%}
-{%- import 'macros/link.html' as link with context -%}
+{%- import 'macros/link.html' as linkr with context -%}
 {%- import 'macros/cards.html' as cards with context -%}
 {%- import 'macros/renderers.html' as render with context -%}
 {%- import 'macros/content_renderer.html' as content_renderer with context -%}
diff --git a/theme/templates/macros/cards.html b/theme/templates/macros/cards.html
index 675d528..5e8a4fc 100644
--- a/theme/templates/macros/cards.html
+++ b/theme/templates/macros/cards.html
@@ -9,19 +9,17 @@
 	</ul>
 {%- endmacro -%}
 
-{%- macro card(title, url, catcolor, lang) -%}
-	<li style="--category-color: {{ catcolor }}">{{ link.render(url, title, lang) }}</li>
+{%- macro card(title, link, catcolor, lang) -%}
+	<li style="--category-color: {{ catcolor }}">{{ linkr.render(link, title, lang) }}</li>
 {%- endmacro -%}
 
-{%- macro card_from_link(url, lang) -%}
-	{%- call(parsedurl, anchor, reflang, is_external, reftype, refid, refpage, tagcattitle, tagcatcolor) link.parse_url(url, lang) -%}
-		{{ card(title=None, url=url, catcolor=tagcatcolor, lang=lang) }}
-	{%- endcall -%}
+{%- macro card_from_link(link, lang) -%}
+	{{ card(title=None, link=link, catcolor=link.category.color, lang=lang) }}
 {%- endmacro -%}
 
 {%- macro card_from_page(page, lang) -%}
 	{%- if not page.slug.startswith('tag-') -%}
-		{{ card_from_link(url=['slug',page.slug,page.lang]|join(':'), lang=lang) }}
+		{{ card_from_link(link=page.link, lang=lang) }}
 	{%- endif -%}
 {%- endmacro -%}
 
diff --git a/theme/templates/macros/content_renderer.html b/theme/templates/macros/content_renderer.html
index 3c0c116..9c0d509 100644
--- a/theme/templates/macros/content_renderer.html
+++ b/theme/templates/macros/content_renderer.html
@@ -53,8 +53,8 @@
 	{%- endif -%}
 {%- endmacro -%}
 
-{%- macro render_image(url, alt, lang, attr = None, title = None) -%}
-	<img {{ render_attr(attr, lang, extra={"src": url, "title": {"value":title, "escape": true}}) }} alt="
+{%- macro render_image(link, alt, lang, attr = None, title = None) -%}
+	<img {{ render_attr(attr, lang, extra={"src": link.urlwithanchor, "title": {"value":title, "escape": true}}) }} alt="
 		{%- if alt is string -%}
 			{{ alt|e }}
 		{%- else -%}
@@ -70,7 +70,7 @@
 {%- endmacro -%}
 
 {%- macro render_block_or_inline(boi, lang) -%}
-	{%- set eclass = boi['class'] -%}
+	{%- set eclass = boi['eclass'] -%}
 	{%- if eclass == "block" -%}
 		{{ render_block(boi,lang) }}
 	{%- elif eclass == "inline" -%}
@@ -89,7 +89,7 @@
 {%- endmacro -%}
 
 {%- macro render_block(block, lang) -%}
-	{%- set etype = block['type'] -%}
+	{%- set etype = block['etype'] -%}
 	{%- if etype == "header" -%}
 		{{ render_block_header(block, lang) }}
 	{%- elif etype == "paragraph" -%}
@@ -210,7 +210,7 @@
 {%- endmacro -%}
 
 {%- macro render_inline(inline, lang) -%}
-	{%- set etype = inline['type'] -%}
+	{%- set etype = inline['etype'] -%}
 	{%- if etype == "space" -%}
 		{{ render_inline_space(inline, lang) }}
 	{%- elif etype == "string" -%}
@@ -320,17 +320,17 @@
 {%- macro render_inline_link(inline, lang) -%}
 	{%- set attr = inline['attr'] -%}
 	{%- set content = inline['content'] -%}
-	{%- set url = inline['url'] -%}
+	{%- set link = inline['link'] -%}
 	{%- set title = inline['title'] -%}
-	{{ link.render(url, content, lang, attr, title) }}
+	{{ linkr.render(link, content, lang, attr, title) }}
 {%- endmacro -%}
 
 {%- macro render_inline_image(inline, lang) -%}
 	{%- set attr = inline['attr'] -%}
 	{%- set alt = inline['alt'] -%}
-	{%- set url = inline['url'] -%}
+	{%- set link = inline['link'] -%}
 	{%- set title = inline['title'] -%}
-	{{ render_image(url, alt, lang, attr, title) }}
+	{{ render_image(link, alt, lang, attr, title) }}
 {%- endmacro -%}
 
 {%- macro render_inline_math(inline, lang) -%}
diff --git a/theme/templates/macros/getters.html b/theme/templates/macros/getters.html
index f5776dd..2ddbe4c 100644
--- a/theme/templates/macros/getters.html
+++ b/theme/templates/macros/getters.html
@@ -1,28 +1,8 @@
-{%- macro tag_by_name(tagname, lang) -%}
-	{%- set tag = tags[tagname] -%}
-	{{ tag.name }}
-	{%- set ns = namespace(tagpage=None, tagcolor=config.theme.default_tag_color, tagtitle=tag.name) -%}
-	{%- set tagpageslug = ["tag", tag.name]|join('-') -%}
-
-	{%- if tagpageslug in pages[lang] -%}
-		{%- set ns.tagpage = pages[lang][tagpageslug] -%}
-		{%- set ns.tagtitle = ns.tagpage.title -%}
-		{%- call(tagcolor) metadata_entry(tagpageslug, lang, 'color') -%}
-			{%- set ns.tagcolor = tagcolor -%}
-		{%- endcall -%}
-	{%- endif -%}
-	
-	{{- caller(ns.tagtitle, ns.tagcolor, [lang, 'tag', tag.name + '.html']|join('/') , ns.tagpage, tag.pages[lang]) -}}
+{%- macro page_by_slug(slug, lang) -%}
+	{{- caller(pages[lang][slug]) -}}
 {%- endmacro -%}
 
-{%- macro metadata_entry(slug, lang, key) -%}
-	{%- if key in pages[lang][slug].metadata -%}
-		{{- caller(pages[lang][slug].metadata[key]) -}}
-	{%- elif key in pages[t.default][slug].metadata -%}
-		{{- caller(pages[t.default][slug].metadata[key]) -}}
-	{%- endif -%}
+{%- macro tag_by_name(name, lang) -%}
+	{{- caller(tags[lang][name]) -}}
 {%- endmacro -%}
 
-{%- macro page_by_slug(slug, lang) -%}
-	{{- caller(pages[lang][slug]) -}}
-{%- endmacro -%}
diff --git a/theme/templates/macros/link.html b/theme/templates/macros/link.html
index fdff6e5..a0e9388 100644
--- a/theme/templates/macros/link.html
+++ b/theme/templates/macros/link.html
@@ -1,102 +1,35 @@
 {%- import 'macros/content_renderer.html' as content_renderer with context -%}
-{%- import 'macros/getters.html' as get with context -%}
 {%- import 'macros/renderers.html' as renderm with context -%}
 
 
-{%- macro render(url, content, lang, attr = None, title = None) -%}
-	{%- call(parsedurl, anchor, reflang, is_external, reftype, refid, refpage, tagcattitle, tagcatcolor) parse_url(url, lang) -%}
-		{%- set ns = namespace(relation=None, fullurl=parsedurl) -%}
-		{%- if anchor -%}
-			{%- set ns.fullurl = [parsedurl, anchor]|join('#') -%}
-		{%- endif -%}
-		{%- if is_external -%}
-			{%- set ns.relation = "external" -%}
-		{%- else -%}
-			{%- set ns.relation = "internal" -%}
-		{%- endif -%}
-		{%- set target = config.theme.link_target[ns.relation] -%}
-		<a {{ content_renderer.render_attr(attr, lang, extra_classes=[ns.relation], extra={"href": ns.fullurl, "title": {"value":title, "escape": true}, "target": target}) }}>
-			{%- if content is string or content is none -%}
-				{%- if content is string and content|length -%}
-					{{ renderm.softbreak_span(content) }}
-				{%- else -%}
-					{%- if reftype == "tag" -%}
-						{{ renderm.softbreak_span(tagcattitle) }}
-					{%- elif reftype == "slug" -%}
-						{{ renderm.softbreak_span(refpage.title) }}
-					{%- endif -%}
-				{%- endif -%}
+{%- macro render(link, content, lang, attr = None, title = None) -%}
+	{%- set ns = namespace(relation=None, url=link.urlwithanchor) -%}
+	{%- if link.is_external -%}
+		{%- set ns.relation = "external" -%}
+	{%- else -%}
+		{%- set ns.relation = "internal" -%}
+		{%- set ns.url = [siteurl, ns.url]|join("/") -%}
+	{%- endif -%}
+	{%- set target = config.link_target[ns.relation] -%}
+	<a {{ content_renderer.render_attr(attr, lang, extra_classes=[ns.relation], extra={"href": ns.url, "title": {"value":title, "escape": true}, "target": target}) }}>
+		{%- if content is string or content is none -%}
+			{%- if content is string and content|length -%}
+				{{ renderm.softbreak_span(content) }}
 			{%- else -%}
-				{{ content_renderer.render_blocks_or_inlines(content, lang) }}
+				{%- if link.type == "tag" -%}
+					{{ renderm.softbreak_span(link.tag.title) }}
+				{%- elif link.type == "slug" -%}
+					{{ renderm.softbreak_span(link.page.title) }}
+				{%- endif -%}
 			{%- endif -%}
-		</a>
-	{%- endcall -%}
+		{%- else -%}
+			{{ content_renderer.render_blocks_or_inlines(content, lang) }}
+		{%- endif -%}
+	</a>
 {%- endmacro -%}
 
 {%- macro render_tag(tag, lang) -%}
-	{%- set url = ['tag',tag.name]|join(':') -%}
-	{%- call(parsedurl, anchor, reflang, is_external, reftype, refid, refpage, tagcattitle, tagcatcolor) parse_url(url, lang) -%}
-		{%- set attr = {"classes": ["tag-link"], "style": {"--category-color": tagcatcolor}} -%}
-		{{ render(url,None,lang,attr=attr,title=tag.name) }}
-	{%- endcall -%}
+	{%- set attr = {"classes": ["tag-link"], "style": {"--category-color": tag.color}} -%}
+	{{ render(tag.link,None,lang,attr=attr,title=tag.name) }}
 {%- endmacro -%}
 
-
-
-{#- returns: (url, anchor, reflang, is_external, reftype, refid, refpage, tagcattitle, tagcatcolor)-#}
-{%- macro parse_url(rawurl, lang) -%}
-	{%- set urlsplit = rawurl.split('#') -%}
-	{%- set ns = namespace(anchor=urlsplit[1]|d(None), url=None, page=None, tagcattitle=None, tagcatcolor=None, tagpage=None, reftype=None, refid=None, reflang=None, is_external = False) -%}
-	{%- set urlwoa = urlsplit[0] -%} {#- url without ns.anchor -#}
-	{%- if not urlwoa|length -%}
-		{{- caller("", ns.anchor, lang, False, None, None, None, None, None) -}}
-	{%- else -%}
-
-		{%- set components = urlwoa.split(':') -%}
-
-		{%- if components|length == 1 -%}
-			{%- set ns.url = ['https://', urlwoa]|join('') -%}
-			{{- caller(ns.url, ns.anchor, lang, True, None, None, None, None, None) -}}
-		{%- else -%}
-			{%- set ns.reftype = components[0] -%}
-			{%- set ns.refid = components[1] -%}
-			{%- set ns.reflang = components[2]|d(lang) -%}
-			{%- if ns.reftype == "slug" -%}
-				{%- call(page) get.page_by_slug(ns.refid, ns.reflang) -%}
-					{%- set ns.url = [siteurl, page.url]|join('/') -%}
-					{%- set ns.page = page -%}
-					{%- call(tagcattitle, tagcatcolor, caturl, catpage, catpages) get.tag_by_name(page.category.name, ns.reflang) -%}
-						{%- set ns.tagcattitle = tagcattitle -%}
-						{%- set ns.tagcatcolor = tagcatcolor -%}
-					{%- endcall -%}
-				{%- endcall -%}
-				{%- if 'link' in ns.page.metadata -%}
-					{%- call(parsedurl, anchor, reflang, is_external, reftype, refid, refpage, tagcattitle, tagcatcolor) parse_url(ns.page.metadata['link'], ns.reflang) -%}
-						{%- set ns.url = parsedurl -%}
-						{%- set ns.anchor = anchor -%}
-						{%- set ns.reflang = reflang -%}
-						{%- set ns.is_external = is_external -%}
-						{%- if not is_external -%}
-							{%- set ns.reftype = reftype -%}
-							{%- set ns.refid = refid -%}
-							{%- set ns.page = refpage -%}
-							{%- set ns.tagcattitle = tagcattitle -%}
-							{%- set ns.tagcatcolor = tagcatcolor -%}
-						{%- endif -%}
-					{%- endcall -%}
-				{%- endif -%}
-				{{- caller(ns.url, ns.anchor, ns.reflang, ns.is_external, ns.reftype, ns.refid, ns.page, ns.tagcattitle, ns.tagcatcolor) -}}
-			{%- elif ns.reftype == "tag" -%}
-				{%- call(tagcattitle, tagcatcolor, tagurl, tagpage, tagpages) get.tag_by_name(ns.refid, ns.reflang) -%}
-					{%- set ns.url = [siteurl, tagurl]|join('/') -%}
-					{%- set ns.tagcattitle = tagcattitle -%}
-					{%- set ns.tagcatcolor = tagcatcolor -%}
-					{%- set ns.tagpage = tagpage -%}
-				{%- endcall -%}
-				{{- caller(ns.url, ns.anchor, ns.reflang, False, ns.reftype, ns.refid, ns.tagpage, ns.tagcattitle, ns.tagcatcolor) -}}
-			{%- else -%}
-				{{- caller(urlwoa, ns.anchor, ns.reflang, True, None, None, None, None, None) -}}
-			{%- endif -%}
-		{%- endif -%}
-	{%- endif -%}
-{%- endmacro -%}
diff --git a/theme/templates/macros/nav.html b/theme/templates/macros/nav.html
index 2717c03..f4dd2c0 100644
--- a/theme/templates/macros/nav.html
+++ b/theme/templates/macros/nav.html
@@ -1,38 +1,31 @@
-{%- import 'macros/link.html' as link with context -%}
+{%- import 'macros/link.html' as linkr with context -%}
 
 {%- macro render_menu(items, lang) -%}
 	<nav>
 		<ul>
 		{% for item in items -%}
-			{{ render_menu_item(item, lang) }}
+			{{ render_menu_item(item.link, lang) }}
 		{%- endfor %}
 		</ul>
 	</nav>
 {%- endmacro -%}
 
-{%- macro render_menu_item(item, lang) -%}
-	{%- if item is string -%}
-		{%- set url = item -%}
-		{%- call(parsedurl, anchor, reflang, is_external, reftype, refid, refpage, tagcattitle, tagcatcolor) link.parse_url(url, lang) -%}
-			<li {% if tagcatcolor -%}style="--category-color: {{ tagcatcolor }}"{%- endif -%}>
-				{{ link.render(url, None, lang) }}
-			</li>
-		{%- endcall -%}
-	{%- else -%}
-		TODO: handle custom menu items
-	{%- endif -%}
+{%- macro render_menu_item(link, lang) -%}
+	<li {% if link.category.color -%}style="--category-color: {{ link.category.color }}"{%- endif -%}>
+		{{ linkr.render(link, None, lang) }}
+	</li>
 {%- endmacro -%}
 
-{%- macro render_breadcrumb_menu(cat, lang, slug = None) -%}
-	{%- call(cattitle, catcolor, caturl, catpage, catpages) get.tag_by_name(cat.name, lang) -%}
-	<nav class="breadcrumb" {% if catcolor -%}style="--category-color: {{ catcolor }}"{%- endif -%}>
+{%- macro render_breadcrumb_menu(cat, lang, page = None) -%}
+	<nav class="breadcrumb" {% if cat.color -%}style="--category-color: {{ cat.color }}"{%- endif -%}>
 		<ul>
-			<li>{{ link.render(['slug',config.theme.homepage_slug]|join(':'), None, lang) }}</li>
-			<li>{{ link.render(['tag',cat.name]|join(':'), None, lang) }}</li>
-			{%- if slug -%}
-			<li>{{ link.render(['slug',slug]|join(':'), None, lang) }}</li>
+			{%- call(homepage) get.page_by_slug(config.homepage_slug, lang) -%}
+			<li>{{ linkr.render(homepage.link, None, lang) }}</li>
+			{%- endcall -%}
+			<li>{{ linkr.render(cat.link, None, lang) }}</li>
+			{%- if page -%}
+			<li>{{ linkr.render(page.link, None, lang) }}</li>
 			{%- endif -%}
 		</ul>
 	</nav>
-	{%- endcall -%}
 {%- endmacro -%}
diff --git a/theme/templates/macros/renderers.html b/theme/templates/macros/renderers.html
index 3292fd3..81a1925 100644
--- a/theme/templates/macros/renderers.html
+++ b/theme/templates/macros/renderers.html
@@ -23,39 +23,21 @@
 	{{ cards.cards_from_pages(relevant_pages[lang], max=s.num|d(None)) }}
 {%- endmacro -%}
 
-{%- macro section_custom(s, lang) -%}
-	{{ cards.open() }}
-	{%- for c in s.content -%}
-		{%- if c is string -%}
-			{{ cards.card_from_link(url=c, lang=lang) }}
-		{%- else -%}
-			{{ cards.card(title=c[lang], url=c.url, catcolor=c.color, lang=lang) }}
-		{%- endif -%}
-	{%- endfor -%}
-	{{ cards.close() }}
-{%- endmacro -%}
-
 {%- macro section_iframe(s, lang) -%}
 	<iframe src="{{ s.url }}"></iframe>
 {%- endmacro -%}
 
 {%- macro section_tag(s, lang) -%}
-	{%- set ns = namespace(tagname=s.tag) -%}
-	{%- if ns.tagname is not string -%}
-		{%- set ns.tagname = ns.tagname.name -%}
+	{%- if s.title is not defined -%}
+		<header>
+			<h2 {% if s.id is defined -%}id="{{ s.id }}"{%- endif %}>{{ softbreak_span(s.tag.title) }}</h2>
+		</header>
+	{%- endif -%}
+	{{ cards.cards_from_pages(s.tag.pages, lang, max=s.num|d(None)) }}
+	
+	{%- if s.tag.page -%}
+		{{ content_renderer.render_content(s.tag.page.content, lang) }}
 	{%- endif -%}
-	{%- call(tagtitle, tagcolor, tagurl, tagpage, tagpages) get.tag_by_name(ns.tagname, lang) -%}
-		{%- if s.title is not defined -%}
-			<header>
-				<h2 {% if s.id is defined -%}id="{{ s.id }}"{%- endif %}>{{ softbreak_span(tagtitle) }}</h2>
-			</header>
-		{%- endif -%}
-		{{ cards.cards_from_pages(tagpages, lang, max=s.num|d(None)) }}
-		
-		{%- if tagpage -%}
-			{{ content_renderer.render_content(tagpage.content, lang) }}
-		{%- endif -%}
-	{%- endcall -%}
 {%- endmacro -%}
 
 {%- macro section(s, lang) -%}
@@ -68,8 +50,6 @@
 		{{ section_news(s, lang) }}
 	{%- elif s.type == "iframe" -%}
 		{{ section_iframe(s, lang) }}
-	{%- elif s.type == "custom" -%}
-		{{ section_custom(s, lang) }}
 	{%- elif s.type == "tag" -%}
 		{{ section_tag(s, lang) }}
 	{%- elif s.type == "relevant" -%}
diff --git a/theme/templates/page.html b/theme/templates/page.html
index 6e85fcb..eee3692 100644
--- a/theme/templates/page.html
+++ b/theme/templates/page.html
@@ -6,7 +6,7 @@
 {% endblock %}
 
 {% block breadcrumb %}
-	{{ nav.render_breadcrumb_menu(page.category, l, page.slug) }}
+	{{ nav.render_breadcrumb_menu(page.category, l, page) }}
 {% endblock breadcrumb %}
 
 {% block main %}
@@ -17,13 +17,13 @@
 		</header>
 		{% endblock %}
 		{% block page_content %}
-		{%- call(s) get.metadata_entry(page.slug, l, 'before') -%}
-			{{ render.sections(s, l) }}
-		{%- endcall -%}
-		{{ content_renderer.render_content(page.content, l) }}
-		{%- call(s) get.metadata_entry(page.slug, l, 'after') -%}
-			{{ render.sections(s, l) }}
-		{%- endcall -%}
+			{%- if 'before' in page.metadata -%}
+				{{ render.sections(page.metadata.before, l) }}
+			{%- endif -%}
+			{{ content_renderer.render_content(page.content, l) }}
+			{%- if 'after' in page.metadata -%}
+				{{ render.sections(page.metadata.after, l) }}
+			{%- endif -%}
 		{% endblock %}
 	</article>
 {% endblock %}
@@ -34,7 +34,11 @@
 			{{ t[l].page.authors_prefix }}
 			<ul>
 			{% for author in page.authors %}
-				<li> {{ author.name|e }} </li>
+				<li><ul>
+				{% for name in author.names %}
+					<li> {{ name|e }} </li>
+				{% endfor %}
+				</ul></li>
 			{% endfor %}
 			</ul>
 			{{ t[l].page.authors_suffix }}
@@ -57,7 +61,7 @@
 
 		<div>
 			{{ t[l].page.category_prefix }}
-			{{ link.render_tag(page.category,l) }}
+			{{ linkr.render_tag(page.category,l) }}
 			{{ t[l].page.category_suffix }}
 		</div>
 
@@ -66,7 +70,7 @@
 			<ul>
 			{% for tag in page.tags %}
 				<li>
-					{{ link.render_tag(tag,l) }}
+					{{ linkr.render_tag(tag,l) }}
 				</li>
 			{% endfor %}
 			</ul>
@@ -78,7 +82,9 @@
 			<ul class="languages">
 			{%- for tlang in t['supported'] -%}
 				<li>
-					{{ link.render(['slug',page.slug,tlang]|join(':'),t[tlang]['langname'],l) }}
+					{%- call(p) get.page_by_slug(page.slug, tlang) -%}
+						{{ linkr.render(p.link,t[tlang]['langname'],l) }}
+					{%- endcall -%}
 				</li>
 			{%- endfor -%}
 			</ul>
diff --git a/theme/templates/tag.html b/theme/templates/tag.html
index 6e80854..c2e9259 100644
--- a/theme/templates/tag.html
+++ b/theme/templates/tag.html
@@ -1,8 +1,6 @@
 {% extends "base.html" %}
 {% block title -%}
-	{%- call(tagtitle, tagcolor, tagurl, tagpage, tagpages) get.tag_by_name(tag.name, l) -%}
-		{{ t[l].title_prefix }}{{ t[l].sitename }}{{ t[l].title_suffix }} - {{ render.softbreak_span(tagtitle)|striptags }}
-	{%- endcall -%}
+	{{ t[l].title_prefix }}{{ t[l].sitename }}{{ t[l].title_suffix }} - {{ render.softbreak_span(tag.title)|striptags }}
 {%- endblock -%}
 
 {% block breadcrumb %}
-- 
GitLab