From 38ab620c055e31568a792cef1c278aa608db7cf8 Mon Sep 17 00:00:00 2001 From: Nguyễn Gia Phong Date: Mon, 20 Mar 2023 04:42:43 +0900 Subject: Switch to XML templating --- .gitignore | 4 ++- pyproject.toml | 9 +++---- src/rub/__init__.py | 62 ++++++++++++++++++++++++++++++++++------------- src/rub/__main__.py | 21 ---------------- src/rub/xml.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 44 deletions(-) delete mode 100644 src/rub/__main__.py create mode 100644 src/rub/xml.py diff --git a/.gitignore b/.gitignore index dbb91d8..48698d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +__pycache__/ dist/ -eg/ +src/* +!src/rub/ diff --git a/pyproject.toml b/pyproject.toml index 6f6167b..50e3fab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,16 +7,15 @@ name = "rub" version = "0.0.1" description = "A static generator" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = { file = "COPYING" } authors = [ { name = "Nguyễn Gia Phong", email = "mcsinyx@disroot.org" } ] -maintainers = [ { name = "Nguyễn Gia Phong", email = "mcsinyx@disroot.org" } ] -keywords = [ "ssg", "feed", "markdown" ] +keywords = [ "ssg", "feed", "xml" ] classifiers = [ "Development Status :: 1 - Planning", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Utilities" ] -dependencies = [ "doit", "Jinja2", "markdown-it-py[linkify,plugins]" ] -urls = { SourceHut = "https://sr.ht/~cnx/rub" } +dependencies = [ "doit", "lxml" ] +urls = { Documentation = "https://rub.parody" } diff --git a/src/rub/__init__.py b/src/rub/__init__.py index dd963e8..904f47a 100644 --- a/src/rub/__init__.py +++ b/src/rub/__init__.py @@ -1,5 +1,5 @@ # Package initialization -# Copyright (C) 2022 Nguyễn Gia Phong +# Copyright (C) 2022-2023 Nguyễn Gia Phong # # This file is part of rub. # @@ -16,33 +16,61 @@ # You should have received a copy of the GNU Affero General Public License # along with rub. If not, see . +from functools import cached_property from os import walk -from os.path import join from pathlib import Path from shutil import copytree, rmtree -from jinja2 import Environment -from markdown_it import MarkdownIt -from mdit_py_plugins.footnote import footnote_plugin +from rub import xml -__all__ = ['glob_files', 'markdown', 'replace'] -md = MarkdownIt('gfm-like').use(footnote_plugin) -jinja = Environment() +__all__ = ['Rubber', 'xml'] -def glob_files(root: Path, suffix: str = '') -> list[Path]: +def glob_files(root: Path, suffix=''): """Return the list of all files in given directory, recursively.""" - return [Path(path)/file for path, dirs, files in walk(root) + return [Path(path).relative_to(root)/file + for path, dirs, files in walk(root) for file in files if file.endswith(suffix)] -def markdown(source: Path, destination: Path) -> None: - """Convert source Markdown to destination HTML segment.""" - template = jinja.from_string(source.read_text()) - destination.write_text(md.render(template.render())) - - -def replace(source: Path, destination: Path) -> None: +def replace(source: Path, destination: Path): """Replace destination with source directory.""" rmtree(destination, ignore_errors=True) copytree(source, destination, dirs_exist_ok=True) + + +class Rubber: + """Static generator.""" + + def __init__(self, generate_article, base, src, cache, out): + self.generate_article = generate_article + self.base, self.src = base, src + self.cache, self.out = cache, out + + @cached_property + def tasks(self): + def assox(): + for k in dir(self): + if not k.startswith('task_'): continue + v = getattr(self, k) + if callable(v): yield k, v + + return dict(assox()) + + def task_base(self): + paths = glob_files(self.base) + return {'doc': 'copy base directory', + 'file_dep': [self.base/path for path in paths], + 'actions': [(replace, [self.base, self.out])], + 'targets': [self.out/path for path in paths], + 'clean': True} + + def task_articles(self): + """process articles into XHTML""" + for path in glob_files(self.src, '.xml'): + source = self.src / path + destination = self.out / path + yield {'name': path, 'doc': f'process {path} into XHTML', + 'file_dep': [source], + 'actions': [(self.generate_article, [source, destination])], + 'targets': [destination], 'clean': True} diff --git a/src/rub/__main__.py b/src/rub/__main__.py deleted file mode 100644 index 8c17c86..0000000 --- a/src/rub/__main__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Package's executable -# Copyright (C) 2022 Nguyễn Gia Phong -# -# This file is part of rub. -# -# Rub is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Rub is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with rub. If not, see . - -from doit import run - -if __name__ == '__main__': run(globals()) diff --git a/src/rub/xml.py b/src/rub/xml.py new file mode 100644 index 0000000..ed61a8b --- /dev/null +++ b/src/rub/xml.py @@ -0,0 +1,70 @@ +# XML processing abstractions +# Copyright (C) 2023 Nguyễn Gia Phong +# +# This file is part of rub. +# +# Rub is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rub is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with rub. If not, see . + +from copy import deepcopy +from functools import partial +from pathlib import Path + +from lxml.etree import QName, XML, XSLT, XSLTExtension + +__all__ = ['NS', 'generator', 'recurse'] + +NS = 'https://rub.parody' + + +def recurse(extension, context, input_node, output_parent): + """Apply template recursively on input node.""" + output = deepcopy(input_node) + for i in output: output.remove(i) + for i in input_node: + for j in extension.apply_templates(context, i): + if not isinstance(j, str): + output.append(deepcopy(j)) + elif len(output) == 0: + if output.text is None: + output.text = j + else: + output.text += j + elif output[-1].tail is None: + output[-1].tail = j + else: + output[-1].tail += j + output_parent.append(output) + + +class Evaluator(XSLTExtension): + def __init__(self, **handlers): + self.handlers = {QName(NS, k).text: v for k, v in handlers.items()} + super().__init__() + + def execute(self, context, self_node, input_node, output_parent): + handle = self.handlers.get(input_node.tag, recurse) + handle(self, context, input_node, output_parent) + + +def generator(xslt, **handlers): + """Return a function taking an XML file and apply given XSLT.""" + stylesheet = xslt.read_bytes() + extensions = {(NS, 'eval'): Evaluator(**handlers)} + transform = XSLT(XML(stylesheet), extensions=extensions) + + def make(src, dest): + dest.parent.mkdir(mode=0o755, parents=True, exist_ok=True) + dest.write_text(str(transform(XML(src.read_bytes())))) + + return make -- cgit 1.4.1