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,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
module Pandorga
|
|
7
|
+
module Commands
|
|
8
|
+
# Delegate to the full content export pipeline shipped with the gem.
|
|
9
|
+
class Export
|
|
10
|
+
def run(argv)
|
|
11
|
+
site_root = Pathname.new(Dir.pwd).expand_path
|
|
12
|
+
script = Pathname.new(Pandorga.gem_path("scripts/content/export-content-json.rb"))
|
|
13
|
+
unless script.file?
|
|
14
|
+
warn "error: export script missing at #{script}"
|
|
15
|
+
return 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
content = argv[0] || "content"
|
|
19
|
+
dest = argv[1] || "_content_json"
|
|
20
|
+
env = ENV.to_h.merge(
|
|
21
|
+
"PANDORGA_SITE_ROOT" => site_root.to_s,
|
|
22
|
+
"RUBYOPT" => ["-I#{Pandorga.gem_path('lib')}", ENV["RUBYOPT"]].compact.join(" ")
|
|
23
|
+
)
|
|
24
|
+
cmd = [RbConfig.ruby, script.to_s, content, dest]
|
|
25
|
+
puts "pandorga export: #{content} → #{dest} (site #{site_root})"
|
|
26
|
+
system(env, *cmd, chdir: site_root.to_s) ? 0 : 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Pandorga
|
|
7
|
+
module Commands
|
|
8
|
+
# Option A (PLT-0.2): copy functions/ and studio/ from the gem into the site root
|
|
9
|
+
# so Cloudflare Pages can compile Functions from the project root.
|
|
10
|
+
class InstallFunctions
|
|
11
|
+
STAMP = ".pandorga-version"
|
|
12
|
+
|
|
13
|
+
def run(argv)
|
|
14
|
+
dest_root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
15
|
+
gem_functions = Pathname.new(Pandorga.gem_path("functions"))
|
|
16
|
+
gem_studio = Pathname.new(Pandorga.gem_path("studio"))
|
|
17
|
+
|
|
18
|
+
unless gem_functions.directory?
|
|
19
|
+
warn "error: gem has no functions/ directory at #{gem_functions}"
|
|
20
|
+
return 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
copy_tree(gem_functions, dest_root.join("functions"))
|
|
24
|
+
copy_tree(gem_studio, dest_root.join("studio")) if gem_studio.directory?
|
|
25
|
+
|
|
26
|
+
stamp = dest_root.join("functions", STAMP)
|
|
27
|
+
stamp.write("#{Pandorga::VERSION}\n")
|
|
28
|
+
puts "pandorga install-functions: copied to #{dest_root} (v#{Pandorga::VERSION})"
|
|
29
|
+
0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def copy_tree(src, dest)
|
|
35
|
+
FileUtils.rm_rf(dest) if dest.exist?
|
|
36
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
37
|
+
FileUtils.cp_r(src, dest)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module Pandorga
|
|
8
|
+
module Commands
|
|
9
|
+
# Scaffold a new site from examples/minimal.
|
|
10
|
+
class New
|
|
11
|
+
EXCLUDE = %w[
|
|
12
|
+
Gemfile.lock
|
|
13
|
+
_site
|
|
14
|
+
vendor
|
|
15
|
+
.bundle
|
|
16
|
+
_content_json
|
|
17
|
+
.jekyll-cache
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
GEMFILE = <<~RUBY
|
|
21
|
+
# frozen_string_literal: true
|
|
22
|
+
|
|
23
|
+
source "https://rubygems.org"
|
|
24
|
+
|
|
25
|
+
gem "pandorga", "~> 1.2"
|
|
26
|
+
# Local checkout: gem "pandorga", path: "../pandorga"
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.2.1"
|
|
28
|
+
gem "csv"
|
|
29
|
+
gem "base64"
|
|
30
|
+
RUBY
|
|
31
|
+
|
|
32
|
+
def run(argv)
|
|
33
|
+
force = false
|
|
34
|
+
parser = OptionParser.new do |opts|
|
|
35
|
+
opts.banner = "Usage: pandorga new PATH [--force]"
|
|
36
|
+
opts.on("--force", "Overwrite an existing directory") { force = true }
|
|
37
|
+
opts.on("-h", "--help", "Show this help") do
|
|
38
|
+
puts opts
|
|
39
|
+
return 0
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
parser.parse!(argv)
|
|
43
|
+
|
|
44
|
+
raw = argv[0]
|
|
45
|
+
if raw.nil? || raw.strip.empty?
|
|
46
|
+
warn "error: PATH is required"
|
|
47
|
+
warn parser.banner
|
|
48
|
+
return 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
dest = Pathname.new(raw).expand_path
|
|
52
|
+
src = Pathname.new(Pandorga.gem_path("examples/minimal"))
|
|
53
|
+
unless src.directory?
|
|
54
|
+
warn "error: scaffold source missing at #{src}"
|
|
55
|
+
return 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if dest.exist?
|
|
59
|
+
nonempty = dest.directory? && dest.children.any?
|
|
60
|
+
if nonempty && !force
|
|
61
|
+
warn "error: #{dest} is not empty (pass --force to overwrite)"
|
|
62
|
+
return 1
|
|
63
|
+
end
|
|
64
|
+
if force
|
|
65
|
+
FileUtils.rm_rf(dest)
|
|
66
|
+
elsif dest.file?
|
|
67
|
+
warn "error: #{dest} exists and is a file"
|
|
68
|
+
return 1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
FileUtils.mkdir_p(dest)
|
|
73
|
+
copy_scaffold(src, dest)
|
|
74
|
+
dest.join("Gemfile").write(GEMFILE)
|
|
75
|
+
|
|
76
|
+
puts "Created site at #{dest}"
|
|
77
|
+
puts
|
|
78
|
+
puts "Next steps:"
|
|
79
|
+
puts " cd #{shell_path(dest)}"
|
|
80
|
+
puts " bundle install"
|
|
81
|
+
puts " bundle exec pandorga doctor"
|
|
82
|
+
puts " bundle exec pandorga serve"
|
|
83
|
+
0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def copy_scaffold(src, dest)
|
|
89
|
+
src.each_child do |child|
|
|
90
|
+
name = child.basename.to_s
|
|
91
|
+
next if EXCLUDE.include?(name)
|
|
92
|
+
next if name == "Gemfile" # rewritten after copy
|
|
93
|
+
|
|
94
|
+
target = dest.join(name)
|
|
95
|
+
if child.directory?
|
|
96
|
+
FileUtils.mkdir_p(target)
|
|
97
|
+
copy_scaffold(child, target)
|
|
98
|
+
else
|
|
99
|
+
FileUtils.cp(child, target)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def shell_path(pathname)
|
|
105
|
+
begin
|
|
106
|
+
pathname.relative_path_from(Pathname.pwd).to_s
|
|
107
|
+
rescue ArgumentError
|
|
108
|
+
pathname.to_s
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require_relative "../object_store_env"
|
|
6
|
+
|
|
7
|
+
module Pandorga
|
|
8
|
+
module Commands
|
|
9
|
+
# Publish content JSON to an S3-compatible object store (R2, MinIO, AWS S3, …).
|
|
10
|
+
class Publish
|
|
11
|
+
def run(argv)
|
|
12
|
+
root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
13
|
+
site_script = root.join("scripts", "content", "publish-content-to-r2.sh")
|
|
14
|
+
if site_script.file? && site_script.expand_path != gem_publish_script
|
|
15
|
+
Dir.chdir(root) do
|
|
16
|
+
exec("bash", site_script.to_s, *argv[1..])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
ObjectStoreEnv.require!(:endpoint, :bucket, :access_key_id, :secret_access_key)
|
|
22
|
+
rescue ArgumentError => e
|
|
23
|
+
warn "error: #{e.message}"
|
|
24
|
+
warn "hint: set S3_* (or legacy R2_*) variables for any S3-compatible bucket"
|
|
25
|
+
return 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
ObjectStoreEnv.apply_aws_env!
|
|
29
|
+
|
|
30
|
+
export_status = Export.new.run(
|
|
31
|
+
[root.join("content").to_s, root.join("_content_json").to_s]
|
|
32
|
+
)
|
|
33
|
+
return export_status if export_status != 0
|
|
34
|
+
|
|
35
|
+
script = gem_publish_script
|
|
36
|
+
unless script.file?
|
|
37
|
+
warn "error: publish script missing at #{script}"
|
|
38
|
+
return 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
env = ENV.to_h.merge("PANDORGA_SITE_ROOT" => root.to_s)
|
|
42
|
+
puts "pandorga publish: syncing #{root.join('_content_json')} → object store"
|
|
43
|
+
system(env, "bash", script.to_s, root.join("_content_json").to_s, chdir: root.to_s) ? 0 : 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def gem_publish_script
|
|
49
|
+
Pathname.new(Pandorga.gem_path("scripts/content/publish-content-to-object-store.sh"))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
module Pandorga
|
|
7
|
+
module Commands
|
|
8
|
+
# Local development: export JSON, then jekyll serve with unpublished content.
|
|
9
|
+
class Serve
|
|
10
|
+
def run(argv)
|
|
11
|
+
root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
12
|
+
Dir.chdir(root) do
|
|
13
|
+
export_status = Export.new.run(%w[content _content_json])
|
|
14
|
+
return export_status if export_status != 0
|
|
15
|
+
|
|
16
|
+
config = %w[_config.yml]
|
|
17
|
+
config << "_config_dev.yml" if root.join("_config_dev.yml").file?
|
|
18
|
+
env = ENV.to_h.merge(
|
|
19
|
+
"CONTENT_API_BASE_URL" => "",
|
|
20
|
+
"JEKYLL_ENV" => "development",
|
|
21
|
+
"PANDORGA_SITE_ROOT" => root.to_s
|
|
22
|
+
)
|
|
23
|
+
cmd = [
|
|
24
|
+
"bundle", "exec", "jekyll", "serve",
|
|
25
|
+
"--unpublished",
|
|
26
|
+
"--config", config.join(",")
|
|
27
|
+
]
|
|
28
|
+
puts "pandorga serve: #{cmd.join(' ')}"
|
|
29
|
+
exec(env, *cmd)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "date"
|
|
7
|
+
|
|
8
|
+
module Pandorga
|
|
9
|
+
module Commands
|
|
10
|
+
# Compose studio/schema.yml from template studio.yml files + registry.
|
|
11
|
+
# Refs: PLT-AC-3, docs/spec.md §5.5
|
|
12
|
+
class StudioSchema
|
|
13
|
+
DEFAULT_MEDIA = {
|
|
14
|
+
"name" => "media",
|
|
15
|
+
"label" => "Media",
|
|
16
|
+
"input" => "content/media",
|
|
17
|
+
"output" => "/media",
|
|
18
|
+
"categories" => %w[image video plot],
|
|
19
|
+
"rename" => "safe"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
DEFAULT_MARKDOWN_BODY = {
|
|
23
|
+
"type" => "code",
|
|
24
|
+
"label" => "Body",
|
|
25
|
+
"description" =>
|
|
26
|
+
"Markdown source (code editor). Paste Liquid {% include %} markers " \
|
|
27
|
+
"as-is — figures, citations, embeds.",
|
|
28
|
+
"options" => { "format" => "markdown" }
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def run(argv)
|
|
32
|
+
root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
33
|
+
templates_root = Pathname.new(Pandorga.gem_path("templates"))
|
|
34
|
+
config_path = root.join("_config.yml")
|
|
35
|
+
unless config_path.file?
|
|
36
|
+
warn "error: no _config.yml in #{root}"
|
|
37
|
+
return 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
config = YAML.safe_load(config_path.read, permitted_classes: [Date]) || {}
|
|
41
|
+
schema = compose(config, templates_root)
|
|
42
|
+
|
|
43
|
+
overrides_path = root.join("studio.overrides.yml")
|
|
44
|
+
if overrides_path.file?
|
|
45
|
+
extra = YAML.safe_load(overrides_path.read, permitted_classes: [Date]) || {}
|
|
46
|
+
apply_overrides!(schema, extra) if extra.is_a?(Hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
out = root.join("studio", "schema.yml")
|
|
50
|
+
FileUtils.mkdir_p(out.dirname)
|
|
51
|
+
out.write(YAML.dump(schema))
|
|
52
|
+
puts "pandorga studio-schema: wrote #{out}"
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Public for gates: build the schema hash without writing.
|
|
57
|
+
def compose(config, templates_root)
|
|
58
|
+
pages = config.dig("pandorga", "pages") || []
|
|
59
|
+
taxonomies = config.dig("pandorga", "taxonomies") || {}
|
|
60
|
+
groups = {}
|
|
61
|
+
group_order = []
|
|
62
|
+
needs_media = false
|
|
63
|
+
needs_markdown = false
|
|
64
|
+
collection_index = {}
|
|
65
|
+
|
|
66
|
+
pages.each do |page|
|
|
67
|
+
next unless page.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
template = page["template"].to_s
|
|
70
|
+
next if template.empty?
|
|
71
|
+
|
|
72
|
+
studio_yml = Pathname.new(templates_root).join(template, "studio.yml")
|
|
73
|
+
next unless studio_yml.file?
|
|
74
|
+
|
|
75
|
+
fragment = YAML.safe_load(studio_yml.read, permitted_classes: [Date]) || {}
|
|
76
|
+
next unless fragment.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
needs_media ||= truthy?(fragment["needs_media"])
|
|
79
|
+
contributions = expand_fragment(fragment, page, taxonomies)
|
|
80
|
+
contributions.each do |contrib|
|
|
81
|
+
group_name = contrib[:group]
|
|
82
|
+
unless groups.key?(group_name)
|
|
83
|
+
groups[group_name] = {
|
|
84
|
+
"name" => group_name,
|
|
85
|
+
"label" => contrib[:group_label],
|
|
86
|
+
"type" => "group",
|
|
87
|
+
"items" => []
|
|
88
|
+
}
|
|
89
|
+
group_order << group_name
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
contrib[:items].each do |item|
|
|
93
|
+
needs_markdown ||= uses_markdown_body?(item)
|
|
94
|
+
groups[group_name]["items"] << item
|
|
95
|
+
if item["type"].to_s == "collection"
|
|
96
|
+
# Deep copy so YAML.dump does not emit aliases shared with content.
|
|
97
|
+
collection_index[item["name"].to_s] = deep_dup(item)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
schema = {
|
|
104
|
+
"version" => 1,
|
|
105
|
+
"generated_by" => "pandorga studio-schema #{Pandorga::VERSION}",
|
|
106
|
+
"content" => group_order.map { |name| groups[name] },
|
|
107
|
+
"collections" => collection_index
|
|
108
|
+
}
|
|
109
|
+
schema["media"] = deep_dup(DEFAULT_MEDIA) if needs_media
|
|
110
|
+
if needs_markdown
|
|
111
|
+
schema["components"] = { "markdown_body" => deep_dup(DEFAULT_MARKDOWN_BODY) }
|
|
112
|
+
end
|
|
113
|
+
schema
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def expand_fragment(fragment, page, taxonomies)
|
|
119
|
+
contributions = []
|
|
120
|
+
default_group = fragment["group"].to_s
|
|
121
|
+
default_group = page["template"].to_s if default_group.empty?
|
|
122
|
+
default_label = fragment["group_label"].to_s
|
|
123
|
+
default_label = default_group.capitalize if default_label.empty?
|
|
124
|
+
|
|
125
|
+
items = []
|
|
126
|
+
|
|
127
|
+
if fragment["collection"].is_a?(Hash)
|
|
128
|
+
name = page["collection"].to_s
|
|
129
|
+
unless name.empty?
|
|
130
|
+
items << instantiate_collection(
|
|
131
|
+
fragment["collection"],
|
|
132
|
+
name,
|
|
133
|
+
page,
|
|
134
|
+
taxonomies
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if fragment["collections"].is_a?(Hash)
|
|
140
|
+
slots = page["collections"].is_a?(Hash) ? page["collections"] : {}
|
|
141
|
+
fragment["collections"].each do |slot, spec|
|
|
142
|
+
next unless spec.is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
name = slots[slot].to_s
|
|
145
|
+
name = slot.to_s if name.empty?
|
|
146
|
+
next if name.empty? || name == "data" || name == "profile"
|
|
147
|
+
|
|
148
|
+
items << instantiate_collection(spec, name, page, taxonomies)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
Array(fragment["files"]).each do |file_spec|
|
|
153
|
+
next unless file_spec.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
items << deep_dup(file_spec)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
unless items.empty?
|
|
159
|
+
contributions << {
|
|
160
|
+
group: default_group,
|
|
161
|
+
group_label: default_label,
|
|
162
|
+
items: items
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
profile = fragment["profile"]
|
|
167
|
+
if profile.is_a?(Hash)
|
|
168
|
+
profile_items = Array(profile["files"]).select { |f| f.is_a?(Hash) }.map { |f| deep_dup(f) }
|
|
169
|
+
unless profile_items.empty?
|
|
170
|
+
contributions << {
|
|
171
|
+
group: profile["group"].to_s.empty? ? "profile" : profile["group"].to_s,
|
|
172
|
+
group_label: profile["group_label"].to_s.empty? ? "Profile" : profile["group_label"].to_s,
|
|
173
|
+
items: profile_items
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
contributions
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def instantiate_collection(spec, name, page, taxonomies)
|
|
182
|
+
entry = deep_dup(spec)
|
|
183
|
+
entry["name"] = name
|
|
184
|
+
entry["type"] ||= "collection"
|
|
185
|
+
if page["collection"].to_s == name && !page["title"].to_s.empty?
|
|
186
|
+
entry["label"] = page["title"].to_s
|
|
187
|
+
elsif entry["label"].to_s.empty?
|
|
188
|
+
entry["label"] = name.capitalize
|
|
189
|
+
end
|
|
190
|
+
entry["path"] ||= "content/collections/#{name}"
|
|
191
|
+
entry["path"] = entry["path"].to_s.gsub("{collection}", name)
|
|
192
|
+
apply_taxonomy!(entry, name, taxonomies)
|
|
193
|
+
entry
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def apply_taxonomy!(entry, collection_name, taxonomies)
|
|
197
|
+
tax = taxonomies[collection_name]
|
|
198
|
+
return unless tax.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
Array(entry["fields"]).each do |field|
|
|
201
|
+
next unless field.is_a?(Hash)
|
|
202
|
+
next unless field["name"].to_s == "tags"
|
|
203
|
+
|
|
204
|
+
field["options"] ||= {}
|
|
205
|
+
next unless field["options"].is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
tags = tax["tags"]
|
|
208
|
+
field["options"]["values"] = tags if tags.is_a?(Array) && !tags.empty?
|
|
209
|
+
max = tax["max"]
|
|
210
|
+
field["options"]["max"] = max if max
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def apply_overrides!(schema, extra)
|
|
215
|
+
if extra["media"].is_a?(Hash)
|
|
216
|
+
schema["media"] = deep_merge(schema["media"] || {}, extra["media"])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if extra["components"].is_a?(Hash)
|
|
220
|
+
schema["components"] = deep_merge(schema["components"] || {}, extra["components"])
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
if extra["collections"].is_a?(Hash)
|
|
224
|
+
extra["collections"].each do |name, patch|
|
|
225
|
+
next unless patch.is_a?(Hash)
|
|
226
|
+
|
|
227
|
+
item = find_content_item(schema, name.to_s)
|
|
228
|
+
if item
|
|
229
|
+
merged = deep_merge(item, patch)
|
|
230
|
+
item.replace(merged)
|
|
231
|
+
end
|
|
232
|
+
if schema["collections"].is_a?(Hash) && schema["collections"][name.to_s]
|
|
233
|
+
schema["collections"][name.to_s] =
|
|
234
|
+
deep_merge(schema["collections"][name.to_s], patch)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
return unless extra["content"].is_a?(Array)
|
|
240
|
+
|
|
241
|
+
schema["content"] ||= []
|
|
242
|
+
extra["content"].each do |group|
|
|
243
|
+
next unless group.is_a?(Hash)
|
|
244
|
+
|
|
245
|
+
existing = schema["content"].find { |g| g["name"].to_s == group["name"].to_s }
|
|
246
|
+
if existing
|
|
247
|
+
existing.replace(deep_merge(existing, group))
|
|
248
|
+
else
|
|
249
|
+
schema["content"] << deep_dup(group)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def find_content_item(schema, name)
|
|
255
|
+
Array(schema["content"]).each do |group|
|
|
256
|
+
next unless group.is_a?(Hash)
|
|
257
|
+
|
|
258
|
+
Array(group["items"]).each do |item|
|
|
259
|
+
return item if item.is_a?(Hash) && item["name"].to_s == name
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def uses_markdown_body?(node)
|
|
266
|
+
case node
|
|
267
|
+
when Hash
|
|
268
|
+
return true if node["component"].to_s == "markdown_body"
|
|
269
|
+
|
|
270
|
+
node.any? { |_, v| uses_markdown_body?(v) }
|
|
271
|
+
when Array
|
|
272
|
+
node.any? { |v| uses_markdown_body?(v) }
|
|
273
|
+
else
|
|
274
|
+
false
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def truthy?(value)
|
|
279
|
+
value == true || value.to_s == "true"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def deep_dup(obj)
|
|
283
|
+
case obj
|
|
284
|
+
when Hash
|
|
285
|
+
obj.transform_values { |v| deep_dup(v) }
|
|
286
|
+
when Array
|
|
287
|
+
obj.map { |v| deep_dup(v) }
|
|
288
|
+
else
|
|
289
|
+
obj
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def deep_merge(left, right)
|
|
294
|
+
return deep_dup(right) unless left.is_a?(Hash) && right.is_a?(Hash)
|
|
295
|
+
|
|
296
|
+
result = deep_dup(left)
|
|
297
|
+
right.each do |key, value|
|
|
298
|
+
result[key] =
|
|
299
|
+
if result.key?(key) && result[key].is_a?(Hash) && value.is_a?(Hash)
|
|
300
|
+
deep_merge(result[key], value)
|
|
301
|
+
else
|
|
302
|
+
deep_dup(value)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
result
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jekyll"
|
|
4
|
+
require_relative "../registry"
|
|
5
|
+
|
|
6
|
+
module Pandorga
|
|
7
|
+
module Jekyll
|
|
8
|
+
# Generates listing pages and nav data from pandorga.pages (PLT-AC-3).
|
|
9
|
+
class Generator < ::Jekyll::Generator
|
|
10
|
+
safe true
|
|
11
|
+
priority :high
|
|
12
|
+
|
|
13
|
+
# Template defaults when registry omits `detail` (English platform paths).
|
|
14
|
+
DEFAULT_DETAILS = {
|
|
15
|
+
"articles" => "/articles/:slug/",
|
|
16
|
+
"blog" => "/posts/:slug/",
|
|
17
|
+
"portfolio" => "/projects/:slug/"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def generate(site)
|
|
21
|
+
pages = PandorgaRegistry.pages(site.config)
|
|
22
|
+
return if pages.empty?
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
PandorgaRegistry.validate!(pages)
|
|
26
|
+
rescue ArgumentError => e
|
|
27
|
+
raise ::Jekyll::Errors::FatalException, e.message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Website shell reads site.data.nav (label/pages/key shape).
|
|
31
|
+
site.data["nav"] = PandorgaRegistry.build_nav(pages)
|
|
32
|
+
site.data["pandorga_pages"] = pages
|
|
33
|
+
site.data["pandorga_nav"] = site.data["nav"]
|
|
34
|
+
site.data["pandorga_home_bands"] = PandorgaRegistry.home_bands(pages)
|
|
35
|
+
|
|
36
|
+
existing_keys = site.pages.map { |p| p.data["key"] }.compact
|
|
37
|
+
pages.each do |entry|
|
|
38
|
+
next unless entry.is_a?(Hash)
|
|
39
|
+
next if existing_keys.include?(entry["key"])
|
|
40
|
+
|
|
41
|
+
emit_page(site, entry)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def emit_page(site, entry)
|
|
48
|
+
path = entry["path"].to_s
|
|
49
|
+
return if path.empty?
|
|
50
|
+
|
|
51
|
+
listing_path = path.end_with?("/") ? path : "#{path}/"
|
|
52
|
+
rel_dir = path.sub(%r{\A/}, "").sub(%r{/\z}, "")
|
|
53
|
+
page = ::Jekyll::PageWithoutAFile.new(site, site.source, rel_dir, "index.html")
|
|
54
|
+
page.data["layout"] = PandorgaRegistry.layout_for(entry)
|
|
55
|
+
page.data["key"] = entry["key"]
|
|
56
|
+
page.data["template"] = entry["template"] if entry["template"]
|
|
57
|
+
page.data["title"] = entry["title"] if entry["title"]
|
|
58
|
+
page.data["description"] = entry["description"] if entry["description"]
|
|
59
|
+
page.data["accent"] = entry["accent"] if entry["accent"]
|
|
60
|
+
page.data["badge"] = entry["badge"] if entry["badge"]
|
|
61
|
+
page.data["lang"] = entry["language"] if entry["language"]
|
|
62
|
+
page.data["permalink"] = listing_path
|
|
63
|
+
# listing_path is explicit: Jekyll reserves page.path for the source file.
|
|
64
|
+
page.data["listing_path"] = listing_path
|
|
65
|
+
page.data["collection"] = entry["collection"] if entry["collection"]
|
|
66
|
+
detail = entry["detail"].to_s
|
|
67
|
+
detail = DEFAULT_DETAILS[entry["template"].to_s].to_s if detail.empty?
|
|
68
|
+
page.data["detail"] = detail unless detail.empty?
|
|
69
|
+
page.content = ""
|
|
70
|
+
site.pages << page
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|