about summary refs log tree commit diff
path: root/src/article.py
diff options
context:
space:
mode:
authoruser <user@node5.net>2024-03-11 15:19:35 +0100
committeruser <user@node5.net>2024-03-11 15:19:35 +0100
commitb692ac3bafb0559a3d2c132187bafda1ce008ee7 (patch)
treed18510b28226a0c0f3466f80060f3a727a7310cb /src/article.py
parent8c956c553f36d9de7061ab27efb44d7e69ba52b7 (diff)
rename article_generator.py module -> article, make functions into article generator class
Diffstat (limited to 'src/article.py')
-rw-r--r--src/article.py177
1 files changed, 177 insertions, 0 deletions
diff --git a/src/article.py b/src/article.py
new file mode 100644
index 0000000..afca0df
--- /dev/null
+++ b/src/article.py
@@ -0,0 +1,177 @@
+import dataclasses
+import datetime
+import os
+import logging
+import pathlib
+import glob
+import typing
+import markdown
+import yaml
+
+
+# Known exceptions, these are raised when generating on program exiting error, and are displayed to the user nicely
+class ArticleHandlerException(Exception):
+    pass
+
+
+class ArticleMetaDataMalformed(ArticleHandlerException):
+    pass
+
+
+class ArticleNoMetaData(ArticleHandlerException):
+    pass
+
+
+@dataclasses.dataclass
+class WebPage:
+    url: str
+    name: str
+
+
+@dataclasses.dataclass
+class MetaData():
+    description: str
+    created: datetime.date
+
+    @property
+    def pretty_print(self) -> str:
+        return f'''
+    Description: {self.description}
+    Created: {self.created}'''
+
+
+def truncate(text: str, max_length: int = 50):
+    if len(text) < max_length:
+        return text
+    else:
+        return text[0:max_length - 1]
+
+
+@dataclasses.dataclass
+class Article(WebPage):
+    metadata: MetaData
+    web_dir: tuple
+    source_path: str
+    source: str
+    html: str
+    modified: datetime.datetime
+    folder_path: typing.Union[None, str] = None
+
+    @property
+    def pretty_print(self) -> str:
+        return f'''
+Name:        {self.name}
+Metadata:    {self.metadata.pretty_print}
+Web dir:     {self.web_dir}
+URL:         {self.url}
+Source path: {self.source_path}
+Folder path: {self.folder_path}
+Modified:    {self.modified}
+HTML:        {truncate(self.html)}
+Source:      {truncate(self.source)}'''
+
+
+@dataclasses.dataclass
+class Folder(WebPage):
+    articles: typing.List[Article] = dataclasses.field(default_factory=list)
+    sub_folders: typing.Dict[str, object] = dataclasses.field(default_factory=dict)
+
+
+class ArticleGenerator:
+    def __init__(self, articles_path: str):
+        self.articles_path = articles_path
+
+    def get_web_dir(self, path, name) -> tuple[str, ...]:
+        dir_structure = path.split(name)[0]
+        # Split into tuple, remove first part, assemble to path again, all to remove the first source dir agnostically
+        dir_structure_prefix_striped = pathlib.Path(dir_structure).parts[2:]
+        return dir_structure_prefix_striped
+
+    def parse_article_meta_data(self, source: str) -> typing.Tuple[str, MetaData]:
+        if source.startswith('---'):
+            meta_data_yml_end_char_index = source.find('---', 3)
+            meta_data_yml = source[3:meta_data_yml_end_char_index]
+            # Strip metadata text from source, before feeding it to the markdown reader
+            source = source[meta_data_yml_end_char_index + 3:]
+            meta_data = yaml.safe_load(meta_data_yml)
+
+            try:
+                meta_data = MetaData(**meta_data)
+            except TypeError as type_error_exception:
+                raise ArticleMetaDataMalformed(str(type_error_exception))
+            return source, meta_data
+        else:
+            raise ArticleNoMetaData(f'No metadata found')
+
+    def get_article(self, path: str) -> Article:
+        logging.debug(path)
+        article_args = {}
+        filename, file_extension = os.path.splitext(path)
+
+        basename = os.path.basename(filename)
+        if basename == 'index':
+            # Article is the folder
+            article_folder_name = os.path.dirname(path)
+            article_args['folder_path'] = article_folder_name
+            dir_basename = os.path.basename(article_folder_name)
+            article_args['name'] = dir_basename
+        else:
+            # Article one file
+            article_args['name'] = os.path.basename(filename)
+        article_args['web_dir'] = self.get_web_dir(path, article_args['name'])
+        article_args['source_path'] = path
+
+        article_args[
+            'url'] = f'{"/" if article_args["web_dir"] else ""}{"/".join(article_args["web_dir"])}/{article_args["name"]}'
+
+        article_args['modified'] = datetime.datetime.utcfromtimestamp(os.path.getmtime(path)).replace(
+            tzinfo=datetime.datetime.now().astimezone().tzinfo)
+
+        with open(path, 'r') as file:
+            source = file.read()
+        article_args['source'], article_args['metadata'] = self.parse_article_meta_data(source)
+        article_args['html'] = markdown.markdown(article_args['source'], extensions=
+        [
+            'fenced_code',
+            'codehilite',
+            'tables',
+            'toc'  # Automatically generates unique IDs for headers allowing for ID URL referral (Anchor)
+        ])
+
+        article = Article(**article_args)
+        return article
+
+    def discover_folder_structure(self, article: Article, articles: Folder):
+        previous_folder = articles
+        for folder_name in article.web_dir:
+            logging.debug(folder_name)
+
+            if folder_name not in previous_folder.sub_folders:
+                logging.debug('new')
+                current_folder = Folder(url=f'{previous_folder.url}{folder_name}/', name=folder_name)
+                previous_folder.sub_folders[folder_name] = current_folder
+            else:
+                logging.debug('reuse')
+                current_folder = previous_folder.sub_folders[folder_name]
+            previous_folder = current_folder
+            logging.debug('')
+
+        previous_folder.articles.append(article)
+
+    def sort_articles(self, folder: Folder):
+        folder.articles = sorted(folder.articles,
+                                 key=lambda
+                                     x: x.metadata.created if x.metadata.created is not None else datetime.date.min,
+                                 reverse=True)
+        for folder in folder.sub_folders.values():
+            self.sort_articles(folder)
+
+    def discover_articles(self):
+        articles_paths = glob.glob(f'{self.articles_path}/**/*.md', recursive=True)  # Equivalent to ls ./**.md
+        articles = Folder(url='/', name='index')
+        for article_path in articles_paths:
+            article = self.get_article(article_path)
+            logging.debug(article.pretty_print)
+            self.discover_folder_structure(articles=articles, article=article)
+        self.sort_articles(articles)
+        return articles