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,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-AC-6 — examples build with the static backend and fictional persona.
|
|
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
|
+
|
|
11
|
+
def run!(*cmd, chdir:)
|
|
12
|
+
env = {
|
|
13
|
+
"RUBYOPT" => "-I#{ROOT.join('lib')}",
|
|
14
|
+
"PANDORGA_SITE_ROOT" => chdir.to_s,
|
|
15
|
+
"CONTENT_API_BASE_URL" => "",
|
|
16
|
+
"CONTENT_EXPORT_SKIP_VALIDATE" => "1",
|
|
17
|
+
"JEKYLL_ENV" => "production"
|
|
18
|
+
}
|
|
19
|
+
stdout, stderr, status = Open3.capture3(env, *cmd, chdir: chdir.to_s)
|
|
20
|
+
return [stdout, stderr] if status.success?
|
|
21
|
+
|
|
22
|
+
warn "FAIL test-examples-build: #{cmd.join(' ')} (cwd=#{chdir})\n#{stdout}\n#{stderr}"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
%w[minimal full].each do |name|
|
|
27
|
+
example = ROOT.join("examples", name)
|
|
28
|
+
unless example.join("_config.yml").file?
|
|
29
|
+
warn "FAIL test-examples-build: missing #{example}"
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
FileUtils.rm_rf(example.join("_site"))
|
|
34
|
+
FileUtils.rm_rf(example.join("_content_json"))
|
|
35
|
+
|
|
36
|
+
run!("bundle", "install", "--quiet", chdir: example) if example.join("Gemfile").file?
|
|
37
|
+
run!("ruby", EXE.to_s, "export", "content", "_content_json", chdir: example)
|
|
38
|
+
run!("bundle", "exec", "jekyll", "build", "--quiet", chdir: example)
|
|
39
|
+
|
|
40
|
+
index = example.join("_site/index.html")
|
|
41
|
+
unless index.file?
|
|
42
|
+
warn "FAIL test-examples-build: #{name} missing _site/index.html"
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
html = index.read
|
|
46
|
+
if html.match?(/Alexandre L\. Heinen|Loeblein Heinen/i)
|
|
47
|
+
warn "FAIL test-examples-build: #{name} home HTML leaked owner identity"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
if html.include?("Ada Example") == false && name == "full"
|
|
51
|
+
warn "FAIL test-examples-build: full example missing fictional persona"
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
unless html.include?("home-hero") || html.include?("home-ledger")
|
|
55
|
+
warn "FAIL test-examples-build: #{name} home layout missing"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
puts "PASS test-examples-build"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Ensures export reuses identical JSON bytes instead of wiping OUTPUT_ROOT.
|
|
5
|
+
|
|
6
|
+
$stdout.sync = true
|
|
7
|
+
$stderr.sync = true
|
|
8
|
+
|
|
9
|
+
require "fileutils"
|
|
10
|
+
require "open3"
|
|
11
|
+
require "pathname"
|
|
12
|
+
require "tmpdir"
|
|
13
|
+
|
|
14
|
+
require_relative "../lib/writing_index_contract"
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
17
|
+
EXPORT_SCRIPT = REPO_ROOT.join("scripts/content/export-content-json.rb")
|
|
18
|
+
|
|
19
|
+
def fail!(message)
|
|
20
|
+
warn message
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run_export!(out_dir)
|
|
25
|
+
stdout, stderr, status = Open3.capture3(
|
|
26
|
+
{ "CONTENT_API_BASE_URL" => "" },
|
|
27
|
+
"ruby", EXPORT_SCRIPT.to_s, "content", out_dir.to_s,
|
|
28
|
+
chdir: REPO_ROOT.to_s
|
|
29
|
+
)
|
|
30
|
+
fail!("export failed:\n#{stderr}\n#{stdout}") unless status.success?
|
|
31
|
+
|
|
32
|
+
stdout
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Dir.mktmpdir("content-json-incremental-") do |dir|
|
|
36
|
+
out = Pathname.new(dir).join("export")
|
|
37
|
+
|
|
38
|
+
first = run_export!(out)
|
|
39
|
+
fail!("first export missing write summary") unless first.include?("changed")
|
|
40
|
+
|
|
41
|
+
sample = out.join("manifest.json")
|
|
42
|
+
fail!("missing manifest") unless sample.file?
|
|
43
|
+
|
|
44
|
+
mtime_before = sample.mtime
|
|
45
|
+
content_before = sample.read
|
|
46
|
+
sleep 1
|
|
47
|
+
|
|
48
|
+
second = run_export!(out)
|
|
49
|
+
fail!("second export should report unchanged files") unless second.match?(/unchanged=[1-9]/)
|
|
50
|
+
|
|
51
|
+
# manifest embeds generated_at, so it should rewrite; pick a leaf that is stable.
|
|
52
|
+
leaf = out.glob("collections/articles/*.json").min_by(&:to_s)
|
|
53
|
+
fail!("no article JSON exported") unless leaf
|
|
54
|
+
|
|
55
|
+
leaf_mtime = leaf.mtime
|
|
56
|
+
leaf_body = leaf.read
|
|
57
|
+
sleep 1
|
|
58
|
+
run_export!(out)
|
|
59
|
+
|
|
60
|
+
fail!("stable article JSON was rewritten") unless leaf.mtime == leaf_mtime
|
|
61
|
+
fail!("stable article JSON bytes changed") unless leaf.read == leaf_body
|
|
62
|
+
|
|
63
|
+
# Touch a published post body via chronology-insensitive check: rewriting the
|
|
64
|
+
# same export dir must keep pruning functional (no crash) and keep manifest.
|
|
65
|
+
fail!("manifest disappeared") unless sample.file?
|
|
66
|
+
fail!("manifest became empty") if sample.read.strip.empty?
|
|
67
|
+
fail!("first manifest snapshot unexpectedly empty") if content_before.strip.empty?
|
|
68
|
+
|
|
69
|
+
puts "Export incremental write OK (sample #{leaf.relative_path_from(out)}, mtime preserved)"
|
|
70
|
+
puts "Note: manifest may still rewrite because generated_at changes (#{mtime_before} -> #{sample.mtime})."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Path filter: rebuild only the changed article plus indexes that embed dates/excerpts.
|
|
74
|
+
Dir.mktmpdir("content-json-path-filter-") do |dir|
|
|
75
|
+
root = Pathname.new(dir)
|
|
76
|
+
content = root.join("content")
|
|
77
|
+
out = root.join("export")
|
|
78
|
+
FileUtils.mkdir_p(content.join("collections/articles"))
|
|
79
|
+
|
|
80
|
+
write_article = lambda do |stem, body|
|
|
81
|
+
content.join("collections/articles/#{stem}.md").write(<<~MARKDOWN)
|
|
82
|
+
---
|
|
83
|
+
title: #{stem}
|
|
84
|
+
key: #{stem}
|
|
85
|
+
date: 2026-08-22
|
|
86
|
+
published: true
|
|
87
|
+
---
|
|
88
|
+
#{body}
|
|
89
|
+
MARKDOWN
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
write_article.call("filter-a", "First A")
|
|
93
|
+
write_article.call("filter-b", "First B")
|
|
94
|
+
|
|
95
|
+
run = lambda do |changed|
|
|
96
|
+
env = {
|
|
97
|
+
"CONTENT_API_BASE_URL" => "",
|
|
98
|
+
"CONTENT_EXPORT_SKIP_VALIDATE" => "1"
|
|
99
|
+
}
|
|
100
|
+
env["CONTENT_EXPORT_CHANGED_PATHS"] = changed unless changed.to_s.empty?
|
|
101
|
+
stdout, stderr, status = Open3.capture3(
|
|
102
|
+
env,
|
|
103
|
+
"ruby", EXPORT_SCRIPT.to_s, content.to_s, out.to_s,
|
|
104
|
+
chdir: REPO_ROOT.to_s
|
|
105
|
+
)
|
|
106
|
+
fail!("path-filter export failed:\n#{stderr}\n#{stdout}") unless status.success?
|
|
107
|
+
|
|
108
|
+
stdout
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
run.call("")
|
|
112
|
+
article_a = out.join("collections/articles/2026-08-22-filter-a.json")
|
|
113
|
+
article_b = out.join("collections/articles/2026-08-22-filter-b.json")
|
|
114
|
+
fail!("missing filter-a") unless article_a.file?
|
|
115
|
+
fail!("missing filter-b") unless article_b.file?
|
|
116
|
+
|
|
117
|
+
b_mtime = article_b.mtime
|
|
118
|
+
b_body = article_b.read
|
|
119
|
+
index_before = out.join("data/writing_index.json").read
|
|
120
|
+
sleep 1
|
|
121
|
+
|
|
122
|
+
write_article.call("filter-a", "Second A — path filter should rebuild this one")
|
|
123
|
+
filtered = run.call("content/collections/articles/filter-a.md")
|
|
124
|
+
fail!("path-filter export should reuse unchanged files") unless filtered.match?(/unchanged=[1-9]/)
|
|
125
|
+
|
|
126
|
+
fail!("unchanged B was rewritten") unless article_b.mtime == b_mtime
|
|
127
|
+
fail!("unchanged B bytes changed") unless article_b.read == b_body
|
|
128
|
+
fail!("changed A was not rewritten") if article_a.read == "First A"
|
|
129
|
+
fail!("changed A body missing") unless article_a.read.include?("Second A")
|
|
130
|
+
|
|
131
|
+
index_after = out.join("data/writing_index.json").read
|
|
132
|
+
fail!("writing_index must rebuild when an article changes") if index_after == index_before
|
|
133
|
+
fail!("manifest missing after path-filter export") unless out.join("manifest.json").file?
|
|
134
|
+
|
|
135
|
+
WritingIndexContract.assert_slugs_match_export!(out)
|
|
136
|
+
|
|
137
|
+
puts "Export path-filter OK (B mtime preserved, A + writing_index rebuilt)"
|
|
138
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-AC-6, PLT-AC-7, PLT-AC-12
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "json"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
10
|
+
EXAMPLE = ROOT.join("examples/minimal")
|
|
11
|
+
EXE = ROOT.join("exe/pandorga")
|
|
12
|
+
DEST = ROOT.join("tmp/export-test")
|
|
13
|
+
|
|
14
|
+
FileUtils.rm_rf(DEST)
|
|
15
|
+
env = {
|
|
16
|
+
"RUBYOPT" => "-I#{ROOT.join('lib')}",
|
|
17
|
+
"PANDORGA_SITE_ROOT" => EXAMPLE.to_s,
|
|
18
|
+
"CONTENT_API_BASE_URL" => "",
|
|
19
|
+
"CONTENT_EXPORT_SKIP_VALIDATE" => "1"
|
|
20
|
+
}
|
|
21
|
+
stdout, stderr, status = Open3.capture3(
|
|
22
|
+
env, "ruby", EXE.to_s, "export",
|
|
23
|
+
"content", DEST.to_s,
|
|
24
|
+
chdir: EXAMPLE.to_s
|
|
25
|
+
)
|
|
26
|
+
unless status.success?
|
|
27
|
+
warn "FAIL test-export: #{stdout}\n#{stderr}"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
manifest = JSON.parse(DEST.join("manifest.json").read)
|
|
32
|
+
unless manifest["schema_version"] == 1
|
|
33
|
+
warn "FAIL test-export: schema_version missing or wrong: #{manifest['schema_version'].inspect}"
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
hello = DEST.join("collections/articles/2026-01-01-hello.json")
|
|
38
|
+
unless hello.file?
|
|
39
|
+
warn "FAIL test-export: missing #{hello}"
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
payload = JSON.parse(hello.read)
|
|
43
|
+
title = payload["title"] || payload.dig("front_matter", "title")
|
|
44
|
+
unless title.to_s.include?("Hello")
|
|
45
|
+
warn "FAIL test-export: title not exported (#{title.inspect})"
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
if payload.key?("body_extended") || payload.dig("front_matter", "body_extended")
|
|
49
|
+
warn "FAIL test-export: private field leaked"
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
puts "PASS test-export"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Gate: every field the exporter writes must survive the trip to the page.
|
|
5
|
+
#
|
|
6
|
+
# Between scripts/content/export-content-json.rb and a rendered card sit two
|
|
7
|
+
# copy steps that drop fields silently:
|
|
8
|
+
#
|
|
9
|
+
# writing_index.json --writingIndexAsListingItem--> { front_matter }
|
|
10
|
+
# collections/*.json --hydrateFrontMatter--------> { front_matter }
|
|
11
|
+
#
|
|
12
|
+
# The first is an explicit field whitelist, so an exported field stays
|
|
13
|
+
# invisible until someone remembers to add a line to it; that is how
|
|
14
|
+
# `language` was exported for weeks and never appeared on a card. The second
|
|
15
|
+
# used to return early before promoting the payload date, so no detail page
|
|
16
|
+
# showed a date at all. Neither failure changes the build's exit code.
|
|
17
|
+
#
|
|
18
|
+
# So: export for real, then run both copy steps through the real runtime and
|
|
19
|
+
# compare key by key. A new exporter field is covered the day it is added —
|
|
20
|
+
# either it reaches the view, or it goes on the documented exclusion list in
|
|
21
|
+
# scripts/test/render-parity/run-field-flow-checks.mjs.
|
|
22
|
+
|
|
23
|
+
require "open3"
|
|
24
|
+
require "pathname"
|
|
25
|
+
require "tmpdir"
|
|
26
|
+
|
|
27
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
28
|
+
EXPORT_SCRIPT = ROOT.join("scripts/content/export-content-json.rb")
|
|
29
|
+
CHECKS = Pathname.new(__dir__).join("render-parity", "run-field-flow-checks.mjs")
|
|
30
|
+
|
|
31
|
+
def fail!(message)
|
|
32
|
+
warn message
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "Testing that exported fields reach the view..."
|
|
37
|
+
|
|
38
|
+
fail!("missing #{CHECKS}") unless CHECKS.file?
|
|
39
|
+
|
|
40
|
+
Dir.mktmpdir("field-flow-") do |tmp|
|
|
41
|
+
out_dir = Pathname.new(tmp).join("_content_json")
|
|
42
|
+
|
|
43
|
+
stdout, stderr, status = Open3.capture3(
|
|
44
|
+
{ "CONTENT_API_BASE_URL" => "" },
|
|
45
|
+
"ruby", EXPORT_SCRIPT.to_s, "content", out_dir.to_s,
|
|
46
|
+
chdir: ROOT.to_s
|
|
47
|
+
)
|
|
48
|
+
fail!("export-content-json.rb failed:\n#{stderr}\n#{stdout}") unless status.success?
|
|
49
|
+
|
|
50
|
+
stdout, stderr, status = Open3.capture3(
|
|
51
|
+
"node", CHECKS.to_s, out_dir.to_s, chdir: ROOT.join("scripts").to_s
|
|
52
|
+
)
|
|
53
|
+
print stdout
|
|
54
|
+
unless status.success?
|
|
55
|
+
warn stderr unless stderr.strip.empty?
|
|
56
|
+
fail!("An exported field never reaches the page.")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
puts "All exported-field flow checks passed."
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Gate: text-entry controls must be painted in theme tokens, by a selector
|
|
5
|
+
# that outranks the Tailwind forms plugin.
|
|
6
|
+
#
|
|
7
|
+
# The forms plugin ships its own base layer for `[type='text']`,
|
|
8
|
+
# `[type='search']`, `textarea` and `select`, setting a white background and a
|
|
9
|
+
# near-black colour. That layer is injected after assets/css/base.css, and an
|
|
10
|
+
# attribute selector scores the same as a single class, so a one-class rule
|
|
11
|
+
# loses the tie on source order. In dark mode the keyword box then renders as
|
|
12
|
+
# a white hole — which is exactly what happened, on every listing page at
|
|
13
|
+
# once, while the build stayed green and no screenshot was taken in dark mode.
|
|
14
|
+
#
|
|
15
|
+
# Two defences, both checked here:
|
|
16
|
+
# 1. the rule that paints the control uses at least two classes (or an
|
|
17
|
+
# equivalent compound), so it wins the cascade outright;
|
|
18
|
+
# 2. it sets background and colour from custom properties, so both palettes
|
|
19
|
+
# follow. A literal colour is how one theme gets broken silently.
|
|
20
|
+
#
|
|
21
|
+
# Checkboxes and radios are out of scope: the plugin styles those through
|
|
22
|
+
# `accent-color`/`currentColor`, which already tracks the theme.
|
|
23
|
+
|
|
24
|
+
require "pathname"
|
|
25
|
+
require "set"
|
|
26
|
+
|
|
27
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
28
|
+
MARKUP_DIRS = [ROOT.join("_layouts"), ROOT.join("_includes")].freeze
|
|
29
|
+
CSS_DIR = ROOT.join("assets/css")
|
|
30
|
+
|
|
31
|
+
# Input types the forms plugin paints as a text field. `<input>` with no type
|
|
32
|
+
# defaults to text, so a missing type counts too.
|
|
33
|
+
TEXT_ENTRY_TYPES = %w[text search email url tel number password date datetime-local month week time].freeze
|
|
34
|
+
|
|
35
|
+
# The printed CV is hard-coded to the light palette by design, and carries no
|
|
36
|
+
# interactive controls; studio/ is the self-hosted SPA.
|
|
37
|
+
SKIP = /cv-print|print-/.freeze
|
|
38
|
+
|
|
39
|
+
def fail!(message)
|
|
40
|
+
warn message
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check!(condition, message)
|
|
45
|
+
fail!(message) unless condition
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Specificity that matters against `[type='search']` (0,1,0): count classes,
|
|
49
|
+
# attributes and ids in one selector of the group.
|
|
50
|
+
def weight(selector)
|
|
51
|
+
selector.scan(/\.[\w-]+/).length +
|
|
52
|
+
selector.scan(/\[[^\]]+\]/).length +
|
|
53
|
+
selector.scan(/#[\w-]+/).length * 100
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def css_rules
|
|
57
|
+
Dir.glob(CSS_DIR.join("**/*.css")).sort.flat_map do |path|
|
|
58
|
+
next [] if File.basename(path).match?(SKIP)
|
|
59
|
+
|
|
60
|
+
text = File.read(path).gsub(%r{/\*.*?\*/}m, "")
|
|
61
|
+
text.scan(/([^{}]+)\{([^{}]*)\}/m).map do |selector_group, body|
|
|
62
|
+
{ file: Pathname.new(path).relative_path_from(ROOT).to_s,
|
|
63
|
+
selectors: selector_group.split(",").map(&:strip).reject(&:empty?),
|
|
64
|
+
body: body }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def text_entry_controls
|
|
70
|
+
found = []
|
|
71
|
+
MARKUP_DIRS.each do |dir|
|
|
72
|
+
Dir.glob(dir.join("**/*.{html,liquid}")).sort.each do |path|
|
|
73
|
+
next if File.basename(path).match?(SKIP)
|
|
74
|
+
|
|
75
|
+
File.read(path).scan(/<(input|textarea|select)\b([^>]*)>/m) do |tag, attrs|
|
|
76
|
+
type = attrs[/\btype\s*=\s*["']([\w-]+)["']/, 1]
|
|
77
|
+
next if tag == "input" && !type.nil? && !TEXT_ENTRY_TYPES.include?(type)
|
|
78
|
+
|
|
79
|
+
classes = attrs[/\bclass\s*=\s*["']([^"']*)["']/, 1].to_s.split(/\s+/)
|
|
80
|
+
found << {
|
|
81
|
+
file: Pathname.new(path).relative_path_from(ROOT).to_s,
|
|
82
|
+
tag: tag,
|
|
83
|
+
type: type || (tag == "input" ? "text" : tag),
|
|
84
|
+
classes: classes
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
found
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
puts "Testing form control theming..."
|
|
93
|
+
|
|
94
|
+
rules = css_rules
|
|
95
|
+
check!(rules.length > 100, "only parsed #{rules.length} CSS rules; the parser has drifted")
|
|
96
|
+
|
|
97
|
+
controls = text_entry_controls
|
|
98
|
+
check!(
|
|
99
|
+
controls.any?,
|
|
100
|
+
"found no text-entry controls in #{MARKUP_DIRS.map { |d| d.basename.to_s }.join(', ')}; " \
|
|
101
|
+
"the listing pages each carry a keyword box, so the scan has drifted"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
controls.each do |control|
|
|
105
|
+
label = "#{control[:file]} <#{control[:tag]} type=#{control[:type]}>"
|
|
106
|
+
check!(
|
|
107
|
+
control[:classes].any?,
|
|
108
|
+
"#{label} carries no class, so nothing can outrank the forms plugin base layer"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
painting = rules.select do |rule|
|
|
112
|
+
next false unless rule[:body].match?(/(?<![-\w])background(?:-color)?\s*:/)
|
|
113
|
+
next false unless rule[:body].match?(/(?<![-\w])color\s*:/)
|
|
114
|
+
|
|
115
|
+
rule[:selectors].any? do |selector|
|
|
116
|
+
control[:classes].any? { |cls| selector.include?(".#{cls}") } && weight(selector) >= 2
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
check!(
|
|
121
|
+
painting.any?,
|
|
122
|
+
"#{label} (class=#{control[:classes].join(' ')}) has no CSS rule that sets both " \
|
|
123
|
+
"background and color with at least two classes of specificity. The Tailwind " \
|
|
124
|
+
"forms plugin paints it white from a later stylesheet, so dark mode shows a white hole."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
painting.each do |rule|
|
|
128
|
+
%w[background color].each do |property|
|
|
129
|
+
value = rule[:body][/(?<![-\w])#{property}(?:-color)?\s*:([^;]+);/, 1].to_s.strip
|
|
130
|
+
next if value.empty?
|
|
131
|
+
|
|
132
|
+
check!(
|
|
133
|
+
value.include?("var(--"),
|
|
134
|
+
"#{rule[:file]}: #{rule[:selectors].join(', ')} sets #{property}: #{value}. " \
|
|
135
|
+
"Use a theme custom property — a literal colour is correct in one palette and wrong in the other."
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
puts " #{label}: painted by #{painting.map { |r| r[:selectors].first }.uniq.join(' / ')}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
puts "All form control theming checks passed (#{controls.length} control(s))."
|