diff options
-rw-r--r-- | README.md | 169 | ||||
-rw-r--r-- | src/pyssg/arg_parser.py | 50 | ||||
-rw-r--r-- | src/pyssg/builder.py | 32 | ||||
-rw-r--r-- | src/pyssg/configuration.py | 151 | ||||
-rw-r--r-- | src/pyssg/page.py | 34 | ||||
-rw-r--r-- | src/pyssg/parser.py | 18 | ||||
-rw-r--r-- | src/pyssg/plt/default.ini | 22 | ||||
-rw-r--r-- | src/pyssg/plt/index.html | 6 | ||||
-rw-r--r-- | src/pyssg/plt/page.html | 4 | ||||
-rw-r--r-- | src/pyssg/plt/rss.xml | 18 | ||||
-rw-r--r-- | src/pyssg/plt/sitemap.xml | 2 | ||||
-rw-r--r-- | src/pyssg/plt/tag.html | 4 | ||||
-rw-r--r-- | src/pyssg/pyssg.py | 85 | ||||
-rw-r--r-- | src/pyssg/utils.py | 28 |
14 files changed, 311 insertions, 312 deletions
@@ -2,12 +2,8 @@ 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/). -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.** - - [x] Build static site parsing `markdown` files ( `*.md` -> `*.html`) - [x] ~~Using plain `*.html` files for templates.~~ Changed to Jinja templates. - [x] Would like to change to something more flexible and easier to manage ([`jinja`](https://jinja.palletsprojects.com/en/3.0.x/), for example). @@ -15,15 +11,20 @@ I'm writing this in *pYtHoN* (thought about doing it in Go, but I'm most comfort - [x] Tag functionality. - [ ] Open Graph (and similar) support. (Technically, this works if you add the correct metadata to the `*.md` files and use the variables available for Jinja) - [x] Build `sitemap.xml` file. + - [ ] Include manually added `*.html` files. - [x] Build `rss.xml` file. - [ ] Join the `static_url` to all relative URLs found to comply with the [RSS 2.0 spec](https://validator.w3.org/feed/docs/rss2.html) (this would be added to the parsed HTML text extracted from the MD files, so it would be available to the created `*.html` and `*.xml` files). Note that depending on the reader, it will append the URL specified in the RSS file or use the [`xml:base`](https://www.rssboard.org/news/151/relative-links) specified ([newsboat](https://newsboat.org/) parses `xml:base`). + - [ ] Include manually added `*.html` files. - [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). +- [x] Configuration file. ~~as an alternative to using command line flags (configuration file options are prioritized).~~ + - [x] Use [`configparser`](https://docs.python.org/3/library/configparser.html) instead of custom config handler. + +**Please note that I've removed the use of command line flags for now as it was too much bloat and unnecessary.** ### To be added/fixed -- [ ] Avoid the program to freak out when there are directories created in advance. +- [x] Avoid the program to freak out when there are directories created in advance. - [ ] Provide more meaningful error messages when you are missing mandatory tags in your `.md` files. ### Markdown features @@ -48,31 +49,35 @@ Just install it with `pip`: 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. +Will add a PKBUILD (and possibly submit it to the AUR) sometime later. ## 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: +1. Get the default configuration file: ```sh -pyssg -s src_dir -d dst_dir -i +pyssg --copy-default-config -c <path/to/config> ``` -You do not have to create any directories, in advance, the command above will do it. -Actually for the moment I will encourage you to **not create** any directories in advance. -That creates the desired directories with the basic templates that can be edited as desired (see variables available for Jinja below). Place your `*.md` files somewhere inside the source directory (`src_dir` in the command above), but outside of the `templates` directory. It accepts sub-directories. +Where `-c` is optional as by default `$XDG_CONFIG_HOME/pyssg/config.ini` is used. + +2. Edit the config file created as needed. -Strongly recommended to edit the `rss.xml` template. +- `config.ini` is parsed using Python's [`configparser`](https://docs.python.org/3/library/configparser.html), [more about the config file](#config-file). -Build the site with: +3. Initialize the directory structures (source, destination, template) and move template files: ```sh -pyssg -s src_dir -d dst_dir -t plt_dir -u https://base.url -b +pyssg -i ``` -Remember to add the mandatory meta-data keys to your `.md` files, these are: +- You can modify the basic templates as needed (see [variables available for Jinja](#available-jinja-variables)). + +- Strongly recommended to edit the `rss.xml` template. + +4. Place your `*.md` files somewhere inside the source directory. It accepts sub-directories. + +- Remember to add the mandatory meta-data keys to your `.md` files, these are: ``` title: the title of your blog entry or whatever @@ -81,54 +86,94 @@ lang: the language the entry is written on summary: a summary of the entry ``` -You can add more meta-data keys as long as it is [Python-Markdown compliant](https://python-markdown.github.io/extensions/meta_data/). +- You can add more meta-data keys as long as it is [Python-Markdown compliant](https://python-markdown.github.io/extensions/meta_data/), and these will ve [available as Jinja variables](#available-jinja-variables). + +- Also strongly recomended to add the `tags` metadata so that `pyssg` generates some nice filtering tags. -Also strongly recomended to add the `tags` test for `pyssg` to generate some nice filtering tags. +5. Build the `*.html` with: + +```sh +pyssg -b +``` + +- After this, you have ready to deploy `*.html` files. + +- For now, the building option also creates the `rss.xml` and `sitemap.xml` files based on templates, including only all converted `*.md` files (and processed tags in case of the sitemap), meaning that separate `*.html` files should be included manually in the template. + +## Config file + +All sections/options need to be compliant with the [`configparser`](https://docs.python.org/3/library/configparser.html). + +At least the sections and options given in the default config should be present: + +```ini +[path] +src=src # source +dst=dst # destination +plt=plt # template +[url] +main=https://example.com +static=https://static.example.com # used for static resources (images, js, css, etc) +default_image=/images/default.png # this will be appended to 'static' at the end +[fmt] # % needs to be escaped with another % +date=%%a, %%b %%d, %%Y @ %%H:%%M %%Z +list_date=%%b %%d +list_sep_date=%%B %%Y +[info] +title=Example site +[other] +force=False +``` + +Along with these, these extra ones will be added on runtime: + +```ini +[fmt] +rss_date=%%a, %%d %%b %%Y %%H:%%M:%%S GMT # fixed +sitemap_date=%%Y-%%m-%%d # fixed +[info] +version= # current 'pyssg' version (0.5.1.dev16, for example) +rss_run_date= # date the program was run, formatted with 'rss_date' +sitemap_run_date= # date the program was run, formatted with 'sitemap_date' +``` -So...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 `/`. +You can add any other option/section that you can later use in the Jinja templates via the exposed config object. -For now, the `-b`uild tag also creates the `rss.xml` and `sitemap.xml` files based on templates including only all converted `*.md` files (and processed tags in case of the sitemap), meaning that separate `*.html` files should be included manually in the template. +Other requisites are: -For more options/flags just checkout `pyssg -h`. +- Urls shouldn't have the trailing slash `/`. +- The only character that needs to be escaped is `%` with another `%`. ## Available Jinja variables -Here is the list of variables that you can use specific Jinja templates with a short description. Note that all urls are without the trailing slash `/`. - -- `config` (`Configuration`) (all): configuration object containing general/global attributes, the useful ones being: - - `title` (`str`): title of the website. - - `url` (`str`): base url of the website. - - `static_url` (`str`): base static url where all static files are located, mostly needed for correct rss feed generator when using a `base` tag and using relative links to files. For more, see [<base>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base). - - `default_image_url` (`str`): as defined in `DEFAULT_IMAGE_URL` configuration option. - - `version` (`str`): version in numeric form, i.e. `0.5.0`. - - `run_date` (`str`): date when the program was run, with format required for rss. -- Pages: - - `all_pages` (`list(Page)`) (all): list of all the pages, sorted by creation time, reversed. - - `page` (`Page`) (`page.html`): page object that contains the following attributes: - - `title` (`str`): title of the page. - - `author` (`str`): author of the page. - - `content` (`str`): actual content of the page. - - `cdatetime` (`str`): creation datetime object of the page. - - `cdate` (`str`): formatted `cdatetime` as the configuration option `DATE_FORMAT`. - - `cdate_list` (`str`): formatted `cdatetime` as the configuration option `LIST_DATE_FORMAT`. - - `cdate_list_sep` (`str`): formatted `cdatetime` as the configuration option `LIST_SEP_DATE_FORMAT`. - - `cdate_rss` (`str`): formatted `cdatetime` as required by rss. - - `cdate_sitemap` (`str`): formatted `cdatetime` as required by sitemap. - - `mdatetime` (`str`): modification datetime object of the page. Defaults to None. - - `mdate` (`str`): formatted `mdatetime` as the configuration option `DATE_FORMAT`. Defaults to None. - - `mdate_list` (`str`): formatted `mdatetime` as the configuration option `LIST_DATE_FORMAT`. - - `mdate_list_sep` (`str`): formatted `mdatetime` as the configuration option `LIST_SEP_DATE_FORMAT`. - - `mdate_rss` (`str`): formatted `mdatetime` as required by rss. - - `mdate_sitemap` (`str`): formatted `mdatetime` as required by sitemap. - - `summary` (`str`): summary of the page, as specified in the `*.md` file. - - `lang` (`str`): page language, used for the general `html` tag `lang` attribute. - - `tags` (`list(tuple(str))`): list of tuple of tags of the page, containing the name and the url of the tag, in that order. Defaults to empty list. - - `url` (`str`): url of the page, this already includes the `config.url`. - - `image_url` (`str`): image url of the page, this already includes the `config.static_url`. Defaults to the `DEFAULT_IMAGE_URL` configuration option. - - `next/previous` (`Page`): reference to the next or previous page object (containing all these attributes). Defaults to None - - `og` (`dict(str, str)`): dict for object graph metadata. - - `meta` (`dict(str, list(str))`): meta dict as obtained from python-markdown, in case you use a meta tag not yet supported, it will be available there. -- Tags: - - `tag` (`tuple(str)`) (`tag.html`): tuple of name and url of the current tag. - - `tag_pages` (`list(Page)`) (`tag.html`): similar to `all_pages` but contains all the pages for the current tag. - - `all_tags` (`list(tuple(str))`) (all): similar to `page.tags` but contains all the tags. +These variables are exposed to use within the templates. The below list is in the form of *variable (type) (available from): description*. `section/option` describe config file section and option and `object.attribute` corresponding object and it's attribute. + +- `config` (`ConfigParser`) (all): parsed config file plus the added options internally (as described in [config file](#config-file)). +- `all_pages` (`list(Page)`) (all): list of all the pages, sorted by creation time, reversed. +- `page` (`Page`) (`page.html`): contains the following attributes (genarally these are parsed from the metadata in the `*.md` files): + - `title` (`str`): title of the page. + - `author` (`str`): author of the page. + - `content` (`str`): actual content of the page, this is the `html`. + - `cdatetime` (`str`): creation datetime object of the page. + - `cdate` (`str`): formatted `cdatetime` as the config option `fmt/date`. + - `cdate_list` (`str`): formatted `cdatetime` as the config option `fmt/list_date`. + - `cdate_list_sep` (`str`): formatted `cdatetime` as the config option `fmt/list_sep_date`. + - `cdate_rss` (`str`): formatted `cdatetime` as required by rss. + - `cdate_sitemap` (`str`): formatted `cdatetime` as required by sitemap. + - `mdatetime` (`str`): modification datetime object of the page. Defaults to `None`. + - `mdate` (`str`): formatted `mdatetime` as the config option `fmt/date`. Defaults to `None`. + - `mdate_list` (`str`): formatted `mdatetime` as the config option `fmt/list_date`. + - `mdate_list_sep` (`str`): formatted `mdatetime` as the config option `fmt/list_sep_date`. + - `mdate_rss` (`str`): formatted `mdatetime` as required by rss. + - `mdate_sitemap` (`str`): formatted `mdatetime` as required by sitemap. + - `summary` (`str`): summary of the page, as specified in the `*.md` file. + - `lang` (`str`): page language, used for the general `html` tag `lang` attribute. + - `tags` (`list(tuple(str))`): list of tuple of tags of the page, containing the name and the url of the tag, in that order. Defaults to empty list. + - `url` (`str`): url of the page, this already includes the `url/main` from config file. + - `image_url` (`str`): image url of the page, this already includes the `url/static`. Defaults to the `url/default_image` config option. + - `next/previous` (`Page`): reference to the next or previous page object (containing all these attributes). Defaults to `None`. + - `og` (`dict(str, str)`): dict for object graph metadata. + - `meta` (`dict(str, list(str))`): meta dict as obtained from python-markdown, in case you use a meta tag not yet supported, it will be available there. +- `tag` (`tuple(str)`) (`tag.html`): tuple of name and url of the current tag. +- `tag_pages` (`list(Page)`) (`tag.html`): similar to `all_pages` but contains all the pages for the current tag. +- `all_tags` (`list(tuple(str))`) (all): similar to `page.tags` but contains all the tags. diff --git a/src/pyssg/arg_parser.py b/src/pyssg/arg_parser.py index 4ee7d57..90fb8c1 100644 --- a/src/pyssg/arg_parser.py +++ b/src/pyssg/arg_parser.py @@ -3,21 +3,40 @@ from argparse import ArgumentParser, Namespace def get_parsed_arguments() -> Namespace: parser = ArgumentParser(prog='pyssg', - description='''Static Site Generator that reads - 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: + description='''Static Site Generator that parses + Markdown files into HTML files. For datetime + formats see: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes''') parser.add_argument('-v', '--version', action='store_true', help='''print program version''') parser.add_argument('-c', '--config', - default='$XDG_CONFIG_HOME/pyssg/pyssgrc', + # don't give a default here, as it would seem like + # --config was passed + # default='$XDG_CONFIG_HOME/pyssg/config.ini', type=str, - help='''config file (path) to read from; defaults to - 'pyssgrc' first, then - '$XDG_CONFIG_HOME/pyssg/pyssgrc' ''') + help='''config file (path) to read from; if not passed, + '$XDG_CONFIG_HOME/pyssg/config.ini' is used''') + parser.add_argument('--copy-default-config', + action='store_true', + help='''copies the default config to path specified in + --config flag''') + parser.add_argument('-i', '--init', + action='store_true', + help='''initializes the directory structures and copies + over default templates''') + parser.add_argument('-b', '--build', + action='store_true', + help='''generates all HTML files by parsing MD files + present in source directory and copies over manually + written HTML files''') + parser.add_argument('-f', '--force', + action='store_true', + help='''force building all pages and not only the + updated ones''') + # really not needed, too much bloat and case scenarios to check for, + # instead, just read from config file or default config file + """ parser.add_argument('-s', '--src', default='src', type=str, @@ -68,17 +87,6 @@ def get_parsed_arguments() -> Namespace: 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, - as well as the 'src' and 'dst' directories''') - parser.add_argument('-b', '--build', - 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() diff --git a/src/pyssg/builder.py b/src/pyssg/builder.py index 84494da..130062e 100644 --- a/src/pyssg/builder.py +++ b/src/pyssg/builder.py @@ -4,8 +4,8 @@ from copy import deepcopy from operator import itemgetter from jinja2 import Environment, Template from markdown import Markdown +from configparser import ConfigParser -from .configuration import Configuration from .database import Database from .parser import MDParser from .page import Page @@ -13,11 +13,11 @@ from .discovery import get_file_list, get_dir_structure class Builder: - def __init__(self, config: Configuration, + def __init__(self, config: ConfigParser, env: Environment, db: Database, md: Markdown): - self.config: Configuration = config + self.config: ConfigParser = config self.env: Environment = env self.db: Database = db self.md: Markdown = md @@ -33,15 +33,19 @@ class Builder: def build(self) -> None: - self.dirs = get_dir_structure(self.config.src, ['templates']) - self.md_files = get_file_list(self.config.src, ['.md'], ['templates']) - self.html_files = get_file_list(self.config.src, ['.html'], ['templates']) + self.dirs = get_dir_structure(self.config.get('path', 'src'), + ['templates']) + self.md_files = get_file_list(self.config.get('path', 'src'), + ['.md'], + ['templates']) + self.html_files = get_file_list(self.config.get('path', 'src'), + ['.html'], + ['templates']) self.__create_dir_structure() self.__copy_html_files() - parser: MDParser = MDParser(self.config.src, - self.md_files, + parser: MDParser = MDParser(self.md_files, self.config, self.db, self.md) @@ -69,7 +73,7 @@ class Builder: # for the dir structure, # doesn't matter if the dir already exists try: - os.makedirs(os.path.join(self.config.dst, d)) + os.makedirs(os.path.join(self.config.get('path', 'dst'), d)) except FileExistsError: pass @@ -79,18 +83,18 @@ class Builder: dst_file: str = None for f in self.html_files: - src_file = os.path.join(self.config.src, f) - dst_file = os.path.join(self.config.dst, f) + src_file = os.path.join(self.config.get('path', 'src'), f) + dst_file = os.path.join(self.config.get('path', 'dst'), f) # only copy files if they have been modified (or are new) - if self.db.update(src_file, remove=f'{self.config.src}/'): + if self.db.update(src_file, remove=f'{self.config.get("path", "src")}/'): shutil.copy2(src_file, dst_file) def __render_articles(self) -> None: article_vars: dict = deepcopy(self.common_vars) # check if only updated should be created - if self.config.force: + if self.config.getboolean('other', 'force'): for p in self.all_pages: article_vars['page'] = p self.__render_template("page.html", @@ -132,5 +136,5 @@ class Builder: template: Template = self.env.get_template(template_name) content: str = template.render(**template_vars) - with open(os.path.join(self.config.dst, file_name), 'w') as f: + with open(os.path.join(self.config.get('path', 'dst'), file_name), 'w') as f: f.write(content) diff --git a/src/pyssg/configuration.py b/src/pyssg/configuration.py index 5a09a7a..dd6bfaa 100644 --- a/src/pyssg/configuration.py +++ b/src/pyssg/configuration.py @@ -1,133 +1,42 @@ -from typing import Union +import sys from importlib.metadata import version +from importlib.resources import path as rpath from datetime import datetime, timezone +from configparser import ConfigParser -class Configuration: - def __init__(self, path: str): - self.path: str = path - # config file specific - self.src: str = None - self.dst: str = None - self.plt: str = None - self.url: str = None - self.static_url: str = None - self.default_image_url: str = None - self.title: str = None - self.dformat: str = None - self.l_dformat: str = None - self.lsep_dformat: str = None - self.force: bool = None +DEFAULT_CONFIG_PATH = '$XDG_CONFIG_HOME/pyssg/config.ini' +VERSION = version('pyssg') - # other - self.version: str = version('pyssg') - self.dformat_rss: str = '%a, %d %b %Y %H:%M:%S GMT' - self.dformat_sitemap: str = '%Y-%m-%d' - self.run_date_rss = datetime.now(tz=timezone.utc).strftime(self.dformat_rss) - self.run_date_sitemap = \ - datetime.now(tz=timezone.utc).strftime(self.dformat_sitemap) +def __check_well_formed_config(config: ConfigParser) -> None: + default_config: ConfigParser = ConfigParser() + with rpath('pyssg.plt', 'default.ini') as p: + default_config.read(p) - def read(self): - try: - lines: list[str] = None - with open(self.path, 'r') as f: - lines = f.readlines() + for section in default_config.sections(): + if not config.has_section(section): + print(f'config does not have section "{section}"') + sys.exit(1) + for option in default_config.options(section): + if not config.has_option(section, option): + print(f'config does not have option "{option}" in section "{section}"') + sys.exit(1) - 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().lower() - v_temp: str = kv[1].strip() - # check if value should be a boolean true - v: Union[str, bool] = v_temp\ - if v_temp.lower() not in ['true', '1', 'yes']\ - else True +def get_parsed_config(path: str) -> ConfigParser: + config: ConfigParser = ConfigParser() + config.read(path) - opts[k] = v + __check_well_formed_config(config) - try: - self.src = opts['src'] - except KeyError: pass + # set other required options + config.set('fmt', 'rss_date', '%%a, %%d %%b %%Y %%H:%%M:%%S GMT') + config.set('fmt', 'sitemap_date', '%%Y-%%m-%%d') + config.set('info', 'version', VERSION) + config.set('info', 'rss_run_date', datetime.now( + tz=timezone.utc).strftime(config.get('fmt', 'rss_date'))) + config.set('info', 'sitemap_run_date', datetime.now( + tz=timezone.utc).strftime(config.get('fmt', 'sitemap_date'))) - try: - self.dst = opts['dst'] - except KeyError: pass - - try: - self.plt = opts['plt'] - except KeyError: pass - - try: - self.url = opts['url'] - except KeyError: pass - - try: - self.static_url = opts['static_url'] - except KeyError: pass - - try: - self.default_image_url = opts['default_image_url'] - except KeyError: pass - - try: - self.title = opts['title'] - 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.plt is None: - self.plt = opts['plt'] - - if self.url is None: - self.url = opts['url'] - - if self.static_url is None: - self.static_url = opts['static_url'] - - if self.default_image_url is None: - self.default_image_url = opts['default_image_url'] - - if self.title is None: - self.title = opts['title'] - - 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'] + return config diff --git a/src/pyssg/page.py b/src/pyssg/page.py index 6b83d39..784749c 100644 --- a/src/pyssg/page.py +++ b/src/pyssg/page.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from .configuration import Configuration +from configparser import ConfigParser class Page: @@ -10,14 +10,14 @@ class Page: mtime: float, html: str, meta: dict, - config: Configuration): + config: ConfigParser): # initial data self.name: str = name self.ctimestamp: float = ctime self.mtimestamp: float = mtime self.content: str = html self.meta: dict = meta - self.config: Configuration = config + self.config: ConfigParser = config # data from self.meta self.title: str = '' @@ -66,23 +66,23 @@ class Page: # dates self.cdatetime = datetime.fromtimestamp(self.ctimestamp, tz=timezone.utc) - self.cdate = self.cdatetime.strftime(self.config.dformat) - self.cdate_list = self.cdatetime.strftime(self.config.l_dformat) - self.cdate_list_sep = self.cdatetime.strftime(self.config.lsep_dformat) - self.cdate_rss = self.cdatetime.strftime(self.config.dformat_rss) + self.cdate = self.cdatetime.strftime(self.config.get('fmt', 'date')) + self.cdate_list = self.cdatetime.strftime(self.config.get('fmt', 'list_date')) + self.cdate_list_sep = self.cdatetime.strftime(self.config.get('fmt', 'list_sep_date')) + self.cdate_rss = self.cdatetime.strftime(self.config.get('fmt', 'rss_date')) self.cdate_sitemap = \ - self.cdatetime.strftime(self.config.dformat_sitemap) + self.cdatetime.strftime(self.config.get('fmt', 'sitemap_date')) # only if file/page has been modified if self.mtimestamp != 0.0: self.mdatetime = datetime.fromtimestamp(self.mtimestamp, tz=timezone.utc) - self.mdate = self.mdatetime.strftime(self.config.dformat) - self.mdate_list = self.mdatetime.strftime(self.config.l_dformat) - self.mdate_list_sep = self.mdatetime.strftime(self.config.lsep_dformat) - self.mdate_rss = self.mdatetime.strftime(self.config.dformat_rss) + self.mdate = self.mdatetime.strftime(self.config.get('fmt', 'date')) + self.mdate_list = self.mdatetime.strftime(self.config.get('fmt', 'list_date')) + self.mdate_list_sep = self.mdatetime.strftime(self.config.get('fmt', 'list_sep_date')) + self.mdate_rss = self.mdatetime.strftime(self.config.get('fmt', 'rss_date')) self.mdate_sitemap = \ - self.mdatetime.strftime(self.config.dformat_sitemap) + self.mdatetime.strftime(self.config.get('fmt', 'sitemap_date')) # not always contains tags try: @@ -91,17 +91,17 @@ class Page: for t in tags_only: self.tags.append((t, - f'{self.config.url}/tag/@{t}.html')) + f'{self.config.get("url", "main")}/tag/@{t}.html')) except KeyError: pass - self.url = f'{self.config.url}/{self.name.replace(".md", ".html")}' + self.url = f'{self.config.get("url", "main")}/{self.name.replace(".md", ".html")}' try: self.image_url = \ - f'{self.config.static_url}/{self.meta["image_url"][0]}' + f'{self.config.get("url", "static")}/{self.meta["image_url"][0]}' except KeyError: self.image_url = \ - f'{self.config.static_url}/{self.config.default_image_url}' + f'{self.config.get("url", "static")}/{self.config.get("url", "default_image")}' # if contains open graph elements try: diff --git a/src/pyssg/parser.py b/src/pyssg/parser.py index f2d23eb..2888fcb 100644 --- a/src/pyssg/parser.py +++ b/src/pyssg/parser.py @@ -1,24 +1,21 @@ import os from operator import itemgetter -from datetime import datetime from markdown import Markdown +from configparser import ConfigParser from .database import Database -from .configuration import Configuration from .page import Page # parser of md files, stores list of pages and tags class MDParser: - def __init__(self, src: str, - files: list[str], - config: Configuration, + def __init__(self, files: list[str], + config: ConfigParser, db: Database, md: Markdown): - self.src: str = src self.files: list[str] = files - self.config: Configuration = config + self.config: ConfigParser = config self.db: Database = db self.md: Markdown = md @@ -32,12 +29,13 @@ class MDParser: self.all_pages = [] self.updated_pages = [] self.all_tags = [] - all_tag_names: list[str] = [] + # not used, not sure why i had this + # all_tag_names: list[str] = [] for f in self.files: - src_file: str = os.path.join(self.src, f) + src_file: str = os.path.join(self.config.get('path', 'src'), f) # get flag if update is successful - updated: bool = self.db.update(src_file, remove=f'{self.src}/') + updated: bool = self.db.update(src_file, remove=f'{self.config.get("path", "src")}/') content: str = self.md.reset().convert(open(src_file).read()) page: Page = Page(f, diff --git a/src/pyssg/plt/default.ini b/src/pyssg/plt/default.ini index 2700d28..ab4eac1 100644 --- a/src/pyssg/plt/default.ini +++ b/src/pyssg/plt/default.ini @@ -1,14 +1,16 @@ -[dir_paths] +[path] src=src dst=dst plt=plt -[urls] -url=https://example.com -static_url=https://static.example.com -default_image_url=/images/default.png -[formats] -date_format=%%a, %%b %%d, %%Y @ %%H:%%M %%Z -list_date_format=%%b %%d -list_sep_date_format=%%B %%Y +[url] +main=https://example.com +static=https://static.example.com +default_image=/images/default.png +[fmt] +date=%%a, %%b %%d, %%Y @ %%H:%%M %%Z +list_date=%%b %%d +list_sep_date=%%B %%Y [info] -title=Example site
\ No newline at end of file +title=Example site +[other] +force=False
\ No newline at end of file diff --git a/src/pyssg/plt/index.html b/src/pyssg/plt/index.html index e06efdb..09ca786 100644 --- a/src/pyssg/plt/index.html +++ b/src/pyssg/plt/index.html @@ -2,11 +2,11 @@ <html lang="en"> <head> <meta charset="utf-8"> - <base href="{{config.static_url}}"> - <title>Index -- {{config.title}}</title> + <base href="{{config.get('url', 'static')}}"> + <title>Index -- {{config.get('info', 'title')}}</title> </head> <body> - <h1>Index -- {{config.title}}</h1> + <h1>Index -- {{config.get('info', 'title')}}</h1> <p>Some text here.</p> <p>Tags: diff --git a/src/pyssg/plt/page.html b/src/pyssg/plt/page.html index 2fc3943..15663fa 100644 --- a/src/pyssg/plt/page.html +++ b/src/pyssg/plt/page.html @@ -2,8 +2,8 @@ <html lang="{{page.lang}}"> <head> <meta charset="utf-8"> - <base href="{{config.static_url}}"> - <title>{{page.title}} -- {{config.title}}</title> + <base href="{{config.get('url', 'static')}}"> + <title>{{page.title}} -- {{config.get('info', 'title')}}</title> </head> <body> <h1>{{page.title}}</h1> diff --git a/src/pyssg/plt/rss.xml b/src/pyssg/plt/rss.xml index 42020d7..be6ddf0 100644 --- a/src/pyssg/plt/rss.xml +++ b/src/pyssg/plt/rss.xml @@ -3,24 +3,24 @@ xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"> <channel> - <title>{{config.title}}</title> - <link>{{config.url}}</link> - <atom:link href="{{config.url}}/rss.xml" rel="self" type="application/rss+xml"/> + <title>{{config.get('info', 'title')}}</title> + <link>{{config.get('url', 'main')}}</link> + <atom:link href="{{config.get('url', 'main')}}/rss.xml" rel="self" type="application/rss+xml"/> <description>Short site description.</description> <language>en-us</language> <category>Blog</category> <copyright>Copyright 2021 Somebody</copyright> <managingEditor>some@one.com (Sombody)</managingEditor> <webMaster>some@one.com (Sombody)</webMaster> - <pubDate>{{config.run_date_rss}}</pubDate> - <lastBuildDate>{{run_date_rss}}</lastBuildDate> - <generator>pyssg v{{config.version}}</generator> + <pubDate>{{config.get('info', 'rss_run_date')}}</pubDate> + <lastBuildDate>{{config.get('info', 'rss_run_date')}}</lastBuildDate> + <generator>pyssg v{{config.get('info', 'version')}}</generator> <docs>https://validator.w3.org/feed/docs/rss2.html</docs> <ttl>30</ttl> <image> - <url>{{config.static_url}}/images/blog.png</url> - <title>{{config.title}}</title> - <link>{{config.url}}</link> + <url>{{config.get('url', 'static')}}/images/blog.png</url> + <title>{{config.get('info', 'title')}}</title> + <link>{{config.get('url', 'main')}}</link> </image> {%for p in all_pages%} <item> diff --git a/src/pyssg/plt/sitemap.xml b/src/pyssg/plt/sitemap.xml index 26ee5c1..af1212a 100644 --- a/src/pyssg/plt/sitemap.xml +++ b/src/pyssg/plt/sitemap.xml @@ -14,7 +14,7 @@ {%for t in all_tags%} <url> <loc>{{t[1]}}</loc> - <lastmod>{{config.run_date_sitemap}}</lastmod> + <lastmod>{{config.get('info', 'sitemap_run_date')}}</lastmod> <changefreq>daily</changefreq> <priority>0.5</priority> </url> diff --git a/src/pyssg/plt/tag.html b/src/pyssg/plt/tag.html index d856ce4..ffd1956 100644 --- a/src/pyssg/plt/tag.html +++ b/src/pyssg/plt/tag.html @@ -2,8 +2,8 @@ <html lang="en"> <head> <meta charset="utf-8"> - <base href="{{config.static_url}}"> - <title>Posts filtered by {{tag[0]}} -- {{config.title}}</title> + <base href="{{config.get('url', 'static')}}"> + <title>Posts filtered by {{tag[0]}} -- {{config.get('info', 'title')}}</title> </head> <body> <h1>Posts filtered by {{tag[0]}}</h1> diff --git a/src/pyssg/pyssg.py b/src/pyssg/pyssg.py index e694565..9b82231 100644 --- a/src/pyssg/pyssg.py +++ b/src/pyssg/pyssg.py @@ -1,7 +1,8 @@ import os -import shutil -from importlib.resources import path +import sys +from importlib.resources import path as rpath from typing import Union +from configparser import ConfigParser from jinja2 import Environment, FileSystemLoader from markdown import Markdown @@ -9,61 +10,65 @@ from yafg import YafgExtension from MarkdownHighlight.highlight import HighlightExtension from markdown_checklist.extension import ChecklistExtension +from .utils import create_dir, copy_file, sanity_check_path from .arg_parser import get_parsed_arguments -from .configuration import Configuration +from .configuration import get_parsed_config, DEFAULT_CONFIG_PATH, VERSION from .database import Database from .builder import Builder def main() -> None: - opts: dict[str, Union[str, bool]] = vars(get_parsed_arguments()) - conf_path: str = opts['config'] - conf_path = os.path.expandvars(conf_path) - - - config: Configuration = None - if os.path.exists('pyssgrc'): - config = Configuration('pyssgrc') - else: - config = Configuration(conf_path) - - config.read() - config.fill_missing(opts) - - if opts['version']: - print(f'pyssg v{config.version}') - return - - if opts['init']: - try: - os.mkdir(config.src) - os.makedirs(os.path.join(config.dst, 'tag')) - os.mkdir(config.plt) - except FileExistsError: - pass - - # copy basic template files + args: dict[str, Union[str, bool]] = vars(get_parsed_arguments()) + if not len(sys.argv) > 1: + print(f'pyssg v{VERSION} - no arguments passed, --help for more') + sys.exit(0) + + if args['version']: + print(f'pyssg v{VERSION}') + sys.exit(0) + + config_path: str = args['config'] if args['config'] else DEFAULT_CONFIG_PATH + config_path = os.path.normpath(os.path.expandvars(config_path)) + sanity_check_path(config_path) + config_dir, _ = os.path.split(config_path) + + if args['copy_default_config']: + create_dir(config_dir) + with rpath('pyssg.plt', 'default.ini') as p: + copy_file(p, config_path) + sys.exit(0) + + if not os.path.exists(config_path): + print(f'''config file does't exist in path "{config_path}"; make sure + the path is correct; use --copy-default-config to if you + haven't already''') + sys.exit(1) + + config: ConfigParser = get_parsed_config(config_path) + + if args['init']: + create_dir(config.get('path', 'src')) + create_dir(os.path.join(config.get('path', 'dst'), 'tag'), True) + create_dir(config.get('path', 'plt')) files: list[str] = ('index.html', 'page.html', 'tag.html', 'rss.xml', 'sitemap.xml') for f in files: - plt_file: str = os.path.join(config.plt, f) - with path('pyssg.plt', f) as p: - if not os.path.exists(plt_file): - shutil.copy(p, plt_file) - - return + plt_file: str = os.path.join(config.get('path', 'plt'), f) + with rpath('pyssg.plt', f) as p: + copy_file(p, plt_file) + sys.exit(0) - if opts['build']: + if args['build']: # start the db - db: Database = Database(os.path.join(config.src, '.files')) + db: Database = Database(os.path.join(config.get('path', 'src'), '.files')) db.read() # the autoescape option could be a security risk if used in a dynamic # website, as far as i can tell - env: Environment = Environment(loader=FileSystemLoader(config.plt), + env: Environment = Environment(loader=FileSystemLoader(config.get('path', 'plt')), autoescape=False, trim_blocks=True, lstrip_blocks=True) @@ -92,4 +97,4 @@ def main() -> None: builder.build() db.write() - return + sys.exit(0) diff --git a/src/pyssg/utils.py b/src/pyssg/utils.py new file mode 100644 index 0000000..8e5d90e --- /dev/null +++ b/src/pyssg/utils.py @@ -0,0 +1,28 @@ +import os +import sys +import shutil + + +def create_dir(path: str, p: bool=False) -> None: + try: + if p: + os.makedirs(path) + else: + os.mkdir(path) + print(f'created directory "{path}"') + except FileExistsError: + print(f'directory "{path}" already exists') + + +def copy_file(src: str, dst: str) -> None: + if not os.path.exists(dst): + shutil.copy(src, dst) + print(f'copied file "{src}" to "{dst}"') + else: + print(f'"{dst}" already exists') + + +def sanity_check_path(path: str) -> None: + if '$' in path: + print(f'"$" character found in path: "{path}"; could be due to non-existant env var.') + sys.exit(1) |