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,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "rouge"
5
+
6
+ module Unaltraweb
7
+ module CodeLexers
8
+ class Url < Rouge::RegexLexer
9
+ title "URL"
10
+ desc "URLs and decomposed query parameters"
11
+ tag "url"
12
+
13
+ state :root do
14
+ rule %r{https?://}, Keyword
15
+ rule %r{#[^\n]*}, Comment::Single
16
+ rule %r{[?&]}, Punctuation
17
+ rule %r{[A-Za-z_][\w.-]*(?==)}, Name::Attribute
18
+ rule %r{=}, Operator
19
+ rule %r{\b\d+(?:\.\d+)*\b}, Num
20
+ rule %r{[^#?&=\d]+}, Text
21
+ rule %r{.}, Text
22
+ end
23
+ end
24
+
25
+ class Spreadsheet < Rouge::RegexLexer
26
+ title "Spreadsheet formula"
27
+ desc "Spreadsheet formulas and cell references"
28
+ tag "spreadsheet"
29
+
30
+ state :root do
31
+ rule %r/\s+/, Text
32
+ rule %r/"(?:[^"]|"")*"/, Str
33
+ rule %r/\b(?:AND|AVERAGE|COUNTIF|DATEDIF|DATE|DAY|HEX2DEC|HLOOKUP|IF|ISBLANK|ISNUMBER|ISTEXT|LEFT|MID|MONTH|NA|NETWORKDAYS|ROUND|SI|SUM|SUMA|SUMIFS|TODAY|TRIM|VLOOKUP|WEEKDAY|XLOOKUP|YEAR)\b(?=\()/i, Name::Builtin
34
+ rule %r/[A-Za-z_][\w.-]*(?=!)/, Name::Namespace
35
+ rule %r/\$?[A-Z]{1,3}\$?\d+(?::\$?[A-Z]{1,3}\$?\d+)?/i, Name::Variable
36
+ rule %r/\d+(?:[.,]\d+)?/, Num
37
+ rule %r/[+\-*\/>=<&,:;!]/, Operator
38
+ rule %r/[()]/, Punctuation
39
+ rule %r/./, Text
40
+ end
41
+ end
42
+
43
+ class FileTree < Rouge::RegexLexer
44
+ title "File tree"
45
+ desc "Directory and file listings"
46
+ tag "filetree"
47
+
48
+ state :root do
49
+ rule %r/^(\s*)(.+?)(\/)(?=\n?$)/ do
50
+ groups Text, Name::Namespace, Punctuation
51
+ end
52
+ rule %r/^(\s*)(.+?)(\.[A-Za-z0-9]+)(?=\n?$)/ do
53
+ groups Text, Name::Variable, Name::Attribute
54
+ end
55
+ rule %r/\s+/, Text
56
+ rule %r/./, Text
57
+ end
58
+ end
59
+ end
60
+
61
+ module CodeBlocks
62
+ module_function
63
+
64
+ LANGUAGE_LABELS = {
65
+ "bash" => "Bash",
66
+ "c" => "C",
67
+ "console" => "Console",
68
+ "cpp" => "C++",
69
+ "csharp" => "C#",
70
+ "css" => "CSS",
71
+ "go" => "Go",
72
+ "haskell" => "Haskell",
73
+ "html" => "HTML",
74
+ "java" => "Java",
75
+ "javascript" => "JavaScript",
76
+ "js" => "JavaScript",
77
+ "json" => "JSON",
78
+ "julia" => "Julia",
79
+ "latex" => "LaTeX",
80
+ "markdown" => "Markdown",
81
+ "matlab" => "MATLAB",
82
+ "php" => "PHP",
83
+ "powershell" => "PowerShell",
84
+ "python" => "Python",
85
+ "r" => "R",
86
+ "ruby" => "Ruby",
87
+ "rust" => "Rust",
88
+ "shell" => "Shell",
89
+ "sh" => "Shell",
90
+ "sql" => "SQL",
91
+ "typescript" => "TypeScript",
92
+ "ts" => "TypeScript",
93
+ "xml" => "XML",
94
+ "yaml" => "YAML",
95
+ "yml" => "YAML"
96
+ }.freeze
97
+ SEMANTIC_LABELS = {
98
+ "url" => { "ca" => "URL", "es" => "URL", "en" => "URL" },
99
+ "spreadsheet" => { "ca" => "Fórmula", "es" => "Fórmula", "en" => "Formula" },
100
+ "filetree" => { "ca" => "Fitxers", "es" => "Archivos", "en" => "Files" }
101
+ }.freeze
102
+
103
+ def transform_html(html, site:, lang:)
104
+ source = html.to_s
105
+ return source unless source.match?(/<pre(?:\s|>)/i)
106
+
107
+ fragment = Nokogiri::HTML::DocumentFragment.parse(source)
108
+ fragment.css("div.highlighter-rouge").each do |wrapper|
109
+ decorate(wrapper, language_for(wrapper), lang)
110
+ end
111
+
112
+ fragment.css("pre > code").to_a.each do |code|
113
+ next if code.ancestors.any? { |ancestor| class_names(ancestor).include?("uw-code-block") }
114
+
115
+ wrap_standalone(code, lang)
116
+ end
117
+
118
+ fragment.to_html
119
+ end
120
+
121
+ def detect_lang(doc)
122
+ configured = doc.data["lang"].to_s
123
+ return normalize_lang(configured) unless configured.empty?
124
+
125
+ relative_path = doc.respond_to?(:relative_path) ? doc.relative_path.to_s : ""
126
+ path_lang = relative_path.tr("\\", "/").split("/")[1].to_s
127
+ fallback = doc.site.config["default_lang"] || doc.site.config["lang"] || "en"
128
+ normalize_lang(path_lang.empty? ? fallback : path_lang)
129
+ end
130
+
131
+ def normalize_lang(lang)
132
+ lang.to_s.downcase.split("-").first.to_s.then { |value| value.empty? ? "en" : value }
133
+ end
134
+
135
+ def decorate(wrapper, language, lang)
136
+ normalized_language = language.to_s.downcase
137
+ normalized_language = "text" if normalized_language.empty?
138
+
139
+ add_classes(wrapper, "uw-code-block", "language-#{normalized_language}")
140
+ wrapper["data-code-language"] = normalized_language
141
+ if LANGUAGE_LABELS.key?(normalized_language) || SEMANTIC_LABELS.key?(normalized_language)
142
+ label = display_label(normalized_language, lang)
143
+ replace_class(wrapper, "uw-code-verbatim", "uw-code-highlighted")
144
+ wrapper["role"] = "group"
145
+ wrapper["aria-label"] = label
146
+ add_header(wrapper, label)
147
+ ensure_line_numbers(wrapper)
148
+ else
149
+ replace_class(wrapper, "uw-code-highlighted", "uw-code-verbatim")
150
+ wrapper.remove_attribute("role")
151
+ wrapper.remove_attribute("aria-label")
152
+ wrapper.element_children.select { |child| class_names(child).include?("uw-code-header") }.each(&:remove)
153
+ remove_line_numbers(wrapper)
154
+ end
155
+ end
156
+
157
+ def wrap_standalone(code, lang)
158
+ source_pre = code.parent
159
+ language = language_for(code) || language_for(source_pre) || "text"
160
+ wrapper = node("div", code.document)
161
+ highlight = node("div", code.document)
162
+
163
+ wrapper["class"] = "highlighter-rouge"
164
+ highlight["class"] = "highlight"
165
+ source_pre.replace(wrapper)
166
+ wrapper.add_child(highlight)
167
+ highlight.add_child(source_pre)
168
+ decorate(wrapper, language, lang)
169
+ end
170
+
171
+ def add_header(wrapper, label)
172
+ return if wrapper.element_children.any? { |child| class_names(child).include?("uw-code-header") }
173
+
174
+ header = node("div", wrapper.document)
175
+ language = node("span", wrapper.document)
176
+ header["class"] = "uw-code-header"
177
+ language["class"] = "uw-code-language"
178
+ language.content = label
179
+ header.add_child(language)
180
+ wrapper.prepend_child(header)
181
+ end
182
+
183
+ def ensure_line_numbers(wrapper)
184
+ if (table = wrapper.at_css("table.rouge-table"))
185
+ mark_line_number_table(table)
186
+ return
187
+ end
188
+
189
+ code = wrapper.at_css(".highlight pre > code") || wrapper.at_css("pre > code")
190
+ return unless code
191
+
192
+ source_pre = code.parent
193
+ line_count = code.text.delete_suffix("\n").count("\n") + 1
194
+ table = line_number_table(code.document, line_count, source_pre, code)
195
+ source_pre.replace(table)
196
+ end
197
+
198
+ def remove_line_numbers(wrapper)
199
+ table = wrapper.at_css("table.rouge-table")
200
+ return unless table
201
+
202
+ source_pre = table.at_css("td.rouge-code > pre")
203
+ return unless source_pre
204
+
205
+ source_pre.unlink
206
+ table.replace(source_pre)
207
+ end
208
+
209
+ def line_number_table(document, line_count, source_pre, code)
210
+ table = node("table", document)
211
+ body = node("tbody", document)
212
+ row = node("tr", document)
213
+ gutter = node("td", document)
214
+ gutter_pre = node("pre", document)
215
+ code_cell = node("td", document)
216
+ code_pre = node("pre", document)
217
+
218
+ table["class"] = "rouge-table"
219
+ gutter["class"] = "rouge-gutter gl"
220
+ gutter_pre["class"] = "lineno"
221
+ gutter_pre.content = (1..line_count).to_a.join("\n") + "\n"
222
+ code_cell["class"] = "rouge-code"
223
+ code_pre["class"] = source_pre["class"].to_s
224
+
225
+ code.unlink
226
+ code_pre.add_child(code)
227
+ gutter.add_child(gutter_pre)
228
+ code_cell.add_child(code_pre)
229
+ row.add_child(gutter)
230
+ row.add_child(code_cell)
231
+ body.add_child(row)
232
+ table.add_child(body)
233
+ mark_line_number_table(table)
234
+ table
235
+ end
236
+
237
+ def mark_line_number_table(table)
238
+ table["role"] = "presentation"
239
+ table.css(".rouge-gutter, pre.lineno").each { |node| node["aria-hidden"] = "true" }
240
+ end
241
+
242
+ def language_for(node)
243
+ return nil unless node
244
+
245
+ language_class = class_names(node).find { |name| name.start_with?("language-") }
246
+ language_class&.delete_prefix("language-")
247
+ end
248
+
249
+ def display_label(language, lang)
250
+ labels = SEMANTIC_LABELS[language]
251
+ return labels[normalize_lang(lang)] || labels.fetch("en") if labels
252
+
253
+ LANGUAGE_LABELS.fetch(language)
254
+ end
255
+
256
+ def class_names(node)
257
+ node["class"].to_s.split
258
+ end
259
+
260
+ def add_classes(node, *classes)
261
+ node["class"] = (class_names(node) + classes).uniq.join(" ")
262
+ end
263
+
264
+ def replace_class(node, old_class, new_class)
265
+ node["class"] = ((class_names(node) - [old_class]) + [new_class]).uniq.join(" ")
266
+ end
267
+
268
+ def node(name, document)
269
+ Nokogiri::XML::Node.new(name, document)
270
+ end
271
+ end
272
+ end
273
+
274
+ Jekyll::Hooks.register %i[documents pages], :post_convert do |doc|
275
+ lang = Unaltraweb::CodeBlocks.detect_lang(doc)
276
+ doc.content = Unaltraweb::CodeBlocks.transform_html(doc.content, site: doc.site, lang: lang)
277
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ # Rewrites Markdown and HTML image references from executable computation
7
+ # sources to their declared figure outputs. If an edited SVG exists for the
8
+ # declared output, it wins over the generated one.
9
+ #
10
+ # Authoring model: a chapter references the compute source the same way it
11
+ # references a diagram source:
12
+ #
13
+ # ![Alt](assets/quarto/figures/boxplot.qmd "Caption"){: data-figure-width="48rem"}
14
+ #
15
+ # The plugin resolves the source, reads its `unaltraweb_compute.outputs`
16
+ # (mode: figure), and rewrites the reference to the first declared output
17
+ # (for example `assets/img/data-visualization/boxplot-housing.svg`). An edited
18
+ # override named like the output with the extension replaced by `.edited.svg`
19
+ # (for example `boxplot-housing.edited.svg`) is preferred and never overwritten.
20
+ #
21
+ # The plugin never renders: figures are produced by the host computation
22
+ # pipeline (`make manual-compute-render`) before Jekyll runs.
23
+ module Unaltraweb
24
+ module ComputationFigureImages
25
+ module_function
26
+
27
+ BASEURL_PREFIX = /\A\{\{\s*site\.baseurl\s*\}\}/.freeze
28
+ REMOTE_PATH_PREFIX = %r{\A(?:[a-z]+:)?//}i.freeze
29
+ FENCED_CODE_BLOCK = /^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$/m.freeze
30
+ COMPUTE_SUFFIXES = %w[.qmd .rmd .r .py .ipynb].freeze
31
+ COMPUTE_SOURCE = /\.(?:qmd|rmd|ipynb|py|r)(?=[\s)"']|$)/i.freeze
32
+ MEDIA_SUFFIX = /\.(?:gif|jpeg|jpg|pdf|png|svg|webp)\z/i.freeze
33
+ EDITED_SUFFIX = ".edited.svg".freeze
34
+
35
+ def rewrite(content, site_source:)
36
+ return content if content.nil? || content.empty?
37
+
38
+ rewrite_outside_code_fences(content) { |chunk| rewrite_chunk(chunk, site_source: site_source) }
39
+ end
40
+
41
+ def rewrite_chunk(content, site_source:)
42
+ out = content.dup
43
+
44
+ out.gsub!(/!\[([^\]]*)\]\(([^)]*?#{COMPUTE_SOURCE})(\s+"[^"]*")?\)/i) do
45
+ alt = Regexp.last_match(1)
46
+ path = published_path_for(Regexp.last_match(2), site_source)
47
+ title = Regexp.last_match(3).to_s
48
+ "![#{alt}](#{path}#{title})"
49
+ end
50
+
51
+ out.gsub!(/(<img\b[^>]*\bsrc=)(["'])([^"']+?#{COMPUTE_SOURCE})\2/i) do
52
+ prefix = Regexp.last_match(1)
53
+ quote = Regexp.last_match(2)
54
+ path = published_path_for(Regexp.last_match(3), site_source)
55
+ "#{prefix}#{quote}#{path}#{quote}"
56
+ end
57
+
58
+ out
59
+ end
60
+
61
+ def rewrite_outside_code_fences(content)
62
+ source = content.to_s
63
+ fences = []
64
+ protected_source = source.gsub(FENCED_CODE_BLOCK) do
65
+ token = "UNALTRAWEBCOMPUTEFIGURECODE#{fences.length}"
66
+ fences << Regexp.last_match(0)
67
+ token
68
+ end
69
+
70
+ transformed = yield(protected_source)
71
+ fences.each_index.reverse_each do |index|
72
+ transformed.gsub!("UNALTRAWEBCOMPUTEFIGURECODE#{index}", fences[index])
73
+ end
74
+
75
+ transformed
76
+ end
77
+
78
+ def published_path_for(source_ref, site_source)
79
+ local = local_asset_path(source_ref, site_source)
80
+ unless local && File.file?(local)
81
+ raise Jekyll::Errors::FatalException, "Computed figure source not found: #{source_ref}. Verify the chapter reference and computation source_roots."
82
+ end
83
+
84
+ outputs = declared_outputs(local)
85
+ if outputs.empty?
86
+ warn_once("not-a-figure-#{local}", "Referenced compute source #{source_ref} declares no figure outputs (unaltraweb_compute.mode: figure, outputs); leaving the reference unchanged.")
87
+ return source_ref
88
+ end
89
+
90
+ output = outputs.first
91
+ edited = output.sub(MEDIA_SUFFIX, EDITED_SUFFIX)
92
+ edited_local = local_asset_path(edited, site_source)
93
+ if edited_local && File.file?(edited_local)
94
+ warn_when_edited_shadows_newer_source(local, output, edited_local, site_source)
95
+ return reference_for(source_ref, edited)
96
+ end
97
+
98
+ output_local = local_asset_path(output, site_source)
99
+ unless output_local && File.file?(output_local)
100
+ raise Jekyll::Errors::FatalException, "Missing rendered figure #{output} for #{source_ref}. Run `manual_computation_render` before building."
101
+ end
102
+
103
+ reference_for(source_ref, output)
104
+ end
105
+
106
+ def reference_for(source_ref, relative_path)
107
+ baseurl = source_ref[BASEURL_PREFIX]
108
+ return "#{baseurl}/#{relative_path}" if baseurl
109
+
110
+ return "/#{relative_path}" if source_ref.start_with?("/")
111
+
112
+ relative_path
113
+ end
114
+
115
+ def warn_when_edited_shadows_newer_source(source_local, output, edited_local, site_source)
116
+ return unless File.mtime(source_local) > File.mtime(edited_local)
117
+
118
+ warn_once(
119
+ "edited-figure-#{edited_local}",
120
+ "#{output.sub(MEDIA_SUFFIX, EDITED_SUFFIX)} exists and is older than #{relative_from(site_source, source_local)}; keeping the edited SVG. Ask before replacing it with a regenerated figure."
121
+ )
122
+ end
123
+
124
+ def relative_from(root, path)
125
+ root = File.expand_path(root)
126
+ expanded = File.expand_path(path)
127
+ return path unless expanded.start_with?("#{root}/")
128
+
129
+ expanded.delete_prefix("#{root}/")
130
+ end
131
+
132
+ def declared_outputs(source_local)
133
+ front = read_front_matter(source_local)
134
+ metadata = front.is_a?(Hash) ? front["unaltraweb_compute"] : nil
135
+ return [] unless metadata.is_a?(Hash)
136
+
137
+ mode = metadata["mode"]
138
+ return [] unless mode.to_s.strip.downcase == "figure"
139
+
140
+ outputs = metadata["outputs"]
141
+ outputs = [metadata["output"]] if outputs.nil? && metadata["output"]
142
+ return [] unless outputs.is_a?(Array)
143
+
144
+ listed = outputs.map(&:to_s).map(&:strip).reject(&:empty?)
145
+ listed.select { |path| path.match?(MEDIA_SUFFIX) && valid_relative_path?(path) }
146
+ end
147
+
148
+ def valid_relative_path?(path)
149
+ relative = Pathname.new(path)
150
+ !relative.absolute? && !relative.each_filename.include?("..")
151
+ end
152
+
153
+ def read_front_matter(path)
154
+ suffix = File.extname(path.to_s).downcase
155
+ text = suffix == ".qmd" || suffix == ".rmd" ? yaml_front_matter(path) : script_front_matter(path)
156
+ return {} if text.empty?
157
+
158
+ YAML.safe_load(text)
159
+ rescue Psych::Exception
160
+ warn_once("invalid-front-matter-#{path}", "Unparseable front matter in #{path}; treating it as a non-figure computation source.")
161
+ {}
162
+ end
163
+
164
+ def yaml_front_matter(path)
165
+ match = /\A---\s*\n(.*?)\n---\s*\n?/m.match(File.read(path, encoding: "UTF-8"))
166
+ match ? match[1] : ""
167
+ end
168
+
169
+ def script_front_matter(path)
170
+ suffix = File.extname(path.to_s).downcase
171
+ if suffix == ".ipynb"
172
+ return ipynb_front_matter(path) rescue ""
173
+ end
174
+
175
+ prefix = suffix == ".r" || suffix == ".rmd" ? "#'" : "#"
176
+ marker = "#{prefix} ---"
177
+ active = false
178
+ block = []
179
+ File.readlines(path, encoding: "UTF-8").each do |line|
180
+ if line.strip == marker
181
+ if active
182
+ return block.join("\n")
183
+ end
184
+ active = true
185
+ next
186
+ end
187
+ block << line.delete_prefix(prefix).sub(/\A /, "") if active
188
+ end
189
+ ""
190
+ end
191
+
192
+ def ipynb_front_matter(path)
193
+ notebook = JSON.parse(File.read(path, encoding: "UTF-8"))
194
+ metadata = notebook["metadata"]
195
+ if metadata.is_a?(Hash) && metadata["unaltraweb_front_matter"].is_a?(Hash)
196
+ return YAML.dump(metadata["unaltraweb_front_matter"])
197
+ end
198
+ cells = notebook["cells"] || []
199
+ cells.each do |cell|
200
+ next unless cell["cell_type"] == "markdown" || cell["cell_type"] == "raw"
201
+
202
+ source = cell["source"]
203
+ text = source.is_a?(Array) ? source.join : source.to_s
204
+ match = /\A---\s*\n(.*?)\n---\s*\n?/m.match(text)
205
+ return match[1] if match
206
+ break
207
+ end
208
+ ""
209
+ end
210
+
211
+ def local_asset_exists?(asset_path, site_source)
212
+ local = local_asset_path(asset_path, site_source)
213
+ local && File.file?(local)
214
+ end
215
+
216
+ def local_asset_path(asset_path, site_source)
217
+ local_path = asset_path
218
+ .sub(BASEURL_PREFIX, "")
219
+ .split(/[?#]/, 2)
220
+ .first.to_s
221
+ .sub(%r{\A/+}, "")
222
+
223
+ return nil if local_path.empty? || local_path.match?(REMOTE_PATH_PREFIX)
224
+
225
+ root = File.expand_path(site_source)
226
+ path = File.expand_path(local_path, root)
227
+ return nil unless path == root || path.start_with?("#{root}/")
228
+
229
+ path
230
+ end
231
+
232
+ def warn_once(key, message)
233
+ @warnings ||= {}
234
+ return if @warnings[key]
235
+
236
+ @warnings[key] = true
237
+ if defined?(Jekyll) && Jekyll.respond_to?(:logger)
238
+ Jekyll.logger.warn("unaltraweb-computations:", message)
239
+ else
240
+ warn("unaltraweb-computations: #{message}")
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ if defined?(Jekyll::Hooks)
247
+ Jekyll::Hooks.register :site, :post_read do |site|
248
+ # Reject generated figure outputs from static files when an edited SVG
249
+ # override exists, so only the authored figure is published.
250
+ site.static_files.reject! do |file|
251
+ relative = file.relative_path.to_s
252
+ next false if relative.empty?
253
+ next false unless relative.match?(Unaltraweb::ComputationFigureImages::MEDIA_SUFFIX)
254
+
255
+ edited = relative.sub(Unaltraweb::ComputationFigureImages::MEDIA_SUFFIX, Unaltraweb::ComputationFigureImages::EDITED_SUFFIX)
256
+ local = Unaltraweb::ComputationFigureImages.local_asset_path(edited, site.source)
257
+ local && File.file?(local)
258
+ end
259
+ end
260
+ end
261
+
262
+ Jekyll::Hooks.register [:documents, :pages], :pre_render do |doc|
263
+ next unless doc.respond_to?(:content) && doc.content
264
+
265
+ doc.content = Unaltraweb::ComputationFigureImages.rewrite(doc.content, site_source: doc.site.source)
266
+ end if defined?(Jekyll::Hooks)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+
6
+ module Unaltraweb
7
+ module ComputationSources
8
+ SUFFIXES = %w[.qmd .rmd .r .py .ipynb].freeze
9
+
10
+ def self.source_roots(site)
11
+ config_path = File.join(site.source, ".unaltraweb", "computations.yml")
12
+ config = File.file?(config_path) ? YAML.safe_load_file(config_path, aliases: false) : {}
13
+ roots = config.is_a?(Hash) ? config.fetch("source_roots", ["_chapters"]) : ["_chapters"]
14
+ Array(roots).map do |root|
15
+ path = Pathname.new(root.to_s)
16
+ value = path.cleanpath.to_s.downcase
17
+ if path.absolute? || value.empty? || value == "." || value.split("/").include?("..")
18
+ raise Jekyll::Errors::FatalException, "Invalid computation source root: #{root}"
19
+ end
20
+ "/#{value}/"
21
+ end
22
+ rescue Psych::Exception => e
23
+ raise Jekyll::Errors::FatalException, "Invalid .unaltraweb/computations.yml: #{e.message}"
24
+ end
25
+
26
+ Jekyll::Hooks.register :site, :post_read do |site|
27
+ roots = source_roots(site)
28
+ site.static_files.reject! do |file|
29
+ relative = file.relative_path.to_s.downcase
30
+ relative.start_with?("/.unaltraweb/") ||
31
+ (roots.any? { |root| relative.start_with?(root) } &&
32
+ (relative.include?("/.quarto/") || relative.end_with?(".quarto_ipynb") || SUFFIXES.any? { |suffix| relative.end_with?(suffix) }))
33
+ end
34
+
35
+ site.collections.each_value do |collection|
36
+ collection.docs.reject! do |document|
37
+ SUFFIXES.include?(File.extname(document.path).downcase)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end