diff --git a/README.md b/README.md index e7fbf50..ae71892 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ resulting .html file will be named. ### Generating the blog -`russell generate` will create a file `run.py` which you can invoke to generate -your static site. +`russell generate` will run the `generate` function in your `config.py`, which +should contain all the instructions for generating HTML and other assets. To test your newly generated site, run `russell serve`. diff --git a/example/config.py b/example/config.py new file mode 100644 index 0000000..4bced83 --- /dev/null +++ b/example/config.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import os.path +import logging +import sass +import russell + +root_path = os.path.dirname(__file__) +args = russell.get_cli_args() +logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)8s [%(name)s] %(message)s", +) +blog = russell.BlogEngine( + root_path=root_path, + root_url=args.root_url or "//localhost", + site_title="Russell example", + site_desc=("An example Russell site."), +) + +# add content +blog.add_pages() +blog.add_posts() + + +def generate(): + # copy and generate assets + blog.copy_assets() + blog.write_file( + "assets/style.css", + sass.compile(filename=os.path.join(blog.root_path, "style.sass")), + ) + blog.add_asset_hashes() + + # generate HTML pages + blog.generate_index(num_posts=3) + blog.generate_archive() + blog.generate_pages() + blog.generate_posts() + blog.generate_tags() + + # generate other stuff + blog.generate_sitemap(https=False) + blog.generate_rss() + blog.write_file("robots.txt", "User-agent: *\nDisallow:\n") diff --git a/example/run.py b/example/run.py deleted file mode 100755 index 23d67ad..0000000 --- a/example/run.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 - -import os.path -import logging -import sass -from russell import BlogEngine - -ROOT_DIR = os.path.dirname(__file__) - -logging.basicConfig() - -blog = BlogEngine( - root_path=ROOT_DIR, - root_url="//localhost", - site_title="Russell example", - site_desc=("An example Russell site."), -) - -# add content -blog.add_pages() -blog.add_posts() - -# copy and generate assets -blog.copy_assets() -blog.write_file( - "assets/style.css", sass.compile(filename=os.path.join(ROOT_DIR, "style.sass")) -) -blog.add_asset_hashes() - -# generate HTML pages -blog.generate_index(num_posts=3) -blog.generate_archive() -blog.generate_pages() -blog.generate_posts() -blog.generate_tags() - -# generate other stuff -blog.generate_sitemap(https=False) -blog.generate_rss() -blog.write_file("robots.txt", "User-agent: *\nDisallow:\n") diff --git a/russell/__init__.py b/russell/__init__.py index 4bbcd1b..38ba502 100644 --- a/russell/__init__.py +++ b/russell/__init__.py @@ -1,2 +1,3 @@ from .__version__ import __version__ from russell.engine import BlogEngine +from russell.cli import get_args as get_cli_args diff --git a/russell/__version__.py b/russell/__version__.py index db55ef1..906d362 100644 --- a/russell/__version__.py +++ b/russell/__version__.py @@ -1 +1 @@ -__version__ = "0.5.10" +__version__ = "0.6.0" diff --git a/russell/cli.py b/russell/cli.py index 9fa14e9..10e0096 100644 --- a/russell/cli.py +++ b/russell/cli.py @@ -1,14 +1,29 @@ import argparse import datetime +import functools +import http.server +import importlib.machinery +import importlib.util import os import os.path +import re import shutil import subprocess import dateutil.tz import slugify -import russell +from russell.__version__ import __version__ + + +def load_config_py(path=None): + if path is None: + path = os.getcwd() + "/config.py" + loader = importlib.machinery.SourceFileLoader("russell_config", path) + spec = importlib.util.spec_from_loader(loader.name, loader) + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + return mod def setup(dest): @@ -96,23 +111,61 @@ def publish(draft_file, update_pubdate=True): def generate(): - subprocess.check_call(["python", "run.py"]) - - -def serve(): + russell_config = load_config_py() + russell_config.generate() + + +class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + hash_pattern = re.compile(r"[0-9a-f]{8,12,16,24,32,48,64}") + + def translate_path(self, path): + path = super().translate_path(path) + try_paths = [] + + directory, filename = path.rsplit("/", maxsplit=1) + if "." in filename: + file_parts = filename.split(".") + if ( + len(file_parts) > 2 + and len(file_parts[1]) % 8 == 0 + and re.match(r"[0-9a-f]", file_parts[1]) + ): + del file_parts[1] + unbusted_path = "/".join([directory, ".".join(file_parts)]) + try_paths.append(unbusted_path) + else: + try_paths.extend([path + ".html", path + "/index.html"]) + + for try_path in try_paths: + if os.path.exists(try_path): + return try_path + return path + + +def serve(dist_dir): try: - subprocess.check_call(["sh", "-c", "cd dist && python -m http.server"]) + httpd = http.server.HTTPServer( + ("127.0.0.1", 8000), + functools.partial(CustomHTTPRequestHandler, directory=dist_dir), + ) + sa = httpd.socket.getsockname() + print("Serving HTTP on http://%s:%s/ ..." % sa) + httpd.serve_forever() except KeyboardInterrupt: pass + finally: + httpd.server_close() def get_parser(): parser = argparse.ArgumentParser("russell") + parser.add_argument("-v", "--verbose", action="store_true") + parser.add_argument("-r", "--root-path", default=os.getcwd()) parser.add_argument( - "-v", + "-V", "--version", action="version", - version="russell version " + str(russell.__version__), + version="russell version " + str(__version__), ) cmd_subparsers = parser.add_subparsers(dest="command") @@ -139,20 +192,29 @@ def get_parser(): ) generate_parser = cmd_subparsers.add_parser("generate") + generate_parser.add_argument("--root-url") serve_parser = cmd_subparsers.add_parser("serve") + serve_parser.add_argument( + "-d", "--dist-dir", default=os.path.join(os.getcwd(), "dist") + ) return parser -def parse_args(parser=None, args=None): - parser = parser or get_parser() - return parser.parse_args(args) +_args = None + + +def get_args(): + global _args + return _args def main(args=None): parser = get_parser() - args = parse_args(parser) + args = parser.parse_args() + global _args + _args = args if not args.command: return parser.print_help() @@ -174,7 +236,7 @@ def main(args=None): if args.command == "generate": return generate() if args.command == "serve": - return serve() + return serve(os.path.join(os.getcwd(), "dist")) if __name__ == "__main__": diff --git a/russell/content.py b/russell/content.py index 0cf2adb..7b62fc7 100644 --- a/russell/content.py +++ b/russell/content.py @@ -12,9 +12,11 @@ SYSTEM_TZINFO = dateutil.tz.tzlocal() -def render_markdown(md): - extensions = ["markdown.extensions.fenced_code"] - return markdown.markdown(md, extensions=extensions) +md = markdown.Markdown(extensions=["markdown.extensions.fenced_code"]) + + +def render_markdown(text): + return md.convert(text) def schema_url(url, https=False): diff --git a/russell/engine.py b/russell/engine.py index d770f96..3492c8d 100644 --- a/russell/engine.py +++ b/russell/engine.py @@ -46,7 +46,14 @@ class BlogEngine: generating end results. """ - def __init__(self, root_path, root_url, site_title, site_desc=None): + def __init__( + self, + root_path, + root_url, + site_title, + site_desc=None, + cache_busting_strategy="qs", + ): """ Constructor. @@ -56,7 +63,10 @@ def __init__(self, root_path, root_url, site_title, site_desc=None): root_url (str): The root URL of your website. site_title (str): The title of your website. site_desc (str): A subtitle or description of your website. + cache_busting_strategy (str): None, "qs" or "part" """ + assert os.path.exists(root_path), "root_path must be an existing directory" + assert root_url, "root_url must be set" self.root_path = root_path self.root_url = root_url self.site_title = site_title @@ -70,6 +80,13 @@ def __init__(self, root_path, root_url, site_title, site_desc=None): self.tags = self.cm.tags self.asset_hash = {} + if cache_busting_strategy == "qs": + self.get_asset_url = self.get_asset_url_qs + elif cache_busting_strategy == "part": + self.get_asset_url = self.get_asset_url_part + else: + LOG.warning("no cache busting will be used!") + self.get_asset_url = str self.jinja = jinja2.Environment( loader=jinja2.FileSystemLoader(os.path.join(root_path, "templates")), @@ -87,7 +104,21 @@ def __init__(self, root_path, root_url, site_title, site_desc=None): } ) - def get_asset_url(self, path): + def get_asset_url_qs(self, path): + """ + Get the URL of an asset. If asset hashes are added and one exists for + the path, it will be appended as a query string. + + Args: + path (str): Path to the file, relative to your "assets" directory. + """ + if path.endswith(self.bust_extensions) and path in self.asset_hash: + path += "?" + self.asset_hash[path] + return self.root_url + "/assets/" + path + + bust_extensions = (".js", ".min.js", ".js.map", ".css", ".min.css", ".css.map") + + def get_asset_url_part(self, path): """ Get the URL of an asset. If asset hashes are added and one exists for the path, it will be appended as a query string. @@ -95,10 +126,12 @@ def get_asset_url(self, path): Args: path (str): Path to the file, relative to your "assets" directory. """ - url = self.root_url + "/assets/" + path - if path in self.asset_hash: - url += "?" + self.asset_hash[path] - return url + if path.endswith(self.bust_extensions) and path in self.asset_hash: + *dirs, filename = path.split("/") + file_parts = filename.split(".", maxsplit=1) + file_parts.insert(1, self.asset_hash[path]) + path = "/".join(dirs + [".".join(file_parts)]) + return self.root_url + "/assets/" + path def add_pages(self, path="pages"): """ diff --git a/tests/engine_test.py b/tests/engine_test.py index f036003..86846fb 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -16,8 +16,9 @@ def test_get_asset_link(engine): def test_get_asset_link_with_hash(engine): - engine.asset_hash["test.css"] = "asdf" - assert "//localhost/assets/test.css?asdf" == engine.get_asset_url("test.css") + engine.asset_hash["test.css"] = "0f4c" + assert "//localhost/assets/test.css?0f4c" == engine.get_asset_url_qs("test.css") + assert "//localhost/assets/test.0f4c.css" == engine.get_asset_url_part("test.css") def test_get_posts_does_not_mutate_posts(engine): diff --git a/tests/functional_cli_test.py b/tests/functional_cli_test.py index 096a531..358556e 100644 --- a/tests/functional_cli_test.py +++ b/tests/functional_cli_test.py @@ -15,7 +15,7 @@ def russell_dir_variation(tmpdir, request): def test_setup_new_and_existing(russell_dir_variation): d = russell_dir_variation russell.cli.setup(str(d)) - assert d.join("run.py").check() + assert d.join("config.py").check() assert d.join("requirements.txt").check() assert d.join(".gitignore").check() assert d.join("style.sass").check()