pandorga 1.2.1
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/CHANGELOG.md +77 -0
- data/DESIGN.md +1434 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/_data/themes/dark.yml +97 -0
- data/_data/themes/light.yml +120 -0
- data/_data/themes/shared.yml +37 -0
- data/_data/translations.yml +85 -0
- data/_includes/chord-tab.html +38 -0
- data/_includes/content-runtime/00-config.html +22 -0
- data/_includes/content-runtime/10-utils.html +176 -0
- data/_includes/content-runtime/20-markdown.html +264 -0
- data/_includes/content-runtime/25-code-blocks.html +315 -0
- data/_includes/content-runtime/30-includes-bib.html +190 -0
- data/_includes/content-runtime/30-includes-xcite.html +122 -0
- data/_includes/content-runtime/30-includes.html +365 -0
- data/_includes/content-runtime/40-listing.html +225 -0
- data/_includes/content-runtime/41-listing-helpers.html +71 -0
- data/_includes/content-runtime/42-listing-filter.html +208 -0
- data/_includes/content-runtime/43-blocks.html +379 -0
- data/_includes/content-runtime/50-detail.html +861 -0
- data/_includes/content-runtime/55-toc.html +176 -0
- data/_includes/content-runtime/60-fragments.html +567 -0
- data/_includes/content-runtime/90-bootstrap.html +76 -0
- data/_includes/cut-in.html +10 -0
- data/_includes/cv/cv-box.html +18 -0
- data/_includes/dict-card.html +24 -0
- data/_includes/figure.html +36 -0
- data/_includes/home/band-index.html +43 -0
- data/_includes/home/band-listing.html +49 -0
- data/_includes/mathjax.html +10 -0
- data/_includes/ongoing-product.html +1 -0
- data/_includes/page/content-runtime.html +20 -0
- data/_includes/page/date_evolution.html +19 -0
- data/_includes/page/figure-lightbox.html +102 -0
- data/_includes/page/formatted_date.html +15 -0
- data/_includes/page/hero-default.html +18 -0
- data/_includes/page/listing-runtime-attrs.html +8 -0
- data/_includes/page/mermaid.html +101 -0
- data/_includes/page/network-graph.html +1253 -0
- data/_includes/page/page-header.html +41 -0
- data/_includes/page/professional-connections.html +2 -0
- data/_includes/page/site-footer.html +101 -0
- data/_includes/page/timeline-section.html +868 -0
- data/_includes/plotly.html +22 -0
- data/_includes/theme/dev-reexport.html +100 -0
- data/_includes/theme/favicons.html +4 -0
- data/_includes/theme/font-loader.html +55 -0
- data/_includes/theme/shell-theme-boot.html +115 -0
- data/_includes/theme/site-logo.html +6 -0
- data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
- data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
- data/_includes/theme/theme-context.liquid +20 -0
- data/_includes/theme/theme-toggle.html +94 -0
- data/_includes/theme/theme-vars-block.liquid +161 -0
- data/_includes/theme/theme-vars.html +16 -0
- data/_includes/transcription.html +80 -0
- data/_includes/tweet.html +22 -0
- data/_includes/video.html +11 -0
- data/_includes/xcite.html +3 -0
- data/_includes/youtube-short.html +26 -0
- data/_includes/youtube.html +12 -0
- data/_layouts/article.html +27 -0
- data/_layouts/articles.html +534 -0
- data/_layouts/bibliography.html +417 -0
- data/_layouts/blog.html +544 -0
- data/_layouts/cv.html +393 -0
- data/_layouts/home.html +319 -0
- data/_layouts/network.html +9 -0
- data/_layouts/pandorga/articles.html +4 -0
- data/_layouts/pandorga/bibliography.html +4 -0
- data/_layouts/pandorga/blog.html +4 -0
- data/_layouts/pandorga/cv.html +4 -0
- data/_layouts/pandorga/media.html +4 -0
- data/_layouts/pandorga/network.html +4 -0
- data/_layouts/pandorga/portfolio.html +4 -0
- data/_layouts/post.html +7 -0
- data/_layouts/product.html +5 -0
- data/_layouts/project.html +27 -0
- data/_layouts/projects.html +534 -0
- data/_layouts/resources.html +387 -0
- data/_layouts/shell.html +203 -0
- data/_layouts/text.html +246 -0
- data/_plugins/bibliography_artifact.rb +57 -0
- data/_plugins/content_backend.rb +37 -0
- data/_plugins/content_collections.rb +51 -0
- data/_plugins/content_detail_paths.rb +58 -0
- data/_plugins/content_headers.rb +73 -0
- data/_plugins/content_keys.rb +310 -0
- data/_plugins/content_local_serve.rb +168 -0
- data/_plugins/content_site_adapter.rb +159 -0
- data/_plugins/content_validators_hook.rb +20 -0
- data/_plugins/content_writing_index.rb +52 -0
- data/_plugins/content_writing_paths.rb +55 -0
- data/_plugins/force_utf8_encoding.rb +13 -0
- data/_plugins/git_chronology.rb +211 -0
- data/_plugins/git_metadata.rb +43 -0
- data/_plugins/job_sections.rb +25 -0
- data/_plugins/lib/content_validators.rb +27 -0
- data/_plugins/lib/resource_validator.rb +56 -0
- data/_plugins/lib/tag_validator.rb +153 -0
- data/_plugins/lib/text_excerpt.rb +56 -0
- data/_plugins/lib/writing_entry.rb +107 -0
- data/_plugins/local_detail_rewrites.rb +194 -0
- data/_plugins/timeline.rb +507 -0
- data/_plugins/writing_slug_limits.rb +70 -0
- data/assets/css/article.css +1330 -0
- data/assets/css/base.css +1432 -0
- data/assets/css/cv.css +13 -0
- data/assets/css/global-link.css +94 -0
- data/assets/css/ledger/bibliography.css +307 -0
- data/assets/css/ledger/chrome.css +118 -0
- data/assets/css/ledger/cv.css +836 -0
- data/assets/css/ledger/detail.css +1270 -0
- data/assets/css/ledger/footer.css +298 -0
- data/assets/css/ledger/home.css +1490 -0
- data/assets/css/ledger/network.css +106 -0
- data/assets/css/ledger/projects.css +716 -0
- data/assets/css/ledger/rascunhos.css +379 -0
- data/assets/css/ledger/sources.css +327 -0
- data/assets/css/ledger/writing.css +754 -0
- data/assets/tailwind-play.js +15 -0
- data/examples/full/Gemfile +7 -0
- data/examples/full/_config.yml +92 -0
- data/examples/full/_redirects +3 -0
- data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
- data/examples/full/content/collections/data/contacts.yml +3 -0
- data/examples/full/content/collections/data/education.yml +4 -0
- data/examples/full/content/collections/jobs/research-fellow.md +12 -0
- data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
- data/examples/full/content/collections/products/ledger-tool.md +6 -0
- data/examples/full/content/collections/projects/demo-kit.md +10 -0
- data/examples/full/content/collections/resources/style-guide.md +7 -0
- data/examples/full/content/pages/cv/summary.md +1 -0
- data/examples/full/content/pages/headers.yml +20 -0
- data/examples/full/index.html +4 -0
- data/examples/minimal/Gemfile +7 -0
- data/examples/minimal/_config.yml +47 -0
- data/examples/minimal/_redirects +3 -0
- data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
- data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
- data/examples/minimal/content/pages/headers.yml +6 -0
- data/examples/minimal/index.html +4 -0
- data/exe/pandorga +7 -0
- data/functions/api/studio/[[path]].js +562 -0
- data/functions/api/studio/_lib/auth.js +131 -0
- data/functions/api/studio/_lib/blocks.js +156 -0
- data/functions/api/studio/_lib/gemini.js +62 -0
- data/functions/api/studio/_lib/github.js +278 -0
- data/functions/api/studio/_lib/guidelines.js +143 -0
- data/functions/api/studio/_lib/paths.js +76 -0
- data/functions/api/studio/_lib/prompts.js +165 -0
- data/functions/api/studio/_lib/rate-limit.js +40 -0
- data/functions/api/studio/_lib/rewrite.js +136 -0
- data/functions/api/studio/_lib/sanitize.js +143 -0
- data/lib/pandorga/cli.rb +66 -0
- data/lib/pandorga/commands/doctor.rb +66 -0
- data/lib/pandorga/commands/export.rb +30 -0
- data/lib/pandorga/commands/install_functions.rb +41 -0
- data/lib/pandorga/commands/new.rb +113 -0
- data/lib/pandorga/commands/publish.rb +53 -0
- data/lib/pandorga/commands/serve.rb +34 -0
- data/lib/pandorga/commands/studio_schema.rb +309 -0
- data/lib/pandorga/jekyll/generator.rb +74 -0
- data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
- data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
- data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
- data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
- data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
- data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
- data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
- data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
- data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
- data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
- data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
- data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
- data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
- data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
- data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
- data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
- data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
- data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
- data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
- data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
- data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
- data/lib/pandorga/jekyll.rb +15 -0
- data/lib/pandorga/object_store_env.rb +51 -0
- data/lib/pandorga/registry/schema.yml +115 -0
- data/lib/pandorga/registry.rb +279 -0
- data/lib/pandorga/version.rb +5 -0
- data/lib/pandorga.rb +19 -0
- data/scripts/cdn-allowlist.json +19 -0
- data/scripts/content/backfill-content-keys.rb +99 -0
- data/scripts/content/band_art.rb +362 -0
- data/scripts/content/export-content-json.rb +741 -0
- data/scripts/content/flatten-ansi-capture.mjs +153 -0
- data/scripts/content/hero_portraits.rb +28 -0
- data/scripts/content/publish-content-to-object-store.sh +78 -0
- data/scripts/content/publish-content-to-r2.sh +5 -0
- data/scripts/content/sync-content-json-to-r2.rb +167 -0
- data/scripts/content/sync-content-redirects.rb +12 -0
- data/scripts/generate-configuration-doc.rb +127 -0
- data/scripts/lib/content_license.rb +50 -0
- data/scripts/lib/content_storage_config.rb +70 -0
- data/scripts/lib/normalize-display-math.mjs +52 -0
- data/scripts/lib/r2_json_sync.rb +76 -0
- data/scripts/lib/withdrawn_public_objects.rb +23 -0
- data/scripts/lib/writing_index_contract.rb +61 -0
- data/scripts/pdf/md-to-pdf.mjs +1400 -0
- data/scripts/pdf/md-to-pdf.sh +27 -0
- data/scripts/test/fixtures/dual-template/_config.yml +32 -0
- data/scripts/test/fixtures/dual-template/index.html +4 -0
- data/scripts/test/render-parity/article-note.json +5 -0
- data/scripts/test/render-parity/blockquote.json +5 -0
- data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
- data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
- data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
- data/scripts/test/render-parity/blocks/archive-card.json +32 -0
- data/scripts/test/render-parity/blocks/cta-link.json +13 -0
- data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
- data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
- data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
- data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
- data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
- data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
- data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
- data/scripts/test/render-parity/blocks/section-banner.json +13 -0
- data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
- data/scripts/test/render-parity/blocks/system-card.json +48 -0
- data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
- data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
- data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
- data/scripts/test/render-parity/cut-in-right.json +9 -0
- data/scripts/test/render-parity/cut-in.json +8 -0
- data/scripts/test/render-parity/figure.json +5 -0
- data/scripts/test/render-parity/load-runtime.mjs +337 -0
- data/scripts/test/render-parity/ongoing-product.json +5 -0
- data/scripts/test/render-parity/plotly.json +5 -0
- data/scripts/test/render-parity/poem.json +5 -0
- data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
- data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
- data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
- data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
- data/scripts/test/render-parity/run-fixture.mjs +159 -0
- data/scripts/test/render-parity/selectors.mjs +43 -0
- data/scripts/test/render-parity/video.json +5 -0
- data/scripts/test/render-parity/youtube-short.json +10 -0
- data/scripts/test/render-parity/youtube.json +8 -0
- data/scripts/test/test-absent-field-absent-element.rb +43 -0
- data/scripts/test/test-cdn-pins.rb +44 -0
- data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
- data/scripts/test/test-configuration-doc.rb +36 -0
- data/scripts/test/test-content-detail-urls.rb +346 -0
- data/scripts/test/test-content-keys.rb +110 -0
- data/scripts/test/test-content-license.rb +45 -0
- data/scripts/test/test-content-render-parity.rb +38 -0
- data/scripts/test/test-content-validators.rb +12 -0
- data/scripts/test/test-css-structure.rb +72 -0
- data/scripts/test/test-cv-export-contract.rb +158 -0
- data/scripts/test/test-detail-render-parity.rb +40 -0
- data/scripts/test/test-dev-reexport.rb +122 -0
- data/scripts/test/test-doctor.rb +39 -0
- data/scripts/test/test-dual-template-instances.rb +87 -0
- data/scripts/test/test-dual-template-runtime.rb +167 -0
- data/scripts/test/test-examples-build.rb +60 -0
- data/scripts/test/test-export-content-json-incremental.rb +138 -0
- data/scripts/test/test-export.rb +53 -0
- data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
- data/scripts/test/test-form-control-theming.rb +143 -0
- data/scripts/test/test-git-chronology.rb +414 -0
- data/scripts/test/test-hero-portraits.mjs +97 -0
- data/scripts/test/test-hero-portraits.rb +162 -0
- data/scripts/test/test-home-band-art.rb +135 -0
- data/scripts/test/test-install-functions.rb +39 -0
- data/scripts/test/test-language-export.rb +79 -0
- data/scripts/test/test-listing-filter-contract.rb +119 -0
- data/scripts/test/test-math-row-breaks.mjs +79 -0
- data/scripts/test/test-math-row-breaks.rb +18 -0
- data/scripts/test/test-new.rb +113 -0
- data/scripts/test/test-no-personal-data.rb +85 -0
- data/scripts/test/test-public-cv-export.rb +164 -0
- data/scripts/test/test-r2-config-consistency.rb +57 -0
- data/scripts/test/test-r2-json-sync.rb +193 -0
- data/scripts/test/test-registry-home.rb +34 -0
- data/scripts/test/test-runtime-js-syntax.rb +64 -0
- data/scripts/test/test-slim-writing-index.rb +126 -0
- data/scripts/test/test-studio-api-contract.rb +99 -0
- data/scripts/test/test-studio-content-files.rb +79 -0
- data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
- data/scripts/test/test-studio-rate-limit.mjs +70 -0
- data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
- data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
- data/scripts/test/test-studio-schema-composition.rb +126 -0
- data/scripts/test/test-studio-slug-config.rb +167 -0
- data/scripts/test/test-template-packages.rb +51 -0
- data/scripts/test/test-text-excerpt.rb +51 -0
- data/scripts/test/test-unwrap-media-src.mjs +39 -0
- data/scripts/test/test-unwrap-media-src.rb +19 -0
- data/scripts/test/test-withdrawn-content-export.rb +69 -0
- data/scripts/test/test-writing-index-export-slugs.rb +84 -0
- data/scripts/test/test-writing-slug-limits.rb +61 -0
- data/scripts/test/test-xcite-chronology.rb +106 -0
- data/scripts/validate.sh +74 -0
- data/scripts/visual/build-cv-pages-json.js +33 -0
- data/scripts/visual/build-site-pages-json.js +94 -0
- data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
- data/scripts/visual/take-screenshots.js +88 -0
- data/scripts/visual/visual-inspection-pages.json +113 -0
- data/studio/assets/index-B_j0pMD5.css +1 -0
- data/studio/assets/index-Ch4YEv9g.js +285 -0
- data/studio/index.html +27 -0
- data/studio/schema.yml +869 -0
- data/studio-app/README.md +27 -0
- data/studio-app/index.html +26 -0
- data/studio-app/package-lock.json +2499 -0
- data/studio-app/package.json +26 -0
- data/studio-app/skills-lock.json +47 -0
- data/studio-app/src/api/client.js +71 -0
- data/studio-app/src/editor/body-editor.js +70 -0
- data/studio-app/src/editor/liquid-core.js +382 -0
- data/studio-app/src/editor/liquid.js +77 -0
- data/studio-app/src/lib/document.js +207 -0
- data/studio-app/src/lib/list-controls.js +317 -0
- data/studio-app/src/lib/schema.js +66 -0
- data/studio-app/src/lib/yaml-doc.js +100 -0
- data/studio-app/src/main.js +3693 -0
- data/studio-app/src/media/browser.js +578 -0
- data/studio-app/src/preview/render.js +430 -0
- data/studio-app/src/styles/studio.css +2826 -0
- data/studio-app/vite.config.js +25 -0
- data/templates/articles/README.md +40 -0
- data/templates/articles/fixtures/article.md +9 -0
- data/templates/articles/screenshots/README.md +6 -0
- data/templates/articles/studio.yml +73 -0
- data/templates/articles/template.yml +21 -0
- data/templates/bibliography/README.md +41 -0
- data/templates/bibliography/fixtures/references.json +9 -0
- data/templates/bibliography/screenshots/README.md +6 -0
- data/templates/bibliography/studio.yml +66 -0
- data/templates/bibliography/template.yml +18 -0
- data/templates/blog/README.md +38 -0
- data/templates/blog/fixtures/post.md +9 -0
- data/templates/blog/screenshots/README.md +6 -0
- data/templates/blog/studio.yml +69 -0
- data/templates/blog/template.yml +20 -0
- data/templates/cv/README.md +34 -0
- data/templates/cv/fixtures/job.md +12 -0
- data/templates/cv/screenshots/README.md +6 -0
- data/templates/cv/studio.yml +200 -0
- data/templates/cv/template.yml +25 -0
- data/templates/media/README.md +38 -0
- data/templates/media/fixtures/resource.md +7 -0
- data/templates/media/screenshots/README.md +6 -0
- data/templates/media/studio.yml +58 -0
- data/templates/media/template.yml +17 -0
- data/templates/network/README.md +34 -0
- data/templates/network/fixtures/sources.json +3 -0
- data/templates/network/screenshots/README.md +6 -0
- data/templates/network/studio.yml +28 -0
- data/templates/network/template.yml +10 -0
- data/templates/portfolio/README.md +39 -0
- data/templates/portfolio/fixtures/project.md +10 -0
- data/templates/portfolio/screenshots/README.md +6 -0
- data/templates/portfolio/studio.yml +89 -0
- data/templates/portfolio/template.yml +18 -0
- metadata +505 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Objects taken down for copyright / unpublished drafts must not re-enter the
|
|
5
|
+
# public export. The R2 sync deletes remote keys that are absent locally, so
|
|
6
|
+
# this list is also the prune set for a clean publish.
|
|
7
|
+
|
|
8
|
+
$stdout.sync = true
|
|
9
|
+
$stderr.sync = true
|
|
10
|
+
|
|
11
|
+
require "json"
|
|
12
|
+
require "open3"
|
|
13
|
+
require "pathname"
|
|
14
|
+
require "tmpdir"
|
|
15
|
+
|
|
16
|
+
require_relative "../lib/withdrawn_public_objects"
|
|
17
|
+
|
|
18
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
19
|
+
EXPORT_SCRIPT = REPO_ROOT.join("scripts/content/export-content-json.rb")
|
|
20
|
+
|
|
21
|
+
WITHDRAWN_EXPORT_PATHS = WithdrawnPublicObjects::EXPORT_PATHS
|
|
22
|
+
STALE_PUBLIC_PHRASES = WithdrawnPublicObjects::STALE_PHRASES
|
|
23
|
+
|
|
24
|
+
def fail!(message)
|
|
25
|
+
warn message
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check!(condition, message)
|
|
30
|
+
fail!(message) unless condition
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check_export!(out)
|
|
34
|
+
manifest = JSON.parse(out.join("manifest.json").read)
|
|
35
|
+
listed = Array(manifest["files"]).flat_map { |_key, paths| Array(paths) }
|
|
36
|
+
|
|
37
|
+
WITHDRAWN_EXPORT_PATHS.each do |relative|
|
|
38
|
+
check!(!out.join(relative).exist?, "export wrote withdrawn object #{relative}")
|
|
39
|
+
check!(!listed.include?(relative), "manifest lists withdrawn object #{relative}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
export_text = out.glob("**/*.json").map(&:read).join("\n")
|
|
43
|
+
STALE_PUBLIC_PHRASES.each do |phrase|
|
|
44
|
+
check!(!export_text.include?(phrase), "export still contains withdrawn phrase #{phrase.inspect}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
puts "Testing withdrawn objects stay out of the public export..."
|
|
49
|
+
|
|
50
|
+
existing = ARGV[0]
|
|
51
|
+
if existing
|
|
52
|
+
out = Pathname.new(existing).expand_path
|
|
53
|
+
check!(out.directory?, "export dir missing: #{out}")
|
|
54
|
+
check_export!(out)
|
|
55
|
+
else
|
|
56
|
+
Dir.mktmpdir("withdrawn-content-export-") do |dir|
|
|
57
|
+
out = Pathname.new(dir).join("export")
|
|
58
|
+
stdout, stderr, status = Open3.capture3(
|
|
59
|
+
{ "CONTENT_API_BASE_URL" => "" },
|
|
60
|
+
"ruby", EXPORT_SCRIPT.to_s, "content", out.to_s,
|
|
61
|
+
chdir: REPO_ROOT.to_s
|
|
62
|
+
)
|
|
63
|
+
fail!("export failed:\n#{stderr}\n#{stdout}") unless status.success?
|
|
64
|
+
|
|
65
|
+
check_export!(out)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
puts "Withdrawn content export OK."
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# writing_index slugs must point at exported JSON (08-23 vs 08-22 class of bug).
|
|
5
|
+
|
|
6
|
+
$stdout.sync = true
|
|
7
|
+
$stderr.sync = true
|
|
8
|
+
|
|
9
|
+
require "fileutils"
|
|
10
|
+
require "json"
|
|
11
|
+
require "pathname"
|
|
12
|
+
require "tmpdir"
|
|
13
|
+
|
|
14
|
+
require_relative "../lib/writing_index_contract"
|
|
15
|
+
|
|
16
|
+
def fail!(message)
|
|
17
|
+
warn message
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def check!(condition, message)
|
|
22
|
+
fail!(message) unless condition
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts "Testing writing_index ↔ export slug contract..."
|
|
26
|
+
|
|
27
|
+
Dir.mktmpdir("writing-index-slugs-") do |dir|
|
|
28
|
+
root = Pathname.new(dir)
|
|
29
|
+
FileUtils.mkdir_p(root.join("collections/articles"))
|
|
30
|
+
FileUtils.mkdir_p(root.join("data"))
|
|
31
|
+
|
|
32
|
+
root.join("collections/articles/2026-08-22-freshy-cooling-map.json").write(<<~JSON)
|
|
33
|
+
{"type":"article","slug":"2026-08-22-freshy-cooling-map","key":"freshy-cooling-map"}
|
|
34
|
+
JSON
|
|
35
|
+
|
|
36
|
+
matching = [
|
|
37
|
+
{
|
|
38
|
+
"slug" => "2026-08-22-freshy-cooling-map",
|
|
39
|
+
"key" => "freshy-cooling-map",
|
|
40
|
+
"type" => "articles",
|
|
41
|
+
"collection" => "articles"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
check!(WritingIndexContract.mismatches(root, matching).empty?,
|
|
45
|
+
"matching 08-22 slug must pass")
|
|
46
|
+
|
|
47
|
+
stale = [
|
|
48
|
+
{
|
|
49
|
+
"slug" => "2026-08-23-freshy-cooling-map",
|
|
50
|
+
"key" => "freshy-cooling-map",
|
|
51
|
+
"type" => "articles",
|
|
52
|
+
"collection" => "articles"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
errors = WritingIndexContract.mismatches(root, stale)
|
|
56
|
+
check!(!errors.empty?, "08-23 index vs 08-22 file must fail")
|
|
57
|
+
check!(errors.any? { |line| line.include?("2026-08-23-freshy-cooling-map") },
|
|
58
|
+
"mismatch should name the bad slug (#{errors.inspect})")
|
|
59
|
+
|
|
60
|
+
root.join("data/writing_index.json").write(JSON.pretty_generate(stale))
|
|
61
|
+
begin
|
|
62
|
+
WritingIndexContract.assert_slugs_match_export!(root)
|
|
63
|
+
fail!("assert_slugs_match_export! should raise on 08-23 vs 08-22")
|
|
64
|
+
rescue RuntimeError => e
|
|
65
|
+
check!(e.message.include?("2026-08-23-freshy-cooling-map"),
|
|
66
|
+
"assertion message should include the stale slug")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
puts "writing_index export slug contract OK"
|
|
71
|
+
|
|
72
|
+
existing = ARGV[0]
|
|
73
|
+
if existing
|
|
74
|
+
out = Pathname.new(existing).expand_path
|
|
75
|
+
check!(out.directory?, "export dir missing: #{out}")
|
|
76
|
+
WritingIndexContract.assert_slugs_match_export!(out)
|
|
77
|
+
index = JSON.parse(out.join("data/writing_index.json").read)
|
|
78
|
+
freshy = index.find { |entry| entry["key"] == "freshy-cooling-map" }
|
|
79
|
+
if freshy
|
|
80
|
+
check!(out.join("collections/articles/#{freshy['slug']}.json").file?,
|
|
81
|
+
"Freshy writing_index slug must match collections/articles/#{freshy['slug']}.json")
|
|
82
|
+
end
|
|
83
|
+
puts "writing_index slugs match #{out}"
|
|
84
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
require_relative "../../_plugins/writing_slug_limits"
|
|
9
|
+
|
|
10
|
+
def check!(condition, message = "condition was false")
|
|
11
|
+
raise "Assertion failed: #{message}" unless condition
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def assert_equal(expected, actual, message = nil)
|
|
15
|
+
return if expected == actual
|
|
16
|
+
|
|
17
|
+
label = message || "expected #{expected.inspect}, got #{actual.inspect}"
|
|
18
|
+
raise "Assertion failed: #{label}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def with_temp_writing_tree
|
|
22
|
+
Dir.mktmpdir("writing-slug-limits") do |tmpdir|
|
|
23
|
+
root = Pathname.new(tmpdir)
|
|
24
|
+
yield root
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "Testing WritingSlugLimits..."
|
|
29
|
+
|
|
30
|
+
check! WritingSlugLimits.ascii_slug?("2026-06-26-escarnio")
|
|
31
|
+
check! !WritingSlugLimits.ascii_slug?("2026-06-26-escárnio"), "accented slug should not be ASCII"
|
|
32
|
+
|
|
33
|
+
assert_equal "2026-06-26-escarnio", WritingSlugLimits.transliterate_slug("2026-06-26-escárnio")
|
|
34
|
+
assert_equal "2026-04-16-otimismo-na-ciencia", WritingSlugLimits.transliterate_slug("2026-04-16-otimismo-na-ciência")
|
|
35
|
+
assert_equal "2026-06-24-por-que-eu-sai-do-mundo-do-drone", WritingSlugLimits.transliterate_slug("2026-06-24-por-que-eu-saí-do-mundo-do-drone")
|
|
36
|
+
|
|
37
|
+
with_temp_writing_tree do |root|
|
|
38
|
+
posts_dir = root.join("content/collections/posts")
|
|
39
|
+
FileUtils.mkdir_p(posts_dir)
|
|
40
|
+
File.write(posts_dir.join("2026-06-26-escárnio.md"), "---\ntitle: Test\n---\n")
|
|
41
|
+
|
|
42
|
+
errors = WritingSlugLimits.collect_errors(root)
|
|
43
|
+
check! errors.any? { |error| error.include?("non-ASCII") }, "expected non-ASCII slug error"
|
|
44
|
+
check! errors.any? { |error| error.include?("2026-06-26-escarnio") }, "expected transliterated suggestion"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
with_temp_writing_tree do |root|
|
|
48
|
+
posts_dir = root.join("content/collections/posts")
|
|
49
|
+
FileUtils.mkdir_p(posts_dir)
|
|
50
|
+
long_slug = "#{'a' * 45}"
|
|
51
|
+
File.write(posts_dir.join("#{long_slug}.md"), "---\ntitle: Test\n---\n")
|
|
52
|
+
|
|
53
|
+
errors = WritingSlugLimits.collect_errors(root)
|
|
54
|
+
check! errors.any? { |error| error.include?("chars (max #{WritingSlugLimits::MAX_LENGTH})") },
|
|
55
|
+
"expected length error"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
repo_errors = WritingSlugLimits.collect_errors(Pathname.new(__dir__).join("..", ".."))
|
|
59
|
+
check! repo_errors.empty?, "repository slug validation failed:\n - #{repo_errors.join("\n - ")}"
|
|
60
|
+
|
|
61
|
+
puts "All WritingSlugLimits tests passed."
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Gate: article/post xcite targets must not be editorially newer than the source.
|
|
5
|
+
# Projects and products may be cited from any dated piece. Same-day companions
|
|
6
|
+
# (EN/PT pairs) may cite each other.
|
|
7
|
+
|
|
8
|
+
require "date"
|
|
9
|
+
require "pathname"
|
|
10
|
+
require "yaml"
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
13
|
+
XCITE_RE = /\{%-?\s*include\s+xcite\.html\s+([^%]+?)-?%\}/i
|
|
14
|
+
KEY_ATTR_RE = /key\s*=\s*["']([^"']+)["']/i
|
|
15
|
+
DATED_KINDS = %w[articles posts].freeze
|
|
16
|
+
FREE_KINDS = %w[projects products].freeze
|
|
17
|
+
|
|
18
|
+
def check!(condition, message)
|
|
19
|
+
raise message unless condition
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def load_front_matter(path)
|
|
23
|
+
text = path.read
|
|
24
|
+
return [{}, ""] unless text.start_with?("---")
|
|
25
|
+
|
|
26
|
+
parts = text.split("---", 3)
|
|
27
|
+
return [{}, text] if parts.length < 3
|
|
28
|
+
|
|
29
|
+
[YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {}, parts[2]]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def editorial_date(path, front_matter)
|
|
33
|
+
raw = front_matter["date"]
|
|
34
|
+
case raw
|
|
35
|
+
when Date then return raw.iso8601
|
|
36
|
+
when Time then return raw.to_date.iso8601
|
|
37
|
+
when String
|
|
38
|
+
day = raw.strip[0, 10]
|
|
39
|
+
return day if day.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
stem = path.basename.to_s
|
|
43
|
+
return Regexp.last_match(1) if stem =~ /\A(\d{4}-\d{2}-\d{2})-/
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def infer_key(path, front_matter, kind)
|
|
49
|
+
key = front_matter["key"].to_s.strip
|
|
50
|
+
return key unless key.empty?
|
|
51
|
+
|
|
52
|
+
stem = path.basename(".md").to_s
|
|
53
|
+
return stem.sub(/\A\d{4}-\d{2}-\d{2}-/, "") if DATED_KINDS.include?(kind)
|
|
54
|
+
|
|
55
|
+
stem
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "Testing xcite chronology (newer texts cite older texts; projects/products free)..."
|
|
59
|
+
|
|
60
|
+
entries = {}
|
|
61
|
+
(DATED_KINDS + FREE_KINDS).each do |kind|
|
|
62
|
+
dir = REPO_ROOT.join("content/collections", kind)
|
|
63
|
+
next unless dir.directory?
|
|
64
|
+
|
|
65
|
+
dir.glob("*.md").each do |path|
|
|
66
|
+
front_matter, body = load_front_matter(path)
|
|
67
|
+
key = infer_key(path, front_matter, kind)
|
|
68
|
+
check!(!key.empty?, "missing key for #{path.relative_path_from(REPO_ROOT)}")
|
|
69
|
+
|
|
70
|
+
entries[key] = {
|
|
71
|
+
"kind" => kind,
|
|
72
|
+
"path" => path.relative_path_from(REPO_ROOT).to_s,
|
|
73
|
+
"date" => editorial_date(path, front_matter),
|
|
74
|
+
"body" => body
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
violations = []
|
|
80
|
+
DATED_KINDS.each do |kind|
|
|
81
|
+
entries.each_value do |source|
|
|
82
|
+
next unless source["kind"] == kind
|
|
83
|
+
|
|
84
|
+
source["body"].scan(XCITE_RE).flatten.each do |attrs|
|
|
85
|
+
match = KEY_ATTR_RE.match(attrs)
|
|
86
|
+
next unless match
|
|
87
|
+
|
|
88
|
+
target_key = match[1]
|
|
89
|
+
target = entries[target_key]
|
|
90
|
+
check!(!target.nil?, "unknown xcite key #{target_key.inspect} in #{source['path']}")
|
|
91
|
+
next if FREE_KINDS.include?(target["kind"])
|
|
92
|
+
next unless DATED_KINDS.include?(target["kind"])
|
|
93
|
+
|
|
94
|
+
src_date = source["date"]
|
|
95
|
+
tgt_date = target["date"]
|
|
96
|
+
check!(!src_date.to_s.empty?, "missing editorial date for #{source['path']}")
|
|
97
|
+
check!(!tgt_date.to_s.empty?, "missing editorial date for #{target['path']}")
|
|
98
|
+
next if src_date >= tgt_date
|
|
99
|
+
|
|
100
|
+
violations << "#{source['path']} (#{src_date}) cites newer #{target['path']} (#{tgt_date}) via key=#{target_key}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
check!(violations.empty?, "xcite chronology violations:\n #{violations.join("\n ")}")
|
|
106
|
+
puts "All xcite chronology checks passed (#{entries.size} keyed entries)."
|
data/scripts/validate.sh
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Platform gates that run against the gem / fixtures / examples.
|
|
3
|
+
# Content-corpus gates stay on the consuming site (website).
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
cd "$ROOT"
|
|
8
|
+
|
|
9
|
+
echo "== pandorga validate =="
|
|
10
|
+
|
|
11
|
+
# Prefer Bundler so gemspec deps (jekyll, …) resolve the same way in CI and
|
|
12
|
+
# local checkouts. Plain `ruby` only sees the system/mise gem path.
|
|
13
|
+
if [[ -f "${ROOT}/Gemfile.lock" ]] && command -v bundle >/dev/null 2>&1; then
|
|
14
|
+
if command -v mise >/dev/null 2>&1; then
|
|
15
|
+
RUBY=(mise exec -- bundle exec ruby)
|
|
16
|
+
else
|
|
17
|
+
RUBY=(bundle exec ruby)
|
|
18
|
+
fi
|
|
19
|
+
elif command -v mise >/dev/null 2>&1; then
|
|
20
|
+
RUBY=(mise exec -- ruby)
|
|
21
|
+
else
|
|
22
|
+
RUBY=(ruby)
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
export RUBYOPT="-I${ROOT}/lib${RUBYOPT:+ $RUBYOPT}"
|
|
26
|
+
|
|
27
|
+
# Explicit allowlist — do not glob site-corpus tests copied during extraction.
|
|
28
|
+
TESTS=(
|
|
29
|
+
scripts/test/test-doctor.rb
|
|
30
|
+
scripts/test/test-new.rb
|
|
31
|
+
scripts/test/test-export.rb
|
|
32
|
+
scripts/test/test-registry-home.rb
|
|
33
|
+
scripts/test/test-template-packages.rb
|
|
34
|
+
scripts/test/test-dual-template-instances.rb
|
|
35
|
+
scripts/test/test-dual-template-runtime.rb
|
|
36
|
+
scripts/test/test-install-functions.rb
|
|
37
|
+
scripts/test/test-no-personal-data.rb
|
|
38
|
+
scripts/test/test-configuration-doc.rb
|
|
39
|
+
scripts/test/test-cv-export-contract.rb
|
|
40
|
+
scripts/test/test-css-structure.rb
|
|
41
|
+
scripts/test/test-form-control-theming.rb
|
|
42
|
+
scripts/test/test-listing-filter-contract.rb
|
|
43
|
+
scripts/test/test-studio-api-contract.rb
|
|
44
|
+
scripts/test/test-studio-rewrite-api-contract.rb
|
|
45
|
+
scripts/test/test-studio-schema-composition.rb
|
|
46
|
+
scripts/test/test-text-excerpt.rb
|
|
47
|
+
scripts/test/test-git-chronology.rb
|
|
48
|
+
scripts/test/test-math-row-breaks.rb
|
|
49
|
+
scripts/test/test-detail-render-parity.rb
|
|
50
|
+
scripts/test/test-absent-field-absent-element.rb
|
|
51
|
+
scripts/test/test-home-band-art.rb
|
|
52
|
+
scripts/test/test-r2-json-sync.rb
|
|
53
|
+
scripts/test/test-writing-slug-limits.rb
|
|
54
|
+
scripts/test/test-examples-build.rb
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
failed=0
|
|
58
|
+
for test in "${TESTS[@]}"; do
|
|
59
|
+
if [[ ! -f "$test" ]]; then
|
|
60
|
+
echo "-- ${test} (missing, skip)"
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
echo "-- ${test}"
|
|
64
|
+
if ! "${RUBY[@]}" "$test"; then
|
|
65
|
+
failed=1
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
if [[ "$failed" -ne 0 ]]; then
|
|
70
|
+
echo "VALIDATE FAILED"
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo "VALIDATE OK"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// build-cv-pages-json.js — Write the cv-pages.json file used by take-screenshots.js.
|
|
3
|
+
//
|
|
4
|
+
// Extracts the 'cv' array from visual-inspection-pages.json and writes it as a flat
|
|
5
|
+
// array to cv-pages.json in the current working directory.
|
|
6
|
+
//
|
|
7
|
+
// Usage (run from repo root):
|
|
8
|
+
// node scripts/visual/build-cv-pages-json.js [outputFile]
|
|
9
|
+
//
|
|
10
|
+
// Arguments:
|
|
11
|
+
// outputFile — Output path for the JSON file (default: cv-pages.json)
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT = path.resolve(path.join(__dirname, '..', '..'));
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const outputFile = path.resolve(process.argv[2] || path.join(REPO_ROOT, 'cv-pages.json'));
|
|
22
|
+
|
|
23
|
+
const cfg = JSON.parse(
|
|
24
|
+
fs.readFileSync(path.join(__dirname, 'visual-inspection-pages.json'), 'utf8')
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(outputFile, JSON.stringify(cfg.cv, null, 2), 'utf8');
|
|
28
|
+
|
|
29
|
+
console.log(`Wrote ${cfg.cv.length} page(s) to ${outputFile}`);
|
|
30
|
+
cfg.cv.forEach((p) => console.log(` ${p.name} → ${p.url}`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// build-site-pages-json.js — Build the site-pages.json file used by take-screenshots.js.
|
|
3
|
+
//
|
|
4
|
+
// Takes the static page list from visual-inspection-pages.json, appends the most
|
|
5
|
+
// recent article and post (from content/), and writes site-pages.json.
|
|
6
|
+
//
|
|
7
|
+
// Usage (run from repo root):
|
|
8
|
+
// node scripts/visual/build-site-pages-json.js [outputFile]
|
|
9
|
+
//
|
|
10
|
+
// Arguments:
|
|
11
|
+
// outputFile — Output path for the JSON file (default: site-pages.json)
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = path.resolve(path.join(__dirname, '..', '..'));
|
|
20
|
+
const CONTENT_ROOT = path.join(REPO_ROOT, 'content');
|
|
21
|
+
const ARTICLES_DIR = path.join(CONTENT_ROOT, 'collections', 'articles');
|
|
22
|
+
const POSTS_DIR = path.join(CONTENT_ROOT, 'collections', 'posts');
|
|
23
|
+
|
|
24
|
+
function gitChronology(repoRelativePath, type) {
|
|
25
|
+
const quotedPath = repoRelativePath.replace(/'/g, "'\\''");
|
|
26
|
+
const quotedType = type.replace(/'/g, "'\\''");
|
|
27
|
+
const raw = execSync(
|
|
28
|
+
`bundle exec ruby -rjson -e "require_relative '_plugins/git_chronology'; puts JSON.generate(GitChronology.chronology_for(ARGV[0], type: ARGV[1]))" -- '${quotedPath}' '${quotedType}'`,
|
|
29
|
+
{ cwd: REPO_ROOT, encoding: 'utf8' }
|
|
30
|
+
).trim();
|
|
31
|
+
const chronology = JSON.parse(raw || '{}');
|
|
32
|
+
return {
|
|
33
|
+
date: chronology.date || '',
|
|
34
|
+
last_updated: chronology.last_updated || chronology.date || '',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function latestMarkdownSlug(dir, repoRelativeDir) {
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const files = fs
|
|
44
|
+
.readdirSync(dir)
|
|
45
|
+
.filter((f) => f.endsWith('.md'));
|
|
46
|
+
|
|
47
|
+
if (files.length === 0) {
|
|
48
|
+
throw new Error(`No markdown files found in ${dir}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return files
|
|
52
|
+
.map((file) => {
|
|
53
|
+
const slug = file.replace(/\.md$/, '');
|
|
54
|
+
const writingType = repoRelativeDir.includes('posts') ? 'post' : 'article';
|
|
55
|
+
const chronology = gitChronology(path.posix.join(repoRelativeDir, file), writingType);
|
|
56
|
+
return {
|
|
57
|
+
slug,
|
|
58
|
+
sortKey: chronology.last_updated || chronology.date || slug,
|
|
59
|
+
};
|
|
60
|
+
})
|
|
61
|
+
.sort((a, b) => String(b.sortKey).localeCompare(String(a.sortKey)))[0].slug;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function main() {
|
|
65
|
+
const outputFile = path.resolve(process.argv[2] || path.join(REPO_ROOT, 'site-pages.json'));
|
|
66
|
+
|
|
67
|
+
const cfg = JSON.parse(
|
|
68
|
+
fs.readFileSync(path.join(__dirname, 'visual-inspection-pages.json'), 'utf8')
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const latestArticleSlug = latestMarkdownSlug(ARTICLES_DIR, 'content/collections/articles');
|
|
72
|
+
const latestPostSlug = latestMarkdownSlug(POSTS_DIR, 'content/collections/posts');
|
|
73
|
+
|
|
74
|
+
const pages = [
|
|
75
|
+
...cfg.site,
|
|
76
|
+
{
|
|
77
|
+
id: 'latest-article',
|
|
78
|
+
name: 'Latest Article',
|
|
79
|
+
url: `/articles/${encodeURIComponent(latestArticleSlug)}/`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'latest-post',
|
|
83
|
+
name: 'Latest Post',
|
|
84
|
+
url: `/posts/${encodeURIComponent(latestPostSlug)}/`,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(outputFile, JSON.stringify(pages, null, 2), 'utf8');
|
|
89
|
+
|
|
90
|
+
console.log(`Wrote ${pages.length} page(s) to ${outputFile}`);
|
|
91
|
+
pages.forEach((p) => console.log(` ${p.name} → ${p.url}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Upload visual-inspection PNG screenshots to Cloudflare R2.
|
|
3
|
+
# Used by .github/workflows/visual-inspection.yml (manual dispatch only).
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
LOCAL_DIR="${1:?Usage: publish-visual-inspection-to-r2.sh <local-dir> <run-prefix> [subdir]}"
|
|
7
|
+
RUN_PREFIX="${2:?Usage: publish-visual-inspection-to-r2.sh <local-dir> <run-prefix> [subdir]}"
|
|
8
|
+
SUBDIR="${3:-}"
|
|
9
|
+
|
|
10
|
+
: "${R2_ENDPOINT:?Set R2_ENDPOINT}"
|
|
11
|
+
: "${R2_BUCKET:?Set R2_BUCKET}"
|
|
12
|
+
: "${R2_ACCESS_KEY_ID:?Set R2_ACCESS_KEY_ID}"
|
|
13
|
+
: "${R2_SECRET_ACCESS_KEY:?Set R2_SECRET_ACCESS_KEY}"
|
|
14
|
+
|
|
15
|
+
export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID"
|
|
16
|
+
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
|
|
17
|
+
export AWS_DEFAULT_REGION="${R2_REGION:-auto}"
|
|
18
|
+
|
|
19
|
+
if ! command -v aws >/dev/null 2>&1; then
|
|
20
|
+
echo "aws CLI is required." >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [ ! -d "$LOCAL_DIR" ]; then
|
|
25
|
+
echo "Local directory not found: $LOCAL_DIR" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
DEST="visual-inspection/${RUN_PREFIX}"
|
|
30
|
+
if [ -n "$SUBDIR" ]; then
|
|
31
|
+
DEST="${DEST}/${SUBDIR}"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
echo "==> R2 sync ${LOCAL_DIR}/ → s3://${R2_BUCKET}/${DEST}/"
|
|
35
|
+
aws s3 sync "$LOCAL_DIR" "s3://${R2_BUCKET}/${DEST}/" \
|
|
36
|
+
--endpoint-url "$R2_ENDPOINT" \
|
|
37
|
+
--exclude "*" \
|
|
38
|
+
--include "*.png" \
|
|
39
|
+
--cache-control "public,max-age=31536000,immutable" \
|
|
40
|
+
--content-type "image/png"
|
|
41
|
+
|
|
42
|
+
echo "==> Visual inspection upload complete: ${DEST}/"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// take-screenshots.js — Screenshot site pages using Puppeteer for visual inspection.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/visual/take-screenshots.js <outputDir> <baseUrl> <pagesJsonFile>
|
|
6
|
+
//
|
|
7
|
+
// Arguments:
|
|
8
|
+
// outputDir — Directory to save screenshots (created if missing)
|
|
9
|
+
// baseUrl — Base URL of the local server (e.g. http://localhost:4000 for the
|
|
10
|
+
// public site; the workflow uses 4001 for the private CV build)
|
|
11
|
+
// pagesJsonFile — Path to a flat JSON array: [{ id, name, url }, ...]
|
|
12
|
+
//
|
|
13
|
+
// Output: saves <id>.png for each page in outputDir.
|
|
14
|
+
//
|
|
15
|
+
// Exit codes:
|
|
16
|
+
// 0 — all screenshots successful
|
|
17
|
+
// 1 — one or more screenshots failed (partial output may exist)
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const puppeteer = require('puppeteer');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const outputDir = path.resolve(process.argv[2] || 'screenshots');
|
|
27
|
+
const baseUrl = (process.argv[3] || 'http://localhost:4000').replace(/\/$/, '');
|
|
28
|
+
const pagesFile = path.resolve(process.argv[4] || 'pages.json');
|
|
29
|
+
|
|
30
|
+
const pages = JSON.parse(fs.readFileSync(pagesFile, 'utf8'));
|
|
31
|
+
|
|
32
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
const browser = await puppeteer.launch({
|
|
35
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let failures = 0;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const executing = new Set();
|
|
42
|
+
const CONCURRENCY_LIMIT = 5;
|
|
43
|
+
|
|
44
|
+
for (const pageConfig of pages) {
|
|
45
|
+
const task = async () => {
|
|
46
|
+
const url = `${baseUrl}${pageConfig.url}`;
|
|
47
|
+
console.log(`Screenshotting: ${pageConfig.name} → ${url}`);
|
|
48
|
+
|
|
49
|
+
const page = await browser.newPage();
|
|
50
|
+
await page.setViewport({ width: 1440, height: 1200 });
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 60000 });
|
|
54
|
+
const outputPath = path.join(outputDir, `${pageConfig.id}.png`);
|
|
55
|
+
await page.screenshot({ path: outputPath, fullPage: true });
|
|
56
|
+
console.log(` ✓ ${path.basename(outputPath)}`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(` ✗ ${pageConfig.name}: ${err.message}`);
|
|
59
|
+
failures += 1;
|
|
60
|
+
} finally {
|
|
61
|
+
await page.close();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const promise = task();
|
|
66
|
+
executing.add(promise);
|
|
67
|
+
promise.finally(() => executing.delete(promise));
|
|
68
|
+
|
|
69
|
+
if (executing.size >= CONCURRENCY_LIMIT) {
|
|
70
|
+
await Promise.race(executing);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await Promise.all(executing);
|
|
75
|
+
} finally {
|
|
76
|
+
await browser.close();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (failures > 0) {
|
|
80
|
+
console.error(`\n${failures} screenshot(s) failed.`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch((err) => {
|
|
86
|
+
console.error(err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|