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,135 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Home band art (docs/features/home-band-art.md).
|
|
5
|
+
#
|
|
6
|
+
# AC-BAND-01 the export writes both plates under data/
|
|
7
|
+
# AC-BAND-03 a second export with no content change reports them unchanged
|
|
8
|
+
# AC-BAND-07 the layout carries no generated SVG markup
|
|
9
|
+
# AC-BAND-08 the network plate prints the counts computed from the same corpus
|
|
10
|
+
# AC-BAND-09 an entry joined to nothing leaves the plate's bar short of full width
|
|
11
|
+
|
|
12
|
+
require "fileutils"
|
|
13
|
+
require "json"
|
|
14
|
+
require "open3"
|
|
15
|
+
require "pathname"
|
|
16
|
+
require "tmpdir"
|
|
17
|
+
|
|
18
|
+
require_relative "../content/band_art"
|
|
19
|
+
|
|
20
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
21
|
+
EXPORT = ROOT.join("scripts/content/export-content-json.rb")
|
|
22
|
+
LAYOUT = ROOT.join("_layouts/home.html")
|
|
23
|
+
INDEX_BAND = ROOT.join("_includes/home/band-index.html")
|
|
24
|
+
EXAMPLE_CONTENT = ROOT.join("examples/full/content")
|
|
25
|
+
|
|
26
|
+
def check!(condition, message = "condition was false")
|
|
27
|
+
raise "Assertion failed: #{message}" unless condition
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
puts "Testing home band art..."
|
|
31
|
+
|
|
32
|
+
# ── The drawing, against a corpus small enough to count by hand ────────────
|
|
33
|
+
|
|
34
|
+
index = [
|
|
35
|
+
{ "key" => "a", "url" => "/articles/a/", "collection" => "articles", "date" => "2026-01-01",
|
|
36
|
+
"tags" => %w[Engineering Writing], "xcites" => [] },
|
|
37
|
+
{ "key" => "b", "url" => "/articles/b/", "collection" => "articles", "date" => "2026-02-01",
|
|
38
|
+
"tags" => ["Engineering"], "xcites" => ["a"] },
|
|
39
|
+
{ "key" => "c", "url" => "/posts/c/", "collection" => "posts", "date" => "2026-03-01",
|
|
40
|
+
"tags" => ["Writing"], "xcites" => %w[a a b] },
|
|
41
|
+
# Joined to nothing, so it is counted in the corpus and drawn nowhere.
|
|
42
|
+
{ "key" => "d", "url" => "/posts/d/", "collection" => "posts", "date" => "2026-04-01",
|
|
43
|
+
"tags" => ["Solitude"], "xcites" => [] },
|
|
44
|
+
# Not writing: the plate's legend never names projects.
|
|
45
|
+
{ "key" => "e", "url" => "/projects/e/", "collection" => "projects", "date" => "2026-05-01",
|
|
46
|
+
"tags" => ["Engineering"], "xcites" => ["a"] }
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
graph = BandArt.idea_graph(index)
|
|
50
|
+
check! graph[:nodes].map { |node| node["key"] } == %w[a b c],
|
|
51
|
+
"unjoined and non-writing entries must stay out of the graph, got #{graph[:nodes].map { |n| n['key'] }}"
|
|
52
|
+
check! graph[:corpus] == 4, "corpus counts every keyed article and post, got #{graph[:corpus]}"
|
|
53
|
+
check! graph[:edges].length == 3, "c cites a twice and it counts once, got #{graph[:edges].length}"
|
|
54
|
+
check! graph[:themes].map { |theme| theme[:name] } .sort == %w[Engineering Writing],
|
|
55
|
+
"a tag used once is not a theme, got #{graph[:themes].map { |t| t[:name] }}"
|
|
56
|
+
|
|
57
|
+
resources = [
|
|
58
|
+
{ "slug" => "one", "category" => "Academic", "type" => "youtube" },
|
|
59
|
+
{ "slug" => "two", "category" => "Academic", "type" => "podcast" },
|
|
60
|
+
{ "slug" => "three", "category" => "Music", "type" => "website" }
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
plates = BandArt.build(writing_index: index, resources: resources)
|
|
64
|
+
check! plates.keys.sort == %w[network sources], "build draws both plates, got #{plates.keys}"
|
|
65
|
+
|
|
66
|
+
# AC-BAND-08: the numbers are counted, not typed. The three numerals are the
|
|
67
|
+
# drawing, so they are read out of the markup rather than out of a caption.
|
|
68
|
+
network = plates["network"]
|
|
69
|
+
numerals = network.scan(%r{class="home-index-metric"[^>]*>(\d+)<}).flatten
|
|
70
|
+
check! numerals == %w[3 3 2], "the metric card must print the computed graph, got #{numerals.inspect}"
|
|
71
|
+
check! network.include?(%(data-band-tally="3 of 4 pieces joined")),
|
|
72
|
+
"the caption must say how much of the corpus is joined, got #{network[%r{data-band-tally="[^"]*"}]}"
|
|
73
|
+
check! network.include?(%(home-index-spine--articles" x="10" y="96" width="150")) &&
|
|
74
|
+
network.include?(%(home-index-spine--posts" x="160" y="96" width="75")),
|
|
75
|
+
"the spine scales to the corpus and stops short of the margin when a piece is unjoined, " "got #{network.scan(%r{home-index-spine--\w+" x="[^"]*" y="[^"]*" width="[^"]*"})}"
|
|
76
|
+
check! plates["sources"].include?('data-band-tally="3 sources · 2 collections"'),
|
|
77
|
+
"sources tally must restate the shelf, got #{plates['sources'][/data-band-tally="[^"]*"/]}"
|
|
78
|
+
check! plates["sources"].include?(">Acad.<") && plates["sources"].include?(">Music<"),
|
|
79
|
+
"shelf labels must use the short forms, got #{plates['sources'].scan(%r{home-index-collection[^>]*>([^<]+)})}"
|
|
80
|
+
|
|
81
|
+
# Nothing may be drawn in a colour the palette cannot repaint.
|
|
82
|
+
plates.each do |name, svg|
|
|
83
|
+
check! !svg.match?(/#[0-9a-fA-F]{3,8}\b/), "#{name} plate carries a literal colour"
|
|
84
|
+
check! svg.start_with?("<svg "), "#{name} plate must be a standalone SVG document"
|
|
85
|
+
check! svg.include?('xmlns="http://www.w3.org/2000/svg"'), "#{name} plate needs the SVG namespace"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# AC-BAND-07: the layout may not hold generated markup any more.
|
|
89
|
+
layout = LAYOUT.read
|
|
90
|
+
check! !layout.include?("<svg"), "_layouts/home.html must not carry generated SVG"
|
|
91
|
+
index_band = INDEX_BAND.read
|
|
92
|
+
check! index_band.include?('data-content-band-art="sources"') ||
|
|
93
|
+
index_band.include?('data-content-band-art="{{ _art_key }}"'),
|
|
94
|
+
"the sources panel needs its mount point"
|
|
95
|
+
check! index_band.include?("data-content-band-art"),
|
|
96
|
+
"the network panel needs its mount point"
|
|
97
|
+
|
|
98
|
+
# ── The export, end to end ────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
Dir.mktmpdir("band-art") do |dir|
|
|
101
|
+
out = Pathname.new(dir).join("export")
|
|
102
|
+
content_root = EXAMPLE_CONTENT.directory? ? EXAMPLE_CONTENT : ROOT.join("content")
|
|
103
|
+
env = {
|
|
104
|
+
"CONTENT_API_BASE_URL" => "",
|
|
105
|
+
"CONTENT_EXPORT_SKIP_VALIDATE" => "1",
|
|
106
|
+
"PANDORGA_SITE_ROOT" => (EXAMPLE_CONTENT.directory? ? ROOT.join("examples/full") : ROOT).to_s,
|
|
107
|
+
"RUBYOPT" => "-I#{ROOT.join('lib')}"
|
|
108
|
+
}
|
|
109
|
+
command = ["ruby", EXPORT.to_s, content_root.to_s, out.to_s]
|
|
110
|
+
|
|
111
|
+
first, status = Open3.capture2e(env, *command, chdir: env["PANDORGA_SITE_ROOT"])
|
|
112
|
+
check! status.success?, "first export failed:\n#{first}"
|
|
113
|
+
|
|
114
|
+
# AC-BAND-01
|
|
115
|
+
%w[band-art-sources.svg band-art-network.svg].each do |name|
|
|
116
|
+
path = out.join("data", name)
|
|
117
|
+
check! path.file?, "the export must write data/#{name}"
|
|
118
|
+
check! path.read.start_with?("<svg "), "data/#{name} must be an SVG document"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
manifest = JSON.parse(out.join("manifest.json").read)
|
|
122
|
+
check! manifest["files"]["band_art"].sort == ["data/band-art-network.svg", "data/band-art-sources.svg"],
|
|
123
|
+
"the manifest must list both plates, got #{manifest['files']['band_art'].inspect}"
|
|
124
|
+
|
|
125
|
+
before = out.join("data", "band-art-sources.svg").read
|
|
126
|
+
|
|
127
|
+
# AC-BAND-03: a re-export must reuse the bytes, or every run churns R2.
|
|
128
|
+
second, status = Open3.capture2e(env, *command, chdir: env["PANDORGA_SITE_ROOT"])
|
|
129
|
+
check! status.success?, "second export failed:\n#{second}"
|
|
130
|
+
check! out.join("data", "band-art-sources.svg").read == before,
|
|
131
|
+
"a re-export with no content change rewrote the sources plate"
|
|
132
|
+
check! second.include?("band_art: 2"), "the export must report both plates:\n#{second}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts "Home band art OK."
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-0.2
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
9
|
+
EXE = ROOT.join("exe/pandorga")
|
|
10
|
+
DEST = ROOT.join("tmp/install-functions-test")
|
|
11
|
+
|
|
12
|
+
FileUtils.rm_rf(DEST)
|
|
13
|
+
DEST.mkpath
|
|
14
|
+
env = { "RUBYOPT" => "-I#{ROOT.join('lib')}" }
|
|
15
|
+
stdout, stderr, status = Open3.capture3(env, "ruby", EXE.to_s, "install-functions", DEST.to_s)
|
|
16
|
+
unless status.success?
|
|
17
|
+
warn "FAIL test-install-functions: #{stdout}\n#{stderr}"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
api = DEST.join("functions/api/studio/[[path]].js")
|
|
22
|
+
unless api.file?
|
|
23
|
+
warn "FAIL test-install-functions: functions not copied"
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
stamp = DEST.join("functions/.pandorga-version")
|
|
27
|
+
unless stamp.file? && stamp.read.match?(/\d+\.\d+\.\d+/)
|
|
28
|
+
warn "FAIL test-install-functions: version stamp missing"
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
# Must not embed a default owner repo in the stub.
|
|
32
|
+
body = api.read
|
|
33
|
+
owner_slug = %w[alexandre lheinen].join
|
|
34
|
+
if body.include?(owner_slug)
|
|
35
|
+
warn "FAIL test-install-functions: personal data in functions stub"
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "PASS test-install-functions"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# The [EN]/[PT] pills read `language` off writing_index entries. The field has
|
|
5
|
+
# always been in front matter, but was never exported, so listings had no way
|
|
6
|
+
# to reach it. Guard both halves: the key is always present, and any value it
|
|
7
|
+
# carries is one the UI knows how to render.
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
require "pathname"
|
|
11
|
+
require "yaml"
|
|
12
|
+
require "date"
|
|
13
|
+
|
|
14
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
15
|
+
EXPORT_ROOT = REPO_ROOT.join("_content_json")
|
|
16
|
+
CONTENT_ROOT = REPO_ROOT.join("content/collections")
|
|
17
|
+
|
|
18
|
+
# Empty is allowed: an unset language renders no pill at all, which is the
|
|
19
|
+
# correct behaviour for an item whose language was never declared.
|
|
20
|
+
ALLOWED = ["", "enus", "ptbr"].freeze
|
|
21
|
+
TAGGED_COLLECTIONS = %w[articles posts].freeze
|
|
22
|
+
|
|
23
|
+
def fail!(message)
|
|
24
|
+
warn message
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check!(condition, message)
|
|
29
|
+
fail!(message) unless condition
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "Testing language export..."
|
|
33
|
+
|
|
34
|
+
index_path = EXPORT_ROOT.join("data/writing_index.json")
|
|
35
|
+
unless index_path.file?
|
|
36
|
+
puts " skipped: no export at #{index_path.relative_path_from(REPO_ROOT)}"
|
|
37
|
+
exit 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
entries = JSON.parse(index_path.read)
|
|
41
|
+
check!(entries.any?, "writing_index.json is empty")
|
|
42
|
+
|
|
43
|
+
missing = entries.reject { |e| e.key?("language") }
|
|
44
|
+
check!(missing.empty?,
|
|
45
|
+
"writing_index entries missing the language key: " \
|
|
46
|
+
"#{missing.take(3).map { |e| e['slug'] }.join(', ')}")
|
|
47
|
+
|
|
48
|
+
bad = entries.reject { |e| ALLOWED.include?(e["language"].to_s) }
|
|
49
|
+
check!(bad.empty?,
|
|
50
|
+
"unknown language values: " \
|
|
51
|
+
"#{bad.take(5).map { |e| "#{e['slug']}=#{e['language'].inspect}" }.join(', ')}")
|
|
52
|
+
|
|
53
|
+
# Front matter is the source of truth; catch a value that exists on disk but
|
|
54
|
+
# is dropped somewhere between parsing and the index.
|
|
55
|
+
TAGGED_COLLECTIONS.each do |collection|
|
|
56
|
+
Dir.glob(CONTENT_ROOT.join(collection, "*.md")).sort.each do |path|
|
|
57
|
+
source = File.read(path)
|
|
58
|
+
next unless source.start_with?("---")
|
|
59
|
+
|
|
60
|
+
front_matter = YAML.safe_load(source.split(/^---\s*$/, 3)[1],
|
|
61
|
+
permitted_classes: [Date, Time]) || {}
|
|
62
|
+
next if front_matter["published"] == false
|
|
63
|
+
|
|
64
|
+
declared = front_matter["language"].to_s.strip
|
|
65
|
+
next if declared.empty?
|
|
66
|
+
|
|
67
|
+
slug = File.basename(path, ".md")
|
|
68
|
+
entry = entries.find { |e| e["slug"] == slug && e["collection"] == collection }
|
|
69
|
+
next if entry.nil?
|
|
70
|
+
|
|
71
|
+
check!(entry["language"].to_s == declared,
|
|
72
|
+
"#{collection}/#{slug}: front matter language #{declared.inspect} " \
|
|
73
|
+
"but index has #{entry['language'].inspect}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
declared_count = entries.count { |e| !e["language"].to_s.empty? }
|
|
78
|
+
puts " #{entries.size} entries, #{declared_count} with a declared language."
|
|
79
|
+
puts "All language export tests passed."
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Gate: every filter facet a page offers must be a facet the filter reads.
|
|
5
|
+
#
|
|
6
|
+
# ListingFilter keeps one state object. A chip group writes `state[group.key]`
|
|
7
|
+
# on click; `filterItems` reads a fixed set of names. Nothing connects the
|
|
8
|
+
# two, so renaming a facet in a layout — `category` to `tag`, say — leaves the
|
|
9
|
+
# chips rendering, highlighting and pushing a query string while the list
|
|
10
|
+
# beNewsreader them never changes. That shipped on the Sources page: every click set
|
|
11
|
+
# a key nothing read.
|
|
12
|
+
#
|
|
13
|
+
# There is no build error and no console error to notice, so the contract is
|
|
14
|
+
# checked here: chip-group keys against the state ListingFilter initialises
|
|
15
|
+
# and the names filterItems actually consumes, plus the URL parameters a
|
|
16
|
+
# layout can set against the ones syncUrl clears.
|
|
17
|
+
|
|
18
|
+
require "pathname"
|
|
19
|
+
|
|
20
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
21
|
+
FILTER = ROOT.join("_includes/content-runtime/42-listing-filter.html")
|
|
22
|
+
LAYOUT_DIR = ROOT.join("_layouts")
|
|
23
|
+
|
|
24
|
+
# A chip group is the only place a layout writes `key: '<literal>'` followed by
|
|
25
|
+
# a label. Matching the pair keeps unrelated object literals out.
|
|
26
|
+
CHIP_GROUP_KEY = /key:\s*(['"])(\w+)\1\s*,\s*(?:\/\*.*?\*\/\s*|\/\/[^\n]*\n\s*)*label:/m
|
|
27
|
+
PARAM_OPTION = /(?:tagParam|categoryParam):\s*(['"])([\w-]+)\1/
|
|
28
|
+
|
|
29
|
+
def fail!(message)
|
|
30
|
+
warn message
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def check!(condition, message)
|
|
35
|
+
fail!(message) unless condition
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts "Testing the listing filter facet contract..."
|
|
39
|
+
|
|
40
|
+
source = FILTER.read
|
|
41
|
+
|
|
42
|
+
state_block = source[/var state = \{(.*?)\n \};/m, 1]
|
|
43
|
+
check!(!state_block.nil?, "could not find the ListingFilter state object in #{FILTER}")
|
|
44
|
+
state_keys = state_block.scan(/^\s*(\w+):/).flatten.uniq
|
|
45
|
+
check!(state_keys.length >= 3, "only found state keys #{state_keys.inspect}; the regex has drifted")
|
|
46
|
+
|
|
47
|
+
filter_items = source[/filterItems: function \(items, state, options\) \{(.*?)\n \}\n \};/m, 1]
|
|
48
|
+
check!(!filter_items.nil?, "could not find ListingFilter.filterItems in #{FILTER}")
|
|
49
|
+
read_keys = filter_items.scan(/\bstate\.(\w+)/).flatten.uniq
|
|
50
|
+
check!(read_keys.length >= 3, "only found read keys #{read_keys.inspect}; the regex has drifted")
|
|
51
|
+
|
|
52
|
+
sync_url = source[/function syncUrl\(\) \{(.*?)\n \}/m, 1]
|
|
53
|
+
check!(!sync_url.nil?, "could not find syncUrl in #{FILTER}")
|
|
54
|
+
cleared_params = sync_url[/\[(.*?)\]\.forEach/m, 1].to_s.scan(/['"]([\w-]+)['"]/).flatten
|
|
55
|
+
check!(cleared_params.any?, "syncUrl clears no query parameters; the regex has drifted")
|
|
56
|
+
|
|
57
|
+
# Dead state is a facet waiting to happen: something can set it and nothing
|
|
58
|
+
# will ever act on it.
|
|
59
|
+
dead = state_keys - read_keys
|
|
60
|
+
check!(
|
|
61
|
+
dead.empty?,
|
|
62
|
+
"ListingFilter initialises #{dead.inspect} but filterItems never reads " \
|
|
63
|
+
"#{dead.length == 1 ? 'it' : 'them'}. A chip group keyed on a dead name " \
|
|
64
|
+
"filters nothing."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
groups = 0
|
|
68
|
+
layouts_with_groups = []
|
|
69
|
+
|
|
70
|
+
Dir.glob(LAYOUT_DIR.join("*.html")).sort.each do |path|
|
|
71
|
+
layout = Pathname.new(path)
|
|
72
|
+
body = layout.read
|
|
73
|
+
keys = body.scan(CHIP_GROUP_KEY).map { |_, key| key }
|
|
74
|
+
next if keys.empty?
|
|
75
|
+
|
|
76
|
+
layouts_with_groups << layout.basename.to_s
|
|
77
|
+
groups += keys.length
|
|
78
|
+
|
|
79
|
+
keys.uniq.each do |key|
|
|
80
|
+
check!(
|
|
81
|
+
state_keys.include?(key),
|
|
82
|
+
"#{layout.basename}: chip group keyed on '#{key}', which ListingFilter " \
|
|
83
|
+
"never initialises (it knows #{state_keys.sort.inspect}). Clicking a " \
|
|
84
|
+
"chip would set a key nothing reads."
|
|
85
|
+
)
|
|
86
|
+
check!(
|
|
87
|
+
read_keys.include?(key),
|
|
88
|
+
"#{layout.basename}: chip group keyed on '#{key}', which filterItems " \
|
|
89
|
+
"never reads (it consumes #{read_keys.sort.inspect}). The chips would " \
|
|
90
|
+
"highlight and the list would not move."
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
check!(
|
|
95
|
+
body.include?("ListingFilter.filterItems"),
|
|
96
|
+
"#{layout.basename} builds chip groups but never calls " \
|
|
97
|
+
"ListingFilter.filterItems, so no click can reach the list"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
body.scan(PARAM_OPTION).each do |_, param|
|
|
101
|
+
check!(
|
|
102
|
+
cleared_params.include?(param),
|
|
103
|
+
"#{layout.basename} writes ?#{param}= but syncUrl never clears it " \
|
|
104
|
+
"(clears #{cleared_params.inspect}), so switching facets leaves a stale filter in the URL"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
check!(
|
|
110
|
+
layouts_with_groups.length >= 3,
|
|
111
|
+
"only #{layouts_with_groups.inspect} declare chip groups; the redesign put a " \
|
|
112
|
+
"filter deck on every listing page, so the scan has probably stopped matching"
|
|
113
|
+
)
|
|
114
|
+
check!(groups >= 5, "only found #{groups} chip groups; the scan has probably stopped matching")
|
|
115
|
+
|
|
116
|
+
puts " state: #{state_keys.sort.join(', ')}"
|
|
117
|
+
puts " read by filterItems: #{read_keys.sort.join(', ')}"
|
|
118
|
+
puts " #{groups} chip group(s) across #{layouts_with_groups.join(', ')}"
|
|
119
|
+
puts "All listing filter facet checks passed."
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unit checks for display-math row-break normalization.
|
|
4
|
+
* See scripts/lib/normalize-display-math.mjs and docs/content/elements.md.
|
|
5
|
+
*/
|
|
6
|
+
import { normalizeDisplayMathTex, normalizeMathRegion } from '../lib/normalize-display-math.mjs';
|
|
7
|
+
|
|
8
|
+
function assertEqual(actual, expected, label) {
|
|
9
|
+
if (actual !== expected) {
|
|
10
|
+
console.error(`FAIL: ${label}`);
|
|
11
|
+
console.error(' expected:', JSON.stringify(expected));
|
|
12
|
+
console.error(' actual: ', JSON.stringify(actual));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
console.log(` ok: ${label}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('Testing display-math row-break normalization...');
|
|
19
|
+
|
|
20
|
+
const alignBroken = [
|
|
21
|
+
'\\begin{align}',
|
|
22
|
+
'p(x) &= \\int p(x,y)\\mathrm{d}y \\',
|
|
23
|
+
'p(y|x) &= \\frac{p(x,y)}{p(x)} \\',
|
|
24
|
+
'p(x|y) &= \\frac{p(y|x)p(x)}{p(y)}',
|
|
25
|
+
'\\end{align}',
|
|
26
|
+
].join('\n');
|
|
27
|
+
|
|
28
|
+
const alignFixed = [
|
|
29
|
+
'\\begin{align}',
|
|
30
|
+
'p(x) &= \\int p(x,y)\\mathrm{d}y \\\\',
|
|
31
|
+
'p(y|x) &= \\frac{p(x,y)}{p(x)} \\\\',
|
|
32
|
+
'p(x|y) &= \\frac{p(y|x)p(x)}{p(y)}',
|
|
33
|
+
'\\end{align}',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
assertEqual(
|
|
37
|
+
normalizeDisplayMathTex(alignBroken),
|
|
38
|
+
alignFixed,
|
|
39
|
+
'align trailing single backslash → \\\\'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
assertEqual(
|
|
43
|
+
normalizeDisplayMathTex('\\begin{bmatrix}x-x_r\\ y-y_r\\end{bmatrix}'),
|
|
44
|
+
'\\begin{bmatrix}x-x_r\\\\ y-y_r\\end{bmatrix}',
|
|
45
|
+
'bmatrix mid-line \\ + space → \\\\'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const alignedOk = [
|
|
49
|
+
'\\begin{aligned}',
|
|
50
|
+
'a &= 1 \\\\',
|
|
51
|
+
'b &= 2',
|
|
52
|
+
'\\end{aligned}',
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
assertEqual(
|
|
56
|
+
normalizeDisplayMathTex(alignedOk),
|
|
57
|
+
alignedOk,
|
|
58
|
+
'already-correct \\\\ left unchanged'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
assertEqual(
|
|
62
|
+
normalizeDisplayMathTex('a = b\\,c\\;d'),
|
|
63
|
+
'a = b\\,c\\;d',
|
|
64
|
+
'spacing commands \\, and \\; untouched outside row envs'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
assertEqual(
|
|
68
|
+
normalizeMathRegion('$$\\begin{align}a &= 1 \\ b &= 2\\end{align}$$'),
|
|
69
|
+
'$$\\begin{align}a &= 1 \\\\ b &= 2\\end{align}$$',
|
|
70
|
+
'$$ region wrapper preserved'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assertEqual(
|
|
74
|
+
normalizeMathRegion('$a\\\\b$'),
|
|
75
|
+
'$a\\\\b$',
|
|
76
|
+
'inline math not normalized'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
console.log('All display-math row-break tests passed.');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "open3"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
8
|
+
SCRIPT = Pathname.new(__dir__).join("test-math-row-breaks.mjs")
|
|
9
|
+
|
|
10
|
+
puts "Testing math row-break normalization..."
|
|
11
|
+
stdout, stderr, status = Open3.capture3("node", SCRIPT.to_s, chdir: ROOT.join("scripts").to_s)
|
|
12
|
+
unless status.success?
|
|
13
|
+
warn stderr
|
|
14
|
+
warn stdout
|
|
15
|
+
raise "math row-break tests failed"
|
|
16
|
+
end
|
|
17
|
+
puts stdout
|
|
18
|
+
puts "All math row-break tests passed."
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT Phase 6 — pandorga new
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "yaml"
|
|
8
|
+
require "date"
|
|
9
|
+
|
|
10
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
11
|
+
EXE = ROOT.join("exe/pandorga")
|
|
12
|
+
DEST = ROOT.join("tmp/new-site-test")
|
|
13
|
+
|
|
14
|
+
def run_new(*args)
|
|
15
|
+
env = { "RUBYOPT" => "-I#{ROOT.join('lib')}" }
|
|
16
|
+
Open3.capture3(env, "ruby", EXE.to_s, "new", *args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run_doctor(dir)
|
|
20
|
+
env = { "RUBYOPT" => "-I#{ROOT.join('lib')}" }
|
|
21
|
+
Open3.capture3(env, "ruby", EXE.to_s, "doctor", dir.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
FileUtils.rm_rf(DEST)
|
|
25
|
+
|
|
26
|
+
stdout, stderr, status = run_new(DEST.to_s)
|
|
27
|
+
unless status.success?
|
|
28
|
+
warn "FAIL test-new: expected exit 0\n#{stdout}\n#{stderr}"
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
unless stdout.include?("Created site at") && stdout.include?("bundle install")
|
|
32
|
+
warn "FAIL test-new: missing next-steps output\n#{stdout}"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
required = %w[
|
|
37
|
+
_config.yml
|
|
38
|
+
Gemfile
|
|
39
|
+
index.html
|
|
40
|
+
_redirects
|
|
41
|
+
content/pages/headers.yml
|
|
42
|
+
content/collections/articles/2026-01-01-hello.md
|
|
43
|
+
content/collections/articles/2026-01-02-second.md
|
|
44
|
+
]
|
|
45
|
+
required.each do |rel|
|
|
46
|
+
unless DEST.join(rel).file?
|
|
47
|
+
warn "FAIL test-new: missing #{rel}"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
%w[Gemfile.lock _site vendor .bundle _content_json .jekyll-cache].each do |name|
|
|
53
|
+
if DEST.join(name).exist?
|
|
54
|
+
warn "FAIL test-new: should exclude #{name}"
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
gemfile = DEST.join("Gemfile").read
|
|
60
|
+
unless gemfile.include?('gem "pandorga", "~> 1.2"')
|
|
61
|
+
warn "FAIL test-new: Gemfile must pin pandorga ~> 1.2\n#{gemfile}"
|
|
62
|
+
exit 1
|
|
63
|
+
end
|
|
64
|
+
unless gemfile.include?("path:") && gemfile.include?("github:")
|
|
65
|
+
warn "FAIL test-new: Gemfile should comment path/github fallbacks"
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
config = YAML.safe_load(DEST.join("_config.yml").read, permitted_classes: [Date]) || {}
|
|
70
|
+
pandorga = config["pandorga"]
|
|
71
|
+
unless pandorga.is_a?(Hash)
|
|
72
|
+
warn "FAIL test-new: missing pandorga: in _config.yml"
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
identity = pandorga["identity"]
|
|
76
|
+
unless identity.is_a?(Hash) && identity["name"].to_s == "Ada Example"
|
|
77
|
+
warn "FAIL test-new: expected Ada Example identity"
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
80
|
+
backend = pandorga.dig("content", "backend")
|
|
81
|
+
unless backend == "static"
|
|
82
|
+
warn "FAIL test-new: expected content.backend: static, got #{backend.inspect}"
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
pages = pandorga["pages"]
|
|
86
|
+
unless pages.is_a?(Array) && pages.any?
|
|
87
|
+
warn "FAIL test-new: expected pandorga.pages entries"
|
|
88
|
+
exit 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
dout, derr, dstatus = run_doctor(DEST)
|
|
92
|
+
unless dstatus.success? && dout.include?("pandorga doctor: ok")
|
|
93
|
+
warn "FAIL test-new: doctor failed on scaffold\n#{dout}\n#{derr}"
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
_out, err, st = run_new(DEST.to_s)
|
|
98
|
+
if st.success?
|
|
99
|
+
warn "FAIL test-new: expected failure without --force on nonempty dir"
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
unless err.include?("--force")
|
|
103
|
+
warn "FAIL test-new: expected --force hint\n#{err}"
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
stdout2, stderr2, status2 = run_new(DEST.to_s, "--force")
|
|
108
|
+
unless status2.success?
|
|
109
|
+
warn "FAIL test-new: --force should succeed\n#{stdout2}\n#{stderr2}"
|
|
110
|
+
exit 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
puts "PASS test-new"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-AC-1
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "find"
|
|
6
|
+
|
|
7
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
8
|
+
|
|
9
|
+
# Public repo home (ADR-001) may appear as a URL or Bundler github: source.
|
|
10
|
+
REPO_URL_OK = %r{(?:github\.com/)?alexandrelheinen/pandorga}
|
|
11
|
+
|
|
12
|
+
PATTERNS = [
|
|
13
|
+
[/pub-[0-9a-f]{32}\.r2\.dev/i, "r2 bucket URL"],
|
|
14
|
+
[/pk_live_[A-Za-z0-9_-]{16,}/, "live publishable key"],
|
|
15
|
+
[/sk_live_[A-Za-z0-9_-]{16,}/, "live secret key"],
|
|
16
|
+
[/github_pat_[A-Za-z0-9_]+/, "github pat"],
|
|
17
|
+
[/Alexandre L\. Heinen/, "owner short name"],
|
|
18
|
+
[/Alexandre Loeblein Heinen/, "owner full name"],
|
|
19
|
+
[/alexandrelheinen\.pages\.dev/i, "pages domain"],
|
|
20
|
+
[/dev\.alexandrelheinen\.studio/i, "android applicationId"]
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
SKIP_DIRS = %w[
|
|
24
|
+
.git
|
|
25
|
+
tmp
|
|
26
|
+
_site
|
|
27
|
+
_content_json
|
|
28
|
+
vendor
|
|
29
|
+
.bundle
|
|
30
|
+
node_modules
|
|
31
|
+
.guidelines
|
|
32
|
+
.agents
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# Migration spec documents the extraction source; .gitmodules points at the
|
|
36
|
+
# owner's private guidelines repo (not shipped as platform code).
|
|
37
|
+
ALLOW_REL = %w[
|
|
38
|
+
docs/spec.md
|
|
39
|
+
docs/decisions.md
|
|
40
|
+
.gitmodules
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
failures = []
|
|
44
|
+
Find.find(ROOT) do |path|
|
|
45
|
+
path = Pathname.new(path)
|
|
46
|
+
if path.directory?
|
|
47
|
+
Find.prune if SKIP_DIRS.include?(path.basename.to_s)
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
next unless path.file?
|
|
51
|
+
next if path.basename.to_s == "test-no-personal-data.rb"
|
|
52
|
+
next if path.extname.match?(/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot|pdf|zip|gz|wasm)$/i)
|
|
53
|
+
|
|
54
|
+
rel = path.relative_path_from(ROOT).to_s
|
|
55
|
+
next if ALLOW_REL.include?(rel)
|
|
56
|
+
|
|
57
|
+
raw = begin
|
|
58
|
+
path.read(mode: "rb")
|
|
59
|
+
rescue StandardError
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
next if raw.include?("\x00")
|
|
63
|
+
|
|
64
|
+
text = raw.force_encoding("UTF-8")
|
|
65
|
+
next unless text.valid_encoding?
|
|
66
|
+
|
|
67
|
+
# Strip allowed repo URLs before scanning for the owner slug.
|
|
68
|
+
scrubbed = text.encode("UTF-8", invalid: :replace, undef: :replace).gsub(REPO_URL_OK, "")
|
|
69
|
+
if scrubbed.match?(/alexandrelheinen/i)
|
|
70
|
+
failures << "#{rel}: owner slug outside allowed repo URL"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
PATTERNS.each do |re, label|
|
|
74
|
+
next unless text.match?(re)
|
|
75
|
+
|
|
76
|
+
failures << "#{rel}: #{label}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if failures.any?
|
|
81
|
+
warn "FAIL test-no-personal-data:\n#{failures.uniq.join("\n")}"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
puts "PASS test-no-personal-data"
|