diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml deleted file mode 100644 index 53dd92f..0000000 --- a/.forgejo/workflows/publish.yml +++ /dev/null @@ -1,17 +0,0 @@ -on: - push: - tags: - - 'v*' -jobs: - publish: - runs-on: based-alpine - steps: - - uses: actions/checkout@v4 - - name: build - run: | - uv sync - uv build - - name: publish - run: | - uv publish --token ${{ secrets.PYPI_TOKEN }} - diff --git a/LICENSE b/LICENSE index 1cbda58..ac101d3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,13 @@ -Copyright (c) 2025 Daniel Fichtinger +Copyright (c) 2025 Daniel Fichtinger -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. Neither the name of the author nor the names of its contributors may - be used to endorse or promote products derived from this software +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index bd7eb9b..9ae4ef4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@

zona

-[zona](https://git.ficd.sh/ficd/zona) is an _opinionated_ static site generator -written in Python. From a structured directory of Markdown content, zona builds -a simple static website. It's designed to get out of your way and let you focus -on writing. +[zona](https://sr.ht/~ficd/zona) is an _opinionated_ static site generator +written in Python. From a structured directory of Markdown content, zona +builds a simple static website. It's designed to get out of your way and +let you focus on writing. -**What do I mean by opinionated?** I built zona primarily for myself. I've tried -making it flexible by exposing as many variables as possible to the template -engine. However, if you're looking for something stable, complete, and fully -configurable, zona may not be for you. If you want a minimal Markdown blog and -are comfortable with modifying `jinja2` templates and CSS, then you're in luck. +**What do I mean by opinionated?** I built zona primarily for myself. I've +tried making it flexible by exposing as many variables as possible to the +template engine. However, if you're looking for something stable, +complete, and fully configurable, zona may not be for you. If you want a +minimal Markdown blog and are comfortable with modifying `jinja2` +templates and CSS, then you're in luck. + +**Note:** This project is in early development, there are no versioned +releases yet, and breaking changes are likely. Versioned releases will be +made and zona will be published to PyPI once it's stable. zona was +previously implemented in Go; I decided to rewrite the project in Python. +If you're interested in seeing the previous codebase (which is feature +incomplete), visit the [~ficd/zona-go](https://git.sr.ht/~ficd/zona-go) +repository. For an example of a website built with zona, please see -[ficd.sh](https://ficd.sh). For a list of known problems, see -[Known Problems](#known-problems). +[ficd.sh](https://ficd.sh). @@ -23,7 +31,6 @@ For an example of a website built with zona, please see - [Getting Started](#getting-started) - [Building](#building) - [Live Preview](#live-preview) - - [Live Reload](#live-reload) - [How It Works](#how-it-works) - [Site Layout](#site-layout) - [Templates](#templates) @@ -38,7 +45,6 @@ For an example of a website built with zona, please see - [Sitemap](#sitemap) - [Ignore List](#ignore-list) - [Drafts](#drafts) -- [Known Problems](#known-problems) @@ -54,29 +60,24 @@ For an example of a website built with zona, please see - Easily configurable sitemap header. - Site footer written in Markdown. - Smart site layout discovery. - - Blog posts are automatically discovered and rendered accordingly (can be - overridden in frontmatter). + - Blog posts are automatically discovered and rendered accordingly (can + be overridden in frontmatter). - Extended Markdown renderer: - Smart internal link resolution. - Syntax highlighting. - Includes Kakoune syntax and [Ashen] highlighting. - [Image labels](#image-labels). - - Many `python-markdown` extensions enabled, including footnotes, tables, - abbreviations, etc. + - Many `python-markdown` extensions enabled, including footnotes, + tables, abbreviations, etc. - LaTeX support. ## Installation -Zona can be installed as a Python package. Instructions for -[`uv`](https://docs.astral.sh/uv/) are provided. +zona is not yet packaged on PyPI. You may use `uv` to install it from this +repository: ```sh -# install latest release -uv tool install zona -# install bleeding edge from git -uv tool install 'git+https://git.ficd.sh/ficd/zona' -# you can also run without installation -uvx zona build --help +uv tool install 'git+https://git.sr.ht/~ficd/zona' ``` ## Usage @@ -86,78 +87,71 @@ available options and arguments._ ### Getting Started -To set up a new website, create a new directory and run `zona init` inside of -it. This creates the required directory structure and writes the default -configuration file. The default templates and default stylesheet are also -written. +To set up a new website, create a new directory and run `zona init` inside +of it. This creates the required directory structure and writes the +default configuration file. The default templates and default stylesheet +are also written. ### Building -To build the website, run `zona build`. The project root is discovered according -to the location of `config.yml`. By default, the output directory is called -`public`, and saved inside the root directory. +To build the website, run `zona build`. The project root is discovered +according to the location of `config.yml`. By default, the output +directory is called `public`, and saved inside the root directory. If you don't want discovery, you can specify the project root as the first argument to `zona build`. You may specify a path for the output using the -`--output/-o` flag. The `--draft/-d` flag includes draft posts in the output. +`--output/-o` flag. The `--draft/-d` flag includes draft posts in the +output. -_Note: the previous build is _not_ cleaned before the new site is built. If -you've deleted some pages, you may need to remove the output directory before -rebuilding._ +_Note: the previous build is _not_ cleaned before the new site is built. +If you've deleted some pages, you may need to remove the output directory +before rebuilding._ ### Live Preview -To make the writing process as frictionless as possible, zona ships with a live -preview server. It spins up an HTTP server, meaning that internal links work -properly (this is not the case if you simply open the `.html` files in your -browser.) +To make the writing process as frictionless as possible, zona ships with a +live preview server. It spins up an HTTP server, meaning that internal +links work properly (this is not the case if you simply open the `.html` +files in your browser.) -Additionally, the server watches for changes to all source files, and rebuilds -the website when they're modified. _Note: the entire website is rebuilt — this -ensures that links are properly resolved._ +Additionally, the server watches for changes to all source files, and +rebuilds the website when they're modified. _Note: the entire website is +rebuilt — this ensures that links are properly resolved._ -Drafts are enabled by default in live preview. Use `--final/-f` to disable them. -By default, the build outputs to a temporary directory. Use `-o/--output` to -override this. +Optionally, live reloading of the browser is also provided. With this +feature (enabled by default), your browser will automatically refresh open +pages whenever the site is rebuilt. The live reloading requires JavaScript +support from the browser — this is why the feature is optional. -**Note**: if the live preview isn't working as expected, try restarting the -server. If you change the configuration or any templates, the server must also -be restarted. The live preview uses the same function as `zona build` -internally; this means that the output is also written to disk. +To start a preview server, use `zona serve`. You can specify the root +directory as its first argument. Use the `--host` to specify a host name +(`localhost` by default) and `--port/-p` to specify a port (default: +`8000`). The `--no-live-reload/-n` disables the live browser reloading +(_automatic site rebuilds are not disabled_). -#### Live Reload +Drafts are enabled by default in live preview. Use `--final/-f` to disable +them. By default, the build outputs to a temporary directory. Use +`-o/--output` to override this. -Optionally, live reloading of the browser is also provided. With this feature -(enabled by default), your browser will automatically refresh open pages -whenever the site is rebuilt. The live reloading requires JavaScript support -from the browser — this is why the feature is optional. - -To start a preview server, use `zona serve`. You can specify the root directory -as its first argument. Use the `--host` to specify a host name (`localhost` by -default) and `--port/-p` to specify a port (default: `8000`). - -The `--live-reload/--no-live-reload` option overrides the value set in the -[config](#configuration) (`true` by default). _Automatic site rebuilds are not -affected_. - -If you are scrolled to the bottom of the page in the browser, and you extend the -height of the page by adding new content, you will automatically be scrolled to -the _new_ bottom after reloading. You may tune the tolerance threshold in the -[configuration](#configuration). +**Note**: if the live preview isn't working as expected, try restarting +the server. If you change the configuration or any templates, the server +must also be restarted. The live preview uses the same function as +`zona build` internally; this means that the output is also written to +disk. #### How It Works -The basic idea is this: after a rebuild, the server needs to notify your browser -to refresh the open pages. We implement this using a small amount of JavaScript. -The server injects a tiny script into any HTML page it serves; which causes your -browser to open a WebSocket connection with the server. When the site is -rebuilt, the server notifies your browser via the WebSocket, which reloads the -page. +The basic idea is this: after a rebuild, the server needs to notify your +browser to refresh the open pages. We implement this using a small amount +of JavaScript. The server injects a tiny script into any HTML page it +serves; which causes your browser to open a WebSocket connection with the +server. When the site is rebuilt, the server notifies your browser via the +WebSocket, which reloads the page. Unfortunately, there is no way to implement this feature without using -JavaScript. **JavaScript is _only_ used for the live preview feature. The script -is injected by the server, and never written to the HTML files in the output -directory.** +JavaScript. **JavaScript is _only_ used for the live preview feature. The +script is injected by the server, and never written to the HTML files in +the output directory.** ### Site Layout @@ -170,31 +164,32 @@ templates/ public/ ``` -The **root** of the zona **project** _must_ contain the configuration file, -`config.yml`, and a directory called `content`. A directory called `templates` -is optional, and merged with the defaults if it exists. `public` is the built -site output — it's recommended to add this path to your `.gitignore`. +The **root** of the zona **project** _must_ contain the configuration +file, `config.yml`, and a directory called `content`. A directory called +`templates` is optional, and prioritized if it exists. `public` is the +built site output — it's recommended to add this path to your +`.gitignore`. The `content` directory is the **root of the website**. Think of it as the -**content root**. For example, suppose your website is hosted at `example.com`. -`content/blog/index.md` corresponds to `example.com/blog`, +**content root**. For example, suppose your website is hosted at +`example.com`. `content/blog/index.md` corresponds to `example.com/blog`, `content/blog/my-post.md` becomes `example.com/blog/my-post`, etc. - Internal links are resolved **relative to the `content` directory.** - Templates are resolved relative to the `template` directory. Markdown files inside a certain directory (`content/blog` by default) are -automatically treated as _blog posts_. This means they are rendered with the -`page` template, and included in the `post_list`, which can be included in your -site using the `post_list` template. +automatically treated as _blog posts_. This means they are rendered with +the `page` template, and included in the `post_list`, which can be +included in your site using the `post_list` template. ### Templates The `templates` directory may contain any `jinja2` template files. You may -modify the existing templates or create your own. Your templates are merged with -the packaged defaults. To apply a certain template to a page, set the `template` -option in its [frontmatter](#frontmatter). The following public variables are -made available to the template engine: +modify the existing templates or create your own. To apply a certain +template to a page, set the `template` option in its +[frontmatter](#frontmatter). The following public variables are made +available to the template engine: | Name | Description | | ---------- | ------------------------------------------------------ | @@ -206,40 +201,43 @@ made available to the template engine: #### Markdown Footer -The `templates` directory can contain a file called `footer.md`. If it exists, -it's parsed and rendered into HTML, then made available to other templates as -the `footer` variable. If `footer.md` is missing but `footer.html` exists, then -it's used instead. **Note: links are _not_ resolved in the footer.** +The `templates` directory can contain a file called `footer.md`. If it +exists, it's parsed and rendered into HTML, then made available to other +templates as the `footer` variable. If `footer.md` is missing but +`footer.html` exists, then it's used instead. **Note: links are _not_ +resolved in the footer.** ### Internal Link Resolution -When zona encounters links in Markdown documents, it attempts to resolve them as -internal links. Links beginning with `/` are resolved relative to the content -root; otherwise, they are resolved relative to the Markdown file. If the link -resolves to an existing file that is part of the website, it's replaced with an -appropriate web-server-friendly link. Otherwise, the link isn't changed. +When zona encounters links in Markdown documents, it attempts to resolve +them as internal links. Links beginning with `/` are resolved relative to +the content root; otherwise, they are resolved relative to the Markdown +file. If the link resolves to an existing file that is part of the +website, it's replaced with an appropriate web-server-friendly link. +Otherwise, the link isn't changed. -For example, suppose the file `blog/post1.md` has a link `./post2.md`. The HTML -output will contain the link `/blog/post2` (which corresponds to -`/blog/post2/index.html`). Link resolution is applied to _all_ internal links, -including those pointing to static resources like images. Links are only -modified if they point to a real file that's not included in the ignore list. +For example, suppose the file `blog/post1.md` has a link `./post2.md`. The +HTML output will contain the link `/blog/post2` (which corresponds to +`/blog/post2/index.html`). Link resolution is applied to _all_ internal +links, including those pointing to static resources like images. Links are +only modified if they point to a real file that's not included in the +ignore list. ### Syntax Highlighting -Zona uses [Pygments] to provide syntax highlighting for fenced code blocks. The -following Pygments plugins are included: +Zona uses [Pygments] to provide syntax highlighting for fenced code +blocks. The following Pygments plugins are included: -- [pygments-kakoune](https://codeberg.com/ficd/pygments-kakoune) - - A lexer providing for highlighting Kakoune code. Available under the `kak` - and `kakrc` aliases. -- [pygments-ashen](https://codeberg.com/ficd/ashen/tree/main/item/pygments/README.md) - - An implementation of the [Ashen](https://codeberg.com/ficd/ashen) theme for - Pygments. +- [pygments-kakoune](https://git.sr.ht/~ficd/pygments-kakoune) + - A lexer providing for highlighting Kakoune code. Available under the + `kak` and `kakrc` aliases. +- [pygments-ashen](https://git.sr.ht/~ficd/ashen/tree/main/item/pygments/README.md) + - An implementation of the [Ashen](https://git.sr.ht/~ficd/ashen) theme + for Pygments. If you want to use any external Pygments styles or lexers, they must be -available in zona's Python environment. For example, you can give zona access to -[Catppucin](https://github.com/catppuccin/python): +available in zona's Python environment. For example, you can give zona +access to [Catppucin](https://github.com/catppuccin/python): ```yaml # config.yml @@ -254,9 +252,9 @@ Then, run zona with the following `uv` command: uvx --with catppucin zona build ``` -Inline syntax highlighting is also provided via a `python-markdown` extension. -If you prefix inline code with a shebang followed by the language identifier, it -will be highlighted. For example: +Inline syntax highlighting is also provided via a `python-markdown` +extension. If you prefix inline code with a shebang followed by the +language identifier, it will be highlighted. For example: ``` `#!python print(f"I love {foobar}!", end="")` @@ -282,9 +280,10 @@ will be rendered as ### Image Labels -A feature unique to zona is **image labels**. They make it easy to annotate -images in your Markdown documents. The alt text Markdown element is rendered as -the label — with support for inline Markdown. Consider this example: +A feature unique to zona is **image labels**. They make it easy to +annotate images in your Markdown documents. The alt text Markdown element +is rendered as the label — with support for inline Markdown. Consider this +example: ```markdown ![This **image** has _markup_.](static/markdown.png) @@ -299,14 +298,14 @@ The above results in the following HTML: ``` The `image-container` class is provided as a convenience for styling. The -default stylesheet centers the label under the image. Note: _links_ inside image -captions are not currently supported. I am looking into a solution. +default stylesheet centers the label under the image. Note: _links_ inside +image captions are not currently supported. I am looking into a solution. ### Frontmatter -YAML frontmatter can be used to configure the metadata of documents. All of them -are optional. `none` is used when the option is unset. The following options are -available: +YAML frontmatter can be used to configure the metadata of documents. All +of them are optional. `none` is used when the option is unset. The +following options are available: | Key | Type & Default | Description | | ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ | @@ -318,7 +317,6 @@ available: | `template` | `str \| none` = `none` | Template to use for this page. Relative to `templates/`, `.html` extension optional. | | `post` | `bool \| none` = `none` | Whether this page is a **post**. `true`/`false` is _absolute_. Leave it unset for automatic detection. | | `draft` | `bool` = `false` | Whether this page is a draft. See [drafts](#drafts) for more. | -| `ignore` | `bool` = `false` | Whether this page should be ignored in _both_ `final` and `draft` contexts. | | `math` | `bool` = `true` | Whether the LaTeX extension should be enabled for this page. | **Note**: you can specify the date in any format that can be parsed by @@ -340,9 +338,10 @@ template: post_list Welcome to my blog! Please find a list of my posts below. ``` -Setting `post: false` is necessary because, by default, all documents inside -`content/blog` are considered to be posts unless explicitly disabled in the -frontmatter. We don't want the post list to list _itself_ as a post. +Setting `post: false` is necessary because, by default, all documents +inside `content/blog` are considered to be posts unless explicitly +disabled in the frontmatter. We don't want the post list to list _itself_ +as a post. Then, you'd create `content/blog/my-post.md` and populate it: @@ -353,32 +352,32 @@ date: July 5, 2025 --- ``` -Because `my-post` is inside the `blog` directory, `post: true` is implied. If -you wanted to put it somewhere outside `blog`, you would need to set +Because `my-post` is inside the `blog` directory, `post: true` is implied. +If you wanted to put it somewhere outside `blog`, you would need to set `post: true` for it to be included in the post list. ## Configuration -Zona is configured in YAML format. The configuration file is called `config.yml` -and it **must** be located in the root of the project — in the same directory as -`content` and `templates`. +Zona is configured in YAML format. The configuration file is called +`config.yml` and it **must** be located in the root of the project — in +the same directory as `content` and `templates`. -Your configuration will be merged with the defaults. `zona init` also writes a -copy of the default configuration to the correct location. If it exists, you'll -be prompted before overwriting it. +Your configuration will be merged with the defaults. `zona init` also +writes a copy of the default configuration to the correct location. If it +exists, you'll be prompted before overwriting it. -**Note:** Currently, not every configuration value is actually used. Only the -useful settings are listed here. +**Note:** Currently, not every configuration value is actually used. Only +the useful settings are listed here. Please see the default configuration: ```yaml -base_url: / sitemap: Home: / ignore: - .marksman.toml markdown: + image_labels: true tab_length: 2 syntax_highlighting: enabled: true @@ -386,70 +385,49 @@ markdown: wrap: false links: external_new_tab: true -build: - clean_output_dir: true - include_drafts: false blog: dir: blog -server: - reload: - enabled: true - scroll_tolerance: 100 ``` -| Name | Description | -| -------------------------------------- | ----------------------------------------------------------------------------------------------- | -| `sitemap` | Sitemap dictionary. See [Sitemap](#sitemap). | -| `ignore` | List of paths to ignore. See [Ignore List](#ignore-list). | -| `markdown.tab_length` | How many spaces should be considered an indentation level. | -| `markdown.syntax_highlighting.enabled` | Whether code should be highlighted. | -| `markdown.syntax_highlighting.theme` | [Pygments] style for highlighting. | -| `markdown.syntax_highlighting.wrap` | Whether the resulting code block should be word wrapped. | -| `markdown.links.external_new_tab` | Whether external links should be opened in a new tab. | -| `build.clean_output_dir` | Whether previous build artifacts should be cleared when building. Recommended to leave this on. | -| `build.include_drafts` | Whether drafts should be included by default. | -| `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. | -| `server.reload.enabled` | Whether the preview server should use [live reload](#live-preview). | -| `server.reload.scroll_tolerance` | The distance, in pixels, from the bottom to still count as "scrolled to bottom". | +| Name | Description | +| -------------------------------------- | --------------------------------------------------------------------------------------------- | +| `sitemap` | Sitemap dictionary. See [Sitemap](#sitemap). | +| `ignore` | List of paths to ignore. See [Ignore List](#ignore-list). | +| `markdown.tab_length` | How many spaces should be considered an indentation level. | +| `markdown.syntax_highlighting.enabled` | Whether code should be highlighted. | +| `markdown.syntax_highlighting.theme` | [Pygments] style for highlighting. | +| `markdown.syntax_highlighting.wrap` | Whether the resulting code block should be word wrapped. | +| `markdown.links.external_new_tab` | Whether external links should be opened in a new tab. | +| `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. | ### Sitemap -You can define a sitemap in the configuration file. This is a list of links that -will be rendered at the top of every page. The `sitemap` is a dictionary of -`string` to `string` pairs, where each key is the displayed text of the link, -and the value if the `href`. Consider this example: +You can define a sitemap in the configuration file. This is a list of +links that will be rendered at the top of every page. The `sitemap` is a +dictionary of `string` to `string` pairs, where each key is the displayed +text of the link, and the value if the `href`. Consider this example: ```yaml sitemap: Home: / About: /about Blog: /blog - Git: https://git.ficd.sh/ficd + Git: https://git.sr.ht/~ficd ``` ### Ignore List -You can set a list of glob patterns in the [configuration](#configuration) that -should be ignored by zona. This is useful because zona makes a copy of _every_ -file it encounters inside the `content` directory, regardless of its type. The -paths must be relative to the `content` directory. +You can set a list of glob patterns in the [configuration](#configuration) +that should be ignored by zona. This is useful because zona makes a copy +of _every_ file it encounters inside the `content` directory, regardless +of its type. The paths must be relative to the `content` directory. ### Drafts -zona allows you to begin writing content without including it in the final build -output. If you set `draft: true` in a page's frontmatter, it will be marked as a -draft. Drafts are completely excluded from `zona build` and `zona serve` unless -the `--draft` flag is specified. +zona allows you to begin writing content without including it in the final +build output. If you set `draft: true` in a page's frontmatter, it will be +marked as a draft. Drafts are completely excluded from `zona build` and +`zona serve` unless the `--draft` flag is specified. -[Ashen]: https://codeberg.com/ficd/ashen +[Ashen]: https://sr.ht/~ficd/ashen [Pygments]: https://pygments.org/ - -## Known Problems - -1. If the user triggers rebuilds in quick succession, the browser is sent the - reload command after the first build, even though a second build may be - underway. This results in a `404` page being served, and the user needs to - manually refresh the browser page. - - **Mitigation:** Don't allow a rebuild until the browser has re-connected to - the WebSocket after the first reload. diff --git a/justfile b/justfile index c0f2e14..cfb1ed2 100644 --- a/justfile +++ b/justfile @@ -2,28 +2,6 @@ default: @just --list -clean: - #!/bin/sh - if [ ! -d "dist" ] && [ ! -d "__pycache__" ]; then - echo "Nothing to clean." - exit 0 - fi - if [ -d "dist" ]; then - echo "Removing dist/" - rm -r dist/ - fi - if [ -d "__pycache__" ]; then - echo "Removing __pycache__/" - rm -r "__pycache__" - fi - -publish: - #!/bin/sh - just clean - uv build - export UV_PUBLISH_TOKEN="$(pass show pypi)" - uv publish - format: uv run ruff format diff --git a/pyproject.toml b/pyproject.toml index ae54eac..fec9f67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,10 @@ [project] name = "zona" -version = "1.0.0" -description = "Opinionated static site generator." -license = "BSD-3-Clause " -license-files = ["LICENSE"] +version = "0.1.0" +description = "Static site generator" readme = "README.md" authors = [ - { name = "Daniel Fichtinger", email = "daniel@ficd.sh" }, + { name = "Daniel Fichtinger", email = "daniel@ficd.ca" }, ] requires-python = ">=3.12" dependencies = [ @@ -27,9 +25,6 @@ dependencies = [ "websockets>=15.0.1", ] -[project.urls] -Repository = "https://git.ficd.sh/ficd/zona" - [project.scripts] zona = "zona.cli:main" @@ -63,7 +58,7 @@ allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m", "feed [tool.ruff] line-length = 70 indent-width = 4 -target-version = "py312" +target-version = "py311" [tool.ruff.lint] fixable = ["ALL"] diff --git a/src/zona/builder.py b/src/zona/builder.py index 5c8af6b..de16bf5 100644 --- a/src/zona/builder.py +++ b/src/zona/builder.py @@ -30,7 +30,6 @@ class ZonaBuilder: self.config.build.include_drafts = True self.items: list[Item] = [] self.item_map: dict[Path, Item] = {} - self.fresh: bool = True def _discover(self): layout = self.layout @@ -49,14 +48,12 @@ class ZonaBuilder: destination=destination, url=str(destination.relative_to(layout.output)), ) - if path.name.endswith( - ".md" - ) and not path.is_relative_to( + if path.name.endswith(".md") and not path.is_relative_to( layout.root / "content" / "static" ): logger.debug(f"Parsing {path.name}.") item.metadata, item.content = parse_metadata(path) - if item.metadata.ignore or ( + if ( item.metadata.draft and not self.config.build.include_drafts ): @@ -72,13 +69,11 @@ class ZonaBuilder: item.copy = False name = destination.stem if name == "index": - item.destination = ( - item.destination.with_suffix(".html") + item.destination = item.destination.with_suffix( + ".html" ) else: - relative = path.relative_to(base).with_suffix( - "" - ) + relative = path.relative_to(base).with_suffix("") name = relative.stem item.destination = ( layout.output @@ -90,9 +85,7 @@ class ZonaBuilder: layout.output ) item.url = ( - "" - if rel_url == Path(".") - else rel_url.as_posix() + "" if rel_url == Path(".") else rel_url.as_posix() ) items.append(item) self.items = items @@ -106,13 +99,6 @@ class ZonaBuilder: else date.min, reverse=True, ) - posts = len(post_list) - for i, item in enumerate(post_list): - prev = post_list[i - 1] if i > 0 else None - next = post_list[i + 1] if i < posts - 2 else None - item.previous = prev - item.next = next - templater = Templater( config=self.config, template_dir=self.layout.templates, @@ -125,9 +111,7 @@ class ZonaBuilder: # write code highlighting stylesheet if self.config.markdown.syntax_highlighting.enabled: pygments_style = zmd.get_style_defs(self.config) - pygments_path = ( - self.layout.output / "static" / "pygments.css" - ) + pygments_path = self.layout.output / "static" / "pygments.css" util.ensure_parents(pygments_path) pygments_path.write_text(pygments_style) for item in self.item_map.values(): @@ -161,17 +145,8 @@ class ZonaBuilder: and self.layout.output.is_dir() ): logger.debug("Removing stale output...") - # only remove output dir's children - # to avoid breaking live preview - for child in self.layout.output.iterdir(): - if child.is_file() or child.is_symlink(): - child.unlink() - elif child.is_dir(): - shutil.rmtree(child) - if not self.fresh: - self.layout = self.layout.refresh() + shutil.rmtree(self.layout.output) logger.debug("Discovering...") self._discover() logger.debug("Building...") self._build() - self.fresh = False diff --git a/src/zona/cli.py b/src/zona/cli.py index 4b48d3a..1d32c4c 100644 --- a/src/zona/cli.py +++ b/src/zona/cli.py @@ -59,9 +59,7 @@ def build( """ if draft: print("Option override: including drafts.") - builder = ZonaBuilder( - cli_root=root, cli_output=output, draft=draft - ) + builder = ZonaBuilder(cli_root=root, cli_output=output, draft=draft) builder.build() @@ -75,9 +73,7 @@ def serve( ] = None, host: Annotated[ str, - typer.Option( - "--host", help="Hostname for live preview server." - ), + typer.Option("--host", help="Hostname for live preview server."), ] = "localhost", port: Annotated[ int, @@ -99,15 +95,14 @@ def serve( bool, typer.Option("--final", "-f", help="Don't include drafts."), ] = False, - live_reload: Annotated[ - bool | None, + no_live_reload: Annotated[ + bool, typer.Option( - "--live-reload/--no-live-reload", - "-l/-L", - help="Automatically reload web preview. Overrides config.", - show_default=False, + "--no-live-reload", + "-n", + help="Don't automatically reload web preview.", ), - ] = None, + ] = False, ): """ Build the website and start a live preview server. @@ -120,17 +115,13 @@ def serve( print("Preview without drafts.") else: print("Preview with drafts.") - if live_reload is None: - reload = None - else: - reload = live_reload server.serve( root=root, output=output, draft=not final, host=host, port=port, - user_reload=reload, + live_reload=not no_live_reload, ) diff --git a/src/zona/config.py b/src/zona/config.py index 752c7f3..8f4a0f2 100644 --- a/src/zona/config.py +++ b/src/zona/config.py @@ -58,17 +58,6 @@ class BuildConfig: include_drafts: bool = False -@dataclass -class ReloadConfig: - enabled: bool = True - scroll_tolerance: int = 100 - - -@dataclass -class ServerConfig: - reload: ReloadConfig = field(default_factory=ReloadConfig) - - IGNORELIST = [".marksman.toml"] @@ -80,15 +69,12 @@ class ZonaConfig: description: str = "My zona website." language: str = "en" # dictionary where key is name, value is url - sitemap: SitemapConfig = field( - default_factory=lambda: {"Home": "/"} - ) + sitemap: SitemapConfig = field(default_factory=lambda: {"Home": "/"}) # list of globs relative to content that should be ignored ignore: list[str] = field(default_factory=lambda: IGNORELIST) markdown: MarkdownConfig = field(default_factory=MarkdownConfig) build: BuildConfig = field(default_factory=BuildConfig) blog: BlogConfig = field(default_factory=BlogConfig) - server: ServerConfig = field(default_factory=ServerConfig) @classmethod def from_file(cls, path: Path) -> "ZonaConfig": diff --git a/src/zona/data/content/static/style.css b/src/zona/data/content/static/style.css index f02717e..641f9cf 100644 --- a/src/zona/data/content/static/style.css +++ b/src/zona/data/content/static/style.css @@ -1,53 +1,20 @@ :root { - --main-text-color: #b4b4b4; - --main-text-opaque-color: rgba(180, 180, 180, 0.8); - --main-bg-color: #121212; - --main-link-color: #df6464; - --main-heading-color: #df6464; - --main-bullet-color: #d87c4a; - --orange-rgb: rgba(216, 124, 74, 0.6); - --main-transparent: rgba(255, 255, 255, 0.15); - --main-small-text-color: rgba(255, 255, 255, 0.45); -} - -html { - scroll-behavior: smooth; + --main-text-color: #b4b4b4; + --main-bg-color: #121212; + --main-link-color: #df6464; + --main-heading-color: #df6464; + --main-bullet-color: #d87c4a; + --main-transparent: rgba(255, 255, 255, 0.15); + --main-small-text-color: rgba(255, 255, 255, 0.45); } body { - margin: 0; - line-height: 1.6; - font-size: 18px; - font-family: sans-serif; - background: var(--main-bg-color); - color: var(--main-text-color); - padding-left: calc(100vw - 100%); -} - -header { - padding-top: -1rem; - margin-top: -1rem; - font-family: monospace; - text-transform: lowercase; -} - -.post-nav { - font-family: monospace; -} - -.post-nav .symbol { - color: var(--main-bullet-color); -} - -.post-nav a { - margin: 0 2px; -} - -.site-logo { - color: inherit; - font-weight: bold; - text-decoration: none; - /* font-size: 1.75rem;*/ + line-height: 1.6; + font-size: 18px; + font-family: sans-serif; + background: var(--main-bg-color); + color: var(--main-text-color); + padding-left: calc(100vw - 100%); } .toclink { @@ -96,351 +63,278 @@ h3, h4, h5, h6 { - color: var(--main-heading-color); + color: var(--main-heading-color); } h1 { - margin-block-start: 0.67rem; - margin-block-end: 0.67rem; - font-size: 2rem; - font-weight: bold; -} - -.title { - text-transform: lowercase; - font-family: monospace; + margin-block-start: 0.67rem; + margin-block-end: 0.67rem; + font-size: 2rem; + font-weight: bold; } article h1:first-of-type { - margin-block-start: 1.67rem; + margin-block-start: 1.67rem; } h2 { - margin-block-start: 0.83rem; - margin-block-end: 0.83rem; - font-size: 1.5rem; - font-weight: bold; + margin-block-start: 0.83rem; + margin-block-end: 0.83rem; + font-size: 1.5rem; + font-weight: bold; } h3 { - margin-block-start: 1rem; - margin-block-end: 1rem; - font-size: 1.17em; - font-weight: bold; + margin-block-start: 1rem; + margin-block-end: 1rem; + font-size: 1.17em; + font-weight: bold; } h4 { - margin-block-start: 1.33rem; - margin-block-end: 1.33rem; - font-size: 1rem; - font-weight: bold; + margin-block-start: 1.33rem; + margin-block-end: 1.33rem; + font-size: 1rem; + font-weight: bold; } -article h1 + h4:first-of-type { - margin-block-start: 0rem; +article h1+h4:first-of-type { + margin-block-start: 0rem; } h5 { - margin-block-start: 1.67rem; - margin-block-end: 1.67rem; - font-size: 0.83rem; - font-weight: bold; + margin-block-start: 1.67rem; + margin-block-end: 1.67rem; + font-size: 0.83rem; + font-weight: bold; } h6 { - margin-block-start: 2.33rem; - margin-block-end: 2.33rem; - font-size: 0.67rem; - font-weight: bold; + margin-block-start: 2.33rem; + margin-block-end: 2.33rem; + font-size: 0.67rem; + font-weight: bold; } ul { - list-style-type: disc; - /* or any other list style */ + list-style-type: disc; + /* or any other list style */ } li::marker { - color: var(--main-bullet-color); - /* Change this to your desired color */ + color: var(--main-bullet-color); + /* Change this to your desired color */ } a { - color: var(--main-link-color); - text-decoration: none; - position: relative; + color: var(--main-link-color); } -a::after { - content: ""; - position: absolute; - left: 0; - bottom: -2px; - width: 100%; - height: 1px; - background-color: currentColor; - transform: scaleX(0); - transform-origin: center; - transition: transform 0.1s ease; -} - -a:hover::after { - transform: scaleX(1); -} -a:has(> code)::after { - display: none; +a:hover { + background: var(--main-transparent); } max-width: 100%; overflow: hidden; img { - display: block; - margin-left: auto; - margin-right: auto; - width: auto; - height: auto; + display: block; + margin-left: auto; + margin-right: auto; + width: auto; + height: auto; } blockquote { - color: var(--main-text-opaque-color); - border-left: 3px solid var(--orange-rgb); - padding: 0 1rem; - margin-left: 0; - margin-right: 0; + color: var(--main-small-text-color); + border-left: 3px solid var(--main-transparent); + padding: 0 1rem; + margin-left: 0; + margin-right: 0; } hr { - border: none; - height: 1px; - background: var(--main-small-text-color); - opacity: 0.5; -} - -time { - color: var(--main-bullet-color); - font-family: monospace; -} - -.post-list-date { - font-size: 0.95rem; + border: none; + height: 1px; + background: var(--main-small-text-color); } code { - background: var(--main-transparent); - border-radius: 0.1875rem; - /* padding: .0625rem .1875rem; */ - /* margin: 0 .1875rem; */ + background: var(--main-transparent); + border-radius: 0.1875rem; + /* padding: .0625rem .1875rem; */ + /* margin: 0 .1875rem; */ } code, pre { - white-space: pre; - word-wrap: break-word; - overflow-wrap: break-word; - font-family: - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace; - font-size: 0.95em; + white-space: pre; + word-wrap: break-word; + overflow-wrap: break-word; + font-family: monospace; + font-size: 0.95em; } pre { - background-color: #1d1d1d; - color: #d5d5d5; - padding: 1em; - border-radius: 5px; - line-height: 1.5; - overflow-x: auto; + background-color: #151515; + color: #d5d5d5; + padding: 1em; + border-radius: 5px; + line-height: 1.5; + overflow-x: auto; } /* Inline code styling */ :not(pre) > code { - padding: 0.2em 0.4em; - font-size: 0.85em; - line-height: 1; - background-color: #1d1d1d; - border-radius: 6px; - vertical-align: middle; - font-family: - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace; + padding: 0.2em 0.4em; + border-radius: 3px; + background-color: #1d1d1d; + color: #d5d5d5; + font-size: 0.85em; } /* Block code styling (inherits from pre) */ pre code { - padding: 0; - border-radius: 0; - background: none; + padding: 0; + border-radius: 0; + background: none; } small { - font-size: 0.95rem; - color: var(--main-small-text-color); + font-size: 0.95rem; + color: var(--main-small-text-color); } small a { - color: inherit; - /* Inherit the color of the surrounding text */ - text-decoration: underline; - /* Optional: Keep the underline to indicate a link */ + color: inherit; + /* Inherit the color of the surrounding text */ + text-decoration: underline; + /* Optional: Keep the underline to indicate a link */ } .title-container { - display: flex; - justify-content: center; - align-items: center; - text-align: center; + display: flex; + justify-content: center; + align-items: center; + text-align: center; } .title-container h1 { - margin: 0; + margin: 0; } .image-container { - text-align: center; - margin: 20px 0; - max-width: 100%; - overflow: hidden; - /* Optional: add some spacing around the image container */ + text-align: center; + margin: 20px 0; + max-width: 100%; + overflow: hidden; + /* Optional: add some spacing around the image container */ } .image-container img { - max-width: 100%; - width: auto; - max-height: 100%; - height: auto; + max-width: 100%; + width: auto; + max-height: 100%; + height: auto; } .fixed .image-container img { - max-width: 308px; - max-height: 308px; + max-width: 308px; + max-height: 308px; } .image-container small { - display: block; - /* Ensure the caption is on a new line */ - margin-top: 5px; - /* Optional: adjust spacing between image and caption */ + display: block; + /* Ensure the caption is on a new line */ + margin-top: 5px; + /* Optional: adjust spacing between image and caption */ } .image-container small a { - color: inherit; - /* Ensure the link color matches the small text */ - text-decoration: underline; - /* Optional: underline to indicate a link */ + color: inherit; + /* Ensure the link color matches the small text */ + text-decoration: underline; + /* Optional: underline to indicate a link */ } #header ul { - list-style-type: none; - padding-left: 0; + list-style-type: none; + padding-left: 0; } #header li { - display: inline; - font-size: 1.2rem; - margin-right: 1.2rem; + display: inline; + font-size: 1.2rem; + margin-right: 1.2rem; } #container { - margin: 2.5rem auto; - width: 90%; - max-width: 60ch; + margin: 2.5rem auto; + width: 90%; + max-width: 60ch; } #postlistdiv ul { - list-style-type: none; - padding-left: 0; + list-style-type: none; + padding-left: 0; } .moreposts { - font-size: 0.95rem; - padding-left: 0.5rem; + font-size: 0.95rem; + padding-left: 0.5rem; } #nextprev { - text-align: center; - margin-top: 1.4rem; - font-size: 0.95rem; + text-align: center; + margin-top: 1.4rem; + font-size: 0.95rem; } #footer { - color: var(--main-small-text-color); + color: var(--main-small-text-color); } table { - border-collapse: collapse; - margin: 1.5rem auto; - width: 100%; - max-width: 100%; - font-size: 0.85rem; - text-align: left; /* Use center if you prefer */ + border-collapse: collapse; + margin: 1.5rem auto; + width: 100%; + max-width: 100%; + font-size: 0.85rem; + text-align: left; /* Use center if you prefer */ } th, td { - border: 1px solid var(--main-transparent); - /*border: 1px solid var(--main-bullet-color);*/ - padding: 0.4rem 0.8rem; - vertical-align: middle; + border: 1px solid var(--main-transparent); + /*border: 1px solid var(--main-bullet-color);*/ + padding: 0.4rem 0.8rem; + vertical-align: middle; } thead th { - font-weight: bold; - background-color: rgba(255, 255, 255, 0.05); - color: var(--main-text-color); + font-weight: bold; + background-color: rgba(255, 255, 255, 0.05); + color: var(--main-text-color); } tbody tr:nth-child(even) { - background-color: rgba(255, 255, 255, 0.02); + background-color: rgba(255, 255, 255, 0.02); } tbody tr:hover { - background-color: rgba(255, 255, 255, 0.05); + background-color: rgba(255, 255, 255, 0.05); } table code { - font-family: - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace; - font-size: 0.85em; - background: #1d1d1d; - padding: 0.1em 0.25em; - border-radius: 3px; + font-family: monospace; + font-size: 0.85em; + background: #1d1d1d; + padding: 0.1em 0.25em; + border-radius: 3px; } caption { - margin-top: 0.5rem; - font-size: 0.8rem; - color: var(--main-small-text-color); + margin-top: 0.5rem; + font-size: 0.8rem; + color: var(--main-small-text-color); } -a > code { - text-decoration: none; - color: var(--main-link-color); - position: relative; -} -a:has(> code) { - text-decoration: none; - background: none; - /* position: static;*/ -} -a:hover > code { - text-decoration: underline; -} - -a:hover:has(> code) { - background: none; -} diff --git a/src/zona/data/server/inject.js b/src/zona/data/server/inject.js deleted file mode 100644 index bc00dde..0000000 --- a/src/zona/data/server/inject.js +++ /dev/null @@ -1,25 +0,0 @@ -(() => { - // if user at the bottom before reload, scroll to new bottom - if (localStorage.getItem("wasAtBottom") === "1") { - localStorage.removeItem("wasAtBottom"); - window.addEventListener("load", () => { - requestAnimationFrame(() => { - window.scrollTo(0, document.body.scrollHeight); - }); - }); - } - - const ws = new WebSocket("__SOCKET_ADDRESS__"); - const tol = __SCROLL_TOLERANCE__; - ws.onmessage = event => { - if (event.data === "reload") { - // store flag if user currently at bottom - const nearBottom = window.innerHeight + window.scrollY - >= document.body.scrollHeight - tol; - if (nearBottom) { - localStorage.setItem("wasAtBottom", "1"); - } - location.reload(); - } - }; -})(); diff --git a/src/zona/data/templates/basic.html b/src/zona/data/templates/basic.html index d86a25a..033d9e3 100644 --- a/src/zona/data/templates/basic.html +++ b/src/zona/data/templates/basic.html @@ -2,7 +2,7 @@ {% block content %} {% if metadata.show_title %} -{% include "title.html" %} +

{{ metadata.title }}

{% endif %} {{ content | safe }} {% endblock %} diff --git a/src/zona/data/templates/page.html b/src/zona/data/templates/page.html index 0c22f8b..3e835b4 100644 --- a/src/zona/data/templates/page.html +++ b/src/zona/data/templates/page.html @@ -1,12 +1,13 @@ -{% extends "base.html" %} {% block content %} {% if metadata.show_title %} {% -include "title.html" %} {% if metadata.date %} -
- -
-{% endif %} {% endif %} {% if is_post %} {% include "post_nav.html" %} {% endif -%} -
+{% extends "base.html" %} +{% block content %} +{% if metadata.show_title %} +

{{ metadata.title }}

+{% endif %} +{% if metadata.date %} +
+{% endif %}
{{ content | safe }}
{% endblock %} + + diff --git a/src/zona/data/templates/post_list.html b/src/zona/data/templates/post_list.html index 85e0be8..8545020 100644 --- a/src/zona/data/templates/post_list.html +++ b/src/zona/data/templates/post_list.html @@ -1,20 +1,17 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} -{% if metadata.show_title %} -{% include "title.html" %} -{% endif %} +{% block content %} + +

{{ metadata.title }}

{{ content | safe }}
{% if post_list %} - -{% endif %} {% endblock %} + +{% endif %} +{% endblock %} diff --git a/src/zona/data/templates/post_nav.html b/src/zona/data/templates/post_nav.html deleted file mode 100644 index 324f110..0000000 --- a/src/zona/data/templates/post_nav.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
- <{% if previous %}prev{% endif %}{% if previous and next %}|{% endif %}{% if next %}next{% endif - %}> -
-
diff --git a/src/zona/data/templates/title.html b/src/zona/data/templates/title.html deleted file mode 100644 index 9753365..0000000 --- a/src/zona/data/templates/title.html +++ /dev/null @@ -1,2 +0,0 @@ -

{{ metadata.title }}

- diff --git a/src/zona/layout.py b/src/zona/layout.py index 3d8de3d..3526c4e 100644 --- a/src/zona/layout.py +++ b/src/zona/layout.py @@ -16,22 +16,6 @@ class Layout: content: Path templates: Path output: Path - shared_templates: util.TempDir | None - _validate: bool - - def refresh(self): - logger.debug("Refreshing layout...") - if ( - self.shared_templates - and not self.shared_templates.removed - ): - logger.debug("Removing stale templates tempdir...") - self.shared_templates.remove() - return self.__class__.from_input( - root=self.root, - output=self.output, - validate=self._validate, - ) @classmethod def from_input( @@ -47,8 +31,6 @@ class Layout: output=(root / "public").resolve() if not output else output, - shared_templates=None, - _validate=validate, ) if validate: logger.debug("Validating site layout...") @@ -57,29 +39,10 @@ class Layout: raise FileNotFoundError( "Missing required content directory!" ) - internal_templates = util.get_resource_dir("templates") - user_templates = layout.templates - if not user_templates.is_dir() or util.is_empty( - user_templates - ): + if not layout.templates.is_dir(): logger.debug("Using default template directory.") # use the included defaults - layout.templates = internal_templates - else: - seen: set[str] = set() - temp = util.TempDir() - logger.debug( - f"Creating shared template directory at {temp}" - ) - for f in user_templates.iterdir(): - if f.is_file(): - util.copy_static_file(f, temp.path) - seen.add(f.name) - for f in internal_templates.iterdir(): - if f.is_file() and f.name not in seen: - util.copy_static_file(f, temp.path) - layout.shared_templates = temp - layout.templates = temp.path + layout.templates = util.get_resource_dir("templates") return layout diff --git a/src/zona/metadata.py b/src/zona/metadata.py index 98b14cf..b079266 100644 --- a/src/zona/metadata.py +++ b/src/zona/metadata.py @@ -18,15 +18,12 @@ class Metadata: date: date description: str | None show_title: bool = True - show_date: bool = True - show_nav: bool = True style: str | None = "/static/style.css" header: bool = True footer: bool = True template: str | None = None post: bool | None = None draft: bool = False - ignore: bool = False math: bool = True diff --git a/src/zona/models.py b/src/zona/models.py index d009476..2e7721c 100644 --- a/src/zona/models.py +++ b/src/zona/models.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from dataclasses import dataclass from enum import Enum from pathlib import Path @@ -23,8 +21,6 @@ class Item: type: ItemType | None = None copy: bool = True post: bool = False - next: Item | None = None - previous: Item | None = None # @dataclass diff --git a/src/zona/server.py b/src/zona/server.py index f8b0000..fa0c033 100644 --- a/src/zona/server.py +++ b/src/zona/server.py @@ -13,7 +13,6 @@ from rich import print from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer -from zona import util from zona.builder import ZonaBuilder from zona.log import get_logger from zona.websockets import WebSocketServer @@ -21,23 +20,18 @@ from zona.websockets import WebSocketServer logger = get_logger() -def make_reload_script( - host: str, port: int, scroll_tolerance: int -) -> str: +def make_reload_script(host: str, port: int) -> str: """Generates the JavaScript that must be injected into HTML pages for the live reloading to work.""" - js = util.get_resource("server/inject.js").contents - js = util.minify_js(js) - address = f"ws://{host}:{port}" - for placeholder, value in ( - ("__SOCKET_ADDRESS__", address), - ("__SCROLL_TOLERANCE__", scroll_tolerance), - ): - if placeholder not in js: - raise ValueError( - f"{placeholder} missing from reload script template!" - ) - js = js.replace(placeholder, str(value)) - return f"" + return f""" + +""" def make_handler_class(script: str): @@ -177,13 +171,12 @@ def serve( draft: bool = True, host: str = "localhost", port: int = 8000, - user_reload: bool | None = None, + live_reload: bool = True, ): """Serve preview website with live reload and automatic rebuild.""" # create temp dir, automatic cleanup with tempfile.TemporaryDirectory() as tmp: builder = ZonaBuilder(root, Path(tmp), draft) - config = builder.config # initial site build builder.build() # use discovered paths if none provided @@ -192,21 +185,13 @@ def serve( if root is None: root = builder.layout.root - # use config value unless overridden by user - reload = config.server.reload.enabled - if user_reload is not None: - reload = user_reload - if reload: - print("Live reloading is enabled.") - # spin up websocket server for live reloading + # spin up websocket server for live reloading + if live_reload: ws_port = port + 1 ws_server = WebSocketServer(host, ws_port) ws_server.start() # generate reload script for injection - scroll_tolerance = config.server.reload.scroll_tolerance - reload_script = make_reload_script( - host, ws_port, scroll_tolerance - ) + reload_script = make_reload_script(host, ws_port) # generate handler with reload script as attribute handler = make_handler_class(reload_script) else: @@ -237,13 +222,9 @@ def serve( observer.schedule( event_handler, path=str(root / "content"), recursive=True ) - templates = root / "templates" - if templates.is_dir(): - observer.schedule( - event_handler, - path=str(templates), - recursive=True, - ) + observer.schedule( + event_handler, path=str(root / "templates"), recursive=True + ) observer.start() # function to shut down gracefully diff --git a/src/zona/templates.py b/src/zona/templates.py index d5e0f22..872762c 100644 --- a/src/zona/templates.py +++ b/src/zona/templates.py @@ -3,7 +3,6 @@ from typing import Literal from jinja2 import Environment, FileSystemLoader, select_autoescape -from zona import util from zona.config import ZonaConfig from zona.markdown import md_to_html from zona.models import Item @@ -36,7 +35,6 @@ class Templater: template_dir: Path, post_list: list[Item], ): - # build temporary template dir self.env: Environment = Environment( loader=FileSystemLoader(template_dir), autoescape=select_autoescape(["html", "xml"]), @@ -78,12 +76,5 @@ class Templater: metadata=meta, header=header, footer=footer, - is_post=item.post, - next=util.normalize_url(item.next.url) - if item.next - else None, - previous=util.normalize_url(item.previous.url) - if item.previous - else None, post_list=self.post_list, ) diff --git a/src/zona/util.py b/src/zona/util.py index 5bf6f53..7e80500 100644 --- a/src/zona/util.py +++ b/src/zona/util.py @@ -1,36 +1,10 @@ import fnmatch -import re -import shutil import string -import tempfile -import weakref from importlib import resources from importlib.resources.abc import Traversable from pathlib import Path from shutil import copy2 -from typing import Any, NamedTuple, override - - -class TempDir: - """Temporary directory that cleans up when it's garbage collected.""" - - def __init__(self): - self._tempdir: str = tempfile.mkdtemp() - self.path: Path = Path(self._tempdir) - self._finalizer: weakref.finalize[Any, Any] = ( - weakref.finalize(self, shutil.rmtree, self._tempdir) - ) - - def remove(self): - self._finalizer() - - @property - def removed(self): - return not self._finalizer.alive - - @override - def __repr__(self) -> str: - return f"" +from typing import NamedTuple class ZonaResource(NamedTuple): @@ -38,17 +12,6 @@ class ZonaResource(NamedTuple): contents: str -def get_resource(path: str) -> ZonaResource: - """Load the packaged resource in data/path""" - file = resources.files("zona").joinpath("data", path) - if file.is_file(): - return ZonaResource(name=path, contents=file.read_text()) - else: - raise FileNotFoundError( - f"{path} is not a valid Zona resource!" - ) - - def get_resources(subdir: str) -> list[ZonaResource]: """Load the packaged resources in data/subdir""" out: list[ZonaResource] = [] @@ -90,15 +53,6 @@ def copy_static_file(src: Path, dst: Path): copy2(src, dst) -def is_empty(path: Path) -> bool: - """If given a file, check if it has any non-whitespace content. - If given a directory, check if it has any children.""" - if path.is_file(): - return path.read_text().strip() == "" - else: - return not any(path.iterdir()) - - def filename_to_title(path: Path) -> str: name = path.stem words = name.replace("-", " ").replace("_", " ") @@ -119,23 +73,3 @@ def should_ignore( fnmatch.fnmatch(str(rel_path), pattern) for pattern in patterns ) - - -MINIFY_JS_PATTERN = re.compile( - r""" - //.*?$ | - /\*.*?\*/ | - \s+ - """, - re.MULTILINE | re.DOTALL | re.VERBOSE, -) - - -def minify_js(js: str) -> str: - """Naively minifies JavaScript by stripping comments and whitespace.""" - return MINIFY_JS_PATTERN.sub( - # replace whitespace with single space, - # strip comments - lambda m: " " if m.group(0).isspace() else "", - js, - ).strip() diff --git a/uv.lock b/uv.lock index 6835dac..0699f3a 100644 --- a/uv.lock +++ b/uv.lock @@ -509,7 +509,7 @@ wheels = [ [[package]] name = "zona" -version = "1.0.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "dacite" },