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