blurry-cli 0.13.0__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/PKG-INFO +21 -16
  2. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/README.md +9 -1
  3. blurry_cli-0.14.0/blurry/__init__.py +210 -0
  4. blurry_cli-0.14.0/blurry/commands/clean.py +19 -0
  5. blurry_cli-0.14.0/blurry/commands/init.py +135 -0
  6. blurry_cli-0.14.0/blurry/file_processors/__init__.py +164 -0
  7. blurry_cli-0.14.0/blurry/gather_file_data_by_directory.py +60 -0
  8. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/images.py +34 -18
  9. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/markdown/__init__.py +10 -2
  10. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/markdown/front_matter.py +2 -2
  11. blurry_cli-0.14.0/blurry/plugins/jinja_plugins/filters.py +43 -0
  12. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/markdown_plugins/container_plugin.py +2 -0
  13. blurry_cli-0.14.0/blurry/py.typed +0 -0
  14. blurry_cli-0.14.0/blurry/runserver_handlers.py +37 -0
  15. blurry_cli-0.14.0/blurry/schema_validation/__init__.py +37 -0
  16. blurry_cli-0.14.0/blurry/schema_validation/models.py +574 -0
  17. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/settings.py +3 -4
  18. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/utils.py +0 -16
  19. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/pyproject.toml +27 -21
  20. blurry_cli-0.13.0/blurry/__init__.py +0 -382
  21. blurry_cli-0.13.0/blurry/schema_validation.py +0 -61
  22. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/LICENSE +0 -0
  23. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/__main__.py +0 -0
  24. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/async_typer.py +0 -0
  25. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/cli.py +0 -0
  26. {blurry_cli-0.13.0/blurry/markdown/renderer_functions → blurry_cli-0.14.0/blurry/commands}/__init__.py +0 -0
  27. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/constants.py +0 -0
  28. {blurry_cli-0.13.0/blurry/plugins/html_plugins → blurry_cli-0.14.0/blurry/markdown/renderer_functions}/__init__.py +0 -0
  29. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/markdown/renderer_functions/render_video.py +0 -0
  30. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/open_graph.py +0 -0
  31. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/__init__.py +0 -0
  32. {blurry_cli-0.13.0/blurry/plugins/jinja_plugins → blurry_cli-0.14.0/blurry/plugins/html_plugins}/__init__.py +0 -0
  33. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/html_plugins/minify_html_plugin.py +0 -0
  34. {blurry_cli-0.13.0/blurry/plugins/markdown_plugins → blurry_cli-0.14.0/blurry/plugins/jinja_plugins}/__init__.py +0 -0
  35. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/jinja_plugins/blurry_image_extension.py +0 -0
  36. /blurry_cli-0.13.0/blurry/py.typed → /blurry_cli-0.14.0/blurry/plugins/markdown_plugins/__init__.py +0 -0
  37. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/markdown_plugins/punctuation_plugin.py +0 -0
  38. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/markdown_plugins/python_code_plugin.py +0 -0
  39. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/sitemap.py +0 -0
  40. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/templates.py +0 -0
  41. {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/types.py +0 -0
@@ -1,18 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: blurry-cli
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: A Mistune-based static site generator for Python
5
- Home-page: https://github.com/blurry-dev/blurry
6
5
  License: MIT
7
6
  Keywords: static-site-generator,seo,pagespeed
8
7
  Author: John Franey
9
8
  Author-email: franey@duck.com
10
- Requires-Python: >=3.10,<4.0
9
+ Requires-Python: >=3.11,<4.0
11
10
  Classifier: Environment :: Console
12
11
  Classifier: License :: OSI Approved :: MIT License
13
12
  Classifier: Operating System :: OS Independent
14
13
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
14
  Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
18
16
  Classifier: Programming Language :: Python :: 3.13
@@ -21,21 +19,20 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
21
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
20
  Classifier: Topic :: Text Processing :: Markup :: HTML
23
21
  Classifier: Topic :: Text Processing :: Markup :: Markdown
24
- Requires-Dist: Jinja2 (>=3.0.0,<4.0.0)
25
- Requires-Dist: PyLD (>=2.0.3,<3.0.0)
26
- Requires-Dist: Wand (>=0.6.6,<0.7.0)
27
- Requires-Dist: dpath (>=2.1.6,<3.0.0)
22
+ Requires-Dist: Jinja2 (>=3.1.5,<4.0.0)
23
+ Requires-Dist: PyLD (>=2.0.4,<3.0.0)
24
+ Requires-Dist: Wand (>=0.6.13,<0.7.0)
25
+ Requires-Dist: dpath (>=2.2.0,<3.0.0)
28
26
  Requires-Dist: ffmpeg-python (>=0.2.0,<0.3.0)
29
27
  Requires-Dist: htmlmin2 (>=0.1.13,<0.2.0)
30
28
  Requires-Dist: jinja2-simple-tags (>=0.6.1,<0.7.0)
31
29
  Requires-Dist: jinjax (>=0.48,<0.49)
32
- Requires-Dist: livereload (>=2.7.0,<3.0.0)
33
- Requires-Dist: mistune (>=3.0.2,<4.0.0)
34
- Requires-Dist: pydantic2-schemaorg (>=0.2.0,<0.3.0)
35
- Requires-Dist: rich (>=13.3.3,<14.0.0)
30
+ Requires-Dist: livereload (>=2.7.1,<3.0.0)
31
+ Requires-Dist: mistune (>=3.1.0,<4.0.0)
32
+ Requires-Dist: pydantic (>=2.10.5,<3.0.0)
33
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
36
34
  Requires-Dist: selectolax (>=0.3.27,<0.4.0)
37
- Requires-Dist: toml (>=0.10.2,<0.11.0)
38
- Requires-Dist: typer (>=0.6.1,<0.7.0)
35
+ Requires-Dist: typer (>=0.15.1,<0.16.0)
39
36
  Project-URL: Repository, https://github.com/blurry-dev/blurry
40
37
  Description-Content-Type: text/markdown
41
38
 
@@ -44,9 +41,10 @@ Description-Content-Type: text/markdown
44
41
  [![Nox build status](https://github.com/blurry-dev/blurry/actions/workflows/github-actions-nox.yml/badge.svg?branch=main)](https://github.com/blurry-dev/blurry/actions/workflows/github-actions-nox.yml)
45
42
  [![PyPI package version](https://img.shields.io/pypi/v/blurry-cli.svg)](https://pypi.org/project/blurry-cli/)
46
43
  [![PyPI Python versions](https://img.shields.io/pypi/pyversions/blurry-cli.svg)](https://pypi.org/project/blurry-cli/)
44
+ [![PyPI downloads per month](https://img.shields.io/pypi/dm/blurry-cli.svg)](https://pypi.python.org/pypi/blurry-cli/)
47
45
  ![Package license](https://img.shields.io/github/license/blurry-dev/blurry.svg)
48
46
 
49
- ![Blurry logo](docs/content/favicon.png)
47
+ ![Blurry logo](https://github.com/blurry-dev/blurry/raw/main/docs/content/favicon.png)
50
48
 
51
49
  ## tl;dr
52
50
 
@@ -57,6 +55,8 @@ Blurry also makes your images responsive, supports embedding Python source code
57
55
 
58
56
  Check out the docs and try it out!
59
57
 
58
+ **Note**: Until v1.0.0, minor versions (0.x.0) may not be backwards compatible.
59
+
60
60
  ## Documentation
61
61
 
62
62
  View the documentation site at <https://blurry-docs.netlify.app/>.
@@ -66,6 +66,11 @@ View the documentation site at <https://blurry-docs.netlify.app/>.
66
66
  Contributions are welcome!
67
67
  Check out [the contribution docs](https://github.com/blurry-dev/blurry/blob/main/CONTRIBUTING.md) to get started.
68
68
 
69
+ ## Security contact information
70
+
71
+ To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
72
+ Tidelift will coordinate the fix and disclosure.
73
+
69
74
  ## Standing on the shoulders of giants
70
75
 
71
76
  Blurry blends together high-quality libraries:
@@ -3,9 +3,10 @@
3
3
  [![Nox build status](https://github.com/blurry-dev/blurry/actions/workflows/github-actions-nox.yml/badge.svg?branch=main)](https://github.com/blurry-dev/blurry/actions/workflows/github-actions-nox.yml)
4
4
  [![PyPI package version](https://img.shields.io/pypi/v/blurry-cli.svg)](https://pypi.org/project/blurry-cli/)
5
5
  [![PyPI Python versions](https://img.shields.io/pypi/pyversions/blurry-cli.svg)](https://pypi.org/project/blurry-cli/)
6
+ [![PyPI downloads per month](https://img.shields.io/pypi/dm/blurry-cli.svg)](https://pypi.python.org/pypi/blurry-cli/)
6
7
  ![Package license](https://img.shields.io/github/license/blurry-dev/blurry.svg)
7
8
 
8
- ![Blurry logo](docs/content/favicon.png)
9
+ ![Blurry logo](https://github.com/blurry-dev/blurry/raw/main/docs/content/favicon.png)
9
10
 
10
11
  ## tl;dr
11
12
 
@@ -16,6 +17,8 @@ Blurry also makes your images responsive, supports embedding Python source code
16
17
 
17
18
  Check out the docs and try it out!
18
19
 
20
+ **Note**: Until v1.0.0, minor versions (0.x.0) may not be backwards compatible.
21
+
19
22
  ## Documentation
20
23
 
21
24
  View the documentation site at <https://blurry-docs.netlify.app/>.
@@ -25,6 +28,11 @@ View the documentation site at <https://blurry-docs.netlify.app/>.
25
28
  Contributions are welcome!
26
29
  Check out [the contribution docs](https://github.com/blurry-dev/blurry/blob/main/CONTRIBUTING.md) to get started.
27
30
 
31
+ ## Security contact information
32
+
33
+ To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
34
+ Tidelift will coordinate the fix and disclosure.
35
+
28
36
  ## Standing on the shoulders of giants
29
37
 
30
38
  Blurry blends together high-quality libraries:
@@ -0,0 +1,210 @@
1
+ import asyncio
2
+ import concurrent.futures
3
+ import importlib
4
+ import os
5
+ import pkgutil
6
+ import sys
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ from livereload import Server
11
+ from rich import print
12
+ from rich.console import Console
13
+ from rich.progress import BarColumn
14
+ from rich.progress import Progress
15
+ from rich.progress import TaskProgressColumn
16
+ from rich.progress import TextColumn
17
+
18
+ from blurry.async_typer import AsyncTyper
19
+ from blurry.cli import print_blurry_name
20
+ from blurry.cli import print_plugin_table
21
+ from blurry.commands.clean import clean_build_directory
22
+ from blurry.commands.init import initialize_new_project
23
+ from blurry.constants import ENV_VAR_PREFIX
24
+ from blurry.file_processors import process_non_markdown_file
25
+ from blurry.file_processors import write_html_file
26
+ from blurry.gather_file_data_by_directory import gather_file_data_by_directory
27
+ from blurry.runserver_handlers import handle_changed_jinja_files
28
+ from blurry.runserver_handlers import handle_changed_markdown_files
29
+ from blurry.runserver_handlers import rebuild_markdown_files
30
+ from blurry.settings import get_build_directory
31
+ from blurry.settings import get_content_directory
32
+ from blurry.settings import SETTINGS
33
+ from blurry.settings import update_settings
34
+ from blurry.sitemap import write_sitemap_file
35
+ from blurry.templates import get_jinja_env
36
+
37
+
38
+ warning_console = Console(stderr=True, style="bold yellow")
39
+
40
+
41
+ app = AsyncTyper()
42
+
43
+
44
+ @app.command(name="clean")
45
+ def clean_command():
46
+ clean_build_directory()
47
+
48
+
49
+ @app.command(name="init")
50
+ def init_command(name: str | None = None, domain: str | None = None):
51
+ initialize_new_project(name, domain)
52
+
53
+
54
+ @app.async_command()
55
+ async def build(release=True, clean: bool = False):
56
+ """Generates HTML content from Markdown files."""
57
+ start = datetime.now()
58
+
59
+ if release:
60
+ print_blurry_name()
61
+ print_plugin_table()
62
+
63
+ update_settings()
64
+ build_directory = get_build_directory()
65
+
66
+ if clean:
67
+ clean_build_directory()
68
+
69
+ build_directory.mkdir(parents=True, exist_ok=True)
70
+
71
+ content_directory = get_content_directory()
72
+ build_directory.mkdir(parents=True, exist_ok=True)
73
+ jinja_env = get_jinja_env()
74
+
75
+ os.environ.setdefault(f"{ENV_VAR_PREFIX}BUILD_MODE", "prod" if release else "dev")
76
+
77
+ file_data_by_directory = gather_file_data_by_directory()
78
+
79
+ markdown_task_count = 0
80
+ non_markdown_task_count = 1 # sitemap
81
+
82
+ with Progress(
83
+ TextColumn("[progress.description]{task.description}"),
84
+ BarColumn(),
85
+ TaskProgressColumn(),
86
+ transient=True,
87
+ ) as progress:
88
+ with concurrent.futures.ThreadPoolExecutor() as executor:
89
+ markdown_task = progress.add_task("[blue]Markdown")
90
+ other_task = progress.add_task("[blue]Other")
91
+
92
+ sitemap_future = executor.submit(write_sitemap_file, file_data_by_directory)
93
+ sitemap_future.add_done_callback(lambda _: progress.advance(other_task))
94
+
95
+ for filepath in content_directory.rglob("*"):
96
+ if filepath.is_dir():
97
+ continue
98
+ if filepath.suffix == ".md":
99
+
100
+ def on_markdown_file_processed(future: concurrent.futures.Future):
101
+ progress.advance(markdown_task)
102
+ if exception := future.exception():
103
+ print(f"{filepath}: could not process file - {exception}")
104
+
105
+ markdown_future = executor.submit(
106
+ write_html_file,
107
+ filepath.relative_to(content_directory),
108
+ file_data_by_directory,
109
+ release,
110
+ jinja_env,
111
+ )
112
+ markdown_task_count += 1
113
+ progress.update(markdown_task, total=markdown_task_count)
114
+ markdown_future.add_done_callback(on_markdown_file_processed)
115
+ continue
116
+ other_future = executor.submit(
117
+ process_non_markdown_file,
118
+ filepath,
119
+ file_data_by_directory,
120
+ jinja_env,
121
+ )
122
+ non_markdown_task_count += 1
123
+ progress.update(other_task, total=non_markdown_task_count)
124
+
125
+ def on_non_markdown_file_processed(future: concurrent.futures.Future):
126
+ progress.advance(other_task)
127
+ if exception := future.exception():
128
+ print(f"{filepath}: could not process file - {exception}")
129
+
130
+ other_future.add_done_callback(on_non_markdown_file_processed)
131
+
132
+ print(
133
+ f"Blurring {markdown_task_count} Markdown files and {non_markdown_task_count} other files"
134
+ )
135
+
136
+ end = datetime.now()
137
+
138
+ difference = end - start
139
+ print(f"Built site in {difference.total_seconds()} seconds")
140
+
141
+
142
+ async def reload_local_plugins_and_build():
143
+ """Reloads a project's local modules and performs a dev build"""
144
+ cwd = str(Path.cwd())
145
+
146
+ for _, package_name, _ in pkgutil.iter_modules([cwd]):
147
+ try:
148
+ module = sys.modules[package_name]
149
+ except KeyError:
150
+ continue
151
+ importlib.reload(module)
152
+
153
+ await build(release=False)
154
+
155
+
156
+ @app.command()
157
+ def runserver():
158
+ """Starts HTTP server with live reloading."""
159
+ print_blurry_name()
160
+ print_plugin_table()
161
+
162
+ update_settings()
163
+ os.environ.setdefault(f"{ENV_VAR_PREFIX}BUILD_MODE", "dev")
164
+
165
+ SETTINGS["RUNSERVER"] = True
166
+
167
+ event_loop = asyncio.get_event_loop()
168
+ event_loop.create_task(build(release=False))
169
+
170
+ jinja_env = get_jinja_env()
171
+
172
+ livereload_server = Server()
173
+ livereload_server.watch(
174
+ f"{SETTINGS['CONTENT_DIRECTORY_NAME']}/**/*.jinja",
175
+ lambda filepaths: handle_changed_jinja_files(filepaths, jinja_env),
176
+ )
177
+ livereload_server.watch(
178
+ f"{SETTINGS['CONTENT_DIRECTORY_NAME']}/**/*.md",
179
+ lambda filepaths: handle_changed_markdown_files(filepaths, jinja_env),
180
+ )
181
+ livereload_server.watch(
182
+ f"{SETTINGS['CONTENT_DIRECTORY_NAME']}/**/*",
183
+ lambda: event_loop.create_task(build(release=False)),
184
+ ignore=lambda filepath: any(
185
+ [
186
+ filepath.endswith(".md"),
187
+ filepath.endswith(".jinja"),
188
+ Path(filepath).is_dir(),
189
+ ]
190
+ ),
191
+ )
192
+ livereload_server.watch(
193
+ f"{SETTINGS['TEMPLATES_DIRECTORY_NAME']}/**/*",
194
+ lambda: rebuild_markdown_files(jinja_env),
195
+ ignore=lambda filepath: Path(filepath).is_dir(),
196
+ )
197
+ livereload_server.watch(
198
+ "./**/*.py",
199
+ lambda: event_loop.create_task(reload_local_plugins_and_build()),
200
+ )
201
+ livereload_server.watch(
202
+ "blurry.toml", lambda: event_loop.create_task(build(release=False))
203
+ )
204
+ livereload_server.serve(
205
+ host=SETTINGS["DEV_HOST"], port=SETTINGS["DEV_PORT"], root=get_build_directory()
206
+ )
207
+
208
+
209
+ def main():
210
+ app()
@@ -0,0 +1,19 @@
1
+ import shutil
2
+
3
+ from rich import print
4
+
5
+ from blurry.settings import get_build_directory
6
+ from blurry.settings import update_settings
7
+
8
+
9
+ def clean_build_directory():
10
+ """Removes the build directory for a clean build."""
11
+ update_settings()
12
+ build_directory = get_build_directory()
13
+
14
+ try:
15
+ shutil.rmtree(build_directory)
16
+ except FileNotFoundError:
17
+ pass
18
+
19
+ print(f"Cleaned build directory: {build_directory} 🧹")
@@ -0,0 +1,135 @@
1
+ import tomllib
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ from rich.prompt import Prompt
6
+
7
+ from blurry.settings import get_content_directory
8
+ from blurry.settings import get_templates_directory
9
+ from blurry.settings import update_settings
10
+
11
+
12
+ BLURRY_CONFIG_TEMPLATE = """
13
+ [blurry]
14
+ domain = "{domain}"
15
+ markdown_file_jinja_template_extension = ".jinja"
16
+
17
+ [blurry.schema_data.sourceOrganization]
18
+ name = "{project_name}"
19
+ url = "https://{domain}"
20
+ """.strip()
21
+
22
+ HOMEPAGE_MARKDOWN = """
23
+ +++
24
+ "@type" = "WebSite"
25
+ name = "Home"
26
+ abstract = "The homepage of {project_name}, built with Blurry."
27
+ datePublished = {date_published}
28
+ +++
29
+
30
+ # Welcome to your new Blurry site
31
+
32
+ To learn more about how to get started, check out Blurry's quick start docs:
33
+
34
+ <https://blurry-docs.netlify.app/getting-started/quick-start/>
35
+ """.strip()
36
+
37
+ BASE_TEMPLATE = """
38
+ <!doctype html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="utf-8">
42
+ <meta name="viewport" content="width=device-width, initial-scale=1">
43
+ <meta name="color-scheme" content="light dark">
44
+ <link
45
+ rel="stylesheet"
46
+ href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
47
+ >
48
+ <title>{% block title %}{% endblock %} | {{ sourceOrganization.name }}</title>
49
+ <meta name="description" content="{% block description %}{% endblock %}">
50
+ <link rel="canonical" href="{{ url }}">
51
+ {{ open_graph_tags|safe }}
52
+ {{ schema_type_tag|safe }}
53
+ </head>
54
+ <body>
55
+ <nav class="container">
56
+ <ul>
57
+ <li><strong>{{ sourceOrganization.name }}</strong></li>
58
+ </ul>
59
+ <ul>
60
+ <li><a href="/">Home</a></li>
61
+ </ul>
62
+ </nav>
63
+
64
+ <main class="container">
65
+ {% block body %}{% endblock %}
66
+ </main>
67
+ </body>
68
+ </html>
69
+ """
70
+
71
+
72
+ WEBSITE_TEMPLATE = """
73
+ {% extends "base.jinja" %}
74
+
75
+ {% block title %}{{ name }}{% endblock %}
76
+ {% block description %}{{ abstract }}{% endblock %}
77
+
78
+ {% block body %}
79
+ <article>
80
+ {{ body|safe }}
81
+ </article>
82
+ {% endblock %}
83
+ """
84
+
85
+
86
+ def initialize_new_project(name: str | None, domain: str | None):
87
+ update_settings()
88
+ blurry_config_file = Path("blurry.toml")
89
+ blurry_template_directory = get_templates_directory()
90
+ blurry_content_directory = get_content_directory()
91
+
92
+ if (
93
+ blurry_config_file.exists()
94
+ or blurry_template_directory.exists()
95
+ or blurry_content_directory.exists()
96
+ ):
97
+ print("Blurry project already initialized.")
98
+ return
99
+ name = name or Prompt.ask(
100
+ "What is the name of your company, project, or website?",
101
+ )
102
+ domain = domain or Prompt.ask("What is your website's domain?")
103
+
104
+ config_text = BLURRY_CONFIG_TEMPLATE.format(domain=domain, project_name=name)
105
+
106
+ try:
107
+ tomllib.loads(config_text)
108
+ except tomllib.TOMLDecodeError as e:
109
+ print(f"Error in configuration file: {e}.")
110
+ print("Please check your input and try again.")
111
+ exit(1)
112
+
113
+ blurry_config_file.write_text(config_text)
114
+
115
+ # Write template files
116
+ blurry_template_directory.mkdir(exist_ok=True)
117
+ base_template_file = blurry_template_directory / "base.jinja"
118
+ base_template_file.write_text(BASE_TEMPLATE)
119
+
120
+ website_template_file = blurry_template_directory / "WebSite.jinja"
121
+ website_template_file.write_text(WEBSITE_TEMPLATE)
122
+
123
+ # Write homepage Markdown file
124
+ date_published = datetime.now().strftime("%Y-%m-%d")
125
+ blurry_content_directory.mkdir(exist_ok=True)
126
+ homepage = blurry_content_directory / "index.md"
127
+ homepage.write_text(
128
+ HOMEPAGE_MARKDOWN.format(
129
+ date_published=date_published,
130
+ project_name=name,
131
+ )
132
+ )
133
+
134
+ print("Blurry project initialized!")
135
+ print("Run 'blurry runserver' to start the dev server.")
@@ -0,0 +1,164 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import json
4
+ import mimetypes
5
+ import shutil
6
+ from copy import deepcopy
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from jinja2 import Environment
12
+ from rich.console import Console
13
+
14
+ from blurry.images import generate_images_for_srcset
15
+ from blurry.open_graph import open_graph_meta_tags
16
+ from blurry.plugins import discovered_html_plugins
17
+ from blurry.schema_validation import validate_front_matter_as_schema
18
+ from blurry.settings import get_build_directory
19
+ from blurry.settings import get_content_directory
20
+ from blurry.settings import SETTINGS
21
+ from blurry.types import MarkdownFileData
22
+ from blurry.types import TemplateContext
23
+ from blurry.utils import content_path_to_url
24
+ from blurry.utils import convert_content_path_to_directory_in_build
25
+ from blurry.utils import format_schema_data
26
+ from blurry.utils import write_index_file_creating_path
27
+
28
+
29
+ warning_console = Console(stderr=True, style="bold yellow")
30
+
31
+
32
+ def process_non_markdown_file(
33
+ filepath: Path, file_data_by_directory, jinja_env: Environment
34
+ ):
35
+ # Process Jinja files
36
+ if ".jinja" in filepath.suffixes:
37
+ process_jinja_file(filepath, jinja_env, file_data_by_directory)
38
+ return
39
+
40
+ CONTENT_DIR = get_content_directory()
41
+ mimetype, _ = mimetypes.guess_type(filepath, strict=False)
42
+ relative_filepath = filepath.relative_to(CONTENT_DIR)
43
+ output_file = get_build_directory() / relative_filepath
44
+
45
+ # Copy file to build directory if it is not already there
46
+ if not output_file.exists():
47
+ output_file.parent.mkdir(exist_ok=True, parents=True)
48
+ shutil.copyfile(filepath, output_file)
49
+
50
+ # Create srcset images
51
+ if mimetype in [
52
+ mimetypes.types_map[".jpg"],
53
+ mimetypes.types_map[".png"],
54
+ ]:
55
+ asyncio.run(generate_images_for_srcset(filepath))
56
+
57
+
58
+ def process_jinja_file(filepath: Path, jinja_env: Environment, file_data_by_directory):
59
+ build_directory = get_build_directory()
60
+ content_directory = get_content_directory()
61
+ template = jinja_env.get_template(str(filepath.relative_to(content_directory)))
62
+ context = {
63
+ "file_data_by_directory": {
64
+ str(path): data for path, data in deepcopy(file_data_by_directory).items()
65
+ },
66
+ "settings": deepcopy(SETTINGS),
67
+ "datetime": datetime,
68
+ }
69
+ filepath_with_new_extension = filepath.with_suffix(
70
+ filepath.suffix.replace(".jinja", "")
71
+ )
72
+ filepath_in_build = build_directory / filepath_with_new_extension.relative_to(
73
+ content_directory
74
+ )
75
+ html = template.render(dataclasses=dataclasses, **context)
76
+ filepath_in_build.write_text(html)
77
+
78
+
79
+ def json_converter_with_dates(item: Any) -> None | str:
80
+ if isinstance(item, datetime):
81
+ return item.strftime("%Y-%M-%D")
82
+
83
+
84
+ def write_html_file(
85
+ filepath: Path,
86
+ file_data_by_directory: dict[Path, list[MarkdownFileData]],
87
+ release: bool,
88
+ jinja_env: Environment,
89
+ ):
90
+ extra_context: TemplateContext = {}
91
+ # Gather data from other files in this directory if this is an index file
92
+ file_data_list = file_data_by_directory[filepath.parent]
93
+ if filepath.name == "index.md":
94
+ sibling_pages = [
95
+ {
96
+ "url": content_path_to_url(f.path),
97
+ **f.front_matter,
98
+ }
99
+ for f in file_data_list
100
+ if f.path != filepath
101
+ ]
102
+ extra_context["sibling_pages"] = sibling_pages
103
+ folder_in_build = convert_content_path_to_directory_in_build(filepath)
104
+
105
+ file_data = [f for f in file_data_list if f.path == filepath][0]
106
+ schema_type = file_data.front_matter.get("@type")
107
+ if not schema_type:
108
+ raise ValueError(
109
+ f"Required @type value missing in file or TOML front matter invalid: "
110
+ f"{filepath}"
111
+ )
112
+ template_extension = SETTINGS["MARKDOWN_FILE_JINJA_TEMPLATE_EXTENSION"]
113
+ template = jinja_env.get_template(f"{schema_type}{template_extension}")
114
+
115
+ # Map custom template name to Schema.org type
116
+ if mapped_schema_type := SETTINGS["TEMPLATE_SCHEMA_TYPES"].get(schema_type):
117
+ file_data.front_matter["@type"] = mapped_schema_type
118
+
119
+ # Include non-schema variables as top-level context values, removing them from
120
+ # front_matter
121
+ front_matter = file_data.front_matter
122
+ schema_variables: TemplateContext = {}
123
+ template_context: TemplateContext = {}
124
+ non_schema_variable_prefix = SETTINGS["FRONTMATTER_NON_SCHEMA_VARIABLE_PREFIX"]
125
+ for key, value in front_matter.items():
126
+ if key.startswith(non_schema_variable_prefix):
127
+ template_context[key.replace(non_schema_variable_prefix, "", 1)] = value
128
+ continue
129
+ schema_variables[key] = value
130
+
131
+ schema_data = json.dumps(
132
+ format_schema_data(schema_variables),
133
+ default=json_converter_with_dates,
134
+ )
135
+
136
+ validate_front_matter_as_schema(filepath, schema_variables, warning_console)
137
+
138
+ schema_type_tag = f'<script type="application/ld+json">{schema_data}</script>'
139
+
140
+ template_context = {
141
+ "body": file_data.body,
142
+ "filepath": filepath,
143
+ "schema_data": schema_data,
144
+ "schema_type_tag": schema_type_tag,
145
+ "open_graph_tags": open_graph_meta_tags(file_data.front_matter),
146
+ "build_path": folder_in_build,
147
+ "file_data_by_directory": {
148
+ str(path): data for path, data in deepcopy(file_data_by_directory).items()
149
+ },
150
+ "settings": deepcopy(SETTINGS),
151
+ **schema_variables,
152
+ **deepcopy(extra_context),
153
+ **template_context,
154
+ }
155
+
156
+ html = template.render(dataclasses=dataclasses, **template_context)
157
+ for html_plugin in discovered_html_plugins:
158
+ try:
159
+ html = html_plugin.load()(html, template_context, release)
160
+ except Exception as err:
161
+ print(f"Error initializing plugin {html_plugin}: {err}")
162
+
163
+ # Write file
164
+ write_index_file_creating_path(folder_in_build, html)