Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
22 changed files with 123 additions and 738 deletions
|
@ -1,20 +0,0 @@
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: based-alpine
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: setup cache
|
|
||||||
id: uv-cache
|
|
||||||
uses: https://git.ficd.sh/ficd/uv-cache@v1
|
|
||||||
- name: build
|
|
||||||
run: |
|
|
||||||
uv sync
|
|
||||||
uv build
|
|
||||||
- name: publish
|
|
||||||
run: |
|
|
||||||
uv publish --token ${{ secrets.PYPI_TOKEN }}
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
# this workflow checks if the project can be built successfully.
|
|
||||||
# it also serves to test whether based-alpine and uv-cache are working properly.
|
|
||||||
# Unit tests will be added here eventually
|
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
test-build:
|
|
||||||
runs-on: based-alpine
|
|
||||||
steps:
|
|
||||||
- name: checkout source
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: setup cache
|
|
||||||
id: uv-cache
|
|
||||||
uses: https://git.ficd.sh/ficd/uv-cache@v1
|
|
||||||
- name: sync and build
|
|
||||||
run: |
|
|
||||||
uv sync
|
|
||||||
uv build
|
|
||||||
uv run zona --version
|
|
25
.kakrc
25
.kakrc
|
@ -1,25 +0,0 @@
|
||||||
# commands to edit important files in the root
|
|
||||||
declare-option str project_root %sh{ git rev-parse --show-toplevel }
|
|
||||||
|
|
||||||
define-command -params 1 root-edit %{
|
|
||||||
edit %exp{%opt{project_root}/%arg{1}}
|
|
||||||
}
|
|
||||||
|
|
||||||
define-command just %{
|
|
||||||
root-edit justfile
|
|
||||||
}
|
|
||||||
define-command pyproject %{
|
|
||||||
root-edit pyproject.toml
|
|
||||||
}
|
|
||||||
define-command readme %{
|
|
||||||
root-edit README.md
|
|
||||||
}
|
|
||||||
|
|
||||||
define-command kakrc %{
|
|
||||||
root-edit .kakrc
|
|
||||||
}
|
|
||||||
|
|
||||||
# change working directory to the package
|
|
||||||
hook global -once BufCreate .* %{
|
|
||||||
change-directory %exp{%opt{project_root}/src/zona}
|
|
||||||
}
|
|
33
CHANGELOG.md
33
CHANGELOG.md
|
@ -1,33 +0,0 @@
|
||||||
# 1.3.0
|
|
||||||
|
|
||||||
- Added RSS feed generation.
|
|
||||||
- Added default post description to configuration.
|
|
||||||
- Added time-of-day support to post `date` frontmatter parsing.
|
|
||||||
- `zona init` now only writes `footer.md` to the templates directory.
|
|
||||||
|
|
||||||
# 1.2.1
|
|
||||||
|
|
||||||
- Added `--version` flag to CLI.
|
|
||||||
|
|
||||||
# 1.2.0
|
|
||||||
|
|
||||||
- Improved the appearance and semantics of post navigation buttons.
|
|
||||||
- Navigation now follows "newer/older" logic.
|
|
||||||
- Added hover symbols to page titles.
|
|
||||||
- Improved the styling of hover symbols and links.
|
|
||||||
|
|
||||||
# 1.1.0
|
|
||||||
|
|
||||||
- Major improvements to default stylesheet.
|
|
||||||
- Frontmatter option to ignore file.
|
|
||||||
- Improvements to title and date rendering in templates.
|
|
||||||
- Added smooth scrolling to default stylesheet.
|
|
||||||
- Fixed a crash when user templates directory was missing when starting the
|
|
||||||
server.
|
|
||||||
- Added "next/previous" navigation buttons to posts.
|
|
||||||
- User template directory is now merged with defaults instead of it being one or
|
|
||||||
the other.
|
|
||||||
|
|
||||||
# 1.0.0
|
|
||||||
|
|
||||||
Initial release!
|
|
110
README.md
110
README.md
|
@ -28,7 +28,6 @@ For an example of a website built with zona, please see
|
||||||
- [Site Layout](#site-layout)
|
- [Site Layout](#site-layout)
|
||||||
- [Templates](#templates)
|
- [Templates](#templates)
|
||||||
- [Markdown Footer](#markdown-footer)
|
- [Markdown Footer](#markdown-footer)
|
||||||
- [RSS Feed Generation](#rss-feed-generation)
|
|
||||||
- [Internal Link Resolution](#internal-link-resolution)
|
- [Internal Link Resolution](#internal-link-resolution)
|
||||||
- [Syntax Highlighting](#syntax-highlighting)
|
- [Syntax Highlighting](#syntax-highlighting)
|
||||||
- [Markdown Extensions](#markdown-extensions)
|
- [Markdown Extensions](#markdown-extensions)
|
||||||
|
@ -50,7 +49,6 @@ For an example of a website built with zona, please see
|
||||||
- Live refresh in browser preview.
|
- Live refresh in browser preview.
|
||||||
- `jinja2` template support with sensible defaults included.
|
- `jinja2` template support with sensible defaults included.
|
||||||
- Basic page, blog post, post list.
|
- Basic page, blog post, post list.
|
||||||
- RSS feed generation.
|
|
||||||
- Glob ignore.
|
- Glob ignore.
|
||||||
- YAML frontmatter.
|
- YAML frontmatter.
|
||||||
- Easily configurable sitemap header.
|
- Easily configurable sitemap header.
|
||||||
|
@ -103,6 +101,10 @@ 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
|
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._
|
||||||
|
|
||||||
### Live Preview
|
### Live Preview
|
||||||
|
|
||||||
To make the writing process as frictionless as possible, zona ships with a live
|
To make the writing process as frictionless as possible, zona ships with a live
|
||||||
|
@ -119,10 +121,9 @@ By default, the build outputs to a temporary directory. Use `-o/--output` to
|
||||||
override this.
|
override this.
|
||||||
|
|
||||||
**Note**: if the live preview isn't working as expected, try restarting the
|
**Note**: if the live preview isn't working as expected, try restarting the
|
||||||
server. If you change the configuration, the server must also be restarted. The
|
server. If you change the configuration or any templates, the server must also
|
||||||
live preview uses the same function as `zona build` internally; this means that
|
be restarted. The live preview uses the same function as `zona build`
|
||||||
the output is also written to disk --- a temporary directory by default, unless
|
internally; this means that the output is also written to disk.
|
||||||
overridden with `-o/--output`.
|
|
||||||
|
|
||||||
#### Live Reload
|
#### Live Reload
|
||||||
|
|
||||||
|
@ -171,8 +172,8 @@ public/
|
||||||
|
|
||||||
The **root** of the zona **project** _must_ contain the configuration file,
|
The **root** of the zona **project** _must_ contain the configuration file,
|
||||||
`config.yml`, and a directory called `content`. A directory called `templates`
|
`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
|
is optional, and prioritized if it exists. `public` is the built site output —
|
||||||
site output — it's recommended to add this path to your `.gitignore`.
|
it's recommended to add this path to your `.gitignore`.
|
||||||
|
|
||||||
The `content` directory is the **root of the website**. Think of it as the
|
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 root**. For example, suppose your website is hosted at `example.com`.
|
||||||
|
@ -190,22 +191,17 @@ site using the `post_list` template.
|
||||||
### Templates
|
### Templates
|
||||||
|
|
||||||
The `templates` directory may contain any `jinja2` template files. You may
|
The `templates` directory may contain any `jinja2` template files. You may
|
||||||
modify the existing templates or create your own. Your templates are merged with
|
modify the existing templates or create your own. To apply a certain template to
|
||||||
the packaged defaults. To apply a certain template to a page, set the `template`
|
a page, set the `template` option in its [frontmatter](#frontmatter). The
|
||||||
option in its [frontmatter](#frontmatter). The following public variables are
|
following public variables are made available to the template engine:
|
||||||
made available to the template engine:
|
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ----------- | -------------------------------------------------------- |
|
| ---------- | ------------------------------------------------------ |
|
||||||
| `content` | The content of this page. |
|
| `content` | The content of this page. |
|
||||||
| `url` | The resolved URL of this page. |
|
| `url` | The resolved URL of this page. |
|
||||||
| `metadata` | The frontmatter of this page (_merged with defaults_). |
|
| `metadata` | The frontmatter of this page (_merged with defaults_). |
|
||||||
| `header` | The sitemap header in HTML form. Can be `False`. |
|
| `header` | The sitemap header in HTML form. Can be `False`. |
|
||||||
| `footer` | The footer in HTML form. Can be `False`. |
|
| `footer` | The footer in HTML form. Can be `False`. |
|
||||||
| `is_post` | Whether this page is a post. |
|
|
||||||
| `newer` | URL of the newer post in the post list. |
|
|
||||||
| `older` | URL of the older post in the post list. |
|
|
||||||
| `post_list` | A sorted list of `Item` objects. Meant for internal use. |
|
|
||||||
|
|
||||||
#### Markdown Footer
|
#### Markdown Footer
|
||||||
|
|
||||||
|
@ -214,17 +210,6 @@ 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
|
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.**
|
it's used instead. **Note: links are _not_ resolved in the footer.**
|
||||||
|
|
||||||
### RSS Feed Generation
|
|
||||||
|
|
||||||
Zona can also generates an RSS feed containing your blog posts. This feature is
|
|
||||||
disabled by default, and you can enable it in the
|
|
||||||
[configuration](#configuration).
|
|
||||||
|
|
||||||
The default location is a file called `rss.xml` in the content root. All RSS
|
|
||||||
related configuration is specified in `config.yml`. If you plan to use the feed,
|
|
||||||
make sure to replace the placeholder `link`, `title`, `description`, and
|
|
||||||
`author` configuration values before you set `enabled: true`.
|
|
||||||
|
|
||||||
### Internal Link Resolution
|
### Internal Link Resolution
|
||||||
|
|
||||||
When zona encounters links in Markdown documents, it attempts to resolve them as
|
When zona encounters links in Markdown documents, it attempts to resolve them as
|
||||||
|
@ -244,11 +229,11 @@ modified if they point to a real file that's not included in the ignore list.
|
||||||
Zona uses [Pygments] to provide syntax highlighting for fenced code blocks. The
|
Zona uses [Pygments] to provide syntax highlighting for fenced code blocks. The
|
||||||
following Pygments plugins are included:
|
following Pygments plugins are included:
|
||||||
|
|
||||||
- [pygments-kakoune](https://codeberg.org/ficd/pygments-kakoune)
|
- [pygments-kakoune](https://codeberg.com/ficd/pygments-kakoune)
|
||||||
- A lexer providing for highlighting Kakoune code. Available under the `kak`
|
- A lexer providing for highlighting Kakoune code. Available under the `kak`
|
||||||
and `kakrc` aliases.
|
and `kakrc` aliases.
|
||||||
- [pygments-ashen](https://codeberg.org/ficd/ashen/tree/main/item/pygments/README.md)
|
- [pygments-ashen](https://codeberg.com/ficd/ashen/tree/main/item/pygments/README.md)
|
||||||
- An implementation of the [Ashen](https://codeberg.org/ficd/ashen) theme for
|
- An implementation of the [Ashen](https://codeberg.com/ficd/ashen) theme for
|
||||||
Pygments.
|
Pygments.
|
||||||
|
|
||||||
If you want to use any external Pygments styles or lexers, they must be
|
If you want to use any external Pygments styles or lexers, they must be
|
||||||
|
@ -322,19 +307,17 @@ 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
|
are optional. `none` is used when the option is unset. The following options are
|
||||||
available:
|
available:
|
||||||
|
|
||||||
| Key | Type & Default | Description |
|
| Key | Type & Default | Description |
|
||||||
| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||||
| `title` | `str` = title-cased filename. | Title of the page. |
|
| `title` | `str` = title-cased filename. | Title of the page. |
|
||||||
| `description` | `str \| none` = `none` | Description. If omitted, default from [config](#configuration) will be used. |
|
| `date` | Date string = file modified time. | Displayed on blog posts and used for post_list sorting. |
|
||||||
| `date` | Date string = file modified time. | Displayed on blog posts and used for post_list sorting. |
|
| `show_title` | `bool` = `true` | Whether `metadata.title` should be included in the template. |
|
||||||
| `show_title` | `bool` = `true` | Whether `metadata.title` should be included in the template. |
|
| `header` | `bool` = `true` | Whether the header sitemap should be rendered. |
|
||||||
| `header` | `bool` = `true` | Whether the header sitemap should be rendered. |
|
| `footer` | `bool` = `true` | Whether the footer should be rendered. |
|
||||||
| `footer` | `bool` = `true` | Whether the footer should be rendered. |
|
| `template` | `str \| none` = `none` | Template to use for this page. Relative to `templates/`, `.html` extension optional. |
|
||||||
| `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. |
|
||||||
| `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. |
|
||||||
| `draft` | `bool` = `false` | Whether this page is a draft. See [drafts](#drafts) for more. |
|
| `math` | `bool` = `true` | Whether the LaTeX extension should be enabled for this page. |
|
||||||
| `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
|
**Note**: you can specify the date in any format that can be parsed by
|
||||||
[`python-dateutil`](https://pypi.org/project/python-dateutil/).
|
[`python-dateutil`](https://pypi.org/project/python-dateutil/).
|
||||||
|
@ -388,23 +371,12 @@ useful settings are listed here.
|
||||||
Please see the default configuration:
|
Please see the default configuration:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
feed:
|
base_url: /
|
||||||
enabled: true
|
|
||||||
timezone: UTC
|
|
||||||
path: rss.xml
|
|
||||||
link: https://example.com
|
|
||||||
title: Zona Website
|
|
||||||
description: My zona website.
|
|
||||||
language: en
|
|
||||||
author:
|
|
||||||
name: John Doe
|
|
||||||
email: john@doe.net
|
|
||||||
sitemap:
|
sitemap:
|
||||||
Home: /
|
Home: /
|
||||||
ignore:
|
ignore:
|
||||||
- .marksman.toml
|
- .marksman.toml
|
||||||
markdown:
|
markdown:
|
||||||
image_labels: true
|
|
||||||
tab_length: 2
|
tab_length: 2
|
||||||
syntax_highlighting:
|
syntax_highlighting:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -417,8 +389,6 @@ build:
|
||||||
include_drafts: false
|
include_drafts: false
|
||||||
blog:
|
blog:
|
||||||
dir: blog
|
dir: blog
|
||||||
defaults:
|
|
||||||
description: A blog post
|
|
||||||
server:
|
server:
|
||||||
reload:
|
reload:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -427,15 +397,6 @@ server:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||||
| `feed.enabled` | Whether RSS feed should be generated. **Off by default**. |
|
|
||||||
| `feed.timezime` | Timezone to use for post `pubDate` values. Must be an IANA compliant string. |
|
|
||||||
| `feed.path` | Location of the feed, relative to content root. |
|
|
||||||
| `feed.link` | The base URL of the website. |
|
|
||||||
| `feed.title` | Website title. |
|
|
||||||
| `feed.description` | Website description. |
|
|
||||||
| `feed.language` | String specifying website's language code. |
|
|
||||||
| `author.name` | Your full name. |
|
|
||||||
| `author.email` | Your email address. |
|
|
||||||
| `sitemap` | Sitemap dictionary. See [Sitemap](#sitemap). |
|
| `sitemap` | Sitemap dictionary. See [Sitemap](#sitemap). |
|
||||||
| `ignore` | List of paths to ignore. See [Ignore List](#ignore-list). |
|
| `ignore` | List of paths to ignore. See [Ignore List](#ignore-list). |
|
||||||
| `markdown.tab_length` | How many spaces should be considered an indentation level. |
|
| `markdown.tab_length` | How many spaces should be considered an indentation level. |
|
||||||
|
@ -446,7 +407,6 @@ server:
|
||||||
| `build.clean_output_dir` | Whether previous build artifacts should be cleared when building. Recommended to leave this on. |
|
| `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. |
|
| `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. |
|
| `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. |
|
||||||
| `blog.defaults.description` | Default description for blog posts with no `description` in their frontmatter. |
|
|
||||||
| `server.reload.enabled` | Whether the preview server should use [live reload](#live-preview). |
|
| `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". |
|
| `server.reload.scroll_tolerance` | The distance, in pixels, from the bottom to still count as "scrolled to bottom". |
|
||||||
|
|
||||||
|
@ -479,7 +439,7 @@ 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
|
draft. Drafts are completely excluded from `zona build` and `zona serve` unless
|
||||||
the `--draft` flag is specified.
|
the `--draft` flag is specified.
|
||||||
|
|
||||||
[Ashen]: https://codeberg.org/ficd/ashen
|
[Ashen]: https://codeberg.com/ficd/ashen
|
||||||
[Pygments]: https://pygments.org/
|
[Pygments]: https://pygments.org/
|
||||||
|
|
||||||
## Known Problems
|
## Known Problems
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "zona"
|
name = "zona"
|
||||||
version = "1.2.2"
|
version = "1.0.0"
|
||||||
description = "Opinionated static site generator."
|
description = "Opinionated static site generator."
|
||||||
license = "BSD-3-Clause "
|
license = "BSD-3-Clause "
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
|
@ -11,7 +11,6 @@ authors = [
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dacite>=1.9.2",
|
"dacite>=1.9.2",
|
||||||
"feedgen>=1.0.0",
|
|
||||||
"jinja2>=3.1.6",
|
"jinja2>=3.1.6",
|
||||||
"l2m4m>=1.0.4",
|
"l2m4m>=1.0.4",
|
||||||
"markdown>=3.8.2",
|
"markdown>=3.8.2",
|
||||||
|
@ -58,12 +57,12 @@ reportUnusedCallResult = false
|
||||||
reportCallInDefaultInitializer = false
|
reportCallInDefaultInitializer = false
|
||||||
enableTypeIgnoreComments = true
|
enableTypeIgnoreComments = true
|
||||||
reportIgnoreCommentWithoutRule = false
|
reportIgnoreCommentWithoutRule = false
|
||||||
allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m", "feedgen", "feedgen.feed"]
|
allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 70
|
line-length = 70
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
target-version = "py312"
|
target-version = "py311"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
fixable = ["ALL"]
|
fixable = ["ALL"]
|
||||||
|
|
|
@ -2,8 +2,6 @@ import shutil
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from feedgen.feed import FeedGenerator
|
|
||||||
|
|
||||||
from zona import markdown as zmd
|
from zona import markdown as zmd
|
||||||
from zona import util
|
from zona import util
|
||||||
from zona.config import ZonaConfig
|
from zona.config import ZonaConfig
|
||||||
|
@ -32,7 +30,6 @@ class ZonaBuilder:
|
||||||
self.config.build.include_drafts = True
|
self.config.build.include_drafts = True
|
||||||
self.items: list[Item] = []
|
self.items: list[Item] = []
|
||||||
self.item_map: dict[Path, Item] = {}
|
self.item_map: dict[Path, Item] = {}
|
||||||
self.fresh: bool = True
|
|
||||||
|
|
||||||
def _discover(self):
|
def _discover(self):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
@ -51,16 +48,12 @@ class ZonaBuilder:
|
||||||
destination=destination,
|
destination=destination,
|
||||||
url=str(destination.relative_to(layout.output)),
|
url=str(destination.relative_to(layout.output)),
|
||||||
)
|
)
|
||||||
if path.name.endswith(
|
if path.name.endswith(".md") and not path.is_relative_to(
|
||||||
".md"
|
|
||||||
) and not path.is_relative_to(
|
|
||||||
layout.root / "content" / "static"
|
layout.root / "content" / "static"
|
||||||
):
|
):
|
||||||
logger.debug(f"Parsing {path.name}.")
|
logger.debug(f"Parsing {path.name}.")
|
||||||
item.metadata, item.content = parse_metadata(
|
item.metadata, item.content = parse_metadata(path)
|
||||||
path, config=self.config
|
if (
|
||||||
)
|
|
||||||
if item.metadata.ignore or (
|
|
||||||
item.metadata.draft
|
item.metadata.draft
|
||||||
and not self.config.build.include_drafts
|
and not self.config.build.include_drafts
|
||||||
):
|
):
|
||||||
|
@ -76,13 +69,11 @@ class ZonaBuilder:
|
||||||
item.copy = False
|
item.copy = False
|
||||||
name = destination.stem
|
name = destination.stem
|
||||||
if name == "index":
|
if name == "index":
|
||||||
item.destination = (
|
item.destination = item.destination.with_suffix(
|
||||||
item.destination.with_suffix(".html")
|
".html"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
relative = path.relative_to(base).with_suffix(
|
relative = path.relative_to(base).with_suffix("")
|
||||||
""
|
|
||||||
)
|
|
||||||
name = relative.stem
|
name = relative.stem
|
||||||
item.destination = (
|
item.destination = (
|
||||||
layout.output
|
layout.output
|
||||||
|
@ -94,55 +85,13 @@ class ZonaBuilder:
|
||||||
layout.output
|
layout.output
|
||||||
)
|
)
|
||||||
item.url = (
|
item.url = (
|
||||||
""
|
"" if rel_url == Path(".") else rel_url.as_posix()
|
||||||
if rel_url == Path(".")
|
|
||||||
else rel_url.as_posix()
|
|
||||||
)
|
)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
self.items = items
|
self.items = items
|
||||||
|
|
||||||
def generate_feed(self) -> bytes:
|
def _build(self):
|
||||||
post_list = self._get_post_list()
|
|
||||||
config = self.config.feed
|
|
||||||
if config.link.endswith("/"):
|
|
||||||
config.link = config.link[:-2]
|
|
||||||
fg = FeedGenerator()
|
|
||||||
fg.id(config.link)
|
|
||||||
fg.title(config.title)
|
|
||||||
author = {
|
|
||||||
"name": config.author.name,
|
|
||||||
"email": config.author.email,
|
|
||||||
}
|
|
||||||
fg.author(author)
|
|
||||||
fg.link(
|
|
||||||
href=f"{config.link}/{config.path}",
|
|
||||||
rel="self",
|
|
||||||
type="application/rss+xml",
|
|
||||||
)
|
|
||||||
fg.language(config.language)
|
|
||||||
fg.description(config.description)
|
|
||||||
|
|
||||||
for post in post_list:
|
|
||||||
assert post.metadata
|
|
||||||
fe = fg.add_entry() # pyright: ignore[reportUnknownVariableType]
|
|
||||||
fe.id(f"{config.link}{util.normalize_url(post.url)}") # pyright: ignore[reportUnknownMemberType]
|
|
||||||
fe.link( # pyright: ignore[reportUnknownMemberType]
|
|
||||||
href=f"{config.link}{util.normalize_url(post.url)}"
|
|
||||||
)
|
|
||||||
fe.title(post.metadata.title) # pyright: ignore[reportUnknownMemberType]
|
|
||||||
fe.author(author) # pyright: ignore[reportUnknownMemberType]
|
|
||||||
desc = post.metadata.description
|
|
||||||
fe.description(desc) # pyright: ignore[reportUnknownMemberType]
|
|
||||||
date = post.metadata.date
|
|
||||||
fe.pubDate(date) # pyright: ignore[reportUnknownMemberType]
|
|
||||||
out: bytes = fg.rss_str(pretty=True) # pyright: ignore[reportUnknownVariableType]
|
|
||||||
assert isinstance(out, bytes)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def _get_post_list(self) -> list[Item]:
|
|
||||||
assert self.items
|
assert self.items
|
||||||
# sort according to date
|
|
||||||
# descending order
|
|
||||||
post_list: list[Item] = sorted(
|
post_list: list[Item] = sorted(
|
||||||
[item for item in self.items if item.post],
|
[item for item in self.items if item.post],
|
||||||
key=lambda item: item.metadata.date
|
key=lambda item: item.metadata.date
|
||||||
|
@ -150,22 +99,6 @@ class ZonaBuilder:
|
||||||
else date.min,
|
else date.min,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
return post_list
|
|
||||||
|
|
||||||
def _build(self):
|
|
||||||
post_list = self._get_post_list()
|
|
||||||
# number of posts
|
|
||||||
# generate RSS here
|
|
||||||
posts = len(post_list)
|
|
||||||
# link post chronology
|
|
||||||
for i, item in enumerate(post_list):
|
|
||||||
# prev: older post
|
|
||||||
older = post_list[i + 1] if i + 1 < posts else None
|
|
||||||
# next: newer post
|
|
||||||
newer = post_list[i - 1] if i > 0 else None
|
|
||||||
item.older = older
|
|
||||||
item.newer = newer
|
|
||||||
|
|
||||||
templater = Templater(
|
templater = Templater(
|
||||||
config=self.config,
|
config=self.config,
|
||||||
template_dir=self.layout.templates,
|
template_dir=self.layout.templates,
|
||||||
|
@ -178,9 +111,7 @@ class ZonaBuilder:
|
||||||
# write code highlighting stylesheet
|
# write code highlighting stylesheet
|
||||||
if self.config.markdown.syntax_highlighting.enabled:
|
if self.config.markdown.syntax_highlighting.enabled:
|
||||||
pygments_style = zmd.get_style_defs(self.config)
|
pygments_style = zmd.get_style_defs(self.config)
|
||||||
pygments_path = (
|
pygments_path = self.layout.output / "static" / "pygments.css"
|
||||||
self.layout.output / "static" / "pygments.css"
|
|
||||||
)
|
|
||||||
util.ensure_parents(pygments_path)
|
util.ensure_parents(pygments_path)
|
||||||
pygments_path.write_text(pygments_style)
|
pygments_path.write_text(pygments_style)
|
||||||
for item in self.item_map.values():
|
for item in self.item_map.values():
|
||||||
|
@ -221,15 +152,7 @@ class ZonaBuilder:
|
||||||
child.unlink()
|
child.unlink()
|
||||||
elif child.is_dir():
|
elif child.is_dir():
|
||||||
shutil.rmtree(child)
|
shutil.rmtree(child)
|
||||||
if not self.fresh:
|
|
||||||
self.layout = self.layout.refresh()
|
|
||||||
logger.debug("Discovering...")
|
logger.debug("Discovering...")
|
||||||
self._discover()
|
self._discover()
|
||||||
logger.debug("Building...")
|
logger.debug("Building...")
|
||||||
self._build()
|
self._build()
|
||||||
if self.config.feed.enabled:
|
|
||||||
rss = self.generate_feed()
|
|
||||||
path = self.layout.output / self.config.feed.path
|
|
||||||
util.ensure_parents(path)
|
|
||||||
path.write_bytes(rss)
|
|
||||||
self.fresh = False
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from importlib.metadata import version as __version__
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
|
@ -60,9 +59,7 @@ def build(
|
||||||
"""
|
"""
|
||||||
if draft:
|
if draft:
|
||||||
print("Option override: including drafts.")
|
print("Option override: including drafts.")
|
||||||
builder = ZonaBuilder(
|
builder = ZonaBuilder(cli_root=root, cli_output=output, draft=draft)
|
||||||
cli_root=root, cli_output=output, draft=draft
|
|
||||||
)
|
|
||||||
builder.build()
|
builder.build()
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,9 +73,7 @@ def serve(
|
||||||
] = None,
|
] = None,
|
||||||
host: Annotated[
|
host: Annotated[
|
||||||
str,
|
str,
|
||||||
typer.Option(
|
typer.Option("--host", help="Hostname for live preview server."),
|
||||||
"--host", help="Hostname for live preview server."
|
|
||||||
),
|
|
||||||
] = "localhost",
|
] = "localhost",
|
||||||
port: Annotated[
|
port: Annotated[
|
||||||
int,
|
int,
|
||||||
|
@ -135,23 +130,8 @@ def serve(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def version_callback(value: bool):
|
|
||||||
if value:
|
|
||||||
print(f"Zona version: {__version__('zona')}")
|
|
||||||
raise typer.Exit()
|
|
||||||
|
|
||||||
|
|
||||||
@app.callback()
|
@app.callback()
|
||||||
def main_entry(
|
def main_entry(
|
||||||
version: Annotated[ # pyright: ignore[reportUnusedParameter]
|
|
||||||
bool | None,
|
|
||||||
typer.Option(
|
|
||||||
"--version",
|
|
||||||
callback=version_callback,
|
|
||||||
is_eager=True,
|
|
||||||
help="Print version info and exit.",
|
|
||||||
),
|
|
||||||
] = None,
|
|
||||||
verbosity: Annotated[
|
verbosity: Annotated[
|
||||||
str,
|
str,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, tzinfo
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dacite import Config as DaciteConfig
|
|
||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
|
|
||||||
from zona.log import get_logger
|
from zona.log import get_logger
|
||||||
|
@ -31,17 +25,9 @@ def find_config(start: Path | None = None) -> Path | None:
|
||||||
SitemapConfig = dict[str, str]
|
SitemapConfig = dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PostDefaultsConfig:
|
|
||||||
description: str = "A blog post"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BlogConfig:
|
class BlogConfig:
|
||||||
dir: str = "blog"
|
dir: str = "blog"
|
||||||
defaults: PostDefaultsConfig = field(
|
|
||||||
default_factory=PostDefaultsConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -83,44 +69,14 @@ class ServerConfig:
|
||||||
reload: ReloadConfig = field(default_factory=ReloadConfig)
|
reload: ReloadConfig = field(default_factory=ReloadConfig)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AuthorConfig:
|
|
||||||
name: str = "John Doe"
|
|
||||||
email: str = "john@doe.net"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FeedConfig:
|
|
||||||
enabled: bool = True
|
|
||||||
timezone: tzinfo = field(default_factory=lambda: ZoneInfo("UTC"))
|
|
||||||
path: str = "rss.xml"
|
|
||||||
link: str = "https://example.com"
|
|
||||||
title: str = "Zona Website"
|
|
||||||
description: str = "My zona website."
|
|
||||||
language: str = "en"
|
|
||||||
author: AuthorConfig = field(default_factory=AuthorConfig)
|
|
||||||
|
|
||||||
|
|
||||||
IGNORELIST = [".marksman.toml"]
|
IGNORELIST = [".marksman.toml"]
|
||||||
|
|
||||||
|
|
||||||
def parse_timezone(s: Any) -> tzinfo:
|
|
||||||
if isinstance(s, str):
|
|
||||||
return ZoneInfo(s)
|
|
||||||
else:
|
|
||||||
raise TypeError(
|
|
||||||
f"Expected {str}, got {type(s)} for config key timezone"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ZonaConfig:
|
class ZonaConfig:
|
||||||
base_url: str = "/"
|
base_url: str = "/"
|
||||||
feed: FeedConfig = field(default_factory=FeedConfig)
|
|
||||||
# dictionary where key is name, value is url
|
# dictionary where key is name, value is url
|
||||||
sitemap: SitemapConfig = field(
|
sitemap: SitemapConfig = field(default_factory=lambda: {"Home": "/"})
|
||||||
default_factory=lambda: {"Home": "/"}
|
|
||||||
)
|
|
||||||
# list of globs relative to content that should be ignored
|
# list of globs relative to content that should be ignored
|
||||||
ignore: list[str] = field(default_factory=lambda: IGNORELIST)
|
ignore: list[str] = field(default_factory=lambda: IGNORELIST)
|
||||||
markdown: MarkdownConfig = field(default_factory=MarkdownConfig)
|
markdown: MarkdownConfig = field(default_factory=MarkdownConfig)
|
||||||
|
@ -129,12 +85,7 @@ class ZonaConfig:
|
||||||
server: ServerConfig = field(default_factory=ServerConfig)
|
server: ServerConfig = field(default_factory=ServerConfig)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, path: Path) -> ZonaConfig:
|
def from_file(cls, path: Path) -> "ZonaConfig":
|
||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
raw = yaml.safe_load(f)
|
raw = yaml.safe_load(f)
|
||||||
config: ZonaConfig = from_dict(
|
return from_dict(data_class=cls, data=raw)
|
||||||
data_class=cls,
|
|
||||||
data=raw,
|
|
||||||
config=DaciteConfig(type_hooks={tzinfo: parse_timezone}),
|
|
||||||
)
|
|
||||||
return config
|
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
:root {
|
:root {
|
||||||
--main-placeholder-color: #b14242;
|
|
||||||
--main-text-color: #b4b4b4;
|
--main-text-color: #b4b4b4;
|
||||||
--main-text-opaque-color: rgba(180, 180, 180, 0.8);
|
|
||||||
--main-bg-color: #121212;
|
--main-bg-color: #121212;
|
||||||
--main-link-color: #df6464;
|
--main-link-color: #df6464;
|
||||||
--main-heading-color: #df6464;
|
--main-heading-color: #df6464;
|
||||||
--main-bullet-color: #d87c4a;
|
--main-bullet-color: #d87c4a;
|
||||||
--orange-rgb: rgba(216, 124, 74, 0.6);
|
|
||||||
--main-transparent: rgba(255, 255, 255, 0.15);
|
--main-transparent: rgba(255, 255, 255, 0.15);
|
||||||
--main-small-text-color: rgba(255, 255, 255, 0.45);
|
--main-small-text-color: rgba(255, 255, 255, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
@ -25,110 +17,10 @@ body {
|
||||||
padding-left: calc(100vw - 100%);
|
padding-left: calc(100vw - 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
|
||||||
padding-top: -1rem;
|
|
||||||
margin-top: -1rem;
|
|
||||||
font-family: monospace;
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-nav {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.95em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-nav .bar {
|
|
||||||
position: relative;
|
|
||||||
bottom: 0.05em;
|
|
||||||
display: inline-block;
|
|
||||||
width: 1px;
|
|
||||||
height: 0.8em;
|
|
||||||
background-color: currentColor;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin: 0 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-nav .placeholder {
|
|
||||||
color: var(--main-placeholder-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-nav .symbol {
|
|
||||||
color: var(--main-bullet-color);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-logo.hover-symbol::before {
|
|
||||||
content: "~/";
|
|
||||||
}
|
|
||||||
|
|
||||||
.title.hover-symbol::before {
|
|
||||||
content: "$";
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-symbol {
|
|
||||||
color: inherit;
|
|
||||||
position: relative;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-symbol::before {
|
|
||||||
font-family: monospace;
|
|
||||||
content: "#";
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
margin-right: 0.25em;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s ease, color 0.15s ease;
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-symbol:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--main-placeholder-color);
|
|
||||||
}
|
|
||||||
.hover-symbol:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc ul {
|
|
||||||
font-family: monospace;
|
|
||||||
text-transform: lowercase;
|
|
||||||
margin: auto;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc ul ul {
|
|
||||||
padding-left: 1em;
|
|
||||||
margin-left: 1em;
|
|
||||||
/* list-style-type: "–– ";*/
|
|
||||||
}
|
|
||||||
.toc ul ul ul {
|
|
||||||
padding-left: 1em;
|
|
||||||
margin-left: 1em;
|
|
||||||
/* list-style-type: "-- ";*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.toclink {
|
.toclink {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: color 0.15s ease;
|
|
||||||
text-transform: lowercase;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
.post-list a {
|
|
||||||
position: relative;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.15s ease;
|
|
||||||
text-transform: lowercase;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toclink::before {
|
.toclink::before {
|
||||||
|
@ -139,16 +31,7 @@ header {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s ease, color 0.15s ease;
|
transition: opacity 0.2s ease;
|
||||||
color: var(--main-link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toclink:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--main-placeholder-color);
|
|
||||||
}
|
|
||||||
.toclink:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 .toclink::before {
|
h1 .toclink::before {
|
||||||
|
@ -167,6 +50,13 @@ h4 .toclink::before {
|
||||||
content: "###";
|
content: "###";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toclink:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.toclink:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* h1, */
|
/* h1, */
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
@ -183,16 +73,6 @@ h1 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
text-transform: lowercase;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
article h1:first-of-type {
|
article h1:first-of-type {
|
||||||
margin-block-start: 1.67rem;
|
margin-block-start: 1.67rem;
|
||||||
}
|
}
|
||||||
|
@ -236,55 +116,23 @@ h6 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*ul {*/
|
|
||||||
/* list-style-type: disc;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: "– ";
|
list-style-type: disc;
|
||||||
}
|
/* or any other list style */
|
||||||
ul ul {
|
|
||||||
padding-left: 1em;
|
|
||||||
margin-left: 1em;
|
|
||||||
list-style-type: "+ ";
|
|
||||||
}
|
|
||||||
ul ul ul {
|
|
||||||
list-style-type: "~ ";
|
|
||||||
}
|
|
||||||
ul ul ul ul {
|
|
||||||
list-style-type: "• ";
|
|
||||||
}
|
|
||||||
ul ul ul ul ul {
|
|
||||||
list-style-type: "– ";
|
|
||||||
}
|
|
||||||
ul ul ul ul ul ul {
|
|
||||||
list-style-type: "+ ";
|
|
||||||
}
|
|
||||||
ul ul ul ul ul ul ul {
|
|
||||||
list-style-type: "~ ";
|
|
||||||
}
|
|
||||||
ul ul ul ul ul ul ul ul {
|
|
||||||
list-style-type: "• ";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li::marker {
|
li::marker {
|
||||||
color: var(--main-bullet-color);
|
color: var(--main-bullet-color);
|
||||||
|
/* Change this to your desired color */
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--main-link-color);
|
color: var(--main-link-color);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: rgba(0, 0, 0, 0);
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration-color: var(--main-placeholder-color);
|
background: var(--main-transparent);
|
||||||
color: var(--main-bullet-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -298,8 +146,8 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
color: var(--main-text-opaque-color);
|
color: var(--main-small-text-color);
|
||||||
border-left: 3px solid var(--orange-rgb);
|
border-left: 3px solid var(--main-transparent);
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
@ -309,16 +157,6 @@ hr {
|
||||||
border: none;
|
border: none;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--main-small-text-color);
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@ -345,7 +183,7 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background-color: #1d1d1d;
|
background-color: #151515;
|
||||||
color: #d5d5d5;
|
color: #d5d5d5;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -521,3 +359,16 @@ caption {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--main-small-text-color);
|
color: var(--main-small-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a > code {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:has(> code) {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover > code {
|
||||||
|
background-color: var(--main-transparent);
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if metadata.show_title %}
|
{% if metadata.show_title %}
|
||||||
{% include "title.html" %}
|
<center><h1>{{ metadata.title }}</h1></center>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
{% extends "base.html" %} {% block content %} {% if metadata.show_title %} {%
|
{% extends "base.html" %}
|
||||||
include "title.html" %} {% if metadata.date.date() %}
|
|
||||||
<center>
|
|
||||||
<time class="post-date" datetime="{{ metadata.date.date() | safe }}"
|
|
||||||
>{{ metadata.date.date() | safe}}</time>
|
|
||||||
</center>
|
|
||||||
{% endif %} {% endif %} {% if is_post %} {% include "post_nav.html" %} {% endif
|
|
||||||
%}
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if metadata.show_title %}
|
||||||
|
<center><h1>{{ metadata.title }}</h1></center>
|
||||||
|
{% endif %}
|
||||||
|
{% if metadata.date %}
|
||||||
|
<center><small><time datetime="{{ metadata.date | safe }}">{{ metadata.date | safe}}</time></small></center>
|
||||||
|
{% endif %}
|
||||||
<article>{{ content | safe }}</article>
|
<article>{{ content | safe }}</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
{% extends "base.html" %} {% block content %} {% if metadata.show_title %} {%
|
{% extends "base.html" %}
|
||||||
include "title.html" %} {% endif %}
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<center><h1>{{ metadata.title }}</h1></center>
|
||||||
|
|
||||||
<article>{{ content | safe }}</article>
|
<article>{{ content | safe }}</article>
|
||||||
|
|
||||||
{% if post_list %}
|
{% if post_list %}
|
||||||
<div class="post-list">
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in post_list %}
|
{% for item in post_list %}
|
||||||
<li>
|
<li><small><time datetime="{{ item.metadata.date | safe }}">{{ item.metadata.date | safe}}</time></small>: <a href="/{{ item.url }}">{{ item.metadata.title }}</a></li>
|
||||||
<time class="post-list-date" datetime="{{ item.metadata.date.date() | safe }}"
|
|
||||||
>{{ item.metadata.date.date() | safe}}</time>: <a href="/{{ item.url }}"
|
|
||||||
>{{ item.metadata.title }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %} {% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<div class="post-nav">
|
|
||||||
<center>
|
|
||||||
<span class="symbol"><</span>{% if newer %}<a href="{{ newer }}">newr</a>{%
|
|
||||||
else %}<span class="placeholder">null</span>{% endif %}<span
|
|
||||||
class="symbol"
|
|
||||||
><span class="bar"></span></span>{% if older %}<a href="{{ older }}"
|
|
||||||
>oldr</a>{% else %}<span class="placeholder">null</span>{% endif %}<span
|
|
||||||
class="symbol"
|
|
||||||
>></span>
|
|
||||||
</center>
|
|
||||||
</div>
|
|
|
@ -1,2 +0,0 @@
|
||||||
<center><h1 class="title hover-symbol"><a id="" href="#">{{ metadata.title }}</a></h1></center>
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
import yaml
|
import yaml
|
||||||
|
@ -17,22 +16,6 @@ class Layout:
|
||||||
content: Path
|
content: Path
|
||||||
templates: Path
|
templates: Path
|
||||||
output: 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
|
@classmethod
|
||||||
def from_input(
|
def from_input(
|
||||||
|
@ -48,8 +31,6 @@ class Layout:
|
||||||
output=(root / "public").resolve()
|
output=(root / "public").resolve()
|
||||||
if not output
|
if not output
|
||||||
else output,
|
else output,
|
||||||
shared_templates=None,
|
|
||||||
_validate=validate,
|
|
||||||
)
|
)
|
||||||
if validate:
|
if validate:
|
||||||
logger.debug("Validating site layout...")
|
logger.debug("Validating site layout...")
|
||||||
|
@ -58,29 +39,10 @@ class Layout:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
"Missing required content directory!"
|
"Missing required content directory!"
|
||||||
)
|
)
|
||||||
internal_templates = util.get_resource_dir("templates")
|
if not layout.templates.is_dir():
|
||||||
user_templates = layout.templates
|
|
||||||
if not user_templates.is_dir() or util.is_empty(
|
|
||||||
user_templates
|
|
||||||
):
|
|
||||||
logger.debug("Using default template directory.")
|
logger.debug("Using default template directory.")
|
||||||
# use the included defaults
|
# use the included defaults
|
||||||
layout.templates = internal_templates
|
layout.templates = util.get_resource_dir("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
|
|
||||||
|
|
||||||
return layout
|
return layout
|
||||||
|
|
||||||
|
@ -125,8 +87,7 @@ def initialize_site(root: Path | None = None):
|
||||||
layout = Layout.from_input(root=root, validate=False)
|
layout = Layout.from_input(root=root, validate=False)
|
||||||
# load template resources
|
# load template resources
|
||||||
logger.debug("Loading internal templates.")
|
logger.debug("Loading internal templates.")
|
||||||
# only write the footer
|
templates = util.get_resources("templates")
|
||||||
templates = [util.get_resource("templates/footer.md")]
|
|
||||||
logger.debug("Loading internal static content.")
|
logger.debug("Loading internal static content.")
|
||||||
static = util.get_resources("content")
|
static = util.get_resources("content")
|
||||||
for dir, resources in [
|
for dir, resources in [
|
||||||
|
@ -147,13 +108,9 @@ def initialize_site(root: Path | None = None):
|
||||||
logger.debug("Loading default configuation.")
|
logger.debug("Loading default configuation.")
|
||||||
config = ZonaConfig()
|
config = ZonaConfig()
|
||||||
logger.debug(f"Writing default configuration to {config_path}.")
|
logger.debug(f"Writing default configuration to {config_path}.")
|
||||||
config_dict = asdict(config)
|
|
||||||
if "feed" in config_dict and "timezone" in config_dict["feed"]:
|
|
||||||
tz: ZoneInfo = config_dict["feed"]["timezone"]
|
|
||||||
config_dict["feed"]["timezone"] = tz.key
|
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(
|
yaml.dump(
|
||||||
config_dict,
|
asdict(config),
|
||||||
f,
|
f,
|
||||||
sort_keys=False,
|
sort_keys=False,
|
||||||
default_flow_style=False,
|
default_flow_style=False,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date, datetime, time, tzinfo
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import frontmatter
|
import frontmatter
|
||||||
|
@ -10,50 +10,31 @@ from dateutil import parser as date_parser
|
||||||
from yaml import YAMLError
|
from yaml import YAMLError
|
||||||
|
|
||||||
import zona.util
|
import zona.util
|
||||||
from zona.config import ZonaConfig
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Metadata:
|
class Metadata:
|
||||||
title: str
|
title: str
|
||||||
date: datetime
|
date: date
|
||||||
description: str
|
description: str | None
|
||||||
show_title: bool = True
|
show_title: bool = True
|
||||||
show_date: bool = True
|
|
||||||
show_nav: bool = True
|
|
||||||
style: str | None = "/static/style.css"
|
style: str | None = "/static/style.css"
|
||||||
header: bool = True
|
header: bool = True
|
||||||
footer: bool = True
|
footer: bool = True
|
||||||
template: str | None = None
|
template: str | None = None
|
||||||
post: bool | None = None
|
post: bool | None = None
|
||||||
draft: bool = False
|
draft: bool = False
|
||||||
ignore: bool = False
|
|
||||||
math: bool = True
|
math: bool = True
|
||||||
|
|
||||||
|
|
||||||
def ensure_timezone(dt: datetime, tz: tzinfo) -> datetime:
|
def parse_date(raw_date: str | date | object) -> date:
|
||||||
if dt.tzinfo is None or dt.utcoffset() is None:
|
if isinstance(raw_date, date):
|
||||||
dt = dt.replace(tzinfo=tz)
|
return raw_date
|
||||||
return dt
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: migrate to using datetime, where user can optionall specify
|
|
||||||
# a time as well. if only date is given, default to time.min
|
|
||||||
def parse_date(
|
|
||||||
raw_date: str | datetime | date | object, tz: tzinfo
|
|
||||||
) -> datetime:
|
|
||||||
if isinstance(raw_date, datetime):
|
|
||||||
return ensure_timezone(raw_date, tz)
|
|
||||||
elif isinstance(raw_date, date):
|
|
||||||
return datetime.combine(raw_date, time.min, tzinfo=tz)
|
|
||||||
assert isinstance(raw_date, str)
|
assert isinstance(raw_date, str)
|
||||||
dt = date_parser.parse(raw_date)
|
return date_parser.parse(raw_date).date()
|
||||||
return ensure_timezone(dt, tz)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_metadata(
|
def parse_metadata(path: Path) -> tuple[Metadata, str]:
|
||||||
path: Path, config: ZonaConfig
|
|
||||||
) -> tuple[Metadata, str]:
|
|
||||||
"""
|
"""
|
||||||
Parses a file and returns parsed Metadata and its content. Defaults
|
Parses a file and returns parsed Metadata and its content. Defaults
|
||||||
are applied for missing fields. If there is no metadata, a Metadata
|
are applied for missing fields. If there is no metadata, a Metadata
|
||||||
|
@ -69,11 +50,10 @@ def parse_metadata(
|
||||||
raw_meta = post.metadata or {}
|
raw_meta = post.metadata or {}
|
||||||
defaults = {
|
defaults = {
|
||||||
"title": zona.util.filename_to_title(path),
|
"title": zona.util.filename_to_title(path),
|
||||||
"date": datetime.fromtimestamp(path.stat().st_ctime),
|
"date": date.fromtimestamp(path.stat().st_ctime),
|
||||||
"description": config.blog.defaults.description,
|
|
||||||
}
|
}
|
||||||
meta = {**defaults, **raw_meta}
|
meta = {**defaults, **raw_meta}
|
||||||
meta["date"] = parse_date(meta.get("date"), config.feed.timezone)
|
meta["date"] = parse_date(meta.get("date"))
|
||||||
try:
|
try:
|
||||||
metadata = from_dict(
|
metadata = from_dict(
|
||||||
data_class=Metadata,
|
data_class=Metadata,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -23,8 +21,6 @@ class Item:
|
||||||
type: ItemType | None = None
|
type: ItemType | None = None
|
||||||
copy: bool = True
|
copy: bool = True
|
||||||
post: bool = False
|
post: bool = False
|
||||||
newer: Item | None = None
|
|
||||||
older: Item | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# @dataclass
|
# @dataclass
|
||||||
|
|
|
@ -237,13 +237,9 @@ def serve(
|
||||||
observer.schedule(
|
observer.schedule(
|
||||||
event_handler, path=str(root / "content"), recursive=True
|
event_handler, path=str(root / "content"), recursive=True
|
||||||
)
|
)
|
||||||
templates = root / "templates"
|
observer.schedule(
|
||||||
if templates.is_dir():
|
event_handler, path=str(root / "templates"), recursive=True
|
||||||
observer.schedule(
|
)
|
||||||
event_handler,
|
|
||||||
path=str(templates),
|
|
||||||
recursive=True,
|
|
||||||
)
|
|
||||||
observer.start()
|
observer.start()
|
||||||
|
|
||||||
# function to shut down gracefully
|
# function to shut down gracefully
|
||||||
|
|
|
@ -3,7 +3,6 @@ from typing import Literal
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
from zona import util
|
|
||||||
from zona.config import ZonaConfig
|
from zona.config import ZonaConfig
|
||||||
from zona.markdown import md_to_html
|
from zona.markdown import md_to_html
|
||||||
from zona.models import Item
|
from zona.models import Item
|
||||||
|
@ -27,6 +26,7 @@ def get_footer(template_dir: Path) -> str | None:
|
||||||
return html_footer.read_text()
|
return html_footer.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: add next/prev post button logic to posts
|
||||||
# TODO: add a recent posts element that can be included elsewhere?
|
# TODO: add a recent posts element that can be included elsewhere?
|
||||||
class Templater:
|
class Templater:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -35,7 +35,6 @@ class Templater:
|
||||||
template_dir: Path,
|
template_dir: Path,
|
||||||
post_list: list[Item],
|
post_list: list[Item],
|
||||||
):
|
):
|
||||||
# build temporary template dir
|
|
||||||
self.env: Environment = Environment(
|
self.env: Environment = Environment(
|
||||||
loader=FileSystemLoader(template_dir),
|
loader=FileSystemLoader(template_dir),
|
||||||
autoescape=select_autoescape(["html", "xml"]),
|
autoescape=select_autoescape(["html", "xml"]),
|
||||||
|
@ -77,12 +76,5 @@ class Templater:
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
header=header,
|
header=header,
|
||||||
footer=footer,
|
footer=footer,
|
||||||
is_post=item.post,
|
|
||||||
newer=util.normalize_url(item.newer.url)
|
|
||||||
if item.newer
|
|
||||||
else None,
|
|
||||||
older=util.normalize_url(item.older.url)
|
|
||||||
if item.older
|
|
||||||
else None,
|
|
||||||
post_list=self.post_list,
|
post_list=self.post_list,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,36 +1,11 @@
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import string
|
import string
|
||||||
import tempfile
|
|
||||||
import weakref
|
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
from importlib.resources.abc import Traversable
|
from importlib.resources.abc import Traversable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copy2
|
from shutil import copy2
|
||||||
from typing import Any, NamedTuple, override
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
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"<TempDir {self.path}>"
|
|
||||||
|
|
||||||
|
|
||||||
class ZonaResource(NamedTuple):
|
class ZonaResource(NamedTuple):
|
||||||
|
@ -44,9 +19,7 @@ def get_resource(path: str) -> ZonaResource:
|
||||||
if file.is_file():
|
if file.is_file():
|
||||||
return ZonaResource(name=path, contents=file.read_text())
|
return ZonaResource(name=path, contents=file.read_text())
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(f"{path} is not a valid Zona resource!")
|
||||||
f"{path} is not a valid Zona resource!"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_resources(subdir: str) -> list[ZonaResource]:
|
def get_resources(subdir: str) -> list[ZonaResource]:
|
||||||
|
@ -90,15 +63,6 @@ def copy_static_file(src: Path, dst: Path):
|
||||||
copy2(src, dst)
|
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:
|
def filename_to_title(path: Path) -> str:
|
||||||
name = path.stem
|
name = path.stem
|
||||||
words = name.replace("-", " ").replace("_", " ")
|
words = name.replace("-", " ").replace("_", " ")
|
||||||
|
@ -111,13 +75,10 @@ def normalize_url(url: str) -> str:
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def should_ignore(
|
def should_ignore(path: Path, patterns: list[str], base: Path) -> bool:
|
||||||
path: Path, patterns: list[str], base: Path
|
|
||||||
) -> bool:
|
|
||||||
rel_path = path.relative_to(base)
|
rel_path = path.relative_to(base)
|
||||||
return any(
|
return any(
|
||||||
fnmatch.fnmatch(str(rel_path), pattern)
|
fnmatch.fnmatch(str(rel_path), pattern) for pattern in patterns
|
||||||
for pattern in patterns
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
54
uv.lock
generated
54
uv.lock
generated
|
@ -44,16 +44,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "feedgen"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "lxml" },
|
|
||||||
{ name = "python-dateutil" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/59/be0a6f852b5dfbf19e6c8e962c8f41407697f9f52a7902250ed98683ae89/feedgen-1.0.0.tar.gz", hash = "sha256:d9bd51c3b5e956a2a52998c3708c4d2c729f2fcc311188e1e5d3b9726393546a", size = 258496, upload-time = "2023-12-25T18:04:08.421Z" }
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -97,46 +87,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/fd/aba08bb9e527168efad57985d7db9a853eb2384b1efa5ca5f3a3794c9cef/latex2mathml-3.78.0-py3-none-any.whl", hash = "sha256:1aeca3dc027b3006ad7b301b7f4a15ffbb4c1451e3dc8c3389e97b37b497e1d6", size = 73673, upload-time = "2025-05-03T16:51:51.991Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/fd/aba08bb9e527168efad57985d7db9a853eb2384b1efa5ca5f3a3794c9cef/latex2mathml-3.78.0-py3-none-any.whl", hash = "sha256:1aeca3dc027b3006ad7b301b7f4a15ffbb4c1451e3dc8c3389e97b37b497e1d6", size = 73673, upload-time = "2025-05-03T16:51:51.991Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lxml"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown"
|
name = "markdown"
|
||||||
version = "3.8.2"
|
version = "3.8.2"
|
||||||
|
@ -509,11 +459,10 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zona"
|
name = "zona"
|
||||||
version = "1.2.1"
|
version = "1.0.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "dacite" },
|
{ name = "dacite" },
|
||||||
{ name = "feedgen" },
|
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "l2m4m" },
|
{ name = "l2m4m" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
|
@ -540,7 +489,6 @@ dev = [
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "dacite", specifier = ">=1.9.2" },
|
{ name = "dacite", specifier = ">=1.9.2" },
|
||||||
{ name = "feedgen", specifier = ">=1.0.0" },
|
|
||||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||||
{ name = "l2m4m", specifier = ">=1.0.4" },
|
{ name = "l2m4m", specifier = ">=1.0.4" },
|
||||||
{ name = "markdown", specifier = ">=3.8.2" },
|
{ name = "markdown", specifier = ">=3.8.2" },
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue