1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350 | diff --git a/src/build.py b/src/build.py
index 2f17d3d..6063035 100644
--- a/src/build.py
+++ b/src/build.py
@@ -2,7 +2,8 @@ import re, sys
import importlib.util
import latex2mathml.converter
-from src.hash import handle_hash_sync
+from .hooks import hook, hook_call, hooks
+from .hash import handle_hash_sync
from jinja2 import Environment, FileSystemLoader
from markdown import markdown
@@ -13,6 +14,7 @@ from .config import find_project_from_path, load_config
from .errors import fatal, html_fatal
from .fileops import is_dotfile, read_file, write_file
from .log import GRAY, die, info, warn
+from . import log
# patch [ ] instead of { }
AttrListTreeprocessor.BASE_RE = r'\[\:?[ ]*([^\]\n ][^\n]*)[ ]*\]'
@@ -26,6 +28,8 @@ replace_filters = [
]
def load_plugins(config):
+ for k in hooks:
+ hooks[k] = None
module_dir = path.abspath(config.tree.plugins)
sys.path.insert(0, module_dir)
try:
@@ -41,15 +45,17 @@ def load_plugins(config):
module = importlib.util.module_from_spec(spec)
module.CONFIG = config
module.compile_page = compile_page
- module.info = info
- module.GRAY = GRAY
+ module.hook = hook
+ module.log = log
spec.loader.exec_module(module)
- return {
+ exports = {
name: value
for name, value in vars(module).items()
if not name.startswith("_")
}
+ hook_call("on_plugins_loaded",exports)
+ return exports
except Exception as e:
raise RuntimeError(f"Failed loading plugin {config.tree.plugins}: {e}") from e
@@ -69,31 +75,32 @@ def html_filter(html_content:str):
return result
def render_math(md_content: str) -> str:
- def block(m):
- return latex2mathml.converter.convert(m.group(1))
- def inline(m):
+ def convert(m):
return latex2mathml.converter.convert(m.group(1))
- md_content = re.sub(r'\<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>\</mi></mrow></math>(.+?)\<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>\</mi></mrow></math>', block, md_content, flags=re.DOTALL)
- md_content = re.sub(r'\<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo stretchy="false">(</mo><mo>.</mo><mo>+</mo><mo>?</mo><mo stretchy="false">)</mo><mi>\</mi></mrow></math>', inline, md_content)
+ md_content = re.sub(r'\<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>\</mi></mrow></math>(.+?)\<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>\</mi></mrow></math>', convert, md_content, flags=re.DOTALL)
+ md_content = re.sub(r'\<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo stretchy="false">(</mo><mo>.</mo><mo>+</mo><mo>?</mo><mo stretchy="false">)</mo><mi>\</mi></mrow></math>', convert, md_content)
+
return md_content
def jinja_handler(config, html_content, plugins=None):
try:
tree = config.tree if config else None
- env = (
- Environment(loader=FileSystemLoader(tree.templates))
- if tree is not None
- else Environment()
- )
+ if tree:
+ templates = tree.templates
+ templates = hook_call("on_jinja_template_dir", templates) or templates
+ env = ( Environment(loader=FileSystemLoader(templates)) )
+
+ else:
+ env = ( Environment() )
+
template = env.from_string(html_content)
- return (
- template.render(**plugins)
- if plugins is not None
- else template.render()
- )
+ result = template.render(**plugins) if plugins is not None else template.render()
+ result = hook_call("on_jinja_template_renderer", result) or result
+ return result
except Exception as e:
+ hook_call("on_jinja_error", e)
return html_fatal(e, f"Template error")
def escape_code_blocks(html_content: str) -> str:
@@ -115,11 +122,13 @@ def escape_code_blocks(html_content: str) -> str:
def save_html(html_content:str, html_dest:str):
makedirs(path.dirname(html_dest), exist_ok=True )
write_file( html_dest, html_content)
+ hook_call("on_page_written", html_dest, html_content)
def process_highlighting(config):
highlight = "noclasses"
if config:
highlight = config.extras.highlight
+ highlight = hook_call("on_highlight_config", highlight) or highlight
return {
"use_pygments": True,
"noclasses": highlight != "noclasses",
@@ -133,6 +142,7 @@ def process_highlighting(config):
def md_to_html(config, md_content:str):
"""Convert a Markdown file to HTML, applying filters and Jinja2 processing, and save to dest."""
math_rendered = render_math(md_content)
+ math_rendered = hook_call("on_math_renderer", math_rendered) or math_rendered
raw_html_content = markdown(
math_rendered,
extensions = [
@@ -152,14 +162,18 @@ def md_to_html(config, md_content:str):
"pymdownx.highlight": process_highlighting(config)
}
)
+ raw_html_content = hook_call("on_md_to_html", raw_html_content) or raw_html_content
escaped_html = escape_code_blocks(raw_html_content)
+ escaped_html = hook_call("on_escape_code", escaped_html) or escaped_html
filtered_html = html_filter(escaped_html)
+ filtered_html = hook_call("on_html_filter", filtered_html) or filtered_html
return filtered_html
def compile_page(md_content:str, html_dest:str | None=None, config=None, plugins=None):
html_raw = md_to_html(config, md_content)
html_content = jinja_handler(config, html_raw, plugins)
+ html_content = hook_call("on_page_rendered", html_content) or html_content
if html_dest is None:
return html_content
else:
@@ -168,6 +182,7 @@ def compile_page(md_content:str, html_dest:str | None=None, config=None, plugins
def compile_file(md_file, html_dest, config=None, plugins=None, force:bool = False):
md_content = read_file(md_file)
+ md_content = hook_call("on_page_read", md_file, md_content) or md_content
if config and not force:
hash = handle_hash_sync(config, md_file)
if hash is None and path.exists(html_dest):
@@ -179,9 +194,11 @@ def compile_file(md_file, html_dest, config=None, plugins=None, force:bool = Fal
def walk_and_build(config, plugins):
md_path = config.tree.markdown
+ hook_call("on_walk_start", config)
for parent, _, files in walk(md_path):
for filename in files:
md_file = path.join(parent, filename)
+ hook_call("on_walk_file", md_file)
md_relpath = path.relpath(md_file,md_path)
if is_dotfile(md_relpath): continue
html_dest = path.join(config.tree.dest, md_relpath)
@@ -189,6 +206,7 @@ def walk_and_build(config, plugins):
html_dest = html_dest.removesuffix(".md") + ".html"
source_file = readlink(md_file) if path.islink(md_file) else md_file
compile_file(source_file, html_dest, config, plugins)
+ hook_call("on_walk_end", config)
def build_if_file(project_path, file):
if project_path:
@@ -208,7 +226,9 @@ def build(building_type:str,project_path:str, file:list[str]):
chdir(project_path)
config = load_config()
plugins = load_plugins(config)
+ hook_call("on_build_start", config)
walk_and_build(config, plugins)
+ hook_call("on_build_end", config)
except Exception as e:
diff --git a/src/config.py b/src/config.py
index 5bd7dc3..e0b7227 100644
--- a/src/config.py
+++ b/src/config.py
@@ -4,6 +4,7 @@ from tomllib import loads
from .fileops import read_file
from .log import GRAY
from .modules import Cache, Config, Extras, Project, Tree, Webview
+from .hooks import hook_call
def find_project_from_path(project_path: str):
@@ -66,12 +67,14 @@ def load_cache_config(config) -> Cache:
def load_config() -> Config:
config_raw_content = read_file("config.toml")
- config = loads(config_raw_content)
- return Config(
- project = load_project_config(config),
- tree = load_tree_config(config),
- webview = load_webview_config(config),
- extras = load_extras_config(config),
- cache = load_cache_config(config),
+ config_loaded = loads(config_raw_content)
+ config = Config(
+ project = load_project_config(config_loaded),
+ tree = load_tree_config(config_loaded),
+ webview = load_webview_config(config_loaded),
+ extras = load_extras_config(config_loaded),
+ cache = load_cache_config(config_loaded),
)
+ config = hook_call("on_config_load", config) or config
+ return config
diff --git a/src/hash.py b/src/hash.py
index 096210b..68c5f91 100644
--- a/src/hash.py
+++ b/src/hash.py
@@ -3,6 +3,7 @@ import json
from os import path
from src.fileops import read_file, write_file
+from src.hooks import hook_call
from src.log import warn
HASH_FILE_CONTENT: dict | None = None
@@ -56,6 +57,7 @@ def handle_hash_sync(config, md_path):
current_hash = md5_handler(md_path)
md_rel_path = path.relpath(md_path, config.tree.markdown)
+ hook_call("on_hash_check", md_rel_path, current_hash)
new_hash = hash_is_modified(md_rel_path, current_hash)
if new_hash is None:
@@ -63,6 +65,7 @@ def handle_hash_sync(config, md_path):
append_md5(md_rel_path, new_hash)
write_hash_file(config)
+ hook_call("on_hash_written", md_rel_path, new_hash)
return True
except Exception as e:
diff --git a/src/hooks.py b/src/hooks.py
new file mode 100644
index 0000000..9d52334
--- /dev/null
+++ b/src/hooks.py
@@ -0,0 +1,58 @@
+from .log import GRAY, warn
+
+hooks = {
+ "on_after_compile_file" : None,
+ "on_after_html_filter" : None,
+ "on_after_jinja" : None,
+ "on_after_math" : None,
+ "on_after_escape_code" : None,
+ "on_after_md_to_html" : None,
+ "on_after_markdown" : None,
+ "on_after_template" : None,
+ "on_before_escape_code" : None,
+ "on_before_compile_file" : None,
+ "on_before_html_filter" : None,
+ "on_before_markdown" : None,
+ "on_before_math" : None,
+ "on_before_md_to_html" : None,
+ "on_before_save" : None,
+ "on_before_template" : None,
+ "on_build_end" : None,
+ "on_build_start" : None,
+ "on_config_load" : None,
+ "on_file_changed" : None,
+ "on_hash_check" : None,
+ "on_hash_written" : None,
+ "on_highlight_config" : None,
+ "on_page_read" : None,
+ "on_page_skip" : None,
+ "on_page_written" : None,
+ "on_plugins_loaded" : None,
+ "on_reload_error" : None,
+ "on_template_error" : None,
+ "on_walk_end" : None,
+ "on_walk_file" : None,
+ "on_walk_start" : None,
+ "on_watch_start" : None,
+ "on_watch_stop" : None,
+}
+
+def hook(hook_name:str):
+ if hook_name in hooks:
+ def hook_fn(fn):
+ if not hooks.get(hook_name) == None:
+ warn(f"Overwriting existing hook '{hook_name}'.")
+ hooks[hook_name]=fn
+ return fn
+ return hook_fn
+
+ else:
+ warn(f"Skipping hook `{GRAY(hook_name)}`: hook is not exists")
+ return lambda fn:fn
+
+def hook_call(hook_name, *args, **kwargs):
+ if hook_name not in hooks:
+ warn(hook_name)
+ func = hooks.get(hook_name)
+ if callable(func):
+ return func(*args, **kwargs)
diff --git a/src/watcher.py b/src/watcher.py
index 32dda4a..78e5f04 100644
--- a/src/watcher.py
+++ b/src/watcher.py
@@ -4,6 +4,8 @@ from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import time
+from src.hooks import hook_call
+
from .config import find_project_from_path, load_config
from .build import compile_file, load_plugins
from .log import warn
@@ -27,6 +29,7 @@ def reload(changed_path, config, plugins, force=None):
return None
return html
except Exception as e:
+ hook_call("on_reload_error", e)
warn(str(e))
def watch_files(config, reload_func:Callable | None=None):
@@ -49,6 +52,7 @@ def watch_files(config, reload_func:Callable | None=None):
if now - last < 0.3:
return
last_reload[reload_path] = now
+ hook_call("on_file_changed", reload_path, config)
file = reload( reload_path, config, plugins, force=force)
if reload_func and file: reload_func(file)
observer = Observer()
@@ -59,6 +63,7 @@ def watch_files(config, reload_func:Callable | None=None):
path=file,
recursive=True
)
+ hook_call("on_watch_start", config)
return observer
def run_watcher(project_path):
@@ -74,6 +79,7 @@ def run_watcher(project_path):
time.sleep(1)
except KeyboardInterrupt:
observers.stop()
+ hook_call("on_watch_stop", config)
observers.join()
except Exception as e:
|