unaltraweb 0.3.0

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 (444) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/Makefile +412 -0
  4. data/README.md +226 -0
  5. data/_config.yml +842 -0
  6. data/_data/i18n/ca.yml +278 -0
  7. data/_data/i18n/en.yml +278 -0
  8. data/_data/i18n/es.yml +278 -0
  9. data/_includes/audio.liquid +30 -0
  10. data/_includes/bib_search.liquid +5 -0
  11. data/_includes/blog-list.liquid +66 -0
  12. data/_includes/book-citation.liquid +33 -0
  13. data/_includes/citation.liquid +26 -0
  14. data/_includes/custom_head.liquid +3 -0
  15. data/_includes/custom_scripts.liquid +5 -0
  16. data/_includes/cv/list.liquid +5 -0
  17. data/_includes/cv/list_groups.liquid +51 -0
  18. data/_includes/cv/map.liquid +29 -0
  19. data/_includes/cv/nested_list.liquid +16 -0
  20. data/_includes/cv/time_table.liquid +118 -0
  21. data/_includes/cv-download-card.liquid +26 -0
  22. data/_includes/developer-mode-switcher.liquid +24 -0
  23. data/_includes/disqus.liquid +13 -0
  24. data/_includes/distill_scripts.liquid +298 -0
  25. data/_includes/documentation-download-detail.liquid +76 -0
  26. data/_includes/documentation-downloads.liquid +71 -0
  27. data/_includes/documentation-profile-attributes.liquid +4 -0
  28. data/_includes/documentation-section-toc.liquid +9 -0
  29. data/_includes/documentation-sidebar.liquid +112 -0
  30. data/_includes/figure.liquid +87 -0
  31. data/_includes/footer.liquid +66 -0
  32. data/_includes/giscus.liquid +25 -0
  33. data/_includes/head.liquid +220 -0
  34. data/_includes/header.liquid +544 -0
  35. data/_includes/latest_posts.liquid +61 -0
  36. data/_includes/localized-date.liquid +5 -0
  37. data/_includes/manual-bibliography.liquid +2 -0
  38. data/_includes/manual-featured-readings.liquid +12 -0
  39. data/_includes/manual-other-readings.liquid +12 -0
  40. data/_includes/manual-page-toc.liquid +11 -0
  41. data/_includes/manual-sidebar.liquid +58 -0
  42. data/_includes/manual-teachers.liquid +74 -0
  43. data/_includes/metadata.liquid +278 -0
  44. data/_includes/news.liquid +48 -0
  45. data/_includes/newsletter.liquid +68 -0
  46. data/_includes/page-hero.liquid +96 -0
  47. data/_includes/page-link.liquid +8 -0
  48. data/_includes/pagination.liquid +22 -0
  49. data/_includes/post-reading-time.liquid +29 -0
  50. data/_includes/previous-next.liquid +37 -0
  51. data/_includes/profile-card.liquid +49 -0
  52. data/_includes/profile-feature-matrix.liquid +75 -0
  53. data/_includes/profile-highlights.liquid +122 -0
  54. data/_includes/project-grid.liquid +26 -0
  55. data/_includes/project-resource-link.liquid +45 -0
  56. data/_includes/project-resources.liquid +21 -0
  57. data/_includes/projects.liquid +38 -0
  58. data/_includes/projects_horizontal.liquid +37 -0
  59. data/_includes/publications-metrics-summary.liquid +69 -0
  60. data/_includes/publications-summary.liquid +127 -0
  61. data/_includes/reading-biblio-controls.liquid +65 -0
  62. data/_includes/reading-cover-card.liquid +93 -0
  63. data/_includes/reading-rating.liquid +11 -0
  64. data/_includes/recommended-readings.liquid +30 -0
  65. data/_includes/related_posts.liquid +39 -0
  66. data/_includes/repository/repo.liquid +48 -0
  67. data/_includes/repository/repo_trophies.liquid +44 -0
  68. data/_includes/repository/repo_user.liquid +35 -0
  69. data/_includes/resume/awards.liquid +19 -0
  70. data/_includes/resume/basics.liquid +28 -0
  71. data/_includes/resume/certificates.liquid +35 -0
  72. data/_includes/resume/education.liquid +54 -0
  73. data/_includes/resume/interests.liquid +34 -0
  74. data/_includes/resume/languages.liquid +31 -0
  75. data/_includes/resume/projects.liquid +32 -0
  76. data/_includes/resume/publications.liquid +28 -0
  77. data/_includes/resume/references.liquid +31 -0
  78. data/_includes/resume/skills.liquid +33 -0
  79. data/_includes/resume/volunteer.liquid +52 -0
  80. data/_includes/resume/work.liquid +52 -0
  81. data/_includes/scripts.liquid +364 -0
  82. data/_includes/search-data.js.liquid +341 -0
  83. data/_includes/selected_papers.liquid +3 -0
  84. data/_includes/t.liquid +31 -0
  85. data/_includes/team-grid.liquid +172 -0
  86. data/_includes/video.liquid +97 -0
  87. data/_layouts/about.liquid +93 -0
  88. data/_layouts/archive.liquid +45 -0
  89. data/_layouts/bib.liquid +677 -0
  90. data/_layouts/book-review.liquid +225 -0
  91. data/_layouts/book-shelf.liquid +66 -0
  92. data/_layouts/cv.liquid +127 -0
  93. data/_layouts/default.liquid +70 -0
  94. data/_layouts/distill.liquid +132 -0
  95. data/_layouts/documentation-download-detail.liquid +5 -0
  96. data/_layouts/documentation-home.liquid +100 -0
  97. data/_layouts/documentation-page.liquid +45 -0
  98. data/_layouts/manual-bib.liquid +73 -0
  99. data/_layouts/manual-chapter.liquid +99 -0
  100. data/_layouts/manual-featured-bib.liquid +100 -0
  101. data/_layouts/manual-home.liquid +120 -0
  102. data/_layouts/none.liquid +1 -0
  103. data/_layouts/outputs.liquid +29 -0
  104. data/_layouts/page.liquid +52 -0
  105. data/_layouts/post.liquid +95 -0
  106. data/_layouts/profile.liquid +36 -0
  107. data/_layouts/profiles.liquid +36 -0
  108. data/_layouts/project.liquid +7 -0
  109. data/_layouts/repositories.liquid +58 -0
  110. data/_layouts/theses.liquid +126 -0
  111. data/_plugins/bibliography_profiles.rb +164 -0
  112. data/_plugins/callouts.rb +104 -0
  113. data/_plugins/code_blocks.rb +277 -0
  114. data/_plugins/computation_figure_images.rb +266 -0
  115. data/_plugins/computation_sources.rb +42 -0
  116. data/_plugins/content_search_index.rb +210 -0
  117. data/_plugins/details.rb +23 -0
  118. data/_plugins/external-posts.rb +124 -0
  119. data/_plugins/figure_captions.rb +800 -0
  120. data/_plugins/file-exists.rb +22 -0
  121. data/_plugins/google-scholar-citations.rb +86 -0
  122. data/_plugins/hide-custom-bibtex.rb +18 -0
  123. data/_plugins/inspirehep-citations.rb +57 -0
  124. data/_plugins/localized_visual_sources.rb +145 -0
  125. data/_plugins/manual_release_metadata.rb +47 -0
  126. data/_plugins/mermaid_mmd_images.rb +233 -0
  127. data/_plugins/profile-pages.rb +28 -0
  128. data/_plugins/remove-accents.rb +32 -0
  129. data/_plugins/reproducible_build_time.rb +55 -0
  130. data/_plugins/search-data.rb +83 -0
  131. data/_plugins/theme-cache-bust.rb +56 -0
  132. data/_plugins/vega_visual_images.rb +290 -0
  133. data/_plugins/web_capture_images.rb +154 -0
  134. data/_sass/_base.scss +2133 -0
  135. data/_sass/_blog.scss +67 -0
  136. data/_sass/_callouts.scss +100 -0
  137. data/_sass/_custom.scss +437 -0
  138. data/_sass/_cv.scss +274 -0
  139. data/_sass/_developer.scss +97 -0
  140. data/_sass/_distill.scss +185 -0
  141. data/_sass/_documentation.scss +2220 -0
  142. data/_sass/_layout.scss +218 -0
  143. data/_sass/_manual.scss +2691 -0
  144. data/_sass/_profile.scss +323 -0
  145. data/_sass/_projects.scss +163 -0
  146. data/_sass/_reading.scss +465 -0
  147. data/_sass/_site-custom.scss +3 -0
  148. data/_sass/_tabs.scss +48 -0
  149. data/_sass/_team.scss +230 -0
  150. data/_sass/_themes.scss +254 -0
  151. data/_sass/_typograms.scss +132 -0
  152. data/_sass/_variables.scss +52 -0
  153. data/_sass/font-awesome/_animated.scss +152 -0
  154. data/_sass/font-awesome/_bordered-pulled.scss +20 -0
  155. data/_sass/font-awesome/_core.scss +49 -0
  156. data/_sass/font-awesome/_fixed-width.scss +7 -0
  157. data/_sass/font-awesome/_functions.scss +57 -0
  158. data/_sass/font-awesome/_icons.scss +13 -0
  159. data/_sass/font-awesome/_list.scss +18 -0
  160. data/_sass/font-awesome/_mixins.scss +65 -0
  161. data/_sass/font-awesome/_rotated-flipped.scss +31 -0
  162. data/_sass/font-awesome/_screen-reader.scss +14 -0
  163. data/_sass/font-awesome/_shims.scss +1578 -0
  164. data/_sass/font-awesome/_sizing.scss +16 -0
  165. data/_sass/font-awesome/_stacked.scss +32 -0
  166. data/_sass/font-awesome/_variables.scss +5044 -0
  167. data/_sass/font-awesome/brands.scss +30 -0
  168. data/_sass/font-awesome/fontawesome.scss +21 -0
  169. data/_sass/font-awesome/regular.scss +27 -0
  170. data/_sass/font-awesome/solid.scss +27 -0
  171. data/_sass/font-awesome/v4-shims.scss +11 -0
  172. data/_sass/tabler-icons/tabler-icons-filled.scss +1646 -0
  173. data/_sass/tabler-icons/tabler-icons-outline.scss +9859 -0
  174. data/_sass/tabler-icons/tabler-icons.scss +11463 -0
  175. data/_scripts/cronitor-analytics-setup.js +9 -0
  176. data/_scripts/giscus-setup.js +48 -0
  177. data/_scripts/google-analytics-setup.js +9 -0
  178. data/_scripts/open-panel-analytics-setup.js +14 -0
  179. data/_scripts/photoswipe-setup.js +11 -0
  180. data/assets/audio/epicaly-short-113909.mp3 +0 -0
  181. data/assets/bibliography/2018-12-22-distill.bib +7 -0
  182. data/assets/css/academicons.min.css +1 -0
  183. data/assets/css/bootstrap-toc.min.css +4 -0
  184. data/assets/css/bootstrap.min.css +7 -0
  185. data/assets/css/bootstrap.min.css.map +1 -0
  186. data/assets/css/jekyll-pygments-themes-github.css +209 -0
  187. data/assets/css/jekyll-pygments-themes-native.css +232 -0
  188. data/assets/css/jupyter-grade3.css +3758 -0
  189. data/assets/css/jupyter-monokai.css +3758 -0
  190. data/assets/css/jupyter.css +383 -0
  191. data/assets/css/main.scss +36 -0
  192. data/assets/css/mdb.min.css +48 -0
  193. data/assets/css/mdb.min.css.map +1 -0
  194. data/assets/css/scholar-icons.css +386 -0
  195. data/assets/fonts/academicons.ttf +0 -0
  196. data/assets/fonts/academicons.woff +0 -0
  197. data/assets/fonts/scholar-icons.ttf +0 -0
  198. data/assets/fonts/scholar-icons.woff +0 -0
  199. data/assets/fonts/tabler-icons-filled.ttf +0 -0
  200. data/assets/fonts/tabler-icons-filled.woff +0 -0
  201. data/assets/fonts/tabler-icons-filled.woff2 +0 -0
  202. data/assets/fonts/tabler-icons-outline.ttf +0 -0
  203. data/assets/fonts/tabler-icons-outline.woff +0 -0
  204. data/assets/fonts/tabler-icons-outline.woff2 +0 -0
  205. data/assets/fonts/tabler-icons.ttf +0 -0
  206. data/assets/fonts/tabler-icons.woff +0 -0
  207. data/assets/fonts/tabler-icons.woff2 +0 -0
  208. data/assets/html/relativity.html +15 -0
  209. data/assets/img/1.jpg +0 -0
  210. data/assets/img/10.jpg +0 -0
  211. data/assets/img/11.jpg +0 -0
  212. data/assets/img/12.jpg +0 -0
  213. data/assets/img/2.jpg +0 -0
  214. data/assets/img/3.jpg +0 -0
  215. data/assets/img/4.jpg +0 -0
  216. data/assets/img/5.jpg +0 -0
  217. data/assets/img/6.jpg +0 -0
  218. data/assets/img/7.jpg +0 -0
  219. data/assets/img/8.jpg +0 -0
  220. data/assets/img/9.jpg +0 -0
  221. data/assets/img/book_covers/the_godfather.jpg +0 -0
  222. data/assets/img/brand/dosquartsdedocs-logo-cafe.svg +31 -0
  223. data/assets/img/brand/dosquartsdedocs-logo-footer-cafe.svg +31 -0
  224. data/assets/img/brand/dosquartsdedocs-logo-white.svg +31 -0
  225. data/assets/img/brand/dosquartsdedocs-logo.svg +31 -0
  226. data/assets/img/prof_pic.jpg +0 -0
  227. data/assets/img/prof_pic_color.png +0 -0
  228. data/assets/img/profile-placeholder.svg +8 -0
  229. data/assets/img/publication_preview/brownian-motion.gif +0 -0
  230. data/assets/img/publication_preview/goodchild1992-gis-cover.jpg +0 -0
  231. data/assets/img/publication_preview/gutierrez2020-jtrg-preview.jpg +0 -0
  232. data/assets/img/publication_preview/harvey1989-postmodernity-cover.jpg +0 -0
  233. data/assets/img/publication_preview/tobler1970-economic-geography-cover.jpg +0 -0
  234. data/assets/img/publication_preview/tuan1977-space-place-cover.jpg +0 -0
  235. data/assets/img/publication_preview/wave-mechanics.gif +0 -0
  236. data/assets/img/publication_preview/zaragozi2025-rooftop-pv-preview.jpg +0 -0
  237. data/assets/img/rhino.png +0 -0
  238. data/assets/img/team/aaron-gutierrez.png +0 -0
  239. data/assets/img/team/alba-font.png +0 -0
  240. data/assets/img/team/alba-viana.png +0 -0
  241. data/assets/img/team/alejandro-gonzalez.png +0 -0
  242. data/assets/img/team/ana-pastor.png +0 -0
  243. data/assets/img/team/antoni-domenech.png +0 -0
  244. data/assets/img/team/benito-zaragozi.png +0 -0
  245. data/assets/img/team/cinta-sanz.png +0 -0
  246. data/assets/img/team/daniel-miravet.png +0 -0
  247. data/assets/img/team/eugenia-altamirano.png +0 -0
  248. data/assets/img/team/fiammetta-brandajs.png +0 -0
  249. data/assets/img/team/francesc-gonzalez.png +0 -0
  250. data/assets/img/team/isabel-paulino.png +0 -0
  251. data/assets/img/team/joan-borras.png +0 -0
  252. data/assets/img/team/julie-wilson.png +0 -0
  253. data/assets/img/team/liliana-sole.png +0 -0
  254. data/assets/img/team/maria-casado.png +0 -0
  255. data/assets/img/team/maria-medina.png +0 -0
  256. data/assets/img/team/marta-nello.png +0 -0
  257. data/assets/img/team/mogrify +1 -0
  258. data/assets/img/team/oscar-saladie.png +0 -0
  259. data/assets/img/team/paolo-russo.png +0 -0
  260. data/assets/img/team/raquel-santos.png +0 -0
  261. data/assets/img/team/salvador-anton.png +0 -0
  262. data/assets/img/team/sunsi-huertas.png +0 -0
  263. data/assets/img/team/user-placeholder.png +0 -0
  264. data/assets/img/team/wilbert-den-hoed.png +0 -0
  265. data/assets/img/team/xavier-delclos.png +0 -0
  266. data/assets/img/template_error.png +0 -0
  267. data/assets/js/bibsearch.js +70 -0
  268. data/assets/js/bootstrap-toc.min.js +5 -0
  269. data/assets/js/bootstrap.bundle.min.js +7 -0
  270. data/assets/js/bootstrap.bundle.min.js.map +1 -0
  271. data/assets/js/callouts.js +115 -0
  272. data/assets/js/chartjs-setup.js +14 -0
  273. data/assets/js/common.js +117 -0
  274. data/assets/js/content-search-match.js +159 -0
  275. data/assets/js/content-search.js +441 -0
  276. data/assets/js/copy_code.js +60 -0
  277. data/assets/js/custom.js +6 -0
  278. data/assets/js/developer-mode.js +103 -0
  279. data/assets/js/diff2html-setup.js +20 -0
  280. data/assets/js/distillpub/overrides.js +27 -0
  281. data/assets/js/distillpub/template.v2.js +9616 -0
  282. data/assets/js/distillpub/template.v2.js.map +1 -0
  283. data/assets/js/distillpub/transforms.v2.js +14557 -0
  284. data/assets/js/distillpub/transforms.v2.js.map +1 -0
  285. data/assets/js/documentation.js +435 -0
  286. data/assets/js/echarts-setup.js +29 -0
  287. data/assets/js/highlight-search-term.js +110 -0
  288. data/assets/js/jupyter_new_tab.js +18 -0
  289. data/assets/js/leaflet-setup.js +22 -0
  290. data/assets/js/manual.js +308 -0
  291. data/assets/js/masonry.js +12 -0
  292. data/assets/js/mathjax-setup.js +46 -0
  293. data/assets/js/mermaid-setup.js +37 -0
  294. data/assets/js/newsletter.js +105 -0
  295. data/assets/js/no_defer.js +24 -0
  296. data/assets/js/plotly-setup.js +52 -0
  297. data/assets/js/profile-nav.js +92 -0
  298. data/assets/js/progress-bar.js +73 -0
  299. data/assets/js/pseudocode-setup.js +18 -0
  300. data/assets/js/search/base-styles.min.js +286 -0
  301. data/assets/js/search/command-score.min.js +7 -0
  302. data/assets/js/search/hotkeys-js/hotkeys.esm.min.js +18 -0
  303. data/assets/js/search/lit/decorators.min.js +7 -0
  304. data/assets/js/search/lit/directives/class-map.min.js +7 -0
  305. data/assets/js/search/lit/directives/join.min.js +7 -0
  306. data/assets/js/search/lit/directives/live.min.js +7 -0
  307. data/assets/js/search/lit/directives/ref.min.js +7 -0
  308. data/assets/js/search/lit/directives/repeat.min.js +7 -0
  309. data/assets/js/search/lit/directives/unsafe-html.min.js +7 -0
  310. data/assets/js/search/lit/index.min.js +7 -0
  311. data/assets/js/search/lit-element/lit-element.min.js +12 -0
  312. data/assets/js/search/lit-html/directives/class-map.min.js +22 -0
  313. data/assets/js/search/lit-html/directives/join.min.js +12 -0
  314. data/assets/js/search/lit-html/directives/live.min.js +28 -0
  315. data/assets/js/search/lit-html/directives/ref.min.js +32 -0
  316. data/assets/js/search/lit-html/directives/repeat.min.js +27 -0
  317. data/assets/js/search/lit-html/directives/unsafe-html.min.js +22 -0
  318. data/assets/js/search/lit-html/is-server.min.js +12 -0
  319. data/assets/js/search/lit-html/lit-html.min.js +12 -0
  320. data/assets/js/search/ninja-action.min.js +101 -0
  321. data/assets/js/search/ninja-footer.min.js +62 -0
  322. data/assets/js/search/ninja-header.min.js +78 -0
  323. data/assets/js/search/ninja-keys.min.js +39 -0
  324. data/assets/js/search/reactive-element/decorators/custom-element.min.js +12 -0
  325. data/assets/js/search/reactive-element/decorators/event-options.min.js +12 -0
  326. data/assets/js/search/reactive-element/decorators/property.min.js +22 -0
  327. data/assets/js/search/reactive-element/decorators/query-all.min.js +17 -0
  328. data/assets/js/search/reactive-element/decorators/query-assigned-elements.min.js +17 -0
  329. data/assets/js/search/reactive-element/decorators/query-assigned-nodes.min.js +17 -0
  330. data/assets/js/search/reactive-element/decorators/query-async.min.js +17 -0
  331. data/assets/js/search/reactive-element/decorators/query.min.js +17 -0
  332. data/assets/js/search/reactive-element/decorators/state.min.js +27 -0
  333. data/assets/js/search/reactive-element/reactive-element.min.js +17 -0
  334. data/assets/js/search-setup.js +17 -0
  335. data/assets/js/shortcut-key.js +11 -0
  336. data/assets/js/tabs.min.js +5 -0
  337. data/assets/js/theme.js +314 -0
  338. data/assets/js/tooltips-setup.js +3 -0
  339. data/assets/js/typograms.js +1341 -0
  340. data/assets/js/vanilla-back-to-top.min.js +1 -0
  341. data/assets/js/vega-setup.js +24 -0
  342. data/assets/js/venobox-setup.js +5 -0
  343. data/assets/js/wechat.js +14 -0
  344. data/assets/js/zoom.js +15 -0
  345. data/assets/json/resume.json +98 -0
  346. data/assets/json/table_data.json +128 -0
  347. data/assets/jupyter/blog.ipynb +48 -0
  348. data/assets/pdf/example_pdf.pdf +0 -0
  349. data/assets/plotly/demo.html +72 -0
  350. data/assets/video/pexels-engin-akyurt-6069112-960x540-30fps.mp4 +0 -0
  351. data/assets/webfonts/fa-brands-400.ttf +0 -0
  352. data/assets/webfonts/fa-brands-400.woff2 +0 -0
  353. data/assets/webfonts/fa-regular-400.ttf +0 -0
  354. data/assets/webfonts/fa-regular-400.woff2 +0 -0
  355. data/assets/webfonts/fa-solid-900.ttf +0 -0
  356. data/assets/webfonts/fa-solid-900.woff2 +0 -0
  357. data/assets/webfonts/fa-v4compatibility.ttf +0 -0
  358. data/assets/webfonts/fa-v4compatibility.woff2 +0 -0
  359. data/docs/Gemfile +5 -0
  360. data/docs/_config.yml +83 -0
  361. data/docs/_documentation/en/01-quick-start.md +115 -0
  362. data/docs/_documentation/en/02-tools.md +146 -0
  363. data/docs/_documentation/en/03-usage.md +107 -0
  364. data/docs/_documentation/en/04-jekyll-basics.md +51 -0
  365. data/docs/_documentation/en/05-pages-collections.md +68 -0
  366. data/docs/_documentation/en/06-github-web-editing.md +92 -0
  367. data/docs/_documentation/en/10-profiles.md +125 -0
  368. data/docs/_documentation/en/11-unaltreselfie.md +39 -0
  369. data/docs/_documentation/en/12-unaltreprojecte.md +38 -0
  370. data/docs/_documentation/en/13-unaltremanual.md +237 -0
  371. data/docs/_documentation/en/14-unaltredocs.md +92 -0
  372. data/docs/_documentation/en/15-profile-checklist.md +39 -0
  373. data/docs/_documentation/en/20-syntax.md +353 -0
  374. data/docs/_documentation/en/21-themes.md +103 -0
  375. data/docs/_documentation/en/22-feature-reference.md +56 -0
  376. data/docs/_documentation/en/23-front-matter.md +51 -0
  377. data/docs/_documentation/en/24-data-files.md +28 -0
  378. data/docs/_documentation/en/30-customization.md +448 -0
  379. data/docs/_documentation/en/31-template.md +55 -0
  380. data/docs/_documentation/en/32-development.md +108 -0
  381. data/docs/_documentation/en/33-reusable-workflows.md +136 -0
  382. data/docs/_documentation/en/40-distribution.md +164 -0
  383. data/docs/_documentation/en/41-bibliometrics.md +137 -0
  384. data/docs/_documentation/en/42-docker-image.md +185 -0
  385. data/docs/_documentation/en/50-faq.md +33 -0
  386. data/docs/_pages/ca/index.md +35 -0
  387. data/docs/_pages/en/index.md +35 -0
  388. data/docs/_pages/es/index.md +35 -0
  389. data/docs/agents/action-prompts/00-start-site-session.txt +10 -0
  390. data/docs/agents/action-prompts/05-create-new-web.txt +5 -0
  391. data/docs/agents/action-prompts/10-content-update.txt +9 -0
  392. data/docs/agents/action-prompts/15-edit-default-content.txt +8 -0
  393. data/docs/agents/action-prompts/20-manual-teaching-materials.txt +18 -0
  394. data/docs/agents/action-prompts/22-manual-style-audit.txt +14 -0
  395. data/docs/agents/action-prompts/23-manual-structure-audit.txt +17 -0
  396. data/docs/agents/action-prompts/25-translation-prepublish.txt +8 -0
  397. data/docs/agents/action-prompts/30-project-site-update.txt +7 -0
  398. data/docs/agents/action-prompts/40-documentation-update.txt +7 -0
  399. data/docs/agents/action-prompts/50-bibliography-entry.txt +7 -0
  400. data/docs/agents/action-prompts/60-bibliometrics-refresh.txt +7 -0
  401. data/docs/agents/action-prompts/70-build-and-review.txt +12 -0
  402. data/docs/agents/manual-authoring-components.md +255 -0
  403. data/docs/agents/mcp-contract.md +162 -0
  404. data/docs/agents/quarto-computation-adoption.md +102 -0
  405. data/docs/assets/img/unaltraweb-docs-hero.svg +36 -0
  406. data/lib/unaltraweb/version.rb +8 -0
  407. data/lib/unaltraweb.rb +31 -0
  408. data/requirements.txt +5 -0
  409. data/scripts/biblio/fetch_scimago_csv.sh +126 -0
  410. data/scripts/biblio/metrics_common.py +154 -0
  411. data/scripts/biblio/metrics_merge_meta.py +73 -0
  412. data/scripts/biblio/metrics_update.py +826 -0
  413. data/scripts/computations/python/Dockerfile +37 -0
  414. data/scripts/computations/r/Dockerfile +31 -0
  415. data/scripts/computations/render.py +1267 -0
  416. data/scripts/cv/render_pdf_preview.sh +36 -0
  417. data/scripts/deploy/publish_branch.sh +80 -0
  418. data/scripts/generate_logo_variants.py +47 -0
  419. data/scripts/manual/Dockerfile +31 -0
  420. data/scripts/manual/build_pdf.py +1780 -0
  421. data/scripts/manual/filters/bibliography.lua +164 -0
  422. data/scripts/manual/filters/code-blocks.lua +102 -0
  423. data/scripts/manual/filters/figure-captions.lua +35 -0
  424. data/scripts/manual/publish_release.sh +281 -0
  425. data/scripts/manual/templates/manual.tex +494 -0
  426. data/scripts/manual/verify_release_assets.py +650 -0
  427. data/scripts/test_gem_build.py +128 -0
  428. data/scripts/test_reproducible_jekyll_build.py +191 -0
  429. data/scripts/test_wheel_install.py +282 -0
  430. data/scripts/unaltraweb-docker-mount.sh +36 -0
  431. data/scripts/unaltraweb-mcp-bootstrap.sh +105 -0
  432. data/scripts/unaltraweb-mcp-cleanup.sh +73 -0
  433. data/scripts/unaltraweb-mcp-project-id.sh +34 -0
  434. data/scripts/validate_distribution.py +563 -0
  435. data/scripts/validate_workflows.py +1285 -0
  436. data/scripts/web_captures/Dockerfile +31 -0
  437. data/scripts/web_captures/capture.mjs +157 -0
  438. data/scripts/web_captures/package-lock.json +56 -0
  439. data/scripts/web_captures/package.json +9 -0
  440. data/scripts/web_captures/render.py +867 -0
  441. data/src/unaltraweb_mcp/component-contract.json +153 -0
  442. data/src/unaltraweb_mcp/component-contract.schema.json +87 -0
  443. data/src/unaltraweb_mcp/docker_mount.py +19 -0
  444. metadata +861 -0
@@ -0,0 +1,22 @@
1
+ module Jekyll
2
+ class FileExistsTag < Liquid::Tag
3
+ def initialize(tag_name, path, tokens)
4
+ super
5
+ @path = path
6
+ end
7
+
8
+ def render(context)
9
+ # Pipe parameter through Liquid to make additional replacements possible
10
+ url = Liquid::Template.parse(@path).render context
11
+
12
+ # Adds the site source, so that it also works with a custom one
13
+ site_source = context.registers[:site].config['source']
14
+ file_path = site_source + '/' + url
15
+
16
+ # Check if file exists (returns true or false)
17
+ "#{File.exist?(file_path.strip!)}"
18
+ end
19
+ end
20
+ end
21
+
22
+ Liquid::Template.register_tag('file_exists', Jekyll::FileExistsTag)
@@ -0,0 +1,86 @@
1
+ require "active_support/all"
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+
5
+ module Helpers
6
+ extend ActiveSupport::NumberHelper
7
+ end
8
+
9
+ module Jekyll
10
+ class GoogleScholarCitationsTag < Liquid::Tag
11
+ Citations = { }
12
+ CITED_BY_REGEX = /Cited by (\d+[,\d]*)/
13
+
14
+ def initialize(tag_name, params, tokens)
15
+ super
16
+ splitted = params.split(" ").map(&:strip)
17
+ @scholar_id = splitted[0]
18
+ @article_id = splitted[1]
19
+
20
+ if @scholar_id.nil? || @scholar_id.empty?
21
+ puts "Invalid scholar_id provided"
22
+ end
23
+
24
+ if @article_id.nil? || @article_id.empty?
25
+ puts "Invalid article_id provided"
26
+ end
27
+ end
28
+
29
+ def render(context)
30
+ article_id = context[@article_id.strip]
31
+ scholar_id = context[@scholar_id.strip]
32
+ article_url = "https://scholar.google.com/citations?view_op=view_citation&hl=en&user=#{scholar_id}&citation_for_view=#{scholar_id}:#{article_id}"
33
+
34
+ begin
35
+ # If the citation count has already been fetched, return it
36
+ if GoogleScholarCitationsTag::Citations[article_id]
37
+ return GoogleScholarCitationsTag::Citations[article_id]
38
+ end
39
+
40
+ # Sleep for a random amount of time to avoid being blocked
41
+ sleep(rand(1.5..3.5))
42
+
43
+ # Fetch the article page
44
+ doc = Nokogiri::HTML(URI.open(article_url, "User-Agent" => "Ruby/#{RUBY_VERSION}"))
45
+
46
+ # Attempt to extract the "Cited by n" string from the meta tags
47
+ citation_count = 0
48
+
49
+ # Look for meta tags with "name" attribute set to "description"
50
+ description_meta = doc.css('meta[name="description"]')
51
+ og_description_meta = doc.css('meta[property="og:description"]')
52
+
53
+ if !description_meta.empty?
54
+ cited_by_text = description_meta[0]['content']
55
+ matches = cited_by_text.match(CITED_BY_REGEX)
56
+
57
+ if matches
58
+ citation_count = matches[1].sub(",", "").to_i
59
+ end
60
+
61
+ elsif !og_description_meta.empty?
62
+ cited_by_text = og_description_meta[0]['content']
63
+ matches = cited_by_text.match(CITED_BY_REGEX)
64
+
65
+ if matches
66
+ citation_count = matches[1].sub(",", "").to_i
67
+ end
68
+ end
69
+
70
+ citation_count = Helpers.number_to_human(citation_count, :format => '%n%u', :precision => 2, :units => { :thousand => 'K', :million => 'M', :billion => 'B' })
71
+
72
+ rescue Exception => e
73
+ # Handle any errors that may occur during fetching
74
+ citation_count = "N/A"
75
+
76
+ # Print the error message including the exception class and message
77
+ puts "Error fetching citation count for #{article_id} in #{article_url}: #{e.class} - #{e.message}"
78
+ end
79
+
80
+ GoogleScholarCitationsTag::Citations[article_id] = citation_count
81
+ return "#{citation_count}"
82
+ end
83
+ end
84
+ end
85
+
86
+ Liquid::Template.register_tag('google_scholar_citations', Jekyll::GoogleScholarCitationsTag)
@@ -0,0 +1,18 @@
1
+ module Jekyll
2
+ module HideCustomBibtex
3
+ def hideCustomBibtex(input)
4
+ keywords = @context.registers[:site].config['filtered_bibtex_keywords']
5
+
6
+ keywords.each do |keyword|
7
+ input = input.gsub(/^.*\b#{keyword}\b *= *\{.*$\n/, '')
8
+ end
9
+
10
+ # Clean superscripts in author lists
11
+ input = input.gsub(/^.*\bauthor\b *= *\{.*$\n/) { |line| line.gsub(/[*†‡§¶‖&^]/, '') }
12
+
13
+ return input
14
+ end
15
+ end
16
+ end
17
+
18
+ Liquid::Template.register_filter(Jekyll::HideCustomBibtex)
@@ -0,0 +1,57 @@
1
+ require "active_support/all"
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ module Helpers
7
+ extend ActiveSupport::NumberHelper
8
+ end
9
+
10
+ module Jekyll
11
+ class InspireHEPCitationsTag < Liquid::Tag
12
+ Citations = { }
13
+
14
+ def initialize(tag_name, params, tokens)
15
+ super
16
+ @recid = params.strip
17
+ end
18
+
19
+ def render(context)
20
+ recid = context[@recid.strip]
21
+ api_url = "https://inspirehep.net/api/literature/?fields=citation_count&q=recid:#{recid}"
22
+
23
+ begin
24
+ # If the citation count has already been fetched, return it
25
+ if InspireHEPCitationsTag::Citations[recid]
26
+ return InspireHEPCitationsTag::Citations[recid]
27
+ end
28
+
29
+ # Fetch the citation count from the API
30
+ uri = URI(api_url)
31
+ response = Net::HTTP.get(uri)
32
+ data = JSON.parse(response)
33
+
34
+ # # Log the response for debugging
35
+ # puts "API Response: #{data.inspect}"
36
+
37
+ # Extract citation count from the JSON data
38
+ citation_count = data["hits"]["hits"][0]["metadata"]["citation_count"].to_i
39
+
40
+ # Format the citation count for readability
41
+ citation_count = Helpers.number_to_human(citation_count, format: '%n%u', precision: 2, units: { thousand: 'K', million: 'M', billion: 'B' })
42
+
43
+ rescue Exception => e
44
+ # Handle any errors that may occur during fetching
45
+ citation_count = "N/A"
46
+
47
+ # Print the error message including the exception class and message
48
+ puts "Error fetching citation count for #{recid}: #{e.class} - #{e.message}"
49
+ end
50
+
51
+ InspireHEPCitationsTag::Citations[recid] = citation_count
52
+ return "#{citation_count}"
53
+ end
54
+ end
55
+ end
56
+
57
+ Liquid::Template.register_tag('inspirehep_citations', Jekyll::InspireHEPCitationsTag)
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unaltraweb
4
+ module LocalizedVisualSources
5
+ module_function
6
+
7
+ VISUAL_SUFFIXES = %w[
8
+ .capture.edited.svg .capture.yaml .capture.yml .capture.svg
9
+ .mermaid.edited.svg .mermaid.svg .plantuml.edited.svg .plantuml.svg
10
+ .mmd.edited.svg .mmd.svg .puml.edited.svg .puml.svg .uml.edited.svg .uml.svg
11
+ .vl.json .vg.json .edited.svg
12
+ .qmd .rmd .ipynb .mermaid .plantuml .mmd .puml .uml .py .r
13
+ .jpeg .tiff .webp .gif .jpg .png .svg .pdf
14
+ ].sort_by { |suffix| -suffix.length }.freeze
15
+ FENCED_CODE_BLOCK = /^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$/m.freeze
16
+ INLINE_CODE = /(`+)[^\n]*?\1/.freeze
17
+ BASEURL_PREFIX = /\A\{\{\s*site\.baseurl\s*\}\}/.freeze
18
+ REMOTE_URL = /\A(?:[a-z][a-z0-9+.-]*:)?\/\//i.freeze
19
+
20
+ def select_url(raw_url, site_source:, lang:, default_lang:, languages:)
21
+ value = raw_url.to_s
22
+ return value if value.empty?
23
+
24
+ prefix = value[BASEURL_PREFIX].to_s
25
+ without_prefix = prefix.empty? ? value : value[prefix.length..]
26
+ decoration_index = [without_prefix.index("?"), without_prefix.index("#")].compact.min
27
+ path = decoration_index ? without_prefix[0...decoration_index] : without_prefix
28
+ decoration = decoration_index ? without_prefix[decoration_index..] : ""
29
+ return value if path.empty? || path.start_with?("#", "data:") || path.match?(REMOTE_URL)
30
+
31
+ current = lang.to_s.strip
32
+ default = default_lang.to_s.strip
33
+ return value if current.empty? || current == default
34
+
35
+ suffix = VISUAL_SUFFIXES.find { |candidate| path.downcase.end_with?(candidate) }
36
+ return value unless suffix
37
+
38
+ stem = path[0...-suffix.length]
39
+ configured = Array(languages).map(&:to_s).map(&:strip).reject(&:empty?) | [current, default]
40
+ return value if configured.any? { |code| stem.downcase.end_with?(".#{code.downcase}") }
41
+
42
+ localized_path = "#{stem}.#{current}#{suffix}"
43
+ local_reference = localized_path.sub(%r{\A/+}, "")
44
+ root = File.realpath(site_source)
45
+ candidate = File.expand_path(local_reference, root)
46
+ return value unless candidate.start_with?(root + File::SEPARATOR) && File.file?(candidate)
47
+
48
+ "#{prefix}#{localized_path}#{decoration}"
49
+ rescue Errno::ENOENT
50
+ value
51
+ end
52
+
53
+ def rewrite(content, site_source:, lang:, default_lang:, languages:)
54
+ source = content.to_s
55
+ return source if lang.to_s.empty? || lang.to_s == default_lang.to_s
56
+
57
+ protected = []
58
+ protected_source = source.gsub(FENCED_CODE_BLOCK) do
59
+ token = "UNALTRAWEBLOCALIZEDVISUALFENCE_#{protected.length}_END"
60
+ protected << Regexp.last_match(0)
61
+ token
62
+ end
63
+ output = rewrite_chunk(
64
+ protected_source,
65
+ site_source: site_source,
66
+ lang: lang,
67
+ default_lang: default_lang,
68
+ languages: languages
69
+ )
70
+ (protected.length - 1).downto(0) do |index|
71
+ output.gsub!("UNALTRAWEBLOCALIZEDVISUALFENCE_#{index}_END", protected[index])
72
+ end
73
+ output
74
+ end
75
+
76
+ def rewrite_chunk(chunk, site_source:, lang:, default_lang:, languages:)
77
+ protected = []
78
+ output = chunk.gsub(INLINE_CODE) do
79
+ token = "UNALTRAWEBLOCALIZEDVISUALCODE_#{protected.length}_END"
80
+ protected << Regexp.last_match(0)
81
+ token
82
+ end
83
+ selector = lambda do |url|
84
+ select_url(url, site_source: site_source, lang: lang, default_lang: default_lang, languages: languages)
85
+ end
86
+ output.gsub!(/(!\[[^\]]*\]\()((?:\{\{\s*site\.baseurl\s*\}\})?[^)\s]+)/) do
87
+ "#{Regexp.last_match(1)}#{selector.call(Regexp.last_match(2))}"
88
+ end
89
+ output.gsub!(/(<img\b[^>]*\bsrc\s*=\s*["'])([^"']+)(["'])/i) do
90
+ "#{Regexp.last_match(1)}#{selector.call(Regexp.last_match(2))}#{Regexp.last_match(3)}"
91
+ end
92
+ (protected.length - 1).downto(0) do |index|
93
+ output.gsub!("UNALTRAWEBLOCALIZEDVISUALCODE_#{index}_END", protected[index])
94
+ end
95
+ output
96
+ end
97
+
98
+ def document_language(document, site)
99
+ value = document.data["lang"].to_s
100
+ return value unless value.empty?
101
+
102
+ relative = document.respond_to?(:relative_path) ? document.relative_path.to_s.tr("\\", "/") : ""
103
+ candidate = relative.split("/")[1].to_s
104
+ candidate.empty? ? (site.config["default_lang"] || site.config["lang"]).to_s : candidate
105
+ end
106
+ end
107
+
108
+ module LocalizedVisualFilter
109
+ def localized_visual(raw_url)
110
+ site = @context.registers[:site]
111
+ page = @context.registers[:page]
112
+ lang = page.respond_to?(:[]) ? page["lang"] : nil
113
+ default_lang = site.config["default_lang"] || site.config["lang"]
114
+ LocalizedVisualSources.select_url(
115
+ raw_url,
116
+ site_source: site.source,
117
+ lang: lang || default_lang,
118
+ default_lang: default_lang,
119
+ languages: site.config["languages"]
120
+ )
121
+ end
122
+ end
123
+ end
124
+
125
+ Liquid::Template.register_filter(Unaltraweb::LocalizedVisualFilter)
126
+
127
+ class UnaltrawebLocalizedVisualSourceGenerator < Jekyll::Generator
128
+ safe true
129
+ priority :highest
130
+
131
+ def generate(site)
132
+ documents = site.collections.values.flat_map(&:docs) + site.pages
133
+ documents.each do |document|
134
+ lang = Unaltraweb::LocalizedVisualSources.document_language(document, site)
135
+ default_lang = (site.config["default_lang"] || site.config["lang"]).to_s
136
+ document.content = Unaltraweb::LocalizedVisualSources.rewrite(
137
+ document.content,
138
+ site_source: site.source,
139
+ lang: lang,
140
+ default_lang: default_lang,
141
+ languages: site.config["languages"]
142
+ )
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Unaltraweb
6
+ module ManualReleaseMetadata
7
+ SELECTOR = /\A(?:latest|v[0-9]{4}\.(?:0[1-9]|1[0-2])(?:\.[1-9][0-9]*)?)\z/
8
+ module_function
9
+
10
+ def metadata(site)
11
+ unaltraweb = site.config["unaltraweb"] || {}
12
+ manual = unaltraweb["manual"] || {}
13
+ configured = (manual["release"] || {})["selector"]
14
+ selector = ENV.fetch("UNALTRAWEB_MANUAL_RELEASE_SELECTOR", configured || "latest").to_s.strip
15
+ unless SELECTOR.match?(selector)
16
+ raise Jekyll::Errors::FatalException,
17
+ "UNALTRAWEB_MANUAL_RELEASE_SELECTOR must be latest, vYYYY.MM, or vYYYY.MM.N"
18
+ end
19
+
20
+ {
21
+ "channel" => selector == "latest" ? "latest" : "stable",
22
+ "schema_version" => 1,
23
+ "selector" => selector,
24
+ }
25
+ end
26
+
27
+ class Generator < Jekyll::Generator
28
+ safe true
29
+ priority :highest
30
+
31
+ def generate(site)
32
+ unaltraweb = site.config["unaltraweb"] || {}
33
+ return unless unaltraweb["site_profile"].to_s == "unaltremanual"
34
+
35
+ manual = unaltraweb["manual"] ||= {}
36
+ release = ManualReleaseMetadata.metadata(site)
37
+ manual["release"] = release
38
+
39
+ page = Jekyll::PageWithoutAFile.new(site, site.source, "", "manual-release.json")
40
+ page.content = JSON.pretty_generate(release) + "\n"
41
+ page.data["layout"] = nil
42
+ page.data["sitemap"] = false
43
+ site.pages << page
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "open3"
5
+ require "shellwords"
6
+
7
+ # Rewrites Markdown and HTML image references from diagram text sources to SVGs.
8
+ # If an edited SVG exists next to the source, it wins over the generated one.
9
+ module Unaltraweb
10
+ module MermaidMmdImages
11
+ module_function
12
+
13
+ BASEURL_PREFIX = /\A\{\{\s*site\.baseurl\s*\}\}/.freeze
14
+ REMOTE_PATH_PREFIX = %r{\A(?:[a-z]+:)?//}i.freeze
15
+ FENCED_CODE_BLOCK = /^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$/m.freeze
16
+ DIAGRAM_EXTENSIONS = %w[.mmd .mermaid .puml .plantuml .uml].freeze
17
+ DIAGRAM_SOURCE = /\.(?:mmd|mermaid|puml|plantuml|uml)/i.freeze
18
+ DIAVISUALS_REMOTE = "git@github.com:dosquartsdedocs/diavisuals.git"
19
+ DIAVISUALS_UV_URL = "git+ssh://git@github.com/dosquartsdedocs/diavisuals.git"
20
+
21
+ def rewrite(content, site_source:, site_config: {})
22
+ return content if content.nil? || content.empty?
23
+
24
+ rewrite_outside_code_fences(content) { |chunk| rewrite_chunk(chunk, site_source: site_source, site_config: site_config) }
25
+ end
26
+
27
+ def rewrite_chunk(content, site_source:, site_config: {})
28
+ out = content.dup
29
+
30
+ out.gsub!(/!\[([^\]]*)\]\(([^)]*?#{DIAGRAM_SOURCE})(\s+"[^"]*")?\)/i) do
31
+ alt = Regexp.last_match(1)
32
+ path = svg_path_for(Regexp.last_match(2), site_source, site_config: site_config)
33
+ title = Regexp.last_match(3).to_s
34
+ "![#{alt}](#{path}#{title})"
35
+ end
36
+
37
+ out.gsub!(/(<img\b[^>]*\bsrc=)(["'])([^"']+?#{DIAGRAM_SOURCE})\2/i) do
38
+ prefix = Regexp.last_match(1)
39
+ quote = Regexp.last_match(2)
40
+ path = svg_path_for(Regexp.last_match(3), site_source, site_config: site_config)
41
+ "#{prefix}#{quote}#{path}#{quote}"
42
+ end
43
+
44
+ out
45
+ end
46
+
47
+ def rewrite_outside_code_fences(content)
48
+ source = content.to_s
49
+ fences = []
50
+ protected_source = source.gsub(FENCED_CODE_BLOCK) do
51
+ token = "UNALTRAWEBFENCEDCODEBLOCK#{fences.length}"
52
+ fences << Regexp.last_match(0)
53
+ token
54
+ end
55
+
56
+ transformed = yield(protected_source)
57
+ fences.each_index.reverse_each do |index|
58
+ transformed.gsub!("UNALTRAWEBFENCEDCODEBLOCK#{index}", fences[index])
59
+ end
60
+
61
+ transformed
62
+ end
63
+
64
+ def svg_path_for(diagram_path, site_source, site_config: {})
65
+ edited = "#{diagram_path}.edited.svg"
66
+ if local_asset_exists?(edited, site_source)
67
+ warn_when_edited_svg_shadows_newer_source(diagram_path, edited, site_source)
68
+ return edited
69
+ end
70
+
71
+ svg_path = "#{diagram_path}.svg"
72
+ render_svg(diagram_path, svg_path, site_source, site_config: site_config)
73
+ svg_path
74
+ end
75
+
76
+ def local_asset_exists?(asset_path, site_source)
77
+ local_path = local_asset_path(asset_path, site_source)
78
+ local_path && File.exist?(local_path)
79
+ end
80
+
81
+ def local_asset_path(asset_path, site_source)
82
+ local_path = asset_path
83
+ .sub(BASEURL_PREFIX, "")
84
+ .split(/[?#]/, 2)
85
+ .first.to_s
86
+ .sub(%r{\A/+}, "")
87
+
88
+ return nil if local_path.empty? || local_path.match?(REMOTE_PATH_PREFIX)
89
+
90
+ root = File.expand_path(site_source)
91
+ path = File.expand_path(local_path, root)
92
+ return nil unless path == root || path.start_with?("#{root}/")
93
+
94
+ path
95
+ end
96
+
97
+ def render_svg(diagram_path, svg_path, site_source, site_config: {})
98
+ return unless render_diagrams?(site_config)
99
+
100
+ source_path = local_asset_path(diagram_path, site_source)
101
+ output_path = local_asset_path(svg_path, site_source)
102
+ return unless source_path && output_path && File.file?(source_path)
103
+ return if File.file?(output_path) && File.mtime(output_path) >= File.mtime(source_path)
104
+
105
+ command, env = diavisuals_command(site_source, site_config)
106
+ unless command
107
+ warn_once("diavisuals-missing", diavisuals_install_message(site_source))
108
+ return
109
+ end
110
+
111
+ FileUtils.mkdir_p(File.dirname(output_path))
112
+ args = [*command, "--project", File.expand_path(site_source), "render-diagram", "--engine", engine_for(source_path), "--format", "svg"]
113
+ args.concat(["--family", diagram_config(site_config)["family"].to_s]) if diagram_config(site_config)["family"].to_s.strip != ""
114
+ profile = diagram_config(site_config)["profile"] || diagram_config(site_config)["compatibility"]
115
+ args.concat(["--profile", profile.to_s]) if profile.to_s.strip != ""
116
+ args.concat([source_path, output_path])
117
+
118
+ stdout, stderr, status = Open3.capture3(env || {}, *args, chdir: site_source)
119
+ return if status.success? && File.file?(output_path)
120
+
121
+ message = "diavisuals could not render #{diagram_path} to SVG"
122
+ detail = [stderr.to_s.strip, stdout.to_s.strip].reject(&:empty?).join("\n")
123
+ warn_once("diavisuals-render-#{source_path}", detail.empty? ? message : "#{message}: #{detail}")
124
+ raise message if fail_on_render_error?(site_config)
125
+ end
126
+
127
+ def warn_when_edited_svg_shadows_newer_source(diagram_path, edited_path, site_source)
128
+ source_path = local_asset_path(diagram_path, site_source)
129
+ edited_local_path = local_asset_path(edited_path, site_source)
130
+ return unless source_path && edited_local_path && File.file?(source_path) && File.file?(edited_local_path)
131
+ return unless File.mtime(source_path) > File.mtime(edited_local_path)
132
+
133
+ warn_once(
134
+ "edited-diagram-#{edited_local_path}",
135
+ "#{edited_path} exists and is older than #{diagram_path}; keeping the edited SVG. Ask before replacing it with a regenerated SVG."
136
+ )
137
+ end
138
+
139
+ def render_diagrams?(site_config)
140
+ env_value = ENV["UNALTRAWEB_RENDER_DIAGRAMS"].to_s.strip.downcase
141
+ return false if %w[0 false no off].include?(env_value)
142
+ return true if %w[1 true yes on].include?(env_value)
143
+
144
+ value = diagram_config(site_config).fetch("render", true)
145
+ ![false, "false", "0", "no", "off"].include?(value)
146
+ end
147
+
148
+ def fail_on_render_error?(site_config)
149
+ value = diagram_config(site_config)["fail_on_render_error"]
150
+ [true, "true", "1", "yes", "on"].include?(value)
151
+ end
152
+
153
+ def diagram_config(site_config)
154
+ unaltraweb = site_config["unaltraweb"] if site_config.respond_to?(:[])
155
+ diagrams = unaltraweb["diagrams"] if unaltraweb.is_a?(Hash)
156
+ diagrams = site_config["diagrams"] if !diagrams.is_a?(Hash) && site_config.respond_to?(:[])
157
+ diagrams.is_a?(Hash) ? diagrams : {}
158
+ end
159
+
160
+ def engine_for(path)
161
+ extension = File.extname(path.to_s).downcase
162
+ return "plantuml" if %w[.puml .plantuml .uml].include?(extension)
163
+
164
+ "mermaid"
165
+ end
166
+
167
+ def diavisuals_command(site_source, site_config)
168
+ configured = diagram_config(site_config)["diavisuals_command"].to_s.strip
169
+ return [Shellwords.split(configured), nil] unless configured.empty?
170
+
171
+ env_command = ENV["DIAVISUALS_COMMAND"].to_s.strip
172
+ return [Shellwords.split(env_command), nil] unless env_command.empty?
173
+
174
+ sibling_candidates(site_source).each do |candidate|
175
+ src = File.join(candidate, "src")
176
+ cli = File.join(src, "diavisuals", "cli.py")
177
+ next unless File.file?(cli)
178
+
179
+ pythonpath = [src, ENV["PYTHONPATH"]].compact.reject(&:empty?).join(File::PATH_SEPARATOR)
180
+ return [["python3", "-m", "diavisuals.cli"], { "PYTHONPATH" => pythonpath }]
181
+ end
182
+
183
+ installed = executable_on_path("diavisuals")
184
+ installed ? [[installed], nil] : [nil, nil]
185
+ end
186
+
187
+ def sibling_candidates(site_source)
188
+ root = File.expand_path(site_source)
189
+ core = File.expand_path("..", __dir__)
190
+ [
191
+ File.expand_path("../diavisuals", root),
192
+ File.expand_path("../diavisuals", core)
193
+ ].uniq
194
+ end
195
+
196
+ def executable_on_path(name)
197
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).map { |dir| File.join(dir, name) }.find do |path|
198
+ File.executable?(path) && !File.directory?(path)
199
+ end
200
+ end
201
+
202
+ def diavisuals_install_message(site_source)
203
+ checkout = File.expand_path("../diavisuals", site_source)
204
+ clone = ["git", "clone", DIAVISUALS_REMOTE, checkout]
205
+ build = ["make", "-C", checkout, "mcp-build"]
206
+ uv = ["uv", "tool", "install", "diavisuals[mcp] @ #{DIAVISUALS_UV_URL}"]
207
+ editable = ["uv", "tool", "install", "--editable", "#{checkout}[mcp]"]
208
+ "diavisuals CLI not found. Install with: #{shell(clone)}; #{shell(build)}. Or use: #{shell(uv)}. Existing checkout: #{shell(editable)}."
209
+ end
210
+
211
+ def shell(command)
212
+ command.map { |part| Shellwords.escape(part.to_s) }.join(" ")
213
+ end
214
+
215
+ def warn_once(key, message)
216
+ @warnings ||= {}
217
+ return if @warnings[key]
218
+
219
+ @warnings[key] = true
220
+ if defined?(Jekyll) && Jekyll.respond_to?(:logger)
221
+ Jekyll.logger.warn("unaltraweb-diagrams:", message)
222
+ else
223
+ warn("unaltraweb-diagrams: #{message}")
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ Jekyll::Hooks.register [:documents, :pages], :pre_render do |doc|
230
+ next unless doc.respond_to?(:content) && doc.content
231
+
232
+ doc.content = Unaltraweb::MermaidMmdImages.rewrite(doc.content, site_source: doc.site.source, site_config: doc.site.config)
233
+ end if defined?(Jekyll::Hooks)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unaltraweb
4
+ module ProfilePages
5
+ module_function
6
+
7
+ def active_profile(site)
8
+ unaltraweb_config = site.config["unaltraweb"] || {}
9
+ (unaltraweb_config["site_profile"] || "unaltreprojecte").to_s
10
+ end
11
+
12
+ def keep_item?(item, profile)
13
+ profiles = item.data["profiles"] || item.data["site_profiles"]
14
+ return true if profiles.nil? || profiles.empty?
15
+
16
+ Array(profiles).map(&:to_s).include?(profile)
17
+ end
18
+ end
19
+ end
20
+
21
+ Jekyll::Hooks.register :site, :post_read do |site|
22
+ active_profile = Unaltraweb::ProfilePages.active_profile(site)
23
+
24
+ site.pages.select! { |page| Unaltraweb::ProfilePages.keep_item?(page, active_profile) }
25
+ site.collections.each_value do |collection|
26
+ collection.docs.select! { |doc| Unaltraweb::ProfilePages.keep_item?(doc, active_profile) }
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # based on https://distresssignal.org/busting-css-cache-with-jekyll-md5-hash
2
+ # https://gist.github.com/BryanSchuetz/2ee8c115096d7dd98f294362f6a667db
3
+ module Jekyll
4
+ module CleanString
5
+ class RemoveAccents
6
+ require 'i18n'
7
+ I18n.config.available_locales = :en
8
+
9
+ attr_accessor :string
10
+
11
+ def initialize(string:)
12
+ self.string = string
13
+ end
14
+
15
+ def digest!
16
+ remove_accents
17
+ end
18
+
19
+ private
20
+
21
+ def remove_accents
22
+ I18n.transliterate(string)
23
+ end
24
+ end
25
+
26
+ def remove_accents(string)
27
+ RemoveAccents.new(string: string).digest!
28
+ end
29
+ end
30
+ end
31
+
32
+ Liquid::Template.register_filter(Jekyll::CleanString)