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,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("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
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
|