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,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AC-PRF-05, AC-PRF-06, AC-PRF-07, AC-PRF-15 — patch sanitizer + block chunking.
|
|
4
|
+
* Spec: docs/features/studio-proof.md §8
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import {
|
|
8
|
+
flattenBlockPatches,
|
|
9
|
+
locateUnique,
|
|
10
|
+
splitIntoBlocks,
|
|
11
|
+
} from "../../functions/api/studio/_lib/blocks.js";
|
|
12
|
+
import { sanitizePatches } from "../../functions/api/studio/_lib/sanitize.js";
|
|
13
|
+
import {
|
|
14
|
+
buildGeminiRequest,
|
|
15
|
+
truncateGuidelines,
|
|
16
|
+
} from "../../functions/api/studio/_lib/prompts.js";
|
|
17
|
+
|
|
18
|
+
const text =
|
|
19
|
+
'Hello wrld.\n\n{% include figure.html src="/media/x.png" %}\n\n`code` and ```\nfence\n```\n';
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
const { patches, stats } = sanitizePatches({
|
|
23
|
+
mode: "correct",
|
|
24
|
+
text,
|
|
25
|
+
patches: [
|
|
26
|
+
{
|
|
27
|
+
start: 6,
|
|
28
|
+
end: 10,
|
|
29
|
+
original: "wrld",
|
|
30
|
+
replacement: "world",
|
|
31
|
+
category: "spelling",
|
|
32
|
+
reason: "typo",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
start: 6,
|
|
36
|
+
end: 10,
|
|
37
|
+
original: "NOPE",
|
|
38
|
+
replacement: "world",
|
|
39
|
+
category: "spelling",
|
|
40
|
+
reason: "bad",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
start: text.indexOf("{%"),
|
|
44
|
+
end: text.indexOf("%}") + 2,
|
|
45
|
+
original: text.slice(text.indexOf("{%"), text.indexOf("%}") + 2),
|
|
46
|
+
replacement: "x",
|
|
47
|
+
category: "grammar",
|
|
48
|
+
reason: "include",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
start: 0,
|
|
52
|
+
end: 5,
|
|
53
|
+
original: "Hello",
|
|
54
|
+
replacement: "Hiya!",
|
|
55
|
+
category: "clarity",
|
|
56
|
+
reason: "style",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
assert.equal(patches.length, 1);
|
|
61
|
+
assert.equal(patches[0].replacement, "world");
|
|
62
|
+
assert.equal(stats.kept, 1);
|
|
63
|
+
assert.ok(stats.dropped >= 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
// Offset recovery: wrong start/end, exact original still locates.
|
|
68
|
+
const { patches } = sanitizePatches({
|
|
69
|
+
mode: "correct",
|
|
70
|
+
text: "alpha beta gamma",
|
|
71
|
+
patches: [
|
|
72
|
+
{
|
|
73
|
+
start: 99,
|
|
74
|
+
end: 103,
|
|
75
|
+
original: "beta",
|
|
76
|
+
replacement: "BETA",
|
|
77
|
+
category: "spelling",
|
|
78
|
+
reason: "case",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
assert.equal(patches.length, 1);
|
|
83
|
+
assert.equal(patches[0].start, 6);
|
|
84
|
+
assert.equal(patches[0].end, 10);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
const { patches } = sanitizePatches({
|
|
89
|
+
mode: "refine",
|
|
90
|
+
text: "A clunky sentence here.",
|
|
91
|
+
patches: [
|
|
92
|
+
{
|
|
93
|
+
start: 0,
|
|
94
|
+
end: 23,
|
|
95
|
+
original: "A clunky sentence here.",
|
|
96
|
+
replacement: "A clearer sentence here.",
|
|
97
|
+
category: "clarity",
|
|
98
|
+
reason: "flow",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
assert.equal(patches.length, 1);
|
|
103
|
+
assert.equal(patches[0].category, "clarity");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
// {% cite %} is protected like include.
|
|
108
|
+
const src = "Antes {% cite turino-2008 %} depois.";
|
|
109
|
+
const cite = "{% cite turino-2008 %}";
|
|
110
|
+
const at = src.indexOf(cite);
|
|
111
|
+
const { patches } = sanitizePatches({
|
|
112
|
+
mode: "refine",
|
|
113
|
+
text: src,
|
|
114
|
+
patches: [
|
|
115
|
+
{
|
|
116
|
+
start: at,
|
|
117
|
+
end: at + cite.length,
|
|
118
|
+
original: cite,
|
|
119
|
+
replacement: "REMOVED",
|
|
120
|
+
category: "clarity",
|
|
121
|
+
reason: "bad",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
assert.equal(patches.length, 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
const body =
|
|
130
|
+
"First paragraph here.\n\nSecond paragraph here.\n\n## Heading\n\nThird paragraph with more words to grow.";
|
|
131
|
+
const blocks = splitIntoBlocks(body, { targetChars: 40, maxChars: 80 });
|
|
132
|
+
assert.ok(blocks.length >= 2);
|
|
133
|
+
assert.equal(blocks[0].start, 0);
|
|
134
|
+
assert.equal(body.slice(blocks[0].start, blocks[0].end), blocks[0].text);
|
|
135
|
+
const joined = blocks.map((b) => b.text).join("");
|
|
136
|
+
// Separators are kept on preceding blocks; concatenation should rebuild body.
|
|
137
|
+
assert.equal(joined, body);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
const body = "Alpha one.\n\nBeta two.\n\nGamma three.";
|
|
142
|
+
const blocks = splitIntoBlocks(body, { targetChars: 10, maxChars: 20 });
|
|
143
|
+
const flat = flattenBlockPatches(blocks, [
|
|
144
|
+
{
|
|
145
|
+
block: 1,
|
|
146
|
+
original: "Beta two.",
|
|
147
|
+
replacement: "Beta too.",
|
|
148
|
+
category: "clarity",
|
|
149
|
+
reason: "tight",
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
assert.equal(flat.length, 1);
|
|
153
|
+
assert.equal(body.slice(flat[0].start, flat[0].end), "Beta two.");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
const hit = locateUnique("aa bb aa", "aa", 5);
|
|
158
|
+
assert.equal(hit.start, 6);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
const req = buildGeminiRequest({
|
|
163
|
+
mode: "correct",
|
|
164
|
+
language: "en",
|
|
165
|
+
path: "content/collections/articles/x.md",
|
|
166
|
+
text: "teh",
|
|
167
|
+
});
|
|
168
|
+
const sys = req.systemInstruction.parts[0].text;
|
|
169
|
+
assert.ok(sys.includes("mechanical corrector"));
|
|
170
|
+
assert.ok(!sys.includes("Writing guidelines (published pack)"));
|
|
171
|
+
assert.equal(
|
|
172
|
+
req.generationConfig.responseSchema.properties.patches.items.required.includes(
|
|
173
|
+
"start"
|
|
174
|
+
),
|
|
175
|
+
true
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
const blocks = splitIntoBlocks("Short prose for refine.\n\nAnother beat.");
|
|
181
|
+
const req = buildGeminiRequest({
|
|
182
|
+
mode: "refine",
|
|
183
|
+
language: "pt-BR",
|
|
184
|
+
path: "content/collections/posts/x.md",
|
|
185
|
+
text: "Short prose for refine.\n\nAnother beat.",
|
|
186
|
+
guidelinesBody: "You are not a co-author.\n",
|
|
187
|
+
blocks,
|
|
188
|
+
});
|
|
189
|
+
const sys = req.systemInstruction.parts[0].text;
|
|
190
|
+
const user = req.contents[0].parts[0].text;
|
|
191
|
+
assert.ok(sys.includes("tone consistency") || sys.includes("Preserve tone"));
|
|
192
|
+
assert.ok(sys.includes("not a co-author"));
|
|
193
|
+
assert.ok(sys.includes("invoked Refine"));
|
|
194
|
+
assert.ok(user.includes("blocks:"));
|
|
195
|
+
assert.ok(user.includes("[0]"));
|
|
196
|
+
assert.ok(
|
|
197
|
+
req.generationConfig.responseSchema.properties.patches.items.required.includes(
|
|
198
|
+
"block"
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
const long = "a".repeat(100);
|
|
205
|
+
const truncated = truncateGuidelines(long, 50);
|
|
206
|
+
assert.ok(truncated.length < long.length);
|
|
207
|
+
assert.ok(truncated.includes("truncated"));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log("All Studio rewrite sanitize checks passed.");
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Refs: PLT-AC-3 — Studio schema includes registry collections and drops them
|
|
5
|
+
# when the page is removed.
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "yaml"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
require "open3"
|
|
11
|
+
|
|
12
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
13
|
+
$LOAD_PATH.unshift(ROOT.join("lib").to_s)
|
|
14
|
+
require "pandorga"
|
|
15
|
+
require "pandorga/commands/studio_schema"
|
|
16
|
+
|
|
17
|
+
EXAMPLE = ROOT.join("examples/full")
|
|
18
|
+
TEMPLATES = ROOT.join("templates")
|
|
19
|
+
EXE = ROOT.join("exe/pandorga")
|
|
20
|
+
|
|
21
|
+
def fail!(message)
|
|
22
|
+
warn "FAIL test-studio-schema-composition: #{message}"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def registered_collections(config)
|
|
27
|
+
names = []
|
|
28
|
+
Array(config.dig("pandorga", "pages")).each do |page|
|
|
29
|
+
next unless page.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
single = page["collection"].to_s
|
|
32
|
+
names << single unless single.empty?
|
|
33
|
+
|
|
34
|
+
cols = page["collections"]
|
|
35
|
+
next unless cols.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
cols.each_value do |name|
|
|
38
|
+
n = name.to_s
|
|
39
|
+
next if n.empty? || n == "data" || n == "profile"
|
|
40
|
+
|
|
41
|
+
names << n
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
names.uniq
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def collection_names_from_schema(schema)
|
|
48
|
+
names = []
|
|
49
|
+
Array(schema["content"]).each do |group|
|
|
50
|
+
next unless group.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
Array(group["items"]).each do |item|
|
|
53
|
+
next unless item.is_a?(Hash)
|
|
54
|
+
next unless item["type"].to_s == "collection"
|
|
55
|
+
|
|
56
|
+
names << item["name"].to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
if schema["collections"].is_a?(Hash)
|
|
60
|
+
names |= schema["collections"].keys.map(&:to_s)
|
|
61
|
+
end
|
|
62
|
+
names.uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
config = YAML.safe_load(EXAMPLE.join("_config.yml").read, aliases: true)
|
|
66
|
+
expected = registered_collections(config)
|
|
67
|
+
fail!("examples/full has no registered collections") if expected.empty?
|
|
68
|
+
|
|
69
|
+
composer = Pandorga::Commands::StudioSchema.new
|
|
70
|
+
schema = composer.compose(config, TEMPLATES)
|
|
71
|
+
present = collection_names_from_schema(schema)
|
|
72
|
+
|
|
73
|
+
missing = expected - present
|
|
74
|
+
fail!("schema missing collections: #{missing.inspect} (have #{present.inspect})") unless missing.empty?
|
|
75
|
+
|
|
76
|
+
unless schema["media"].is_a?(Hash)
|
|
77
|
+
fail!("composed schema should include a media block when templates need it")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
unless schema["content"].is_a?(Array) && !schema["content"].empty?
|
|
81
|
+
fail!("composed schema should include content groups")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Taxonomy from examples/full should land on the articles tags field.
|
|
85
|
+
articles_entry = schema.dig("collections", "articles")
|
|
86
|
+
fail!("articles collection missing from index") unless articles_entry.is_a?(Hash)
|
|
87
|
+
tags_field = Array(articles_entry["fields"]).find { |f| f.is_a?(Hash) && f["name"] == "tags" }
|
|
88
|
+
tag_values = tags_field&.dig("options", "values")
|
|
89
|
+
unless tag_values == ["Writing"]
|
|
90
|
+
fail!("articles tags should come from pandorga.taxonomies (got #{tag_values.inspect})")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Remove the articles page; articles must leave the schema.
|
|
94
|
+
reduced = Marshal.load(Marshal.dump(config))
|
|
95
|
+
reduced["pandorga"]["pages"].reject! { |p| p.is_a?(Hash) && p["key"].to_s == "articles" }
|
|
96
|
+
reduced_schema = composer.compose(reduced, TEMPLATES)
|
|
97
|
+
reduced_names = collection_names_from_schema(reduced_schema)
|
|
98
|
+
if reduced_names.include?("articles")
|
|
99
|
+
fail!("articles still present after removing the page from the registry")
|
|
100
|
+
end
|
|
101
|
+
still_expected = registered_collections(reduced)
|
|
102
|
+
still_missing = still_expected - reduced_names
|
|
103
|
+
unless still_missing.empty?
|
|
104
|
+
fail!("after removal, still missing: #{still_missing.inspect}")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# CLI writes schema under a temp site root (optional end-to-end check).
|
|
108
|
+
Dir.mktmpdir("pandorga-studio-schema-") do |dir|
|
|
109
|
+
site = Pathname.new(dir)
|
|
110
|
+
FileUtils.cp(EXAMPLE.join("_config.yml"), site.join("_config.yml"))
|
|
111
|
+
env = { "RUBYOPT" => "-I#{ROOT.join('lib')}" }
|
|
112
|
+
stdout, stderr, status = Open3.capture3(
|
|
113
|
+
env, "ruby", EXE.to_s, "studio-schema", site.to_s
|
|
114
|
+
)
|
|
115
|
+
fail!("CLI failed: #{stdout}\n#{stderr}") unless status.success?
|
|
116
|
+
|
|
117
|
+
written = site.join("studio/schema.yml")
|
|
118
|
+
fail!("CLI did not write #{written}") unless written.file?
|
|
119
|
+
|
|
120
|
+
from_disk = YAML.safe_load(written.read, aliases: true)
|
|
121
|
+
disk_names = collection_names_from_schema(from_disk)
|
|
122
|
+
disk_missing = expected - disk_names
|
|
123
|
+
fail!("CLI schema missing: #{disk_missing.inspect}") unless disk_missing.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
puts "PASS test-studio-schema-composition (#{expected.length} collections; removal + CLI ok)"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# AC-STU: Studio schema replaces .pages.yml as the live CMS model.
|
|
4
|
+
# Spec: docs/features/studio.md
|
|
5
|
+
|
|
6
|
+
require "date"
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
11
|
+
CONFIG_PATH = REPO_ROOT.join("studio/schema.yml")
|
|
12
|
+
|
|
13
|
+
def check!(condition, message = "condition was false")
|
|
14
|
+
raise "Assertion failed: #{message}" unless condition
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def each_content_entry(nodes, &block)
|
|
18
|
+
Array(nodes).each do |entry|
|
|
19
|
+
next unless entry.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
type = entry["type"].to_s
|
|
22
|
+
if type == "group"
|
|
23
|
+
each_content_entry(entry["items"], &block)
|
|
24
|
+
else
|
|
25
|
+
yield entry
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_collection(config, name)
|
|
31
|
+
found = nil
|
|
32
|
+
each_content_entry(config["content"]) do |entry|
|
|
33
|
+
found = entry if entry["type"].to_s == "collection" && entry["name"].to_s == name
|
|
34
|
+
end
|
|
35
|
+
found
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def field_names(collection)
|
|
39
|
+
(collection["fields"] || []).map { |field| field["name"].to_s }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "Testing Studio schema (studio/schema.yml)..."
|
|
43
|
+
|
|
44
|
+
check!(CONFIG_PATH.file?, "missing studio/schema.yml")
|
|
45
|
+
|
|
46
|
+
config = YAML.safe_load(CONFIG_PATH.read, permitted_classes: [Date, Time], aliases: true) || {}
|
|
47
|
+
check!(config["media"].is_a?(Hash) || config["media"].is_a?(String) || config["media"].is_a?(Array),
|
|
48
|
+
"studio/schema.yml must define media")
|
|
49
|
+
|
|
50
|
+
media = config["media"]
|
|
51
|
+
media_input = nil
|
|
52
|
+
if media.is_a?(Hash)
|
|
53
|
+
media_input = media["input"].to_s
|
|
54
|
+
check!(media_input.include?("content/media"),
|
|
55
|
+
"schema media.input must point under content/media")
|
|
56
|
+
check!(media["output"].to_s.start_with?("/media"),
|
|
57
|
+
"schema media.output must be a /media public path")
|
|
58
|
+
elsif media.is_a?(Array)
|
|
59
|
+
images = media.find { |entry| entry.is_a?(Hash) && entry["name"].to_s == "images" } || media.first
|
|
60
|
+
media_input = images.is_a?(Hash) ? images["input"].to_s : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if media_input && !media_input.empty?
|
|
64
|
+
each_content_entry(config["content"]) do |entry|
|
|
65
|
+
(entry["fields"] || []).each do |field|
|
|
66
|
+
next unless field.is_a?(Hash) && field["type"].to_s == "image"
|
|
67
|
+
|
|
68
|
+
path = field.dig("options", "path").to_s
|
|
69
|
+
next if path.empty?
|
|
70
|
+
|
|
71
|
+
check!(path == media_input || path.start_with?("#{media_input}/"),
|
|
72
|
+
"#{entry['name']}.#{field['name']} options.path must start with media.input " \
|
|
73
|
+
"(#{media_input.inspect}); got #{path.inspect}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
posts = find_collection(config, "posts")
|
|
79
|
+
articles = find_collection(config, "articles")
|
|
80
|
+
check!(!posts.nil?, "missing posts collection in studio/schema.yml")
|
|
81
|
+
check!(!articles.nil?, "missing articles collection in studio/schema.yml")
|
|
82
|
+
|
|
83
|
+
check!(posts["format"].to_s == "yaml-frontmatter",
|
|
84
|
+
"posts must use yaml-frontmatter")
|
|
85
|
+
check!(articles["format"].to_s == "yaml-frontmatter",
|
|
86
|
+
"articles must use yaml-frontmatter")
|
|
87
|
+
check!(posts.dig("operations", "delete") == false,
|
|
88
|
+
"posts operations.delete must be false")
|
|
89
|
+
check!(articles.dig("operations", "delete") == false,
|
|
90
|
+
"articles operations.delete must be false")
|
|
91
|
+
|
|
92
|
+
posts_fields = field_names(posts)
|
|
93
|
+
articles_fields = field_names(articles)
|
|
94
|
+
|
|
95
|
+
check!(!posts_fields.include?("description"),
|
|
96
|
+
"posts must not expose description (excerpts come from the body)")
|
|
97
|
+
check!(!posts_fields.include?("last_updated"),
|
|
98
|
+
"posts must not expose last_updated (build derives it from Git)")
|
|
99
|
+
check!(!articles_fields.include?("last_updated"),
|
|
100
|
+
"articles must not expose last_updated (build derives it from Git)")
|
|
101
|
+
check!(articles_fields.include?("description"),
|
|
102
|
+
"articles must keep description for editorial deck copy")
|
|
103
|
+
|
|
104
|
+
filename = posts["filename"].to_s
|
|
105
|
+
check!(filename.include?("{year}") && filename.include?("{key}"),
|
|
106
|
+
"posts filename must be dated + key (got #{filename.inspect})")
|
|
107
|
+
articles_filename = articles["filename"].to_s
|
|
108
|
+
check!(articles_filename.include?("{year}") && articles_filename.include?("{key}"),
|
|
109
|
+
"articles filename must be dated + key (got #{articles_filename.inspect})")
|
|
110
|
+
check!(posts.dig("view", "primary").to_s == "title",
|
|
111
|
+
"posts list primary should stay title (filename uses key separately)")
|
|
112
|
+
check!(articles.dig("view", "primary").to_s == "title",
|
|
113
|
+
"articles list primary should stay title (filename uses key separately)")
|
|
114
|
+
|
|
115
|
+
%w[posts articles].each do |name|
|
|
116
|
+
collection = name == "posts" ? posts : articles
|
|
117
|
+
key_field = (collection["fields"] || []).find { |field| field["name"].to_s == "key" }
|
|
118
|
+
check!(!key_field.nil?, "#{name} must define a key field")
|
|
119
|
+
check!(key_field["required"] == true, "#{name} key must be required for filename creation")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
products = find_collection(config, "products")
|
|
123
|
+
projects = find_collection(config, "projects")
|
|
124
|
+
check!(!products.nil?, "missing products collection")
|
|
125
|
+
check!(!projects.nil?, "missing projects collection")
|
|
126
|
+
check!(products["filename"].to_s.include?("{key}"),
|
|
127
|
+
"products filename must use key (got #{products['filename'].inspect})")
|
|
128
|
+
check!(projects["filename"].to_s.include?("{key}"),
|
|
129
|
+
"projects filename must use key (got #{projects['filename'].inspect})")
|
|
130
|
+
|
|
131
|
+
pipeline = REPO_ROOT.join(".github/workflows/content-pipeline.yml").read
|
|
132
|
+
on_push = pipeline[/\non:\n push:\n(?: .*\n)*/]
|
|
133
|
+
check!(!on_push.nil? && !on_push.include?("paths:"),
|
|
134
|
+
"content-pipeline.yml on.push must not use paths: (CMS commits must always trigger)")
|
|
135
|
+
check!(!pipeline.include?("MODE=skip"),
|
|
136
|
+
"content-pipeline.yml must not skip empty CMS commits: those still need an R2 publish")
|
|
137
|
+
check!(pipeline.include?("if: github.ref == 'refs/heads/main'"),
|
|
138
|
+
"content-pipeline.yml must publish on main for push and manual run")
|
|
139
|
+
check!(!pipeline.match?(/sveltia/i),
|
|
140
|
+
"content-pipeline.yml must not reference Sveltia")
|
|
141
|
+
|
|
142
|
+
index = REPO_ROOT.join("studio/index.html").read
|
|
143
|
+
check!(!index.include?("app.pagescms.org"),
|
|
144
|
+
"studio/index.html must not redirect to Pages CMS (AC-STU-08)")
|
|
145
|
+
check!(!index.match?(/sveltia/i),
|
|
146
|
+
"studio/index.html must not load or mention Sveltia")
|
|
147
|
+
check!(index.include?("studio-logo"),
|
|
148
|
+
"studio/index.html must use the Studio logo favicon")
|
|
149
|
+
check!(index.include?('id="app"') || index.include?("src/main"),
|
|
150
|
+
"studio/index.html must mount the Studio SPA")
|
|
151
|
+
|
|
152
|
+
api = REPO_ROOT.join("functions/api/studio/[[path]].js")
|
|
153
|
+
check!(api.file?, "missing functions/api/studio/[[path]].js")
|
|
154
|
+
api_src = api.read
|
|
155
|
+
check!(api_src.include?("STUDIO_ALLOWLIST") || REPO_ROOT.join("functions/api/studio/_lib/auth.js").read.include?("STUDIO_ALLOWLIST"),
|
|
156
|
+
"Studio API must enforce STUDIO_ALLOWLIST")
|
|
157
|
+
check!(REPO_ROOT.join("functions/api/studio/_lib/github.js").read.include?("GITHUB_TOKEN"),
|
|
158
|
+
"Studio API must use server-side GITHUB_TOKEN")
|
|
159
|
+
check!(!api_src.include?("ghp_"), "API must not hardcode a GitHub PAT (AC-STU-09)")
|
|
160
|
+
|
|
161
|
+
editor = REPO_ROOT.join("studio-app/src/editor/body-editor.js").read
|
|
162
|
+
check!(editor.include?("autolink: false"),
|
|
163
|
+
"TipTap Link must set autolink: false (AC-STU-03)")
|
|
164
|
+
check!(editor.include?("linkOnPaste: false"),
|
|
165
|
+
"TipTap Link must set linkOnPaste: false")
|
|
166
|
+
|
|
167
|
+
puts "All Studio schema / shell checks passed."
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Refs: PLT-AC-4 — each template ships README, template.yml, fixtures, screenshots.
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
7
|
+
TEMPLATES = %w[cv articles blog media network bibliography portfolio].freeze
|
|
8
|
+
TEMPLATES_ROOT = ROOT.join("templates")
|
|
9
|
+
|
|
10
|
+
failed = false
|
|
11
|
+
|
|
12
|
+
TEMPLATES.each do |name|
|
|
13
|
+
dir = TEMPLATES_ROOT.join(name)
|
|
14
|
+
unless dir.directory?
|
|
15
|
+
warn "FAIL test-template-packages: missing templates/#{name}/"
|
|
16
|
+
failed = true
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
readme = dir.join("README.md")
|
|
21
|
+
unless readme.file? && readme.read.strip.length.positive?
|
|
22
|
+
warn "FAIL test-template-packages: #{name} missing README.md"
|
|
23
|
+
failed = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
yaml = dir.join("template.yml")
|
|
27
|
+
unless yaml.file? && yaml.read.strip.length.positive?
|
|
28
|
+
warn "FAIL test-template-packages: #{name} missing template.yml"
|
|
29
|
+
failed = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
fixtures = dir.join("fixtures")
|
|
33
|
+
unless fixtures.directory? && fixtures.children.any? { |c| c.file? }
|
|
34
|
+
warn "FAIL test-template-packages: #{name} missing fixtures/ with sample files"
|
|
35
|
+
failed = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
screenshots = dir.join("screenshots")
|
|
39
|
+
has_shot_dir = screenshots.directory?
|
|
40
|
+
has_shot_asset = has_shot_dir && screenshots.children.any? do |c|
|
|
41
|
+
c.file? && (c.basename.to_s.match?(/\.(png|jpe?g|webp|gif)\z/i) || c.basename.to_s == "README.md" || c.basename.to_s == ".gitkeep")
|
|
42
|
+
end
|
|
43
|
+
unless has_shot_asset
|
|
44
|
+
warn "FAIL test-template-packages: #{name} missing screenshots/ (asset, README, or .gitkeep)"
|
|
45
|
+
failed = true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
exit 1 if failed
|
|
50
|
+
|
|
51
|
+
puts "PASS test-template-packages"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
7
|
+
require REPO_ROOT.join("_plugins/lib/text_excerpt").to_s
|
|
8
|
+
|
|
9
|
+
def fail!(message)
|
|
10
|
+
warn message
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check!(condition, message)
|
|
15
|
+
fail!(message) unless condition
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts "Testing TextExcerpt listing previews..."
|
|
19
|
+
|
|
20
|
+
heading_body = <<~MD
|
|
21
|
+
## Contexto
|
|
22
|
+
|
|
23
|
+
Os dados públicos do INSEE permitem medir os casamentos celebrados na França.
|
|
24
|
+
MD
|
|
25
|
+
|
|
26
|
+
excerpt = TextExcerpt.from_markdown(heading_body)
|
|
27
|
+
check!(excerpt.start_with?("Contexto: Os dados públicos"),
|
|
28
|
+
"headings should become 'Title: ' prefixes (got #{excerpt.inspect})")
|
|
29
|
+
|
|
30
|
+
long = "palavra " * 80
|
|
31
|
+
truncated = TextExcerpt.from_markdown(long)
|
|
32
|
+
check!(truncated.end_with?("..."), "long bodies must truncate with an ellipsis")
|
|
33
|
+
check!(truncated.length <= 220, "truncated excerpt must stay within 220 characters")
|
|
34
|
+
|
|
35
|
+
liquid = "{% include plotly.html src=\"/media/plots/x.html\" %}\n\nDepois do gráfico vem o texto."
|
|
36
|
+
check!(TextExcerpt.from_markdown(liquid) == "Depois do gráfico vem o texto.",
|
|
37
|
+
"liquid includes must not leak into the excerpt")
|
|
38
|
+
|
|
39
|
+
fields = TextExcerpt.listing_fields({ "description" => " Deck editorial. " }, heading_body)
|
|
40
|
+
check!(fields["description"] == "Deck editorial.",
|
|
41
|
+
"listing_fields must keep the trimmed editorial description")
|
|
42
|
+
check!(fields["excerpt"].start_with?("Contexto:"),
|
|
43
|
+
"listing_fields must still generate a body excerpt")
|
|
44
|
+
|
|
45
|
+
empty_desc = TextExcerpt.listing_fields({ "description" => "" }, heading_body)
|
|
46
|
+
check!(empty_desc["description"].empty?,
|
|
47
|
+
"empty editorial description stays empty in the index")
|
|
48
|
+
check!(!empty_desc["excerpt"].empty?,
|
|
49
|
+
"empty editorial description still gets a generated excerpt")
|
|
50
|
+
|
|
51
|
+
puts "All TextExcerpt checks passed."
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Mirrors unwrapMediaSrc in content-runtime/10-utils.html and md-to-pdf.mjs.
|
|
3
|
+
function unwrapMediaSrc(path) {
|
|
4
|
+
const value = String(path == null ? '' : path).trim();
|
|
5
|
+
if (!value) return '';
|
|
6
|
+
const md = value.match(/^\[([^\]]*)\]\(([^)\s]+)\)\s*$/);
|
|
7
|
+
if (md) return String(md[2] || '').trim();
|
|
8
|
+
const angle = value.match(/^<([^>\s]+)>\s*$/);
|
|
9
|
+
if (angle) return String(angle[1] || '').trim();
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const cases = [
|
|
14
|
+
['https://example.com/a.png', 'https://example.com/a.png'],
|
|
15
|
+
[
|
|
16
|
+
'[https://example.com/a.png](https://example.com/a.png)',
|
|
17
|
+
'https://example.com/a.png',
|
|
18
|
+
],
|
|
19
|
+
['[label](https://example.com/a.png)', 'https://example.com/a.png'],
|
|
20
|
+
['<https://example.com/a.png>', 'https://example.com/a.png'],
|
|
21
|
+
['media/images/a.png', 'media/images/a.png'],
|
|
22
|
+
['[not a full](link) leftover', '[not a full](link) leftover'],
|
|
23
|
+
['', ''],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
let failed = 0;
|
|
27
|
+
for (const [input, expected] of cases) {
|
|
28
|
+
const got = unwrapMediaSrc(input);
|
|
29
|
+
if (got !== expected) {
|
|
30
|
+
console.error(`FAIL: ${JSON.stringify(input)} → ${JSON.stringify(got)} (want ${JSON.stringify(expected)})`);
|
|
31
|
+
failed += 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (failed) {
|
|
36
|
+
console.error(`unwrapMediaSrc: ${failed} failure(s)`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
console.log(`PASS: unwrapMediaSrc (${cases.length} cases)`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Keep unwrapMediaSrc (Pages CMS Markdown-autolink mangling of include src=)
|
|
5
|
+
# honest against a small Node mirror. The browser runtime and PDF exporter
|
|
6
|
+
# share the same contract.
|
|
7
|
+
|
|
8
|
+
require "pathname"
|
|
9
|
+
require "open3"
|
|
10
|
+
|
|
11
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
12
|
+
SCRIPT = REPO_ROOT.join("scripts", "test", "test-unwrap-media-src.mjs")
|
|
13
|
+
|
|
14
|
+
puts "Testing unwrapMediaSrc (CMS autolink unwrap)..."
|
|
15
|
+
|
|
16
|
+
stdout, stderr, status = Open3.capture3("node", SCRIPT.to_s)
|
|
17
|
+
print stdout
|
|
18
|
+
warn stderr unless stderr.empty?
|
|
19
|
+
exit(status.exitstatus)
|