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,72 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# An unbalanced brace in CSS does not fail a build and does not error in a
|
|
5
|
+
# browser. The parser simply swallows everything after it into the open block,
|
|
6
|
+
# so a stylesheet keeps loading with a 200 while an arbitrary number of its
|
|
7
|
+
# rules silently stop applying. That is exactly what a bad merge resolution
|
|
8
|
+
# produced here: the page rendered, the console was clean, and a third of one
|
|
9
|
+
# sheet had quietly stopped existing.
|
|
10
|
+
#
|
|
11
|
+
# Also catches conflict markers, which are valid-enough text to survive a
|
|
12
|
+
# build and are far easier to spot here than on a rendered page.
|
|
13
|
+
|
|
14
|
+
require "pathname"
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
17
|
+
CSS_ROOT = REPO_ROOT.join("assets/css")
|
|
18
|
+
|
|
19
|
+
CONFLICT_MARKERS = /^(?:<{7}|={7}|>{7})(?:\s|$)/.freeze
|
|
20
|
+
|
|
21
|
+
def fail!(message)
|
|
22
|
+
warn message
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Strip comments and strings so braces inside them are not counted.
|
|
27
|
+
def strip_noise(source)
|
|
28
|
+
out = source.gsub(%r{/\*.*?\*/}m, "")
|
|
29
|
+
out.gsub(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/, '""')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "Testing CSS structure..."
|
|
33
|
+
|
|
34
|
+
sheets = Dir.glob(CSS_ROOT.join("**", "*.css")).sort
|
|
35
|
+
fail!("no stylesheets found under #{CSS_ROOT}") if sheets.empty?
|
|
36
|
+
|
|
37
|
+
errors = []
|
|
38
|
+
|
|
39
|
+
sheets.each do |path|
|
|
40
|
+
relative = Pathname.new(path).relative_path_from(REPO_ROOT)
|
|
41
|
+
source = File.read(path)
|
|
42
|
+
|
|
43
|
+
source.each_line.with_index(1) do |line, number|
|
|
44
|
+
next unless line.match?(CONFLICT_MARKERS)
|
|
45
|
+
|
|
46
|
+
errors << "#{relative}:#{number}: unresolved conflict marker"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
cleaned = strip_noise(source)
|
|
50
|
+
depth = 0
|
|
51
|
+
negative_at = nil
|
|
52
|
+
|
|
53
|
+
cleaned.each_line.with_index(1) do |line, number|
|
|
54
|
+
depth += line.count("{") - line.count("}")
|
|
55
|
+
if depth.negative? && negative_at.nil?
|
|
56
|
+
negative_at = number
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if negative_at
|
|
62
|
+
errors << "#{relative}:#{negative_at}: closing brace with no matching opener"
|
|
63
|
+
elsif depth.positive?
|
|
64
|
+
errors << "#{relative}: #{depth} unclosed brace(s) — every rule after the " \
|
|
65
|
+
"offending block is swallowed and silently stops applying"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
|
|
70
|
+
|
|
71
|
+
puts " #{sheets.size} stylesheet(s) balanced, no conflict markers."
|
|
72
|
+
puts "All CSS structure tests passed."
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Refs: PLT-AC-7
|
|
5
|
+
# CV collection fields outside the public allowlist must not enter export JSON.
|
|
6
|
+
# Uses a temp Ada Example fixture — no personal site data.
|
|
7
|
+
|
|
8
|
+
$stdout.sync = true
|
|
9
|
+
$stderr.sync = true
|
|
10
|
+
|
|
11
|
+
require "json"
|
|
12
|
+
require "open3"
|
|
13
|
+
require "pathname"
|
|
14
|
+
require "tmpdir"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
|
|
17
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
18
|
+
EXPORT_SCRIPT = ROOT.join("scripts/content/export-content-json.rb")
|
|
19
|
+
|
|
20
|
+
PRIVATE_SNIPPETS = [
|
|
21
|
+
"PRIVATE_EXTENDED_JOB_BODY_MARKER_ada_example",
|
|
22
|
+
"PRIVATE_SINGLE_PAGE_JOB_BODY_MARKER_ada_example",
|
|
23
|
+
"PRIVATE_CONTACT_SECRET_ada@secret.example",
|
|
24
|
+
"PRIVATE_EXTENDED_PRODUCT_BODY_MARKER_ada_example",
|
|
25
|
+
"PRIVATE_PRODUCT_NOTES_ada_example"
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def fail!(message)
|
|
29
|
+
warn "FAIL test-cv-export-contract: #{message}"
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check!(condition, message)
|
|
34
|
+
fail!(message) unless condition
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def write_site!(site)
|
|
38
|
+
site.join("_config.yml").write(<<~YAML)
|
|
39
|
+
title: "CV Export Contract Fixture"
|
|
40
|
+
pandorga:
|
|
41
|
+
identity:
|
|
42
|
+
name: "Ada Example"
|
|
43
|
+
short_name: "A. Example"
|
|
44
|
+
url: "https://example.org"
|
|
45
|
+
locale: en
|
|
46
|
+
content:
|
|
47
|
+
backend: static
|
|
48
|
+
base_url: ""
|
|
49
|
+
pages:
|
|
50
|
+
- key: cv
|
|
51
|
+
template: cv
|
|
52
|
+
title: Curriculum Vitæ
|
|
53
|
+
path: /pages/cv/
|
|
54
|
+
collections: { jobs: jobs, products: products }
|
|
55
|
+
home: { band: hero }
|
|
56
|
+
YAML
|
|
57
|
+
|
|
58
|
+
jobs = site.join("content/collections/jobs")
|
|
59
|
+
products = site.join("content/collections/products")
|
|
60
|
+
data = site.join("content/collections/data")
|
|
61
|
+
FileUtils.mkdir_p([jobs, products, data])
|
|
62
|
+
|
|
63
|
+
jobs.join("research-fellow.md").write(<<~MD)
|
|
64
|
+
---
|
|
65
|
+
title: Research Fellow
|
|
66
|
+
key: research-fellow
|
|
67
|
+
company: Example Labs
|
|
68
|
+
role: Research Fellow
|
|
69
|
+
start: 2024-01
|
|
70
|
+
end: 2026-06
|
|
71
|
+
location: Example City
|
|
72
|
+
summary: Fictional public CV entry for Ada Example.
|
|
73
|
+
body_public: Public paragraph that may appear in the export.
|
|
74
|
+
body_extended: |
|
|
75
|
+
PRIVATE_EXTENDED_JOB_BODY_MARKER_ada_example
|
|
76
|
+
Extended copy for private PDF builds only.
|
|
77
|
+
body_single_page: |
|
|
78
|
+
PRIVATE_SINGLE_PAGE_JOB_BODY_MARKER_ada_example
|
|
79
|
+
Single-page private PDF copy.
|
|
80
|
+
contact_email: PRIVATE_CONTACT_SECRET_ada@secret.example
|
|
81
|
+
published: true
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
Markdown body for Ada Example research role.
|
|
85
|
+
MD
|
|
86
|
+
|
|
87
|
+
products.join("ledger-tool.md").write(<<~MD)
|
|
88
|
+
---
|
|
89
|
+
title: Ledger Tool
|
|
90
|
+
key: ledger-tool
|
|
91
|
+
product: Ledger Tool
|
|
92
|
+
company: Example Labs
|
|
93
|
+
body_extended: PRIVATE_EXTENDED_PRODUCT_BODY_MARKER_ada_example
|
|
94
|
+
private_notes: PRIVATE_PRODUCT_NOTES_ada_example
|
|
95
|
+
published: true
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
Public product summary for Ada Example.
|
|
99
|
+
MD
|
|
100
|
+
|
|
101
|
+
# Public contacts only. Secrets belong on collection fields outside the
|
|
102
|
+
# allowlist (contact_email above), not in committed public data files.
|
|
103
|
+
data.join("contacts.yml").write(<<~YAML)
|
|
104
|
+
- label: Example Org
|
|
105
|
+
href: https://example.org
|
|
106
|
+
YAML
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_export!(site, out)
|
|
110
|
+
env = {
|
|
111
|
+
"PANDORGA_SITE_ROOT" => site.to_s,
|
|
112
|
+
"CONTENT_API_BASE_URL" => "",
|
|
113
|
+
"CONTENT_EXPORT_SKIP_VALIDATE" => "1",
|
|
114
|
+
"RUBYOPT" => "-I#{ROOT.join('lib')}"
|
|
115
|
+
}
|
|
116
|
+
stdout, stderr, status = Open3.capture3(
|
|
117
|
+
env,
|
|
118
|
+
"ruby", EXPORT_SCRIPT.to_s, "content", out.to_s,
|
|
119
|
+
chdir: site.to_s
|
|
120
|
+
)
|
|
121
|
+
fail!("export failed:\n#{stderr}\n#{stdout}") unless status.success?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts "Testing CV export contract (PLT-AC-7)..."
|
|
125
|
+
|
|
126
|
+
Dir.mktmpdir("cv-export-contract-") do |dir|
|
|
127
|
+
site = Pathname.new(dir).join("site")
|
|
128
|
+
out = Pathname.new(dir).join("export")
|
|
129
|
+
site.mkpath
|
|
130
|
+
write_site!(site)
|
|
131
|
+
run_export!(site, out)
|
|
132
|
+
|
|
133
|
+
job_json = out.join("collections/jobs/research-fellow.json")
|
|
134
|
+
product_json = out.join("collections/products/ledger-tool.json")
|
|
135
|
+
check!(job_json.file?, "missing exported job JSON")
|
|
136
|
+
check!(product_json.file?, "missing exported product JSON")
|
|
137
|
+
|
|
138
|
+
job = JSON.parse(job_json.read)
|
|
139
|
+
product = JSON.parse(product_json.read)
|
|
140
|
+
job_fm = job["front_matter"] || {}
|
|
141
|
+
product_fm = product["front_matter"] || {}
|
|
142
|
+
|
|
143
|
+
%w[body_extended body_single_page contact_email].each do |field|
|
|
144
|
+
check!(!job_fm.key?(field), "job front_matter still has #{field}")
|
|
145
|
+
end
|
|
146
|
+
check!(!job_fm["body_public"].to_s.strip.empty?, "job dropped body_public")
|
|
147
|
+
|
|
148
|
+
%w[body_extended private_notes].each do |field|
|
|
149
|
+
check!(!product_fm.key?(field), "product front_matter still has #{field}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
blob = out.glob("**/*").select(&:file?).map(&:read).join("\n")
|
|
153
|
+
PRIVATE_SNIPPETS.each do |snippet|
|
|
154
|
+
check!(!blob.include?(snippet), "private snippet leaked: #{snippet}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
puts "PASS test-cv-export-contract"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Gate: the build-time and browser detail views must emit the same contract.
|
|
5
|
+
#
|
|
6
|
+
# Detail pages exist twice. The public site assembles them in the browser from
|
|
7
|
+
# _includes/content-runtime/50-detail.html, which calls the shared ledger
|
|
8
|
+
# blocks. The private/studio build renders them at build time from
|
|
9
|
+
# _layouts/text.html, which reimplements the tag chips and the language pill
|
|
10
|
+
# in Liquid — it has no way to call the JS. One stylesheet,
|
|
11
|
+
# assets/css/ledger/detail.css, serves both, so a class renamed on one side
|
|
12
|
+
# silently unstyles the other, and the language table can drift into two
|
|
13
|
+
# different vocabularies without anything failing.
|
|
14
|
+
#
|
|
15
|
+
# The layout already states this contract in a comment. This makes it a gate.
|
|
16
|
+
|
|
17
|
+
require "open3"
|
|
18
|
+
require "pathname"
|
|
19
|
+
|
|
20
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
21
|
+
CHECKS = Pathname.new(__dir__).join("render-parity", "run-detail-parity-checks.mjs")
|
|
22
|
+
|
|
23
|
+
def fail!(message)
|
|
24
|
+
warn message
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "Testing detail view render parity (Liquid build vs browser runtime)..."
|
|
29
|
+
|
|
30
|
+
fail!("missing #{CHECKS}") unless CHECKS.file?
|
|
31
|
+
|
|
32
|
+
stdout, stderr, status = Open3.capture3("node", CHECKS.to_s, chdir: ROOT.join("scripts").to_s)
|
|
33
|
+
|
|
34
|
+
print stdout
|
|
35
|
+
unless status.success?
|
|
36
|
+
warn stderr unless stderr.strip.empty?
|
|
37
|
+
fail!("The two detail render paths have drifted apart.")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
puts "All detail render parity checks passed."
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "json"
|
|
6
|
+
require "ostruct"
|
|
7
|
+
|
|
8
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
9
|
+
require_relative "../../_plugins/content_local_serve"
|
|
10
|
+
require_relative "../../_plugins/local_detail_rewrites"
|
|
11
|
+
|
|
12
|
+
def fail!(msg)
|
|
13
|
+
warn "FAIL: #{msg}"
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def check!(condition, msg)
|
|
18
|
+
fail!(msg) unless condition
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
puts "Testing dev re-export feature..."
|
|
22
|
+
|
|
23
|
+
# 1. Check template file presence and key identifiers
|
|
24
|
+
include_file = REPO_ROOT.join("_includes/theme/dev-reexport.html")
|
|
25
|
+
check!(include_file.file?, "Missing _includes/theme/dev-reexport.html")
|
|
26
|
+
include_content = include_file.read
|
|
27
|
+
check!(include_content.include?('id="dev-reexport"'), "Missing id='dev-reexport' in include")
|
|
28
|
+
check!(include_content.include?('id="dev-reexport-btn"'), "Missing id='dev-reexport-btn' in include")
|
|
29
|
+
check!(include_content.include?('id="dev-reexport-label"'), "Missing id='dev-reexport-label' in include")
|
|
30
|
+
check!(include_content.include?('id="dev-reexport-icon"'), "Missing id='dev-reexport-icon' in include")
|
|
31
|
+
check!(include_content.include?("/__dev/reexport"), "Missing /__dev/reexport fetch in include")
|
|
32
|
+
check!(include_content.include?("text-tertiary"), "Missing text-tertiary token styling for dev button")
|
|
33
|
+
puts " ✓ Template structure and styling tokens verified"
|
|
34
|
+
|
|
35
|
+
# 2. Check shell layout includes it with guard
|
|
36
|
+
shell_file = REPO_ROOT.join("_layouts/shell.html")
|
|
37
|
+
shell_content = shell_file.read
|
|
38
|
+
check!(shell_content.include?("theme/dev-reexport.html"), "Missing dev-reexport.html inclusion in _layouts/shell.html")
|
|
39
|
+
check!(shell_content.match?(/site\.content_include_unpublished.*theme\/dev-reexport\.html/m),
|
|
40
|
+
"dev-reexport.html must be guarded by site.content_include_unpublished")
|
|
41
|
+
puts " ✓ Shell layout conditional guard verified"
|
|
42
|
+
|
|
43
|
+
# 3. Check that public build evaluation does not render dev-reexport button
|
|
44
|
+
require "liquid"
|
|
45
|
+
guard_snippet = "{% if site.content_include_unpublished or site.unpublished %}dev_present{% else %}dev_absent{% endif %}"
|
|
46
|
+
parsed_guard = Liquid::Template.parse(guard_snippet)
|
|
47
|
+
check!(parsed_guard.render("site" => {}) == "dev_absent",
|
|
48
|
+
"Public build (no dev flags) must NOT render dev-reexport button")
|
|
49
|
+
check!(parsed_guard.render("site" => { "content_include_unpublished" => true }) == "dev_present",
|
|
50
|
+
"Dev build with content_include_unpublished must render dev-reexport button")
|
|
51
|
+
check!(parsed_guard.render("site" => { "unpublished" => true }) == "dev_present",
|
|
52
|
+
"Dev build with unpublished must render dev-reexport button")
|
|
53
|
+
puts " ✓ Liquid conditional guard correctly excludes button in public builds and includes it in dev"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# 4. Check REEXPORT_PATHS definition in LocalDetailRewrites
|
|
57
|
+
check!(Jekyll::ContentDetailRewrites::REEXPORT_PATHS.include?("/__dev/reexport"),
|
|
58
|
+
"REEXPORT_PATHS must include /__dev/reexport")
|
|
59
|
+
check!(Jekyll::ContentDetailRewrites::REEXPORT_PATHS.include?("/api/dev/reexport"),
|
|
60
|
+
"REEXPORT_PATHS must include /api/dev/reexport")
|
|
61
|
+
puts " ✓ REEXPORT_PATHS verified"
|
|
62
|
+
|
|
63
|
+
# 5. Check handle_reexport_request permission guard
|
|
64
|
+
fake_res = {}
|
|
65
|
+
def fake_res.[]=(key, val)
|
|
66
|
+
@headers ||= {}
|
|
67
|
+
@headers[key] = val
|
|
68
|
+
end
|
|
69
|
+
def fake_res.status=(val)
|
|
70
|
+
@status = val
|
|
71
|
+
end
|
|
72
|
+
def fake_res.status
|
|
73
|
+
@status
|
|
74
|
+
end
|
|
75
|
+
def fake_res.body=(val)
|
|
76
|
+
@body = val
|
|
77
|
+
end
|
|
78
|
+
def fake_res.body
|
|
79
|
+
@body
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# When dev options are nil/empty: must be 403 Forbidden
|
|
83
|
+
Jekyll::ContentLocalServe.serve_options = nil
|
|
84
|
+
Jekyll::ContentLocalServe.site_config = nil
|
|
85
|
+
Jekyll::ContentLocalServe.handle_reexport_request(nil, fake_res)
|
|
86
|
+
check!(fake_res.status == 403, "handle_reexport_request must return 403 when dev flags are absent")
|
|
87
|
+
puts " ✓ Production permission guard returns 403"
|
|
88
|
+
|
|
89
|
+
# When dev option is present:
|
|
90
|
+
fake_res2 = {}
|
|
91
|
+
def fake_res2.[]=(key, val)
|
|
92
|
+
@headers ||= {}
|
|
93
|
+
@headers[key] = val
|
|
94
|
+
end
|
|
95
|
+
def fake_res2.status=(val)
|
|
96
|
+
@status = val
|
|
97
|
+
end
|
|
98
|
+
def fake_res2.status
|
|
99
|
+
@status
|
|
100
|
+
end
|
|
101
|
+
def fake_res2.body=(val)
|
|
102
|
+
@body = val
|
|
103
|
+
end
|
|
104
|
+
def fake_res2.body
|
|
105
|
+
@body
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
dev_options = {
|
|
109
|
+
"source" => REPO_ROOT.to_s,
|
|
110
|
+
"destination" => REPO_ROOT.join("_site").to_s,
|
|
111
|
+
"content_include_unpublished" => true,
|
|
112
|
+
"local_content_serve" => true,
|
|
113
|
+
"content_api_base_url" => ""
|
|
114
|
+
}
|
|
115
|
+
Jekyll::ContentLocalServe.serve_options = dev_options
|
|
116
|
+
Jekyll::ContentLocalServe.handle_reexport_request(nil, fake_res2)
|
|
117
|
+
check!(fake_res2.status == 200, "handle_reexport_request should return 200 in dev mode, got #{fake_res2.status}: #{fake_res2.body}")
|
|
118
|
+
parsed = JSON.parse(fake_res2.body)
|
|
119
|
+
check!(parsed["ok"] == true, "Response payload must have ok: true")
|
|
120
|
+
puts " ✓ Dev mode re-export execution returned 200 OK"
|
|
121
|
+
|
|
122
|
+
puts "All dev re-export tests passed!"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-0.3, PLT-AC-5
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "open3"
|
|
6
|
+
|
|
7
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
8
|
+
EXAMPLE = ROOT.join("examples/minimal")
|
|
9
|
+
EXE = ROOT.join("exe/pandorga")
|
|
10
|
+
|
|
11
|
+
def run_doctor(dir)
|
|
12
|
+
env = { "RUBYOPT" => "-I#{ROOT.join('lib')}" }
|
|
13
|
+
Open3.capture3(env, "ruby", EXE.to_s, "doctor", dir.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
stdout, stderr, status = run_doctor(EXAMPLE)
|
|
17
|
+
unless status.success?
|
|
18
|
+
warn "FAIL test-doctor: expected exit 0\n#{stdout}\n#{stderr}"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
unless stdout.include?("pandorga doctor: ok")
|
|
22
|
+
warn "FAIL test-doctor: unexpected stdout:\n#{stdout}"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
bad = ROOT.join("tmp/doctor-bad-fixture")
|
|
27
|
+
bad.mkpath
|
|
28
|
+
bad.join("_config.yml").write("title: x\n")
|
|
29
|
+
_out, err, st = run_doctor(bad)
|
|
30
|
+
if st.success?
|
|
31
|
+
warn "FAIL test-doctor: expected failure without pandorga: key"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
unless err.include?("pandorga:")
|
|
35
|
+
warn "FAIL test-doctor: expected error about pandorga: key\n#{err}"
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "PASS test-doctor"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-AC-4 — same template twice with different collections yields two pages.
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "jekyll"
|
|
8
|
+
|
|
9
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
10
|
+
$LOAD_PATH.unshift(ROOT.join("lib").to_s)
|
|
11
|
+
require "pandorga/registry"
|
|
12
|
+
require "pandorga/jekyll/generator"
|
|
13
|
+
|
|
14
|
+
FIXTURE = ROOT.join("scripts/test/fixtures/dual-template")
|
|
15
|
+
config_path = FIXTURE.join("_config.yml")
|
|
16
|
+
unless config_path.file?
|
|
17
|
+
warn "FAIL test-dual-template-instances: missing #{config_path}"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
site_yaml = YAML.safe_load(config_path.read, aliases: true)
|
|
22
|
+
pages = PandorgaRegistry.pages(site_yaml)
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
PandorgaRegistry.validate!(pages)
|
|
26
|
+
rescue ArgumentError => e
|
|
27
|
+
warn "FAIL test-dual-template-instances: registry rejected dual articles: #{e.message}"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
article_pages = pages.select { |p| p["template"].to_s == "articles" }
|
|
32
|
+
unless article_pages.length == 2
|
|
33
|
+
warn "FAIL test-dual-template-instances: expected 2 articles pages, got #{article_pages.length}"
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
collections = article_pages.map { |p| p["collection"].to_s }.uniq
|
|
38
|
+
paths = article_pages.map { |p| p["path"].to_s }.uniq
|
|
39
|
+
keys = article_pages.map { |p| p["key"].to_s }.uniq
|
|
40
|
+
unless collections.length == 2 && paths.length == 2 && keys.length == 2
|
|
41
|
+
warn "FAIL test-dual-template-instances: dual pages must differ in key, collection, and path"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
writing = PandorgaRegistry.writing_collections(pages)
|
|
46
|
+
collections.each do |name|
|
|
47
|
+
unless writing.key?(name)
|
|
48
|
+
warn "FAIL test-dual-template-instances: writing_collections missing #{name.inspect}"
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Dir.mktmpdir("pandorga-dual-template") do |dest|
|
|
54
|
+
jekyll_config = Jekyll.configuration(
|
|
55
|
+
"source" => FIXTURE.to_s,
|
|
56
|
+
"destination" => dest,
|
|
57
|
+
"quiet" => true,
|
|
58
|
+
"safe" => false
|
|
59
|
+
)
|
|
60
|
+
# Overlay pandorga registry from the fixture without requiring a theme build.
|
|
61
|
+
jekyll_config["pandorga"] = site_yaml["pandorga"]
|
|
62
|
+
|
|
63
|
+
site = Jekyll::Site.new(jekyll_config)
|
|
64
|
+
Pandorga::Jekyll::Generator.new.generate(site)
|
|
65
|
+
|
|
66
|
+
generated = site.pages.select { |p| keys.include?(p.data["key"].to_s) }
|
|
67
|
+
got_keys = generated.map { |p| p.data["key"].to_s }.sort
|
|
68
|
+
unless got_keys == keys.sort
|
|
69
|
+
warn "FAIL test-dual-template-instances: generator pages #{got_keys.inspect}, expected #{keys.sort.inspect}"
|
|
70
|
+
exit 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
got_paths = generated.map { |p| p.data["permalink"].to_s }.sort
|
|
74
|
+
expected_paths = paths.map { |p| p.end_with?("/") ? p : "#{p}/" }.sort
|
|
75
|
+
unless got_paths == expected_paths
|
|
76
|
+
warn "FAIL test-dual-template-instances: permalinks #{got_paths.inspect}, expected #{expected_paths.inspect}"
|
|
77
|
+
exit 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
layouts = generated.map { |p| p.data["layout"].to_s }.uniq
|
|
81
|
+
unless layouts == ["articles"]
|
|
82
|
+
warn "FAIL test-dual-template-instances: expected articles layout for both, got #{layouts.inspect}"
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
puts "PASS test-dual-template-instances"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-AC-4 — dual template instances emit distinct listing runtime config in HTML.
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "jekyll"
|
|
8
|
+
require "liquid"
|
|
9
|
+
|
|
10
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
11
|
+
$LOAD_PATH.unshift(ROOT.join("lib").to_s)
|
|
12
|
+
require "pandorga/registry"
|
|
13
|
+
require "pandorga/jekyll/generator"
|
|
14
|
+
|
|
15
|
+
FIXTURE = ROOT.join("scripts/test/fixtures/dual-template")
|
|
16
|
+
ATTRS_INCLUDE = ROOT.join("_includes/page/listing-runtime-attrs.html")
|
|
17
|
+
ARTICLES_LAYOUT = ROOT.join("_layouts/articles.html")
|
|
18
|
+
|
|
19
|
+
config_path = FIXTURE.join("_config.yml")
|
|
20
|
+
unless config_path.file?
|
|
21
|
+
warn "FAIL test-dual-template-runtime: missing #{config_path}"
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless ATTRS_INCLUDE.file?
|
|
26
|
+
warn "FAIL test-dual-template-runtime: missing #{ATTRS_INCLUDE}"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless ARTICLES_LAYOUT.file?
|
|
31
|
+
warn "FAIL test-dual-template-runtime: missing #{ARTICLES_LAYOUT}"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
site_yaml = YAML.safe_load(config_path.read, aliases: true)
|
|
36
|
+
pages = PandorgaRegistry.pages(site_yaml)
|
|
37
|
+
PandorgaRegistry.validate!(pages)
|
|
38
|
+
|
|
39
|
+
article_pages = pages.select { |p| p["template"].to_s == "articles" }
|
|
40
|
+
unless article_pages.length == 2
|
|
41
|
+
warn "FAIL test-dual-template-runtime: expected 2 articles pages, got #{article_pages.length}"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
layout_src = ARTICLES_LAYOUT.read
|
|
46
|
+
%w[listing-runtime-attrs data-listing-collection].each do |token|
|
|
47
|
+
next if layout_src.include?(token)
|
|
48
|
+
|
|
49
|
+
warn "FAIL test-dual-template-runtime: articles layout missing #{token.inspect}"
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attrs_src = ATTRS_INCLUDE.read
|
|
54
|
+
%w[listing_collection listing_path listing_detail data-listing-collection data-listing-path data-listing-detail].each do |token|
|
|
55
|
+
next if attrs_src.include?(token)
|
|
56
|
+
|
|
57
|
+
warn "FAIL test-dual-template-runtime: listing-runtime-attrs missing #{token.inspect}"
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if layout_src.match?(/loadWritingIndex\(\s*['"]articles['"]\s*\)/)
|
|
62
|
+
warn "FAIL test-dual-template-runtime: articles layout still hardcodes loadWritingIndex('articles')"
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if layout_src.include?("'/pages/articles/?tag='") || layout_src.include?('"/pages/articles/?tag="')
|
|
67
|
+
warn "FAIL test-dual-template-runtime: articles layout still hardcodes /pages/articles/ tag href"
|
|
68
|
+
exit 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attrs_template = Liquid::Template.parse(ATTRS_INCLUDE.read)
|
|
72
|
+
|
|
73
|
+
Dir.mktmpdir("pandorga-dual-template-runtime") do |dest|
|
|
74
|
+
jekyll_config = Jekyll.configuration(
|
|
75
|
+
"source" => FIXTURE.to_s,
|
|
76
|
+
"destination" => dest,
|
|
77
|
+
"quiet" => true,
|
|
78
|
+
"safe" => false
|
|
79
|
+
)
|
|
80
|
+
jekyll_config["pandorga"] = site_yaml["pandorga"]
|
|
81
|
+
|
|
82
|
+
site = Jekyll::Site.new(jekyll_config)
|
|
83
|
+
Pandorga::Jekyll::Generator.new.generate(site)
|
|
84
|
+
|
|
85
|
+
keys = article_pages.map { |p| p["key"].to_s }
|
|
86
|
+
generated = site.pages.select { |p| keys.include?(p.data["key"].to_s) }
|
|
87
|
+
unless generated.length == 2
|
|
88
|
+
warn "FAIL test-dual-template-runtime: generator emitted #{generated.length} pages, expected 2"
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
html_by_key = {}
|
|
93
|
+
generated.each do |page|
|
|
94
|
+
%w[collection listing_path detail].each do |field|
|
|
95
|
+
value = page.data[field].to_s
|
|
96
|
+
if value.empty?
|
|
97
|
+
warn "FAIL test-dual-template-runtime: page #{page.data['key']} missing #{field}"
|
|
98
|
+
exit 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
html = attrs_template.render(
|
|
103
|
+
"page" => {
|
|
104
|
+
"collection" => page.data["collection"],
|
|
105
|
+
"listing_path" => page.data["listing_path"],
|
|
106
|
+
"permalink" => page.data["permalink"],
|
|
107
|
+
"detail" => page.data["detail"],
|
|
108
|
+
"key" => page.data["key"]
|
|
109
|
+
},
|
|
110
|
+
"include" => {
|
|
111
|
+
"default_collection" => "articles",
|
|
112
|
+
"default_path" => "/pages/articles/",
|
|
113
|
+
"default_detail" => "/articles/:slug/"
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
html_by_key[page.data["key"].to_s] = html
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
collections = generated.map { |p| p.data["collection"].to_s }.uniq
|
|
120
|
+
paths = generated.map { |p| p.data["listing_path"].to_s }.uniq
|
|
121
|
+
details = generated.map { |p| p.data["detail"].to_s }.uniq
|
|
122
|
+
unless collections.length == 2 && paths.length == 2 && details.length == 2
|
|
123
|
+
warn "FAIL test-dual-template-runtime: page data must differ in collection, listing_path, and detail " \
|
|
124
|
+
"(collections=#{collections.inspect} paths=#{paths.inspect} details=#{details.inspect})"
|
|
125
|
+
exit 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
htmls = html_by_key.values
|
|
129
|
+
unless htmls.length == 2 && htmls.uniq.length == 2
|
|
130
|
+
warn "FAIL test-dual-template-runtime: rendered listing attrs must differ per instance: #{html_by_key.inspect}"
|
|
131
|
+
exit 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
article_pages.each do |entry|
|
|
135
|
+
key = entry["key"].to_s
|
|
136
|
+
html = html_by_key.fetch(key)
|
|
137
|
+
expected_collection = entry["collection"].to_s
|
|
138
|
+
expected_path = entry["path"].to_s
|
|
139
|
+
expected_path = "#{expected_path}/" unless expected_path.end_with?("/")
|
|
140
|
+
expected_detail = entry["detail"].to_s
|
|
141
|
+
|
|
142
|
+
unless html.include?("data-listing-collection=\"#{expected_collection}\"")
|
|
143
|
+
warn "FAIL test-dual-template-runtime: #{key} HTML missing collection #{expected_collection.inspect}: #{html}"
|
|
144
|
+
exit 1
|
|
145
|
+
end
|
|
146
|
+
unless html.include?("data-listing-path=\"#{expected_path}\"")
|
|
147
|
+
warn "FAIL test-dual-template-runtime: #{key} HTML missing path #{expected_path.inspect}: #{html}"
|
|
148
|
+
exit 1
|
|
149
|
+
end
|
|
150
|
+
unless html.include?("data-listing-detail=\"#{expected_detail}\"")
|
|
151
|
+
warn "FAIL test-dual-template-runtime: #{key} HTML missing detail #{expected_detail.inspect}: #{html}"
|
|
152
|
+
exit 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Both instances must not collapse onto the English articles defaults.
|
|
157
|
+
if htmls.all? { |html| html.include?('data-listing-collection="articles"') }
|
|
158
|
+
warn "FAIL test-dual-template-runtime: both instances still point at collection articles"
|
|
159
|
+
exit 1
|
|
160
|
+
end
|
|
161
|
+
if htmls.all? { |html| html.include?('data-listing-path="/pages/articles/"') }
|
|
162
|
+
warn "FAIL test-dual-template-runtime: both instances still point at /pages/articles/"
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
puts "PASS test-dual-template-runtime"
|