From 9c2b363404a47088d85fce118dfd50b61ead33b3 Mon Sep 17 00:00:00 2001
From: David Luevano Alvarado <david@luevano.xyz>
Date: Tue, 18 May 2021 22:56:18 -0600
Subject: add more configuration options and refactor its parsing

---
 ChangeLog                  |  5 +++
 README.md                  | 33 ++++++++++++-----
 src/pyssg/builder.py       | 45 +++++++++--------------
 src/pyssg/configuration.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++
 src/pyssg/pyssg.py         | 57 +++++++++++++++++++++++------
 5 files changed, 182 insertions(+), 48 deletions(-)
 create mode 100644 src/pyssg/configuration.py

diff --git a/ChangeLog b/ChangeLog
index 4c27f95..3104adb 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,11 @@
 CHANGES
 =======
 
+v0.2.2
+------
+
+* fix sorting of pages and update default templates
+
 v0.2.1
 ------
 
diff --git a/README.md b/README.md
index 79d6707..67649b8 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,28 @@
-# pyssg
+# pyssg - Static Site Generator written in Python
 
-Static Site Generator inspired by Roman Zolotarev's [`ssg5`](https://rgz.ee/bin/ssg5) and [`rssg`](https://rgz.ee/bin/rssg), Luke Smith's [`lb` and `sup`](https://github.com/LukeSmithxyz/lb) and, pedantic.software's [`blogit`](https://pedantic.software/git/blogit/).
+Inspired (initially) by Roman Zolotarev's [`ssg5`](https://rgz.ee/bin/ssg5) and [`rssg`](https://rgz.ee/bin/rssg), Luke Smith's [`lb` and `sup`](https://github.com/LukeSmithxyz/lb) and, pedantic.software's great (but *"mamador"*, as I would say in spanish) [`blogit`](https://pedantic.software/git/blogit/).
 
-The reason of making this in python is because I was tired of wanting (some) features from all of these minimal programs, but being a pain in the ass to maintain or add features on my own, specially to `blogit`...; making minimalist software it's great and all, but there is a limit on how pretentious and elitist a software can be for me to accept it.
+I'm writing this in *pYtHoN* (thought about doing it in Go, but I'm most comfortable with Python at the moment) because I want features from all of these minimal programs (and more), but I don't really want to be stitching each one of the features on any of these programs, because they're written in a way to only work as how they were first imagined to work like; I already tried adding features to `ssg` and ended up rewriting it in POSIX shell, but it was a pain in the ass when I tried to add even more, and don't get me started on trying to extend `blogit`... And also because I want to.
 
 ## Current features
 
-This is still a WIP. Still doesn't build `sitemap.xml` or `rss.xml` files.
+**This is still a WIP. Still doesn't build `sitemap.xml` or `rss.xml` files.**
 
 - [x] Build static site parsing `markdown` files ( `*.md` -> `*.html`)
+	- [x] Using plain `*.html` files for templates.
+		- [ ] Would like to change to something more flexible and easier to manage ([`jinja`](), for example).
 	- [x] Preserves hand-made `*.html` files.
 	- [x] Tag functionality.
 	- [ ] Open Graph (and similar) support.
 - [ ] Build `sitemap.xml` file.
 - [ ] Build `rss.xml` file.
+- [x] Only build page if `*.md` is new or updated.
+	- [ ] Extend this to tag pages and index (right now all tags and index is built no matter if no new/updated file is present).
+- [x] Configuration file as an alternative to using command line flags (configuration file options are prioritized).
 
-## Markdown features
+### Markdown features
 
-This program uses [`python-markdown`](https://python-markdown.github.io/) package with the following [extensions](https://python-markdown.github.io/extensions/):
+This program uses the base [`markdown` syntax]() plus additional syntax, all thanks to [`python-markdown`](https://python-markdown.github.io/) that provides [extensions](https://python-markdown.github.io/extensions/). The following extensions are used:
 
 - Extra (collection of QoL extensions).
 - Meta-Data.
@@ -26,8 +31,20 @@ This program uses [`python-markdown`](https://python-markdown.github.io/) packag
 - Table of Contents.
 - WikiLinks.
 
+## Installation
+
+Just install it with `pip`:
+
+```sh
+pip install pyssg
+```
+
+*EW!*, I know..., I will try to make a PKBUILD and release it in AUR or something; hit me up if you do it to add it here.
+
 ## Usage
 
+It is intended to be used as a standalone terminal program running on the "root" directory where you have the `src` and `dst` directories in (defaults for both flags).
+
 First initialize the directories you're going to use for the source files and destination files:
 
 ```sh
@@ -39,7 +56,7 @@ That creates the desired directories with the basic templates that can be edited
 Build the site with:
 
 ```sh
-pyssg -s src_dir -d dst_dir -b
+pyssg -s src_dir -d dst_dir -u https://base.url -b
 ```
 
-That creates all `*.html` for the site and can be easily moved to the server. Where an optional `-u` flag can be provided for the base URL (don't include the trailing slash `/`)
+That creates all `*.html` for the site and can be easily moved to the server. Here, the `-u` flag is technically optional in the sense that you'll not receive a warning/error, but it's used to prepend links with this URL (not strictly required everywhere), so don't ignore it; also don't include the trailing `/`.
diff --git a/src/pyssg/builder.py b/src/pyssg/builder.py
index b8acf08..8472b20 100644
--- a/src/pyssg/builder.py
+++ b/src/pyssg/builder.py
@@ -2,6 +2,7 @@ import os
 import shutil
 from copy import deepcopy
 
+from .configuration import Configuration
 from .template import Template
 from .database import Database
 from .parser import MDParser
@@ -9,37 +10,19 @@ from .page import Page
 from .discovery import get_file_list, get_dir_structure
 
 class HTMLBuilder:
-    def __init__(self, src: str,
-                 dst: str,
-                 base_url: str,
+    def __init__(self, config: Configuration,
                  template: Template,
-                 db: Database,
-                 dformat: str=None,
-                 l_dformat: str=None,
-                 lsep_dformat: str=None):
-        self.src: str = src
-        self.dst: str = dst
-        self.base_url: str = base_url
+                 db: Database):
+        self.src: str = config.src
+        self.dst: str = config.dst
+        self.base_url: str = config.base_url
+        self.dformat: str = config.dformat
+        self.l_dformat: str = config.l_dformat
+        self.lsep_dformat: str = config.lsep_dformat
+        self.force: bool = config.force
+
         self.template: Template = template
         self.db: Database = db
-        self.dformat: str = None
-        self.l_dformat: str = None
-        self.lsep_dformat: str = None
-
-        if dformat is not None:
-            self.dformat = dformat
-        else:
-            self.dformat = "%a, %d %b, %Y @ %H:%M %Z"
-
-        if l_dformat is not None:
-            self.l_dformat = l_dformat
-        else:
-            self.l_dformat = "%b %d"
-
-        if lsep_dformat is not None:
-            self.lsep_dformat = lsep_dformat
-        else:
-            self.lsep_dformat = "%B %Y"
 
         self.dirs: list[str] = None
         self.md_files: list[str] = None
@@ -61,7 +44,11 @@ class HTMLBuilder:
         self.__create_article_index(parser.all_tags, parser.all_pages)
 
         # create each category of html pages
-        self.__create_articles(parser.updated_pages)
+        # check if all pages should be created
+        if self.force:
+            self.__create_articles(parser.all_pages)
+        else:
+            self.__create_articles(parser.updated_pages)
         self.__create_tags(parser.all_tags, parser.all_pages)
 
 
diff --git a/src/pyssg/configuration.py b/src/pyssg/configuration.py
new file mode 100644
index 0000000..3f7acfe
--- /dev/null
+++ b/src/pyssg/configuration.py
@@ -0,0 +1,90 @@
+import os
+
+
+class Configuration:
+    def __init__(self, path: str):
+        self.path: str = path
+        self.src: str = None
+        self.dst: str = None
+        self.base_url: str = None
+        self.dformat: str = None
+        self.l_dformat: str = None
+        self.lsep_dformat: str = None
+        self.force: bool = None
+
+
+    def read(self):
+        try:
+            lines: list[str] = None
+            with open(self.path, 'r') as f:
+                lines = f.readlines()
+
+            opts: dict[str, Union[str, bool]] = dict()
+            for l in lines:
+                kv: list[str] = l.split('=', 1)
+                if len(kv) != 2:
+                    raise Exception('wrong config syntax')
+
+                k: str = kv[0].strip()
+                k_temp: str = kv[0].strip()
+                # check if value should be a boolean true
+                v: Union[str, bool] = k_temp\
+                    if k_temp.lower() not in ['true', '1', 'yes']\
+                    else True
+
+                opts[k] = v
+
+            try:
+                self.src = opts['SRC_PATH']
+            except KeyError: pass
+
+            try:
+                self.dst = opts['SRC_PATH']
+            except KeyError: pass
+
+            try:
+                self.base_url = opts['BASE_URL']
+            except KeyError: pass
+
+            try:
+                self.dformat = opts['DATE_FORMAT']
+            except KeyError: pass
+
+            try:
+                self.l_dformat = opts['LIST_DATE_FORMAT']
+            except KeyError: pass
+
+            try:
+                self.lsep_dformat = opts['LIST_SEP_DATE_FORMAT']
+            except KeyError: pass
+
+            try:
+                # if the parser above didn't read a boolean true, then take it
+                # as a false anyways
+                self.force = opts['FORCE'] if opts['FORCE'] is True else False
+            except KeyError: pass
+
+        except OSError: pass
+
+
+    def fill_missing(self, opts: dict[str, Union[str, bool]]) -> None:
+        if self.src is None:
+            self.src = opts['src']
+
+        if self.dst is None:
+            self.dst = opts['dst']
+
+        if self.base_url is None:
+            self.base_url = opts['url']
+
+        if self.dformat is None:
+            self.dformat = opts['date-format']
+
+        if self.l_dformat is None:
+            self.l_dformat = opts['list-date-format']
+
+        if self.lsep_dformat is None:
+            self.lsep_dformat = opts['list-sep-date-format']
+
+        if self.force is None:
+            self.force = opts['force']
diff --git a/src/pyssg/pyssg.py b/src/pyssg/pyssg.py
index 3cda0bc..2f46c18 100644
--- a/src/pyssg/pyssg.py
+++ b/src/pyssg/pyssg.py
@@ -1,6 +1,7 @@
 import os
 from argparse import ArgumentParser, Namespace
 
+from .configuration import Configuration
 from .database import Database
 from .template import Template
 from .builder import HTMLBuilder
@@ -9,7 +10,16 @@ from .builder import HTMLBuilder
 def get_options() -> Namespace:
     parser = ArgumentParser(prog='pyssg',
                             description='''Static Site Generator that reads
-                            Markdown files and creates HTML files.''')
+                            Markdown files and creates HTML files.\nIf
+                            [-c]onfig file is provided (or exists in default
+                            location) all other options are ignored.\nFor
+                            datetime formats see:
+                            https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes''')
+    parser.add_argument('-c', '--config',
+                        default='$XDG_CONFIG_HOME/pyssg/pyssgrc',
+                        type=str,
+                        help='''config file (path) to read from; defaults to
+                        '$XDG_CONFIG_HOME/pyssg/pyssgrc' ''')
     parser.add_argument('-s', '--src',
                         default='src',
                         type=str,
@@ -24,6 +34,24 @@ def get_options() -> Namespace:
                         default='',
                         type=str,
                         help='''base url without trailing slash''')
+    parser.add_argument('--date-format',
+                        default='%a, %b %d, %Y @ %H:%M %Z',
+                        type=str,
+                        help='''date format used inside pages (for creation and
+                        modification times, for example); defaults to '%a, %b
+                        %d, %Y @ %H:%M %Z' ('Tue, Mar 16, 2021 @ 02:46 UTC',
+                        for example)''')
+    parser.add_argument('--list-date-format',
+                        default='%b %d',
+                        type=str,
+                        help='''date format used for page entries in a list;
+                        defaults to '%b %d' ('Mar 16', for example)''')
+    parser.add_argument('--list-sep-date-format',
+                        default='%B %Y',
+                        type=str,
+                        help='''date format used for the separator between page
+                        entries in a list; defaults to '%B %Y' ('March 2021',
+                        for example)''')
     parser.add_argument('-i', '--init',
                         action='store_true',
                         help='''initializes the dir structure, templates,
@@ -32,38 +60,45 @@ def get_options() -> Namespace:
                         action='store_true',
                         help='''generates all html files and passes over
                         existing (handmade) ones''')
+    parser.add_argument('-f', '--force',
+                        action='store_true',
+                        help='''force building all pages and not only the
+                        updated ones''')
 
     return parser.parse_args()
 
 
 def main() -> None:
-    opts: dict[str] = vars(get_options())
-    src: str = opts['src']
-    dst: str = opts['dst']
-    base_url: str = opts['url']
+    opts: dict[str, Union[str, bool]] = vars(get_options())
+    conf_path: str = opts['config']
+    conf_path = os.path.expandvars(conf_path)
+
+    config: Configuration = Configuration(conf_path)
+    config.read()
+    config.fill_missing(opts)
 
     if opts['init']:
         try:
-            os.mkdir(src)
-            os.makedirs(os.path.join(dst, 'tag'))
+            os.mkdir(config.src)
+            os.makedirs(os.path.join(config.dst, 'tag'))
         except FileExistsError:
             pass
 
         # write default templates
-        template: Template = Template(src)
+        template: Template = Template(config.src)
         template.write()
         return
 
     if opts['build']:
         # start the db
-        db: Database = Database(os.path.join(src, '.files'))
+        db: Database = Database(os.path.join(config.src, '.files'))
         db.read()
 
         # read templates
-        template: Template = Template(src)
+        template: Template = Template(config.src)
         template.read()
 
-        builder: HTMLBuilder = HTMLBuilder(src, dst, base_url, template, db)
+        builder: HTMLBuilder = HTMLBuilder(config, template, db)
         builder.build()
 
         db.write()
-- 
cgit v1.2.3-70-g09d2