diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml new file mode 100644 index 0000000..fde3206 --- /dev/null +++ b/.forgejo/workflows/publish.yml @@ -0,0 +1,20 @@ +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 }} + diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..2a05ee6 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,18 @@ +# 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 diff --git a/.kakrc b/.kakrc new file mode 100644 index 0000000..688bcbe --- /dev/null +++ b/.kakrc @@ -0,0 +1,25 @@ +# 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} +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9f6eed9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# 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! diff --git a/README.md b/README.md index dcccab2..6fe124a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ For an example of a website built with zona, please see - [Site Layout](#site-layout) - [Templates](#templates) - [Markdown Footer](#markdown-footer) + - [RSS Feed Generation](#rss-feed-generation) - [Internal Link Resolution](#internal-link-resolution) - [Syntax Highlighting](#syntax-highlighting) - [Markdown Extensions](#markdown-extensions) @@ -49,6 +50,7 @@ For an example of a website built with zona, please see - Live refresh in browser preview. - `jinja2` template support with sensible defaults included. - Basic page, blog post, post list. +- RSS feed generation. - Glob ignore. - YAML frontmatter. - Easily configurable sitemap header. @@ -101,10 +103,6 @@ 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. -_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 @@ -121,9 +119,10 @@ By default, the build outputs to a temporary directory. Use `-o/--output` to override this. **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. +server. If you change the configuration, 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 --- a temporary directory by default, unless +overridden with `-o/--output`. #### Live Reload @@ -172,8 +171,8 @@ 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`. +is optional, and merged with the defaults if it exists. `public` is the built +site output — it's recommended to add this path to your `.gitignore`. The `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`. @@ -191,17 +190,22 @@ 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. Your templates are merged with +the packaged defaults. To apply a certain template to a page, set the `template` +option in its [frontmatter](#frontmatter). The following public variables are +made available to the template engine: -| Name | Description | -| ---------- | ------------------------------------------------------ | -| `content` | The content of this page. | -| `url` | The resolved URL of this page. | -| `metadata` | The frontmatter of this page (_merged with defaults_). | -| `header` | The sitemap header in HTML form. Can be `False`. | -| `footer` | The footer in HTML form. Can be `False`. | +| Name | Description | +| ----------- | -------------------------------------------------------- | +| `content` | The content of this page. | +| `url` | The resolved URL of this page. | +| `metadata` | The frontmatter of this page (_merged with defaults_). | +| `header` | The sitemap header 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 @@ -210,6 +214,17 @@ 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.** +### 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 When zona encounters links in Markdown documents, it attempts to resolve them as @@ -229,11 +244,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 following Pygments plugins are included: -- [pygments-kakoune](https://codeberg.com/ficd/pygments-kakoune) +- [pygments-kakoune](https://codeberg.org/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-ashen](https://codeberg.org/ficd/ashen/tree/main/item/pygments/README.md) + - An implementation of the [Ashen](https://codeberg.org/ficd/ashen) theme for Pygments. If you want to use any external Pygments styles or lexers, they must be @@ -307,17 +322,19 @@ 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 | -| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `title` | `str` = title-cased filename. | Title of the page. | -| `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. | -| `header` | `bool` = `true` | Whether the header sitemap 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. | -| `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. | -| `math` | `bool` = `true` | Whether the LaTeX extension should be enabled for this page. | +| Key | Type & Default | Description | +| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `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. | +| `show_title` | `bool` = `true` | Whether `metadata.title` should be included in the template. | +| `header` | `bool` = `true` | Whether the header sitemap 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. | +| `post` | `bool \| none` = `none` | Whether this page is a **post**. `true`/`false` is _absolute_. Leave it unset for automatic detection. | +| `draft` | `bool` = `false` | Whether this page is a draft. See [drafts](#drafts) for more. | +| `ignore` | `bool` = `false` | Whether this page should be ignored in _both_ `final` and `draft` contexts. | +| `math` | `bool` = `true` | Whether the LaTeX extension should be enabled for this page. | **Note**: you can specify the date in any format that can be parsed by [`python-dateutil`](https://pypi.org/project/python-dateutil/). @@ -371,12 +388,23 @@ useful settings are listed here. Please see the default configuration: ```yaml -base_url: / +feed: + 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: Home: / ignore: - .marksman.toml markdown: + image_labels: true tab_length: 2 syntax_highlighting: enabled: true @@ -389,6 +417,8 @@ build: include_drafts: false blog: dir: blog + defaults: + description: A blog post server: reload: enabled: true @@ -397,6 +427,15 @@ server: | 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). | | `ignore` | List of paths to ignore. See [Ignore List](#ignore-list). | | `markdown.tab_length` | How many spaces should be considered an indentation level. | @@ -407,6 +446,7 @@ server: | `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. | +| `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.scroll_tolerance` | The distance, in pixels, from the bottom to still count as "scrolled to bottom". | @@ -439,7 +479,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 the `--draft` flag is specified. -[Ashen]: https://codeberg.com/ficd/ashen +[Ashen]: https://codeberg.org/ficd/ashen [Pygments]: https://pygments.org/ ## Known Problems diff --git a/pyproject.toml b/pyproject.toml index caf20fb..185c12d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zona" -version = "1.0.0" +version = "1.2.2" description = "Opinionated static site generator." license = "BSD-3-Clause " license-files = ["LICENSE"] @@ -11,6 +11,7 @@ authors = [ requires-python = ">=3.12" dependencies = [ "dacite>=1.9.2", + "feedgen>=1.0.0", "jinja2>=3.1.6", "l2m4m>=1.0.4", "markdown>=3.8.2", @@ -57,12 +58,12 @@ reportUnusedCallResult = false reportCallInDefaultInitializer = false enableTypeIgnoreComments = true reportIgnoreCommentWithoutRule = false -allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m"] +allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m", "feedgen", "feedgen.feed"] [tool.ruff] line-length = 70 indent-width = 4 -target-version = "py311" +target-version = "py312" [tool.ruff.lint] fixable = ["ALL"] diff --git a/src/zona/builder.py b/src/zona/builder.py index 12b5458..dd9509f 100644 --- a/src/zona/builder.py +++ b/src/zona/builder.py @@ -2,6 +2,8 @@ import shutil from datetime import date from pathlib import Path +from feedgen.feed import FeedGenerator + from zona import markdown as zmd from zona import util from zona.config import ZonaConfig @@ -30,6 +32,7 @@ class ZonaBuilder: self.config.build.include_drafts = True self.items: list[Item] = [] self.item_map: dict[Path, Item] = {} + self.fresh: bool = True def _discover(self): layout = self.layout @@ -48,12 +51,16 @@ class ZonaBuilder: destination=destination, url=str(destination.relative_to(layout.output)), ) - if path.name.endswith(".md") and not path.is_relative_to( + if path.name.endswith( + ".md" + ) and not path.is_relative_to( layout.root / "content" / "static" ): logger.debug(f"Parsing {path.name}.") - item.metadata, item.content = parse_metadata(path) - if ( + item.metadata, item.content = parse_metadata( + path, config=self.config + ) + if item.metadata.ignore or ( item.metadata.draft and not self.config.build.include_drafts ): @@ -69,11 +76,13 @@ class ZonaBuilder: item.copy = False name = destination.stem if name == "index": - item.destination = item.destination.with_suffix( - ".html" + item.destination = ( + item.destination.with_suffix(".html") ) else: - relative = path.relative_to(base).with_suffix("") + relative = path.relative_to(base).with_suffix( + "" + ) name = relative.stem item.destination = ( layout.output @@ -85,13 +94,55 @@ class ZonaBuilder: layout.output ) item.url = ( - "" if rel_url == Path(".") else rel_url.as_posix() + "" + if rel_url == Path(".") + else rel_url.as_posix() ) items.append(item) self.items = items - def _build(self): + def generate_feed(self) -> bytes: + 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 + # sort according to date + # descending order post_list: list[Item] = sorted( [item for item in self.items if item.post], key=lambda item: item.metadata.date @@ -99,6 +150,22 @@ class ZonaBuilder: else date.min, 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( config=self.config, template_dir=self.layout.templates, @@ -111,7 +178,9 @@ class ZonaBuilder: # write code highlighting stylesheet if self.config.markdown.syntax_highlighting.enabled: pygments_style = zmd.get_style_defs(self.config) - pygments_path = self.layout.output / "static" / "pygments.css" + pygments_path = ( + self.layout.output / "static" / "pygments.css" + ) util.ensure_parents(pygments_path) pygments_path.write_text(pygments_style) for item in self.item_map.values(): @@ -152,7 +221,15 @@ class ZonaBuilder: child.unlink() elif child.is_dir(): shutil.rmtree(child) + if not self.fresh: + self.layout = self.layout.refresh() logger.debug("Discovering...") self._discover() logger.debug("Building...") 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 diff --git a/src/zona/cli.py b/src/zona/cli.py index a7e2dd5..da15a22 100644 --- a/src/zona/cli.py +++ b/src/zona/cli.py @@ -1,3 +1,4 @@ +from importlib.metadata import version as __version__ from pathlib import Path from typing import Annotated @@ -59,7 +60,9 @@ def build( """ if draft: print("Option override: including drafts.") - builder = ZonaBuilder(cli_root=root, cli_output=output, draft=draft) + builder = ZonaBuilder( + cli_root=root, cli_output=output, draft=draft + ) builder.build() @@ -73,7 +76,9 @@ def serve( ] = None, host: Annotated[ str, - typer.Option("--host", help="Hostname for live preview server."), + typer.Option( + "--host", help="Hostname for live preview server." + ), ] = "localhost", port: Annotated[ int, @@ -130,8 +135,23 @@ def serve( ) +def version_callback(value: bool): + if value: + print(f"Zona version: {__version__('zona')}") + raise typer.Exit() + + @app.callback() 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[ str, typer.Option( diff --git a/src/zona/config.py b/src/zona/config.py index cc56ec2..84844c7 100644 --- a/src/zona/config.py +++ b/src/zona/config.py @@ -1,7 +1,13 @@ +from __future__ import annotations + from dataclasses import dataclass, field +from datetime import datetime, tzinfo from pathlib import Path +from typing import Any +from zoneinfo import ZoneInfo import yaml +from dacite import Config as DaciteConfig from dacite import from_dict from zona.log import get_logger @@ -25,9 +31,17 @@ def find_config(start: Path | None = None) -> Path | None: SitemapConfig = dict[str, str] +@dataclass +class PostDefaultsConfig: + description: str = "A blog post" + + @dataclass class BlogConfig: dir: str = "blog" + defaults: PostDefaultsConfig = field( + default_factory=PostDefaultsConfig + ) @dataclass @@ -69,14 +83,44 @@ class ServerConfig: 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"] +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 class ZonaConfig: base_url: str = "/" + feed: FeedConfig = field(default_factory=FeedConfig) # dictionary where key is name, value is url - sitemap: SitemapConfig = field(default_factory=lambda: {"Home": "/"}) + sitemap: SitemapConfig = field( + default_factory=lambda: {"Home": "/"} + ) # list of globs relative to content that should be ignored ignore: list[str] = field(default_factory=lambda: IGNORELIST) markdown: MarkdownConfig = field(default_factory=MarkdownConfig) @@ -85,7 +129,12 @@ class ZonaConfig: server: ServerConfig = field(default_factory=ServerConfig) @classmethod - def from_file(cls, path: Path) -> "ZonaConfig": + def from_file(cls, path: Path) -> ZonaConfig: with open(path, "r") as f: raw = yaml.safe_load(f) - return from_dict(data_class=cls, data=raw) + config: ZonaConfig = from_dict( + data_class=cls, + data=raw, + config=DaciteConfig(type_hooks={tzinfo: parse_timezone}), + ) + return config diff --git a/src/zona/data/content/static/style.css b/src/zona/data/content/static/style.css index 284a5ce..caa0eef 100644 --- a/src/zona/data/content/static/style.css +++ b/src/zona/data/content/static/style.css @@ -1,14 +1,22 @@ :root { + --main-placeholder-color: #b14242; --main-text-color: #b4b4b4; + --main-text-opaque-color: rgba(180, 180, 180, 0.8); --main-bg-color: #121212; --main-link-color: #df6464; --main-heading-color: #df6464; --main-bullet-color: #d87c4a; + --orange-rgb: rgba(216, 124, 74, 0.6); --main-transparent: rgba(255, 255, 255, 0.15); --main-small-text-color: rgba(255, 255, 255, 0.45); } +html { + scroll-behavior: smooth; +} + body { + margin: 0; line-height: 1.6; font-size: 18px; font-family: sans-serif; @@ -17,10 +25,110 @@ body { 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 { position: relative; text-decoration: none; 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 { @@ -31,7 +139,16 @@ body { top: 50%; transform: translateY(-50%); opacity: 0; - transition: opacity 0.2s ease; + transition: opacity 0.15s ease, color 0.15s ease; + color: var(--main-link-color); +} + +.toclink:hover::before { + opacity: 1; + color: var(--main-placeholder-color); +} +.toclink:hover { + background-color: transparent; } h1 .toclink::before { @@ -50,13 +167,6 @@ h4 .toclink::before { content: "###"; } -.toclink:hover::before { - opacity: 1; -} -.toclink:hover { - background-color: transparent; -} - /* h1, */ h2, h3, @@ -73,6 +183,16 @@ h1 { font-weight: bold; } +.title { + text-transform: lowercase; + font-family: monospace; +} + +.title a { + color: inherit; + text-decoration: none; +} + article h1:first-of-type { margin-block-start: 1.67rem; } @@ -116,23 +236,55 @@ h6 { font-weight: bold; } +/*ul {*/ +/* list-style-type: disc;*/ +/*}*/ + ul { - list-style-type: disc; - /* or any other list style */ + list-style-type: "– "; +} +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 { color: var(--main-bullet-color); - /* Change this to your desired color */ } a { color: var(--main-link-color); 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 { - background: var(--main-transparent); + text-decoration-color: var(--main-placeholder-color); + color: var(--main-bullet-color); } max-width: 100%; @@ -146,8 +298,8 @@ img { } blockquote { - color: var(--main-small-text-color); - border-left: 3px solid var(--main-transparent); + color: var(--main-text-opaque-color); + border-left: 3px solid var(--orange-rgb); padding: 0 1rem; margin-left: 0; margin-right: 0; @@ -157,6 +309,16 @@ hr { border: none; height: 1px; background: var(--main-small-text-color); + opacity: 0.5; +} + +time { + color: var(--main-bullet-color); + font-family: monospace; +} + +.post-list-date { + font-size: 0.95rem; } code { @@ -183,7 +345,7 @@ pre { } pre { - background-color: #151515; + background-color: #1d1d1d; color: #d5d5d5; padding: 1em; border-radius: 5px; @@ -359,16 +521,3 @@ caption { font-size: 0.8rem; 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); -} diff --git a/src/zona/data/templates/basic.html b/src/zona/data/templates/basic.html index 033d9e3..d86a25a 100644 --- a/src/zona/data/templates/basic.html +++ b/src/zona/data/templates/basic.html @@ -2,7 +2,7 @@ {% block content %} {% if metadata.show_title %} -