zensical 0.0.9__cp310-abi3-musllinux_1_2_i686.whl → 0.0.17__cp310-abi3-musllinux_1_2_i686.whl

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.

Potentially problematic release.


This version of zensical might be problematic. Click here for more details.

Files changed (310) hide show
  1. zensical/__init__.py +6 -6
  2. zensical/__main__.py +2 -2
  3. zensical/bootstrap/.github/workflows/docs.yml +1 -0
  4. zensical/bootstrap/zensical.toml +6 -6
  5. zensical/config.py +223 -201
  6. zensical/extensions/__init__.py +2 -2
  7. zensical/extensions/emoji.py +22 -27
  8. zensical/extensions/links.py +21 -25
  9. zensical/extensions/preview.py +29 -41
  10. zensical/extensions/search.py +83 -83
  11. zensical/extensions/utilities/__init__.py +2 -2
  12. zensical/extensions/utilities/filter.py +5 -10
  13. zensical/main.py +35 -48
  14. zensical/markdown.py +82 -21
  15. zensical/templates/.icons/lucide/LICENSE +39 -0
  16. zensical/templates/.icons/lucide/ampersand.svg +1 -1
  17. zensical/templates/.icons/lucide/anchor.svg +1 -1
  18. zensical/templates/.icons/lucide/balloon.svg +1 -0
  19. zensical/templates/.icons/lucide/birdhouse.svg +1 -0
  20. zensical/templates/.icons/lucide/book-search.svg +1 -0
  21. zensical/templates/.icons/lucide/brush-cleaning.svg +1 -1
  22. zensical/templates/.icons/lucide/bubbles.svg +1 -1
  23. zensical/templates/.icons/lucide/calendar-fold.svg +1 -1
  24. zensical/templates/.icons/lucide/calendars.svg +1 -0
  25. zensical/templates/.icons/lucide/cannabis-off.svg +1 -0
  26. zensical/templates/.icons/lucide/chess-bishop.svg +1 -0
  27. zensical/templates/.icons/lucide/chess-king.svg +1 -0
  28. zensical/templates/.icons/lucide/chess-knight.svg +1 -0
  29. zensical/templates/.icons/lucide/chess-pawn.svg +1 -0
  30. zensical/templates/.icons/lucide/chess-queen.svg +1 -0
  31. zensical/templates/.icons/lucide/chess-rook.svg +1 -0
  32. zensical/templates/.icons/lucide/circle-pile.svg +1 -0
  33. zensical/templates/.icons/lucide/clock-check.svg +1 -0
  34. zensical/templates/.icons/lucide/cloud-backup.svg +1 -0
  35. zensical/templates/.icons/lucide/cloud-sync.svg +1 -0
  36. zensical/templates/.icons/lucide/file-archive.svg +1 -1
  37. zensical/templates/.icons/lucide/file-audio-2.svg +1 -1
  38. zensical/templates/.icons/lucide/file-audio.svg +1 -1
  39. zensical/templates/.icons/lucide/file-axis-3-d.svg +1 -1
  40. zensical/templates/.icons/lucide/file-axis-3d.svg +1 -1
  41. zensical/templates/.icons/lucide/file-badge-2.svg +1 -1
  42. zensical/templates/.icons/lucide/file-badge.svg +1 -1
  43. zensical/templates/.icons/lucide/file-bar-chart-2.svg +1 -1
  44. zensical/templates/.icons/lucide/file-bar-chart.svg +1 -1
  45. zensical/templates/.icons/lucide/file-box.svg +1 -1
  46. zensical/templates/.icons/lucide/file-braces-corner.svg +1 -0
  47. zensical/templates/.icons/lucide/file-braces.svg +1 -0
  48. zensical/templates/.icons/lucide/file-chart-column-increasing.svg +1 -1
  49. zensical/templates/.icons/lucide/file-chart-column.svg +1 -1
  50. zensical/templates/.icons/lucide/file-chart-line.svg +1 -1
  51. zensical/templates/.icons/lucide/file-chart-pie.svg +1 -1
  52. zensical/templates/.icons/lucide/file-check-2.svg +1 -1
  53. zensical/templates/.icons/lucide/file-check-corner.svg +1 -0
  54. zensical/templates/.icons/lucide/file-check.svg +1 -1
  55. zensical/templates/.icons/lucide/file-clock.svg +1 -1
  56. zensical/templates/.icons/lucide/file-code-2.svg +1 -1
  57. zensical/templates/.icons/lucide/file-code-corner.svg +1 -0
  58. zensical/templates/.icons/lucide/file-code.svg +1 -1
  59. zensical/templates/.icons/lucide/file-cog-2.svg +1 -1
  60. zensical/templates/.icons/lucide/file-cog.svg +1 -1
  61. zensical/templates/.icons/lucide/file-diff.svg +1 -1
  62. zensical/templates/.icons/lucide/file-digit.svg +1 -1
  63. zensical/templates/.icons/lucide/file-down.svg +1 -1
  64. zensical/templates/.icons/lucide/file-edit.svg +1 -1
  65. zensical/templates/.icons/lucide/file-exclamation-point.svg +1 -0
  66. zensical/templates/.icons/lucide/file-headphone.svg +1 -0
  67. zensical/templates/.icons/lucide/file-heart.svg +1 -1
  68. zensical/templates/.icons/lucide/file-image.svg +1 -1
  69. zensical/templates/.icons/lucide/file-input.svg +1 -1
  70. zensical/templates/.icons/lucide/file-json-2.svg +1 -1
  71. zensical/templates/.icons/lucide/file-json.svg +1 -1
  72. zensical/templates/.icons/lucide/file-key-2.svg +1 -1
  73. zensical/templates/.icons/lucide/file-key.svg +1 -1
  74. zensical/templates/.icons/lucide/file-line-chart.svg +1 -1
  75. zensical/templates/.icons/lucide/file-lock-2.svg +1 -1
  76. zensical/templates/.icons/lucide/file-lock.svg +1 -1
  77. zensical/templates/.icons/lucide/file-minus-2.svg +1 -1
  78. zensical/templates/.icons/lucide/file-minus-corner.svg +1 -0
  79. zensical/templates/.icons/lucide/file-minus.svg +1 -1
  80. zensical/templates/.icons/lucide/file-music.svg +1 -1
  81. zensical/templates/.icons/lucide/file-output.svg +1 -1
  82. zensical/templates/.icons/lucide/file-pen-line.svg +1 -1
  83. zensical/templates/.icons/lucide/file-pen.svg +1 -1
  84. zensical/templates/.icons/lucide/file-pie-chart.svg +1 -1
  85. zensical/templates/.icons/lucide/file-play.svg +1 -1
  86. zensical/templates/.icons/lucide/file-plus-2.svg +1 -1
  87. zensical/templates/.icons/lucide/file-plus-corner.svg +1 -0
  88. zensical/templates/.icons/lucide/file-plus.svg +1 -1
  89. zensical/templates/.icons/lucide/file-question-mark.svg +1 -1
  90. zensical/templates/.icons/lucide/file-question.svg +1 -1
  91. zensical/templates/.icons/lucide/file-scan.svg +1 -1
  92. zensical/templates/.icons/lucide/file-search-2.svg +1 -1
  93. zensical/templates/.icons/lucide/file-search-corner.svg +1 -0
  94. zensical/templates/.icons/lucide/file-search.svg +1 -1
  95. zensical/templates/.icons/lucide/file-signal.svg +1 -0
  96. zensical/templates/.icons/lucide/file-signature.svg +1 -1
  97. zensical/templates/.icons/lucide/file-sliders.svg +1 -1
  98. zensical/templates/.icons/lucide/file-spreadsheet.svg +1 -1
  99. zensical/templates/.icons/lucide/file-symlink.svg +1 -1
  100. zensical/templates/.icons/lucide/file-terminal.svg +1 -1
  101. zensical/templates/.icons/lucide/file-text.svg +1 -1
  102. zensical/templates/.icons/lucide/file-type-2.svg +1 -1
  103. zensical/templates/.icons/lucide/file-type-corner.svg +1 -0
  104. zensical/templates/.icons/lucide/file-type.svg +1 -1
  105. zensical/templates/.icons/lucide/file-up.svg +1 -1
  106. zensical/templates/.icons/lucide/file-user.svg +1 -1
  107. zensical/templates/.icons/lucide/file-video-2.svg +1 -1
  108. zensical/templates/.icons/lucide/file-video-camera.svg +1 -1
  109. zensical/templates/.icons/lucide/file-video.svg +1 -1
  110. zensical/templates/.icons/lucide/file-volume-2.svg +1 -1
  111. zensical/templates/.icons/lucide/file-volume.svg +1 -1
  112. zensical/templates/.icons/lucide/file-warning.svg +1 -1
  113. zensical/templates/.icons/lucide/file-x-2.svg +1 -1
  114. zensical/templates/.icons/lucide/file-x-corner.svg +1 -0
  115. zensical/templates/.icons/lucide/file-x.svg +1 -1
  116. zensical/templates/.icons/lucide/file.svg +1 -1
  117. zensical/templates/.icons/lucide/files.svg +1 -1
  118. zensical/templates/.icons/lucide/fingerprint-pattern.svg +1 -0
  119. zensical/templates/.icons/lucide/fishing-hook.svg +1 -0
  120. zensical/templates/.icons/lucide/flashlight-off.svg +1 -1
  121. zensical/templates/.icons/lucide/flashlight.svg +1 -1
  122. zensical/templates/.icons/lucide/folder-git-2.svg +1 -1
  123. zensical/templates/.icons/lucide/form.svg +1 -0
  124. zensical/templates/.icons/lucide/gamepad-directional.svg +1 -0
  125. zensical/templates/.icons/lucide/git-branch-minus.svg +1 -0
  126. zensical/templates/.icons/lucide/hd.svg +1 -0
  127. zensical/templates/.icons/lucide/helicopter.svg +1 -0
  128. zensical/templates/.icons/lucide/layers-plus.svg +1 -0
  129. zensical/templates/.icons/lucide/memory-stick.svg +1 -1
  130. zensical/templates/.icons/lucide/microchip.svg +1 -1
  131. zensical/templates/.icons/lucide/mouse-pointer-2-off.svg +1 -0
  132. zensical/templates/.icons/lucide/paint-bucket.svg +1 -1
  133. zensical/templates/.icons/lucide/plug.svg +1 -1
  134. zensical/templates/.icons/lucide/ruler-dimension-line.svg +1 -1
  135. zensical/templates/.icons/lucide/scale.svg +1 -1
  136. zensical/templates/.icons/lucide/scissors-square-dashed-bottom.svg +1 -1
  137. zensical/templates/.icons/lucide/scissors-square.svg +1 -1
  138. zensical/templates/.icons/lucide/scooter.svg +1 -0
  139. zensical/templates/.icons/lucide/search-alert.svg +1 -0
  140. zensical/templates/.icons/lucide/shredder.svg +1 -1
  141. zensical/templates/.icons/lucide/solar-panel.svg +1 -0
  142. zensical/templates/.icons/lucide/square-bottom-dashed-scissors.svg +1 -1
  143. zensical/templates/.icons/lucide/square-scissors.svg +1 -1
  144. zensical/templates/.icons/lucide/sticker.svg +1 -1
  145. zensical/templates/.icons/lucide/sticky-note.svg +1 -1
  146. zensical/templates/.icons/lucide/stone.svg +1 -0
  147. zensical/templates/.icons/lucide/thermometer-sun.svg +1 -1
  148. zensical/templates/.icons/lucide/thumbs-down.svg +1 -1
  149. zensical/templates/.icons/lucide/thumbs-up.svg +1 -1
  150. zensical/templates/.icons/lucide/tickets-plane.svg +1 -1
  151. zensical/templates/.icons/lucide/tickets.svg +1 -1
  152. zensical/templates/.icons/lucide/toolbox.svg +1 -0
  153. zensical/templates/.icons/lucide/van.svg +1 -0
  154. zensical/templates/.icons/lucide/waves-arrow-down.svg +1 -0
  155. zensical/templates/.icons/lucide/waves-arrow-up.svg +1 -0
  156. zensical/templates/.icons/lucide/weight-tilde.svg +1 -0
  157. zensical/templates/.icons/octicons/boolean-off-16.svg +1 -0
  158. zensical/templates/.icons/octicons/boolean-off-24.svg +1 -0
  159. zensical/templates/.icons/octicons/boolean-on-16.svg +1 -0
  160. zensical/templates/.icons/octicons/boolean-on-24.svg +1 -0
  161. zensical/templates/.icons/octicons/compose-16.svg +1 -0
  162. zensical/templates/.icons/octicons/compose-24.svg +1 -0
  163. zensical/templates/.icons/octicons/crosshairs-16.svg +1 -0
  164. zensical/templates/.icons/octicons/crosshairs-24.svg +1 -0
  165. zensical/templates/.icons/octicons/dice-16.svg +1 -0
  166. zensical/templates/.icons/octicons/dice-24.svg +1 -0
  167. zensical/templates/.icons/octicons/exclamation-16.svg +1 -0
  168. zensical/templates/.icons/octicons/exclamation-24.svg +1 -0
  169. zensical/templates/.icons/octicons/file-check-16.svg +1 -0
  170. zensical/templates/.icons/octicons/file-check-24.svg +1 -0
  171. zensical/templates/.icons/octicons/flowchart-16.svg +1 -0
  172. zensical/templates/.icons/octicons/flowchart-24.svg +1 -0
  173. zensical/templates/.icons/octicons/focus-center-16.svg +1 -0
  174. zensical/templates/.icons/octicons/focus-center-24.svg +1 -0
  175. zensical/templates/.icons/octicons/git-branch-check-16.svg +1 -0
  176. zensical/templates/.icons/octicons/git-branch-check-24.svg +1 -0
  177. zensical/templates/.icons/octicons/graph-bar-horizontal-16.svg +1 -0
  178. zensical/templates/.icons/octicons/graph-bar-horizontal-24.svg +1 -0
  179. zensical/templates/.icons/octicons/graph-bar-vertical-16.svg +1 -0
  180. zensical/templates/.icons/octicons/graph-bar-vertical-24.svg +1 -0
  181. zensical/templates/.icons/octicons/inbox-fill-16.svg +1 -0
  182. zensical/templates/.icons/octicons/inbox-fill-24.svg +1 -0
  183. zensical/templates/.icons/octicons/node-16.svg +1 -0
  184. zensical/templates/.icons/octicons/node-24.svg +1 -0
  185. zensical/templates/.icons/octicons/pencil-ai-16.svg +1 -0
  186. zensical/templates/.icons/octicons/pencil-ai-24.svg +1 -0
  187. zensical/templates/.icons/octicons/smiley-frown-16.svg +1 -0
  188. zensical/templates/.icons/octicons/smiley-frown-24.svg +1 -0
  189. zensical/templates/.icons/octicons/smiley-frustrated-16.svg +1 -0
  190. zensical/templates/.icons/octicons/smiley-frustrated-24.svg +1 -0
  191. zensical/templates/.icons/octicons/smiley-grin-16.svg +1 -0
  192. zensical/templates/.icons/octicons/smiley-grin-24.svg +1 -0
  193. zensical/templates/.icons/octicons/smiley-neutral-16.svg +1 -0
  194. zensical/templates/.icons/octicons/smiley-neutral-24.svg +1 -0
  195. zensical/templates/.icons/octicons/spacing-large-16.svg +1 -0
  196. zensical/templates/.icons/octicons/spacing-large-24.svg +1 -0
  197. zensical/templates/.icons/octicons/spacing-medium-16.svg +1 -0
  198. zensical/templates/.icons/octicons/spacing-medium-24.svg +1 -0
  199. zensical/templates/.icons/octicons/spacing-small-16.svg +1 -0
  200. zensical/templates/.icons/octicons/spacing-small-24.svg +1 -0
  201. zensical/templates/.icons/octicons/split-view-16.svg +1 -0
  202. zensical/templates/.icons/octicons/split-view-24.svg +1 -0
  203. zensical/templates/.icons/octicons/unwrap-16.svg +1 -0
  204. zensical/templates/.icons/octicons/unwrap-24.svg +1 -0
  205. zensical/templates/.icons/octicons/vscode-16.svg +1 -1
  206. zensical/templates/.icons/octicons/vscode-32.svg +1 -1
  207. zensical/templates/.icons/octicons/vscode-48.svg +1 -1
  208. zensical/templates/.icons/octicons/wrap-16.svg +1 -0
  209. zensical/templates/.icons/octicons/wrap-24.svg +1 -0
  210. zensical/templates/.icons/simple/acode.svg +1 -0
  211. zensical/templates/.icons/simple/apacheavro.svg +1 -0
  212. zensical/templates/.icons/simple/appimage.svg +1 -0
  213. zensical/templates/.icons/simple/appmanager.svg +1 -0
  214. zensical/templates/.icons/simple/autentique.svg +1 -0
  215. zensical/templates/.icons/simple/b4x.svg +1 -0
  216. zensical/templates/.icons/simple/bioconductor.svg +1 -0
  217. zensical/templates/.icons/simple/coolify.svg +1 -0
  218. zensical/templates/.icons/simple/cursor.svg +1 -0
  219. zensical/templates/.icons/simple/dash0.svg +1 -0
  220. zensical/templates/.icons/simple/dodopayments.svg +1 -0
  221. zensical/templates/.icons/simple/elk.svg +1 -0
  222. zensical/templates/.icons/simple/fishaudio.svg +1 -0
  223. zensical/templates/.icons/simple/ghostty.svg +1 -0
  224. zensical/templates/.icons/simple/glance.svg +1 -0
  225. zensical/templates/.icons/simple/hashcat.svg +1 -0
  226. zensical/templates/.icons/simple/kando.svg +1 -0
  227. zensical/templates/.icons/simple/labex.svg +1 -0
  228. zensical/templates/.icons/simple/listenhub.svg +1 -0
  229. zensical/templates/.icons/simple/luanti.svg +1 -0
  230. zensical/templates/.icons/simple/maas.svg +1 -1
  231. zensical/templates/.icons/simple/mailbox.svg +1 -0
  232. zensical/templates/.icons/simple/mangacollec.svg +1 -0
  233. zensical/templates/.icons/simple/mdblist.svg +1 -0
  234. zensical/templates/.icons/simple/minimax.svg +1 -0
  235. zensical/templates/.icons/simple/newgrounds.svg +1 -0
  236. zensical/templates/.icons/simple/nodegui.svg +1 -0
  237. zensical/templates/.icons/simple/openrouter.svg +1 -0
  238. zensical/templates/.icons/simple/passbolt.svg +1 -0
  239. zensical/templates/.icons/simple/plane.svg +1 -0
  240. zensical/templates/.icons/simple/postiz.svg +1 -0
  241. zensical/templates/.icons/simple/qlty.svg +1 -0
  242. zensical/templates/.icons/simple/rekaui.svg +1 -0
  243. zensical/templates/.icons/simple/retroachievements.svg +1 -0
  244. zensical/templates/.icons/simple/root.svg +1 -0
  245. zensical/templates/.icons/simple/setuptools.svg +1 -0
  246. zensical/templates/.icons/simple/tanstack.svg +1 -0
  247. zensical/templates/.icons/simple/textual.svg +1 -0
  248. zensical/templates/assets/javascripts/LICENSE +29 -0
  249. zensical/templates/assets/javascripts/bundle.8ffeb9c9.min.js +3 -0
  250. zensical/templates/assets/javascripts/workers/search.e2d2d235.min.js +1 -0
  251. zensical/templates/assets/stylesheets/classic/main.9a39631f.min.css +1 -0
  252. zensical/templates/assets/stylesheets/modern/main.d4922b3c.min.css +1 -0
  253. zensical/templates/base.html +4 -4
  254. zensical/zensical.abi3.so +0 -0
  255. zensical/zensical.pyi +7 -13
  256. {zensical-0.0.9.dist-info → zensical-0.0.17.dist-info}/METADATA +9 -5
  257. {zensical-0.0.9.dist-info → zensical-0.0.17.dist-info}/RECORD +262 -172
  258. {zensical-0.0.9.dist-info → zensical-0.0.17.dist-info}/WHEEL +1 -1
  259. {zensical-0.0.9.dist-info → zensical-0.0.17.dist-info}/licenses/LICENSE.md +1 -1
  260. zensical.libs/libgcc_s-f5fcfe20.so.1 +0 -0
  261. zensical/templates/.icons/simple/aerlingus.svg +0 -1
  262. zensical/templates/.icons/simple/aerospike.svg +0 -1
  263. zensical/templates/.icons/simple/aew.svg +0 -1
  264. zensical/templates/.icons/simple/affinity.svg +0 -1
  265. zensical/templates/.icons/simple/affinitydesigner.svg +0 -1
  266. zensical/templates/.icons/simple/affinityphoto.svg +0 -1
  267. zensical/templates/.icons/simple/affinitypublisher.svg +0 -1
  268. zensical/templates/.icons/simple/alfaromeo.svg +0 -1
  269. zensical/templates/.icons/simple/allocine.svg +0 -1
  270. zensical/templates/.icons/simple/alteryx.svg +0 -1
  271. zensical/templates/.icons/simple/altiumdesigner.svg +0 -1
  272. zensical/templates/.icons/simple/alx.svg +0 -1
  273. zensical/templates/.icons/simple/authy.svg +0 -1
  274. zensical/templates/.icons/simple/canva.svg +0 -1
  275. zensical/templates/.icons/simple/codepen.svg +0 -1
  276. zensical/templates/.icons/simple/cognizant.svg +0 -1
  277. zensical/templates/.icons/simple/dbt.svg +0 -1
  278. zensical/templates/.icons/simple/flipkart.svg +0 -1
  279. zensical/templates/.icons/simple/googlefit.svg +0 -1
  280. zensical/templates/.icons/simple/heroku.svg +0 -1
  281. zensical/templates/.icons/simple/informatica.svg +0 -1
  282. zensical/templates/.icons/simple/invision.svg +0 -1
  283. zensical/templates/.icons/simple/jaguar.svg +0 -1
  284. zensical/templates/.icons/simple/landrover.svg +0 -1
  285. zensical/templates/.icons/simple/logitech.svg +0 -1
  286. zensical/templates/.icons/simple/logitechg.svg +0 -1
  287. zensical/templates/.icons/simple/mailboxdotorg.svg +0 -1
  288. zensical/templates/.icons/simple/minetest.svg +0 -1
  289. zensical/templates/.icons/simple/mulesoft.svg +0 -1
  290. zensical/templates/.icons/simple/musescore.svg +0 -1
  291. zensical/templates/.icons/simple/nexusmods.svg +0 -1
  292. zensical/templates/.icons/simple/openai.svg +0 -1
  293. zensical/templates/.icons/simple/pocket.svg +0 -1
  294. zensical/templates/.icons/simple/quip.svg +0 -1
  295. zensical/templates/.icons/simple/salesforce.svg +0 -1
  296. zensical/templates/.icons/simple/scribd.svg +0 -1
  297. zensical/templates/.icons/simple/sendgrid.svg +0 -1
  298. zensical/templates/.icons/simple/shutterstock.svg +0 -1
  299. zensical/templates/.icons/simple/slack.svg +0 -1
  300. zensical/templates/.icons/simple/tunein.svg +0 -1
  301. zensical/templates/.icons/simple/twilio.svg +0 -1
  302. zensical/templates/.icons/simple/walmart.svg +0 -1
  303. zensical/templates/.icons/simple/warnerbros.svg +0 -1
  304. zensical/templates/.icons/simple/westerndigital.svg +0 -1
  305. zensical/templates/assets/javascripts/bundle.21aa498e.min.js +0 -3
  306. zensical/templates/assets/javascripts/workers/search.5e1f2129.min.js +0 -1
  307. zensical/templates/assets/stylesheets/classic/main.6eec86b3.min.css +0 -1
  308. zensical/templates/assets/stylesheets/modern/main.2644c6b7.min.css +0 -1
  309. zensical.libs/libgcc_s-27e5a392.so.1 +0 -0
  310. {zensical-0.0.9.dist-info → zensical-0.0.17.dist-info}/entry_points.txt +0 -0
zensical/config.py CHANGED
@@ -1,7 +1,7 @@
1
- # Copyright (c) 2025 Zensical and contributors
1
+ # Copyright (c) 2025-2026 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under DCO
4
+ # All contributions are certified under the DCO
5
5
 
6
6
  # Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  # of this software and associated documentation files (the "Software"), to
@@ -26,22 +26,24 @@ from __future__ import annotations
26
26
  import hashlib
27
27
  import importlib
28
28
  import os
29
+ from pathlib import Path
29
30
  import pickle
30
- import yaml
31
-
32
- try:
33
- import tomllib
34
- except ModuleNotFoundError:
35
- import tomli as tomllib # type: ignore
31
+ from typing import IO, Any
32
+ from urllib.parse import urlparse
36
33
 
34
+ import yaml
37
35
  from click import ClickException
38
36
  from deepmerge import always_merger
39
- from typing import Any, IO
40
37
  from yaml import BaseLoader, Loader, YAMLError
41
38
  from yaml.constructor import ConstructorError
42
- from urllib.parse import urlparse
43
39
 
44
- from .extensions.emoji import to_svg, twemoji
40
+ from zensical.extensions.emoji import to_svg, twemoji
41
+
42
+ try:
43
+ import tomllib
44
+ except ModuleNotFoundError:
45
+ import tomli as tomllib # type: ignore[no-redef]
46
+
45
47
 
46
48
  # ----------------------------------------------------------------------------
47
49
  # Globals
@@ -64,9 +66,7 @@ side, and use it directly when needed. It's a hack but will do for now.
64
66
 
65
67
 
66
68
  class ConfigurationError(ClickException):
67
- """
68
- Configuration resolution or validation failed.
69
- """
69
+ """Configuration resolution or validation failed."""
70
70
 
71
71
 
72
72
  # ----------------------------------------------------------------------------
@@ -75,22 +75,17 @@ class ConfigurationError(ClickException):
75
75
 
76
76
 
77
77
  def parse_config(path: str) -> dict:
78
- """
79
- Parse configuration file.
80
- """
78
+ """Parse configuration file."""
81
79
  # Decide by extension; no need to convert to Path
82
80
  _, ext = os.path.splitext(path)
83
81
  if ext.lower() == ".toml":
84
82
  return parse_zensical_config(path)
85
- else:
86
- return parse_mkdocs_config(path)
83
+ return parse_mkdocs_config(path)
87
84
 
88
85
 
89
86
  def parse_zensical_config(path: str) -> dict:
90
- """
91
- Parse zensical.toml configuration file.
92
- """
93
- global _CONFIG
87
+ """Parse zensical.toml configuration file."""
88
+ global _CONFIG # noqa: PLW0603
94
89
  with open(path, "rb") as f:
95
90
  config = tomllib.load(f)
96
91
  if "project" in config:
@@ -102,11 +97,9 @@ def parse_zensical_config(path: str) -> dict:
102
97
 
103
98
 
104
99
  def parse_mkdocs_config(path: str) -> dict:
105
- """
106
- Parse mkdocs.yml configuration file.
107
- """
108
- global _CONFIG
109
- with open(path, "r") as f:
100
+ """Parse mkdocs.yml configuration file."""
101
+ global _CONFIG # noqa: PLW0603
102
+ with open(path, encoding="utf-8") as f:
110
103
  config = _yaml_load(f)
111
104
 
112
105
  # Apply defaults and return parsed configuration
@@ -114,24 +107,30 @@ def parse_mkdocs_config(path: str) -> dict:
114
107
  return _CONFIG
115
108
 
116
109
 
117
- def get_config():
118
- """
119
- Return configuration.
120
- """
121
- return _CONFIG
110
+ def get_config() -> dict:
111
+ """Return configuration."""
112
+ # We assume this function is only called after populating `_CONFIG`.
113
+ return _CONFIG # type: ignore[return-value]
122
114
 
123
115
 
124
116
  def get_theme_dir() -> str:
125
- """
126
- Return the theme directory.
127
- """
117
+ """Return the theme directory."""
128
118
  path = os.path.dirname(os.path.abspath(__file__))
129
119
  return os.path.join(path, "templates")
130
120
 
131
121
 
122
+ def get_custom_theme_dir(config: dict) -> str | None:
123
+ """Return the custom theme directory."""
124
+ path = os.path.dirname(os.path.abspath(__file__))
125
+ if config["theme"].get("custom_dir"):
126
+ return os.path.join(path, config["theme"].get("custom_dir"))
127
+
128
+ # Otherwise, return no path
129
+ return None
130
+
131
+
132
132
  def _apply_defaults(config: dict, path: str) -> dict:
133
- """
134
- Apply default settings in configuration.
133
+ """Apply default settings in configuration.
135
134
 
136
135
  Note that this is loosely based on the defaults that MkDocs sets in its own
137
136
  configuration system, which we won't port for compatibility right now, as
@@ -146,12 +145,12 @@ def _apply_defaults(config: dict, path: str) -> dict:
146
145
 
147
146
  # Set site directory
148
147
  set_default(config, "site_dir", "site", str)
149
- if ".." in config.get("site_dir"):
148
+ if ".." in config.get("site_dir", ""):
150
149
  raise ConfigurationError("site_dir must not contain '..'")
151
150
 
152
151
  # Set docs directory
153
152
  set_default(config, "docs_dir", "docs", str)
154
- if ".." in config.get("docs_dir"):
153
+ if ".." in config.get("docs_dir", ""):
155
154
  raise ConfigurationError("docs_dir must not contain '..'")
156
155
 
157
156
  # Set defaults for core settings
@@ -169,21 +168,27 @@ def _apply_defaults(config: dict, path: str) -> dict:
169
168
  set_default(config, "edit_uri", None, str)
170
169
 
171
170
  # Set defaults for repository name settings
171
+ docs_dir = config.get("docs_dir")
172
+ repo_names = {
173
+ "github.com": "GitHub",
174
+ "gitlab.com": "Gitlab",
175
+ "bitbucket.org": "Bitbucket",
176
+ }
177
+ edit_uris = {
178
+ "github.com": f"edit/master/{docs_dir}",
179
+ "gitlab.com": f"edit/master/{docs_dir}",
180
+ "bitbucket.org": f"src/default/{docs_dir}",
181
+ }
172
182
  repo_url = config.get("repo_url")
173
- if repo_url and not config.get("repo_name"):
174
- docs_dir = config.get("docs_dir")
183
+ if repo_url:
175
184
  host = urlparse(repo_url).hostname or ""
176
- if host == "github.com":
177
- set_default(config, "repo_name", "GitHub", str)
178
- set_default(config, "edit_uri", f"edit/master/{docs_dir}", str)
179
- elif host == "gitlab.com":
180
- set_default(config, "repo_name", "GitLab", str)
181
- set_default(config, "edit_uri", f"edit/master/{docs_dir}", str)
182
- elif host == "bitbucket.org":
183
- set_default(config, "repo_name", "Bitbucket", str)
184
- set_default(config, "edit_uri", f"src/default/{docs_dir}", str)
185
- elif host:
186
- config["repo_name"] = host.split(".")[0].title()
185
+ if not config.get("repo_name"):
186
+ if host in repo_names:
187
+ set_default(config, "repo_name", repo_names[host], str)
188
+ elif host:
189
+ config["repo_name"] = host.split(".")[0].title()
190
+ if host in edit_uris:
191
+ set_default(config, "edit_uri", edit_uris[host], str)
187
192
 
188
193
  # Remove trailing slash from edit_uri if present
189
194
  edit_uri = config.get("edit_uri")
@@ -258,18 +263,12 @@ def _apply_defaults(config: dict, path: str) -> dict:
258
263
 
259
264
  # Set defaults for theme admonition icons
260
265
  admonition = set_default(icon, "admonition", {}, dict)
261
- set_default(admonition, "note", None, str)
262
- set_default(admonition, "abstract", None, str)
263
- set_default(admonition, "info", None, str)
264
- set_default(admonition, "tip", None, str)
265
- set_default(admonition, "success", None, str)
266
- set_default(admonition, "question", None, str)
267
- set_default(admonition, "warning", None, str)
268
- set_default(admonition, "failure", None, str)
269
- set_default(admonition, "danger", None, str)
270
- set_default(admonition, "bug", None, str)
271
- set_default(admonition, "example", None, str)
272
- set_default(admonition, "quote", None, str)
266
+ if isinstance(admonition, dict):
267
+ icon["admonition"] = {
268
+ str(key): str(value)
269
+ for key, value in admonition.items()
270
+ if value is not None
271
+ }
273
272
 
274
273
  # Set defaults for theme palette settings and normalize to list
275
274
  palette = theme.setdefault("palette", [])
@@ -292,73 +291,17 @@ def _apply_defaults(config: dict, path: str) -> dict:
292
291
  set_default(toggle, "name", None, str)
293
292
 
294
293
  # Set defaults for extra settings
294
+ if "extra" in config and not isinstance(config["extra"], dict):
295
+ raise ConfigurationError(
296
+ "The 'extra' setting must be a mapping/dictionary."
297
+ )
295
298
  extra = set_default(config, "extra", {}, dict)
296
- set_default(extra, "homepage", None, str)
297
- set_default(extra, "scope", None, str)
298
- set_default(extra, "annotate", {}, dict)
299
- set_default(extra, "tags", {}, dict)
300
- set_default(extra, "generator", True, bool)
299
+
300
+ if "polyfills" in extra and not isinstance(extra["polyfills"], list):
301
+ raise ConfigurationError(
302
+ "The 'extra.polyfills' setting must be a list."
303
+ )
301
304
  set_default(extra, "polyfills", [], list)
302
- set_default(extra, "analytics", None, dict)
303
-
304
- # Set defaults for extra analytics settings
305
- analytics = extra.get("analytics")
306
- if analytics:
307
- set_default(analytics, "provider", None, str)
308
- set_default(analytics, "property", None, str)
309
- set_default(analytics, "feedback", None, dict)
310
-
311
- # Set defaults for extra analytics feedback settings
312
- feedback = analytics.get("feedback")
313
- if feedback:
314
- set_default(feedback, "title", None, str)
315
- set_default(feedback, "ratings", [], list)
316
-
317
- # Set defaults for each rating entry
318
- ratings = feedback.setdefault("ratings", [])
319
- for entry in ratings:
320
- set_default(entry, "icon", None, str)
321
- set_default(entry, "name", None, str)
322
- set_default(entry, "data", None, str)
323
- set_default(entry, "note", None, str)
324
-
325
- # Set defaults for extra consent settings
326
- consent = extra.setdefault("consent", None)
327
- if consent:
328
- set_default(consent, "title", None, str)
329
- set_default(consent, "description", None, str)
330
- set_default(consent, "actions", [], list)
331
-
332
- # Set defaults for extra consent cookie settings
333
- cookies = consent.setdefault("cookies", {})
334
- for key, value in cookies.items():
335
- if isinstance(value, str):
336
- cookies[key] = {"name": value, "checked": False}
337
-
338
- # Set defaults for each cookie entry
339
- set_default(cookies[key], "name", None, str)
340
- set_default(cookies[key], "checked", False, bool)
341
-
342
- # Set defaults for extra social settings
343
- social = extra.setdefault("social", [])
344
- for entry in social:
345
- set_default(entry, "icon", None, str)
346
- set_default(entry, "name", None, str)
347
- set_default(entry, "link", None, str)
348
-
349
- # Set defaults for extra alternate settings
350
- alternate = extra.setdefault("alternate", [])
351
- for entry in alternate:
352
- set_default(entry, "name", None, str)
353
- set_default(entry, "link", None, str)
354
- set_default(entry, "lang", None, str)
355
-
356
- # Set defaults for extra version settings
357
- version = extra.setdefault("version", None)
358
- if version:
359
- set_default(version, "provider", None, str)
360
- set_default(version, "default", None, str)
361
- set_default(version, "alias", False, bool)
362
305
 
363
306
  # Ensure all non-existent values are all empty strings (for now)
364
307
  config["extra"] = _convert_extra(extra)
@@ -435,19 +378,38 @@ def _apply_defaults(config: dict, path: str) -> dict:
435
378
  tabbed = config["mdx_configs"].get("pymdownx.tabbed", {})
436
379
  if isinstance(tabbed.get("slugify"), dict):
437
380
  object = tabbed["slugify"].get("object", "pymdownx.slugs.slugify")
438
- tabbed["slugify"] = _resolve(object)(**tabbed["slugify"].get("kwds"))
381
+ tabbed["slugify"] = _resolve(object)(
382
+ **tabbed["slugify"].get("kwds", {})
383
+ )
439
384
 
440
385
  # Table of contents extension configuration - resolve slugification function
441
386
  toc = config["mdx_configs"]["toc"]
442
387
  if isinstance(toc.get("slugify"), dict):
443
388
  object = toc["slugify"].get("object", "pymdownx.slugs.slugify")
444
- toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds"))
389
+ toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds", {}))
445
390
 
446
391
  # Superfences extension configuration - resolve format function
447
392
  superfences = config["mdx_configs"].get("pymdownx.superfences", {})
448
393
  for fence in superfences.get("custom_fences", []):
449
394
  if isinstance(fence.get("format"), str):
450
395
  fence["format"] = _resolve(fence.get("format"))
396
+ elif isinstance(fence.get("format"), dict):
397
+ object = fence["format"].get(
398
+ "object", "pymdownx.superfences.fence_code_format"
399
+ )
400
+ fence["format"] = _resolve(object)(
401
+ **fence["format"].get("kwds", {})
402
+ )
403
+ if isinstance(fence.get("validator"), str):
404
+ fence["validator"] = _resolve(fence.get("validator"))
405
+ elif isinstance(fence.get("validator"), dict):
406
+ object = fence["validator"].get("object")
407
+ callable_object = (
408
+ _resolve(object) if object else lambda *args, **kwargs: True
409
+ )
410
+ fence["validator"] = callable_object(
411
+ **fence["validator"].get("kwds", {})
412
+ )
451
413
 
452
414
  # Ensure the table of contents title is initialized, as it's used inside
453
415
  # the template, and the table of contents extension is always defined
@@ -456,14 +418,31 @@ def _apply_defaults(config: dict, path: str) -> dict:
456
418
 
457
419
  # Convert plugins configuration
458
420
  config["plugins"] = _convert_plugins(config.get("plugins", []), config)
421
+
422
+ # Set up mkdocstrings, which touches plugins and Markdown extensions
423
+ if "mkdocstrings" in config["plugins"]:
424
+ mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"]
425
+ if mkdocstrings_config.pop("enabled", True):
426
+ mkdocstrings_config["markdown_extensions"] = [
427
+ {ext: mdx_configs.get(ext, {})} for ext in markdown_extensions
428
+ ]
429
+ config["markdown_extensions"].append("mkdocstrings")
430
+ config["mdx_configs"]["mkdocstrings"] = mkdocstrings_config
431
+
432
+ # List all source files for mkdocstrings
433
+ config["source_files"] = _list_sources(config, path)
434
+
435
+ # Hash all templates, so we rebuild if something changes
436
+ config["template_hash"] = _hash(_list_templates(config))
459
437
  return config
460
438
 
461
439
 
462
440
  def set_default(
463
- entry: dict, key: str, default: Any, data_type: type = None
464
- ) -> any:
465
- """
466
- Set a key to a default value if it isn't set, and optionally cast it to the specified data type.
441
+ entry: dict, key: str, default: Any, data_type: type | None = None
442
+ ) -> Any:
443
+ """Set a key to a default value if it isn't set.
444
+
445
+ Optionally cast it to the specified data type.
467
446
  """
468
447
  if key in entry and entry[key] is None:
469
448
  del entry[key]
@@ -476,24 +455,70 @@ def set_default(
476
455
  try:
477
456
  entry[key] = data_type(entry[key])
478
457
  except (ValueError, TypeError) as e:
479
- raise ValueError(f"Failed to cast key '{key}' to {data_type}: {e}")
458
+ raise ValueError(
459
+ f"Failed to cast key '{key}' to {data_type}: {e}"
460
+ ) from e
480
461
 
481
462
  # Return the resulting value
482
463
  return entry[key]
483
464
 
484
465
 
485
- def _hash(data: any) -> int:
486
- """
487
- Compute a hash for the given data.
488
- """
489
- hash = hashlib.sha1(pickle.dumps(data))
466
+ def _hash(data: Any) -> int:
467
+ """Compute a hash for the given data."""
468
+ hash = hashlib.sha1(pickle.dumps(data)) # noqa: S324
490
469
  return int(hash.hexdigest(), 16) % (2**64)
491
470
 
492
471
 
472
+ def _list_sources(config: dict, config_file: str) -> list[tuple[str, int]]:
473
+ """List all absolute links to source files for mkdocstrings."""
474
+ python_paths = (
475
+ config["plugins"]
476
+ .get("mkdocstrings", {})
477
+ .get("config", {})
478
+ .get("handlers", {})
479
+ .get("python", {})
480
+ .get("paths", ())
481
+ )
482
+ roots_with_hash = []
483
+ for python_path in python_paths:
484
+ path = Path(config_file).parent.joinpath(python_path).resolve()
485
+
486
+ # Collect all files under this root with modification times to detect
487
+ # changes. We'll replace this with proper dependency tracking later.
488
+ files = [(path, int(os.path.getmtime(path)))]
489
+ if path.is_dir():
490
+ for subpath in path.rglob("*"):
491
+ files.extend([(subpath, int(os.path.getmtime(subpath)))])
492
+ roots_with_hash.append((str(path), _hash(files)))
493
+
494
+ return sorted(roots_with_hash)
495
+
496
+
497
+ def _list_templates(config: dict) -> list[tuple[str, int]]:
498
+ """List all template files in the theme directories."""
499
+ dirs = [get_theme_dir()]
500
+ if "custom_dir" in config["theme"]:
501
+ custom_dir = get_custom_theme_dir(config)
502
+ if custom_dir is not None:
503
+ dirs.append(custom_dir)
504
+
505
+ # Collect file paths and their mtimes
506
+ files_with_mtime = []
507
+ for directory in dirs:
508
+ for path, _, files in os.walk(directory):
509
+ if ".icons" in path:
510
+ continue
511
+ for file in files:
512
+ file_path = os.path.join(path, file)
513
+ mtime = int(os.path.getmtime(file_path))
514
+ files_with_mtime.append((file_path, mtime))
515
+
516
+ # Sort by file path for deterministic order
517
+ return sorted(files_with_mtime)
518
+
519
+
493
520
  def _convert_extra(data: dict | list) -> dict | list:
494
- """
495
- Recursively convert all None values in a dictionary or list to empty strings.
496
- """
521
+ """Recursively convert None values in a dictionary/list to empty strings."""
497
522
  if isinstance(data, dict):
498
523
  # Process each key-value pair in the dictionary
499
524
  return {
@@ -502,7 +527,7 @@ def _convert_extra(data: dict | list) -> dict | list:
502
527
  else ("" if value is None else value)
503
528
  for key, value in data.items()
504
529
  }
505
- elif isinstance(data, list):
530
+ if isinstance(data, list):
506
531
  # Process each item in the list
507
532
  return [
508
533
  _convert_extra(item)
@@ -510,14 +535,11 @@ def _convert_extra(data: dict | list) -> dict | list:
510
535
  else ("" if item is None else item)
511
536
  for item in data
512
537
  ]
513
- else:
514
- return data
538
+ return data
515
539
 
516
540
 
517
- def _resolve(symbol: str):
518
- """
519
- Resolve a symbol to its corresponding Python object.
520
- """
541
+ def _resolve(symbol: str) -> Any:
542
+ """Resolve a symbol to its corresponding Python object."""
521
543
  module_path, func_name = symbol.rsplit(".", 1)
522
544
  module = importlib.import_module(module_path)
523
545
  return getattr(module, func_name)
@@ -526,17 +548,15 @@ def _resolve(symbol: str):
526
548
  # -----------------------------------------------------------------------------
527
549
 
528
550
 
529
- def _convert_nav(nav: dict) -> dict:
530
- """
531
- Convert MkDocs navigation
532
- """
551
+ def _convert_nav(nav: list) -> list:
552
+ """Convert MkDocs navigation."""
533
553
  return [_convert_nav_item(entry) for entry in nav]
534
554
 
535
555
 
536
- def _convert_nav_item(item: str | dict | list) -> dict:
537
- """
538
- Convert MkDocs shorthand navigation structure into something more manageable
539
- as we need to annotate each item with a title, URL, icon, and children.
556
+ def _convert_nav_item(item: str | dict | list) -> dict | list:
557
+ """Convert MkDocs shorthand navigation structure into something manageable.
558
+
559
+ We need to annotate each item with a title, URL, icon, and children.
540
560
  """
541
561
  if isinstance(item, str):
542
562
  return {
@@ -550,19 +570,19 @@ def _convert_nav_item(item: str | dict | list) -> dict:
550
570
  }
551
571
 
552
572
  # Handle Title: URL
553
- elif isinstance(item, dict):
573
+ if isinstance(item, dict):
554
574
  for title, value in item.items():
555
575
  if isinstance(value, str):
556
576
  return {
557
577
  "title": str(title),
558
- "url": value,
578
+ "url": value.strip(),
559
579
  "canonical_url": None,
560
580
  "meta": None,
561
581
  "children": [],
562
- "is_index": _is_index(value),
582
+ "is_index": _is_index(value.strip()),
563
583
  "active": False,
564
584
  }
565
- elif isinstance(value, list):
585
+ if isinstance(value, list):
566
586
  return {
567
587
  "title": str(title),
568
588
  "url": None,
@@ -572,28 +592,25 @@ def _convert_nav_item(item: str | dict | list) -> dict:
572
592
  "is_index": False,
573
593
  "active": False,
574
594
  }
595
+ raise TypeError(f"Unknown nav item value type: {type(value)}")
575
596
 
576
597
  # Handle a list of items
577
598
  elif isinstance(item, list):
578
599
  return [_convert_nav_item(child) for child in item]
579
- else:
580
- raise ValueError(f"Unknown nav item type: {type(item)}")
600
+
601
+ raise TypeError(f"Unknown nav item type: {type(item)}")
581
602
 
582
603
 
583
604
  def _is_index(path: str) -> bool:
584
- """
585
- Returns, whether the given path points to a section index.
586
- """
587
- return path.endswith(("index.md", "README.md"))
605
+ """Returns, whether the given path points to a section index."""
606
+ return os.path.basename(path) in ("index.md", "README.md")
588
607
 
589
608
 
590
609
  # -----------------------------------------------------------------------------
591
610
 
592
611
 
593
- def _convert_extra_javascript(value: list[any]) -> list:
594
- """
595
- Ensure extra_javascript uses a structured format.
596
- """
612
+ def _convert_extra_javascript(value: list) -> list:
613
+ """Ensure extra_javascript uses a structured format."""
597
614
  for i, item in enumerate(value):
598
615
  if isinstance(item, str):
599
616
  value[i] = {
@@ -608,9 +625,7 @@ def _convert_extra_javascript(value: list[any]) -> list:
608
625
  item.setdefault("async", False)
609
626
  item.setdefault("defer", False)
610
627
  else:
611
- raise ValueError(
612
- f"Unknown extra_javascript item type: {type(item)}"
613
- )
628
+ raise TypeError(f"Unknown extra_javascript item type: {type(item)}")
614
629
 
615
630
  # Return resulting value
616
631
  return value
@@ -619,12 +634,10 @@ def _convert_extra_javascript(value: list[any]) -> list:
619
634
  # -----------------------------------------------------------------------------
620
635
 
621
636
 
622
- def _convert_markdown_extensions(value: any):
623
- """
624
- Convert Markdown extensions configuration to what Python Markdown expects.
625
- """
637
+ def _convert_markdown_extensions(value: Any) -> tuple[list[str], dict]:
638
+ """Convert Markdown extensions to what Python Markdown expects."""
626
639
  markdown_extensions = ["toc", "tables"]
627
- mdx_configs = {"toc": {}, "tables": {}}
640
+ mdx_configs: dict[str, dict[str, Any]] = {"toc": {}, "tables": {}}
628
641
 
629
642
  # In case of Python Markdown Extensions, we allow to omit the necessary
630
643
  # quotes around the extension names, so we need to hoist the extensions
@@ -632,14 +645,24 @@ def _convert_markdown_extensions(value: any):
632
645
  # actually parse the configuration.
633
646
  if "pymdownx" in value:
634
647
  pymdownx = value.pop("pymdownx")
635
- for ext, config in pymdownx.items():
648
+ for ext, conf in pymdownx.items():
636
649
  # Special case for blocks extension, which has another level of
637
650
  # nesting. This is the only extension that requires this.
638
651
  if ext == "blocks":
639
- for block, config in config.items():
652
+ for block, config in conf.items():
640
653
  value[f"pymdownx.{ext}.{block}"] = config
641
654
  else:
642
- value[f"pymdownx.{ext}"] = config
655
+ value[f"pymdownx.{ext}"] = conf
656
+
657
+ # Same as for Python Markdown extensions, see above
658
+ if "zensical" in value:
659
+ zensical = value.pop("zensical")
660
+ for ext, conf in zensical.items():
661
+ if ext == "extensions":
662
+ for key, config in conf.items():
663
+ value[f"zensical.{ext}.{key}"] = config
664
+ else:
665
+ value[f"zensical.{ext}"] = conf
643
666
 
644
667
  # Extensions can be defined as a dict
645
668
  if isinstance(value, dict):
@@ -664,16 +687,13 @@ def _convert_markdown_extensions(value: any):
664
687
  # ----------------------------------------------------------------------------
665
688
 
666
689
 
667
- def _convert_plugins(value: any, config: dict) -> list:
668
- """
669
- Convert plugins configuration to something we can work with.
670
- """
690
+ def _convert_plugins(value: Any, config: dict) -> dict:
691
+ """Convert plugins configuration to something we can work with."""
671
692
  plugins = {}
672
693
 
673
694
  # Plugins can be defined as a dict
674
695
  if isinstance(value, dict):
675
- for name, data in value.items():
676
- plugins[name] = data
696
+ plugins.update(value)
677
697
 
678
698
  # Plugins can also be defined as a list
679
699
  else:
@@ -730,8 +750,7 @@ def _convert_plugins(value: any, config: dict) -> list:
730
750
  def _yaml_load(
731
751
  source: IO, loader: type[BaseLoader] | None = None
732
752
  ) -> dict[str, Any]:
733
- """
734
- Load configuration file and resolve environment variables and parent files.
753
+ """Load configuration file, resolve environment variables and parent files.
735
754
 
736
755
  Note that INHERIT is only a bandaid that was introduced to allow for some
737
756
  degree of modularity, but with serious shortcomings. Zensical will use a
@@ -746,12 +765,12 @@ def _yaml_load(
746
765
  source.read()
747
766
  .replace("material.extensions", "zensical.extensions")
748
767
  .replace("materialx", "zensical.extensions"),
749
- Loader=Loader,
768
+ Loader=Loader, # noqa: S506
750
769
  )
751
770
  except YAMLError as e:
752
771
  raise ConfigurationError(
753
772
  f"Encountered an error parsing the configuration file: {e}"
754
- )
773
+ ) from e
755
774
  if config is None:
756
775
  return {}
757
776
 
@@ -763,9 +782,10 @@ def _yaml_load(
763
782
  )
764
783
  if not os.path.exists(abspath):
765
784
  raise ConfigurationError(
766
- f"Inherited config file '{relpath}' doesn't exist at '{abspath}'."
785
+ f"Inherited config file '{relpath}' "
786
+ f"doesn't exist at '{abspath}'."
767
787
  )
768
- with open(abspath, "r") as fd:
788
+ with open(abspath, encoding="utf-8") as fd:
769
789
  parent = _yaml_load(fd, loader)
770
790
  config = always_merger.merge(parent, config)
771
791
 
@@ -773,9 +793,11 @@ def _yaml_load(
773
793
  return config
774
794
 
775
795
 
776
- def _construct_env_tag(loader: yaml.Loader, node: yaml.Node):
777
- """
778
- Assign value of ENV variable referenced at node.
796
+ def _construct_env_tag(
797
+ loader: yaml.Loader,
798
+ node: yaml.ScalarNode | yaml.SequenceNode | yaml.MappingNode,
799
+ ) -> Any:
800
+ """Assign value of ENV variable referenced at node.
779
801
 
780
802
  MkDocs supports the use of !ENV to reference environment variables in YAML
781
803
  configuration files. We won't likely support this in Zensical, but for now
@@ -804,7 +826,7 @@ def _construct_env_tag(loader: yaml.Loader, node: yaml.Node):
804
826
  else:
805
827
  raise ConstructorError(
806
828
  context=f"expected a scalar or sequence node, but found {node.id}",
807
- start_mark=node.start_mark,
829
+ context_mark=node.start_mark,
808
830
  )
809
831
 
810
832
  # Resolve environment variable