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,567 @@
|
|
|
1
|
+
{% raw %}
|
|
2
|
+
function assertManifestSchema(manifest) {
|
|
3
|
+
var expected = (window.ContentRuntimeConfig && window.ContentRuntimeConfig.expectedSchemaVersion) || 1;
|
|
4
|
+
var version = manifest && manifest.schema_version;
|
|
5
|
+
// Legacy manifests without schema_version are treated as version 1 during migration.
|
|
6
|
+
if (version == null) version = 1;
|
|
7
|
+
if (Number(version) !== Number(expected)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'Unsupported content schema_version ' + version +
|
|
10
|
+
' (shell expects ' + expected + '). Redeploy content or the shell together.'
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function listPaths(collection, manifestKey) {
|
|
16
|
+
var manifestResponse = await fetch(buildUrl('manifest.json'));
|
|
17
|
+
if (manifestResponse.ok) {
|
|
18
|
+
var manifest = await manifestResponse.json();
|
|
19
|
+
assertManifestSchema(manifest);
|
|
20
|
+
var files = manifest.files && manifest.files[manifestKey];
|
|
21
|
+
if (Array.isArray(files)) return files;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var response = await fetch(buildUrl('?prefix=' + collection + '/'));
|
|
25
|
+
if (!response.ok) throw new Error('Bucket listing failed: ' + response.status);
|
|
26
|
+
var xml = await response.text();
|
|
27
|
+
var doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
28
|
+
var re = new RegExp('^' + collection.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/.+\\.json$');
|
|
29
|
+
return Array.from(doc.getElementsByTagName('Key'))
|
|
30
|
+
.map(function (node) { return node.textContent; })
|
|
31
|
+
.filter(function (path) { return re.test(path); });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var jsonCache = Object.create(null);
|
|
35
|
+
var collectionCache = Object.create(null);
|
|
36
|
+
|
|
37
|
+
async function loadCollection(collection, manifestKey) {
|
|
38
|
+
var cacheKey = String(collection) + '\0' + String(manifestKey || '');
|
|
39
|
+
if (collectionCache[cacheKey]) return collectionCache[cacheKey];
|
|
40
|
+
collectionCache[cacheKey] = (async function () {
|
|
41
|
+
var paths = await listPaths(collection, manifestKey);
|
|
42
|
+
var items = await Promise.all(paths.map(function (path) {
|
|
43
|
+
return fetch(buildUrl(path)).then(function (response) {
|
|
44
|
+
// Skip missing keys so a stale manifest cannot blank Rascunhos,
|
|
45
|
+
// Articles, or xcite resolution for the rest of the collection.
|
|
46
|
+
if (response.status === 404) {
|
|
47
|
+
console.warn('Skipping missing content object', path);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok) throw new Error('Content fetch failed: ' + path);
|
|
51
|
+
return response.json();
|
|
52
|
+
});
|
|
53
|
+
}));
|
|
54
|
+
return items.filter(Boolean).map(hydrateFrontMatter).filter(function (item) {
|
|
55
|
+
var includeUnpublished = !!(window.ContentRuntimeConfig && window.ContentRuntimeConfig.includeUnpublished);
|
|
56
|
+
if (includeUnpublished) return true;
|
|
57
|
+
return !(item.front_matter && item.front_matter.published === false);
|
|
58
|
+
});
|
|
59
|
+
})().catch(function (error) {
|
|
60
|
+
delete collectionCache[cacheKey];
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
return collectionCache[cacheKey];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadJson(path) {
|
|
67
|
+
var key = String(path || '');
|
|
68
|
+
if (jsonCache[key]) return jsonCache[key];
|
|
69
|
+
jsonCache[key] = fetch(buildUrl(path)).then(function (response) {
|
|
70
|
+
if (!response.ok) throw new Error('Content JSON fetch failed: ' + path);
|
|
71
|
+
return response.json();
|
|
72
|
+
}).catch(function (error) {
|
|
73
|
+
delete jsonCache[key];
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
return jsonCache[key];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var textCache = Object.create(null);
|
|
80
|
+
|
|
81
|
+
async function loadText(path) {
|
|
82
|
+
var key = String(path || '');
|
|
83
|
+
if (textCache[key]) return textCache[key];
|
|
84
|
+
textCache[key] = fetch(buildUrl(path)).then(function (response) {
|
|
85
|
+
if (!response.ok) throw new Error('Content asset fetch failed: ' + path);
|
|
86
|
+
return response.text();
|
|
87
|
+
}).catch(function (error) {
|
|
88
|
+
delete textCache[key];
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
return textCache[key];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function loadCollectionItem(collection, slug) {
|
|
95
|
+
return loadJson('collections/' + collection + '/' + slug + '.json').then(hydrateFrontMatter);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writingIndexAsListingItem(entry) {
|
|
99
|
+
entry = entry || {};
|
|
100
|
+
return {
|
|
101
|
+
slug: entry.slug,
|
|
102
|
+
key: entry.key,
|
|
103
|
+
url: entry.url,
|
|
104
|
+
type: entry.type,
|
|
105
|
+
collection: entry.collection,
|
|
106
|
+
date: entry.date,
|
|
107
|
+
last_updated: entry.last_updated,
|
|
108
|
+
aliases: Array.isArray(entry.aliases) ? entry.aliases : [],
|
|
109
|
+
front_matter: {
|
|
110
|
+
title: entry.title,
|
|
111
|
+
category: entry.category,
|
|
112
|
+
description: entry.description,
|
|
113
|
+
excerpt: entry.excerpt,
|
|
114
|
+
thumbnail: entry.thumbnail,
|
|
115
|
+
language: entry.language,
|
|
116
|
+
date: entry.date,
|
|
117
|
+
last_updated: entry.last_updated,
|
|
118
|
+
company: entry.company,
|
|
119
|
+
product: entry.product,
|
|
120
|
+
project: entry.project,
|
|
121
|
+
tagline: entry.tagline,
|
|
122
|
+
start_date: entry.start_date,
|
|
123
|
+
end_date: entry.end_date,
|
|
124
|
+
sort_order: entry.sort_order,
|
|
125
|
+
status: entry.status,
|
|
126
|
+
label: entry.label,
|
|
127
|
+
icon: entry.icon,
|
|
128
|
+
tags: entry.tags,
|
|
129
|
+
external_url: entry.external_url,
|
|
130
|
+
article_url: entry.article_url,
|
|
131
|
+
github: entry.github,
|
|
132
|
+
key: entry.key
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadWritingIndex(filterCollection) {
|
|
138
|
+
return loadJson('data/writing_index.json').then(function (entries) {
|
|
139
|
+
var items = (entries || []).map(writingIndexAsListingItem);
|
|
140
|
+
if (!filterCollection) return items;
|
|
141
|
+
return items.filter(function (item) {
|
|
142
|
+
return item.collection === filterCollection || item.type === filterCollection;
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stripDatedSlugPrefix(slug) {
|
|
148
|
+
return String(slug || '').replace(/^(?:\d{4}-\d{2}-\d{2}-|\d{8}-)/, '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveWritingAlias(entries, slug) {
|
|
152
|
+
if (!slug) return null;
|
|
153
|
+
var items = entries || [];
|
|
154
|
+
var exact = items.find(function (item) { return item.slug === slug; });
|
|
155
|
+
if (exact) return exact;
|
|
156
|
+
|
|
157
|
+
var aliased = items.find(function (item) {
|
|
158
|
+
return Array.isArray(item.aliases) && item.aliases.indexOf(slug) !== -1;
|
|
159
|
+
});
|
|
160
|
+
if (aliased) return aliased;
|
|
161
|
+
|
|
162
|
+
var stripped = stripDatedSlugPrefix(slug);
|
|
163
|
+
if (stripped && stripped !== slug) {
|
|
164
|
+
return items.find(function (item) { return item.key === stripped; }) || null;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function adoptCanonicalDetailUrl(entry) {
|
|
170
|
+
if (!entry || !entry.url) return;
|
|
171
|
+
var current = normalizePathname(window.location.pathname);
|
|
172
|
+
var target = normalizePathname(entry.url);
|
|
173
|
+
if (current !== target) {
|
|
174
|
+
history.replaceState(null, '', entry.url);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stripSingleParagraph(html) {
|
|
179
|
+
var value = String(html || '').trim();
|
|
180
|
+
var match = value.match(/^<p(?:\s[^>]*)?>([\s\S]*)<\/p>$/i);
|
|
181
|
+
return match ? match[1].trim() : value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function pageFragmentPath(relativePath) {
|
|
185
|
+
var value = String(relativePath || '').trim().replace(/^\/+/, '').replace(/^pages\//, '');
|
|
186
|
+
if (!value) return '';
|
|
187
|
+
if (!/\.json$/i.test(value)) value = value + '.json';
|
|
188
|
+
return 'pages/' + value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mountMarkdownFragment(relativePath, elementOrId, options) {
|
|
192
|
+
options = options || {};
|
|
193
|
+
var el = typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId;
|
|
194
|
+
if (!el) return Promise.resolve();
|
|
195
|
+
var loadingText = options.loadingText || 'Loading...';
|
|
196
|
+
el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(loadingText) + '</p>';
|
|
197
|
+
return loadJson(pageFragmentPath(relativePath)).then(function (fragment) {
|
|
198
|
+
el.innerHTML = renderMarkdown(fragment.body_markdown || '');
|
|
199
|
+
}).catch(function (error) {
|
|
200
|
+
console.error(error);
|
|
201
|
+
el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(options.errorText || 'Content could not be loaded right now.') + '</p>';
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Page titles and home band introductions both come from the one
|
|
206
|
+
* `content/pages/headers.yml`, exported to pages/headers.json and keyed by
|
|
207
|
+
* page key. loadJson caches by path, so the page header hydrator and the
|
|
208
|
+
* home band hydrator share a single fetch on the home page.
|
|
209
|
+
*/
|
|
210
|
+
var PAGE_HEADERS_PATH = 'pages/headers.json';
|
|
211
|
+
|
|
212
|
+
function loadPageHeaders() {
|
|
213
|
+
return loadJson(PAGE_HEADERS_PATH).then(function (data) {
|
|
214
|
+
return data && typeof data === 'object' ? data : {};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hydratePageHeader(root) {
|
|
219
|
+
if (!root) return Promise.resolve();
|
|
220
|
+
var pageKey = root.getAttribute('data-content-page-header');
|
|
221
|
+
if (!pageKey) return Promise.resolve();
|
|
222
|
+
var titleEl = root.querySelector('[data-content-header-title]');
|
|
223
|
+
var eyebrowEl = root.querySelector('[data-content-header-eyebrow]');
|
|
224
|
+
var tagsEl = root.querySelector('[data-content-header-tags]');
|
|
225
|
+
var standfirstEl = root.querySelector('[data-content-header-standfirst]');
|
|
226
|
+
return loadPageHeaders().then(function (headers) {
|
|
227
|
+
var data = headers[pageKey];
|
|
228
|
+
if (titleEl) {
|
|
229
|
+
var title = data && data.title;
|
|
230
|
+
if (title) {
|
|
231
|
+
titleEl.innerHTML = stripSingleParagraph(parseInlineMarkdown(String(title)));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (eyebrowEl && data && data.eyebrow) {
|
|
235
|
+
eyebrowEl.textContent = data.eyebrow;
|
|
236
|
+
eyebrowEl.hidden = false;
|
|
237
|
+
eyebrowEl.classList.remove('hidden');
|
|
238
|
+
}
|
|
239
|
+
// Tags are field-backed like every other slot: an absent list, an
|
|
240
|
+
// empty one, or one holding nothing but blanks leaves the line hidden
|
|
241
|
+
// rather than printing a row of separators.
|
|
242
|
+
if (tagsEl && data) {
|
|
243
|
+
var tags = (Array.isArray(data.tags) ? data.tags : [])
|
|
244
|
+
.map(function (tag) { return tag == null ? '' : String(tag).trim(); })
|
|
245
|
+
.filter(Boolean);
|
|
246
|
+
if (tags.length) {
|
|
247
|
+
tagsEl.textContent = tags.join('. ');
|
|
248
|
+
tagsEl.hidden = false;
|
|
249
|
+
tagsEl.classList.remove('hidden');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Optional standfirst. Absent on most headers today, so the element
|
|
253
|
+
// stays hidden rather than collapsing to an empty paragraph.
|
|
254
|
+
if (standfirstEl && data && data.standfirst) {
|
|
255
|
+
standfirstEl.innerHTML = stripSingleParagraph(
|
|
256
|
+
parseInlineMarkdown(String(data.standfirst))
|
|
257
|
+
);
|
|
258
|
+
standfirstEl.hidden = false;
|
|
259
|
+
standfirstEl.classList.remove('hidden');
|
|
260
|
+
}
|
|
261
|
+
}).catch(function (error) {
|
|
262
|
+
console.error(error);
|
|
263
|
+
if (titleEl) {
|
|
264
|
+
titleEl.textContent = 'Page header could not be loaded right now.';
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Home band introductions, from content/pages/headers.yml — the same file
|
|
270
|
+
* and the same entries the page headers read, which is why a band title
|
|
271
|
+
* and its listing page's title are never written twice.
|
|
272
|
+
*
|
|
273
|
+
* Each band on the home page declares which entry it reads by page key
|
|
274
|
+
* (`data-content-home-section="articles"`) and marks the three slots it can
|
|
275
|
+
* fill. A slot whose field is absent or empty is taken out of the document
|
|
276
|
+
* rather than left standing empty: an editor who deletes a note gets no
|
|
277
|
+
* note, no gap and no stand-in, which is the same rule the layouts follow
|
|
278
|
+
* for every other field. The same happens when the fetch fails, so a band
|
|
279
|
+
* never keeps a half-filled banner on screen.
|
|
280
|
+
*
|
|
281
|
+
* Titles carry the `*italic*` convention the page headers use, so they go
|
|
282
|
+
* through the same parseInlineMarkdown/stripSingleParagraph pair as
|
|
283
|
+
* hydratePageHeader's title rather than a second markup rule.
|
|
284
|
+
*/
|
|
285
|
+
function fillHomeSectionSlot(el, value, inline) {
|
|
286
|
+
if (!el) return;
|
|
287
|
+
var text = value == null ? '' : String(value).trim();
|
|
288
|
+
if (!text) {
|
|
289
|
+
el.remove();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (inline) {
|
|
293
|
+
el.innerHTML = stripSingleParagraph(parseInlineMarkdown(text));
|
|
294
|
+
} else {
|
|
295
|
+
el.textContent = text;
|
|
296
|
+
}
|
|
297
|
+
el.hidden = false;
|
|
298
|
+
el.classList.remove('hidden');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function applyHomeSection(root, section) {
|
|
302
|
+
section = section || {};
|
|
303
|
+
fillHomeSectionSlot(root.querySelector('[data-content-section-kicker]'), section.kicker, false);
|
|
304
|
+
fillHomeSectionSlot(root.querySelector('[data-content-section-title]'), section.title, true);
|
|
305
|
+
fillHomeSectionSlot(root.querySelector('[data-content-section-note]'), section.note, true);
|
|
306
|
+
/* Page accent from headers.yml (`accent: green|ochre`). Applied on the
|
|
307
|
+
* home band so the section title marker, CTAs and chips follow the same
|
|
308
|
+
* hue as the listing page. Blue is the site default — no class. */
|
|
309
|
+
var accent = String(section.accent || '').trim().toLowerCase();
|
|
310
|
+
var band = root.closest('section') || root;
|
|
311
|
+
['green', 'ochre'].forEach(function (name) {
|
|
312
|
+
band.classList.toggle('page-accent--' + name, accent === name);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function hydrateHomeSections() {
|
|
317
|
+
var roots = Array.prototype.slice.call(
|
|
318
|
+
document.querySelectorAll('[data-content-home-section]')
|
|
319
|
+
);
|
|
320
|
+
if (!roots.length) return Promise.resolve();
|
|
321
|
+
return loadPageHeaders().then(function (sections) {
|
|
322
|
+
roots.forEach(function (root) {
|
|
323
|
+
applyHomeSection(root, sections[root.getAttribute('data-content-home-section')]);
|
|
324
|
+
});
|
|
325
|
+
}).catch(function (error) {
|
|
326
|
+
console.error(error);
|
|
327
|
+
roots.forEach(function (root) { applyHomeSection(root, null); });
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* The home page's reference band opens each panel with a plate drawn from
|
|
332
|
+
* that section's own content by the export (docs/features/home-band-art.md).
|
|
333
|
+
* The markup arrives as an SVG document and is injected inline, never as an
|
|
334
|
+
* <img>: every stroke and fill in it is a custom property, and an external
|
|
335
|
+
* document cannot see the palette the page is currently wearing.
|
|
336
|
+
*
|
|
337
|
+
* The plate's caption is counted, not written, so it travels on the root
|
|
338
|
+
* element as data attributes and is lifted back out into HTML here. Text
|
|
339
|
+
* left inside the viewBox would scale with the panel; the label register
|
|
340
|
+
* has one size and keeps it.
|
|
341
|
+
*/
|
|
342
|
+
function bandArtPath(name) {
|
|
343
|
+
return 'data/band-art-' + String(name || '').replace(/[^a-z0-9-]/gi, '') + '.svg';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function renderBandArtCaption(svg) {
|
|
347
|
+
var legend = (svg.getAttribute('data-band-legend') || '').split(',').filter(Boolean);
|
|
348
|
+
var tally = svg.getAttribute('data-band-tally') || '';
|
|
349
|
+
if (!legend.length && !tally) return '';
|
|
350
|
+
var keys = legend.map(function (entry) {
|
|
351
|
+
var parts = entry.split(':');
|
|
352
|
+
return '<span class="home-index-key"><span class="home-index-swatch home-index-swatch--' +
|
|
353
|
+
escapeAttr(parts[0]) + '" aria-hidden="true"></span>' + escapeHtml(parts[1] || '') + '</span>';
|
|
354
|
+
}).join('');
|
|
355
|
+
return '<figcaption class="home-index-plate-head">' +
|
|
356
|
+
'<span class="home-index-legend">' + keys + '</span>' +
|
|
357
|
+
'<span class="home-index-tally">' + escapeHtml(tally) + '</span>' +
|
|
358
|
+
'</figcaption>';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function mountBandArt(el) {
|
|
362
|
+
var name = el.getAttribute('data-content-band-art');
|
|
363
|
+
if (!name) return Promise.resolve();
|
|
364
|
+
return loadText(bandArtPath(name)).then(function (markup) {
|
|
365
|
+
el.innerHTML = markup;
|
|
366
|
+
var svg = el.querySelector('svg');
|
|
367
|
+
if (!svg) return;
|
|
368
|
+
el.insertAdjacentHTML('afterbegin', renderBandArtCaption(svg));
|
|
369
|
+
el.hidden = false;
|
|
370
|
+
}).catch(function (error) {
|
|
371
|
+
console.error(error);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function hydrateBandArt() {
|
|
376
|
+
return Promise.all(
|
|
377
|
+
Array.prototype.slice.call(document.querySelectorAll('[data-content-band-art]')).map(mountBandArt)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
var contactsCache = null;
|
|
382
|
+
var contactsPromise = null;
|
|
383
|
+
|
|
384
|
+
function ensureContacts() {
|
|
385
|
+
if (contactsCache) return Promise.resolve(contactsCache);
|
|
386
|
+
if (!contactsPromise) {
|
|
387
|
+
contactsPromise = loadJson('data/contacts.json').then(function (data) {
|
|
388
|
+
contactsCache = Array.isArray(data) ? data : [];
|
|
389
|
+
return contactsCache;
|
|
390
|
+
}).catch(function (error) {
|
|
391
|
+
console.error(error);
|
|
392
|
+
contactsPromise = null;
|
|
393
|
+
contactsCache = [];
|
|
394
|
+
return contactsCache;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return contactsPromise;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderProfessionalConnection(contact) {
|
|
401
|
+
var href = contact.href || '#';
|
|
402
|
+
var isMailto = href.indexOf('mailto:') === 0;
|
|
403
|
+
var targetRel = isMailto ? '' : ' target="_blank" rel="noopener"';
|
|
404
|
+
return [
|
|
405
|
+
'<a href="', escapeAttr(href), '"', targetRel,
|
|
406
|
+
' class="group flex h-full items-center gap-3 no-underline">',
|
|
407
|
+
'<div class="w-12 h-12 rounded-[9999px] bg-surface-container-highest flex items-center justify-center group-hover:bg-primary-container transition-colors">',
|
|
408
|
+
'<span class="material-symbols-outlined text-on-surface group-hover:text-on-primary-container">',
|
|
409
|
+
escapeHtml(contact.icon || ''),
|
|
410
|
+
'</span></div><div>',
|
|
411
|
+
'<span class="block marginalia text-tertiary">',
|
|
412
|
+
escapeHtml(contact.badge || ''),
|
|
413
|
+
'</span><span class="font-label text-on-surface">',
|
|
414
|
+
escapeHtml(contact.name || ''),
|
|
415
|
+
'</span></div></a>'
|
|
416
|
+
].join('');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function renderHeroContact(contact) {
|
|
420
|
+
var href = contact.href || '#';
|
|
421
|
+
var isMailto = href.indexOf('mailto:') === 0;
|
|
422
|
+
var targetRel = isMailto ? '' : ' target="_blank" rel="noopener"';
|
|
423
|
+
return [
|
|
424
|
+
'<a href="', escapeAttr(href), '"', targetRel,
|
|
425
|
+
' class="ledger-cta ledger-cta--emphasis ledger-cta--emphasis-alt home-hero-cta">',
|
|
426
|
+
'<span class="material-symbols-outlined ledger-cta-icon" aria-hidden="true">',
|
|
427
|
+
escapeHtml(contact.icon || ''),
|
|
428
|
+
'</span><span>', escapeHtml(contact.name || ''), '</span></a>'
|
|
429
|
+
].join('');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function renderFooterContact(contact) {
|
|
433
|
+
var href = contact.href || '#';
|
|
434
|
+
var isMailto = href.indexOf('mailto:') === 0;
|
|
435
|
+
var targetRel = isMailto ? '' : ' target="_blank" rel="noopener"';
|
|
436
|
+
return [
|
|
437
|
+
'<a href="', escapeAttr(href), '"', targetRel,
|
|
438
|
+
' class="marginalia text-on-surface-variant hover:text-primary transition-all duration-300">',
|
|
439
|
+
escapeHtml(contact.name || ''),
|
|
440
|
+
'</a>'
|
|
441
|
+
].join('');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function hydrateContacts() {
|
|
445
|
+
var grids = document.querySelectorAll('[data-content-contacts]');
|
|
446
|
+
var footers = document.querySelectorAll('[data-content-footer-contacts]');
|
|
447
|
+
var heroSlots = document.querySelectorAll('[data-content-hero-contacts]');
|
|
448
|
+
if (!grids.length && !footers.length && !heroSlots.length) return Promise.resolve();
|
|
449
|
+
return ensureContacts().then(function (contacts) {
|
|
450
|
+
grids.forEach(function (el) {
|
|
451
|
+
el.innerHTML = contacts.map(renderProfessionalConnection).join('');
|
|
452
|
+
});
|
|
453
|
+
footers.forEach(function (el) {
|
|
454
|
+
el.innerHTML = contacts.map(renderFooterContact).join('');
|
|
455
|
+
});
|
|
456
|
+
heroSlots.forEach(function (el) {
|
|
457
|
+
el.outerHTML = contacts.map(renderHeroContact).join('');
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Home hero plate. pages/home/portraits.json lists every image in
|
|
463
|
+
// content/media/images/hero/. One entry is chosen per document load.
|
|
464
|
+
// A file that fails to load is skipped. The plate stays black until one
|
|
465
|
+
// pool file has loaded, and stays black when every entry fails.
|
|
466
|
+
function heroPortraitEntries(data) {
|
|
467
|
+
var list = Array.isArray(data) ? data : [];
|
|
468
|
+
var entries = [];
|
|
469
|
+
var i;
|
|
470
|
+
for (i = 0; i < list.length; i++) {
|
|
471
|
+
var entry = list[i];
|
|
472
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
473
|
+
var src = typeof entry.src === 'string' ? entry.src.trim() : '';
|
|
474
|
+
if (!src) continue;
|
|
475
|
+
var alt = typeof entry.alt === 'string' ? entry.alt.trim() : '';
|
|
476
|
+
entries.push({ src: src, alt: alt });
|
|
477
|
+
}
|
|
478
|
+
return entries;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function pickHeroPortrait(entries, random) {
|
|
482
|
+
if (!entries || !entries.length) return null;
|
|
483
|
+
var roll = typeof random === 'function' ? random() : Math.random();
|
|
484
|
+
if (typeof roll !== 'number' || !(roll >= 0) || roll >= 1) return entries[0];
|
|
485
|
+
return entries[Math.floor(roll * entries.length)];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function applyHeroPortrait(img, chosen) {
|
|
489
|
+
var nextSrc = resolveAssetUrl(chosen.src);
|
|
490
|
+
var nextAlt = chosen.alt || '';
|
|
491
|
+
if (!nextSrc) return Promise.resolve(false);
|
|
492
|
+
if (img.getAttribute('src') === nextSrc) {
|
|
493
|
+
img.alt = nextAlt;
|
|
494
|
+
img.classList.add('is-shown');
|
|
495
|
+
return Promise.resolve(true);
|
|
496
|
+
}
|
|
497
|
+
return new Promise(function (resolve) {
|
|
498
|
+
var probe = new Image();
|
|
499
|
+
probe.onload = function () {
|
|
500
|
+
img.src = nextSrc;
|
|
501
|
+
img.alt = nextAlt;
|
|
502
|
+
img.classList.add('is-shown');
|
|
503
|
+
resolve(true);
|
|
504
|
+
};
|
|
505
|
+
probe.onerror = function () {
|
|
506
|
+
resolve(false);
|
|
507
|
+
};
|
|
508
|
+
probe.src = nextSrc;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function showHeroPortrait(img, entries) {
|
|
513
|
+
var chosen = pickHeroPortrait(entries, Math.random);
|
|
514
|
+
if (!chosen) return Promise.resolve();
|
|
515
|
+
var start = entries.indexOf(chosen);
|
|
516
|
+
var ordered = entries.slice(start).concat(entries.slice(0, start));
|
|
517
|
+
var index = 0;
|
|
518
|
+
function next() {
|
|
519
|
+
if (index >= ordered.length) return Promise.resolve();
|
|
520
|
+
var entry = ordered[index];
|
|
521
|
+
index += 1;
|
|
522
|
+
return applyHeroPortrait(img, entry).then(function (ok) {
|
|
523
|
+
if (ok) return;
|
|
524
|
+
return next();
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return next();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function hydrateHeroPortrait() {
|
|
531
|
+
var img = document.querySelector('[data-content-hero-portrait]');
|
|
532
|
+
if (!img) return Promise.resolve();
|
|
533
|
+
return loadJson('pages/home/portraits.json').then(function (data) {
|
|
534
|
+
return showHeroPortrait(img, heroPortraitEntries(data));
|
|
535
|
+
}).catch(function (error) {
|
|
536
|
+
console.error(error);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function hydratePageFragments() {
|
|
541
|
+
document.querySelectorAll('[data-content-page-header]').forEach(function (el) {
|
|
542
|
+
hydratePageHeader(el);
|
|
543
|
+
});
|
|
544
|
+
document.querySelectorAll('[data-content-fragment]').forEach(function (el) {
|
|
545
|
+
var path = el.getAttribute('data-content-fragment');
|
|
546
|
+
if (!path) return;
|
|
547
|
+
mountMarkdownFragment(path, el, {
|
|
548
|
+
loadingText: el.getAttribute('data-content-loading') || 'Loading...',
|
|
549
|
+
errorText: el.getAttribute('data-content-error') || 'Content could not be loaded right now.'
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
hydrateHomeSections();
|
|
553
|
+
hydrateBandArt();
|
|
554
|
+
hydrateContacts();
|
|
555
|
+
hydrateHeroPortrait();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function schedulePageFragmentHydration() {
|
|
559
|
+
if (document.readyState === 'loading') {
|
|
560
|
+
document.addEventListener('DOMContentLoaded', hydratePageFragments);
|
|
561
|
+
} else {
|
|
562
|
+
hydratePageFragments();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
schedulePageFragmentHydration();
|
|
567
|
+
{% endraw %}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{% raw %}
|
|
2
|
+
|
|
3
|
+
return {
|
|
4
|
+
articleUrl: articleUrl,
|
|
5
|
+
buildUrl: buildUrl,
|
|
6
|
+
contentBase: contentBase,
|
|
7
|
+
contentSlug: contentSlug,
|
|
8
|
+
detailUrl: detailUrl,
|
|
9
|
+
slugFromDetailPattern: slugFromDetailPattern,
|
|
10
|
+
postUrl: postUrl,
|
|
11
|
+
resourceUrl: resourceUrl,
|
|
12
|
+
escapeHtml: escapeHtml,
|
|
13
|
+
escapeAttr: escapeAttr,
|
|
14
|
+
leadSentence: leadSentence,
|
|
15
|
+
chronologySortKey: chronologySortKey,
|
|
16
|
+
formatDate: formatDate,
|
|
17
|
+
renderDateEvolution: renderDateEvolution,
|
|
18
|
+
renderCardMetaBlock: renderCardMetaBlock,
|
|
19
|
+
renderCardFooterDates: renderCardFooterDates,
|
|
20
|
+
loadCollection: loadCollection,
|
|
21
|
+
loadCollectionItem: loadCollectionItem,
|
|
22
|
+
loadJson: loadJson,
|
|
23
|
+
loadWritingIndex: loadWritingIndex,
|
|
24
|
+
writingIndexAsListingItem: writingIndexAsListingItem,
|
|
25
|
+
resolveWritingAlias: resolveWritingAlias,
|
|
26
|
+
adoptCanonicalDetailUrl: adoptCanonicalDetailUrl,
|
|
27
|
+
productUrl: productUrl,
|
|
28
|
+
projectUrl: projectUrl,
|
|
29
|
+
hydratePageHeader: hydratePageHeader,
|
|
30
|
+
hydratePageFragments: hydratePageFragments,
|
|
31
|
+
hydrateContacts: hydrateContacts,
|
|
32
|
+
mountMarkdownFragment: mountMarkdownFragment,
|
|
33
|
+
pageFragmentPath: pageFragmentPath,
|
|
34
|
+
placeholderHtml: placeholderHtml,
|
|
35
|
+
renderRichContent: renderRichContent,
|
|
36
|
+
decorateCodeBlocks: decorateCodeBlocks,
|
|
37
|
+
renderAnsiHtml: renderAnsiHtml,
|
|
38
|
+
enhanceArticleContent: enhanceArticleContent,
|
|
39
|
+
insertRuntimeBackLink: insertRuntimeBackLink,
|
|
40
|
+
removeRuntimeBackLink: removeRuntimeBackLink,
|
|
41
|
+
renderRuntimeDetailSection: renderRuntimeDetailSection,
|
|
42
|
+
reportDetailLoadError: reportDetailLoadError,
|
|
43
|
+
buildDetailActions: buildDetailActions,
|
|
44
|
+
buildTextSidebar: buildTextSidebar,
|
|
45
|
+
buildTextBackLink: buildTextBackLink,
|
|
46
|
+
buildTextFooterNav: buildTextFooterNav,
|
|
47
|
+
assignHeadingIds: assignHeadingIds,
|
|
48
|
+
buildTocEntries: buildTocEntries,
|
|
49
|
+
mountArticleToc: mountArticleToc,
|
|
50
|
+
removeArticleToc: removeArticleToc,
|
|
51
|
+
resolveTocLang: resolveTocLang,
|
|
52
|
+
translate: translate,
|
|
53
|
+
typesetMath: typesetMath,
|
|
54
|
+
renderMarkdown: renderMarkdown,
|
|
55
|
+
textExcerpt: textExcerpt,
|
|
56
|
+
listingPreview: listingPreview,
|
|
57
|
+
renderLangPill: renderLangPill,
|
|
58
|
+
pickFeatured: pickFeatured,
|
|
59
|
+
renderTagChips: renderTagChips,
|
|
60
|
+
renderMetaStrip: renderMetaStrip,
|
|
61
|
+
renderCtaLink: renderCtaLink,
|
|
62
|
+
renderSectionBanner: renderSectionBanner,
|
|
63
|
+
renderLedgerRow: renderLedgerRow,
|
|
64
|
+
renderArchiveCard: renderArchiveCard,
|
|
65
|
+
renderSystemCard: renderSystemCard,
|
|
66
|
+
renderPosterMedia: renderPosterMedia,
|
|
67
|
+
ensureBibliographyIndex: ensureBibliographyIndex,
|
|
68
|
+
bibField: bibField,
|
|
69
|
+
formatIeeeEntry: formatIeeeEntry,
|
|
70
|
+
formatIeeeAuthors: formatIeeeAuthors,
|
|
71
|
+
getSessionRandomSeed: getSessionRandomSeed,
|
|
72
|
+
sessionRandomCompare: sessionRandomCompare,
|
|
73
|
+
Listing: Listing,
|
|
74
|
+
ListingFilter: ListingFilter
|
|
75
|
+
};
|
|
76
|
+
{% endraw %}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Typographic cut-in: a 3×4 portrait floated into the column so the following
|
|
3
|
+
paragraph wraps around it (six body line boxes tall). See docs/content/elements.md#cut-in-portraits.
|
|
4
|
+
|
|
5
|
+
{% include cut-in.html src="https://…" alt="Jean Lucas" %}
|
|
6
|
+
{% include cut-in.html src="/media/images/foo.jpg" alt="…" side="right" %}
|
|
7
|
+
{% endcomment %}
|
|
8
|
+
{% if include.src %}
|
|
9
|
+
<figure class="article-cut-in-plate{% if include.side == 'right' %} article-cut-in--right{% endif %}"><img class="article-cut-in{% if include.side == 'right' %} article-cut-in--right{% endif %}" src="{{ include.src }}" alt="{{ include.alt | default: '' }}" width="120" loading="lazy"></figure>
|
|
10
|
+
{% endif %}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
CV register section: a title rail that sticks beside the entry column while
|
|
3
|
+
the entries scroll past it. Every section of the CV page is the same shape,
|
|
4
|
+
so the shell lives here rather than being retyped five times.
|
|
5
|
+
|
|
6
|
+
Parameters:
|
|
7
|
+
title — section heading (required)
|
|
8
|
+
content — pre-captured section body (required)
|
|
9
|
+
id — anchor id, e.g. "products" for the product back-link
|
|
10
|
+
body_class — extra classes on the entry column
|
|
11
|
+
{%- endcomment -%}
|
|
12
|
+
<section class="cv-register"{% if include.id %} id="{{ include.id }}"{% endif %}>
|
|
13
|
+
<div class="cv-register-rail">
|
|
14
|
+
<h2 class="section-title cv-register-title">{{ include.title }}</h2>
|
|
15
|
+
<span class="cv-register-rule" aria-hidden="true"></span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="cv-register-body{% if include.body_class %} {{ include.body_class }}{% endif %}">{{ include.content }}</div>
|
|
18
|
+
</section>
|