-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Waylon S. Walker
committed
Jan 29, 2025
1 parent
1b8d4fd
commit e8114e7
Showing
23 changed files
with
1,671 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,287 @@ | ||
# Markata Plugin Development Guide | ||
|
||
This guide will help you create plugins for the Markata static site generator. | ||
Follow these patterns and best practices to ensure your plugin integrates | ||
seamlessly with Markata's architecture. | ||
|
||
## Plugin Structure | ||
|
||
Every Markata plugin follows this standard structure: | ||
|
||
1. **Docstring and Imports**: Begin with a comprehensive docstring followed by | ||
required imports | ||
2. **Plugin Identification**: Define plugin name constants | ||
3. **Configuration Models**: Define Pydantic models for configuration | ||
4. **Hook Implementations**: Implement lifecycle hooks with proper decorators | ||
|
||
## Required Components | ||
|
||
### 1. Documentation | ||
|
||
Every plugin must start with a docstring following this format: | ||
|
||
```python | ||
""" | ||
Plugin name and one-line description | ||
# Installation | ||
Installation instructions, including: | ||
- Required pip packages | ||
- Configuration in markata.toml | ||
- Any environment variables needed | ||
\`\`\`toml | ||
hooks = [ | ||
"markata.plugins.load", | ||
] | ||
\`\`\` | ||
# Configuration | ||
Document all configuration options: | ||
\`\`\`toml | ||
[markata.plugin_name] | ||
option1 = "default" # Description of option1 | ||
option2 = 42 # Description of option2 | ||
\`\`\` | ||
# Usage | ||
Explain how to use the plugin, with examples. | ||
# Notes | ||
Any additional information, caveats, or best practices. | ||
""" | ||
``` | ||
|
||
### 2. Imports and Plugin Identity | ||
|
||
```python | ||
from markata.hookspec import hook_impl, register_attr | ||
import pydantic | ||
from typing import List, Optional, Union | ||
|
||
MARKATA_PLUGIN_NAME = "My Plugin" # Human readable name | ||
MARKATA_PLUGIN_PACKAGE_NAME = "my-plugin" # Package/import name | ||
``` | ||
|
||
### 3. Configuration Models | ||
|
||
Define Pydantic models using appropriate configuration based on usage pattern: | ||
|
||
```python | ||
class PluginConfig(pydantic.BaseModel): | ||
option1: str = "default" | ||
option2: int = 42 | ||
|
||
model_config = pydantic.ConfigDict( | ||
validate_assignment=True, # For config models | ||
arbitrary_types_allowed=True, | ||
extra="allow", | ||
str_strip_whitespace=True, | ||
validate_default=True, | ||
coerce_numbers_to_str=True, | ||
populate_by_name=True, | ||
) | ||
|
||
class Config(pydantic.BaseModel): | ||
plugin_name: PluginConfig = PluginConfig() | ||
|
||
@hook_impl | ||
@register_attr("config_models") | ||
def config_model(markata: "Markata") -> None: | ||
markata.config_models.append(Config) | ||
``` | ||
|
||
### 4. Hook Implementation | ||
|
||
Implement hooks based on when your plugin needs to run in the lifecycle: | ||
|
||
```python | ||
@hook_impl | ||
@register_attr("my_attribute") # Register any new markata attributes | ||
def hook_name(markata: "Markata") -> None: | ||
""" | ||
Implement your plugin logic here. | ||
Always type hint markata parameter. | ||
Register attributes you add to markata. | ||
""" | ||
pass | ||
``` | ||
|
||
## Lifecycle Hooks | ||
|
||
Choose appropriate hooks based on when your plugin needs to run: | ||
|
||
1. `config_model`: Load configuration models | ||
2. `post_model`: Load post models | ||
3. `create_models`: Model creation/merging | ||
4. `load_config`: Load configuration | ||
5. `configure`: Configuration setup | ||
6. `validate_config`: Configuration validation | ||
7. `glob`: File discovery | ||
8. `load`: File loading | ||
9. `validate_posts`: Post validation | ||
10. `pre_render`: Pre-render processing | ||
11. `render`: Content rendering | ||
12. `post_render`: Post-render processing | ||
13. `save`: Save to disk | ||
14. `teardown`: Cleanup on exit | ||
|
||
## Working with Posts | ||
|
||
### Searching/Filtering Posts | ||
|
||
```python | ||
# Use markata.map for post operations | ||
posts = markata.map( | ||
'post', # What to return | ||
filter='"tag" in post.tags', # Filter condition | ||
sort='date', # Sort field | ||
reverse=True # Sort direction | ||
) | ||
``` | ||
|
||
### Iterating Over All Posts | ||
|
||
Typically plugins will iterate over all posts that are not markated as `skip` | ||
posts, these posts have been manually flagged by the user to skip, or have been | ||
identified early as already completed in previoud builds. | ||
|
||
```python | ||
for post in markata.filter('not skip'): | ||
# Do something with post | ||
``` | ||
|
||
### Creating Posts | ||
|
||
```python | ||
post_args = { | ||
"markata": markata, | ||
"path": "example.md", | ||
"content": "# Example\nContent here", | ||
"raw": "# Example\nContent here", | ||
"title": "Example Post", | ||
"description": "An example post", | ||
"published": True, | ||
"date": "2025-01-29" | ||
} | ||
|
||
post = markata.Post.parse_obj(post_args) | ||
markata.Post.validate(post) | ||
markata.posts.append(post) | ||
``` | ||
|
||
## Complete Example | ||
|
||
Here's a complete example of a tag statistics plugin: | ||
|
||
```python | ||
""" | ||
Tag Statistics Plugin | ||
Generates statistics about tag usage across all posts. | ||
# Installation | ||
\`\`\` toml | ||
hooks = ["markata.plugins.tag_stats"] | ||
\`\`\` | ||
# Configuration | ||
\`\`\` toml | ||
[tag_stats] | ||
output_file = "tag-stats.md" # Output file for statistics | ||
min_count = 1 # Minimum tag count to include | ||
\`\`\` | ||
""" | ||
|
||
from markata.hookspec import hook_impl, register_attr | ||
import pydantic | ||
from pathlib import Path | ||
from typing import Optional | ||
from collections import Counter | ||
|
||
MARKATA_PLUGIN_NAME = "Tag Statistics" | ||
MARKATA_PLUGIN_PACKAGE_NAME = "tag-stats" | ||
|
||
class TagStatsConfig(pydantic.BaseModel): | ||
output_file: Path = Path("tag-stats.md") | ||
min_count: int = 1 | ||
|
||
model_config = pydantic.ConfigDict( | ||
validate_assignment=True, | ||
arbitrary_types_allowed=True, | ||
extra="allow", | ||
str_strip_whitespace=True, | ||
validate_default=True, | ||
coerce_numbers_to_str=True, | ||
populate_by_name=True, | ||
) | ||
|
||
class Config(pydantic.BaseModel): | ||
tag_stats: TagStatsConfig = TagStatsConfig() | ||
|
||
@hook_impl | ||
@register_attr("config_models") | ||
def config_model(markata: "Markata") -> None: | ||
markata.config_models.append(Config) | ||
|
||
@hook_impl | ||
def post_render(markata: "Markata") -> None: | ||
# Collect all tags | ||
all_tags = [] | ||
for post in markata.posts: | ||
if hasattr(post, "tags"): | ||
all_tags.extend(post.tags) | ||
|
||
# Count tags | ||
tag_counts = Counter(all_tags) | ||
|
||
# Generate markdown | ||
content = "# Tag Statistics\n\n" | ||
content += "| Tag | Count |\n|-----|-------|\n" | ||
|
||
for tag, count in sorted(tag_counts.items()): | ||
if count >= markata.config.tag_stats.min_count: | ||
content += f"| {tag} | {count} |\n" | ||
|
||
# Create statistics post | ||
post_args = { | ||
"markata": markata, | ||
"path": str(markata.config.tag_stats.output_file), | ||
"content": content, | ||
"raw": content, | ||
"title": "Tag Statistics", | ||
"description": "Statistics about tag usage", | ||
"published": True, | ||
"date": markata.today | ||
} | ||
|
||
post = markata.Post.parse_obj(post_args) | ||
markata.Post.validate(post) | ||
markata.posts.append(post) | ||
|
||
``` | ||
|
||
## CLI Plugins | ||
|
||
To add CLI commands: | ||
|
||
```python | ||
@hook_impl() | ||
def cli(app, markata): | ||
@app.command() | ||
def my_command(): | ||
"""Command description for --help""" | ||
# Command implementation | ||
``` | ||
|
||
> note throughout this file nested markdown fences were escaped for | ||
> compatibility with the prompt, do not escape them. for example, you should | ||
> not use \`\`\` toml but rather ``` toml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.