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,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// flatten-ansi-capture.mjs — turn a terminal capture into an ```ansi fence.
|
|
3
|
+
//
|
|
4
|
+
// An `ansi` fence is rendered by _includes/content-runtime/25-code-blocks.html,
|
|
5
|
+
// which honours SGR colour and nothing else. Real captures are not that clean:
|
|
6
|
+
// anything that paints columns does it by moving the cursor, and neofetch in
|
|
7
|
+
// particular draws its whole info block by jumping up twenty rows and right
|
|
8
|
+
// forty-three columns for every line. Pasted as-is, the result is one column of
|
|
9
|
+
// overstruck text.
|
|
10
|
+
//
|
|
11
|
+
// So the moves are played out here, onto a character grid, and the result is
|
|
12
|
+
// re-emitted as ordinary lines that carry colour and nothing else. What the
|
|
13
|
+
// fence stores is then what a reader sees.
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// script -qec "neofetch" /dev/null > capture.txt
|
|
17
|
+
// node scripts/content/flatten-ansi-capture.mjs capture.txt > block.txt
|
|
18
|
+
//
|
|
19
|
+
// `script` is what makes the capture worth taking: neofetch, like most tools,
|
|
20
|
+
// drops its colour when stdout is not a terminal, and `script` gives it a pty
|
|
21
|
+
// to write to. Paste the output between ```ansi fences.
|
|
22
|
+
//
|
|
23
|
+
// --raw emits the 0x1b control byte instead of the printable `\e[` spelling.
|
|
24
|
+
// The default is the printable form, because that is what survives an editor,
|
|
25
|
+
// a JSON export and a diff; the renderer reads both.
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
|
|
29
|
+
const ESC = String.fromCharCode(27);
|
|
30
|
+
|
|
31
|
+
/* SGR is accumulated rather than replaced. Tools write the attributes of one
|
|
32
|
+
run as several sequences in a row — neofetch's logo is `\e[0m\e[31m\e[1m` —
|
|
33
|
+
so treating each `m` as the whole state throws away every colour. */
|
|
34
|
+
function makeAttr() {
|
|
35
|
+
return { fg: null, bg: null, bold: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applySgr(attr, params) {
|
|
39
|
+
const codes = params === '' ? ['0'] : params.split(';');
|
|
40
|
+
for (let i = 0; i < codes.length; i++) {
|
|
41
|
+
const n = parseInt(codes[i], 10);
|
|
42
|
+
if (Number.isNaN(n) || n === 0) { attr.fg = null; attr.bg = null; attr.bold = false; }
|
|
43
|
+
else if (n === 1) attr.bold = true;
|
|
44
|
+
else if (n === 22) attr.bold = false;
|
|
45
|
+
else if (n === 39) attr.fg = null;
|
|
46
|
+
else if (n === 49) attr.bg = null;
|
|
47
|
+
else if ((n >= 30 && n <= 37) || (n >= 90 && n <= 97)) attr.fg = String(n);
|
|
48
|
+
else if ((n >= 40 && n <= 47) || (n >= 100 && n <= 107)) attr.bg = String(n);
|
|
49
|
+
else if (n === 38 || n === 48) {
|
|
50
|
+
const target = n === 38 ? 'fg' : 'bg';
|
|
51
|
+
if (codes[i + 1] === '5') { attr[target] = `${n};5;${codes[i + 2]}`; i += 2; }
|
|
52
|
+
else if (codes[i + 1] === '2') {
|
|
53
|
+
attr[target] = `${n};2;${codes[i + 2]};${codes[i + 3]};${codes[i + 4]}`;
|
|
54
|
+
i += 4;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sgrOf(attr) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (attr.bold) parts.push('1');
|
|
63
|
+
if (attr.fg) parts.push(attr.fg);
|
|
64
|
+
if (attr.bg) parts.push(attr.bg);
|
|
65
|
+
return parts.join(';');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function flattenAnsi(input) {
|
|
69
|
+
const grid = [];
|
|
70
|
+
let row = 0;
|
|
71
|
+
let col = 0;
|
|
72
|
+
const attr = makeAttr();
|
|
73
|
+
|
|
74
|
+
const cell = (r, c) => {
|
|
75
|
+
while (grid.length <= r) grid.push([]);
|
|
76
|
+
const line = grid[r];
|
|
77
|
+
while (line.length <= c) line.push({ ch: ' ', sgr: '', bg: false });
|
|
78
|
+
return line[c];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < input.length; i++) {
|
|
82
|
+
const ch = input[i];
|
|
83
|
+
if (ch === ESC && input[i + 1] === '[') {
|
|
84
|
+
const m = /^\x1b\[([0-9;?]*)([A-Za-z])/.exec(input.slice(i));
|
|
85
|
+
if (m) {
|
|
86
|
+
const [, params, final] = m;
|
|
87
|
+
const n = parseInt(params, 10) || 1;
|
|
88
|
+
// `?` params belong to the private mode sequences (cursor hide, wrap
|
|
89
|
+
// off) that every capture opens and closes with. Nothing to draw.
|
|
90
|
+
if (final === 'm') applySgr(attr, params.replace(/\?/g, ''));
|
|
91
|
+
else if (final === 'A') row = Math.max(0, row - n);
|
|
92
|
+
else if (final === 'B') row += n;
|
|
93
|
+
else if (final === 'C') col += n;
|
|
94
|
+
else if (final === 'D') col = Math.max(0, col - n);
|
|
95
|
+
else if (final === 'G') col = Math.max(0, n - 1);
|
|
96
|
+
i += m[0].length - 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (ch === '\n') { row++; col = 0; continue; }
|
|
101
|
+
if (ch === '\r') { col = 0; continue; }
|
|
102
|
+
if (ch === ESC || ch < ' ') continue;
|
|
103
|
+
const target = cell(row, col);
|
|
104
|
+
target.ch = ch;
|
|
105
|
+
target.sgr = sgrOf(attr);
|
|
106
|
+
target.bg = attr.bg != null;
|
|
107
|
+
col++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lines = grid.map((line) => {
|
|
111
|
+
// Trailing blanks are padding unless they carry a background colour, which
|
|
112
|
+
// is how the palette strips at the foot of a neofetch block are drawn.
|
|
113
|
+
let last = -1;
|
|
114
|
+
for (let i = 0; i < line.length; i++) if (line[i].ch !== ' ' || line[i].bg) last = i;
|
|
115
|
+
let text = '';
|
|
116
|
+
let active = '';
|
|
117
|
+
for (let i = 0; i <= last; i++) {
|
|
118
|
+
const c = line[i];
|
|
119
|
+
// A plain space shows no attribute of its own, so it stays inside the
|
|
120
|
+
// run it sits in rather than forcing a reset mid-line.
|
|
121
|
+
const want = c.ch === ' ' && !c.bg ? active : c.sgr;
|
|
122
|
+
// Every run states its attributes absolutely, leading with the reset.
|
|
123
|
+
// SGR is additive, so `\e[1m` after a red run is *bold red*, not bold
|
|
124
|
+
// default — and that is exactly the pair neofetch's logo alternates
|
|
125
|
+
// between. Emitting the difference would silently repaint the white
|
|
126
|
+
// letters of the Ubuntu mark in red.
|
|
127
|
+
if (want !== active) { text += `${ESC}[${want ? `0;${want}` : '0'}m`; active = want; }
|
|
128
|
+
text += c.ch;
|
|
129
|
+
}
|
|
130
|
+
if (active) text += `${ESC}[0m`;
|
|
131
|
+
return text;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
while (lines.length && lines[lines.length - 1].replace(/\x1b\[[0-9;]*m/g, '').trim() === '') {
|
|
135
|
+
lines.pop();
|
|
136
|
+
}
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const args = process.argv.slice(2);
|
|
141
|
+
const raw = args.includes('--raw');
|
|
142
|
+
const source = args.filter((a) => !a.startsWith('--'))[0];
|
|
143
|
+
|
|
144
|
+
if (!source) {
|
|
145
|
+
console.error('usage: flatten-ansi-capture.mjs <capture.txt> [--raw]');
|
|
146
|
+
process.exit(2);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// latin1 throughout: a capture is a byte stream, and decoding it as UTF-8
|
|
150
|
+
// would fold the box-drawing bytes some logos use into replacement characters.
|
|
151
|
+
const flattened = flattenAnsi(fs.readFileSync(source, 'latin1'));
|
|
152
|
+
const out = raw ? flattened : flattened.split(ESC + '[').join('\\e[');
|
|
153
|
+
process.stdout.write(Buffer.from(out + '\n', 'latin1'));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The home hero pool is the folder. The content export writes the public
|
|
4
|
+
# paths to pages/home/portraits.json. The author uploads a photograph under
|
|
5
|
+
# content/media/images/hero/ and does not keep a second list.
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
|
|
9
|
+
module HeroPortraits
|
|
10
|
+
IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .gif].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def entries(content_root)
|
|
15
|
+
image_files(Pathname.new(content_root)).map do |file|
|
|
16
|
+
{ "src" => "/media/images/hero/#{file.basename}" }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def image_files(root)
|
|
21
|
+
dir = root.join("media/images/hero")
|
|
22
|
+
return [] unless dir.directory?
|
|
23
|
+
|
|
24
|
+
dir.children.select do |path|
|
|
25
|
+
path.file? && IMAGE_EXTENSIONS.include?(path.extname.downcase)
|
|
26
|
+
end.sort_by { |path| path.basename.to_s }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Publish exported editorial JSON and media to an S3-compatible bucket.
|
|
3
|
+
# Accepts S3_* or legacy R2_* environment variables.
|
|
4
|
+
# Used by `pandorga publish` and CI content pipelines.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
8
|
+
SITE_ROOT="${PANDORGA_SITE_ROOT:-$ROOT}"
|
|
9
|
+
EXPORT_ROOT="${1:-$SITE_ROOT/_content_json}"
|
|
10
|
+
|
|
11
|
+
# Prefer S3_* names; fall back to R2_* for Cloudflare R2 callers.
|
|
12
|
+
S3_ENDPOINT="${S3_ENDPOINT:-${R2_ENDPOINT:-}}"
|
|
13
|
+
S3_BUCKET="${S3_BUCKET:-${R2_BUCKET:-}}"
|
|
14
|
+
S3_ACCESS_KEY_ID="${S3_ACCESS_KEY_ID:-${R2_ACCESS_KEY_ID:-}}"
|
|
15
|
+
S3_SECRET_ACCESS_KEY="${S3_SECRET_ACCESS_KEY:-${R2_SECRET_ACCESS_KEY:-}}"
|
|
16
|
+
S3_REGION="${S3_REGION:-${R2_REGION:-auto}}"
|
|
17
|
+
|
|
18
|
+
: "${S3_ENDPOINT:?Set S3_ENDPOINT or R2_ENDPOINT (S3-compatible API URL)}"
|
|
19
|
+
: "${S3_BUCKET:?Set S3_BUCKET or R2_BUCKET}"
|
|
20
|
+
: "${S3_ACCESS_KEY_ID:?Set S3_ACCESS_KEY_ID or R2_ACCESS_KEY_ID}"
|
|
21
|
+
: "${S3_SECRET_ACCESS_KEY:?Set S3_SECRET_ACCESS_KEY or R2_SECRET_ACCESS_KEY}"
|
|
22
|
+
|
|
23
|
+
export AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY_ID"
|
|
24
|
+
export AWS_SECRET_ACCESS_KEY="$S3_SECRET_ACCESS_KEY"
|
|
25
|
+
export AWS_DEFAULT_REGION="$S3_REGION"
|
|
26
|
+
|
|
27
|
+
# Keep sync helper (still reads R2_* names) working.
|
|
28
|
+
export R2_ENDPOINT="$S3_ENDPOINT"
|
|
29
|
+
export R2_BUCKET="$S3_BUCKET"
|
|
30
|
+
export R2_ACCESS_KEY_ID="$S3_ACCESS_KEY_ID"
|
|
31
|
+
export R2_SECRET_ACCESS_KEY="$S3_SECRET_ACCESS_KEY"
|
|
32
|
+
export R2_REGION="$S3_REGION"
|
|
33
|
+
|
|
34
|
+
if ! command -v aws >/dev/null 2>&1; then
|
|
35
|
+
echo "aws CLI is required for object-store publish." >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
run_ruby() {
|
|
40
|
+
if command -v mise >/dev/null 2>&1; then
|
|
41
|
+
mise exec -- ruby "$@"
|
|
42
|
+
else
|
|
43
|
+
ruby "$@"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sync_json_prefix() {
|
|
48
|
+
local prefix="$1"
|
|
49
|
+
local src="$EXPORT_ROOT/$prefix"
|
|
50
|
+
if [ -d "$src" ]; then
|
|
51
|
+
echo "==> Object store replace sync $prefix/"
|
|
52
|
+
run_ruby "$ROOT/scripts/content/sync-content-json-to-r2.rb" "$EXPORT_ROOT" "$prefix"
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
echo "==> Publishing content JSON to bucket: ${S3_BUCKET}"
|
|
57
|
+
echo "==> Endpoint: ${S3_ENDPOINT}"
|
|
58
|
+
sync_json_prefix collections
|
|
59
|
+
sync_json_prefix data
|
|
60
|
+
sync_json_prefix pages
|
|
61
|
+
|
|
62
|
+
echo "==> Upload manifest.json"
|
|
63
|
+
aws s3 cp "$EXPORT_ROOT/manifest.json" "s3://${S3_BUCKET}/manifest.json" \
|
|
64
|
+
--endpoint-url "$S3_ENDPOINT" \
|
|
65
|
+
--cache-control "public,max-age=60" \
|
|
66
|
+
--content-type "application/json"
|
|
67
|
+
|
|
68
|
+
MEDIA_ROOT="$SITE_ROOT/content/media"
|
|
69
|
+
if [ -d "$MEDIA_ROOT" ]; then
|
|
70
|
+
echo "==> Sync media/"
|
|
71
|
+
aws s3 sync "$MEDIA_ROOT" "s3://${S3_BUCKET}/media" \
|
|
72
|
+
--endpoint-url "$S3_ENDPOINT" \
|
|
73
|
+
--delete \
|
|
74
|
+
--size-only \
|
|
75
|
+
--cache-control "public,max-age=31536000,immutable"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "==> Object-store publish complete"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Incremental JSON prefix sync to Cloudflare R2.
|
|
5
|
+
# Skip aws s3 cp when list-objects-v2 ETag is a single-part MD5 equal to the
|
|
6
|
+
# local file (case-insensitive). Upload if missing, ETag blank, multipart
|
|
7
|
+
# ("-" in ETag), or hash mismatch. Delete remote keys absent locally.
|
|
8
|
+
# Stale bodies cannot survive: mismatch still uploads and extras still delete.
|
|
9
|
+
# writing_index.json is always PUT so listings match the live object set.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# sync-content-json-to-r2.rb <export_root> <prefix>
|
|
13
|
+
# Example:
|
|
14
|
+
# sync-content-json-to-r2.rb _content_json collections
|
|
15
|
+
|
|
16
|
+
require "digest"
|
|
17
|
+
require "json"
|
|
18
|
+
require "open3"
|
|
19
|
+
require "pathname"
|
|
20
|
+
require "set"
|
|
21
|
+
|
|
22
|
+
require_relative "../lib/r2_json_sync"
|
|
23
|
+
require_relative "../lib/withdrawn_public_objects"
|
|
24
|
+
|
|
25
|
+
$stdout.sync = true
|
|
26
|
+
$stderr.sync = true
|
|
27
|
+
|
|
28
|
+
EXPORT_ROOT = Pathname.new(ARGV[0] || "_content_json").expand_path
|
|
29
|
+
PREFIX = (ARGV[1] || "").sub(%r{\A/+}, "").sub(%r{/+\z}, "")
|
|
30
|
+
|
|
31
|
+
def fail!(message)
|
|
32
|
+
warn message
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
fail!("Usage: #{$PROGRAM_NAME} <export_root> <prefix>") if PREFIX.empty?
|
|
37
|
+
|
|
38
|
+
endpoint = ENV["R2_ENDPOINT"].to_s
|
|
39
|
+
bucket = ENV["R2_BUCKET"].to_s
|
|
40
|
+
fail!("R2_ENDPOINT is required") if endpoint.empty?
|
|
41
|
+
fail!("R2_BUCKET is required") if bucket.empty?
|
|
42
|
+
fail!("aws CLI is required") if Open3.capture3("bash", "-lc", "command -v aws").last.exitstatus != 0
|
|
43
|
+
|
|
44
|
+
SRC = EXPORT_ROOT.join(PREFIX)
|
|
45
|
+
fail!("Missing local prefix directory: #{SRC}") unless SRC.directory?
|
|
46
|
+
|
|
47
|
+
CACHE_CONTROL = "public,max-age=60"
|
|
48
|
+
|
|
49
|
+
# The export is mostly JSON, and was entirely JSON until the home band plates
|
|
50
|
+
# started shipping as drawings. Serving an SVG as application/json costs nothing
|
|
51
|
+
# for a fetch that reads it as text, and everything for anything that does not.
|
|
52
|
+
CONTENT_TYPES = {
|
|
53
|
+
".json" => "application/json",
|
|
54
|
+
".svg" => "image/svg+xml"
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
def content_type_for(path)
|
|
58
|
+
CONTENT_TYPES.fetch(path.extname.downcase) do
|
|
59
|
+
fail!("no content type registered for #{path.extname} (#{path}); add one to CONTENT_TYPES")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def run_aws(*args)
|
|
64
|
+
cmd = ["aws", *args, "--endpoint-url", ENV.fetch("R2_ENDPOINT")]
|
|
65
|
+
stdout, stderr, status = Open3.capture3(
|
|
66
|
+
{
|
|
67
|
+
"AWS_ACCESS_KEY_ID" => ENV.fetch("R2_ACCESS_KEY_ID"),
|
|
68
|
+
"AWS_SECRET_ACCESS_KEY" => ENV.fetch("R2_SECRET_ACCESS_KEY"),
|
|
69
|
+
"AWS_DEFAULT_REGION" => ENV.fetch("R2_REGION", "auto")
|
|
70
|
+
},
|
|
71
|
+
*cmd
|
|
72
|
+
)
|
|
73
|
+
[stdout, stderr, status]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def list_remote_objects(prefix)
|
|
77
|
+
etags = {}
|
|
78
|
+
token = nil
|
|
79
|
+
loop do
|
|
80
|
+
args = [
|
|
81
|
+
"s3api", "list-objects-v2",
|
|
82
|
+
"--bucket", ENV.fetch("R2_BUCKET"),
|
|
83
|
+
"--prefix", "#{prefix}/",
|
|
84
|
+
"--output", "json"
|
|
85
|
+
]
|
|
86
|
+
args += ["--continuation-token", token] if token
|
|
87
|
+
stdout, stderr, status = run_aws(*args)
|
|
88
|
+
fail!("list-objects-v2 failed for #{prefix}/: #{stderr}") unless status.success?
|
|
89
|
+
|
|
90
|
+
payload = stdout.strip.empty? ? {} : JSON.parse(stdout)
|
|
91
|
+
etags.merge!(R2JsonSync.contents_to_etags(payload["Contents"]))
|
|
92
|
+
break unless payload["IsTruncated"]
|
|
93
|
+
|
|
94
|
+
token = payload["NextContinuationToken"]
|
|
95
|
+
fail!("list-objects-v2 truncated without continuation token") if token.to_s.empty?
|
|
96
|
+
end
|
|
97
|
+
etags
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def local_md5(path)
|
|
101
|
+
Digest::MD5.hexdigest(path.binread)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local_files = SRC.find.select(&:file?).map do |path|
|
|
105
|
+
rel = path.relative_path_from(SRC).to_s
|
|
106
|
+
key = "#{PREFIX}/#{rel}"
|
|
107
|
+
[path, key, local_md5(path)]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
remote_etags = list_remote_objects(PREFIX)
|
|
111
|
+
plan = R2JsonSync.plan(
|
|
112
|
+
local_files.map { |(_path, key, md5)| { "key" => key, "md5" => md5 } },
|
|
113
|
+
remote_etags
|
|
114
|
+
)
|
|
115
|
+
upload_keys = plan["upload"].to_set
|
|
116
|
+
|
|
117
|
+
uploaded = 0
|
|
118
|
+
local_files.each do |path, key, _md5|
|
|
119
|
+
next unless upload_keys.include?(key)
|
|
120
|
+
|
|
121
|
+
_stdout, stderr, status = run_aws(
|
|
122
|
+
"s3", "cp", path.to_s, "s3://#{ENV.fetch('R2_BUCKET')}/#{key}",
|
|
123
|
+
"--cache-control", CACHE_CONTROL,
|
|
124
|
+
"--content-type", content_type_for(path)
|
|
125
|
+
)
|
|
126
|
+
fail!("upload failed for #{key}: #{stderr}") unless status.success?
|
|
127
|
+
|
|
128
|
+
uploaded += 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
deleted = 0
|
|
132
|
+
plan["delete"].each do |key|
|
|
133
|
+
_stdout, stderr, status = run_aws(
|
|
134
|
+
"s3", "rm", "s3://#{ENV.fetch('R2_BUCKET')}/#{key}"
|
|
135
|
+
)
|
|
136
|
+
fail!("delete failed for #{key}: #{stderr}") unless status.success?
|
|
137
|
+
|
|
138
|
+
deleted += 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
skipped = plan["skip"].length
|
|
142
|
+
local_keys = local_files.map { |(_, key, _)| key }.to_set
|
|
143
|
+
|
|
144
|
+
remote_after = list_remote_objects(PREFIX)
|
|
145
|
+
extra = (remote_after.keys.to_set - local_keys).to_a.sort
|
|
146
|
+
missing = (local_keys - remote_after.keys.to_set).to_a.sort
|
|
147
|
+
unless extra.empty? && missing.empty?
|
|
148
|
+
fail!("R2 keyset drift for #{PREFIX}/ extra=#{extra.inspect} missing=#{missing.inspect}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
local_files.each do |_path, key, md5|
|
|
152
|
+
etag = remote_after[key].to_s
|
|
153
|
+
next if R2JsonSync.multipart_etag?(etag)
|
|
154
|
+
|
|
155
|
+
unless etag.casecmp?(md5)
|
|
156
|
+
fail!("R2 origin mismatch for #{key}: etag=#{etag.inspect} md5=#{md5.inspect}")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
WithdrawnPublicObjects::EXPORT_PATHS.each do |relative|
|
|
161
|
+
next unless relative.start_with?("#{PREFIX}/")
|
|
162
|
+
next unless remote_after.key?(relative)
|
|
163
|
+
|
|
164
|
+
fail!("withdrawn object still present on R2: #{relative}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
puts "R2 replace sync #{PREFIX}/: uploaded=#{uploaded} skipped=#{skipped} deleted=#{deleted}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
require_relative "../../_plugins/content_keys"
|
|
7
|
+
|
|
8
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
9
|
+
|
|
10
|
+
ContentKeys.validate!(REPO_ROOT)
|
|
11
|
+
ContentKeys.sync_redirects_file!(REPO_ROOT)
|
|
12
|
+
puts "Content redirects synced to #{REPO_ROOT.join('_redirects')}"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Regenerate docs/configuration.md from lib/pandorga/registry/schema.yml.
|
|
5
|
+
# Refs: PLT-AC-11
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
ROOT = Pathname.new(__dir__).join("..").expand_path
|
|
11
|
+
SCHEMA_PATH = ROOT.join("lib/pandorga/registry/schema.yml")
|
|
12
|
+
DOC_PATH = ROOT.join("docs/configuration.md")
|
|
13
|
+
|
|
14
|
+
def fail!(message)
|
|
15
|
+
warn message
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def required_cell(value)
|
|
20
|
+
case value
|
|
21
|
+
when true then "yes"
|
|
22
|
+
when false then "no"
|
|
23
|
+
when nil then ""
|
|
24
|
+
else value.to_s
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_keys_table(keys, include_required:)
|
|
29
|
+
lines = []
|
|
30
|
+
if include_required
|
|
31
|
+
lines << "| Key | Type | Required | Notes |"
|
|
32
|
+
lines << "|---|---|---|---|"
|
|
33
|
+
else
|
|
34
|
+
lines << "| Key | Type | Notes |"
|
|
35
|
+
lines << "|---|---|---|"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
keys.each do |key, meta|
|
|
39
|
+
meta = {} unless meta.is_a?(Hash)
|
|
40
|
+
label = meta["key_label"] || key
|
|
41
|
+
type = meta["type"].to_s
|
|
42
|
+
notes = meta["notes"].to_s
|
|
43
|
+
if include_required
|
|
44
|
+
lines << "| `#{label}` | #{type} | #{required_cell(meta['required'])} | #{notes} |"
|
|
45
|
+
else
|
|
46
|
+
lines << "| `#{label}` | #{type} | #{notes} |"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
lines.join("\n")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_home_attributes(entries)
|
|
53
|
+
lines = ["| Key | Notes |", "|---|---|"]
|
|
54
|
+
entries.each do |entry|
|
|
55
|
+
entry = {} unless entry.is_a?(Hash)
|
|
56
|
+
lines << "| #{entry['key']} | #{entry['notes']} |"
|
|
57
|
+
end
|
|
58
|
+
lines.join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def generate(schema)
|
|
62
|
+
out = []
|
|
63
|
+
out << "# Configuration reference"
|
|
64
|
+
out << ""
|
|
65
|
+
out << "Generated from `lib/pandorga/registry/schema.yml`. Gate: keep in sync"
|
|
66
|
+
out << "via `scripts/generate-configuration-doc.rb` (`PLT-AC-11`)."
|
|
67
|
+
out << ""
|
|
68
|
+
|
|
69
|
+
%w[identity content home].each do |section|
|
|
70
|
+
data = schema.fetch(section)
|
|
71
|
+
out << "## #{data.fetch('title')}"
|
|
72
|
+
out << ""
|
|
73
|
+
include_required = section == "identity"
|
|
74
|
+
out << render_keys_table(data.fetch("keys"), include_required: include_required)
|
|
75
|
+
out << ""
|
|
76
|
+
intro = data["intro"].to_s.strip
|
|
77
|
+
unless intro.empty?
|
|
78
|
+
out << intro
|
|
79
|
+
out << ""
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
tax = schema.fetch("taxonomies")
|
|
84
|
+
out << "## #{tax.fetch('title')}"
|
|
85
|
+
out << ""
|
|
86
|
+
out << tax.fetch("prose").to_s.strip
|
|
87
|
+
out << ""
|
|
88
|
+
|
|
89
|
+
pages = schema.fetch("pages")
|
|
90
|
+
out << "## #{pages.fetch('title')}"
|
|
91
|
+
out << ""
|
|
92
|
+
out << render_keys_table(pages.fetch("keys"), include_required: true)
|
|
93
|
+
out << ""
|
|
94
|
+
footer = pages["footer"].to_s.strip
|
|
95
|
+
out << footer unless footer.empty?
|
|
96
|
+
out << ""
|
|
97
|
+
|
|
98
|
+
home_attrs = pages.fetch("home_attributes")
|
|
99
|
+
out << "### #{home_attrs.fetch('title')}"
|
|
100
|
+
out << ""
|
|
101
|
+
out << render_home_attributes(home_attrs.fetch("entries"))
|
|
102
|
+
out << ""
|
|
103
|
+
|
|
104
|
+
"#{out.join("\n").rstrip}\n"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
schema = YAML.safe_load(SCHEMA_PATH.read)
|
|
108
|
+
fail!("schema missing or empty: #{SCHEMA_PATH}") unless schema.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
text = generate(schema)
|
|
111
|
+
|
|
112
|
+
if ARGV.include?("--check")
|
|
113
|
+
unless DOC_PATH.file? && DOC_PATH.read == text
|
|
114
|
+
warn "FAIL: docs/configuration.md is stale; run scripts/generate-configuration-doc.rb"
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
puts "OK docs/configuration.md matches schema"
|
|
118
|
+
exit 0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if ARGV.include?("--stdout")
|
|
122
|
+
print text
|
|
123
|
+
exit 0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
DOC_PATH.write(text)
|
|
127
|
+
puts "Wrote #{DOC_PATH.relative_path_from(ROOT)}"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
# Shared editorial license metadata (_config.yml → content_license).
|
|
7
|
+
module ContentLicense
|
|
8
|
+
# No default rights_holder — sites must set content_license.rights_holder.
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
"id" => "CC-BY-4.0",
|
|
11
|
+
"name" => "Creative Commons Attribution 4.0 International",
|
|
12
|
+
"url" => "https://creativecommons.org/licenses/by/4.0/",
|
|
13
|
+
"page_path" => "/pages/license/"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
SOFTWARE = {
|
|
17
|
+
"id" => "MIT",
|
|
18
|
+
"name" => "MIT License",
|
|
19
|
+
"url" => "https://opensource.org/licenses/MIT",
|
|
20
|
+
"file" => "LICENSE"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
EDITORIAL_TYPES = %w[article post product project job resource].freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def load(repo_root = Pathname.new(__dir__).join("..", "..").expand_path)
|
|
28
|
+
config_path = repo_root.join("_config.yml")
|
|
29
|
+
return DEFAULTS.dup unless config_path.file?
|
|
30
|
+
|
|
31
|
+
config = YAML.safe_load(config_path.read) || {}
|
|
32
|
+
raw = config["content_license"]
|
|
33
|
+
return DEFAULTS.dup unless raw.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
DEFAULTS.merge(raw.transform_keys(&:to_s))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def payload(license = load)
|
|
39
|
+
license.slice("id", "name", "url", "rights_holder", "page_path")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def copyright_notice(license = load, year: Time.now.utc.year)
|
|
43
|
+
holder = license["rights_holder"] || DEFAULTS["rights_holder"]
|
|
44
|
+
"© #{year} #{holder}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def editorial?(type)
|
|
48
|
+
EDITORIAL_TYPES.include?(type)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
# Shared reader for the public content store base URL.
|
|
7
|
+
# Prefers pandorga.content.base_url; falls back to content_api_base_url.
|
|
8
|
+
module ContentStorageConfig
|
|
9
|
+
PLACEHOLDER_MARKERS = [
|
|
10
|
+
"__R2_PUBLIC_BASE_URL__",
|
|
11
|
+
"REPLACE_WITH_R2_PUBLIC_BASE_URL",
|
|
12
|
+
"__S3_PUBLIC_BASE_URL__",
|
|
13
|
+
"REPLACE_WITH_S3_PUBLIC_BASE_URL"
|
|
14
|
+
].freeze
|
|
15
|
+
LEGACY_GCS_HOST = "storage.googleapis.com"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def repo_root
|
|
20
|
+
Pathname.new(__dir__).join("..", "..").expand_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def config_path
|
|
24
|
+
repo_root.join("_config.yml")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load_config
|
|
28
|
+
YAML.safe_load(config_path.read, aliases: true) || {}
|
|
29
|
+
rescue Errno::ENOENT
|
|
30
|
+
{}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def base_url
|
|
34
|
+
env = ENV["S3_PUBLIC_BASE_URL"].to_s.strip
|
|
35
|
+
env = ENV["R2_PUBLIC_BASE_URL"].to_s.strip if env.empty?
|
|
36
|
+
env = ENV["CONTENT_API_BASE_URL"].to_s.strip if env.empty?
|
|
37
|
+
unless env.empty?
|
|
38
|
+
return env.chomp("/")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
config = load_config
|
|
42
|
+
begin
|
|
43
|
+
require "pandorga/registry"
|
|
44
|
+
url = PandorgaRegistry.content_base_url(config)
|
|
45
|
+
return url unless url.empty?
|
|
46
|
+
rescue LoadError
|
|
47
|
+
# CLI gates may run without the gem on LOAD_PATH.
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
url = config["content_api_base_url"].to_s.strip
|
|
51
|
+
url.empty? ? nil : url.chomp("/")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def placeholder?(url = base_url)
|
|
55
|
+
return true if url.nil? || url.empty?
|
|
56
|
+
|
|
57
|
+
PLACEHOLDER_MARKERS.any? { |marker| url.include?(marker) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def legacy_gcs?(url = base_url)
|
|
61
|
+
return false if url.nil? || url.empty?
|
|
62
|
+
|
|
63
|
+
url.include?(LEGACY_GCS_HOST)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configured_for_live_checks?
|
|
67
|
+
url = base_url
|
|
68
|
+
!url.nil? && !url.empty? && !placeholder?(url) && !legacy_gcs?(url)
|
|
69
|
+
end
|
|
70
|
+
end
|