diff --git a/.github/workflows/deploy-markata-docs.yml b/.github/workflows/deploy-markata-docs.yml
index 250c1247..0d9cc896 100644
--- a/.github/workflows/deploy-markata-docs.yml
+++ b/.github/workflows/deploy-markata-docs.yml
@@ -10,23 +10,12 @@ jobs:
build-deploy-docs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- # - name: Get current date
- # id: date
- # run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
-
- # - name: Cache
- # uses: actions/cache@v2
- # with:
- # path: .markata.cache
- # key: ${{ runner.os }}-${{ hashfiles('markata.toml') }}-${{ steps.date.outputs.date }}-markata
- # restore-keys: |
- # ${{ runner.os }}-${{ hashfiles('markata.toml') }}-markata
+ - uses: actions/checkout@v3
- name: Set up Python 3.10
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v4
with:
- python-version: '3.10'
+ python-version: "3.10"
- name: install markata
run: pip install -e .
diff --git a/.github/workflows/release-markata.yml b/.github/workflows/release-markata.yml
index 26e0bcf5..aecbd3b6 100644
--- a/.github/workflows/release-markata.yml
+++ b/.github/workflows/release-markata.yml
@@ -4,8 +4,8 @@ on:
workflow_dispatch:
push:
paths:
- - 'markata/**'
- - 'pyproject.toml'
+ - "markata/**"
+ - "pyproject.toml"
env:
HATCH_INDEX_USER: __token__
@@ -15,9 +15,10 @@ jobs:
release-markata:
runs-on: ubuntu-latest
steps:
-
- - uses: actions/checkout@v2
- - uses: waylonwalker/hatch-action@v2
+ - uses: actions/checkout@v4
+ - uses: waylonwalker/hatch-action@v3
with:
- before-command: "test-lint"
-
+ before-command: "lint-format"
+ env:
+ # required for gh release
+ GH_TOKEN: ${{ github.token }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93bff835..5ecf2209 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,69 @@
# Markata Changelog
+## 0.8.0
+
+- pydantic support
+
+### Pydantic Support
+
+Now plugins are configured through a pydantic Config object.
+
+### breaking changes
+
+There are a number of breaking changes going into 0.8.0. Use caution when
+upgrading.
+
+#### glob config is now under markata.glob
+
+```diff
+- [markata]
+- glob_patterns = "pages/**/*.md"
++ [markata.glob]
++ glob_patterns = "pages/**/*.md"
+```
+
+#### Feeds are now a list
+
+```toml
+[markata.feeds.published]
+template="pages/templates/archive_template.html"
+card_template = "pages/templates/feed_card.html"
+filter="date<=today and templateKey in ['blog-post', 'til'] and status.lower()=='published'"
+sort="date"
+```
+
+> old
+
+```toml
+[[markata.feeds.published]]
+template="pages/templates/archive_template.html"
+card_template = "pages/templates/feed_card.html"
+filter="date<=today and templateKey in ['blog-post', 'til'] and status.lower()=='published'"
+sort="date"
+```
+
+> new
+
+### markata.summary.filter_count is now a list
+
+The old way was to set up a dict, where the keys were the name, now its a list
+of Objects with an explicit name field.
+
+```toml
+[markata.summary.filter_count.drafts]
+filter="published == 'False'"
+color='red'
+```
+
+> Old
+
+```toml
+[[markata.summary.filter_count]]
+name='drafts'
+filter="published == 'False'"
+color='red'
+```
+
## 0.7.4
- Fix: Icon resize broken from PIL 10.0.0 release
@@ -41,11 +105,15 @@ markata tui
- Fix: broken `markata new` command due to pydantic v2 compatability with copier.
+## 0.6.2
+
+Update License and Security files.
+
## 0.6.1
-- Fix: allow feeds to be used from within markdown
+- Fix: allow feeds to be used from within Markdown.
-### Feeds in markdown
+### Feeds in Markdown
```markdown
{% for post in markata.feeds.docs.posts %}
@@ -72,7 +140,7 @@ markata tui
- Fix: properly set the pyinstrument profiler to prevent recurrsion errors
0.6.dev13 #123
- Clean: cli attributes (`runner`, `summary`, `server`, `plugins`) are now
- added as Markata properties through `register_atter` rather than directly to
+ added as Markata properties through `register_attr` rather than directly to
the class 0.6.0.dev13 #107
- Fix: Markata tui will remain running even when the runner fails 0.6.0.dev13
#107
@@ -90,12 +158,12 @@ markata tui
wikilinks are now enabled by default ex: `[[home-page]]`. This will create a
link `home-page `. This will
-automagically just work if you leave `markata.plugins.flat_slug` plugin enabled
+automagically work if you leave `markata.plugins.flat_slug` plugin enabled
(which is by default).
> ProTip: this was highly inspired by the
> [marksman-lsp](https://github.com/artempyanykh/marksman) by
-> [artempyanykh](https://github.com/artempyanykh/) which can autocomplete post
+> [artempyanykh](https://github.com/artempyanykh/), which can autocomplete post
> links in this style for you.
[[home-page]]
diff --git a/README.md b/README.md
index a86ff219..225ea29c 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,6 @@
-## Coming soon
-
-pydantic all the things. All post objects and config will become pydantic objects. This will allow for validation to happen early, and referencing post attributes or config, it can be assumed that they exist whether they were explicitly created or cohersed to their defaults early in the build.
-
----
-
A static site generator that will give you a great site with many standard web
features like rss, sitemaps, and seo tags, out of the box. Running `markata
build` will get you a that only requires you to write Markdown. If you have
@@ -22,6 +16,16 @@ additional features that you want, don't worry, since markata is built
completely on plugins you can develop and install your own plugins to add the
features you want.
+> This has been a pet project for me to learn library development, plugin
+> driven design, diskcache, and more. It is the core of what builds my own site [waylonwalker.com](https://waylonwalker.com).
+
+## Disclaimer
+
+Make sure that you pin down what version of markata you want to use. If you
+are starting a new project that's probably the latest version from
+[pypi](https://pypi.org/project/markata). Things are likely to change in major
+releases, I do my best to document them, and not to break patches.
+
## QuickStart
Markata is fully configurable through a `markata.toml` file, but the defaults
@@ -46,6 +50,9 @@ echo '# My First Post' > first-post.md
echo '# Hello World' > hello-world.md
```
+> This example shows how you can build a site from only a single markdown
+> file.
+
### Build your site
Install markata into your virtual environment and run `markata build`. It will
diff --git a/docs/admonitions.md b/docs/admonitions.md
new file mode 100644
index 00000000..697e2ef2
--- /dev/null
+++ b/docs/admonitions.md
@@ -0,0 +1,106 @@
+---
+title: Admonitions
+description: This is what the default admonition styles look like and how to create them.
+---
+
+???+ note open by default
+you can open a details tab with '+'
+
+
+??? note closed by default
+ you can open a details tab with '+'
+
+## all of the admonitions
+
+
+!!! note
+ a note
+
+
+!!! abstract
+ an abstract
+
+
+!!! info
+
+ admonitions
+
+
+!!! tip
+
+ You should think about using admonitions
+
+
+!!! success
+
+ Run Successful!
+
+
+!!! question
+
+ What do you think of this?
+
+
+!!! source
+ Add some source code.
+ ```python
+ print('hello world')
+
+````
+
+
+!!! warning
+ a warning
+
+
+!!! failure
+ a failure
+
+
+!!! danger
+ some danger
+
+
+!!! bug
+ a bug
+
+
+!!! example
+ an example
+
+ ``` python
+ print('hello world')
+ ```
+
+
+!!! quote
+
+ a quote
+
+ > include a nice quote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+````
diff --git a/docs/index.md b/docs/index.md
index 8b8df186..c712f5b9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,7 +1,6 @@
---
title: Getting Started with Markata
description: Guide to get going with Markata
-
---
Markata is a fully plugins all the way down static site generator for
@@ -25,7 +24,7 @@ echo '# Hello World' > hello-world.md
### Build your site
-``` bash
+```bash
pip install markata
markata build
@@ -38,42 +37,42 @@ pipx run markata build
You will likely want to set things like `title`, `date`, `description`,
`published`, or `template` per post, this can all be done inside yaml frontmatter.
-``` markdown
+```markdown
---
templateKey: blog-post
-tags: ['python',]
-title: My Awesome Post
+tags: ["python"]
+title: My Awesome Post
date: 2022-01-21T16:40:34
published: False
-
---
This is my awesome post.
-
```
> Frontmatter is not required, but definitely gives you more control over your site.
## Next steps
+
_blog starter_
The [blog-starter](https://blog-starter.markata.dev/) has a really great write
-up on how to use markata. You can see it in your brower at the
+up on how to use markata. You can see it in your brower at the
[link](https://blog-starter.markata.dev/) or run it yourself `pipx run markata
new blog`.
## Examples Gallary
-Markata has a project gallery to show off sites built with markata. Please
+Markata has a project gallery to show off sites built with markata. Please
[submit](https://github.com/WaylonWalker/markata/issues/78) yours, and check
out the [project-gallery](http://markata.dev/project-gallery/) for inspiration.
## Deploying to a sub route
+
_gh pages_
To deploy a subroute, add a markata.path_prifix to your config (markata.toml).
-``` toml
+```toml
[markata]
path_prefix='my-sub-route'
```
@@ -86,17 +85,17 @@ check out
## Markata Docs
-Not much is documented yet, lots of work to do on the docs. Checkout
+Not much is documented yet, lots of work to do on the docs. Checkout
[LifeCycle](https://markata.dev/markata/lifecycle/) to see what a more
finished one looks like.
-UPDATE - the
+UPDATE - the
[`base_cli`](https://markata.dev/markata/plugins/base_cli/) is also up to
date and includes a lot of examples of how to use the markata cli.
> **Yes** this library generates it's own docs
-* [All Modules](https://markata.dev/autodoc/)
-* [Core Modules](https://markata.dev/core_modules/)
-* [Plugins](https://markata.dev/plugins/)
-* [color theme](https://markata.dev/color-theme/)
+- [All Modules](https://markata.dev/autodoc/)
+- [Core Modules](https://markata.dev/core_modules/)
+- [Plugins](https://markata.dev/plugins/)
+- [color theme](https://markata.dev/color-theme/)
diff --git a/docs/nav.md b/docs/nav.md
index 9f8a4807..328e4b2b 100644
--- a/docs/nav.md
+++ b/docs/nav.md
@@ -1,7 +1,7 @@
---
title: Creating your Navbar
description: Guide to creating a navbar in markata using the default template.
-
+jinja: false
---
Creating navbar links with the default markata templates is done by adding
@@ -13,11 +13,12 @@ The following example will create two links, one to the root of the site, with
the text `markata` and one to the github repo for markata with the text of
`GitHub`.
-``` toml
+```toml
[markata.nav]
'markata'='/'
'GitHub'='https://github.com/WaylonWalker/markata'
```
+
### Result
The resulting navbar would look something like this.
@@ -41,11 +42,10 @@ If you want to continue using this method of maintaining your nav links with a
custom template, add this block to your template where you want your nav to
appear.
-``` html
+```html
-{% for text, link in config.get('nav', {}).items() %}
- {{text}}
-{% endfor %}
+ {% for text, link in markata.config.nav.items() %}
+ {{text}}
+ {% endfor %}
```
-
diff --git a/markata.toml b/markata.toml
index 615742f1..22d581e1 100644
--- a/markata.toml
+++ b/markata.toml
@@ -22,6 +22,7 @@ markdown_backend='markdown-it-py'
# 2 weeks in seconds
default_cache_expire = 1209600
+# subroute = "docs"
## Markata Setup
glob_patterns = "docs/**/*.md,CHANGELOG.md"
@@ -29,6 +30,7 @@ output_dir = "markout"
assets_dir = "static"
hooks = [
"markata.plugins.publish_source",
+# "markata.plugins.subroute",
"markata.plugins.docs",
# "markata.plugins.prevnext",
"markata.plugins.service_worker",
@@ -40,7 +42,7 @@ disabled_hooks = [
# 'markata.plugins.seo',
'markata.plugins.heading_link',
'markata.plugins.manifest',
-'markata.plugins.rss'
+# 'markata.plugins.rss'
]
## Site Config
@@ -122,13 +124,14 @@ config = {markata = "markata"}
# card_template="""
# """
-[markata.feeds.project-gallery]
+[[markata.feeds]]
+slug='project-gallery'
title="Project Gallery"
-filter="'project-gallery' in path"
+filter="'project-gallery' in str(path)"
sort='title'
card_template="""
-{{ title }}
+{{ title }}
"""
-[markata.feeds.docs]
+[[markata.feeds]]
+slug='docs'
+title="Documentation"
filter='"markata" not in slug and "tests" not in slug and "404" not in slug'
sort='slug'
-card_template="{{ title }}{{ description }}
"
+card_template="{{ title }}{{ description }}
"
-[markata.feeds.autodoc]
+[[markata.feeds]]
+slug='all'
title="All Markata Modules"
filter="True"
card_template="""
-
- {{ title }}
+
+ {{ title }}
{{ article_html[:article_html.find('
')] }}
@@ -157,13 +163,14 @@ card_template="""
"""
-[markata.feeds.core_modules]
+[[markata.feeds]]
+slug='core-modules'
title="Markata Core Modules"
filter="'plugin' not in slug and 'test' not in slug and title.endswith('.py')"
card_template="""
-
- {{ title }}
+
+ {{ title }}
{{ article_html[:article_html.find('
')] }}
@@ -171,13 +178,14 @@ card_template="""
"""
-[markata.feeds.plugins]
+[[markata.feeds]]
+slug='plugins'
title="Markata Plugins"
filter="'plugin' in slug and 'test' not in slug"
card_template="""
-
- {{ title }}
+
+ {{ title }}
{{ article_html[:article_html.find('
')] }}
@@ -190,6 +198,8 @@ ignore=[
'jinja_md.md',
'post_template.md',
'publish_html.md',
+'CHANGELOG.md',
+'feeds.md',
]
[[markata.head.meta]]
@@ -206,15 +216,18 @@ key='n'
[markata.summary]
grid_attr = ['tags', 'series']
-[markata.summary.filter_count.drafts]
+[[markata.summary.filter_count]]
+name='drafts'
filter="published == 'False'"
color='red'
-[markata.summary.filter_count.articles]
+[[markata.summary.filter_count]]
+name='articles'
color='dark_orange'
-[markata.summary.filter_count.py_modules]
-filter='"plugin" not in slug and "docs" not in path'
+[[markata.summary.filter_count]]
+name='py_modules'
+filter='"plugin" not in slug and "docs" not in str(path)'
color="yellow1"
[markata.summary.filter_count.published]
@@ -222,9 +235,61 @@ filter="published == 'True'"
color='green1'
[markata.summary.filter_count.plugins]
-filter='"plugin" in slug and "docs" not in path'
+filter='"plugin" in slug and "docs" not in str(path)'
color="blue"
[markata.summary.filter_count.docs]
-filter="'docs' in path"
+filter="'docs' in str(path)"
color='purple'
+
+[markata.post_model]
+include = ['date', 'description', 'published', 'slug', 'title', 'content', 'html']
+repr_include = ['date', 'description', 'published', 'slug', 'title', 'output_html']
+
+[markata.render_markdown]
+backend='markdown-it-py'
+
+# [markata.markdown_it_py]
+# config='gfm-like'
+# # markdown_it built-in plugins
+# enable = [ "table" ]
+# disable = [ "image" ]
+
+# # markdown_it built-in plugin options
+# [markata.markdown_it_py.options_update]
+# linkify = true
+# html = true
+# typographer = true
+# highlight = 'markata.plugins.md_it_highlight_code:highlight_code'
+
+# add custom markdown_it plugins
+[[markata.render_markdown.extensions]]
+plugin = "mdit_py_plugins.admon:admon_plugin"
+
+[[markata.render_markdown.extensions]]
+plugin = "mdit_py_plugins.admon:admon_plugin"
+
+[[markata.render_markdown.extensions]]
+plugin = "mdit_py_plugins.attrs:attrs_plugin"
+config = {spans = true}
+
+[[markata.render_markdown.extensions]]
+plugin = "mdit_py_plugins.attrs:attrs_block_plugin"
+
+[[markata.render_markdown.extensions]]
+plugin = "markata.plugins.mdit_details:details_plugin"
+
+[[markata.render_markdown.extensions]]
+plugin = "mdit_py_plugins.anchors:anchors_plugin"
+
+[markata.render_markdown.extensions.config]
+permalink = true
+permalinkSymbol = ' '
+
+[[markata.render_markdown.extensions]]
+plugin = "markata.plugins.md_it_wikilinks:wikilinks_plugin"
+config = {markata = "markata"}
+
+[markata.glob]
+glob_patterns = "docs/**/*.md,CHANGELOG.md"
+use_gitignore = true
diff --git a/markata/__about__.py b/markata/__about__.py
index ed9d4d87..2b3551ba 100644
--- a/markata/__about__.py
+++ b/markata/__about__.py
@@ -1 +1 @@
-__version__ = "0.7.4"
+__version__ = "0.8.0.dev7"
diff --git a/markata/__init__.py b/markata/__init__.py
index 67be73dc..cd01c8f3 100644
--- a/markata/__init__.py
+++ b/markata/__init__.py
@@ -5,20 +5,20 @@
import atexit
import datetime
-from datetime import timedelta
import hashlib
import importlib
import logging
import os
-from pathlib import Path
import sys
import textwrap
-from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
+from datetime import timedelta
+from pathlib import Path
+from typing import Any, Dict, Iterable, Optional
-from checksumdir import dirhash
-from diskcache import FanoutCache
-import frontmatter
import pluggy
+import pydantic
+from checksumdir import dirhash
+from diskcache import Cache
from rich.console import Console
from rich.progress import track
from rich.table import Table
@@ -54,6 +54,7 @@
]
DEFAULT_HOOKS = [
+ "markata.plugins.copy_assets",
"markata.plugins.heading_link",
"markata.plugins.pyinstrument",
"markata.plugins.glob",
@@ -62,16 +63,13 @@
"markata.plugins.render_markdown",
"markata.plugins.manifest",
# "markata.plugins.generator",
- "markata.plugins.jinja_md",
"markata.plugins.feeds",
"markata.plugins.auto_description",
"markata.plugins.seo",
"markata.plugins.post_template",
"markata.plugins.covers",
- "markata.plugins.copy_assets",
"markata.plugins.publish_html",
"markata.plugins.flat_slug",
- "markata.plugins.datetime",
"markata.plugins.rss",
"markata.plugins.icon_resize",
"markata.plugins.sitemap",
@@ -84,6 +82,10 @@
"markata.plugins.tui",
"markata.plugins.setup_logging",
"markata.plugins.redirects",
+ "markata.plugins.post_model",
+ "markata.plugins.config_model",
+ "markata.plugins.create_models",
+ "markata.plugins.jinja_md",
]
DEFUALT_CONFIG = {
@@ -97,56 +99,83 @@
}
-class Post(frontmatter.Post):
- html: str
-
-
-def set_phase(function: Callable) -> Any:
- def wrapper(self: Markata, *args: Tuple, **kwargs: Dict) -> Any:
- self.phase = function.__name__
- self.phase_file.parent.mkdir(exist_ok=True)
- self.phase_file.write_text(self.phase)
- result = function(self, *args, **kwargs)
- self.phase = function.__name__
- self.phase_file.parent.mkdir(exist_ok=True)
- self.phase_file.write_text(self.phase)
- return result
-
- return wrapper
+class HooksConfig(pydantic.BaseModel):
+ hooks: list = ["default"]
+ disabled_hooks: list = []
class Markata:
- def __init__(self, console: Console = None) -> None:
- self.phase = "starting"
+ def __init__(self: "Markata", console: Console = None, config=None) -> None:
+ self.stages_ran = set()
+ self.threded = False
+ self._cache = None
+ self._precache = None
self.MARKATA_CACHE_DIR = Path(".") / ".markata.cache"
self.MARKATA_CACHE_DIR.mkdir(exist_ok=True)
- self.phase_file: Path = self.MARKATA_CACHE_DIR / "phase.txt"
+ self._pm = pluggy.PluginManager("markata")
+ self._pm.add_hookspecs(hookspec.MarkataSpecs)
+ if config is not None:
+ self.config = config
+ with self.cache as cache:
+ self.init_cache_stats = cache.stats()
self.registered_attrs = hookspec.registered_attrs
- self.configure()
+ self.post_models = []
+ self.config_models = []
+ if config is not None:
+ raw_hooks = config
+ else:
+ raw_hooks = standard_config.load("markata")
+ self.hooks_conf = HooksConfig.parse_obj(raw_hooks)
+ try:
+ default_index = self.hooks_conf.hooks.index("default")
+ hooks = [
+ *self.hooks_conf.hooks[:default_index],
+ *DEFAULT_HOOKS,
+ *self.hooks_conf.hooks[default_index + 1 :],
+ ]
+ self.hooks_conf.hooks = [
+ hook for hook in hooks if hook not in self.hooks_conf.disabled_hooks
+ ]
+ except ValueError:
+ # 'default' is not in hooks , do not replace with default_hooks
+ pass
+
+ self._register_hooks()
if console is not None:
self._console = console
atexit.register(self.teardown)
-
- with self.cache as cache:
- self.init_cache_stats = cache.stats()
+ self.precache
@property
- def cache(self) -> FanoutCache:
- return FanoutCache(self.MARKATA_CACHE_DIR, statistics=True)
+ def cache(self: "Markata") -> Cache:
+ # if self.threded:
+ # FanoutCache(self.MARKATA_CACHE_DIR, statistics=True)
+ if self._cache is not None:
+ return self._cache
+ self._cache = Cache(self.MARKATA_CACHE_DIR, statistics=True)
+
+ return self._cache
- def __getattr__(self, item: str) -> Any:
- if item in self._pm.hook.__dict__.keys():
+ @property
+ def precache(self: "Markata") -> None:
+ if self._precache is None:
+ self.cache.expire()
+ self._precache = {k: self.cache.get(k) for k in self.cache.iterkeys()}
+ return self._precache
+
+ def __getattr__(self: "Markata", item: str) -> Any:
+ if item in self._pm.hook.__dict__:
# item is a hook, return a callable function
return lambda: self.run(item)
- if item in self.__dict__.keys():
+ if item in self.__dict__:
# item is an attribute, return it
return self.__getitem__(item)
- elif item in self.registered_attrs.keys():
+ elif item in self.registered_attrs:
# item is created by a plugin, run it
stage_to_run_to = max(
- [attr["lifecycle"] for attr in self.registered_attrs[item]]
+ [attr["lifecycle"] for attr in self.registered_attrs[item]],
).name
self.run(stage_to_run_to)
return getattr(self, item)
@@ -154,7 +183,7 @@ def __getattr__(self, item: str) -> Any:
# Markata does not know what this is, raise
raise AttributeError(f"'Markata' object has no attribute '{item}'")
- def __rich__(self) -> Table:
+ def __rich__(self: "Markata") -> Table:
grid = Table.grid()
grid.add_column("label")
grid.add_column("value")
@@ -164,65 +193,61 @@ def __rich__(self) -> Table:
return grid
- def bust_cache(self) -> Markata:
+ def bust_cache(self: "Markata") -> Markata:
with self.cache as cache:
cache.clear()
return self
- @set_phase
def configure(self) -> Markata:
sys.path.append(os.getcwd())
- self.config = {**DEFUALT_CONFIG, **standard_config.load("markata")}
- if isinstance(self.config["glob_patterns"], str):
- self.config["glob_patterns"] = self.config["glob_patterns"].split(",")
- elif isinstance(self.config["glob_patterns"], list):
- self.config["glob_patterns"] = list(self.config["glob_patterns"])
- else:
- raise TypeError("glob_patterns must be list or str")
- self.glob_patterns = self.config["glob_patterns"]
-
- if "hooks" not in self.config:
- self.hooks = [""]
- if isinstance(self.config["hooks"], str):
- self.hooks = self.config["hooks"].split(",")
- if isinstance(self.config["hooks"], list):
- self.hooks = self.config["hooks"]
-
- if "disabled_hooks" not in self.config:
- self.disabled_hooks = [""]
- if isinstance(self.config["disabled_hooks"], str):
- self.disabled_hooks = self.config["disabled_hooks"].split(",")
- if isinstance(self.config["disabled_hooks"], list):
- self.disabled_hooks = self.config["disabled_hooks"]
-
- if not self.config.get("output_dir", "markout").endswith(
- self.config.get("path_prefix", "")
- ):
- self.config["output_dir"] = (
- self.config.get("output_dir", "markout")
- + "/"
- + self.config.get("path_prefix", "").rstrip("/")
- )
- if (
- len((output_split := self.config.get("output_dir", "markout").split("/")))
- > 1
- ):
- if "path_prefix" not in self.config.keys():
- self.config["path_prefix"] = "/".join(output_split[1:]) + "/"
- if not self.config.get("path_prefix", "").endswith("/"):
- self.config["path_prefix"] = self.config.get("path_prefix", "") + "/"
-
- self.config["output_dir"] = self.config["output_dir"].lstrip("/")
- self.config["path_prefix"] = self.config["path_prefix"].lstrip("/")
+ # self.config = {**DEFUALT_CONFIG, **standard_config.load("markata")}
+ # if isinstance(self.config["glob_patterns"], str):
+ # self.config["glob_patterns"] = self.config["glob_patterns"].split(",")
+ # elif isinstance(self.config["glob_patterns"], list):
+ # self.config["glob_patterns"] = list(self.config["glob_patterns"])
+ # else:
+ # raise TypeError("glob_patterns must be list or str")
+ # self.glob_patterns = self.config["glob_patterns"]
+
+ # self.hooks = self.config["hooks"]
+
+ # if "disabled_hooks" not in self.config:
+ # self.disabled_hooks = [""]
+ # if isinstance(self.config["disabled_hooks"], str):
+ # self.disabled_hooks = self.config["disabled_hooks"].split(",")
+ # if isinstance(self.config["disabled_hooks"], list):
+ # self.disabled_hooks = self.config["disabled_hooks"]
+
+ # if not self.config.get("output_dir", "markout").endswith(
+ # self.config.get("path_prefix", "")
+ # ):
+ # self.config["output_dir"] = (
+ # self.config.get("output_dir", "markout") +
+ # "/" +
+ # self.config.get("path_prefix", "").rstrip("/")
+ # )
+ # if (
+ # len((output_split := self.config.get("output_dir", "markout").split("/"))) >
+ # 1
+ # ):
+ # if "path_prefix" not in self.config.keys():
+ # self.config["path_prefix"] = "/".join(output_split[1:]) + "/"
+ # if not self.config.get("path_prefix", "").endswith("/"):
+ # self.config["path_prefix"] = self.config.get("path_prefix", "") + "/"
+
+ # self.config["output_dir"] = self.config["output_dir"].lstrip("/")
+ # self.config["path_prefix"] = self.config["path_prefix"].lstrip("/")
try:
- default_index = self.hooks.index("default")
+ default_index = self.hooks_conf.hooks.index("default")
hooks = [
- *self.hooks[:default_index],
+ *self.hooks_conf.hooks[:default_index],
*DEFAULT_HOOKS,
- *self.hooks[default_index + 1 :],
+ *self.hooks_conf.hooks[default_index + 1 :],
+ ]
+ self.config.hooks = [
+ hook for hook in hooks if hook not in self.config.disabled_hooks
]
- self.hooks = [hook for hook in hooks if hook not in self.disabled_hooks]
except ValueError:
# 'default' is not in hooks , do not replace with default_hooks
pass
@@ -284,15 +309,7 @@ def make_hash(self, *keys: str) -> str:
return hashlib.md5("".join(str_keys).encode("utf-8")).hexdigest()
@property
- def phase(self) -> str:
- return self._phase
-
- @phase.setter
- def phase(self, value: str) -> None:
- self._phase = value
-
- @property
- def content_dir_hash(self) -> str:
+ def content_dir_hash(self: "Markata") -> str:
hashes = [
dirhash(dir)
for dir in self.content_directories
@@ -301,72 +318,95 @@ def content_dir_hash(self) -> str:
return self.make_hash(*hashes)
@property
- def console(self) -> Console:
+ def console(self: "Markata") -> Console:
try:
return self._console
except AttributeError:
self._console = Console()
return self._console
- def describe(self) -> dict[str, str]:
- return {"version": __version__, "phase": self.phase}
+ def describe(self: "Markata") -> dict[str, str]:
+ return {"version": __version__}
- def _to_dict(self) -> dict[str, Iterable]:
+ def _to_dict(self: "Markata") -> dict[str, Iterable]:
return {"config": self.config, "articles": [a.to_dict() for a in self.articles]}
- def to_dict(self) -> dict:
+ def to_dict(self: "Markata") -> dict:
return self._to_dict()
- def to_json(self) -> str:
+ def to_json(self: "Markata") -> str:
import json
return json.dumps(self.to_dict(), indent=4, sort_keys=True, default=str)
- def _register_hooks(self) -> None:
- for hook in self.hooks:
+ def _register_hooks(self: "Markata") -> None:
+ sys.path.append(os.getcwd())
+ for hook in self.hooks_conf.hooks:
try:
# module style plugins
plugin = importlib.import_module(hook)
except ModuleNotFoundError as e:
# class style plugins
if "." in hook:
- mod = importlib.import_module(".".join(hook.split(".")[:-1]))
- plugin = getattr(mod, hook.split(".")[-1])
+ try:
+ mod = importlib.import_module(".".join(hook.split(".")[:-1]))
+ plugin = getattr(mod, hook.split(".")[-1])
+ except ModuleNotFoundError as e:
+ raise ModuleNotFoundError(
+ f"module {hook} not found\n{sys.path}"
+ ) from e
else:
raise e
self._pm.register(plugin)
- def __iter__(self, description: str = "working...") -> Iterable[frontmatter.Post]:
- articles: Iterable[frontmatter.Post] = track(
- self.articles, description=description, transient=True, console=self.console
+ def __iter__(
+ self: "Markata", description: str = "working..."
+ ) -> Iterable["Markata.Post"]:
+ articles: Iterable[Markata.Post] = track(
+ self.articles,
+ description=description,
+ transient=True,
+ console=self.console,
)
return articles
- def iter_articles(self, description: str) -> Iterable[frontmatter.Post]:
- articles: Iterable[frontmatter.Post] = track(
- self.articles, description=description, transient=True, console=self.console
+ def iter_articles(self: "Markata", description: str) -> Iterable[Markata.Post]:
+ articles: Iterable[Markata.Post] = track(
+ self.articles,
+ description=description,
+ transient=True,
+ console=self.console,
)
return articles
- def teardown(self) -> Markata:
+ def teardown(self: "Markata") -> Markata:
"""give special access to the teardown lifecycle method"""
self._pm.hook.teardown(markata=self)
return self
- def run(self, lifecycle: LifeCycle = None) -> Markata:
+ def run(self: "Markata", lifecycle: LifeCycle = None) -> Markata:
if lifecycle is None:
- lifecycle = getattr(LifeCycle, max(LifeCycle._member_map_))
+ lifecycle = max(LifeCycle._member_map_.values())
if isinstance(lifecycle, str):
lifecycle = LifeCycle[lifecycle]
- stages_to_run = [m for m in LifeCycle._member_map_ if LifeCycle[m] <= lifecycle]
+ stages_to_run = [
+ m
+ for m in LifeCycle._member_map_
+ if (LifeCycle[m] <= lifecycle) and (m not in self.stages_ran)
+ ]
+
+ if not stages_to_run:
+ self.console.log(f"{lifecycle.name} already ran")
+ return self
self.console.log(f"running {stages_to_run}")
for stage in stages_to_run:
self.console.log(f"{stage} running")
getattr(self._pm.hook, stage)(markata=self)
+ self.stages_ran.add(stage)
self.console.log(f"{stage} complete")
with self.cache as cache:
@@ -374,7 +414,7 @@ def run(self, lifecycle: LifeCycle = None) -> Markata:
if hits + misses > 0:
self.console.log(
- f"lifetime cache hit rate {round(hits/ (hits + misses)*100, 2)}%"
+ f"lifetime cache hit rate {round(hits/ (hits + misses)*100, 2)}%",
)
if misses > 0:
@@ -385,7 +425,7 @@ def run(self, lifecycle: LifeCycle = None) -> Markata:
if hits + misses > 0:
self.console.log(
- f"run cache hit rate {round(hits/ (hits + misses)*100, 2)}%"
+ f"run cache hit rate {round(hits/ (hits + misses)*100, 2)}%",
)
if misses > 0:
@@ -393,8 +433,8 @@ def run(self, lifecycle: LifeCycle = None) -> Markata:
return self
- def filter(self, filter: str) -> List:
- def evalr(a: Post) -> Any:
+ def filter(self: "Markata", filter: str) -> list:
+ def evalr(a: Markata.Post) -> Any:
try:
return eval(
filter,
@@ -411,14 +451,14 @@ def evalr(a: Post) -> Any:
return [a for a in self.articles if evalr(a)]
def map(
- self,
+ self: "Markata",
func: str = "title",
filter: str = "True",
sort: str = "True",
reverse: bool = True,
- *args: Tuple,
- **kwargs: Dict,
- ) -> List:
+ *args: tuple,
+ **kwargs: dict,
+ ) -> list:
import copy
def try_sort(a: Any) -> int:
@@ -442,8 +482,9 @@ def try_sort(a: Any) -> int:
try:
return int(
datetime.datetime.combine(
- value, datetime.datetime.min.time()
- ).timestamp()
+ value,
+ datetime.datetime.min.time(),
+ ).timestamp(),
)
except Exception:
try:
@@ -475,7 +516,8 @@ def try_sort(a: Any) -> int:
variable = str(e).split("'")[1]
missing_in_posts = self.map(
- "path", filter=f'"{variable}" not in post.keys()'
+ "path",
+ filter=f'"{variable}" not in post.keys()',
)
message = (
f"variable: '{variable}' is missing in {len(missing_in_posts)} posts"
@@ -483,7 +525,7 @@ def try_sort(a: Any) -> int:
if len(missing_in_posts) > 10:
message += (
f"\nfirst 10 paths to posts missing {variable}"
- f"[{','.join(missing_in_posts)}..."
+ f"[{','.join([str(p) for p in missing_in_posts[:10]])}..."
)
else:
message += f"\npaths to posts missing {variable} {missing_in_posts}"
@@ -496,3 +538,4 @@ def try_sort(a: Any) -> int:
def load_ipython_extension(ipython):
ipython.user_ns["m"] = Markata()
ipython.user_ns["markata"] = ipython.user_ns["m"]
+ ipython.user_ns["markata"] = ipython.user_ns["m"]
diff --git a/markata/cli/__main__.py b/markata/cli/__main__.py
index 2bbe245a..150e2a3a 100644
--- a/markata/cli/__main__.py
+++ b/markata/cli/__main__.py
@@ -6,7 +6,7 @@
class MarkataWidget(Widget):
- def __init__(self, markata: Markata, widget: str = "server"):
+ def __init__(self, markata: Markata, widget: str = "server") -> None:
super().__init__(widget)
self.m = markata
self.widget = widget
diff --git a/markata/cli/cli.py b/markata/cli/cli.py
index 1ce84a0f..66087ac6 100644
--- a/markata/cli/cli.py
+++ b/markata/cli/cli.py
@@ -40,7 +40,7 @@ def version_callback(value: bool) -> None:
from markata import __version__
typer.echo(f"Markata CLI Version: {__version__}")
- raise typer.Exit()
+ raise typer.Exit
def json_callback(value: bool) -> None:
@@ -48,7 +48,7 @@ def json_callback(value: bool) -> None:
from markata import Markata
typer.echo(Markata().to_json())
- raise typer.Exit()
+ raise typer.Exit
app = typer.Typer(
@@ -60,10 +60,16 @@ def json_callback(value: bool) -> None:
@app.callback()
def main(
version: bool = typer.Option(
- None, "--version", callback=version_callback, is_eager=True
+ None,
+ "--version",
+ callback=version_callback,
+ is_eager=True,
),
to_json: bool = typer.Option(
- None, "--to-json", callback=json_callback, is_eager=True
+ None,
+ "--to-json",
+ callback=json_callback,
+ is_eager=True,
),
) -> None:
# Do other global stuff, handle other global options here
diff --git a/markata/cli/plugins.py b/markata/cli/plugins.py
index 4207ada2..446ade39 100644
--- a/markata/cli/plugins.py
+++ b/markata/cli/plugins.py
@@ -11,7 +11,7 @@
class Plugins:
- def __init__(self, markata: "Markata"):
+ def __init__(self, markata: "Markata") -> None:
self.m = markata
def __rich__(self) -> Panel:
@@ -26,11 +26,14 @@ def __rich__(self) -> Panel:
".".join(plugin.__name__.split(".")[:-1]),
".[/]",
plugin.__name__.split(".")[-1],
- ]
- )
+ ],
+ ),
)
return Panel(
- grid, title=f"plugins {num_plugins}", border_style="gold1", expand=False
+ grid,
+ title=f"plugins {num_plugins}",
+ border_style="gold1",
+ expand=False,
)
diff --git a/markata/cli/server.py b/markata/cli/server.py
index 900f264c..065d1a50 100644
--- a/markata/cli/server.py
+++ b/markata/cli/server.py
@@ -1,12 +1,16 @@
import atexit
-from pathlib import Path
import time
-from typing import Union
+from pathlib import Path
+from typing import TYPE_CHECKING, Union
+import typer
from rich.panel import Panel
from markata.hookspec import hook_impl, register_attr
+if TYPE_CHECKING:
+ from markata import Markata
+
def find_port(port: int = 8000) -> int:
"""Find a port not in ues starting at given port"""
@@ -15,17 +19,17 @@ def find_port(port: int = 8000) -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(("localhost", port)) == 0:
return find_port(port=port + 1)
- else:
- return port
+ return port
class Server:
def __init__(
- self,
+ self: "Server",
+ *,
auto_restart: bool = True,
directory: Union[str, "Path"] = None,
port: int = 8000,
- ):
+ ) -> None:
if directory is None:
from markata import Markata
@@ -88,7 +92,10 @@ def __rich__(self) -> Panel:
else:
return Panel(
- "[red]server died", title=self.title, border_style="red", expand=True
+ "[red]server died",
+ title=self.title,
+ border_style="red",
+ expand=True,
)
@@ -107,13 +114,27 @@ def get_server(self):
Markata.server = property(get_server)
-if __name__ == "__main__":
+def run_server() -> None:
from rich.live import Live
- from markata import Markata
-
from .cli import run_until_keyboard_interrupt
- m = Markata()
with Live(Server(), refresh_per_second=1, screen=True):
run_until_keyboard_interrupt()
+
+
+@hook_impl()
+def cli(app: typer.Typer, markata: "Markata") -> None:
+ server_app = typer.Typer()
+ app.add_typer(server_app)
+
+ @server_app.callback(invoke_without_command=True)
+ def serve():
+ """
+ Serve the site locally.
+ """
+ run_server()
+
+
+if __name__ == "__main__":
+ run_server()
diff --git a/markata/cli/summary.py b/markata/cli/summary.py
index 5198c844..57b00a89 100644
--- a/markata/cli/summary.py
+++ b/markata/cli/summary.py
@@ -41,15 +41,18 @@
of posts.
```
-[markata.summary.filter_count.drafts]
+[[markata.summary.filter_count]]
+name='drafts'
filter="published == 'False'"
color='red'
-[markata.summary.filter_count.articles]
+[[markata.summary.filter_count]]
+name='articles'
color='dark_orange'
-[markata.summary.filter_count.py_modules]
-filter='"plugin" not in slug and "docs" not in path'
+[[markata.summary.filter_count]]
+name='py_modules'
+filter='"plugin" not in slug and "docs" not in str(path)'
color="yellow1"
[markata.summary.filter_count.published]
@@ -57,11 +60,11 @@
color='green1'
[markata.summary.filter_count.plugins]
-filter='"plugin" in slug and "docs" not in path'
+filter='"plugin" in slug and "docs" not in str(path)'
color="blue"
[markata.summary.filter_count.docs]
-filter="'docs' in path"
+filter="'docs' in str(path)"
color='purple'
```
@@ -78,11 +81,13 @@
```
"""
from collections import Counter
-from typing import TYPE_CHECKING, Union
+from typing import List, TYPE_CHECKING, Union
from more_itertools import flatten
+import pydantic
from rich.panel import Panel
from rich.table import Table
+import typer
from markata.hookspec import hook_impl, register_attr
@@ -90,6 +95,29 @@
from markata import Markata
+class FilterCount(pydantic.BaseModel):
+ name: str
+ filter: str = "True"
+ color: str = "white"
+
+
+class SummaryConfig(pydantic.BaseModel):
+ grid_attr: List[str] = ["tags", "series"]
+ filter_count: List[FilterCount] = FilterCount(
+ name="drafts", filter="published == 'False'", color="red"
+ )
+
+
+class Config(pydantic.BaseModel):
+ summary: SummaryConfig = SummaryConfig()
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
class Summary:
def __init__(self, m: "Markata", simple: bool = False) -> None:
self.m = m
@@ -99,23 +127,22 @@ def get_grid(self) -> None:
"create a rich grid to display the summary"
self.grid = Table.grid(expand=True)
- for name, config in (
- self.m.config.get("summary", {})
- .get("filter_count", {"aricles": {"color": "purple", "filter": "True"}})
- .items()
- ):
- self.filter_count(name, **config)
+ for filter_count in self.m.config.summary.filter_count:
+ self.filter_count(filter_count)
- for attr in self.m.config.get("summary", {}).get("grid_attr", []):
+ for attr in self.m.config.summary.grid_attr:
self.grid_attr(attr)
return self.grid
def filter_count(
- self, title: str, filter: str = "True", color: str = "white"
+ self,
+ fc: FilterCount,
) -> None:
"add a row in the grid for the number of items in a filter config"
- self.grid.add_row(f"[{color}]{len(self.m.map(filter=filter))}[/] {title}")
+ self.grid.add_row(
+ f"[{fc.color}]{len(self.m.map(filter=fc.filter))}[/] {fc.name}"
+ )
def grid_attr(self, attr: str) -> None:
"add attribute the the object grid"
@@ -123,10 +150,10 @@ def grid_attr(self, attr: str) -> None:
flatten(
[
tags if isinstance(tags, list) else [tags]
- for a in self.m.articles
+ for a in self.m.posts
if (tags := a.get(attr, None)) is not None
- ]
- )
+ ],
+ ),
)
if len(posts) > 0:
self.grid.add_row()
@@ -135,15 +162,16 @@ def grid_attr(self, attr: str) -> None:
self.grid.add_row(f'{count} {" "*(3-len(str(count)))} {post}')
def __rich__(self) -> Union[Panel, Table]:
- try:
- grid = self.get_grid()
- except Exception:
- grid = "Error"
+ grid = self.get_grid()
+
if self.simple:
return grid
else:
return Panel(
- grid, title="[gold1]summary[/]", border_style="magenta", expand=False
+ grid,
+ title="[gold1]summary[/]",
+ border_style="magenta",
+ expand=False,
)
@@ -162,6 +190,24 @@ def get_summary(self):
Markata.summary = property(get_summary)
+@hook_impl()
+def cli(app: typer.Typer, markata: "Markata") -> None:
+ """
+ Markata hook to implement base cli commands.
+ """
+ summary_app = typer.Typer()
+ app.add_typer(summary_app, name="summary")
+
+ @summary_app.callback(invoke_without_command=True)
+ def summary():
+ "show the application summary"
+ from rich import print
+
+ markata.console.quiet = True
+
+ print(Summary(markata, simple=True))
+
+
if __name__ == "__main__":
from rich import print
diff --git a/markata/hookspec.py b/markata/hookspec.py
index abc84267..8942f6fc 100644
--- a/markata/hookspec.py
+++ b/markata/hookspec.py
@@ -57,7 +57,7 @@ def decorator_register(
"func": func,
"funcname": func.__code__.co_name,
"lifecycle": getattr(LifeCycle, func.__code__.co_name),
- }
+ },
)
@functools.wraps(func)
diff --git a/markata/lifecycle.py b/markata/lifecycle.py
index b1254e47..2185a8b7 100644
--- a/markata/lifecycle.py
+++ b/markata/lifecycle.py
@@ -20,17 +20,25 @@ class LifeCycle(Enum):
"""
LifeCycle currently supports the following steps.
+
* configure - load and fix configuration
* glob - find files
* load - load files
+ * validate_posts
* pre_render - clean up files/metadata before render
* render - render content
* post_render - clean up rendered content
* save - store results to disk
+ * teardown - runs on exit
"""
+ config_model = auto()
+ post_model = auto()
+ create_models = auto()
+ load_config = auto()
configure = auto()
+ validate_config = auto()
glob = auto()
load = auto()
pre_render = auto()
diff --git a/markata/plugins/auto_description.py b/markata/plugins/auto_description.py
index ad1c4000..6fe4a8f1 100644
--- a/markata/plugins/auto_description.py
+++ b/markata/plugins/auto_description.py
@@ -52,7 +52,8 @@
"""
from itertools import compress
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Dict
+from typing import Any, Dict, TYPE_CHECKING
+import html
import commonmark
@@ -87,6 +88,7 @@ def get_description(article: "Post") -> str:
# deduplicate paragraph_nodes based on unique source position
unique_paragraph_nodes = list(compress(paragraph_nodes, unique_mask))
paragraphs = " ".join([p.first_child.literal for p in unique_paragraph_nodes])
+ paragraphs = html.escape(paragraphs)
return paragraphs
@@ -108,10 +110,10 @@ def set_description(
config,
)
- description_from_cache = cache.get(key)
+ description_from_cache = markata.precache.get(key)
if description_from_cache is None:
description = get_description(article)[:max_description]
- markata.cache.add(key, description, expire=config["cache_expire"])
+ markata.cache.add(key, description, expire=markata.config.default_cache_expire)
else:
description = description_from_cache
@@ -150,7 +152,7 @@ def try_config_get(key: str) -> Any:
value
for description_key in config
if (value := try_config_get(description_key))
- ]
+ ],
)
with markata.cache as cache:
diff --git a/markata/plugins/auto_title.py b/markata/plugins/auto_title.py
index 7b54534b..d364e1a8 100644
--- a/markata/plugins/auto_title.py
+++ b/markata/plugins/auto_title.py
@@ -4,7 +4,7 @@
@hook_impl
-@register_attr("articles")
+@register_attr("articles", "posts")
def pre_render(markata) -> None:
for article in markata.filter('title==""'):
article["title"] = (
diff --git a/markata/plugins/base_cli.py b/markata/plugins/base_cli.py
index eefcba5e..1c26ca35 100644
--- a/markata/plugins/base_cli.py
+++ b/markata/plugins/base_cli.py
@@ -7,7 +7,7 @@
[`list`](https://markata.dev/markata/plugins/base_cli/#list-function)
commands as part of the main markata cli.
-## Building Your Site with the Cli
+# Building Your Site with the Cli
Your Markata Site can be build completely from the command line.
@@ -22,7 +22,7 @@
[`build`](https://markata.dev/markata/plugins/base_cli/#build-function)
section for more examples.
-## Listing your articles
+# Listing your articles
Markata list is a tool to help list out artile attributes right to your
terminal. This is very helpful to find articles on larger sites, or
@@ -36,7 +36,7 @@
[`list`](https://markata.dev/markata/plugins/base_cli/#list-function)
section for more examples.
-## Creating "new" things with the cli
+# Creating "new" things with the cli
The `new` cli is built on copier templates, and allows you to build a new blog
from a starter repo, make new posts, and new plugins. Before you start dumping
@@ -83,7 +83,9 @@
import shutil
import sys
import traceback
-from typing import Callable, Optional, TYPE_CHECKING
+import toml
+import json
+from typing import Callable, Literal, Optional, TYPE_CHECKING
import warnings
from rich import print as rich_print
@@ -126,7 +128,9 @@ def cli(app: typer.Typer, markata: "Markata") -> None:
"""
plugins_app = typer.Typer()
+ config_app = typer.Typer()
app.add_typer(plugins_app)
+ app.add_typer(config_app)
@app.command()
def tui(ctx: typer.Context) -> None:
@@ -146,9 +150,54 @@ def tui(ctx: typer.Context) -> None:
def plugins():
"create new things from templates"
- @plugins_app.command()
- def show() -> None:
- rich_print(markata.plugins)
+ @config_app.callback()
+ def config():
+ "configuration management"
+
+ @config_app.command()
+ def show(
+ verbose: bool = typer.Option(
+ False,
+ "--verbose",
+ "-v",
+ ),
+ ) -> None:
+ if verbose:
+ markata.console.quiet = False
+ else:
+ markata.console.quiet = True
+ rich_print(markata.config)
+
+ @config_app.command()
+ def generate(
+ verbose: bool = typer.Option(
+ False,
+ "--verbose",
+ "-v",
+ ),
+ ) -> None:
+ if verbose:
+ markata.console.quiet = False
+ else:
+ markata.console.quiet = True
+
+ rich_print(toml.dumps(json.loads(markata.config.json())))
+
+ @config_app.command()
+ def get(key: str) -> None:
+ keys = key.split(".")
+ markata.console.quiet = True
+ keys_processed = ""
+ value = markata.config
+ na = Literal["na"]
+ for key in keys:
+ value = getattr(value, key, na)
+ keys_processed = f"{keys_processed}.{key}".strip(".")
+ if value is na:
+ rich_print(f"{keys_processed} not found")
+ sys.exit(1)
+
+ rich_print(value)
new_app = typer.Typer()
app.add_typer(new_app)
@@ -160,8 +209,9 @@ def new():
@new_app.command()
def blog(
directory: Path = typer.Argument(
- ..., help="The directory to create the blog in."
- )
+ ...,
+ help="The directory to create the blog in.",
+ ),
) -> None:
"""
Create a new blog from using the template from
@@ -172,7 +222,8 @@ def blog(
typer.echo(f"creating a new project in {directory.absolute()}")
url = markata.config.get("starters", {}).get(
- "blog", "git+https://github.com/WaylonWalker/markata-blog-starter"
+ "blog",
+ "git+https://github.com/WaylonWalker/markata-blog-starter",
)
run_copy(url, directory)
@@ -188,7 +239,8 @@ def post() -> None:
typer.echo(f"creating a new post in {Path().absolute()}/posts")
url = markata.config.get("starters", {}).get(
- "post", "git+https://github.com/WaylonWalker/markata-post-template"
+ "post",
+ "git+https://github.com/WaylonWalker/markata-post-template",
)
run_copy(url, Path("."))
@@ -202,10 +254,11 @@ def plugin() -> None:
typer.echo(
f"creating a new plugin in {Path().absolute()}"
- f"//plugins"
+ f"//plugins",
)
url = markata.config.get("starters", {}).get(
- "post", "git+https://github.com/WaylonWalker/markata-plugin-template"
+ "post",
+ "git+https://github.com/WaylonWalker/markata-plugin-template",
)
run_copy(url, Path("."))
@@ -308,7 +361,7 @@ def list(
posts on it. It makes slicing in by `templatekey`, `tag`, or
`date` much easier.
- ### default list
+ # default list
By default `markata list` will list all titles in a pager, for all posts
being loaded by markata.
@@ -317,7 +370,7 @@ def list(
markata list
```
- ### Skip the pager
+ # Skip the pager
Markata uses rich for its pager, it's pretty smart about when to
use the pager or pass text to the next thing in the pipeline,
@@ -327,7 +380,7 @@ def list(
markata list --no-pager
```
- ### List other attributes
+ # List other attributes
You can list any other attribute tied to your posts. These are
added through either your yaml frontmatter at the start of your
@@ -348,7 +401,7 @@ def list(
markata list --map content
```
- ### List more than one attribute
+ # List more than one attribute
You can create new attributes as you map to echo out by
combining existing attributes.
@@ -357,7 +410,7 @@ def list(
markata list --map 'title + " , " + slug'
```
- ### Using Python objects as map
+ # Using Python objects as map
You can access attributes of each post attribute that you map
over. For instance on my blog, each post has a date that is a
@@ -370,7 +423,7 @@ def list(
markata list --map 'str(date.year) + "," + title'
```
- ### Filtering posts
+ # Filtering posts
Posts are filtered with python syntax, you will have all
attributes tied to your posts available to filter with.
@@ -379,7 +432,7 @@ def list(
markata list --filter "'__' not in title"
```
- ### Filtering by dates
+ # Filtering by dates
If your site has dates tied to your posts you can filter by
date. On my blog this makes a ton of sense and is quite useful.
@@ -394,7 +447,7 @@ def list(
markata list --filter "date.year==today.year"
```
- ### Full Content Search
+ # Full Content Search
You can also search the full content of each post for specific
words.
@@ -403,7 +456,7 @@ def list(
markata list --filter "'python' in content"
```
- ### Filtering by frontmatter data
+ # Filtering by frontmatter data
I use a templateKey on my personal blog to determine which
template to render the page with. I can fitler my posts by a
@@ -413,7 +466,7 @@ def list(
markata list --filter "templateKey=='til'"
```
- ### Combining filters
+ # Combining filters
Filters can be combined together quite like maps can, it's all
just python syntax.
@@ -422,7 +475,7 @@ def list(
markata list --filter "templateKey=='til' and date == today"
```
- ### Sorting posts
+ # Sorting posts
Posts can be sorted by attributes on your post, and they can
even be reversed.
@@ -432,14 +485,14 @@ def list(
markta list --sort date --reverse
```
- ### Putting it all together
+ # Putting it all together
The real power of all this comes when you combine them all into
lists that work for you and your workflow. This really makes
working on larger projects so much easier to find things.
- ### Making a fuzzy picker for your posts
+ # Making a fuzzy picker for your posts
Here is a bash command to open an fzf picker for todays posts,
then open it in your `$EDITOR`
@@ -454,7 +507,7 @@ def list(
xargs -I {} $EDITOR {}
```
- ### Combining wtih nvim Telescope
+ # Combining wtih nvim Telescope
Here is the same command setup as a Telescope picker for neovim.
@@ -502,7 +555,7 @@ def clean(
Cleans up output generated by markata including both the output_dir and
the .markata_cache.
- ### Dry Run
+ # Dry Run
You can run with `--dry-run` to see what markata is about to do.
@@ -513,7 +566,7 @@ def clean(
```
- ### Running clean
+ # Running clean
Running markata clean will fully delete all of the directories created
by markata.
@@ -524,7 +577,7 @@ def clean(
removing cache directory: .markata.cache base_cli.py:405
```
- ### Running Quietly
+ # Running Quietly
Running with `--quiet` will remove all of the directories created by
markata without announcing what it is doing.
@@ -542,18 +595,18 @@ def _clean(markata, quiet: bool = False, dry_run: bool = False):
markata.console.log(
f'{"[DRYRUN]" if dry_run else ""}'
- f'removing outptut directory: {markata.config.get("output_dir")}'
+ f"removing outptut directory: {markata.config.output_dir}",
)
if not dry_run:
try:
- shutil.rmtree(str(markata.config.get("output_dir")))
+ shutil.rmtree(str(markata.config.output_dir))
except FileNotFoundError:
warnings.warn(
- f'output directory: {markata.config.get("output_dir")} does not exist'
+ f"output directory: {markata.config.output_dir} does not exist",
)
markata.console.log(
- f'{"[DRYRUN]" if dry_run else ""} removing cache directory: .markata.cache'
+ f'{"[DRYRUN]" if dry_run else ""} removing cache directory: .markata.cache',
)
if not dry_run:
try:
diff --git a/markata/plugins/config_model.py b/markata/plugins/config_model.py
new file mode 100644
index 00000000..28d72038
--- /dev/null
+++ b/markata/plugins/config_model.py
@@ -0,0 +1,101 @@
+from pathlib import Path
+from typing import Optional, TYPE_CHECKING
+
+from polyfactory.factories.pydantic_factory import ModelFactory
+import pydantic
+from pydantic import ConfigDict, AnyUrl, PositiveInt
+from pydantic_settings import BaseSettings
+
+from markata import standard_config
+from markata.hookspec import hook_impl, register_attr
+from pydantic_extra_types.color import Color
+
+if TYPE_CHECKING:
+ from markata import Markata
+
+
+class Config(BaseSettings):
+ hooks: list[str] = ["default"]
+ disabled_hooks: list[str] = []
+ markdown_extensions: list[str] = []
+ default_cache_expire: PositiveInt = 3600
+ output_dir: pydantic.DirectoryPath = Path("markout")
+ assets_dir: Path = pydantic.Field(
+ Path("static"),
+ description="The directory to store static assets",
+ )
+ nav: dict[str, str] = {"home": "/"}
+ site_version: int = 1
+ markdown_backend: str = "markdown-it-py"
+ url: Optional[AnyUrl] = None
+ title: Optional[str] = "Markata Site"
+ description: Optional[str] = None
+ rss_description: Optional[str] = None
+ author_name: Optional[str] = None
+ author_email: Optional[str] = None
+ lang: str = "en"
+ repo_url: Optional[AnyUrl] = None
+ repo_branch: str = "main"
+ theme_color: Color = "#322D39"
+ background_color: Color = "#B73CF6"
+ start_url: str = "/"
+ site_name: Optional[str] = None
+ short_name: Optional[str] = None
+ display: str = "minimal-ui"
+ twitter_card: str = "summary_large_image"
+ twitter_creator: Optional[str] = None
+ twitter_site: Optional[str] = None
+ path_prefix: Optional[str] = ""
+ model_config = ConfigDict(env_prefix="markata_", extra="allow")
+
+ def __getitem__(self, item):
+ "for backwards compatability"
+ return getattr(self, item)
+
+ def __setitem__(self, key, item):
+ "for backwards compatability"
+ return setattr(self, key, item)
+
+ def get(self, item, default):
+ "for backwards compatability"
+ return getattr(self, item, default)
+
+ def keys(self):
+ "for backwards compatability"
+ return self.__dict__.keys()
+
+ def toml(self: "Config") -> str:
+ import tomlkit
+
+ doc = tomlkit.document()
+
+ for key, value in self.dict().items():
+ doc.add(key, value)
+ doc.add(tomlkit.comment(key))
+ if value:
+ doc[key] = value
+ return tomlkit.dumps(doc)
+
+
+# def add_doc(doc: pydantic.Document) -> None:
+
+
+@hook_impl
+@register_attr("post_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
+@hook_impl(tryfirst=True)
+@register_attr("config")
+def load_config(markata: "Markata") -> None:
+ if "config" not in markata.__dict__.keys():
+ config = standard_config.load("markata")
+ if config == {}:
+ markata.config = markata.Config()
+ else:
+ markata.config = markata.Config.parse_obj(config)
+
+
+class ConfigFactory(ModelFactory):
+ __model__ = Config
diff --git a/markata/plugins/copy_assets.py b/markata/plugins/copy_assets.py
index 0d87c800..1483d1dc 100644
--- a/markata/plugins/copy_assets.py
+++ b/markata/plugins/copy_assets.py
@@ -1,5 +1,4 @@
import shutil
-from pathlib import Path
from typing import TYPE_CHECKING
from markata.hookspec import hook_impl
@@ -10,9 +9,10 @@
@hook_impl
def save(markata: "Markata") -> None:
- output_dir = Path(str(markata.config.get("output_dir", "markout")))
- assets_dir = Path(str(markata.config.get("assets_dir", "static")))
-
with markata.console.status("copying assets", spinner="aesthetic", speed=0.2):
- if assets_dir.exists():
- shutil.copytree(assets_dir, output_dir, dirs_exist_ok=True)
+ if markata.config.assets_dir.exists():
+ shutil.copytree(
+ markata.config.assets_dir,
+ markata.config.output_dir,
+ dirs_exist_ok=True,
+ )
diff --git a/markata/plugins/covers.py b/markata/plugins/covers.py
index a0369757..0ca79e53 100644
--- a/markata/plugins/covers.py
+++ b/markata/plugins/covers.py
@@ -30,10 +30,10 @@
text_padding = [0,0]
```
"""
-import time
from functools import lru_cache
from pathlib import Path
-from typing import TYPE_CHECKING, List, Optional, Tuple, Union
+import time
+from typing import List, Optional, TYPE_CHECKING, Tuple, Union
from PIL import Image, ImageDraw, ImageFont
from rich.progress import BarColumn, Progress
@@ -93,10 +93,7 @@ def draw_text(
bounding_box = [padding[0], padding[1], width - padding[2], height - padding[3]]
max_size = (bounding_box[2] - bounding_box[0], bounding_box[3] - bounding_box[1])
x1, y1, x2, y2 = bounding_box
- if font_path:
- font = get_font(font_path, draw, text, max_size=max_size)
- else:
- font = None
+ font = get_font(font_path, draw, text, max_size=max_size) if font_path else None
w, h = draw.textsize(text, font=font)
x = (x2 - x1 - w) / 2 + x1
y = (y2 - y1 - h) / 2 + y1
@@ -133,10 +130,7 @@ def make_cover(
) -> None:
if output_path.exists():
return
- if template_path:
- image = Image.open(template_path)
- else:
- image = Image.new("RGB", (800, 450))
+ image = Image.open(template_path) if template_path else Image.new("RGB", (800, 450))
draw_text(
image=image,
@@ -155,7 +149,6 @@ def make_cover(
)
draw_text(image, text_font, text, text_font_color, text_padding)
- output_path.parent.mkdir(exist_ok=True)
image.save(output_path)
ratio = image.size[1] / image.size[0]
@@ -223,7 +216,7 @@ def save(markata: "Markata") -> None:
make_cover(
title=title,
color=cover["font_color"],
- output_path=Path(markata.config["output_dir"])
+ output_path=Path(markata.config.output_dir)
/ (article["slug"] + cover["name"] + ".png"),
template_path=cover.get("template", None),
font_path=cover.get("font", None),
@@ -234,16 +227,18 @@ def save(markata: "Markata") -> None:
text_padding=text_padding,
resizes=cover.get("resizes"),
markata=markata,
- )
+ ),
)
progress = Progress(
- BarColumn(bar_width=None), transient=True, console=markata.console
+ BarColumn(bar_width=None),
+ transient=True,
+ console=markata.console,
)
task_id = progress.add_task("loading markdown")
progress.update(task_id, total=len(futures))
with progress:
- while not all([f.done() for f in futures]):
+ while not all(f.done() for f in futures):
time.sleep(0.1)
progress.update(task_id, total=len([f for f in futures if f.done()]))
[f.result() for f in futures]
diff --git a/markata/plugins/create_covers.py b/markata/plugins/create_covers.py
index 6d35318f..d83397c6 100644
--- a/markata/plugins/create_covers.py
+++ b/markata/plugins/create_covers.py
@@ -11,7 +11,10 @@
def get_font(
- path: Path, draw: ImageDraw.Draw, title: str, size: int = 250
+ path: Path,
+ draw: ImageDraw.Draw,
+ title: str,
+ size: int = 250,
) -> ImageFont.FreeTypeFont:
font = ImageFont.truetype(path, size=size)
if draw.textsize(title, font=font)[0] > 800:
@@ -21,7 +24,11 @@ def get_font(
@background.task
def make_cover(
- title: str, color: str, output_path: Path, template_path: Path, font_path: Path
+ title: str,
+ color: str,
+ output_path: Path,
+ template_path: Path,
+ font_path: Path,
) -> None:
image = Image.open(template_path)
diff --git a/markata/plugins/create_models.py b/markata/plugins/create_models.py
new file mode 100644
index 00000000..5766e60b
--- /dev/null
+++ b/markata/plugins/create_models.py
@@ -0,0 +1,39 @@
+from typing import TYPE_CHECKING, List
+
+from more_itertools import unique_everseen
+from pydantic import create_model
+
+from markata.hookspec import hook_impl, register_attr
+
+if TYPE_CHECKING:
+ from markata import Markata
+
+
+class Config:
+ env_prefix = "markata_"
+ extras = "allow"
+
+
+class PostConfig:
+ title = "Markata.Post"
+ arbitrary_types_allowed = True
+ copy_on_model_validation = False
+
+
+@hook_impl
+@register_attr("Post", "Posts", "Config")
+def create_models(markata: "Markata") -> None:
+ post_models = tuple(unique_everseen(markata.post_models))
+ markata.Post = create_model(
+ "Post",
+ __base__=post_models,
+ )
+ markata.Posts = create_model(
+ "Posts",
+ posts=(List[markata.Post], ...),
+ )
+ markata.Post.markata = markata
+ markata.Config = create_model(
+ "Config",
+ __base__=tuple(unique_everseen(markata.config_models)),
+ )
diff --git a/markata/plugins/datetime.py b/markata/plugins/datetime.py
index 7b73f661..71d384f3 100644
--- a/markata/plugins/datetime.py
+++ b/markata/plugins/datetime.py
@@ -2,7 +2,7 @@
import datetime
from typing import TYPE_CHECKING
-import dateutil.parser
+import dateutil
import pytz
from markata.hookspec import hook_impl
@@ -31,5 +31,5 @@ def load(markata: "Markata") -> None:
article["today"] = datetime.date.today()
article["now"] = datetime.datetime.now()
article["datetime"] = date
- if date is not None:
- article["date"] = date.date()
+ # if date is not None:
+ # article["date"] = date.date()
diff --git a/markata/plugins/default_doc_template.md b/markata/plugins/default_doc_template.md
index 4a8a6f0a..df59e581 100644
--- a/markata/plugins/default_doc_template.md
+++ b/markata/plugins/default_doc_template.md
@@ -1,23 +1,13 @@
----
-title: {{file.name}}
-published: True
-slug: {{slug}}
-edit_link: {{edit_link}}
-path: {{file.stem}}.md
-today: {{datetime.datetime.today()}}
-description: Docs for {{file.stem}}
-
----
-
{{ ast.get_docstring(tree) }}
{% for node in nodes %}
-!!! {{node.type}} {{node.name}} {{node.type}}
+!! {{node.type}} {{node.name}} {{node.type}}
{{ indent(ast.get_docstring(node) or '', ' ') }}
- ???+ source "{{node.name}} source "
- ``` python
+???+ source "{{node.name}} source "
+
+```python
{{ indent(ast.get_source_segment(raw_source, node), ' ') }}
- ```
+```
{% endfor %}
diff --git a/markata/plugins/default_feed_style.xsl b/markata/plugins/default_feed_style.xsl
new file mode 100644
index 00000000..ba6dfd68
--- /dev/null
+++ b/markata/plugins/default_feed_style.xsl
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+ Web Feed
+
+
+
+
+
+
+
+ This is a web feed, also known as an RSS feed. Subscribe by copying the URL from the address bar into your newsreader.
+
+
+ Visit About Feeds to get started with newsreaders and subscribing. It’s free.
+
+
+
+
+
+ Recent Items
+
+
+
+
+
+
+
+ Published:
+
+
+
+
+
+
+
+
+
+
diff --git a/markata/plugins/default_post_template.html b/markata/plugins/default_post_template.html
index 324f9a66..419b582d 100644
--- a/markata/plugins/default_post_template.html
+++ b/markata/plugins/default_post_template.html
@@ -1,776 +1,1083 @@
+
- {{ title }}
-
-
- {% if description %}
-
- {% endif %}
- {% for attr in config.get("seo", []) %}
- {% endfor %}
-
-
- {% if config.get('icon') %}
-
- {% endif %}
-
-
+
-
- {% if 'markata.plugins.service_worker' in config.hooks %}
+
+ a.help svg {
+ height: 24px;
+ width: 24px;
+ }
+
+ .highlight {
+ background: var(--color-bg-code);
+ color: var(--color-text);
+ filter: brightness(var(--overlay-brightness));
+ border-radius: 0 0 4px 4px;
+ }
+
+ .highlight .c {
+ color: #8b8b8b
+ }
+
+ /* Comment */
+ .highlight .err {
+ color: #960050;
+ background-color: #1e0010
+ }
+
+ /* Error */
+ .highlight .k {
+ color: #c678dd
+ }
+
+ /* Keyword */
+ .highlight .l {
+ color: #ae81ff
+ }
+
+ /* Literal */
+ .highlight .n {
+ color: #abb2bf
+ }
+
+ /* Name */
+ .highlight .o {
+ color: #c678dd
+ }
+
+ /* Operator */
+ .highlight .p {
+ color: #abb2bf
+ }
+
+ /* Punctuation */
+ .highlight .ch {
+ color: #8b8b8b
+ }
+
+ /* Comment.Hashbang */
+ .highlight .cm {
+ color: #8b8b8b
+ }
+
+ /* Comment.Multiline */
+ .highlight .cp {
+ color: #8b8b8b
+ }
+
+ /* Comment.Preproc */
+ .highlight .cpf {
+ color: #8b8b8b
+ }
+
+ /* Comment.PreprocFile */
+ .highlight .c1 {
+ color: #8b8b8b
+ }
+
+ /* Comment.Single */
+ .highlight .cs {
+ color: #8b8b8b
+ }
+
+ /* Comment.Special */
+ .highlight .gd {
+ color: #c678dd
+ }
+
+ /* Generic.Deleted */
+ .highlight .ge {
+ font-style: italic
+ }
+
+ /* Generic.Emph */
+ .highlight .gi {
+ color: #a6e22e
+ }
+
+ /* Generic.Inserted */
+ .highlight .gs {
+ font-weight: bold
+ }
+
+ /* Generic.Strong */
+ .highlight .gu {
+ color: #8b8b8b
+ }
+
+ /* Generic.Subheading */
+ .highlight .kc {
+ color: #c678dd
+ }
+
+ /* Keyword.Constant */
+ .highlight .kd {
+ color: #c678dd
+ }
+
+ /* Keyword.Declaration */
+ .highlight .kn {
+ color: #c678dd
+ }
+
+ /* Keyword.Namespace */
+ .highlight .kp {
+ color: #c678dd
+ }
+
+ /* Keyword.Pseudo */
+ .highlight .kr {
+ color: #c678dd
+ }
+
+ /* Keyword.Reserved */
+ .highlight .kt {
+ color: #c678dd
+ }
+
+ /* Keyword.Type */
+ .highlight .ld {
+ color: #e6db74
+ }
+
+ /* Literal.Date */
+ .highlight .m {
+ color: #ae81ff
+ }
+
+ /* Literal.Number */
+ .highlight .s {
+ color: #e6db74
+ }
+
+ /* Literal.String */
+ .highlight .na {
+ color: #a6e22e
+ }
+
+ /* Name.Attribute */
+ .highlight .nb {
+ color: #98c379
+ }
+
+ /* Name.Builtin */
+ .highlight .nc {
+ color: #abb2bf
+ }
+
+ /* Name.Class */
+ .highlight .no {
+ color: #c678dd
+ }
+
+ /* Name.Constant */
+ .highlight .nd {
+ color: #abb2bf
+ }
+
+ /* Name.Decorator */
+ .highlight .ni {
+ color: #abb2bf
+ }
+
+ /* Name.Entity */
+ .highlight .ne {
+ color: #a6e22e
+ }
+
+ /* Name.Exception */
+ .highlight .nf {
+ color: #61afef
+ }
+
+ /* Name.Function */
+ .highlight .nl {
+ color: #abb2bf
+ }
+
+ /* Name.Label */
+ .highlight .nn {
+ color: #abb2bf
+ }
+
+ /* Name.Namespace */
+ .highlight .nx {
+ color: #a6e22e
+ }
+
+ /* Name.Other */
+ .highlight .py {
+ color: #abb2bf
+ }
+
+ /* Name.Property */
+ .highlight .nt {
+ color: #c678dd
+ }
+
+ /* Name.Tag */
+ .highlight .nv {
+ color: #abb2bf
+ }
+
+ /* Name.Variable */
+ .highlight .ow {
+ color: #c678dd
+ }
+
+ /* Operator.Word */
+ .highlight .w {
+ color: #abb2bf
+ }
+
+ /* Text.Whitespace */
+ .highlight .mb {
+ color: #ae81ff
+ }
+
+ /* Literal.Number.Bin */
+ .highlight .mf {
+ color: #ae81ff
+ }
+
+ /* Literal.Number.Float */
+ .highlight .mh {
+ color: #ae81ff
+ }
+
+ /* Literal.Number.Hex */
+ .highlight .mi {
+ color: #ae81ff
+ }
+
+ /* Literal.Number.Integer */
+ .highlight .mo {
+ color: #ae81ff
+ }
+
+ /* Literal.Number.Oct */
+ .highlight .sa {
+ color: #e6db74
+ }
+
+ /* Literal.String.Affix */
+ .highlight .sb {
+ color: #e6db74
+ }
+
+ /* Literal.String.Backtick */
+ .highlight .sc {
+ color: #e6db74
+ }
+
+ /* Literal.String.Char */
+ .highlight .dl {
+ color: #e6db74
+ }
+
+ /* Literal.String.Delimiter */
+ .highlight .sd {
+ color: #98c379
+ }
+
+ /* Literal.String.Doc */
+ .highlight .s2 {
+ color: #98c379
+ }
+
+ /* Literal.String.Double */
+ .highlight .se {
+ color: #ae81ff
+ }
+
+ /* Literal.String.Escape */
+ .highlight .sh {
+ color: #e6db74
+ }
+
+ /* Literal.String.Heredoc */
+ .highlight .si {
+ color: #e6db74
+ }
+
+ /* Literal.String.Interpol */
+ .highlight .sx {
+ color: #e6db74
+ }
+
+ /* Literal.String.Other */
+ .highlight .sr {
+ color: #e6db74
+ }
+
+ /* Literal.String.Regex */
+ .highlight .s1 {
+ color: #e6db74
+ }
+
+ /* Literal.String.Single */
+ .highlight .ss {
+ color: #e6db74
+ }
+
+ /* Literal.String.Symbol */
+ .highlight .bp {
+ color: #abb2bf
+ }
+
+ /* Name.Builtin.Pseudo */
+ .highlight .fm {
+ color: #61afef
+ }
+
+ /* Name.Function.Magic */
+ .highlight .vc {
+ color: #abb2bf
+ }
+
+ /* Name.Variable.Class */
+ .highlight .vg {
+ color: #abb2bf
+ }
+
+ /* Name.Variable.Global */
+ .highlight .vi {
+ color: #abb2bf
+ }
+
+ /* Name.Variable.Instance */
+ .highlight .vm {
+ color: #abb2bf
+ }
+
+ /* Name.Variable.Magic */
+ .highlight .il {
+ color: #ae81ff
+ }
+
+ /* Literal.Number.Integer.Long */
+
+ /* Tab style starts here */
+ .tabbed-set {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ margin: 1em 0;
+ border-radius: 0.1rem;
+ }
+
+ .tabbed-set>input {
+ display: none;
+ }
+
+ .tabbed-set label {
+ width: auto;
+ padding: 0.9375em 1.25em 0.78125em;
+ font-weight: 700;
+ font-size: 0.84em;
+ white-space: nowrap;
+ border-bottom: 0.15rem solid transparent;
+ border-top-left-radius: 0.1rem;
+ border-top-right-radius: 0.1rem;
+ cursor: pointer;
+ transition: background-color 250ms, color 250ms;
+ }
+
+ .tabbed-set .tabbed-content {
+ width: 100%;
+ display: none;
+ box-shadow: 0 -.05rem #ddd;
+ }
+
+ .tabbed-set input {
+ position: absolute;
+ opacity: 0;
+ }
+
+ /* fonts */
+ h1 {
+ font-weight: 700;
+ }
+
+ h1#title a {
+ font-size: 16px;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ margin-top: 3rem;
+ }
+
+ h1 {
+ font-size: 2.5em;
+ margin-top: 5rem;
+ }
+
+ h2 {
+ font-size: 1.63rem;
+ margin-top: 5rem;
+ }
+
+
+
+ p {
+ font-size: 21px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ }
+
+ @media only screen and (max-width: 700px) {
+ p {
+ font-size: 18px;
+ }
+ }
+
+ @media only screen and (max-width: 600px) {
+ p {
+ font-size: 16px;
+ }
+ }
+
+ @media only screen and (max-width: 500px) {
+ p {
+ font-size: 14px;
+ }
+ }
+
+ @media only screen and (max-width: 400px) {
+ p {
+ font-size: 12px;
+ }
+ }
+
+
+ pre {
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 400;
+ line-height: 18.5714px;
+ */
+ }
+
+ a {
+ font-weight: 600;
+ text-decoration-color: var(--color-accent);
+ color: var(--color-link);
+ padding: .3rem .5rem;
+ display: inline-block;
+ }
+
+ .admonition,
+ details {
+ box-shadow: 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ margin: 5rem 0;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ text-align: left;
+ padding: 0;
+ border: 0;
+
+ }
+
+ .admonition {
+ padding-bottom: 1rem;
+ }
+
+ details[open] {
+ padding-bottom: .5rem;
+ }
+
+ .admonition p {
+ padding: .2rem .6rem;
+ }
+
+ .admonition-title,
+ .details-title,
+ summary {
+ background: var(--color-bg-2);
+ padding: 0;
+ margin: 0;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ }
+
+ summary:hover {
+ cursor: pointer;
+ }
+
+ summary.admonition-title,
+ summary.details-title {
+ padding: .5rem;
+ padding-left: 1rem;
+ }
+
+ .note {
+ border-left: 4px solid #f1fa8c;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #f1fa8c,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .note>.admonition-title {
+ border-bottom: 1px solid #3c3d2d;
+ }
+
+ .abstract {
+ border-left: 4px solid #8be9fd;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #8be9fd,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .abstract>.admonition-title {
+ border-bottom: 1px solid #2c3a3f;
+ }
+
+ .info {
+ border-left: 4px solid;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #8bb0fd,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .info>.admonition-title {
+ border-bottom: 1px solid #2c313f;
+ }
+
+ .tip {
+ border-left: 4px solid #008080;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #008080,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .tip>.admonition-title {
+ border-bottom: 1px solid #1b2a2b;
+ }
+
+ .success {
+ border-left: 4px solid #50fa7b;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #50fa7b,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .success>.admonition-title {
+ border-bottom: 1px solid #263e2b;
+ }
+
+ .question {
+ border-left: 4px solid #a7fcbd;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #a7fcbd,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .question>.admonition-title {
+ border-bottom: 1px solid #303e35;
+ }
+
+ .warning {
+ border-left: 4px solid #ffb86c;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #ffb86c,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .warning>.admonition-title {
+ border-bottom: 1px solid #3f3328;
+ }
+
+ .failure {
+ border-left: 4px solid #b23b3b;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #b23b3b,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .failure>.admonition-title {
+ border-bottom: 1px solid #34201f;
+ }
+
+ .danger {
+ border-left: 4px solid #ff5555;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #ff5555,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .danger>.admonition-title {
+ border-bottom: 1px solid #402523;
+ }
+
+ .bug {
+ border-left: 4px solid #b2548a;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #b2548a,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .bug>.admonition-title {
+ border-bottom: 1px solid #32232c;
+ }
+
+ .example {
+ border-left: 4px solid #bd93f9;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #bd93f9,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .example>.admonition-title {
+ border-bottom: 1px solid #332d3e;
+ }
+
+ .source {
+ border-left: 4px solid #bd93f9;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #bd93f9,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .source>.admonition-title {
+ border-bottom: 1px solid #332d3e;
+ }
+
+ .quote {
+ border-left: 4px solid #999;
+ box-shadow:
+ -0.8rem 0rem 1rem -1rem #999,
+ 0.2rem 0rem 1rem rgb(0, 0, 0, .4);
+ }
+
+ .quote>.admonition-title {
+ border-bottom: 1px solid #2d2e2f;
+ }
+
+ table {
+ margin: 1rem 0;
+ border-collapse: collapse;
+ border-spacing: 0;
+ display: block;
+ max-width: -moz-fit-content;
+ max-width: fit-content;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ table thead th {
+ border: solid 1px var(--color-text);
+ padding: 10px;
+ text-align: left;
+ }
+
+ table tbody td {
+ border: solid 1px var(--color-text);
+ padding: 10px;
+ }
+
+ .theme-switch {
+ z-index: 10;
+ display: inline-block;
+ height: 34px;
+ position: relative;
+ width: 60px;
+
+ display: flex;
+ justify-content: flex-end;
+ margin-right: 1rem;
+ margin-left: auto;
+ position: fixed;
+ right: 1rem;
+ top: 1rem;
+ }
+
+ .theme-switch input {
+ display: none;
+
+ }
+
+ .slider {
+ background-color: #ccc;
+ bottom: 0;
+ cursor: pointer;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: .4s;
+ }
+
+ .slider:before {
+ background-color: #fff;
+ bottom: 4px;
+ content: "";
+ height: 26px;
+ left: 4px;
+ position: absolute;
+ transition: .4s;
+ width: 26px;
+ }
+
+ input:checked+.slider {
+ background-color: #343434;
+ }
+
+ input:checked+.slider:before {
+ background-color: #848484;
+ }
+
+ input:checked+.slider:before {
+ transform: translateX(26px);
+ }
+
+ .slider.round {
+ border-radius: 34px;
+ }
+
+ .slider.round:before {
+ border-radius: 50%;
+ }
+
+ main p img {
+ width: 100%;
+ width: -moz-available;
+ width: -webkit-fill-available;
+ width: fill-available;
+ }
+
+ details>* {
+ margin: 1rem;
+ }
+
+ .admonition>* {
+ margin: 1rem;
+ }
+
+ p.admonition-title,
+ summary {
+ margin: 0;
+ padding-left: 1.2rem;
+ }
+
+ .small {
+ font-size: .9rem;
+ color: #888;
+ }
+
+ admonition+admonition {
+ margin-top: 20rem;
+ }
+
+ ::-webkit-scrollbar {
+ height: 12px;
+ background-color: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background-color: #d3d3d32e;
+ border-radius: 6px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+
+ {% if 'markata.plugins.service_worker' in config.hooks %}
- {% endif %}
-
- {{ config.get('head', {}).pop('text') if 'text' in config.get('head',{}).keys() }}{% for tag, meta in config.get('head', {}).items() %}{% for _meta in meta %}
- <{{ tag }} {% for attr, value in _meta.items() %}{{ attr }}="{{ value }}"{% endfor %}/> {% endfor %}{% endfor %}
+ {% endif %} {% for text in config.head.text %} {{ text.value }}{% endfor %}
+ {% for meta in config.head.meta %}{% for _meta in meta %}
+
+ {% endfor %}{% endfor %}
-
- {% for text, link in config.get('nav', {}).items() %}
- {{text}}
+
+ {% for text, link in config.nav.items() %}
+ {{text}}
{% endfor %}
-
+
-
-
+ {{ body }}
+
+
diff --git a/markata/plugins/default_post_template.html.jinja b/markata/plugins/default_post_template.html.jinja
new file mode 100644
index 00000000..fd58f974
--- /dev/null
+++ b/markata/plugins/default_post_template.html.jinja
@@ -0,0 +1,1084 @@
+
+
+
+
+ {% if post.title or config.title %}
+ {{ post.title or config.title }}
+ {% endif %}
+
+
+ {% if post.description or config.description %}
+
+ {% endif %} {% if config.icon %}
+
+ {% endif %}
+
+
+
+
+ {% if 'markata.plugins.service_worker' in config.hooks %}
+
+ {% endif %} {% for text in config.head.text %} {{ text.value }}{% endfor %}
+ {% for meta in config.head.meta %}{% for _meta in meta %}
+
+ {% endfor %}{% endfor %}
+
+
+
+ {% for text, link in config.nav.items() %}
+ {{text}}
+ {% endfor %}
+
+
+
+
+
+ {{ body }}
+
+
+
+
diff --git a/markata/plugins/default_rss_template.xml b/markata/plugins/default_rss_template.xml
new file mode 100644
index 00000000..b0348374
--- /dev/null
+++ b/markata/plugins/default_rss_template.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ {{ feed.config.name | e }}
+ {{ markata.config.url | e }}
+ {{ markata.config.description | e }}
+ Markata
+
+ {{ markata.config.icon | e }}
+ {{ markata.config.url | e }}
+
+ {{ markata.config.lang | e }}
+{% for post in feed.posts %}
+ -
+
{{ post.title | e }}
+ {{ markata.config.url | e }}/{{ markata.config.path_prefix | e }}{{ post.slug | e }}
+ {{ post.description | e }}
+ {{ markata.config.url | e }}/{{ markata.config.path_prefix | e }}{{ post.slug | e }}
+ {{ post.date | e }}
+ {{ markata.config.author_name | e }}
+
+{% endfor %}
+{{ post.today | e }}
+
+
diff --git a/markata/plugins/default_sitemap_template.xml b/markata/plugins/default_sitemap_template.xml
new file mode 100644
index 00000000..bf1f3b69
--- /dev/null
+++ b/markata/plugins/default_sitemap_template.xml
@@ -0,0 +1,12 @@
+
+
+
+{% for post in feed.posts %}
+
+ {{ markata.config.url | e }}/{{ markata.config.path_prefix | e }}{{ post.slug | e }}
+ {{ post.date | e }}
+ daily
+ 0.8
+
+{% endfor %}
+
diff --git a/markata/plugins/default_xsl_template.xsl b/markata/plugins/default_xsl_template.xsl
new file mode 100644
index 00000000..d81d6473
--- /dev/null
+++ b/markata/plugins/default_xsl_template.xsl
@@ -0,0 +1,1177 @@
+
+
+
+
+
+
+
+
+ {% if post.title or config.title %}
+ {{ post.title or config.title }}
+ {% endif %}
+
+
+ {% if post.description or config.description %}
+
+ {% endif %} {% if config.icon %}
+
+ {% endif %}
+
+
+
+
+ {% if 'markata.plugins.service_worker' in config.hooks %}
+
+ {% endif %} {% for text in config.head.text %} {{ text.value }}{% endfor %}
+ {% for meta in config.head.meta %}{% for _meta in meta %}
+
+ {% endfor %}{% endfor %}
+
+
+
+
+ This is a web feed, also known as an RSS feed. Subscribe by copying the URL from the address bar into your newsreader.
+
+ {% for text, link in config.nav.items() %}
+
+ {{text}}
+
+ {% endfor %}
+
+
+
+
+
+
+ {{ title }} {% if config.get %}
+
+
+ {% endif %}
+
+
+
+
+
+
+ {{ body }}
+
+
+
+
+
+
+
diff --git a/markata/plugins/docs.py b/markata/plugins/docs.py
index 7d5d5842..78eb7257 100644
--- a/markata/plugins/docs.py
+++ b/markata/plugins/docs.py
@@ -3,12 +3,15 @@
"""
import ast
import datetime
+from functools import lru_cache
+from os import path
from pathlib import Path
import textwrap
from typing import List, TYPE_CHECKING
import frontmatter
import jinja2
+import pydantic
from markata.hookspec import hook_impl, register_attr
@@ -47,16 +50,18 @@ def glob(markata: "MarkataDocs") -> None:
"""
- markata.py_files = list(Path().glob("**/*.py"))
+ import glob
- content_directories = list(set([f.parent for f in markata.py_files]))
- if "content_directories" in markata.__dict__.keys():
+ markata.py_files = [Path(f) for f in glob.glob("**/*.py", recursive=True)]
+
+ content_directories = list({f.parent for f in markata.py_files})
+ if "content_directories" in markata.__dict__:
markata.content_directories.extend(content_directories)
else:
markata.content_directories = content_directories
try:
- ignore = markata.config["glob"]["use_gitignore"] or True
+ ignore = True
except KeyError:
ignore = True
@@ -71,23 +76,27 @@ def glob(markata: "MarkataDocs") -> None:
if Path(".markataignore").exists():
lines.extend(Path(".markataignore").read_text().splitlines())
- spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
+ spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
- markata.py_files = [
- file for file in markata.py_files if not spec.match_file(str(file))
- ]
+ markata.py_files = [
+ file for file in markata.py_files if not spec.match_file(str(file))
+ ]
-def make_article(markata: "Markata", file: Path) -> frontmatter.Post:
- raw_source = file.read_text()
- tree = ast.parse(raw_source)
- add_parents(tree)
- nodes = [
- n
- for n in ast.walk(tree)
- if isinstance(n, ast.FunctionDef) or isinstance(n, ast.ClassDef)
- ]
+@lru_cache
+def get_template():
+ jinja_env = jinja2.Environment()
+ template = jinja_env.from_string(
+ (Path(__file__).parent / "default_doc_template.md").read_text(),
+ )
+ return template
+
+def make_article(markata: "Markata", file: Path, cache) -> frontmatter.Post:
+ with open(file) as f:
+ raw_source = f.read()
+ key = markata.make_hash("docs", "file", raw_source)
+ slug = f"{file.parent}/{file.stem}".lstrip("/").lstrip("./")
edit_link = (
str(markata.config.get("repo_url", "https://github.com/"))
+ "edit/"
@@ -95,25 +104,55 @@ def make_article(markata: "Markata", file: Path) -> frontmatter.Post:
+ "/"
+ str(file)
)
+ article_from_cache = markata.precache.get(key)
+ if article_from_cache is not None:
+ article = article_from_cache
+ else:
+ tree = ast.parse(raw_source)
+ add_parents(tree)
+ nodes = [
+ n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.ClassDef))
+ ]
- slug = f"{file.parent}/{file.stem}".lstrip("/").lstrip("./")
+ article = get_template().render(
+ ast=ast,
+ file=file,
+ slug=slug,
+ edit_link=edit_link,
+ tree=tree,
+ datetime=datetime,
+ nodes=nodes,
+ raw_source=raw_source,
+ indent=textwrap.indent,
+ )
+ cache.add(
+ key,
+ article,
+ expire=markata.config.default_cache_expire,
+ )
- jinja_env = jinja2.Environment()
- article = jinja_env.from_string(
- (Path(__file__).parent / "default_doc_template.md").read_text()
- ).render(
- ast=ast,
- file=file,
- slug=slug,
- edit_link=edit_link,
- tree=tree,
- datetime=datetime,
- nodes=nodes,
- raw_source=raw_source,
- indent=textwrap.indent,
- )
+ try:
+ article = markata.Post(
+ markata=markata,
+ path=str(file).replace(".py", ".md"),
+ title=file.name,
+ content=article,
+ ast=ast,
+ file=file,
+ slug=slug,
+ edit_link=edit_link,
+ datetime=datetime,
+ )
+
+ except pydantic.ValidationError as e:
+ from markata.plugins.load import ValidationError, get_models
+
+ models = get_models(markata=markata, error=e)
+ models = list(models.values())
+ models = "\n".join(models)
+ raise ValidationError(f"{e}\n\n{models}\nfailed to load {path}") from e
- return frontmatter.loads(article)
+ return article
@hook_impl
@@ -125,4 +164,5 @@ def load(markata: "MarkataDocs") -> None:
if "articles" not in markata.__dict__:
markata.articles = []
for py_file in markata.py_files:
- markata.articles.append(make_article(markata, py_file))
+ with markata.cache as cache:
+ markata.articles.append(make_article(markata, py_file, cache))
diff --git a/markata/plugins/feeds.py b/markata/plugins/feeds.py
index 21831dea..67f7cd57 100644
--- a/markata/plugins/feeds.py
+++ b/markata/plugins/feeds.py
@@ -4,7 +4,7 @@
rendered with a `card_template` before being applied to the `body` of the
`template`.
-## Installation
+# Installation
This plugin is built-in and enabled by default, but in you want to be very
explicit you can add it to your list of existing plugins.
@@ -15,9 +15,9 @@
]
```
-## Configuration
+# Configuration
-### set default template and card_template
+# set default template and card_template
At the root of the markata.feeds config you may set `template`, and
`card_template`. These will become your defaults for every feed you create.
@@ -31,7 +31,7 @@
card_template="plugins/feed_card_template.html"
```
-### pages
+# pages
Underneath of the `markata.feeds` we will create a new map for each page where
the name of the map will be the name of the page.
@@ -41,11 +41,13 @@
single post.
``` toml
-[markata.feeds.all-posts]
+[[markata.feeds]]
+title="All Posts"
+slug='all'
filter="True"
```
-### template
+# template
The `template` configuration key is a file path to the template that you want
to use to create the feed. You may set the default template you want to use
@@ -73,7 +75,7 @@
I highly reccomend putting your `body` in a ``, and wrapping your
`card_template`s in an ``.
-### card_template
+# card_template
All keys available from each post is available to put into your jinja
template. These can either be placed there in your post frontmatter, or
@@ -83,7 +85,9 @@
title and date.
``` toml
-[markata.feeds.all]
+[[markata.feeds]]
+slug='all'
+title='All Posts'
filter="True"
card_template='''
@@ -94,19 +98,21 @@
'''
```
-### filter
+# filter
The filter is a python expression ran on every post that expects to return a
boolean. The variables available to this expression are every key in your
frontmatter, plus the `timedelta` function, and `parse` function to more easily
work with dates.
-## Feed Examples
+# Feed Examples
True can be passed in to make a feed of all the posts you have.
``` toml
-[markata.feeds.archive]
+[[markata.feeds]]
+slug='all'
+title='All Posts'
filter="True"
```
@@ -114,7 +120,9 @@
example creates a feed that includes every post where published is `True`.
``` toml
-[markata.feeds.draft]
+[[markata.feeds]]
+slug='draft'
+title='Draft'
filter="published=='False'"
```
@@ -126,10 +134,14 @@
posts and for today's posts respectively.
``` toml
-[markata.feeds.scheduled]
+[[markata.feeds]]
+slug='scheduled'
+title='Scheduled'
filter="date>today"
-[markata.feeds.today]
+[[markata.feeds]]
+slug='today'
+title='Today'
filter="date==today"
```
@@ -137,7 +149,9 @@
can check for the existence of a tag in the list.
``` toml
-[markata.feeds.python]
+[[markata.feeds]]
+slug='python'
+title='Python'
filter="date<=today and 'python' in tags"
```
@@ -145,37 +159,45 @@
one example of the main feed on my blog.
``` toml
-[markata.feeds.blog]
+[[markata.feeds]]
+slug='blog'
+title='Blog'
filter="date<=today and templateKey in ['blog-post'] and published =='True'"
```
Here is another example that shows my drafts for a particular tag.
``` toml
-[markata.feeds.python-draft]
+[[markata.feeds]]
+slug='python-draft'
+title='Python Draft'
filter="date<=today and 'python' in tags and published=='False'"
```
-## Defaults
+# Defaults
By default feeds will create one feed page at `/archive/` that includes all
posts.
-[markata.feeds.archive]
+[[markata.feeds]]
+slug='archive'
+title='All Posts'
filter="True"
"""
-from dataclasses import dataclass
import datetime
-from pathlib import Path
import shutil
import textwrap
-from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
+from dataclasses import dataclass
+from functools import lru_cache
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, List, Optional
+import pydantic
+import typer
from jinja2 import Template, Undefined
from rich import print as rich_print
from rich.table import Table
-import typer
from markata import Markata, __version__
from markata.hookspec import hook_impl, register_attr
@@ -193,12 +215,50 @@ class MarkataFilterError(RuntimeError):
...
+class FeedConfig(pydantic.BaseModel):
+ DEFAULT_TITLE: str = "All Posts"
+
+ title: str = DEFAULT_TITLE
+ slug: str = None
+ name: Optional[str] = None
+ filter: str = "True"
+ sort: str = "date"
+ reverse: bool = False
+ rss: bool = True
+ sitemap: bool = True
+ card_template: str = """
+
+
+ {{ post.title }}
+
+
+ """
+ template: str = Path(__file__).parent / "default_post_template.html.jinja"
+ rss_template: str = Path(__file__).parent / "default_rss_template.xml"
+ sitemap_template: str = Path(__file__).parent / "default_sitemap_template.xml"
+ xsl_template: str = Path(__file__).parent / "default_xsl_template.xsl"
+
+ @pydantic.validator("name", pre=True, always=True)
+ def default_name(cls, v, *, values):
+ return v or str(values.get("slug")).replace("-", "_")
+
+ @pydantic.validator("card_template", "template", pre=True, always=True)
+ def read_template(cls, v, *, values) -> str:
+ if isinstance(v, Path):
+ return str(v.read_text())
+ return v
+
+
+class FeedsConfig(pydantic.BaseModel):
+ feeds: List[FeedConfig] = [FeedConfig(slug="archive")]
+
+
@dataclass
class Feed:
"""
A storage class for markata feed objects.
- ## Usage
+ # Usage
``` python
from markata import Markata
@@ -212,13 +272,19 @@ class Feed:
```
"""
- name: str
- config: Dict
- posts: list
+ config: FeedConfig
_m: Markata
+ @property
+ def name(self):
+ return self.config.name
+
+ @property
+ def posts(self):
+ return self.map("post")
+
def map(self, func="post", **args):
- return self._m.map(func, **{**self.config, **args})
+ return self._m.map(func, **{**self.config.dict(), **args})
class Feeds:
@@ -284,24 +350,18 @@ class Feeds:
```
"""
- def __init__(self, markata: Markata):
+ def __init__(self, markata: Markata) -> None:
self._m = markata
+ self.config = {f.name: f for f in markata.config.feeds}
self.refresh()
def refresh(self) -> None:
"""
Refresh all of the feeds objects
"""
- self.config = self._m.config.get("feeds", dict())
- for page, page_conf in self.config.items():
- name = page.replace("-", "_").lower()
- feed = Feed(
- name=name,
- posts=self._m.map("post", **page_conf),
- config=page_conf,
- _m=self._m,
- )
- self.__setattr__(name, feed)
+ for feed in self._m.config.feeds:
+ feed = Feed(config=feed, _m=self._m)
+ self.__setattr__(feed.name, feed)
def __iter__(self):
return iter(self.config.keys())
@@ -313,7 +373,7 @@ def values(self):
return [self[feed] for feed in self.config.keys()]
def items(self):
- return [(key, self[key]) for key in self.config.keys()]
+ return [(key, self[key]) for key in self.config]
def __getitem__(self, key: str) -> Any:
return getattr(self, key.replace("-", "_").lower())
@@ -324,6 +384,10 @@ def _dict_panel(self, config) -> str:
"""
msg = ""
for key, value in config.items():
+ if isinstance(value, str):
+ if len(value) > 50:
+ value = value[:50] + "..."
+ value = value
msg = msg + f"[grey46]{key}[/][magenta3]:[/] [grey66]{value}[/]\n"
return msg
@@ -336,33 +400,31 @@ def __rich__(self) -> Table:
table.add_column("posts", justify="left", style="green")
table.add_column("config", style="magenta")
- for name in self.config.keys():
+ for name in self.config:
table.add_row(
- name, str(len(self[name].posts)), self._dict_panel(self.config[name])
+ name,
+ str(len(self[name].posts)),
+ self._dict_panel(self.config[name].dict()),
)
return table
-@hook_impl
-@register_attr("feeds")
-def configure(markata: Markata) -> None:
- """
- configure the default values for the feeds plugin
- """
- if "feeds" not in markata.config.keys():
- markata.config["feeds"] = dict()
- config = markata.config.get("feeds", dict())
- if "archive" not in config.keys():
- config["archive"] = dict()
- config["archive"]["filter"] = "True"
-
- default_post_template = markata.config.get("feeds_config", {}).get(
- "template", Path(__file__).parent / "default_post_template.html"
- )
+@hook_impl(tryfirst=True)
+def config_model(markata: Markata) -> None:
+ markata.config_models.append(FeedsConfig)
+
- for page, page_conf in config.items():
- if "template" not in page_conf.keys():
- page_conf["template"] = default_post_template
+# @hook_impl
+# @register_attr("feeds")
+# def configure(markata: Markata) -> None:
+# """
+# configure the default values for the feeds plugin
+# """
+# if "feeds" not in markata.config.keys():
+# if "archive" not in config.keys():
+
+# for page, page_conf in config.items():
+# if "template" not in page_conf.keys():
@hook_impl
@@ -379,115 +441,164 @@ def save(markata: Markata) -> None:
"""
Creates a new feed page for each page in the config.
"""
- feeds = markata.config.get("feeds", {})
-
- description = markata.get_config("description") or ""
- url = markata.get_config("url") or ""
-
- for page, page_conf in feeds.items():
- create_page(
- markata,
- page,
- description=description,
- url=url,
- **page_conf,
- )
+ with markata.cache as cache:
+ for feed in markata.feeds.values():
+ create_page(
+ markata,
+ feed,
+ cache,
+ )
- home = Path(str(markata.config["output_dir"])) / "index.html"
- archive = Path(str(markata.config["output_dir"])) / "archive" / "index.html"
+ home = Path(str(markata.config.output_dir)) / "index.html"
+ archive = Path(str(markata.config.output_dir)) / "archive" / "index.html"
if not home.exists() and archive.exists():
shutil.copy(str(archive), str(home))
+ xsl_template = get_template(feed.config.xsl_template)
+ xsl = xsl_template.render(
+ markata=markata,
+ __version__=__version__,
+ today=datetime.datetime.today(),
+ config=markata.config,
+ )
+ xsl_file = Path(markata.config.output_dir) / "rss.xsl"
+ xsl_file.write_text(xsl)
+
+
+@lru_cache()
+def get_template(src) -> Template:
+ try:
+ return Template(Path(src).read_text(), undefined=SilentUndefined)
+ except FileNotFoundError:
+ return Template(src, undefined=SilentUndefined)
+ except OSError: # File name too long
+ return Template(src, undefined=SilentUndefined)
+
def create_page(
markata: Markata,
- page: str,
- tags: Optional[List] = None,
- published: str = "True",
- template: Optional[Union[Path, str]] = None,
- card_template: Optional[str] = None,
- filter: Optional[str] = None,
- description: Optional[str] = None,
- url: Optional[str] = None,
- title: Optional[str] = "feed",
- sort: str = "True",
- reverse: bool = False,
- **rest,
+ feed: Feed,
+ cache,
) -> None:
"""
create an html unorderd list of posts.
"""
- posts = markata.feeds[page].posts
- cards = [create_card(markata, post, card_template) for post in posts]
+ posts = feed.posts
+
+ cards = [
+ create_card(markata, post, feed.config.card_template, cache) for post in posts
+ ]
cards.insert(0, "")
+ cards = "".join(cards)
- # if template is None:
- # template = Path(__file__).parent / "default_post_template.html"
-
- with open(template) as f:
- template = Template(f.read(), undefined=SilentUndefined)
- output_file = Path(markata.config["output_dir"]) / page / "index.html"
- canonical_url = f"{url}/{page}/"
+ template = get_template(feed.config.template)
+ rss_template = get_template(feed.config.rss_template)
+ sitemap_template = get_template(feed.config.sitemap_template)
+ output_file = Path(markata.config.output_dir) / feed.config.slug / "index.html"
+ canonical_url = f"{markata.config.url}/{feed.config.slug}/"
output_file.parent.mkdir(exist_ok=True, parents=True)
- with open(output_file, "w+") as f:
- f.write(
- template.render(
- markata=markata,
- __version__=__version__,
- body="".join(cards),
- url=url,
- description=description,
- title=title,
- canonical_url=canonical_url,
- today=datetime.datetime.today(),
- config=markata.config,
- )
+ rss_output_file = Path(markata.config.output_dir) / feed.config.slug / "rss.xml"
+ rss_output_file.parent.mkdir(exist_ok=True, parents=True)
+
+ sitemap_output_file = (
+ Path(markata.config.output_dir) / feed.config.slug / "sitemap.xml"
+ )
+ sitemap_output_file.parent.mkdir(exist_ok=True, parents=True)
+
+ key = markata.make_hash(
+ "feeds",
+ template,
+ __version__,
+ cards,
+ markata.config.url,
+ markata.config.description,
+ feed.config.title,
+ canonical_url,
+ datetime.datetime.today(),
+ markata.config,
+ )
+
+ feed_html_from_cache = markata.precache.get(key)
+ if feed_html_from_cache is None:
+ feed_html = template.render(
+ markata=markata,
+ __version__=__version__,
+ body=cards,
+ url=markata.config.url,
+ description=markata.config.description,
+ title=feed.config.title,
+ canonical_url=canonical_url,
+ today=datetime.datetime.today(),
+ config=markata.config,
)
+ with markata.cache as cache:
+ markata.cache.set(key, feed_html)
+
+ feed_rss = rss_template.render(markata=markata, feed=feed)
+ feed_sitemap = sitemap_template.render(markata=markata, feed=feed)
+
+ output_file.write_text(feed_html)
+ rss_output_file.write_text(feed_rss)
+ sitemap_output_file.write_text(feed_sitemap)
def create_card(
- markata: "Markata", post: "Post", template: Optional[str] = None
+ markata: "Markata",
+ post: "Post",
+ template: Optional[str] = None,
+ cache=None,
) -> Any:
"""
Creates a card for one post based on the configured template. If no
template is configured it will create one with the post title and dates
(if present).
"""
+ key = markata.make_hash("feeds", template, str(post), post.content)
+
+ card = markata.precache.get(key)
+ if card is not None:
+ return card
+
if template is None:
template = markata.config.get("feeds_config", {}).get("card_template", None)
if template is None:
- if "date" in post.keys():
- return textwrap.dedent(
+ if "date" in post:
+ card = textwrap.dedent(
f"""
-
- {post['title']}
- {post['date'].year}-
- {post['date'].month}-
- {post['date'].day}
+
+ {post.title}
+ {post.date.year}-
+ {post.date.month}-
+ {post.date.day}
- """
+ """,
)
else:
- return textwrap.dedent(
+ card = textwrap.dedent(
f"""
-
- {post['title']}
+
+ {post.title}
- """
+ """,
)
- try:
- _template = Template(Path(template).read_text())
- except FileNotFoundError:
- _template = Template(template)
- return _template.render(markata=markata, **post.to_dict())
+ else:
+ try:
+ _template = Template(Path(template).read_text())
+ except FileNotFoundError:
+ _template = Template(template)
+ except OSError: # File name too long
+ _template = Template(template)
+ card = _template.render(post=post, **post.to_dict())
+ cache.add(key, card)
+ return card
@hook_impl
diff --git a/markata/plugins/flat_slug.py b/markata/plugins/flat_slug.py
index 9695f74f..aa044482 100644
--- a/markata/plugins/flat_slug.py
+++ b/markata/plugins/flat_slug.py
@@ -15,7 +15,7 @@
---
-This is my first post it will be at `/my-post/`
+This is my first post it will be at `/my-post/`
reguardless of filename.
```
@@ -25,18 +25,47 @@
the filename without the extension, unless you explicitly set your slug in
frontmatter.
-* `/pages/my-post.md` becomes `/my-post/`
-* `/pages/blog/a-blog-post.md` becomes `/a-blog-post/`
+* `/pages/my-post.md` becomes `/my-post/`
+* `/pages/blog/a-blog-post.md` becomes `/a-blog-post/`
"""
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import Dict, Optional
+import pydantic
from slugify import slugify
-from markata.hookspec import hook_impl
+from markata import Markata
+from markata.hookspec import hook_impl, register_attr
-if TYPE_CHECKING:
- from markata import Markata
+
+class FlatSlugConfig(pydantic.BaseModel):
+ slugify: bool = True
+
+
+class Config(pydantic.BaseModel):
+ flat_slug: FlatSlugConfig = FlatSlugConfig()
+
+
+class FlatSlugPost(pydantic.BaseModel):
+ should_slugify: Optional[bool] = None
+
+ @pydantic.validator("should_slugify", pre=True, always=True)
+ def default_slugify(cls: "FlatSlugPost", v: bool, *, values: Dict) -> bool:
+ if not v:
+ return cls.markata.config.flat_slug.slugify
+ return v
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: Markata) -> None:
+ markata.config_models.append(Config)
+
+
+@hook_impl
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(FlatSlugPost)
@hook_impl(tryfirst=True)
@@ -44,12 +73,12 @@ def pre_render(markata: "Markata") -> None:
"""
Sets the article slug if one is not already set in the frontmatter.
"""
- should_slugify = markata.config.get("slugify", True)
for article in markata.iter_articles(description="creating slugs"):
stem = article.get(
- "slug", Path(article.get("path", article.get("title", ""))).stem
+ "slug",
+ Path(article.get("path", article.get("title", ""))).stem,
)
- if should_slugify:
- article["slug"] = "/".join([slugify(s) for s in stem.split("/")])
+ if article.should_slugify:
+ article.slug = "/".join([slugify(s) for s in stem.split("/")])
else:
- article["slug"] = stem
+ article.slug = stem
diff --git a/markata/plugins/generator.py b/markata/plugins/generator.py
index 92ccb609..b1a5e1c7 100644
--- a/markata/plugins/generator.py
+++ b/markata/plugins/generator.py
@@ -7,7 +7,7 @@
@hook_impl(trylast=True)
def render(markata: Markata) -> None:
- should_prettify = markata.config.get("prettify_html", False)
+ should_prettify = markata.config.prettify_html
for article in markata.iter_articles("add ssg tag"):
soup = BeautifulSoup(article.html, features="lxml")
tag = soup.new_tag("meta")
diff --git a/markata/plugins/glob.py b/markata/plugins/glob.py
index 4ea146b9..c98196ab 100644
--- a/markata/plugins/glob.py
+++ b/markata/plugins/glob.py
@@ -1,8 +1,9 @@
"""Default glob plugin"""
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import List, TYPE_CHECKING, Union
from more_itertools import flatten
+import pydantic
from markata.hookspec import hook_impl, register_attr
@@ -10,16 +11,42 @@
from markata import Markata
+class GlobConfig(pydantic.BaseModel):
+ glob_patterns: Union[List[str], str] = ["**/*.md"]
+ use_gitignore: bool = True
+
+ @pydantic.validator("glob_patterns")
+ def convert_to_list(cls, v):
+ if not isinstance(v, list):
+ return v.split(",")
+ return v
+
+
+class Config(pydantic.BaseModel):
+ glob: GlobConfig = GlobConfig()
+
+
+@hook_impl
+@register_attr("post_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
@hook_impl
@register_attr("content_directories", "files")
def glob(markata: "Markata") -> None:
markata.files = list(
- flatten([Path().glob(str(pattern)) for pattern in markata.glob_patterns])
+ flatten(
+ [
+ Path().glob(str(pattern))
+ for pattern in markata.config.glob.glob_patterns
+ ],
+ ),
)
- markata.content_directories = list(set([f.parent for f in markata.files]))
+ markata.content_directories = list({f.parent for f in markata.files})
try:
- ignore = markata.config["glob"]["use_gitignore"] or True
+ ignore = True
except KeyError:
ignore = True
@@ -34,8 +61,22 @@ def glob(markata: "Markata") -> None:
if Path(".markataignore").exists():
lines.extend(Path(".markataignore").read_text().splitlines())
- spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
+ key = markata.make_hash("glob", "spec", lines)
+ spec = markata.precache.get(key)
+ if spec is None:
+ spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
+ with markata.cache as cache:
+ cache.set(key, spec)
+
+ def check_spec(file: str) -> bool:
+ key = markata.make_hash("glob", "check_spec", file)
+ check = markata.precache.get(key)
+ if check is not None:
+ return check
+
+ check = spec.match_file(str(file))
+ with markata.cache as cache:
+ cache.set(key, check)
+ return check
- markata.files = [
- file for file in markata.files if not spec.match_file(str(file))
- ]
+ markata.files = [file for file in markata.files if not check_spec(str(file))]
diff --git a/markata/plugins/heading_link.py b/markata/plugins/heading_link.py
index 357f0b09..6e4c27b6 100644
--- a/markata/plugins/heading_link.py
+++ b/markata/plugins/heading_link.py
@@ -4,7 +4,7 @@
"""
from pathlib import Path
import re
-from typing import Any, TYPE_CHECKING
+from typing import TYPE_CHECKING
from bs4 import BeautifulSoup
@@ -22,8 +22,6 @@ def post_render(markata: Markata) -> None:
This plugin creates a link svg next to all headings.
"""
- config = markata.get_plugin_config(__file__)
- should_prettify = markata.config.get("prettify_html", False)
with markata.cache as cache:
for article in markata.iter_articles("link headers"):
key = markata.make_hash(
@@ -34,17 +32,21 @@ def post_render(markata: Markata) -> None:
article.html,
)
- html_from_cache = cache.get(key)
+ html_from_cache = markata.precache.get(key)
if html_from_cache is None:
- html = link_headings(article, should_prettify)
- cache.add(key, html, expire=config["cache_expire"])
+ html = link_headings(article)
+ cache.add(
+ key,
+ html,
+ expire=markata.config.default_cache_expire,
+ )
else:
html = html_from_cache
article.html = html
-def link_headings(article: "Post", prettify: bool = False) -> Any:
+def link_headings(article: "Post") -> str:
"""
Use BeautifulSoup to find all headings and run link_heading on them.
"""
@@ -55,8 +57,6 @@ def link_headings(article: "Post", prettify: bool = False) -> Any:
and heading.get("id", "") != "title"
):
link_heading(soup, heading)
- if prettify:
- return soup.prettify()
return str(soup)
@@ -89,22 +89,7 @@ def link_heading(soup: "bs4.BeautifulSoup", heading: "bs4.element.Tag") -> None:
path = soup.new_tag(
"path",
- d=(
- "M9.199 13.599a5.99 5.99 0 0 0 3.949 2.345 5.987 5.987 0 0 0"
- "5.105-1.702l2.995-2.994a5.992 5.992 0 0 0 1.695-4.285 5.976 5.976 0"
- "0 0-1.831-4.211 5.99 5.99 0 0 0-6.431-1.242 6.003 6.003 0 0 0-1.905"
- "1.24l-1.731 1.721a.999.999 0 1 0 1.41 1.418l1.709-1.699a3.985 3.985"
- "0 0 1 2.761-1.123 3.975 3.975 0 0 1 2.799 1.122 3.997 3.997 0 0 1"
- ".111 5.644l-3.005 3.006a3.982 3.982 0 0 1-3.395 1.126 3.987 3.987 0"
- "0 1-2.632-1.563A1 1 0 0 0 9.201 13.6zm5.602-3.198a5.99 5.99 0 0"
- "0-3.949-2.345 5.987 5.987 0 0 0-5.105 1.702l-2.995 2.994a5.992"
- "5.992 0 0 0-1.695 4.285 5.976 5.976 0 0 0 1.831 4.211 5.99 5.99 0 0"
- "0 6.431 1.242 6.003 6.003 0 0 0 1.905-1.24l1.723-1.723a.999.999 0 1"
- "0-1.414-1.414L9.836 19.81a3.985 3.985 0 0 1-2.761 1.123 3.975 3.975"
- "0 0 1-2.799-1.122 3.997 3.997 0 0 1-.111-5.644l3.005-3.006a3.982"
- "3.982 0 0 1 3.395-1.126 3.987 3.987 0 0 1 2.632 1.563 1 1 0 0 0"
- "1.602-1.198z"
- ),
+ d="M9.199 13.599a5.99 5.99 0 0 0 3.949 2.345 5.987 5.987 0 0 0 5.105-1.702l2.995-2.994a5.992 5.992 0 0 0 1.695-4.285 5.976 5.976 0 0 0-1.831-4.211 5.99 5.99 0 0 0-6.431-1.242 6.003 6.003 0 0 0-1.905 1.24l-1.731 1.721a.999.999 0 1 0 1.41 1.418l1.709-1.699a3.985 3.985 0 0 1 2.761-1.123 3.975 3.975 0 0 1 2.799 1.122 3.997 3.997 0 0 1 .111 5.644l-3.005 3.006a3.982 3.982 0 0 1-3.395 1.126 3.987 3.987 0 0 1-2.632-1.563A1 1 0 0 0 9.201 13.6zm5.602-3.198a5.99 5.99 0 0 0-3.949-2.345 5.987 5.987 0 0 0-5.105 1.702l-2.995 2.994a5.992 5.992 0 0 0-1.695 4.285 5.976 5.976 0 0 0 1.831 4.211 5.99 5.99 0 0 0 6.431 1.242 6.003 6.003 0 0 0 1.905-1.24l1.723-1.723a.999.999 0 1 0-1.414-1.414L9.836 19.81a3.985 3.985 0 0 1-2.761 1.123 3.975 3.975 0 0 1-2.799-1.122 3.997 3.997 0 0 1-.111-5.644l3.005-3.006a3.982 3.982 0 0 1 3.395-1.126 3.987 3.987 0 0 1 2.632 1.563 1 1 0 0 0 1.602-1.198z",
)
svg.append(path)
link.append(span)
diff --git a/markata/plugins/icon_resize.py b/markata/plugins/icon_resize.py
index 0b52a036..9a5e4417 100644
--- a/markata/plugins/icon_resize.py
+++ b/markata/plugins/icon_resize.py
@@ -1,57 +1,104 @@
-"""Icon Resize Plugin"""
-from pathlib import Path
-from typing import TYPE_CHECKING
+"""Icon Resize Plugin
+
+Resized favicon to a set of common sizes.
+
+## markata.plugins.icon_resize configuration
+```toml title=markata.toml
+[markata]
+output_dir = "markout"
+assets_dir = "static"
+icon = "static/icon.png"
+```
+
+"""
from PIL import Image
+from pathlib import Path
+from typing import Dict, List, Optional, TYPE_CHECKING
-from markata.hookspec import hook_impl, register_attr
+from markata.hookspec import register_attr
-if TYPE_CHECKING:
- from typing import Dict, List
+if TYPE_CHECKING:
from markata import Markata
- class MarkataIcons(Markata):
- icons: List[Dict[str, str]]
+import pydantic
+
+from markata.hookspec import hook_impl
+
+
+class Config(pydantic.BaseModel):
+ output_dir: pydantic.DirectoryPath = "markout"
+ assets_dir: Path = pydantic.Field(
+ Path("static"),
+ description="The directory to store static assets",
+ )
+ icon: Optional[Path] = None
+ icon_out_file: Optional[Path] = None
+ icons: Optional[List[Dict[str, str]]] = []
+
+ @pydantic.validator("icon")
+ def ensure_icon_exists(cls, v, *, values: Dict) -> Path:
+ if v is None:
+ return
+ if v.exists():
+ return v
+
+ icon = Path(values["assets_dir"]) / v
+
+ if icon.exists():
+ return icon
+ else:
+ raise FileNotFoundError(v)
+
+ @pydantic.validator("icon_out_file", pre=True, always=True)
+ def default_icon_out_file(cls, v, *, values: Dict) -> Path:
+ if v is None and values["icon"] is not None:
+ return Path(values["output_dir"]) / values["icon"]
+ return v
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
@hook_impl
@register_attr("icons")
-def render(markata: "MarkataIcons") -> None:
- if "icon" not in markata.config:
+def render(markata: "Markata") -> None:
+ if markata.config.icon is None:
return
- base_out_file = Path(markata.config["output_dir"]) / markata.config["icon"]
- with Image.open(Path(markata.config["assets_dir"]) / markata.config["icon"]) as img:
- markata.icons = []
+ with Image.open(markata.config.icon) as img:
for width in [48, 72, 96, 144, 192, 256, 384, 512]:
height = int(float(img.size[1]) * float(width / float(img.size[0])))
filename = Path(
- f"{base_out_file.stem}_{width}x{height}{base_out_file.suffix}"
+ f"{markata.config.icon_out_file.stem}_{width}x{height}{markata.config.icon_out_file.suffix}",
)
- markata.icons.append(
+ markata.config.icons.append(
{
"src": str(filename),
"sizes": f"{width}x{width}",
"type": f"image/{img.format}".lower(),
"purpose": "any maskable",
- }
+ },
)
@hook_impl
-def save(markata: "MarkataIcons") -> None:
- if "icon" not in markata.config:
+def save(markata: "Markata") -> None:
+ if markata.config.icon is None:
return
- base_out_file = Path(markata.config["output_dir"]) / markata.config["icon"]
for width in [48, 72, 96, 144, 192, 256, 384, 512]:
- with Image.open(
- Path(markata.config["assets_dir"]) / markata.config["icon"]
- ) as img:
+ with Image.open(markata.config.icon) as img:
height = int(float(img.size[1]) * float(width / float(img.size[0])))
img = img.resize((width, height), Image.LANCZOS)
filename = Path(
- f"{base_out_file.stem}_{width}x{height}{base_out_file.suffix}"
+ f"{markata.config.icon_out_file.stem}_{width}x{height}{markata.config.icon_out_file.suffix}",
)
- out_file = Path(markata.config["output_dir"]) / filename
+ out_file = Path(markata.config.output_dir) / filename
+ if out_file.exists():
+ continue
+ img = img.resize((width, height), Image.LANCZOS)
img.save(out_file)
diff --git a/markata/plugins/jinja_md.py b/markata/plugins/jinja_md.py
index 31d11299..4fe66b02 100644
--- a/markata/plugins/jinja_md.py
+++ b/markata/plugins/jinja_md.py
@@ -4,16 +4,16 @@
The markata instance is passed into the template, giving you access to things
such as all of your articles, config, and this post as post.
-## Examples
+# Examples
first we can grab a few things out of the frontmatter of this post.
``` markdown
-### {{ post.title }}
+# {{ post.title }}
{{ post.description }}
```
-### one-liner list of links
+# one-liner list of links
This one-liner will render a list of markdown links into your markdown at build
time. It's quite handy to pop into posts.
@@ -22,7 +22,7 @@
{{ '\\n'.join(markata.map('f"* [{title}]({slug})"', sort='slug')) }}
```
-### jinja for to markdown list of links
+# jinja for to markdown list of links
Sometimes quoting things like your filters are hard to do in a one line without
running out of quote variants. Jinja for loops can make this much easier.
@@ -33,7 +33,7 @@
{% endfor %}
```
-### jinja for to html list of links
+# jinja for to html list of links
Since markdown is a superset of html, you can just render out html into your
post and it is still valid.
@@ -46,7 +46,7 @@
```
-## Ignoring files
+# Ignoring files
It is possible to ignore files by adding an ignore to your `markata.jinja_md`
config in your `markata.toml` file. This ignore follows the `gitwildmatch`
@@ -63,7 +63,7 @@
Docs such as this jinja_md.py file will get converted to jinja_md.md during
build time, so use `.md` extensions instead of `.py`.
-## Ignoring a single file
+# Ignoring a single file
You can also ignore a single file right from the articles frontmatter, by
adding `jinja: false`.
@@ -75,7 +75,7 @@
---
```
-## Escaping
+# Escaping
Sometimes you want the ability to have jinja templates in a post, but also the
ability to keep a raw jinja template. There are a couple of techniques that
@@ -90,7 +90,7 @@
{{ '{{' }} '\\n'.join(markata.map('f"* [{title}]({slug})"', sort='slug')) {{ '}}' }}
```
-## Creating a jinja extension
+# Creating a jinja extension
Here is a bit of a boilerplate example of a jinja extension.
@@ -132,16 +132,15 @@ def run(self, arg, caller):
```
"""
-import copy
from pathlib import Path
from typing import List, TYPE_CHECKING
-from deepmerge import always_merger
import jinja2
from jinja2 import TemplateSyntaxError, Undefined, UndefinedError, nodes
from jinja2.ext import Extension
import pathspec
import pkg_resources
+import pydantic
from markata import __version__
from markata.hookspec import hook_impl, register_attr
@@ -166,7 +165,10 @@ def parse(self, parser):
line_number = next(parser.stream).lineno
file = [parser.parse_expression()]
return nodes.CallBlock(
- self.call_method("_read_file", file), [], [], ""
+ self.call_method("_read_file", file),
+ [],
+ [],
+ "",
).set_lineno(line_number)
def _read_file(self, file, caller):
@@ -181,7 +183,7 @@ class _SilentUndefined(Undefined):
"""
silence undefined variable errors in jinja templates.
- ### Example
+ # Example
```python
template = '{{ variable }}'
article.content = Template( template, undefined=_SilentUndefined).render()
@@ -198,7 +200,17 @@ class PostTemplateSyntaxError(TemplateSyntaxError):
"""
-@hook_impl
+class JinjaMd(pydantic.BaseModel):
+ jinja: bool = True
+
+
+@hook_impl()
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(JinjaMd)
+
+
+@hook_impl()
@register_attr("prevnext")
def pre_render(markata: "Markata") -> None:
"""
@@ -208,33 +220,30 @@ def pre_render(markata: "Markata") -> None:
as `markata`.
"""
- config = markata.get_plugin_config("jinja_md")
- ignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", config.get("ignore", []))
+ config = markata.config.jinja_md
+ ignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", config.ignore)
# for article in markata.iter_articles(description="jinja_md"):
jinja_env = jinja2.Environment(
extensions=[IncludeRawExtension, *register_jinja_extensions(config)],
)
- _full_config = copy.deepcopy(markata.config)
for article in markata.articles:
if article.get("jinja", True) and not ignore_spec.match_file(article["path"]):
try:
- article.content = jinja_env.from_string(article.content).render(
- __version__=__version__,
- markata=markata,
- config=always_merger.merge(
- _full_config,
- copy.deepcopy(
- article.get(
- "config_overrides",
- {},
- ),
- ),
- ),
- **article,
- )
+ key = markata.make_hash(article.content)
+ content_from_cache = markata.precache.get(key)
+ if content_from_cache is None:
+ article.content = jinja_env.from_string(article.content).render(
+ __version__=__version__,
+ **article,
+ post=article,
+ )
+ with markata.cache:
+ markata.cache.set(key, article.content)
+ else:
+ article.content = content_from_cache
# prevent double rendering
- article["jinja"] = False
+ article.jinja = False
except TemplateSyntaxError as e:
errorline = article.content.split("\n")[e.lineno - 1]
msg = f"""
@@ -246,3 +255,16 @@ def pre_render(markata: "Markata") -> None:
raise PostTemplateSyntaxError(msg, lineno=e.lineno)
except UndefinedError as e:
raise UndefinedError(f'{e} in {article["path"]}')
+
+
+class JinjaMdConfig(pydantic.BaseModel):
+ ignore: List[str] = []
+
+
+class Config(pydantic.BaseModel):
+ jinja_md: JinjaMdConfig = JinjaMdConfig()
+
+
+@hook_impl(tryfirst=True)
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
diff --git a/markata/plugins/load.py b/markata/plugins/load.py
index acd5e5cd..80a256ac 100644
--- a/markata/plugins/load.py
+++ b/markata/plugins/load.py
@@ -1,13 +1,13 @@
"""Default load plugin."""
-import time
+import itertools
from pathlib import Path
from typing import TYPE_CHECKING, Callable, List, Optional
import frontmatter
+import pydantic
from rich.progress import BarColumn, Progress
from yaml.parser import ParserError
-from markata.background import task
from markata.hookspec import hook_impl, register_attr
if TYPE_CHECKING:
@@ -17,31 +17,68 @@ class MarkataMarkdown(Markata):
articles: List = []
+class ValidationError(ValueError):
+ ...
+
+
@hook_impl
-@register_attr("articles")
+@register_attr("articles", "posts")
def load(markata: "MarkataMarkdown") -> None:
- progress = Progress(
- BarColumn(bar_width=None), transient=True, console=markata.console
+ Progress(
+ BarColumn(bar_width=None),
+ transient=True,
+ console=markata.console,
+ )
+ markata.console.log(f"found {len(markata.files)} posts")
+ markata.posts_obj = markata.Posts.parse_obj(
+ {"posts": [get_post(article, markata) for article in markata.files]},
)
- if not markata.config.get("repo_url", "https://github.com/").endswith("/"):
- markata.config["repo_url"] = (
- markata.config.get("repo_url", "https://github.com/") + "/"
- )
-
- futures = [get_post(article, markata) for article in markata.files]
- task_id = progress.add_task("loading markdown")
- progress.update(task_id, total=len(futures))
- with progress:
- while not all([f.done() for f in futures]):
- time.sleep(0.1)
- progress.update(task_id, total=len([f for f in futures if f.done()]))
- articles = [f.result() for f in futures]
- articles = [a for a in articles if a]
- markata.articles = articles
-
-
-@task
+ markata.posts = markata.posts_obj.posts
+ markata.articles = markata.posts
+
+
def get_post(path: Path, markata: "Markata") -> Optional[Callable]:
+ if markata.Post:
+ post = pydantic_get_post(path=path, markata=markata)
+ return post
+ else:
+ return legacy_get_post(path=path, markata=markata)
+
+
+def get_models(markata: "Markata", error: pydantic.ValidationError) -> List:
+ fields = []
+ for err in error.errors():
+ fields.extend(err["loc"])
+
+ models = {field: f"{field} used by " for field in fields}
+
+ for field, model in set(
+ itertools.product(
+ fields,
+ markata.post_models,
+ ),
+ ):
+ if field in model.__fields__:
+ models[field] += f"'{model.__module__}.{model.__name__}'"
+
+ return models
+
+
+def pydantic_get_post(path: Path, markata: "Markata") -> Optional[Callable]:
+ try:
+ post = markata.Post.parse_file(markata=markata, path=path)
+ markata.Post.validate(post)
+
+ except pydantic.ValidationError as e:
+ models = get_models(markata=markata, error=e)
+ models = list(models.values())
+ models = "\n".join(models)
+ raise ValidationError(f"{e}\n\n{models}\nfailed to load {path}") from e
+
+ return post
+
+
+def legacy_get_post(path: Path, markata: "Markata") -> Optional[Callable]:
default = {
"cover": "",
"title": "",
@@ -55,6 +92,7 @@ def get_post(path: Path, markata: "Markata") -> Optional[Callable]:
try:
post: "Post" = frontmatter.load(path)
post.metadata = {**default, **post.metadata}
+ post["content"] = post.content
except ParserError:
return None
post = default
@@ -63,10 +101,6 @@ def get_post(path: Path, markata: "Markata") -> Optional[Callable]:
post = default
post.metadata["path"] = str(path)
post["edit_link"] = (
- str(markata.config.get("repo_url", "https://github.com/"))
- + "edit/"
- + str(markata.config.get("repo_branch", "main"))
- + "/"
- + str(post["path"])
+ markata.config.repo_url + "edit/" + markata.config.repo_branch + "/" + post.path
)
return post
diff --git a/markata/plugins/manifest.py b/markata/plugins/manifest.py
index 35bc5c49..cb31212b 100644
--- a/markata/plugins/manifest.py
+++ b/markata/plugins/manifest.py
@@ -3,8 +3,6 @@
from pathlib import Path
from typing import TYPE_CHECKING
-from bs4 import BeautifulSoup
-
from markata.hookspec import hook_impl
if TYPE_CHECKING:
@@ -13,49 +11,18 @@
@hook_impl
def render(markata: "MarkataIcons") -> None:
- if "icons" in markata.__dict__.keys():
- icons = markata.icons
- else:
- icons = []
+ icons = markata.icons if "icons" in markata.__dict__ else []
manifest = {
- "name": markata.get_config("site_name") or "",
- "short_name": markata.get_config("short_name") or "",
- "start_url": markata.get_config("start_url") or "",
- "display": markata.get_config("display") or "",
- "background_color": markata.get_config("background_color") or "",
- "theme_color": markata.get_config("theme_color") or "",
- "description": markata.get_config("description") or "",
+ "name": markata.config.site_name,
+ "short_name": markata.config.short_name,
+ "start_url": markata.config.start_url,
+ "display": markata.config.display,
+ "background_color": str(markata.config.background_color),
+ "theme_color": str(markata.config.theme_color),
+ "description": markata.config.description,
"icons": icons,
}
filepath = Path(markata.config["output_dir"]) / "manifest.json"
- filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.touch(exist_ok=True)
with open(filepath, "w+") as f:
json.dump(manifest, f, ensure_ascii=True, indent=4)
- config = markata.get_plugin_config(__file__)
- should_prettify = markata.config.get("prettify_html", False)
- with markata.cache as cache:
- for article in markata.iter_articles("add manifest link"):
- key = markata.make_hash(
- "seo",
- "manifest",
- article.content,
- article.html,
- )
- html_from_cache = cache.get(key)
-
- if html_from_cache is None:
- soup = BeautifulSoup(article.html, features="lxml")
- link = soup.new_tag("link")
- link.attrs["rel"] = "manifest"
- link.attrs["href"] = "/manifest.json"
- soup.head.append(link)
-
- if should_prettify:
- html = soup.prettify()
- else:
- html = str(soup)
- cache.add(key, html, expire=config["cache_expire"])
- else:
- html = html_from_cache
- article.html = html
diff --git a/markata/plugins/md_it_highlight_code.py b/markata/plugins/md_it_highlight_code.py
index 87b7f381..80855baa 100644
--- a/markata/plugins/md_it_highlight_code.py
+++ b/markata/plugins/md_it_highlight_code.py
@@ -1,6 +1,6 @@
from pygments import highlight
from pygments.formatters import HtmlFormatter
-from pygments.lexers import get_lexer_by_name
+from pygments.lexers import ClassNotFound, get_lexer_by_name
COPY_ICON = ' '
HELP_ICON = ' '
@@ -9,7 +9,11 @@
def highlight_code(code, name, attrs, markata=None):
"""Code highlighter for markdown-it-py."""
- lexer = get_lexer_by_name(name or "text")
+ try:
+ lexer = get_lexer_by_name(name or "text")
+ except ClassNotFound:
+ lexer = get_lexer_by_name("text")
+
import re
pattern = r'(\w+)\s*=\s*(".*?"|\S+)'
diff --git a/markata/plugins/md_it_wikilinks.py b/markata/plugins/md_it_wikilinks.py
index 185b159e..b648a617 100644
--- a/markata/plugins/md_it_wikilinks.py
+++ b/markata/plugins/md_it_wikilinks.py
@@ -115,18 +115,18 @@ def _wikilinks_inline(state: StateInline, silent: bool):
else:
link, id = text, None
possible_pages = markata.filter(
- f'path.split("/")[-1].split(".")[0].replace("_", "-") == "{link.replace("_", "-")}"'
+ f'str(path).split("/")[-1].split(".")[0].replace("_", "-") == "{link.replace("_", "-")}"',
)
if len(possible_pages) == 1:
link = possible_pages[0].get("slug", f"/{text}")
elif len(possible_pages) > 1:
logger.warning(
- f"wikilink [[{text}]] ({link}, {id}) has duplicate matches, defaulting to the first"
+ f"wikilink [[{text}]] ({link}, {id}) has duplicate matches, defaulting to the first",
)
link = possible_pages[0].get("slug", f"/{text}")
else:
logger.warning(
- f"wikilink [[{text}]] ({link}, {id}) no matches, defaulting to '/{text}'"
+ f"wikilink [[{text}]] ({link}, {id}) no matches, defaulting to '/{text}'",
)
link = text
diff --git a/markata/plugins/mdit_details.py b/markata/plugins/mdit_details.py
index 28b66b63..1e9bdb55 100644
--- a/markata/plugins/mdit_details.py
+++ b/markata/plugins/mdit_details.py
@@ -43,7 +43,7 @@ def details(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bo
maximum = state.eMarks[startLine]
# Check out the first character quickly, which should filter out most of non-containers
- if MARKER_CHAR != ord(state.src[start]):
+ if ord(state.src[start]) != MARKER_CHAR:
return False
# Check out the rest of the marker string
diff --git a/markata/plugins/post_model.py b/markata/plugins/post_model.py
new file mode 100644
index 00000000..07867139
--- /dev/null
+++ b/markata/plugins/post_model.py
@@ -0,0 +1,348 @@
+import datetime
+import logging
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+
+import dateparser
+import pydantic
+import yaml
+from polyfactory.factories.pydantic_factory import ModelFactory
+from pydantic import ConfigDict
+from slugify import slugify
+
+from markata.hookspec import hook_impl, register_attr
+
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+ from pydantic.typing import ReprArgs
+
+ from markata import Markata
+
+
+class Post(pydantic.BaseModel):
+ markata: Any = None
+ path: Path
+ slug: Optional[str] = None
+ href: Optional[str] = None
+ published: bool = False
+ description: Optional[str] = None
+ content: str = None
+ # date: Union[datetime.date, str]=None
+ date: Optional[Union[datetime.date, str]] = None
+ # pydantic.Field(
+ # default_factory=lambda: datetime.date.min
+ # )
+ date_time: Optional[datetime.datetime] = None
+ today: datetime.date = pydantic.Field(default_factory=datetime.date.today)
+ now: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.utcnow)
+ load_time: float = 0
+ profile: Optional[str] = None
+ title: str = None
+ model_config = ConfigDict(
+ validate_assignment=True,
+ arbitrary_types_allowed=True,
+ extra="allow",
+ )
+
+ def __repr_args__(self: "Post") -> "ReprArgs":
+ return [
+ (key, value)
+ for key, value in self.__dict__.items()
+ if key in self.markata.config.post_model.repr_include
+ ]
+
+ @property
+ def metadata(self: "Post") -> Dict:
+ "for backwards compatability"
+ return self.__dict__
+
+ def to_dict(self: "Post") -> Dict:
+ "for backwards compatability"
+ return self.__dict__
+
+ def __getitem__(self: "Post", item: str) -> Any:
+ "for backwards compatability"
+ return getattr(self, item)
+
+ def __setitem__(self: "Post", key: str, item: Any) -> None:
+ "for backwards compatability"
+ setattr(self, key, item)
+
+ def get(self: "Post", item: str, default: Any) -> Any:
+ "for backwards compatability"
+ return getattr(self, item, default)
+
+ def keys(self: "Post") -> List[str]:
+ "for backwards compatability"
+ return self.__dict__.keys()
+
+ # def json(
+ # self: "Post",
+ # include: Iterable = None,
+ # all: bool = False,
+ # **kwargs,
+ # ) -> str:
+ # """
+ # override function to give a default include value that will include
+ # user configured includes.
+ # """
+ # if all:
+ # return pydantic.create_model("Post", **self)(**self).json(
+ # **kwargs,
+ # )
+ # if include:
+ # return pydantic.create_model("Post", **self)(**self).json(
+ # include=include,
+ # **kwargs,
+ # )
+ # return pydantic.create_model("Post", **self)(**self).json(
+ # include={i: True for i in self.markata.config.post_model.include},
+ # **kwargs,
+ # )
+
+ def yaml(self: "Post") -> str:
+ """
+ dump model to yaml
+ """
+ import yaml
+
+ return yaml.dump(
+ self.dict(
+ include={i: True for i in self.markata.config.post_model.include}
+ ),
+ Dumper=yaml.CDumper,
+ )
+
+ def markdown(self: "Post") -> str:
+ """
+ dump model to markdown
+ """
+
+ import yaml
+
+ frontmatter = yaml.dump(
+ self.dict(
+ include={
+ i: True
+ for i in [
+ _i
+ for _i in self.markata.config.post_model.include
+ if _i != "content"
+ ]
+ }
+ ),
+ Dumper=yaml.CDumper,
+ )
+ post = "---\n"
+ post += frontmatter
+ post += "---\n\n"
+
+ if self.content:
+ post += self.content
+ return post
+
+ @classmethod
+ def parse_file(cls, markata, path: Union[Path, str], **kwargs) -> "Post":
+ if isinstance(path, Path):
+ if path.suffix in [".md", ".markdown"]:
+ return cls.parse_markdown(markata=markata, path=path, **kwargs)
+ elif isinstance(path, str):
+ if path.endswith(".md") or path.endswith(".markdown"):
+ return cls.parse_markdown(markata=markata, path=path, **kwargs)
+ return super(Post, cls).parse_file(path, **kwargs)
+
+ @classmethod
+ def parse_markdown(cls, markata, path: Union[Path, str], **kwargs) -> "Post":
+ if isinstance(path, str):
+ path = Path(path)
+ text = path.read_text()
+ try:
+ _, fm, *content = text.split("---\n")
+ content = "---\n".join(content)
+ try:
+ fm = yaml.load(fm, Loader=yaml.CBaseLoader)
+ except yaml.YAMLError:
+ fm = {}
+ except ValueError:
+ fm = {}
+ content = text
+ if fm is None or isinstance(fm, str):
+ fm = {}
+
+ post_args = {
+ "markata": markata,
+ "path": path,
+ "content": content,
+ **fm,
+ }
+
+ return markata.Post(**post_args)
+
+ def dumps(self):
+ """
+ dumps raw article back out
+ """
+ return f"---\n{self.yaml()}\n\n---\n\n{self.content}"
+
+ @pydantic.validator("slug", pre=True, always=True)
+ def default_slug(cls, v, *, values):
+ return v or slugify(str(values["path"].stem))
+
+ @pydantic.validator("slug", pre=True, always=True)
+ def index_slug_is_empty(cls, v, *, values):
+ if v == "index":
+ return ""
+ return v
+
+ @pydantic.validator("href", pre=True, always=True)
+ def default_href(cls, v, *, values):
+ if v:
+ return v
+ return f"/{values['slug'].strip('/')}/".replace("//", "/")
+
+ @pydantic.validator("title", pre=True, always=True)
+ def title_title(cls, v, *, values):
+ title = v or Path(values["path"]).stem.replace("-", " ")
+ return title.title()
+
+ @pydantic.validator("date_time", pre=True, always=True)
+ def dateparser_datetime(cls, v, *, values):
+ if isinstance(v, str):
+ d = dateparser.parse(v)
+ if d is None:
+ raise ValueError(f'"{v}" is not a valid date')
+ return v
+
+ @pydantic.validator("date_time", pre=True, always=True)
+ def date_is_datetime(cls, v, *, values):
+ if v is None and "date" not in values:
+ values["markata"].console.log(f"{values['path']} has no date")
+ return datetime.datetime.now()
+ if v is None and values["date"] is None:
+ values["markata"].console.log(f"{values['path']} has no date")
+ return datetime.datetime.now()
+ if isinstance(v, datetime.datetime):
+ return v
+ if isinstance(values["date"], datetime.datetime):
+ return values["date"]
+ if isinstance(v, datetime.date):
+ return datetime.datetime.combine(v, datetime.time.min)
+ if isinstance(values["date"], datetime.date):
+ return datetime.datetime.combine(values["date"], datetime.time.min)
+ return v
+
+ @pydantic.validator("date_time", pre=True, always=True)
+ def mindate_time(cls, v, *, values):
+ if v is None and "date" not in values:
+ values["markata"].console.log(f"{values['path']} has no date")
+ return datetime.datetime.min
+ if values["date"] is None:
+ values["markata"].console.log(f"{values['path']} has no date")
+ return datetime.datetime.min
+ if isinstance(v, datetime.datetime):
+ return v
+ if isinstance(values["date"], datetime.datetime):
+ return values["date"]
+ if isinstance(v, datetime.date):
+ return datetime.datetime.combine(v, datetime.time.min)
+ if isinstance(values["date"], datetime.date):
+ return datetime.datetime.combine(values["date"], datetime.time.min)
+ return v
+
+ @pydantic.validator("date", pre=True, always=True)
+ def dateparser_date(cls, v, *, values):
+ if v is None:
+ return datetime.date.min
+ if isinstance(v, str):
+ d = cls.markata.precache.get(v)
+ if d is not None:
+ return d
+ d = dateparser.parse(v)
+ if d is None:
+ raise ValueError(f'"{v}" is not a valid date')
+ d = d.date()
+ with cls.markata.cache as cache:
+ cache.add(v, d)
+ return d
+ return v
+
+ # @pydantic.validator("date", pre=True, always=True)
+ # def datetime_is_date(cls, v, *, values):
+ # if isinstance(v, datetime.date):
+ # return v
+ # if isinstance(v, datetime.datetime):
+ # return v.date()
+
+ # @pydantic.validator("date", pre=True, always=True)
+ # def mindate(cls, v, *, values):
+ # if v is None:
+ # return datetime.date.min
+ # return v
+
+
+class PostModelConfig(pydantic.BaseModel):
+ "Configuration for the Post model"
+
+ def __init__(self, **data) -> None:
+ """
+
+ include: post attributes to include by default in Post
+ model serialization.
+ repr_include: post attributes to include by default in Post
+ repr. If `repr_include` is None, it will default to
+ `include`, but it is likely that you want less in the repr
+ than serialized output.
+
+ example:
+
+ ``` toml title='markata.toml'
+ [markata.post_model]
+ include = ['date', 'description', 'published',
+ 'slug', 'title', 'content', 'html']
+ repr_include = ['date', 'description', 'published', 'slug', 'title']
+ ```
+ """
+ super().__init__(**data)
+
+ include: List[str] = [
+ "date",
+ "description",
+ "published",
+ "slug",
+ "title",
+ "content",
+ "html",
+ ]
+ repr_include: Optional[List[str]] = [
+ "date",
+ "description",
+ "published",
+ "slug",
+ "title",
+ ]
+
+ @pydantic.validator("repr_include", pre=True, always=True)
+ def repr_include_validator(cls, v, *, values):
+ if v:
+ return v
+ return values.get("include", None)
+
+
+class Config(pydantic.BaseModel):
+ post_model: PostModelConfig = pydantic.Field(default_factory=PostModelConfig)
+
+
+@hook_impl(trylast=True)
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(Post)
+
+
+@hook_impl(tryfirst=True)
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
+class PostFactory(ModelFactory):
+ __model__ = Post
diff --git a/markata/plugins/post_template.py b/markata/plugins/post_template.py
index 0354f2b9..9161a905 100644
--- a/markata/plugins/post_template.py
+++ b/markata/plugins/post_template.py
@@ -1,7 +1,7 @@
"""
-## Add head configuration
+# Add head configuration
This snippet allows users to configure their head in `markata.toml`.
@@ -69,12 +69,12 @@
```
"""
-import copy
+import inspect
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, List, Union
-from deepmerge import always_merger
import jinja2
+import pydantic
from jinja2 import Template, Undefined
from more_itertools import flatten
@@ -92,6 +92,117 @@ def _fail_with_undefined_error(self, *args, **kwargs):
return ""
+def optional(*fields):
+ def dec(_cls):
+ for field in fields:
+ _cls.__fields__[field].default = None
+ return _cls
+
+ if (
+ fields
+ and inspect.isclass(fields[0])
+ and issubclass(fields[0], pydantic.BaseModel)
+ ):
+ cls = fields[0]
+ fields = cls.__fields__
+ return dec(cls)
+ return dec
+
+
+class Style(pydantic.BaseModel):
+ color_bg: str = "#1f2022"
+ color_bg_code: str = "#1f2022"
+ color_text: str = "#eefbfe"
+ color_link: str = "#fb30c4"
+ color_accent: str = "#e1bd00c9"
+ overlay_brightness: str = ".85"
+ body_width: str = "800px"
+ color_bg_light: str = "#eefbfe"
+ color_bg_code_light: str = "#eefbfe"
+ color_text_light: str = "#1f2022"
+ color_link_light: str = "#fb30c4"
+ color_accent_light: str = "#ffeb00"
+ overlay_brightness_light: str = ".95"
+
+
+@optional
+class StyleOverrides(Style):
+ ...
+
+
+class Meta(pydantic.BaseModel):
+ name: str
+ content: str
+
+
+class Text(pydantic.BaseModel):
+ value: str
+
+
+class Link(pydantic.BaseModel):
+ rel: str = "canonical"
+ href: str
+
+
+class HeadConfig(pydantic.BaseModel):
+ meta: List[Meta] = []
+ link: List[Link] = []
+ text: Union[List[Text], str] = ""
+
+ @pydantic.validator("text", pre=True)
+ def text_to_list(cls, v):
+ if isinstance(v, list):
+ return "\n".join([text["value"] for text in v])
+ return v
+
+ @property
+ def html(self):
+ html = self.text
+ html += "\n"
+ for meta in self.meta:
+ html += f' \n'
+ for link in self.link:
+ html += f' \n'
+ return html
+
+
+class Config(pydantic.BaseModel):
+ head: HeadConfig = HeadConfig()
+ style: Style = Style()
+ post_template: str = None
+
+ @pydantic.validator("post_template", pre=True, always=True)
+ def default_post_template(cls, v):
+ if v is None:
+ return (
+ Path(__file__).parent / "default_post_template.html.jinja"
+ ).read_text()
+ if isinstance(v, Path):
+ return v.read_text()
+ if isinstance(v, str) and Path(v).exists():
+ return Path(v).read_text()
+ return v
+
+
+class PostOverrides(pydantic.BaseModel):
+ head: HeadConfig = HeadConfig()
+ style: Style = StyleOverrides()
+
+
+class Post(pydantic.BaseModel):
+ config_overrides: PostOverrides = PostOverrides()
+
+
+@hook_impl(tryfirst=True)
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
+@hook_impl(tryfirst=True)
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(Post)
+
+
@hook_impl
def configure(markata: "Markata") -> None:
"""
@@ -101,12 +212,12 @@ def configure(markata: "Markata") -> None:
configuration.
"""
- raw_text = markata.config.get("head", {}).get("text", "")
+ # raw_text = "\n".join([t.value for t in markata.config.head.text])
- if isinstance(raw_text, list):
- markata.config["head"]["text"] = "\n".join(
- flatten([t.values() for t in raw_text])
- )
+ # if isinstance(raw_text, list):
+ # markata.config["head"]["text"] = "\n".join(
+ # flatten([t.values() for t in raw_text]),
+ # )
@hook_impl
@@ -117,63 +228,59 @@ def pre_render(markata: "Markata") -> None:
allowing an simpler jinja template. This enablees the use of the
`markata.head.text` list in configuration.
"""
- for article in [a for a in markata.articles if "config_overrides" in a.keys()]:
+ for article in [a for a in markata.articles if "config_overrides" in a]:
raw_text = article.get("config_overrides", {}).get("head", {}).get("text", "")
if isinstance(raw_text, list):
article["config_overrides"]["head"]["text"] = "\n".join(
- flatten([t.values() for t in raw_text])
+ flatten([t.values() for t in raw_text]),
)
@hook_impl
def render(markata: "Markata") -> None:
- if "post_template" in markata.config:
- template_file = markata.config["post_template"]
- else:
- template_file = Path(__file__).parent / "default_post_template.html"
- with open(template_file) as f:
- template = Template(f.read(), undefined=SilentUndefined)
+ template = Template(markata.config.post_template, undefined=SilentUndefined)
if "{{" in str(markata.config.get("head", {})):
- head_template = Template(
- str(markata.config.get("head", {})), undefined=SilentUndefined
+ Template(
+ str(markata.config.get("head", {})),
+ undefined=SilentUndefined,
)
else:
- head_template = None
- head = {}
-
- _full_config = copy.deepcopy(markata.config)
+ pass
+ merged_config = markata.config
for article in [a for a in markata.articles if hasattr(a, "html")]:
- if head_template:
- head = eval(
- head_template.render(
- __version__=__version__,
- config=_full_config,
- **article,
- )
- )
-
- merged_config = {
- **_full_config,
- **{"head": head},
- }
-
- merged_config = always_merger.merge(
- merged_config,
- copy.deepcopy(
- article.get(
- "config_overrides",
- {},
- )
- ),
- )
+ # TODO do we need to handle merge??
+ # if head_template:
+ # head = eval(
+ # head_template.render(
+ # __version__=__version__,
+ # config=_full_config,
+ # **article,
+ # )
+ # )
+
+ # merged_config = {
+ # **_full_config,
+ # **{"head": head},
+ # }
+
+ # merged_config = always_merger.merge(
+ # merged_config,
+ # copy.deepcopy(
+ # article.get(
+ # "config_overrides",
+ # {},
+ # )
+ # ),
+ # )
article.html = template.render(
__version__=__version__,
body=article.html,
toc=markata.md.toc, # type: ignore
config=merged_config,
+ post=article,
**article.metadata,
)
diff --git a/markata/plugins/prevnext.py b/markata/plugins/prevnext.py
index a44f1d53..aac3d416 100644
--- a/markata/plugins/prevnext.py
+++ b/markata/plugins/prevnext.py
@@ -157,7 +157,7 @@ def pre_render(markata: "Markata") -> None:
article.get(
"config_overrides",
{},
- )
+ ),
),
),
**article,
diff --git a/markata/plugins/publish_dev_to_source.py b/markata/plugins/publish_dev_to_source.py
index c590aea5..82f6b6ce 100644
--- a/markata/plugins/publish_dev_to_source.py
+++ b/markata/plugins/publish_dev_to_source.py
@@ -1,9 +1,10 @@
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING
import frontmatter
+import pydantic
-from markata.hookspec import hook_impl
+from markata.hookspec import hook_impl, register_attr
if TYPE_CHECKING:
from markata import Markata
@@ -48,6 +49,17 @@ def join_lines(article):
return "\n".join(lines)
+class PublishDevToSourcePost(pydantic.BaseModel):
+ markata: Markata
+ canonical_url: Optional[str] = None
+
+
+@hook_impl
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(PublishDevToSourcePost)
+
+
@hook_impl
def post_render(markata: "Markata") -> None:
for post in markata.iter_articles(description="saving source documents"):
@@ -60,22 +72,19 @@ def post_render(markata: "Markata") -> None:
article.content = join_lines(article.content)
if "canonical_url" not in article:
- article["canonical_url"] = f'{markata.config["url"]}/{post["slug"]}/'
+ article["canonical_url"] = f"{markata.config.url}/{post.slug}/"
if "published" not in article:
article["published"] = True
if "cover_image" not in article:
- article[
- "cover_image"
- ] = f"{markata.config['images_url']}/{post['slug']}.png"
+ article["cover_image"] = f"{markata.config.images_url}/{post.slug}.png"
post.dev_to = article
@hook_impl
def save(markata: "Markata") -> None:
- output_dir = Path(str(markata.config["output_dir"]))
- output_dir.mkdir(parents=True, exist_ok=True)
+ output_dir = Path(str(markata.config.output_dir))
for post in markata.iter_articles(description="saving source documents"):
with open(output_dir / Path(post["slug"]) / "dev.md", "w+") as f:
f.write(frontmatter.dumps(post.dev_to))
diff --git a/markata/plugins/publish_html.py b/markata/plugins/publish_html.py
index a8f6b6d3..7a5cdd49 100644
--- a/markata/plugins/publish_html.py
+++ b/markata/plugins/publish_html.py
@@ -2,7 +2,7 @@
Sets the articles `output_html` path, and saves the article's `html` to the
`output_html` file.
-## Ouptut Directory
+## Ouptut Directory
Output will always be written inside of the configured `output_dir`
@@ -17,7 +17,7 @@
markata will save the articles `html` to the `output_html` specified in the
articles metadata, loaded from frontmatter.
-### 404 example use case
+## 404 example use case
Here is an example use case of explicitly setting the output_html. By default
markata will turn `pages/404.md` into `markout/404/index.html`, but many
@@ -55,41 +55,71 @@
"""
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Dict, Optional
-from markata.hookspec import hook_impl
+import pydantic
+from slugify import slugify
+
+from markata.hookspec import hook_impl, register_attr
if TYPE_CHECKING:
from markata import Markata
-def _is_relative_to(output_dir: Path, output_html: Path):
- try:
- output_html.relative_to(output_dir)
- return True
- except ValueError:
- return False
+class OutputHTML(pydantic.BaseModel):
+ markata: Any = None
+ path: Path
+ slug: str = None
+ output_html: Path = None
+
+ @pydantic.validator("slug", pre=True, always=True)
+ @classmethod
+ def default_slug(cls, v, *, values):
+ if v is None:
+ return slugify(str(values["path"].stem))
+ return v
+
+ @pydantic.validator("output_html", pre=True, always=True)
+ def default_output_html(
+ cls: "OutputHTML", v: Optional[Path], *, values: Dict
+ ) -> Path:
+ if isinstance(v, str):
+ v = Path(v)
+ if v is not None:
+ return v
+ if "slug" not in values:
+ for validator in cls.__validators__["slug"]:
+ values["slug"] = validator.func(cls, v, values=values)
+
+ if values["slug"] == "index":
+ return cls.markata.config.output_dir / "index.html"
+ return cls.markata.config.output_dir / values["slug"] / "index.html"
+
+ @pydantic.validator("output_html")
+ def output_html_relative(
+ cls: "OutputHTML", v: Optional[Path], *, values: Dict
+ ) -> Path:
+ if isinstance(v, str):
+ v = Path(v)
+ if cls.markata.config.output_dir.absolute() not in v.absolute().parents:
+ return cls.markata.config.output_dir / v
+ return v
+
+ @pydantic.validator("output_html")
+ def output_html_exists(
+ cls: "OutputHTML", v: Optional[Path], *, values: Dict
+ ) -> Path:
+ if isinstance(v, str):
+ v = Path(v)
+ if not v.parent.exists():
+ v.parent.mkdir(parents=True, exist_ok=True)
+ return v
@hook_impl
-def pre_render(markata: "Markata") -> None:
- """
- Sets the `output_html` in the articles metadata. If the output is
- explicitly given, it will make sure its in the `output_dir`, if it is not
- explicitly set it will use the articles slug.
- """
- output_dir = Path(markata.config["output_dir"]) # type: ignore
- output_dir.mkdir(parents=True, exist_ok=True)
-
- for article in markata.articles:
- if "output_html" in article.metadata:
- article_path = Path(article["output_html"])
- if not _is_relative_to(output_dir, article_path):
- article["output_html"] = output_dir / article["output_html"]
- elif article["slug"] == "index":
- article["output_html"] = output_dir / "index.html"
- else:
- article["output_html"] = output_dir / article["slug"] / "index.html"
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(OutputHTML)
@hook_impl
@@ -99,19 +129,6 @@ def save(markata: "Markata") -> None:
is relative to the specified `output_dir`. If its not relative to the
`output_dir` it will log an error and move on.
"""
- output_dir = Path(markata.config["output_dir"]) # type: ignore
for article in markata.articles:
- article_path = Path(article["output_html"])
- if _is_relative_to(output_dir, article_path):
- article_path.parent.mkdir(parents=True, exist_ok=True)
- with open(article_path, "w+") as f:
- f.write(article.html)
- else:
- markata.console.log(
- f'article "{article["path"]}" '
- f"attempted to write to "
- f'"{article["output_html"]}"'
- f"outside of the configured output_dir "
- f'"{output_dir}"'
- )
+ article.output_html.write_text(article.html)
diff --git a/markata/plugins/publish_source.py b/markata/plugins/publish_source.py
index db9f032b..6f53a3fe 100644
--- a/markata/plugins/publish_source.py
+++ b/markata/plugins/publish_source.py
@@ -39,18 +39,23 @@ def _save(output_dir: Path, article: frontmatter.Post) -> None:
"""
saves the article to the output directory at its specified slug.
"""
- with open(
- output_dir / Path(article["slug"]).parent / Path(article["path"]).name, "w+"
- ) as f:
- f.write(frontmatter.dumps(article))
+ path = Path(
+ output_dir / Path(article["slug"]).parent / Path(article["path"]).name,
+ )
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(article.dumps())
-def _strip_unserializable_values(article: frontmatter.Post) -> frontmatter.Post:
+def _strip_unserializable_values(
+ markata: "Markata",
+ article: frontmatter.Post,
+) -> frontmatter.Post:
"""
Returns an article with only yaml serializable frontmatter.
"""
_article = frontmatter.Post(
- article.content, **{k: v for k, v in article.metadata.items() if k != "content"}
+ article.content,
+ **{k: v for k, v in article.metadata.items() if k != "content"},
)
kwargs = {
"Dumper": yaml.cyaml.CSafeDumper,
@@ -62,6 +67,8 @@ def _strip_unserializable_values(article: frontmatter.Post) -> frontmatter.Post:
yaml.dump({key: value}, **kwargs)
except RepresenterError:
del _article[key]
+ if markata.Post:
+ _article = markata.Post(**_article.metadata, path=str(article.path))
return _article
@@ -75,11 +82,12 @@ def save(markata: "Markata") -> None:
"""
output_dir = Path(str(markata.config["output_dir"]))
- output_dir.mkdir(parents=True, exist_ok=True)
- for article in markata.iter_articles(description="saving source documents"):
+ for (
+ article
+ ) in markata.articles: # iter_articles(description="saving source documents"):
try:
_save(output_dir, article)
except RepresenterError:
- _article = _strip_unserializable_values(article)
+ _article = _strip_unserializable_values(markata, article)
_save(output_dir, _article)
diff --git a/markata/plugins/pyinstrument.py b/markata/plugins/pyinstrument.py
index a6c98f84..a6520510 100644
--- a/markata/plugins/pyinstrument.py
+++ b/markata/plugins/pyinstrument.py
@@ -4,68 +4,92 @@
The profile will be saved to /_profile/index.html
"""
from pathlib import Path
+from typing import Any, Optional
+
+import pydantic
from markata import Markata
from markata.hookspec import hook_impl, register_attr
try:
from pyinstrument import Profiler
+
+ SHOULD_PROFILE = True
except ModuleNotFoundError:
+ SHOULD_PROFILE = False
"ignore if pyinstrument does not exist"
...
-class MarkataInstrument(Markata):
- should_profile = False
- profiler = None
+class ProfilerConfig(pydantic.BaseModel):
+ output_dir: pydantic.DirectoryPath = Path("markout")
+ should_profile: bool = SHOULD_PROFILE
+ profiler: Optional[
+ Any
+ ] = None # No valicator for type pyinstrument.profiler.Profiler
+ output_file: Optional[Path] = None
+ @pydantic.validator("output_file", pre=True, always=True)
+ def validate_output_file(cls, v, *, values):
+ if v is None:
+ output_file = values["output_dir"] / "_profile" / "index.html"
+ output_file.parent.mkdir(parents=True, exist_ok=True)
+ return output_file
+ return v
-@hook_impl(tryfirst=True)
-@register_attr("should_profile", "profiler")
-def configure(markata: MarkataInstrument) -> None:
- "set the should_profile variable"
- markata.profiler = None
- if "should_profile" not in markata.__dict__.keys():
- markata.should_profile = markata.config.get("pyinstrument", {}).get(
- "should_profile", True
- )
+class Config(pydantic.BaseModel):
+ profiler: ProfilerConfig = ProfilerConfig()
- if markata.should_profile and "profiler" not in markata.__dict__.keys():
- try:
- markata.profiler = Profiler(async_mode="disabled")
- markata.profiler.start()
- except NameError:
- "ignore if Profiler does not exist"
- ...
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
+@hook_impl
+def configure(markata: "Markata") -> None:
+ # profiler must exist in the same thread and cannot be configured through pydantic validation
+ if (
+ markata.config.profiler.should_profile
+ and markata.config.profiler.profiler is None
+ ):
+ markata.config.profiler.profiler = Profiler()
+ markata.config.profiler.profiler.start()
@hook_impl(trylast=True)
-def save(markata: MarkataInstrument) -> None:
+def save(markata: Markata) -> None:
"stop the profiler and save as late as possible"
- if markata.should_profile:
- try:
- if "profiler" in markata.__dict__.keys():
- output_file = (
- Path(markata.config["output_dir"]) / "_profile" / "index.html"
- )
- output_file.parent.mkdir(parents=True, exist_ok=True)
- markata.profiler.stop()
- html = markata.profiler.output_html()
- output_file.write_text(html)
- markata.console.print(markata.profiler.output_text())
-
- except AttributeError:
- "ignore if markata does not have a profiler attribute"
- ...
+ if markata.config.profiler.should_profile:
+ if markata.config.profiler.profiler is not None:
+ if markata.config.profiler.profiler.is_running:
+ try:
+ markata.config.profiler.profiler.stop()
+ html = markata.config.profiler.profiler.output_html()
+ markata.config.profiler.output_file.write_text(html)
+ markata.console.print(
+ markata.config.profiler.profiler.output_text()
+ )
+
+ except AttributeError:
+ markata.console.log(
+ "profiler not available, skipping save pyinstrument save",
+ )
+ markata.console.log(
+ "[red]to enable profiler [wheat1][itallic]pip install 'markata\[pyinstrument]'",
+ )
@hook_impl
-def teardown(markata: MarkataInstrument) -> None:
+def teardown(markata: Markata) -> None:
"stop the profiler on exit"
+ # import logging
- if markata.should_profile:
- if hasattr(markata, "profiler"):
- if markata.profiler is not None:
- if markata.profiler.is_running:
- markata.profiler.stop()
+ # logger = logging.getLogger()
+ # logger.handlers.clear()
+ if markata.config.profiler.should_profile:
+ if markata.config.profiler.profiler is not None:
+ if markata.config.profiler.profiler.is_running:
+ markata.config.profiler.profiler.stop()
diff --git a/markata/plugins/redirects.py b/markata/plugins/redirects.py
index d905dad4..64270828 100644
--- a/markata/plugins/redirects.py
+++ b/markata/plugins/redirects.py
@@ -63,7 +63,7 @@
| Redirect by country or language | No | `/ /us 302 Country=us` | ... |
| Redirect by cookie | No | `/* /preview/:splat 302 Cookie=preview` | ... |
-> Compare with
+> Compare with
> [cloudflare-pages](https://developers.cloudflare.com/pages/platform/redirects/)
!!! tip
@@ -72,25 +72,47 @@
moved without you realizing.
"""
-from dataclasses import asdict, dataclass
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import Dict, Optional
from jinja2 import Template
+import pydantic
+from markata import Markata
from markata.hookspec import hook_impl
-
-if TYPE_CHECKING:
- from markata import Markata
+from pydantic import ConfigDict
DEFAULT_REDIRECT_TEMPLATE = Path(__file__).parent / "default_redirect_template.html"
-@dataclass
-class Redirect:
+class Redirect(pydantic.BaseModel):
"DataClass to store the original and new url"
original: str
new: str
+ markata: Markata
+ model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
+
+
+class RedirectsConfig(pydantic.BaseModel):
+ assets_dir: Path = Path("static")
+ redirects_file: Optional[Path] = None
+
+ @pydantic.validator("redirects_file", always=True)
+ def default_redirects_file(
+ cls: "RedirectsConfig", v: Path, *, values: Dict
+ ) -> Path:
+ if not v:
+ return Path(values["assets_dir"]) / "_redirects"
+ return v
+
+
+class Config(pydantic.BaseModel):
+ redirects: RedirectsConfig = RedirectsConfig()
+
+
+@hook_impl(tryfirst=True)
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
@hook_impl
@@ -98,17 +120,14 @@ def save(markata: "Markata") -> None:
"""
saves an index.html in the directory called out by the redirect.
"""
- assets_dir: str = str(markata.config.get("assets_dir", "static"))
- redirects_file = Path(
- str(markata.config.get("redirects", Path(assets_dir) / "_redirects"))
- )
+ redirects_file = Path(markata.config.redirects.redirects_file)
if redirects_file.exists():
raw_redirects = redirects_file.read_text().split("\n")
else:
raw_redirects = []
redirects = [
- Redirect(*s)
+ Redirect(original=s[0], new=s[1], markata=markata)
for r in raw_redirects
if "*" not in r and len(s := r.split()) == 2 and not r.strip().startswith("#")
]
@@ -119,10 +138,7 @@ def save(markata: "Markata") -> None:
template_file = DEFAULT_REDIRECT_TEMPLATE
template = Template(template_file.read_text())
- output_dir = Path(markata.config["output_dir"]) # type: ignore
- output_dir.mkdir(parents=True, exist_ok=True)
-
for redirect in redirects:
- file = output_dir / redirect.original.strip("/") / "index.html"
+ file = markata.config.output_dir / redirect.original.strip("/") / "index.html"
file.parent.mkdir(parents=True, exist_ok=True)
- file.write_text(template.render(**asdict(redirect), config=markata.config))
+ file.write_text(template.render(redirect.dict(), config=markata.config))
diff --git a/markata/plugins/render_markdown.py b/markata/plugins/render_markdown.py
index a8a4e921..979ebc88 100644
--- a/markata/plugins/render_markdown.py
+++ b/markata/plugins/render_markdown.py
@@ -85,38 +85,77 @@
"""
import copy
+from enum import Enum
import importlib
-from typing import List, TYPE_CHECKING
+from typing import Dict, List, Optional, TYPE_CHECKING
-import markdown
+import pydantic
-from markata import DEFAULT_MD_EXTENSIONS
from markata.hookspec import hook_impl, register_attr
from markata.plugins.md_it_highlight_code import highlight_code
if TYPE_CHECKING:
from markata import Markata
- class MarkataMarkdown(Markata):
- articles: List = []
- md: markdown.Markdown = markdown.Markdown()
- markdown_extensions: List = []
+
+class Backend(str, Enum):
+ markdown = "markdown"
+ markdown2 = "markdown2"
+ markdown_it_py = "markdown-it-py"
+
+
+class MdItExtension(pydantic.BaseModel):
+ plugin: str
+ config: Dict = None
+
+
+class RenderMarkdownConfig(pydantic.BaseModel):
+ backend: Backend = Backend("markdown-it-py")
+ extensions: List[MdItExtension] = []
+ cache_expire: int = 3600
+
+ @pydantic.validator("extensions")
+ def convert_to_list(cls, v):
+ if not isinstance(v, list):
+ return [v]
+ return v
+
+
+class Config(pydantic.BaseModel):
+ render_markdown: RenderMarkdownConfig = RenderMarkdownConfig()
+
+
+class RenderMarkdownPost(pydantic.BaseModel):
+ article_html: Optional[str] = None
+ html: Optional[str] = None
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
+@hook_impl()
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(RenderMarkdownPost)
@hook_impl(tryfirst=True)
@register_attr("md", "markdown_extensions")
-def configure(markata: "MarkataMarkdown") -> None:
+def configure(markata: "Markata") -> None:
"Sets up a markdown instance as md"
- if "markdown_extensions" not in markata.config:
- markdown_extensions = [""]
- if isinstance(markata.config["markdown_extensions"], str):
- markdown_extensions = [markata.config["markdown_extensions"]]
- if isinstance(markata.config["markdown_extensions"], list):
- markdown_extensions = markata.config["markdown_extensions"]
- else:
- raise TypeError("markdown_extensions should be List[str]")
+ # if "markdown_extensions" not in markata.config:
+ # markdown_extensions = [""]
+ # if isinstance(markata.config["markdown_extensions"], str):
+ # markdown_extensions = [markata.config["markdown_extensions"]]
+ # if isinstance(markata.config["markdown_extensions"], list):
+ # markdown_extensions = markata.config["markdown_extensions"]
+ # else:
+ # raise TypeError("markdown_extensions should be List[str]")
- markata.markdown_extensions = [*DEFAULT_MD_EXTENSIONS, *markdown_extensions]
+ # markata.markdown_extensions = [*DEFAULT_MD_EXTENSIONS, *markdown_extensions]
if (
markata.config.get("markdown_backend", "")
@@ -154,7 +193,7 @@ def configure(markata: "MarkataMarkdown") -> None:
markata.md.disable(plugin)
plugins = copy.deepcopy(
- markata.config.get("markdown_it_py", {}).get("plugins", [])
+ markata.config.get("markdown_it_py", {}).get("plugins", []),
)
for plugin in plugins:
if isinstance(plugin["plugin"], str):
@@ -166,7 +205,7 @@ def configure(markata: "MarkataMarkdown") -> None:
plugin_func,
)
plugin["config"] = plugin.get("config", {})
- for k, v in plugin["config"].items():
+ for k, _v in plugin["config"].items():
if k == "markata":
plugin["config"][k] = markata
@@ -183,18 +222,22 @@ def configure(markata: "MarkataMarkdown") -> None:
):
import markdown2
- markata.md = markdown2.Markdown(extras=markata.markdown_extensions)
+ markata.md = markdown2.Markdown(
+ extras=markata.config.render_markdown.extensions
+ )
markata.md.toc = ""
else:
import markdown
- markata.md = markdown.Markdown(extensions=markata.markdown_extensions)
+ markata.md = markdown.Markdown(
+ extensions=markata.config.render_markdown.extensions
+ )
@hook_impl(tryfirst=True)
-@register_attr("articles")
+@register_attr("articles", "posts")
def render(markata: "Markata") -> None:
- config = markata.get_plugin_config(__file__)
+ config = markata.config.render_markdown
with markata.cache as cache:
for article in markata.iter_articles("rendering markdown"):
key = markata.make_hash(
@@ -202,14 +245,14 @@ def render(markata: "Markata") -> None:
"render",
article.content,
)
- html_from_cache = cache.get(key)
+ html_from_cache = markata.precache.get(key)
if html_from_cache is None:
html = markata.md.convert(article.content)
- cache.add(key, html, expire=config["cache_expire"])
+ cache.add(key, html, expire=config.cache_expire)
else:
html = html_from_cache
article.html = html
article.article_html = copy.deepcopy(html)
- article["html"] = html
- article["article_html"] = article.article_html
+ article.html = html
+ article.article_html = article.article_html
diff --git a/markata/plugins/rss.py b/markata/plugins/rss.py
index 22f2b95b..ce8e293f 100644
--- a/markata/plugins/rss.py
+++ b/markata/plugins/rss.py
@@ -1,7 +1,9 @@
"""Default glob plugin"""
+import datetime
from pathlib import Path
from typing import TYPE_CHECKING
+import pytz
from feedgen.feed import FeedGenerator
from markata.hookspec import hook_impl
@@ -17,42 +19,48 @@ class MarkataRss(Markata):
@hook_impl(trylast=True)
def render(markata: "MarkataRss") -> None:
fg = FeedGenerator()
- url = markata.get_config("url") or ""
- title = markata.get_config("title") or "rss_feed"
- name = markata.get_config("author_name") or ""
- email = markata.get_config("author_email") or ""
- icon = markata.get_config("icon") or ""
- lang = markata.get_config("lang") or ""
- rss_description = markata.get_config("rss_description") or "rss_feed"
+ url = markata.config.url or ""
+ title = markata.config.title
+ name = markata.config.author_name
+ email = markata.config.author_email
+ icon = str(markata.config.icon)
+ lang = markata.config.lang
+ rss_description = markata.config.rss_description or "rss feed"
- fg.id(url + "/rss.xml")
+ fg.id(str(url) + "/rss.xml")
fg.title(title)
fg.author(
{
"name": name,
"email": email,
- }
+ },
)
- fg.link(href=url, rel="alternate")
+ fg.link(href=str(url), rel="alternate")
fg.logo(icon)
fg.subtitle(rss_description)
- fg.link(href=url + "/rss.xml", rel="self")
+ fg.link(href=str(url) + "/rss.xml", rel="self")
fg.language(lang)
try:
- all_posts = reversed(sorted(markata.articles, key=lambda x: x["date"]))
+ all_posts = sorted(markata.articles, key=lambda x: x["date"], reverse=True)
posts = [post for post in all_posts if post["published"] == "True"]
except BaseException:
posts = markata.articles
for article in posts:
fe = fg.add_entry()
- fe.id(url + "/" + article["slug"])
- fe.title(article.metadata["title"])
- fe.published(article.metadata["datetime"])
- fe.description(article.metadata["description"])
- fe.summary(article.metadata["long_description"])
- fe.link(href=url + "/" + article["slug"])
+ fe.id(str(url + "/" + article.slug))
+ fe.title(article.title)
+ fe.published(
+ datetime.datetime.combine(
+ article.date or datetime.datetime.min.date(),
+ datetime.datetime.min.time(),
+ pytz.UTC,
+ )
+ )
+ fe.description(article.description)
+ fe.summary(article.long_description)
+ fe.link(href=str(url) + "/" + article.slug)
fe.content(article.article_html.translate(dict.fromkeys(range(32))))
markata.fg = fg
diff --git a/markata/plugins/seo.py b/markata/plugins/seo.py
index c5183fd8..a14e007d 100644
--- a/markata/plugins/seo.py
+++ b/markata/plugins/seo.py
@@ -1,6 +1,6 @@
"""manifest plugin"""
import logging
-from typing import TYPE_CHECKING, Any, Dict, List
+from typing import Any, Dict, List, TYPE_CHECKING
from bs4 import BeautifulSoup
@@ -29,7 +29,7 @@ def _create_seo(
if article.metadata["description"] == "" or None:
try:
article.metadata["description"] = " ".join(
- [p.text for p in soup.find(id="post-body").find_all("p")]
+ [p.text for p in soup.find(id="post-body").find_all("p")],
).strip()[:120]
except AttributeError:
article.metadata["description"] = ""
@@ -139,8 +139,8 @@ def _create_seo_tag(meta: dict, soup: BeautifulSoup) -> "Tag":
def _get_or_warn(config: Dict, key: str, default: str) -> Any:
if key not in config.keys():
- logger.warn(
- f"{key} is missing from markata.toml config, using default value {default}"
+ logger.warning(
+ f"{key} is missing from markata.toml config, using default value {default}",
)
return config.get(key, default)
@@ -171,7 +171,7 @@ def render(markata: Markata) -> None:
str(config_seo),
)
- html_from_cache = cache.get(key)
+ html_from_cache = markata.precache.get(key)
if html_from_cache is None:
soup = BeautifulSoup(article.html, features="lxml")
@@ -205,11 +205,8 @@ def render(markata: Markata) -> None:
meta_url.attrs["content"] = f'{url}/{article.metadata["slug"]}/'
soup.head.append(meta_url)
- if should_prettify:
- html = soup.prettify()
- else:
- html = str(soup)
- cache.add(key, html, expire=markata.config["default_cache_expire"])
+ html = soup.prettify() if should_prettify else str(soup)
+ cache.add(key, html, expire=markata.config.default_cache_expire)
else:
html = html_from_cache
diff --git a/markata/plugins/service_worker.js b/markata/plugins/service_worker.js
new file mode 100644
index 00000000..25c63882
--- /dev/null
+++ b/markata/plugins/service_worker.js
@@ -0,0 +1,69 @@
+/*
+ Copyright 2016 Google Inc. All Rights Reserved.
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+// Names of the two caches used in this version of the service worker.
+// Change to v2, etc. when you update any of the local resources, which will
+// in turn trigger the install event again.
+const PRECACHE = '0d062b54dad9e26b12c7e398a83de193';
+const RUNTIME = '0d062b54dad9e26b12c7e398a83de193';
+
+// A list of local resources we always want to be cached.
+const PRECACHE_URLS = ['8bitcc.ico', 'archive', 'archive-styles.css', 'furo-purge.min.css', 'main.min.css', 'manifest.json', 'one-dark.min.css', 'scroll.css'];
+
+// The install handler takes care of precaching the resources we always need.
+self.addEventListener('install', event => {
+ event.waitUntil(
+ caches.open(PRECACHE)
+ .then(cache => cache.addAll(PRECACHE_URLS))
+ .then(self.skipWaiting())
+ );
+});
+
+// The activate handler takes care of cleaning up old caches.
+self.addEventListener('activate', event => {
+ const currentCaches = [PRECACHE, RUNTIME];
+ event.waitUntil(
+ caches.keys().then(cacheNames => {
+ return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
+ }).then(cachesToDelete => {
+ return Promise.all(cachesToDelete.map(cacheToDelete => {
+ return caches.delete(cacheToDelete);
+ }));
+ }).then(() => self.clients.claim())
+ );
+});
+
+// The fetch handler serves responses for same-origin resources from a cache.
+// If no response is found, it populates the runtime cache with the response
+// from the network before returning it to the page.
+self.addEventListener('fetch', event => {
+ // Skip cross-origin requests, like those for Google Analytics.
+ if (event.request.url.startsWith(self.location.origin)) {
+ event.respondWith(
+ caches.match(event.request).then(cachedResponse => {
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ return caches.open(RUNTIME).then(cache => {
+ return fetch(event.request).then(response => {
+ // Put a copy of the response in the runtime cache.
+ return cache.put(event.request, response.clone()).then(() => {
+ return response;
+ });
+ });
+ });
+ })
+ );
+ }
+});
\ No newline at end of file
diff --git a/markata/plugins/service_worker.py b/markata/plugins/service_worker.py
index 372b46fd..0afe3f81 100644
--- a/markata/plugins/service_worker.py
+++ b/markata/plugins/service_worker.py
@@ -3,7 +3,7 @@
viewable offline, and potentially more responsive as the user goes between good
and bad connections.
-## Configuration
+# Configuration
Enable this plugin by adding it to your `markata.toml` hooks list.
@@ -24,12 +24,12 @@
precache_urls = ['archive-styles.css', 'scroll.css', 'manifest.json']
```
-## cache busting
+# cache busting
Markata uses the checksum.dirhash of your output directory as the cache key.
This is likely to change and bust the cache on every build.
-## pre-caching feeds
+# pre-caching feeds
You can add and entire feed to your precache, this will automatically load
these posts into the cache anytime someone visits your site and their browser
@@ -50,18 +50,43 @@
"""
import copy
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import List, Optional, TYPE_CHECKING
from checksumdir import dirhash
from jinja2 import Template
+import pydantic
from markata import __version__
from markata.hookspec import hook_impl
+from pydantic import ConfigDict
if TYPE_CHECKING:
from markata import Markata
-DEFAULT_PRECACHE_URLS = ["index.html", "./"]
+
+class ServiceWorkerConfig(pydantic.BaseModel):
+ output_dir: pydantic.DirectoryPath = None
+ precache_urls: List[str] = ["index.html", "./"]
+ precache_posts: bool = False
+ precache_feeds: bool = False
+ template_file: Optional[Path] = None
+ template: Optional[Template] = None
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+
+ @pydantic.validator("template_file", always=True, pre=True)
+ def validate_template_file(cls, v):
+ if v is None:
+ return Path(__file__).parent / "default_service_worker_template.js"
+ return v
+
+
+class Config(pydantic.BaseModel):
+ service_worker: ServiceWorkerConfig = ServiceWorkerConfig()
+
+
+@hook_impl(tryfirst=True)
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
@hook_impl(trylast=True)
@@ -71,17 +96,17 @@ def render(markata: "Markata") -> None:
`markata.plugins.service_worker.save`.
"""
- markata.config["precache_urls"] = markata.config.get("precache_urls", [])
- markata.config["precache_urls"].extend(DEFAULT_PRECACHE_URLS)
+ config = markata.config.service_worker
- for feed, config in markata.config.get("feeds").items():
- markata.config["precache_urls"].append(f"/{feed}/")
+ if config.precache_feeds:
+ for feed, config in markata.config.feeds:
+ config.precache_urls.append(f"/{feed}/")
- if config.get("precache", False):
- for post in markata.map("post", **config):
- markata.config["precache_urls"].append(f'/{post.get("slug", "")}/')
+ if config.precache_posts:
+ for post in markata.map("post", **config):
+ config.precache_urls.append(f'/{post.get("slug", "")}/')
- markata.config["precache_urls"] = list(set(markata.config["precache_urls"]))
+ config.precache_urls = list(set(config.precache_urls))
@hook_impl(trylast=True)
@@ -90,19 +115,12 @@ def save(markata: "Markata") -> None:
Renders the service-worker.js file with your precache urls, and dirhash.
"""
- if "service_worker_template" in markata.config:
- template_file = markata.config["service_worker_template"]
- else:
- template_file = Path(__file__).parent / "default_service_worker_template.js"
- with open(template_file) as f:
- template = Template(f.read())
-
- output_dir = Path(markata.config.get("output_dir", "markout"))
- service_worker_file = output_dir / "service-worker.js"
+ template = Template(markata.config.service_worker.template_file.read_text())
service_worker_js = template.render(
__version__=__version__,
config=copy.deepcopy(markata.config),
- output_dirhash=dirhash(output_dir),
+ output_dirhash=dirhash(markata.config.output_dir),
)
- service_worker_file.write_text(service_worker_js)
+ output_file = markata.config.output_dir / "service-worker.js"
+ output_file.write_text(service_worker_js)
diff --git a/markata/plugins/setup_logging.py b/markata/plugins/setup_logging.py
index a818f847..ffb58aca 100644
--- a/markata/plugins/setup_logging.py
+++ b/markata/plugins/setup_logging.py
@@ -4,24 +4,24 @@
`log_dir` is not configured. The log file will be named after the
`.log`
-## The log files
+# The log files
There will be 6 log files created based on log level and file type.
```
markout/_logs
├── debug
-│ └── index.html
+│ └── index.html
├── debug.log
├── info
-│ └── index.html
+│ └── index.html
├── info.log
├── warning
-│ └── index.html
+│ └── index.html
└── warning.log
```
-## Configuration
+# Configuration
Ensure that setup_logging is in your hooks. You can check if `setup_logging`
is in your hooks by running `markata list --hooks` from your terminal and
@@ -38,7 +38,7 @@
]
```
-## Log Template
+# Log Template
``` toml
[markata]
@@ -54,7 +54,7 @@
You can see the latest default `log_template` on
[GitHub](https://github.com/WaylonWalker/markata/blob/main/markata/plugins/default_log_template.html)
-## Disable Logging
+# Disable Logging
If you do not want logging, you can explicityly disable it by adding it to your
`[markata.disabled_hooks]` array in your `[markata.toml]`
@@ -71,14 +71,15 @@
"""
import datetime
import logging
-import sys
from pathlib import Path
-from typing import TYPE_CHECKING
+import sys
+from typing import Optional, TYPE_CHECKING
from jinja2 import Template, Undefined
+import pydantic
from rich.logging import RichHandler
-from markata.hookspec import hook_impl
+from markata.hookspec import hook_impl, register_attr
if TYPE_CHECKING:
from markata import Markata
@@ -115,22 +116,38 @@ def setup_log(markata: "Markata", level: int = logging.INFO) -> Path:
return path
+class LoggingConfig(pydantic.BaseModel):
+ output_dir: pydantic.DirectoryPath = Path("markout")
+ log_dir: Optional[Path] = None
+ template: Optional[Path] = Path(__file__).parent / "default_log_template.html"
+
+ @pydantic.validator("log_dir", pre=True, always=True)
+ def validate_log_dir(cls, v, *, values):
+ if v is None:
+ return values["output_dir"] / "_logs"
+ return Path(v)
+
+
+class Config(pydantic.BaseModel):
+ logging: LoggingConfig = LoggingConfig()
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
def setup_text_log(markata: "Markata", level: int = logging.INFO) -> Path:
"""
sets up a plain text log in markata's configured `log_dir`, or
`output_dir/_logs` if `log_dir` is not configured. The log file will be
named after the `.log`
"""
- log_file = Path(
- str(
- markata.config.get(
- "log_dir",
- Path(str(markata.config.get("output_dir", "markout")))
- / "_logs"
- / (logging.getLevelName(level).lower() + ".log"),
- )
- )
+ log_file = markata.config.logging.log_dir / (
+ logging.getLevelName(level).lower() + ".log"
)
+
if has_file_handler(log_file):
return log_file
@@ -154,34 +171,23 @@ def setup_html_log(markata: "Markata", level: int = logging.INFO) -> Path:
named after the `/index.html`. The goal of this is to give
"""
- log_file = Path(
- str(
- markata.config.get(
- "log_dir",
- Path(str(markata.config.get("output_dir", "markout")))
- / "_logs"
- / logging.getLevelName(level).lower()
- / "index.html",
- )
- )
+ log_file = (
+ markata.config.logging.log_dir
+ / logging.getLevelName(level).lower()
+ / "index.html"
)
if has_file_handler(log_file):
return log_file
- if not log_file.parent.exists():
- log_file.parent.mkdir(parents=True)
+ log_file.parent.mkdir(parents=True, exist_ok=True)
+
if not log_file.exists():
- template_file = Path(
- str(
- markata.config.get(
- "log_template", Path(__file__).parent / "default_log_template.html"
- )
- )
+ template = Template(
+ markata.config.logging.template.read_text(), undefined=SilentUndefined
)
- template = Template(template_file.read_text(), undefined=SilentUndefined)
log_header = template.render(
- title=str(markata.config.get("title", "markata build")) + " logs",
+ title=markata.config.title + " logs",
config=markata.config,
)
log_file.write_text(log_header)
@@ -190,7 +196,7 @@ def setup_html_log(markata: "Markata", level: int = logging.INFO) -> Path:
f.write(
f"""
Path:
{datetime.datetime.now()} running "{command}"
- """
+ """,
)
fh = logging.FileHandler(log_file)
fh.setLevel(level)
@@ -213,7 +219,7 @@ def setup_html_log(markata: "Markata", level: int = logging.INFO) -> Path:
%(message)s
- """
+ """,
)
fh.setFormatter(fh_formatter)
logging.getLogger("").addHandler(fh)
diff --git a/markata/plugins/sitemap.py b/markata/plugins/sitemap.py
index bc7326d2..1fdc7262 100644
--- a/markata/plugins/sitemap.py
+++ b/markata/plugins/sitemap.py
@@ -1,28 +1,54 @@
from pathlib import Path
+from typing import Optional
import anyconfig
+import pydantic
from markata import Markata
from markata.hookspec import hook_impl, register_attr
+class SiteMapUrl(pydantic.BaseModel):
+ slug: str = pydantic.Field(..., exclude=True)
+ loc: str = pydantic.Field(None, include=True)
+ changefreq: str = pydantic.Field("daily", include=True)
+ priority: str = pydantic.Field("0.7", include=True)
+
+ @pydantic.validator("loc")
+ def default_loc(cls, v, *, values):
+ if v is None:
+ return cls.markata.config.url + "/" + values["slug"] + "/"
+ return v
+
+ def dict(self, *args, **kwargs):
+ return {"url": {**super().dict(*args, **kwargs)}}
+
+
+class SiteMapPost(pydantic.BaseModel):
+ slug: str = None
+ published: bool = True
+ sitemap_url: Optional[SiteMapUrl] = None
+
+ @pydantic.validator("sitemap_url", pre=False, always=True)
+ def default_loc(cls, v, *, values):
+ if v is None:
+ return SiteMapUrl(markata=cls.markata, slug=values["slug"])
+ if v.markata is None:
+ return SiteMapUrl(markata=cls.markata, slug=values["slug"])
+ return v
+
+
+@hook_impl()
+@register_attr("post_models")
+def post_model(markata: "Markata") -> None:
+ markata.post_models.append(SiteMapPost)
+
+
@hook_impl
@register_attr("sitemap")
def render(markata: Markata) -> None:
- url = markata.get_config("url") or ""
-
sitemap = {
- "urlset": [
- {
- "url": {
- "loc": url + "/" + article["slug"] + "/",
- "changefreq": "daily",
- "priority": "0.7",
- }
- }
- for article in markata.articles
- if article["published"] == "True"
- ]
+ "urlset": markata.map("post.sitemap_url.dict()", filter="post.published")
}
sitemap = (
@@ -31,21 +57,20 @@ def render(markata: Markata) -> None:
.replace(
"",
(
- ''
- " "
+ '\n'
),
)
.replace("", "\n")
)
- setattr(markata, "sitemap", sitemap)
+ markata.sitemap = sitemap
@hook_impl
def save(markata: Markata) -> None:
- with open(Path(markata.config["output_dir"]) / "sitemap.xml", "w") as f:
+ with open(Path(markata.config.output_dir) / "sitemap.xml", "w") as f:
f.write(markata.sitemap) # type: ignore
diff --git a/markata/plugins/subroute.py b/markata/plugins/subroute.py
index 73ee57e7..6cc0aa14 100644
--- a/markata/plugins/subroute.py
+++ b/markata/plugins/subroute.py
@@ -1,18 +1,43 @@
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import Dict
-from markata.hookspec import hook_impl
+import pydantic
+from rich.markdown import Markdown
-if TYPE_CHECKING:
- from markata import Markata
+from markata import Markata
+from markata.hookspec import hook_impl, register_attr
+from pydantic import ConfigDict
-@hook_impl(tryfirst=True)
-def pre_render(markata: "Markata") -> None:
- """
- Sets the article slug if one is not already set in the frontmatter.
- """
- subroute = markata.config.get("subroute", "")
- for article in markata.iter_articles(description="creating slugs"):
- slug = Path(article.get("slug", Path(article["path"]).stem))
- article["slug"] = slug.parent / Path(subroute) / slug.stem
+class Config(pydantic.BaseModel):
+ subroute: Path = Path("")
+
+
+class SubroutePost(pydantic.BaseModel):
+ markata: Markata
+ model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
+
+ @pydantic.validator("slug")
+ @classmethod
+ def relative_to_subroute(cls, v, *, values: Dict) -> Path:
+ subroute = cls.markata.config.subroute
+ if subroute == Path(""):
+ return v
+
+ slug = Path(v)
+
+ if not slug.relative_to(subroute):
+ return slug.parent / subroute / slug.stem
+ return v
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
+
+@hook_impl()
+@register_attr("post_models")
+def post_model(markata: Markdown) -> None:
+ markata.post_models.append(SubroutePost)
diff --git a/markata/plugins/to_json.py b/markata/plugins/to_json.py
index 839d53ab..58a13827 100644
--- a/markata/plugins/to_json.py
+++ b/markata/plugins/to_json.py
@@ -1,5 +1,4 @@
import json
-from pathlib import Path
from typing import TYPE_CHECKING
from markata.hookspec import hook_impl
@@ -10,6 +9,5 @@
@hook_impl
def save(markata: "Markata") -> None:
- output_file = Path(markata.config["output_dir"]) / "markata.json"
- output_file.parent.mkdir(parents=True, exist_ok=True)
+ output_file = markata.config.output_dir / "markata.json"
output_file.write_text(json.dumps(markata.to_dict(), default=str))
diff --git a/markata/plugins/tui.py b/markata/plugins/tui.py
index e090d129..529c867f 100644
--- a/markata/plugins/tui.py
+++ b/markata/plugins/tui.py
@@ -1,9 +1,32 @@
import subprocess
+from typing import List
+import pydantic
from rich.console import RenderableType
from markata import Markata
-from markata.hookspec import hook_impl
+from markata.hookspec import hook_impl, register_attr
+
+
+class TuiKey(pydantic.BaseModel):
+ name: str
+ key: str
+
+
+class TuiConfig(pydantic.BaseModel):
+ new_cmd: List[str] = ["markata", "new", "post"]
+ keymap: List[TuiKey] = [TuiKey(name="new", key="n")]
+
+
+class Config(pydantic.BaseModel):
+ tui: TuiConfig = TuiConfig()
+
+
+@hook_impl()
+@register_attr("config_models")
+def config_model(markata: "Markata") -> None:
+ markata.config_models.append(Config)
+
try:
from textual.app import App
@@ -11,18 +34,18 @@
from textual.widgets import Footer
class MarkataWidget(Widget):
- def __init__(self, markata: Markata, widget: str = "server"):
+ def __init__(self, markata: Markata, widget: str = "server") -> None:
super().__init__(widget)
self.m = markata
self.widget = widget
self.renderable = getattr(self.m, self.widget)
- def render(self):
- return self.renderable
+ def render(self):
+ return self.renderable
- async def update(self, renderable: RenderableType) -> None:
- self.renderable = renderable
- self.refresh()
+ async def update(self, renderable: RenderableType) -> None:
+ self.renderable = renderable
+ self.refresh()
class MarkataApp(App):
async def on_load(self, event):
diff --git a/markata/scripts/migrate_to_slugify.py b/markata/scripts/migrate_to_slugify.py
index bc92cade..eb0a5f7a 100644
--- a/markata/scripts/migrate_to_slugify.py
+++ b/markata/scripts/migrate_to_slugify.py
@@ -15,7 +15,6 @@
slugify=false
```
"""
-from pathlib import Path
from slugify import slugify
@@ -36,10 +35,8 @@ def routed_slugify(text):
for o in original_urls
if routed_slugify(o) != o
]
- assets_dir: str = str(m.config.get("assets_dir", "static"))
- redirects_file = Path(
- str(m.config.get("redirects", Path(assets_dir) / "_redirects"))
- )
+ assets_dir: str = m.config.assets_dir
+ redirects_file = m.config.get.redirects_file
redirects_file.touch()
with open(redirects_file, "a") as f:
f.write("\n")
diff --git a/markata/standard_config.py b/markata/standard_config.py
index bd3c1eda..0e20a072 100644
--- a/markata/standard_config.py
+++ b/markata/standard_config.py
@@ -82,7 +82,6 @@
import anyconfig
-# path_spec_type = List[Dict[str, Union[Path, str, List[str]]]]
path_spec_type = List
@@ -213,7 +212,7 @@ def _load_env(tool: str) -> Dict:
Args:
tool (str): name of the tool to configure
"""
- vars = [var for var in os.environ.keys() if var.startswith(tool.upper())]
+ vars = [var for var in os.environ if var.startswith(tool.upper())]
return {
var.lower().strip(tool.lower()).strip("_").strip("-"): os.environ[var]
for var in vars
diff --git a/pyproject.toml b/pyproject.toml
index 040c051f..8fef3cf1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,63 +1,63 @@
[project]
classifiers = [
- "Development Status :: 4 - Beta",
- "Environment :: Console",
- "Environment :: Web Environment",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Natural Language :: English",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Programming Language :: Python",
- "Topic :: Documentation",
- "Topic :: Software Development",
- "Topic :: Text Processing",
- "Typing :: Typed",
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Environment :: Web Environment",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Programming Language :: Python",
+ "Topic :: Documentation",
+ "Topic :: Software Development",
+ "Topic :: Text Processing",
+ "Typing :: Typed",
]
dependencies = [
- "anyconfig",
- "beautifulsoup4",
- "checksumdir",
- "commonmark",
- "cookiecutter",
- "copier",
- "deepmerge",
- "diskcache",
- "feedgen",
- "jinja2",
- "linkify-it-py",
- "markdown-it-py[plugins]",
- "markdown2[all]",
- "more-itertools",
- "pathspec",
- "pillow",
- "pluggy",
- "pydantic<2.0", # for copier
- "pymdown-extensions",
- "python-frontmatter",
- "python-slugify",
- "pytz",
- "rich",
- "toml",
- "typer",
+ "anyconfig",
+ "beautifulsoup4",
+ "checksumdir",
+ "commonmark",
+ "cookiecutter",
+ "copier",
+ "deepmerge",
+ "diskcache",
+ "feedgen",
+ "jinja2",
+ "linkify-it-py",
+ "markdown-it-py[plugins]",
+ "markdown2[all]",
+ "more-itertools",
+ "pathspec",
+ "pillow",
+ "pluggy",
+ "polyfactory",
+ "pydantic>=2.0",
+ "pydantic_extra_types>=2.0",
+ "pydantic_settings",
+ "pymdown-extensions",
+ "python-frontmatter",
+ "python-slugify",
+ "pytz",
+ "rich",
+ "toml",
+ "typer",
+ 'dateparser',
]
-dynamic = [
- "version",
-]
+dynamic = ["version"]
description = "Static site generator plugins all the way down."
-keywords = [
- "static-site",
-]
+keywords = ["static-site"]
name = "markata"
readme = "README.md"
requires-python = ">=3.6"
@@ -70,34 +70,27 @@ email = "waylon@waylonwalker.com"
email = "waylon@markata.dev"
[build-system]
-requires = [
- "hatchling>=1.4.1",
-]
+requires = ["hatchling>=1.4.1"]
build-backend = "hatchling.build"
[project.license]
file = "LICENSE"
[project.optional-dependencies]
-tui = [
- "textual",
- "trogon",
-]
+tui = ["textual", "trogon"]
dev = [
- "black==22.10.0",
- "hatch",
- "interrogate",
- "mypy",
- "pre-commit",
- "pytest",
- "pytest-cov",
- "pytest-mock",
- "pytest-tmp-files",
- "ruff",
-]
-pyinstrument = [
- "pyinstrument",
+ "black",
+ "hatch",
+ "interrogate",
+ "mypy",
+ "pre-commit",
+ "pytest",
+ "pytest-cov",
+ "pytest-mock",
+ "pytest-tmp-files",
+ "ruff",
]
+pyinstrument = ["pyinstrument"]
[project.urls]
Homepage = "https://markata.dev"
@@ -143,58 +136,54 @@ path = "markata/__about__.py"
[tool.coverage.run]
branch = true
parallel = true
-omit = [
- "markata/__about__.py",
-]
+omit = ["markata/__about__.py"]
[tool.coverage.report]
-exclude_lines = [
- "no cov",
- "if __name__ == .__main__.:",
- "if TYPE_CHECKING:",
- ]
+exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
[tool.hatch.envs.default]
dependencies = [
- "black==22.10.0",
- "pytest",
- "pytest-cov",
- "pytest-mock",
- "pytest-tmp-files",
- "ruff",
+ "isort",
+ "black",
+ "pytest",
+ "pytest-cov",
+ "pytest-mock",
+ "pytest-tmp-files",
+ "ruff",
+ "pyinstrument",
]
[tool.hatch.envs.dev]
dependencies = [
- "black==22.10.0",
- "ipython",
- "pytest",
- "pytest-cov",
- "pytest-tmp-files",
- "ruff",
+ "black",
+ "ipython",
+ "polyfactory",
+ "black==22.10.0",
+ "ipython",
+ "pytest",
+ "pytest-cov",
+ "pytest-tmp-files",
+ "ruff",
]
[tool.hatch.envs.test]
[[tool.hatch.envs.test.matrix]]
-python = [
- "38",
- "39",
- "310",
- "311",
-]
+python = ["38", "39", "310", "311"]
[tool.hatch.envs.default.scripts]
-cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=markata --cov=tests"
+cov = "pytest tests --cov-report=term-missing --cov-config=pyproject.toml --cov=markata --cov=tests"
no-cov = "cov --no-cov"
lint = "ruff markata"
format = "black --check markata"
build-docs = "markata build"
-lint-test = [
- "lint",
- "format",
- "cov",
-]
+lint-format = ['lint', 'format']
+lint-test = ["lint", "format", "cov"]
test-lint = "lint-test"
+build = 'markata build'
+serve = "python -m http.server --bind 0.0.0.0 8000 --directory markout"
+ruff-fix = "ruff --fix markata"
+black-fix = "black markata"
+fix = ["black-fix", "ruff-fix"]
[tool.hatch.envs.test.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=markata --cov=tests"
@@ -202,9 +191,13 @@ cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=markat
[tool.hatch.build.targets.wheel]
[tool.hatch.build.targets.sdist]
-exclude = [
- "/.github",
-]
+exclude = ["/.github"]
[tool.ruff]
-ignore=['E501'] # let black control line length errors
+# E501 let black control line length errors
+ignore = ["E501"]
+
+target-version = "py37"
+[tool.ruff.per-file-ignores]
+'tests/**/*.py' = ['S101']
+select = ["A", "E"]
diff --git a/tests/plugins/conftest.py b/tests/plugins/conftest.py
deleted file mode 100644
index c6f0bcd7..00000000
--- a/tests/plugins/conftest.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import os
-from contextlib import contextmanager
-from pathlib import Path
-
-
-@contextmanager
-def set_directory(path: str) -> None:
- """Sets the cwd within the context
-
- Args:
- path (Path): The path to the cwd
-
- Yields:
- None
- """
-
- origin = Path().absolute()
- try:
- os.chdir(path)
- yield
- finally:
- os.chdir(origin)
diff --git a/tests/plugins/test_default_post_template.py b/tests/plugins/test_default_post_template.py
deleted file mode 100644
index 2320e710..00000000
--- a/tests/plugins/test_default_post_template.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from pathlib import Path
-
-import toml
-from conftest import set_directory
-
-from markata import Markata
-from markata.plugins import post_template
-
-
-def test_head_config_text_str(tmp_path):
- with set_directory(tmp_path):
- m = Markata()
- m.config["head"] = {}
- m.config["head"]["text"] = "here"
- post_template.configure(m)
- assert m.config["head"]["text"] == "here"
-
-
-def test_head_config_text_dict(tmp_path):
- with set_directory(tmp_path):
- m = Markata()
- m.config["head"] = {}
- m.config["head"]["text"] = [{"value": "one"}, {"value": "two"}]
- post_template.configure(m)
- assert m.config["head"]["text"] == "one\ntwo"
-
-
-def test_head_config_text_str_toml(tmp_path):
- with set_directory(tmp_path):
- Path("markata.toml").write_text(
- toml.dumps({"markata": {"head": {"text": "here"}}})
- )
- m = Markata()
- post_template.configure(m)
- assert m.config["head"]["text"] == "here"
-
-
-def test_head_config_text_list_toml(tmp_path):
- with set_directory(tmp_path):
- Path("markata.toml").write_text(
- toml.dumps(
- {"markata": {"head": {"text": [{"value": "one"}, {"value": "two"}]}}}
- )
- )
- m = Markata()
- post_template.configure(m)
- assert m.config["head"]["text"] == "one\ntwo"
diff --git a/tests/plugins/test_flat_slug.py b/tests/plugins/test_flat_slug.py
deleted file mode 100644
index 31d1b38c..00000000
--- a/tests/plugins/test_flat_slug.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import pytest
-
-from markata import Markata
-from markata.plugins import flat_slug
-
-
-@pytest.mark.parametrize(
- "article, slug",
- [
- ({"slug": "post_one"}, "post-one"),
- ({"slug": "post-one"}, "post-one"),
- ({"slug": "post one"}, "post-one"),
- ({"slug": "POST_ONE"}, "post-one"),
- ({"slug": "posts/post_one"}, "posts/post-one"),
- ({"slug": "posts/post-one"}, "posts/post-one"),
- ({"slug": "posts/post one"}, "posts/post-one"),
- ({"slug": "posts/POST_ONE"}, "posts/post-one"),
- ({"slug": "POSTS/post_one"}, "posts/post-one"),
- ({"slug": "/posts/post_one"}, "/posts/post-one"),
- ],
-)
-def test_flat_slug(article, slug) -> None:
- "ensure that the explicit workflow works"
- m = Markata()
- m.articles = [article]
- flat_slug.pre_render(m)
-
- assert m.articles[0]["slug"] == slug
-
-
-@pytest.mark.parametrize(
- "article, slug",
- [
- ({"path": "post_one.md"}, "post-one"),
- ({"path": "post-one.md"}, "post-one"),
- ({"path": "post one.md"}, "post-one"),
- ({"path": "POST_ONE.md"}, "post-one"),
- ({"path": "posts/post_one.md"}, "posts/post-one"),
- ({"path": "posts/post-one.md"}, "posts/post-one"),
- ({"path": "posts/post one.md"}, "posts/post-one"),
- ({"path": "posts/POST_ONE.md"}, "posts/post-one"),
- ({"path": "POSTS/post_one.md"}, "posts/post-one"),
- ({"path": "/posts/post_one.md"}, "/posts/post-one"),
- ],
-)
-def test_flat_slug_path(article, slug) -> None:
- "ensure that the second most explicit workflow works"
- m = Markata()
- m.articles = [article]
- flat_slug.pre_render(m)
-
-
-@pytest.mark.parametrize(
- "article, slug",
- [
- ({"title": "post_one"}, "post-one"),
- ({"title": "post-one"}, "post-one"),
- ({"title": "post one"}, "post-one"),
- ({"title": "POST_ONE"}, "post-one"),
- ({"title": "/post_one"}, "post-one"),
- ({"title": "posts/post_one"}, "posts/post-one"),
- ({"title": "posts/post-one"}, "posts/post-one"),
- ({"title": "posts/post one"}, "posts/post-one"),
- ({"title": "posts/POST_ONE"}, "posts/post-one"),
- ({"title": "POSTS/post_one"}, "posts/post-one"),
- ({"title": "/posts/post_one"}, "/posts/post-one"),
- ],
-)
-def test_flat_slug_title(article, slug) -> None:
- "ensure that the second most explicit workflow works"
- m = Markata()
- m.articles = [article]
- flat_slug.pre_render(m)
diff --git a/tests/plugins/test_post_model.py b/tests/plugins/test_post_model.py
new file mode 100644
index 00000000..71ceab0f
--- /dev/null
+++ b/tests/plugins/test_post_model.py
@@ -0,0 +1,30 @@
+from markata import Markata
+from markata.plugins.config_model import ConfigFactory
+from markata.plugins.post_model import PostFactory
+
+
+def test_post() -> None:
+ config = ConfigFactory().build(hooks=[], disabled_hooks=[])
+ post = PostFactory().build(
+ markata=Markata(config=config),
+ path="pages/my-post.md",
+ slug=None,
+ title=None,
+ )
+
+ assert post.slug == "my-post"
+ assert post.title == "My Post"
+
+
+def test_markata_init() -> None:
+ config = ConfigFactory().build(
+ hooks=[
+ "markata.plugins.create_models",
+ "markata.plugins.post_model",
+ "markata.plugins.config_model",
+ ],
+ disabled_hooks=[],
+ )
+
+ markata = Markata(config=config)
+ markata.run()
diff --git a/tests/plugins/test_redirects.py b/tests/plugins/test_redirects.py
index 6696e2fb..2b3375f9 100644
--- a/tests/plugins/test_redirects.py
+++ b/tests/plugins/test_redirects.py
@@ -1,13 +1,28 @@
"""
Tests the redirects plugin
"""
+# context manager to set the directory
+from contextlib import contextmanager
from pathlib import Path
-from conftest import set_directory
import pytest
from markata import Markata
from markata.plugins import redirects
+import os
+
+
+@contextmanager
+def set_directory(path):
+ """
+ context manager to set the directory
+ """
+ cwd = Path.cwd()
+ try:
+ os.chdir(path)
+ yield
+ finally:
+ os.chdir(cwd)
@pytest.mark.parametrize(
diff --git a/tests/test_doc_page.py b/tests/test_doc_page.py
deleted file mode 100644
index 3dedf878..00000000
--- a/tests/test_doc_page.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import os
-import textwrap
-from typing import Any
-
-import pytest
-
-from markata import Markata
-
-
-@pytest.fixture(scope="session")
-def make_project(tmp_path_factory: Any) -> Any:
- project = tmp_path_factory.mktemp("project")
- module = project / "my_module.py"
- module.write_text(
- textwrap.dedent(
- """
- '''
- Module level docstring
- '''
-
- def my_func():
- '''
- docstring for my_func
- '''
- class MyClass:
- '''
- docstring for MyClass
- '''
-
- def my_method(self):
- '''
- docstring for my_method
- '''
-
- """
- )
- )
- markta_toml = project / "markata.toml"
- markta_toml.write_text(
- textwrap.dedent(
- """
- [markata]
- hooks = [
- "markata.plugins.docs",
- "default",
- ]
- """
- )
- )
-
- return project
-
-
-def test_loaded(make_project: Any) -> None:
- os.chdir(make_project)
- m = Markata()
- assert len(m.py_files) == 1
-
-
-@pytest.fixture(scope="session")
-def test_run(make_project: Any) -> Any:
- os.chdir(make_project)
- m = Markata()
- m.run()
- return make_project
-
-
-def test_markout_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- assert markout.exists()
-
-
-def test_index_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- index = markout / "my-module" / "index.html"
- assert index.exists()
-
-
-def test_rss_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- rss = markout / "rss.xml"
- assert rss.exists()
diff --git a/tests/test_one_default_page.py b/tests/test_one_default_page.py
deleted file mode 100644
index 69f3a33a..00000000
--- a/tests/test_one_default_page.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import os
-import textwrap
-from typing import Any
-
-import pytest
-
-from markata import Markata
-
-
-@pytest.fixture()
-def make_index(tmp_path: Any) -> Any:
- pages = tmp_path / "pages"
- pages.mkdir()
- fn = pages / "index.md"
- fn.write_text(
- textwrap.dedent(
- """
- ---
- templateKey: blog-post
- tags: ['python',]
- title: My Awesome Post
- date: 2022-01-21T16:40:34
- published: False
-
- ---
-
- This is my awesome post.
- """
- )
- )
- return tmp_path
-
-
-def test_loaded(make_index: Any) -> None:
- os.chdir(make_index)
- m = Markata()
- assert len(m.articles) == 1
-
-
-@pytest.fixture()
-def test_run(make_index: Any) -> Any:
- os.chdir(make_index)
- m = Markata()
- m.run()
- return make_index
-
-
-def test_markout_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- assert markout.exists()
-
-
-def test_index_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- index = markout / "index.html"
- assert index.exists()
-
-
-def test_rss_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- rss = markout / "rss.xml"
- assert rss.exists()
diff --git a/tests/test_one_default_page_with_path_prefix.py b/tests/test_one_default_page_with_path_prefix.py
deleted file mode 100644
index 9de9ba98..00000000
--- a/tests/test_one_default_page_with_path_prefix.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import os
-import textwrap
-from typing import Any
-
-import pytest
-
-from markata import Markata
-
-
-@pytest.fixture()
-def make_index(tmp_path: Any) -> Any:
- pages = tmp_path / "pages"
- pages.mkdir()
- fn = pages / "index.md"
- fn.write_text(
- textwrap.dedent(
- """
- ---
- templateKey: blog-post
- tags: ['python',]
- title: My Awesome Post
- date: 2022-01-21T16:40:34
- published: False
-
- ---
-
- This is my awesome post.
- """
- )
- )
- return tmp_path
-
-
-def test_loaded(make_index: Any) -> None:
- os.chdir(make_index)
- m = Markata()
- assert len(m.articles) == 1
-
-
-@pytest.fixture()
-def test_run(make_index: Any) -> Any:
- os.chdir(make_index)
- m = Markata()
- m.config["output_dir"] = "markout/sub-route/"
- m.config["path_prefix"] = "sub-route/"
- m.run()
- return make_index
-
-
-def test_markout_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- assert markout.exists()
- sub = test_run / "markout/sub-route"
- assert sub.exists()
-
-
-def test_index_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- sub = test_run / "markout/sub-route"
- markout_index = markout / "index.html"
- assert not markout_index.exists()
- sub_index = sub / "index.html"
- assert sub_index.exists()
-
-
-def test_rss_exists(test_run: Any) -> Any:
- markout = test_run / "markout"
- sub = test_run / "markout/sub-route"
- rss = markout / "rss.xml"
- assert not rss.exists()
- sub_rss = sub / "rss.xml"
- assert sub_rss.exists()