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.
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/PKG-INFO +21 -16
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/README.md +9 -1
- blurry_cli-0.14.0/blurry/__init__.py +210 -0
- blurry_cli-0.14.0/blurry/commands/clean.py +19 -0
- blurry_cli-0.14.0/blurry/commands/init.py +135 -0
- blurry_cli-0.14.0/blurry/file_processors/__init__.py +164 -0
- blurry_cli-0.14.0/blurry/gather_file_data_by_directory.py +60 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/images.py +34 -18
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/markdown/__init__.py +10 -2
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/markdown/front_matter.py +2 -2
- blurry_cli-0.14.0/blurry/plugins/jinja_plugins/filters.py +43 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/markdown_plugins/container_plugin.py +2 -0
- blurry_cli-0.14.0/blurry/py.typed +0 -0
- blurry_cli-0.14.0/blurry/runserver_handlers.py +37 -0
- blurry_cli-0.14.0/blurry/schema_validation/__init__.py +37 -0
- blurry_cli-0.14.0/blurry/schema_validation/models.py +574 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/settings.py +3 -4
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/utils.py +0 -16
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/pyproject.toml +27 -21
- blurry_cli-0.13.0/blurry/__init__.py +0 -382
- blurry_cli-0.13.0/blurry/schema_validation.py +0 -61
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/LICENSE +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/__main__.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/async_typer.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/cli.py +0 -0
- {blurry_cli-0.13.0/blurry/markdown/renderer_functions → blurry_cli-0.14.0/blurry/commands}/__init__.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/constants.py +0 -0
- {blurry_cli-0.13.0/blurry/plugins/html_plugins → blurry_cli-0.14.0/blurry/markdown/renderer_functions}/__init__.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/markdown/renderer_functions/render_video.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/open_graph.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/__init__.py +0 -0
- {blurry_cli-0.13.0/blurry/plugins/jinja_plugins → blurry_cli-0.14.0/blurry/plugins/html_plugins}/__init__.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/html_plugins/minify_html_plugin.py +0 -0
- {blurry_cli-0.13.0/blurry/plugins/markdown_plugins → blurry_cli-0.14.0/blurry/plugins/jinja_plugins}/__init__.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/jinja_plugins/blurry_image_extension.py +0 -0
- /blurry_cli-0.13.0/blurry/py.typed → /blurry_cli-0.14.0/blurry/plugins/markdown_plugins/__init__.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/markdown_plugins/punctuation_plugin.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/plugins/markdown_plugins/python_code_plugin.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/sitemap.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/templates.py +0 -0
- {blurry_cli-0.13.0 → blurry_cli-0.14.0}/blurry/types.py +0 -0
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: blurry-cli
|
|
3
|
-
Version: 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.
|
|
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.
|
|
25
|
-
Requires-Dist: PyLD (>=2.0.
|
|
26
|
-
Requires-Dist: Wand (>=0.6.
|
|
27
|
-
Requires-Dist: dpath (>=2.
|
|
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.
|
|
33
|
-
Requires-Dist: mistune (>=3.0
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist: rich (>=13.
|
|
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:
|
|
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
|
[](https://github.com/blurry-dev/blurry/actions/workflows/github-actions-nox.yml)
|
|
45
42
|
[](https://pypi.org/project/blurry-cli/)
|
|
46
43
|
[](https://pypi.org/project/blurry-cli/)
|
|
44
|
+
[](https://pypi.python.org/pypi/blurry-cli/)
|
|
47
45
|

|
|
48
46
|
|
|
49
|
-

|
|
47
|
+

|
|
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
|
[](https://github.com/blurry-dev/blurry/actions/workflows/github-actions-nox.yml)
|
|
4
4
|
[](https://pypi.org/project/blurry-cli/)
|
|
5
5
|
[](https://pypi.org/project/blurry-cli/)
|
|
6
|
+
[](https://pypi.python.org/pypi/blurry-cli/)
|
|
6
7
|

|
|
7
8
|
|
|
8
|
-

|
|
9
|
+

|
|
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)
|