diff --git a/README.md b/README.md
index 9ae4ef4..df3c866 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,22 @@
zona
-[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.
+[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.
-**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.
+**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
+[zona-go](https://git.ficd.sh/ficd/zona-go) repository.
For an example of a website built with zona, please see
[ficd.sh](https://ficd.sh).
@@ -31,6 +29,7 @@ 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)
@@ -60,15 +59,15 @@ 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
@@ -77,7 +76,7 @@ zona is not yet packaged on PyPI. You may use `uv` to install it from this
repository:
```sh
-uv tool install 'git+https://git.sr.ht/~ficd/zona'
+uv tool install 'git+https://git.ficd.sh/ficd/zona'
```
## Usage
@@ -87,71 +86,78 @@ 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._
-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.
+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.
-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_).
+**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.
-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.
+#### Live Reload
-**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.
+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).
#### 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
@@ -164,32 +170,30 @@ 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 prioritized 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. 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 |
| ---------- | ------------------------------------------------------ |
@@ -201,43 +205,40 @@ 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://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.
+- [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.
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
@@ -252,9 +253,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="")`
@@ -280,10 +281,9 @@ 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

@@ -298,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 |
| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ |
@@ -338,10 +338,9 @@ 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:
@@ -352,32 +351,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
@@ -385,49 +384,60 @@ 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. |
-| `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. |
+| 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". |
### 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.sr.ht/~ficd
+ Git: https://git.ficd.sh/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://sr.ht/~ficd/ashen
+[Ashen]: https://codeberg.com/ficd/ashen
[Pygments]: https://pygments.org/
diff --git a/src/zona/cli.py b/src/zona/cli.py
index 1d32c4c..a7e2dd5 100644
--- a/src/zona/cli.py
+++ b/src/zona/cli.py
@@ -95,14 +95,15 @@ def serve(
bool,
typer.Option("--final", "-f", help="Don't include drafts."),
] = False,
- no_live_reload: Annotated[
- bool,
+ live_reload: Annotated[
+ bool | None,
typer.Option(
- "--no-live-reload",
- "-n",
- help="Don't automatically reload web preview.",
+ "--live-reload/--no-live-reload",
+ "-l/-L",
+ help="Automatically reload web preview. Overrides config.",
+ show_default=False,
),
- ] = False,
+ ] = None,
):
"""
Build the website and start a live preview server.
@@ -115,13 +116,17 @@ 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,
- live_reload=not no_live_reload,
+ user_reload=reload,
)
diff --git a/src/zona/config.py b/src/zona/config.py
index 4bd7a42..cc56ec2 100644
--- a/src/zona/config.py
+++ b/src/zona/config.py
@@ -58,6 +58,17 @@ 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"]
@@ -71,6 +82,7 @@ class ZonaConfig:
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/server/inject.js b/src/zona/data/server/inject.js
new file mode 100644
index 0000000..bc00dde
--- /dev/null
+++ b/src/zona/data/server/inject.js
@@ -0,0 +1,25 @@
+(() => {
+ // 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/server.py b/src/zona/server.py
index fa0c033..23c4c0b 100644
--- a/src/zona/server.py
+++ b/src/zona/server.py
@@ -13,6 +13,7 @@ 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
@@ -20,18 +21,23 @@ from zona.websockets import WebSocketServer
logger = get_logger()
-def make_reload_script(host: str, port: int) -> str:
+def make_reload_script(
+ host: str, port: int, scroll_tolerance: int
+) -> str:
"""Generates the JavaScript that must be injected into HTML pages for the live reloading to work."""
- return f"""
-
-"""
+ 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""
def make_handler_class(script: str):
@@ -171,12 +177,13 @@ def serve(
draft: bool = True,
host: str = "localhost",
port: int = 8000,
- live_reload: bool = True,
+ user_reload: bool | None = None,
):
"""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
@@ -185,13 +192,21 @@ def serve(
if root is None:
root = builder.layout.root
- # spin up websocket server for live reloading
- if live_reload:
+ # 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
ws_port = port + 1
ws_server = WebSocketServer(host, ws_port)
ws_server.start()
# generate reload script for injection
- reload_script = make_reload_script(host, ws_port)
+ scroll_tolerance = config.server.reload.scroll_tolerance
+ reload_script = make_reload_script(
+ host, ws_port, scroll_tolerance
+ )
# generate handler with reload script as attribute
handler = make_handler_class(reload_script)
else:
diff --git a/src/zona/util.py b/src/zona/util.py
index 7e80500..2d5c514 100644
--- a/src/zona/util.py
+++ b/src/zona/util.py
@@ -1,4 +1,5 @@
import fnmatch
+import re
import string
from importlib import resources
from importlib.resources.abc import Traversable
@@ -12,6 +13,15 @@ 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] = []
@@ -65,11 +75,28 @@ def normalize_url(url: str) -> str:
return url
-def should_ignore(
- path: Path, patterns: list[str], base: Path
-) -> bool:
+def should_ignore(path: Path, patterns: list[str], base: Path) -> bool:
rel_path = path.relative_to(base)
return any(
- fnmatch.fnmatch(str(rel_path), pattern)
- for pattern in patterns
+ 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()