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,800 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Unaltraweb
6
+ module FigureCaptions
7
+ module_function
8
+
9
+ PANEL_SEQUENCE = ("a".."z").to_a.freeze
10
+ FENCED_CODE_BLOCK = /^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$/m.freeze
11
+
12
+ def enabled?(site)
13
+ config = config_for(site)
14
+ config.fetch("enabled", true) != false
15
+ end
16
+
17
+ def collections(site)
18
+ config = config_for(site)
19
+ Array(config["collections"] || ["chapters"]).map(&:to_s)
20
+ end
21
+
22
+ def config_for(site)
23
+ unaltraweb = site.config["unaltraweb"] || {}
24
+ unaltraweb["figure_captions"] || {}
25
+ end
26
+
27
+ def process_collection?(site, label)
28
+ enabled?(site) && collections(site).include?(label.to_s)
29
+ end
30
+
31
+ def process_page?(page)
32
+ return false unless enabled?(page.site)
33
+
34
+ value = page.data["figure_captions"]
35
+ value == true || value.to_s == "true" || (value.respond_to?(:fetch) && value.fetch("enabled", true) != false)
36
+ end
37
+
38
+ def detect_lang(doc)
39
+ return doc.data["lang"].to_s unless doc.data["lang"].to_s.empty?
40
+
41
+ rel = doc.relative_path.to_s.tr("\\", "/")
42
+ parts = rel.split("/")
43
+ parts[1].to_s.empty? ? doc.site.config["default_lang"].to_s : parts[1].to_s
44
+ end
45
+
46
+ def label_for(site, lang)
47
+ i18n = site.data["i18n"] || {}
48
+ lang_data = i18n[lang] || i18n[site.config["default_lang"]] || {}
49
+ label = lang_data.dig("figures", "label") if lang_data.respond_to?(:dig)
50
+ label ||= lang_data["figure"]
51
+ label ||= "Figure"
52
+ label.to_s
53
+ end
54
+
55
+ def table_label_for(site, lang)
56
+ i18n = site.data["i18n"] || {}
57
+ lang_data = i18n[lang] || i18n[site.config["default_lang"]] || {}
58
+ label = lang_data.dig("tables", "label") if lang_data.respond_to?(:dig)
59
+ label ||= lang_data["table"]
60
+ label ||= "Table"
61
+ label.to_s
62
+ end
63
+
64
+ def listing_label_for(site, lang)
65
+ i18n = site.data["i18n"] || {}
66
+ lang_data = i18n[lang] || i18n[site.config["default_lang"]] || {}
67
+ label = lang_data.dig("listings", "label") if lang_data.respond_to?(:dig)
68
+ label ||= lang_data["listing"]
69
+ label ||= "Code example"
70
+ label.to_s
71
+ end
72
+
73
+ def transform_markdown_images(text, lang, label)
74
+ out = +""
75
+ source = text.to_s
76
+ index = 0
77
+ count = 0
78
+
79
+ while index < source.length
80
+ if (block = parse_subfigures_block(source, index))
81
+ html, count = subfigures_html(block, lang, label, count)
82
+ out << "\n\n" << html << "\n\n"
83
+ index = block[:end_idx]
84
+ next
85
+ end
86
+
87
+ if source[index] == "!" && source[index + 1] == "["
88
+ parsed = parse_markdown_image(source, index)
89
+ if parsed
90
+ alt = parsed[:alt]
91
+ url_and_title = parsed[:url_and_title]
92
+ attrs_raw = parsed[:attrs]
93
+ next_index = parsed[:end_idx]
94
+
95
+ if attrs_raw && attrs_raw.match?(/(\.no-figure|data-no-figure\s*=\s*["']true["'])/)
96
+ out << source[index...next_index]
97
+ else
98
+ url, title = split_url_and_title(url_and_title)
99
+ caption = title.to_s.empty? ? alt.to_s : title.to_s
100
+ attrs = kramdown_attrs_to_html(attrs_raw)
101
+ count += 1
102
+
103
+ out << "\n\n"
104
+ out << figure_html(
105
+ img: %(<img src="#{h(url.strip)}" alt="#{h(strip_liquid(alt))}"#{attrs.empty? ? "" : " #{attrs}"}>),
106
+ caption: render_inline_markdown(caption),
107
+ label: label,
108
+ lang: lang,
109
+ count: count,
110
+ classes: figure_classes_for(url),
111
+ attrs: figure_container_attrs(attrs_raw)
112
+ )
113
+ out << "\n\n"
114
+ end
115
+ index = next_index
116
+ next
117
+ end
118
+ end
119
+
120
+ out << source[index]
121
+ index += 1
122
+ end
123
+
124
+ out
125
+ end
126
+
127
+ def transform_markdown_tables(text, lang, label)
128
+ out = +""
129
+ source = text.to_s
130
+ index = 0
131
+ count = 0
132
+
133
+ while index < source.length
134
+ if (block = parse_table_block(source, index))
135
+ html, count = table_html(block, lang, label, count)
136
+ out << "\n\n" << html << "\n\n"
137
+ index = block[:end_idx]
138
+ next
139
+ end
140
+
141
+ out << source[index]
142
+ index += 1
143
+ end
144
+
145
+ out
146
+ end
147
+
148
+ def transform_markdown_listings(text, lang, label)
149
+ out = +""
150
+ source = text.to_s
151
+ index = 0
152
+ count = 0
153
+
154
+ while index < source.length
155
+ if (block = parse_listing_block(source, index))
156
+ html, count = listing_html(block, lang, label, count)
157
+ out << "\n\n" << html << "\n\n"
158
+ index = block[:end_idx]
159
+ next
160
+ end
161
+
162
+ out << source[index]
163
+ index += 1
164
+ end
165
+
166
+ out
167
+ end
168
+
169
+ def transform_markdown_sugar(text, lang, figure_label, table_label, listing_label = "Code example")
170
+ transform_outside_code_fences(text) do |chunk|
171
+ transformed = transform_markdown_images(chunk, lang, figure_label)
172
+ transformed = transform_markdown_tables(transformed, lang, table_label)
173
+ transform_markdown_listings(transformed, lang, listing_label)
174
+ end
175
+ end
176
+
177
+ def transform_outside_code_fences(text)
178
+ source = text.to_s
179
+ fences = []
180
+ protected_source = source.gsub(FENCED_CODE_BLOCK) do
181
+ token = "UNALTRAWEBFENCEDCODEBLOCK#{fences.length}"
182
+ fences << Regexp.last_match(0)
183
+ token
184
+ end
185
+
186
+ transformed = yield(protected_source)
187
+ fences.each_index.reverse_each do |index|
188
+ transformed.gsub!("UNALTRAWEBFENCEDCODEBLOCK#{index}", fences[index])
189
+ end
190
+
191
+ transformed
192
+ end
193
+
194
+ def parse_subfigures_block(source, start_index)
195
+ return nil unless line_start?(source, start_index) && source[start_index, 3] == ":::"
196
+
197
+ line_end = source.index("\n", start_index) || source.length
198
+ opening = source[start_index...line_end]
199
+ match = opening.match(/\A:::\s*subfigures(?:\s+([^\s"]+))?(?:\s+"([^"]+)")?\s*\z/)
200
+ return nil unless match
201
+
202
+ body_start = line_end == source.length ? line_end : line_end + 1
203
+ closing = source.match(/^:::\s*$/m, body_start)
204
+ return nil unless closing
205
+
206
+ {
207
+ layout: match[1].to_s,
208
+ caption: match[2].to_s,
209
+ body: source[body_start...closing.begin(0)],
210
+ raw: source[start_index...closing.end(0)],
211
+ end_idx: closing.end(0)
212
+ }
213
+ end
214
+
215
+ def parse_table_block(source, start_index)
216
+ return nil unless line_start?(source, start_index) && source[start_index, 3] == ":::"
217
+
218
+ line_end = source.index("\n", start_index) || source.length
219
+ opening = source[start_index...line_end]
220
+ match = opening.match(/\A:::\s*table(?:\s+"([^"]+)")?\s*\z/)
221
+ return nil unless match
222
+
223
+ body_start = line_end == source.length ? line_end : line_end + 1
224
+ closing = source.match(/^:::\s*$/m, body_start)
225
+ return nil unless closing
226
+
227
+ {
228
+ caption: match[1].to_s,
229
+ body: source[body_start...closing.begin(0)],
230
+ raw: source[start_index...closing.end(0)],
231
+ end_idx: closing.end(0)
232
+ }
233
+ end
234
+
235
+ def parse_listing_block(source, start_index)
236
+ return nil unless line_start?(source, start_index) && source[start_index, 3] == ":::"
237
+
238
+ line_end = source.index("\n", start_index) || source.length
239
+ opening = source[start_index...line_end]
240
+ match = opening.match(/\A:::\s*listing\s+"([^"]+)"\s*\z/)
241
+ return nil unless match
242
+
243
+ body_start = line_end == source.length ? line_end : line_end + 1
244
+ closing = source.match(/^:::\s*$/m, body_start)
245
+ return nil unless closing
246
+
247
+ {
248
+ caption: match[1],
249
+ body: source[body_start...closing.begin(0)],
250
+ raw: source[start_index...closing.end(0)],
251
+ end_idx: closing.end(0)
252
+ }
253
+ end
254
+
255
+ def line_start?(source, index)
256
+ index.zero? || source[index - 1] == "\n"
257
+ end
258
+
259
+ def subfigures_html(block, lang, label, count)
260
+ images = parse_subfigure_images(block[:body])
261
+ return [block[:raw], count] if images.empty?
262
+
263
+ count += 1
264
+ rows = subfigure_rows(block[:layout], images.length)
265
+ any_diagram = images.any? { |img| diagram_path?(img[:url]) }
266
+ outer_classes = ["md-figure", "md-subfigure-set"]
267
+ outer_classes << "mermaid-figure" if any_diagram
268
+
269
+ row_html = rows.map do |row|
270
+ cells = row.map do |slot|
271
+ image = images[slot[:index]]
272
+ attrs = kramdown_attrs_to_html(image[:attrs])
273
+ panel_attrs = subfigure_panel_attrs(image[:attrs])
274
+ caption = render_inline_markdown(image[:caption])
275
+ panel = h(slot[:label])
276
+ caption_html = caption.to_s.strip.empty? ? "" : %Q{<p class="md-subfigure-caption"><span class="md-subfigure-label">#{panel}</span> #{caption}</p>}
277
+ %Q{<div class="md-subfigure" data-panel="#{panel}"#{panel_attrs}>#{caption_html}<img src="#{h(image[:url].strip)}" alt="#{h(strip_liquid(image[:alt]))}"#{attrs.empty? ? "" : " #{attrs}"}></div>}
278
+ end.join("\n")
279
+ %Q{<div class="md-subfigure-row" data-count="#{row.length}">\n#{cells}\n</div>}
280
+ end.join("\n")
281
+
282
+ clean_caption = render_inline_markdown(block[:caption]).to_s.strip
283
+ figcaption = clean_caption.empty? ? "" : %(<figcaption class="md-figcaption"><span class="figlabel">#{h(label)} #{count}.</span> #{clean_caption}</figcaption>)
284
+
285
+ html = <<~HTML.strip
286
+ <figure id="fig-#{h(lang)}-#{count}" class="#{outer_classes.join(' ')}" data-layout="#{h(block[:layout])}">
287
+ #{figcaption}
288
+ <div class="md-figure-inner">
289
+ <div class="md-subfigure-grid">
290
+ #{row_html}
291
+ </div>
292
+ </div>
293
+ </figure>
294
+ HTML
295
+
296
+ [html, count]
297
+ end
298
+
299
+ def table_html(block, lang, label, count)
300
+ parsed_table = parse_markdown_table(block[:body])
301
+ return [block[:raw], count] unless parsed_table
302
+
303
+ count += 1
304
+ caption = render_inline_markdown(block[:caption]).to_s.strip
305
+ caption_html = caption.empty? ? "" : %(<figcaption class="md-table-caption"><span class="figlabel">#{h(label)} #{count}.</span> #{caption}</figcaption>)
306
+ thead = table_row_html(parsed_table[:headers], "th", parsed_table[:alignments])
307
+ tbody = parsed_table[:rows].map { |row| table_row_html(row, "td", parsed_table[:alignments]) }.join("\n")
308
+
309
+ html = <<~HTML.strip
310
+ <figure id="tbl-#{h(lang)}-#{count}" class="md-table">
311
+ #{caption_html}
312
+ <div class="md-table-inner">
313
+ <table class="md-table-element">
314
+ <thead>
315
+ #{thead}
316
+ </thead>
317
+ <tbody>
318
+ #{tbody}
319
+ </tbody>
320
+ </table>
321
+ </div>
322
+ </figure>
323
+ HTML
324
+
325
+ [html, count]
326
+ end
327
+
328
+ def listing_html(block, lang, label, count)
329
+ code_token = block[:body].to_s.strip
330
+ return [block[:raw], count] unless code_token.match?(/\AUNALTRAWEBFENCEDCODEBLOCK\d+\z/)
331
+
332
+ count += 1
333
+ caption = render_inline_markdown(block[:caption]).to_s.strip
334
+ caption_html = %(<figcaption class="md-code-caption"><span class="figlabel">#{h(label)} #{count}.</span> #{caption}</figcaption>)
335
+ html = <<~HTML.strip
336
+ <figure id="lst-#{h(lang)}-#{count}" class="md-code-listing" markdown="1">
337
+ #{caption_html}
338
+
339
+ #{code_token}
340
+ </figure>
341
+ HTML
342
+
343
+ [html, count]
344
+ end
345
+
346
+ def parse_markdown_table(body)
347
+ lines = body.to_s.lines.map(&:rstrip)
348
+ lines.shift while lines.first&.strip&.empty?
349
+ lines.pop while lines.last&.strip&.empty?
350
+ return nil if lines.length < 2
351
+
352
+ headers = split_table_row(lines[0])
353
+ alignments = parse_table_alignment(lines[1], headers.length)
354
+ return nil unless alignments
355
+
356
+ rows = lines[2..].to_a.reject { |line| line.strip.empty? }.map { |line| normalize_table_row(split_table_row(line), headers.length) }
357
+ {
358
+ headers: normalize_table_row(headers, headers.length),
359
+ alignments: alignments,
360
+ rows: rows
361
+ }
362
+ end
363
+
364
+ def split_table_row(line)
365
+ source = line.to_s.strip
366
+ source = source[1..] if source.start_with?("|")
367
+ source = source[0...-1] if source.end_with?("|") && !source.end_with?("\\|")
368
+
369
+ cells = []
370
+ current = +""
371
+ escaped = false
372
+ source.each_char do |char|
373
+ if char == "|" && !escaped
374
+ cells << current.strip
375
+ current = +""
376
+ else
377
+ current << char
378
+ end
379
+ escaped = char == "\\" && !escaped
380
+ end
381
+ cells << current.strip
382
+ cells.map { |cell| cell.gsub("\\|", "|") }
383
+ end
384
+
385
+ def parse_table_alignment(line, column_count)
386
+ cells = normalize_table_row(split_table_row(line), column_count)
387
+ return nil if cells.empty?
388
+
389
+ cells.map do |cell|
390
+ marker = cell.strip
391
+ return nil unless marker.match?(/\A:?-{3,}:?\z/)
392
+
393
+ if marker.start_with?(":") && marker.end_with?(":")
394
+ "center"
395
+ elsif marker.end_with?(":")
396
+ "right"
397
+ elsif marker.start_with?(":")
398
+ "left"
399
+ end
400
+ end
401
+ end
402
+
403
+ def normalize_table_row(cells, column_count)
404
+ row = cells.first(column_count)
405
+ row << "" while row.length < column_count
406
+ row
407
+ end
408
+
409
+ def table_row_html(cells, tag, alignments)
410
+ content = cells.each_with_index.map do |cell, index|
411
+ align = alignments[index]
412
+ align_attr = align.to_s.empty? ? "" : %( style="text-align: #{h(align)}")
413
+ %(<#{tag}#{align_attr}>#{render_inline_markdown(cell.to_s.strip)}</#{tag}>)
414
+ end.join
415
+
416
+ "<tr>#{content}</tr>"
417
+ end
418
+
419
+ def parse_subfigure_images(body)
420
+ images = []
421
+ index = 0
422
+ source = body.to_s
423
+
424
+ while index < source.length
425
+ if source[index] == "!" && source[index + 1] == "["
426
+ parsed = parse_markdown_image(source, index)
427
+ if parsed
428
+ url, title = split_url_and_title(parsed[:url_and_title])
429
+ images << {
430
+ alt: parsed[:alt].to_s,
431
+ url: url.to_s,
432
+ caption: title.to_s.empty? ? parsed[:alt].to_s : title.to_s,
433
+ attrs: parsed[:attrs]
434
+ }
435
+ index = parsed[:end_idx]
436
+ next
437
+ end
438
+ end
439
+ index += 1
440
+ end
441
+
442
+ images
443
+ end
444
+
445
+ def subfigure_rows(layout, image_count)
446
+ used = []
447
+ rows = []
448
+ parsed_rows = layout.to_s.split("/").map { |row| subfigure_row_tokens(row) }.reject(&:empty?)
449
+
450
+ parsed_rows.each do |tokens|
451
+ row = []
452
+ tokens.each do |token|
453
+ index = panel_index(token)
454
+ index = next_unused_index(image_count, used) if index.nil? || index >= image_count || used.include?(index)
455
+ next if index.nil?
456
+
457
+ used << index
458
+ row << { index: index, label: panel_label(token, index) }
459
+ end
460
+ rows << row if row.any?
461
+ end
462
+
463
+ while used.length < image_count
464
+ row = []
465
+ [4, image_count - used.length].min.times do
466
+ index = next_unused_index(image_count, used)
467
+ break if index.nil?
468
+
469
+ used << index
470
+ row << { index: index, label: PANEL_SEQUENCE[index] || (index + 1).to_s }
471
+ end
472
+ rows << row if row.any?
473
+ end
474
+
475
+ rows
476
+ end
477
+
478
+ def subfigure_row_tokens(row)
479
+ row.to_s.split("+").flat_map do |token|
480
+ value = token.strip
481
+ if value.match?(/\A[a-zA-Z]{2,}\z/)
482
+ value.each_char.map(&:downcase)
483
+ else
484
+ [value]
485
+ end
486
+ end.reject(&:empty?)
487
+ end
488
+
489
+ def panel_index(token)
490
+ value = token.to_s.strip
491
+ return nil if value.empty?
492
+ return value.downcase.ord - "a".ord if value.match?(/\A[a-zA-Z]\z/)
493
+ return value.to_i - 1 if value.match?(/\A\d+\z/)
494
+
495
+ nil
496
+ end
497
+
498
+ def panel_label(token, index)
499
+ value = token.to_s.strip
500
+ return value.downcase if value.match?(/\A[a-zA-Z]\z/)
501
+
502
+ PANEL_SEQUENCE[index] || (index + 1).to_s
503
+ end
504
+
505
+ def next_unused_index(image_count, used)
506
+ (0...image_count).find { |candidate| !used.include?(candidate) }
507
+ end
508
+
509
+ def parse_markdown_image(source, start_index)
510
+ index = start_index + 2
511
+ alt, index = read_balanced(source, index, "[", "]")
512
+ return nil unless alt && source[index - 1] == "]" && source[index] == "("
513
+
514
+ index += 1
515
+ url_and_title, index = read_until_closing_paren(source, index)
516
+ return nil unless url_and_title && source[index] == ")"
517
+
518
+ index += 1
519
+ index = skip_spaces(source, index)
520
+
521
+ attrs_raw = nil
522
+ if source[index] == "{"
523
+ close_index = source.index("}", index)
524
+ return nil unless close_index
525
+
526
+ attrs_raw = source[index..close_index].sub(/\A\{:\s*/, "{").strip
527
+ index = close_index + 1
528
+ end
529
+
530
+ { alt: alt, url_and_title: url_and_title, attrs: attrs_raw, end_idx: index }
531
+ end
532
+
533
+ def read_balanced(source, index, open_char, close_char)
534
+ depth = 1
535
+ start = index
536
+ while index < source.length
537
+ char = source[index]
538
+ if char == "\\"
539
+ index += 2
540
+ next
541
+ elsif char == open_char
542
+ depth += 1
543
+ elsif char == close_char
544
+ depth -= 1
545
+ return [source[start...index], index + 1] if depth.zero?
546
+ end
547
+ index += 1
548
+ end
549
+ [nil, index]
550
+ end
551
+
552
+ def read_until_closing_paren(source, index)
553
+ depth = 1
554
+ start = index
555
+ while index < source.length
556
+ char = source[index]
557
+ if char == "\\"
558
+ index += 2
559
+ next
560
+ elsif char == "("
561
+ depth += 1
562
+ elsif char == ")"
563
+ depth -= 1
564
+ return [source[start...index], index] if depth.zero?
565
+ end
566
+ index += 1
567
+ end
568
+ [nil, index]
569
+ end
570
+
571
+ def skip_spaces(source, index)
572
+ index += 1 while index < source.length && source[index] =~ /[ \t]/
573
+ index
574
+ end
575
+
576
+ def subfigure_panel_attrs(raw)
577
+ styles = []
578
+ width = kramdown_attr_value(raw, "data-subfigure-width") || kramdown_attr_value(raw, "width")
579
+ height = kramdown_attr_value(raw, "data-subfigure-height") || kramdown_attr_value(raw, "height")
580
+ figure_width = kramdown_attr_value(raw, "data-figure-width-web") || kramdown_attr_value(raw, "data-figure-width")
581
+ figure_height = kramdown_attr_value(raw, "data-figure-height-web") || kramdown_attr_value(raw, "data-figure-height")
582
+ inline_style = kramdown_attr_value(raw, "style")
583
+
584
+ styles << "--md-subfigure-width: #{width};" unless width.to_s.strip.empty?
585
+ styles << "--md-subfigure-image-height: #{height};" unless height.to_s.strip.empty?
586
+ styles << "--md-subfigure-image-width: #{figure_width};" unless figure_width.to_s.strip.empty?
587
+ styles << "--md-subfigure-image-max-height: #{figure_height};" unless figure_height.to_s.strip.empty?
588
+ inline_style.to_s.scan(/--md-subfigure-[\w-]+\s*:\s*[^;]+/) do |declaration|
589
+ styles << "#{declaration.strip};"
590
+ end
591
+
592
+ styles.empty? ? "" : %( style="#{h(styles.join(' '))}")
593
+ end
594
+
595
+ def kramdown_attr_value(raw, name)
596
+ source = raw.to_s.strip.sub(/\A\{:\s*/, "").sub(/\A\{\s*/, "").sub(/\s*\}\z/, "")
597
+ match = source.match(/(?:\A|\s)#{Regexp.escape(name)}\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s}]+))/)
598
+ return nil unless match
599
+
600
+ match[1] || match[2] || match[3]
601
+ end
602
+
603
+ def split_url_and_title(value)
604
+ text = value.to_s.strip
605
+ if text =~ /\s+["“]([^"”]+)["”]\s*\z/m
606
+ title = Regexp.last_match(1).strip
607
+ url = text.sub(/\s+["“][^"”]+["”]\s*\z/m, "").strip
608
+ [url, title]
609
+ elsif text =~ /\s+['‘]([^'’]+)['’]\s*\z/m
610
+ title = Regexp.last_match(1).strip
611
+ url = text.sub(/\s+['‘][^'’]+['’]\s*\z/m, "").strip
612
+ [url, title]
613
+ else
614
+ [text, nil]
615
+ end
616
+ end
617
+
618
+ def wrap_html_images(output, lang, label)
619
+ return output unless output.to_s.include?("<img")
620
+
621
+ count = output.scan(/<figure\b[^>]*\bclass=["'][^"']*\bmd-figure\b/i).size
622
+ output.gsub(%r{<p>\s*(<img\b[^>]*>)\s*</p>}mi) do
623
+ img_tag = Regexp.last_match(1)
624
+ next Regexp.last_match(0) if img_tag =~ /\bdata-no-figure\s*=\s*["']true["']/i
625
+
626
+ caption = extract_attr(img_tag, "title").to_s
627
+ caption = extract_attr(img_tag, "alt").to_s if caption.empty?
628
+ count += 1
629
+
630
+ figure_html(
631
+ img: img_tag,
632
+ caption: render_inline_markdown(CGI.unescapeHTML(caption)),
633
+ label: label,
634
+ lang: lang,
635
+ count: count,
636
+ classes: figure_classes_for(extract_attr(img_tag, "src")),
637
+ attrs: figure_container_attrs(img_tag)
638
+ )
639
+ end
640
+ end
641
+
642
+ def figure_html(img:, caption:, label:, lang:, count:, classes: ["md-figure"], attrs: "")
643
+ classes = Array(classes)
644
+ classes = ["md-figure"] if classes.empty?
645
+ clean_caption = caption.to_s.strip
646
+ figcaption = if clean_caption.empty?
647
+ ""
648
+ else
649
+ %(<figcaption class="md-figcaption"><span class="figlabel">#{h(label)} #{count}.</span> #{clean_caption}</figcaption>)
650
+ end
651
+
652
+ <<~HTML.strip
653
+ <figure id="fig-#{h(lang)}-#{count}" class="#{classes.map { |klass| h(klass) }.join(' ')}"#{attrs}>
654
+ #{figcaption}
655
+ <div class="md-figure-inner">#{img}</div>
656
+ </figure>
657
+ HTML
658
+ end
659
+
660
+ def figure_classes_for(path)
661
+ classes = ["md-figure"]
662
+ classes << "mermaid-figure" if diagram_path?(path)
663
+ classes
664
+ end
665
+
666
+ def figure_container_attrs(raw)
667
+ styles = []
668
+ width = kramdown_attr_value(raw, "data-figure-width-web") || kramdown_attr_value(raw, "data-figure-width")
669
+ height = kramdown_attr_value(raw, "data-figure-height-web") || kramdown_attr_value(raw, "data-figure-height")
670
+ unless width.to_s.strip.empty?
671
+ styles << "--md-figure-width: #{width.strip};"
672
+ styles << "--md-figure-image-width: 100%;"
673
+ end
674
+ styles << "--md-figure-height: #{height.strip};" unless height.to_s.strip.empty?
675
+ return "" if styles.empty?
676
+
677
+ %( style="#{h(styles.join(' '))}")
678
+ end
679
+
680
+ def diagram_path?(path)
681
+ path.to_s.match?(/\.(?:mmd|mermaid|puml|plantuml|uml)(?:\.edited)?\.svg(?:[?#].*)?\z/i) ||
682
+ path.to_s.match?(/\.(?:mmd|mermaid|puml|plantuml|uml)(?:[?#].*)?\z/i)
683
+ end
684
+
685
+ def mermaid_path?(path)
686
+ diagram_path?(path)
687
+ end
688
+
689
+ def render_inline_markdown(text)
690
+ html = text.to_s.dup
691
+ code_spans = []
692
+ html.gsub!(/`([^`\n]+)`/) do
693
+ index = code_spans.length
694
+ code_spans << %(<code class="language-plaintext highlighter-rouge">#{h(Regexp.last_match(1))}</code>)
695
+ "\u0000INLINE_CODE_#{index}\u0000"
696
+ end
697
+ math_spans = []
698
+ html.gsub!(/(^|[^\\$])\$(?!\$)((?:\\.|[^\n$\\])+?)\$(?!\$)/) do
699
+ index = math_spans.length
700
+ math_spans << %($#{Regexp.last_match(2)}$)
701
+ "#{Regexp.last_match(1)}\u0000INLINE_MATH_#{index}\u0000"
702
+ end
703
+ html.gsub!(/\[([^\]]+)\]\((\S+?)(?:\s+"([^"]*)")?\)/) do
704
+ label = Regexp.last_match(1)
705
+ url = Regexp.last_match(2)
706
+ title = Regexp.last_match(3)
707
+ title_attr = title.to_s.empty? ? "" : %( title="#{h(title)}")
708
+ %(<a href="#{h(url)}"#{title_attr}>#{label}</a>)
709
+ end
710
+ html.gsub!(/\*\*([^*]+)\*\*/, '<strong>\1</strong>')
711
+ html.gsub!(/\*([^*]+)\*/, '<em>\1</em>')
712
+ math_spans.each_with_index do |math, index|
713
+ html.gsub!("\u0000INLINE_MATH_#{index}\u0000", math)
714
+ end
715
+ code_spans.each_with_index do |code, index|
716
+ html.gsub!("\u0000INLINE_CODE_#{index}\u0000", code)
717
+ end
718
+ html
719
+ end
720
+
721
+ def extract_attr(tag, name)
722
+ tag[/\b#{Regexp.escape(name)}="([^"]*)"/i, 1] || tag[/\b#{Regexp.escape(name)}='([^']*)'/i, 1]
723
+ end
724
+
725
+ def strip_liquid(value)
726
+ value.to_s.gsub(/\{%.*?%\}/m, "").gsub(/\{\{.*?\}\}/m, "").strip
727
+ end
728
+
729
+ def h(value)
730
+ value.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
731
+ end
732
+
733
+ def kramdown_attrs_to_html(raw)
734
+ return "" if raw.to_s.strip.empty?
735
+
736
+ source = raw.to_s.strip.sub(/\A\{:\s*/, "").sub(/\A\{\s*/, "").sub(/\s*\}\z/, "")
737
+ return "" if source.empty?
738
+
739
+ source = source.tr("“”’‘", %q{""''})
740
+ classes = []
741
+ id = nil
742
+ other = []
743
+ source.scan(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/).each do |token|
744
+ if token.start_with?(".")
745
+ classes << token[1..]
746
+ elsif token.start_with?("#")
747
+ id = token[1..]
748
+ else
749
+ other << token
750
+ end
751
+ end
752
+
753
+ attrs = []
754
+ attrs << %(id="#{h(id)}") if id && !id.empty?
755
+ attrs << %(class="#{h(classes.join(" "))}") if classes.any?
756
+ attrs.concat(other)
757
+ attrs.join(" ")
758
+ end
759
+ end
760
+ end
761
+
762
+ class UnaltrawebFigureCaptionGenerator < Jekyll::Generator
763
+ safe true
764
+ priority :low
765
+
766
+ def generate(site)
767
+ return unless Unaltraweb::FigureCaptions.enabled?(site)
768
+
769
+ Unaltraweb::FigureCaptions.collections(site).each do |collection_label|
770
+ collection = site.collections[collection_label]
771
+ next unless collection
772
+
773
+ collection.docs.each do |doc|
774
+ lang = Unaltraweb::FigureCaptions.detect_lang(doc)
775
+ figure_label = Unaltraweb::FigureCaptions.label_for(site, lang)
776
+ table_label = Unaltraweb::FigureCaptions.table_label_for(site, lang)
777
+ listing_label = Unaltraweb::FigureCaptions.listing_label_for(site, lang)
778
+ doc.content = Unaltraweb::FigureCaptions.transform_markdown_sugar(doc.content, lang, figure_label, table_label, listing_label)
779
+ end
780
+ end
781
+ end
782
+ end
783
+
784
+ Jekyll::Hooks.register :documents, :post_render do |doc|
785
+ next unless doc.collection && Unaltraweb::FigureCaptions.process_collection?(doc.site, doc.collection.label)
786
+
787
+ lang = Unaltraweb::FigureCaptions.detect_lang(doc)
788
+ label = Unaltraweb::FigureCaptions.label_for(doc.site, lang)
789
+ doc.output = Unaltraweb::FigureCaptions.wrap_html_images(doc.output, lang, label)
790
+ end
791
+
792
+ Jekyll::Hooks.register :pages, :pre_render do |page|
793
+ next unless Unaltraweb::FigureCaptions.process_page?(page)
794
+
795
+ lang = Unaltraweb::FigureCaptions.detect_lang(page)
796
+ figure_label = Unaltraweb::FigureCaptions.label_for(page.site, lang)
797
+ table_label = Unaltraweb::FigureCaptions.table_label_for(page.site, lang)
798
+ listing_label = Unaltraweb::FigureCaptions.listing_label_for(page.site, lang)
799
+ page.content = Unaltraweb::FigureCaptions.transform_markdown_sugar(page.content, lang, figure_label, table_label, listing_label)
800
+ end