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.
Files changed (376) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +77 -0
  3. data/DESIGN.md +1434 -0
  4. data/LICENSE +21 -0
  5. data/README.md +81 -0
  6. data/_data/themes/dark.yml +97 -0
  7. data/_data/themes/light.yml +120 -0
  8. data/_data/themes/shared.yml +37 -0
  9. data/_data/translations.yml +85 -0
  10. data/_includes/chord-tab.html +38 -0
  11. data/_includes/content-runtime/00-config.html +22 -0
  12. data/_includes/content-runtime/10-utils.html +176 -0
  13. data/_includes/content-runtime/20-markdown.html +264 -0
  14. data/_includes/content-runtime/25-code-blocks.html +315 -0
  15. data/_includes/content-runtime/30-includes-bib.html +190 -0
  16. data/_includes/content-runtime/30-includes-xcite.html +122 -0
  17. data/_includes/content-runtime/30-includes.html +365 -0
  18. data/_includes/content-runtime/40-listing.html +225 -0
  19. data/_includes/content-runtime/41-listing-helpers.html +71 -0
  20. data/_includes/content-runtime/42-listing-filter.html +208 -0
  21. data/_includes/content-runtime/43-blocks.html +379 -0
  22. data/_includes/content-runtime/50-detail.html +861 -0
  23. data/_includes/content-runtime/55-toc.html +176 -0
  24. data/_includes/content-runtime/60-fragments.html +567 -0
  25. data/_includes/content-runtime/90-bootstrap.html +76 -0
  26. data/_includes/cut-in.html +10 -0
  27. data/_includes/cv/cv-box.html +18 -0
  28. data/_includes/dict-card.html +24 -0
  29. data/_includes/figure.html +36 -0
  30. data/_includes/home/band-index.html +43 -0
  31. data/_includes/home/band-listing.html +49 -0
  32. data/_includes/mathjax.html +10 -0
  33. data/_includes/ongoing-product.html +1 -0
  34. data/_includes/page/content-runtime.html +20 -0
  35. data/_includes/page/date_evolution.html +19 -0
  36. data/_includes/page/figure-lightbox.html +102 -0
  37. data/_includes/page/formatted_date.html +15 -0
  38. data/_includes/page/hero-default.html +18 -0
  39. data/_includes/page/listing-runtime-attrs.html +8 -0
  40. data/_includes/page/mermaid.html +101 -0
  41. data/_includes/page/network-graph.html +1253 -0
  42. data/_includes/page/page-header.html +41 -0
  43. data/_includes/page/professional-connections.html +2 -0
  44. data/_includes/page/site-footer.html +101 -0
  45. data/_includes/page/timeline-section.html +868 -0
  46. data/_includes/plotly.html +22 -0
  47. data/_includes/theme/dev-reexport.html +100 -0
  48. data/_includes/theme/favicons.html +4 -0
  49. data/_includes/theme/font-loader.html +55 -0
  50. data/_includes/theme/shell-theme-boot.html +115 -0
  51. data/_includes/theme/site-logo.html +6 -0
  52. data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
  53. data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
  54. data/_includes/theme/theme-context.liquid +20 -0
  55. data/_includes/theme/theme-toggle.html +94 -0
  56. data/_includes/theme/theme-vars-block.liquid +161 -0
  57. data/_includes/theme/theme-vars.html +16 -0
  58. data/_includes/transcription.html +80 -0
  59. data/_includes/tweet.html +22 -0
  60. data/_includes/video.html +11 -0
  61. data/_includes/xcite.html +3 -0
  62. data/_includes/youtube-short.html +26 -0
  63. data/_includes/youtube.html +12 -0
  64. data/_layouts/article.html +27 -0
  65. data/_layouts/articles.html +534 -0
  66. data/_layouts/bibliography.html +417 -0
  67. data/_layouts/blog.html +544 -0
  68. data/_layouts/cv.html +393 -0
  69. data/_layouts/home.html +319 -0
  70. data/_layouts/network.html +9 -0
  71. data/_layouts/pandorga/articles.html +4 -0
  72. data/_layouts/pandorga/bibliography.html +4 -0
  73. data/_layouts/pandorga/blog.html +4 -0
  74. data/_layouts/pandorga/cv.html +4 -0
  75. data/_layouts/pandorga/media.html +4 -0
  76. data/_layouts/pandorga/network.html +4 -0
  77. data/_layouts/pandorga/portfolio.html +4 -0
  78. data/_layouts/post.html +7 -0
  79. data/_layouts/product.html +5 -0
  80. data/_layouts/project.html +27 -0
  81. data/_layouts/projects.html +534 -0
  82. data/_layouts/resources.html +387 -0
  83. data/_layouts/shell.html +203 -0
  84. data/_layouts/text.html +246 -0
  85. data/_plugins/bibliography_artifact.rb +57 -0
  86. data/_plugins/content_backend.rb +37 -0
  87. data/_plugins/content_collections.rb +51 -0
  88. data/_plugins/content_detail_paths.rb +58 -0
  89. data/_plugins/content_headers.rb +73 -0
  90. data/_plugins/content_keys.rb +310 -0
  91. data/_plugins/content_local_serve.rb +168 -0
  92. data/_plugins/content_site_adapter.rb +159 -0
  93. data/_plugins/content_validators_hook.rb +20 -0
  94. data/_plugins/content_writing_index.rb +52 -0
  95. data/_plugins/content_writing_paths.rb +55 -0
  96. data/_plugins/force_utf8_encoding.rb +13 -0
  97. data/_plugins/git_chronology.rb +211 -0
  98. data/_plugins/git_metadata.rb +43 -0
  99. data/_plugins/job_sections.rb +25 -0
  100. data/_plugins/lib/content_validators.rb +27 -0
  101. data/_plugins/lib/resource_validator.rb +56 -0
  102. data/_plugins/lib/tag_validator.rb +153 -0
  103. data/_plugins/lib/text_excerpt.rb +56 -0
  104. data/_plugins/lib/writing_entry.rb +107 -0
  105. data/_plugins/local_detail_rewrites.rb +194 -0
  106. data/_plugins/timeline.rb +507 -0
  107. data/_plugins/writing_slug_limits.rb +70 -0
  108. data/assets/css/article.css +1330 -0
  109. data/assets/css/base.css +1432 -0
  110. data/assets/css/cv.css +13 -0
  111. data/assets/css/global-link.css +94 -0
  112. data/assets/css/ledger/bibliography.css +307 -0
  113. data/assets/css/ledger/chrome.css +118 -0
  114. data/assets/css/ledger/cv.css +836 -0
  115. data/assets/css/ledger/detail.css +1270 -0
  116. data/assets/css/ledger/footer.css +298 -0
  117. data/assets/css/ledger/home.css +1490 -0
  118. data/assets/css/ledger/network.css +106 -0
  119. data/assets/css/ledger/projects.css +716 -0
  120. data/assets/css/ledger/rascunhos.css +379 -0
  121. data/assets/css/ledger/sources.css +327 -0
  122. data/assets/css/ledger/writing.css +754 -0
  123. data/assets/tailwind-play.js +15 -0
  124. data/examples/full/Gemfile +7 -0
  125. data/examples/full/_config.yml +92 -0
  126. data/examples/full/_redirects +3 -0
  127. data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
  128. data/examples/full/content/collections/data/contacts.yml +3 -0
  129. data/examples/full/content/collections/data/education.yml +4 -0
  130. data/examples/full/content/collections/jobs/research-fellow.md +12 -0
  131. data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
  132. data/examples/full/content/collections/products/ledger-tool.md +6 -0
  133. data/examples/full/content/collections/projects/demo-kit.md +10 -0
  134. data/examples/full/content/collections/resources/style-guide.md +7 -0
  135. data/examples/full/content/pages/cv/summary.md +1 -0
  136. data/examples/full/content/pages/headers.yml +20 -0
  137. data/examples/full/index.html +4 -0
  138. data/examples/minimal/Gemfile +7 -0
  139. data/examples/minimal/_config.yml +47 -0
  140. data/examples/minimal/_redirects +3 -0
  141. data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
  142. data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
  143. data/examples/minimal/content/pages/headers.yml +6 -0
  144. data/examples/minimal/index.html +4 -0
  145. data/exe/pandorga +7 -0
  146. data/functions/api/studio/[[path]].js +562 -0
  147. data/functions/api/studio/_lib/auth.js +131 -0
  148. data/functions/api/studio/_lib/blocks.js +156 -0
  149. data/functions/api/studio/_lib/gemini.js +62 -0
  150. data/functions/api/studio/_lib/github.js +278 -0
  151. data/functions/api/studio/_lib/guidelines.js +143 -0
  152. data/functions/api/studio/_lib/paths.js +76 -0
  153. data/functions/api/studio/_lib/prompts.js +165 -0
  154. data/functions/api/studio/_lib/rate-limit.js +40 -0
  155. data/functions/api/studio/_lib/rewrite.js +136 -0
  156. data/functions/api/studio/_lib/sanitize.js +143 -0
  157. data/lib/pandorga/cli.rb +66 -0
  158. data/lib/pandorga/commands/doctor.rb +66 -0
  159. data/lib/pandorga/commands/export.rb +30 -0
  160. data/lib/pandorga/commands/install_functions.rb +41 -0
  161. data/lib/pandorga/commands/new.rb +113 -0
  162. data/lib/pandorga/commands/publish.rb +53 -0
  163. data/lib/pandorga/commands/serve.rb +34 -0
  164. data/lib/pandorga/commands/studio_schema.rb +309 -0
  165. data/lib/pandorga/jekyll/generator.rb +74 -0
  166. data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
  167. data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
  168. data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
  169. data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
  170. data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
  171. data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
  172. data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
  173. data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
  174. data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
  175. data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
  176. data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
  177. data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
  178. data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
  179. data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
  180. data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
  181. data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
  182. data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
  183. data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
  184. data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
  185. data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
  186. data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
  187. data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
  188. data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
  189. data/lib/pandorga/jekyll.rb +15 -0
  190. data/lib/pandorga/object_store_env.rb +51 -0
  191. data/lib/pandorga/registry/schema.yml +115 -0
  192. data/lib/pandorga/registry.rb +279 -0
  193. data/lib/pandorga/version.rb +5 -0
  194. data/lib/pandorga.rb +19 -0
  195. data/scripts/cdn-allowlist.json +19 -0
  196. data/scripts/content/backfill-content-keys.rb +99 -0
  197. data/scripts/content/band_art.rb +362 -0
  198. data/scripts/content/export-content-json.rb +741 -0
  199. data/scripts/content/flatten-ansi-capture.mjs +153 -0
  200. data/scripts/content/hero_portraits.rb +28 -0
  201. data/scripts/content/publish-content-to-object-store.sh +78 -0
  202. data/scripts/content/publish-content-to-r2.sh +5 -0
  203. data/scripts/content/sync-content-json-to-r2.rb +167 -0
  204. data/scripts/content/sync-content-redirects.rb +12 -0
  205. data/scripts/generate-configuration-doc.rb +127 -0
  206. data/scripts/lib/content_license.rb +50 -0
  207. data/scripts/lib/content_storage_config.rb +70 -0
  208. data/scripts/lib/normalize-display-math.mjs +52 -0
  209. data/scripts/lib/r2_json_sync.rb +76 -0
  210. data/scripts/lib/withdrawn_public_objects.rb +23 -0
  211. data/scripts/lib/writing_index_contract.rb +61 -0
  212. data/scripts/pdf/md-to-pdf.mjs +1400 -0
  213. data/scripts/pdf/md-to-pdf.sh +27 -0
  214. data/scripts/test/fixtures/dual-template/_config.yml +32 -0
  215. data/scripts/test/fixtures/dual-template/index.html +4 -0
  216. data/scripts/test/render-parity/article-note.json +5 -0
  217. data/scripts/test/render-parity/blockquote.json +5 -0
  218. data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
  219. data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
  220. data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
  221. data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
  222. data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
  223. data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
  224. data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
  225. data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
  226. data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
  227. data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
  228. data/scripts/test/render-parity/blocks/archive-card.json +32 -0
  229. data/scripts/test/render-parity/blocks/cta-link.json +13 -0
  230. data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
  231. data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
  232. data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
  233. data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
  234. data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
  235. data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
  236. data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
  237. data/scripts/test/render-parity/blocks/section-banner.json +13 -0
  238. data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
  239. data/scripts/test/render-parity/blocks/system-card.json +48 -0
  240. data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
  241. data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
  242. data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
  243. data/scripts/test/render-parity/cut-in-right.json +9 -0
  244. data/scripts/test/render-parity/cut-in.json +8 -0
  245. data/scripts/test/render-parity/figure.json +5 -0
  246. data/scripts/test/render-parity/load-runtime.mjs +337 -0
  247. data/scripts/test/render-parity/ongoing-product.json +5 -0
  248. data/scripts/test/render-parity/plotly.json +5 -0
  249. data/scripts/test/render-parity/poem.json +5 -0
  250. data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
  251. data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
  252. data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
  253. data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
  254. data/scripts/test/render-parity/run-fixture.mjs +159 -0
  255. data/scripts/test/render-parity/selectors.mjs +43 -0
  256. data/scripts/test/render-parity/video.json +5 -0
  257. data/scripts/test/render-parity/youtube-short.json +10 -0
  258. data/scripts/test/render-parity/youtube.json +8 -0
  259. data/scripts/test/test-absent-field-absent-element.rb +43 -0
  260. data/scripts/test/test-cdn-pins.rb +44 -0
  261. data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
  262. data/scripts/test/test-configuration-doc.rb +36 -0
  263. data/scripts/test/test-content-detail-urls.rb +346 -0
  264. data/scripts/test/test-content-keys.rb +110 -0
  265. data/scripts/test/test-content-license.rb +45 -0
  266. data/scripts/test/test-content-render-parity.rb +38 -0
  267. data/scripts/test/test-content-validators.rb +12 -0
  268. data/scripts/test/test-css-structure.rb +72 -0
  269. data/scripts/test/test-cv-export-contract.rb +158 -0
  270. data/scripts/test/test-detail-render-parity.rb +40 -0
  271. data/scripts/test/test-dev-reexport.rb +122 -0
  272. data/scripts/test/test-doctor.rb +39 -0
  273. data/scripts/test/test-dual-template-instances.rb +87 -0
  274. data/scripts/test/test-dual-template-runtime.rb +167 -0
  275. data/scripts/test/test-examples-build.rb +60 -0
  276. data/scripts/test/test-export-content-json-incremental.rb +138 -0
  277. data/scripts/test/test-export.rb +53 -0
  278. data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
  279. data/scripts/test/test-form-control-theming.rb +143 -0
  280. data/scripts/test/test-git-chronology.rb +414 -0
  281. data/scripts/test/test-hero-portraits.mjs +97 -0
  282. data/scripts/test/test-hero-portraits.rb +162 -0
  283. data/scripts/test/test-home-band-art.rb +135 -0
  284. data/scripts/test/test-install-functions.rb +39 -0
  285. data/scripts/test/test-language-export.rb +79 -0
  286. data/scripts/test/test-listing-filter-contract.rb +119 -0
  287. data/scripts/test/test-math-row-breaks.mjs +79 -0
  288. data/scripts/test/test-math-row-breaks.rb +18 -0
  289. data/scripts/test/test-new.rb +113 -0
  290. data/scripts/test/test-no-personal-data.rb +85 -0
  291. data/scripts/test/test-public-cv-export.rb +164 -0
  292. data/scripts/test/test-r2-config-consistency.rb +57 -0
  293. data/scripts/test/test-r2-json-sync.rb +193 -0
  294. data/scripts/test/test-registry-home.rb +34 -0
  295. data/scripts/test/test-runtime-js-syntax.rb +64 -0
  296. data/scripts/test/test-slim-writing-index.rb +126 -0
  297. data/scripts/test/test-studio-api-contract.rb +99 -0
  298. data/scripts/test/test-studio-content-files.rb +79 -0
  299. data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
  300. data/scripts/test/test-studio-rate-limit.mjs +70 -0
  301. data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
  302. data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
  303. data/scripts/test/test-studio-schema-composition.rb +126 -0
  304. data/scripts/test/test-studio-slug-config.rb +167 -0
  305. data/scripts/test/test-template-packages.rb +51 -0
  306. data/scripts/test/test-text-excerpt.rb +51 -0
  307. data/scripts/test/test-unwrap-media-src.mjs +39 -0
  308. data/scripts/test/test-unwrap-media-src.rb +19 -0
  309. data/scripts/test/test-withdrawn-content-export.rb +69 -0
  310. data/scripts/test/test-writing-index-export-slugs.rb +84 -0
  311. data/scripts/test/test-writing-slug-limits.rb +61 -0
  312. data/scripts/test/test-xcite-chronology.rb +106 -0
  313. data/scripts/validate.sh +74 -0
  314. data/scripts/visual/build-cv-pages-json.js +33 -0
  315. data/scripts/visual/build-site-pages-json.js +94 -0
  316. data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
  317. data/scripts/visual/take-screenshots.js +88 -0
  318. data/scripts/visual/visual-inspection-pages.json +113 -0
  319. data/studio/assets/index-B_j0pMD5.css +1 -0
  320. data/studio/assets/index-Ch4YEv9g.js +285 -0
  321. data/studio/index.html +27 -0
  322. data/studio/schema.yml +869 -0
  323. data/studio-app/README.md +27 -0
  324. data/studio-app/index.html +26 -0
  325. data/studio-app/package-lock.json +2499 -0
  326. data/studio-app/package.json +26 -0
  327. data/studio-app/skills-lock.json +47 -0
  328. data/studio-app/src/api/client.js +71 -0
  329. data/studio-app/src/editor/body-editor.js +70 -0
  330. data/studio-app/src/editor/liquid-core.js +382 -0
  331. data/studio-app/src/editor/liquid.js +77 -0
  332. data/studio-app/src/lib/document.js +207 -0
  333. data/studio-app/src/lib/list-controls.js +317 -0
  334. data/studio-app/src/lib/schema.js +66 -0
  335. data/studio-app/src/lib/yaml-doc.js +100 -0
  336. data/studio-app/src/main.js +3693 -0
  337. data/studio-app/src/media/browser.js +578 -0
  338. data/studio-app/src/preview/render.js +430 -0
  339. data/studio-app/src/styles/studio.css +2826 -0
  340. data/studio-app/vite.config.js +25 -0
  341. data/templates/articles/README.md +40 -0
  342. data/templates/articles/fixtures/article.md +9 -0
  343. data/templates/articles/screenshots/README.md +6 -0
  344. data/templates/articles/studio.yml +73 -0
  345. data/templates/articles/template.yml +21 -0
  346. data/templates/bibliography/README.md +41 -0
  347. data/templates/bibliography/fixtures/references.json +9 -0
  348. data/templates/bibliography/screenshots/README.md +6 -0
  349. data/templates/bibliography/studio.yml +66 -0
  350. data/templates/bibliography/template.yml +18 -0
  351. data/templates/blog/README.md +38 -0
  352. data/templates/blog/fixtures/post.md +9 -0
  353. data/templates/blog/screenshots/README.md +6 -0
  354. data/templates/blog/studio.yml +69 -0
  355. data/templates/blog/template.yml +20 -0
  356. data/templates/cv/README.md +34 -0
  357. data/templates/cv/fixtures/job.md +12 -0
  358. data/templates/cv/screenshots/README.md +6 -0
  359. data/templates/cv/studio.yml +200 -0
  360. data/templates/cv/template.yml +25 -0
  361. data/templates/media/README.md +38 -0
  362. data/templates/media/fixtures/resource.md +7 -0
  363. data/templates/media/screenshots/README.md +6 -0
  364. data/templates/media/studio.yml +58 -0
  365. data/templates/media/template.yml +17 -0
  366. data/templates/network/README.md +34 -0
  367. data/templates/network/fixtures/sources.json +3 -0
  368. data/templates/network/screenshots/README.md +6 -0
  369. data/templates/network/studio.yml +28 -0
  370. data/templates/network/template.yml +10 -0
  371. data/templates/portfolio/README.md +39 -0
  372. data/templates/portfolio/fixtures/project.md +10 -0
  373. data/templates/portfolio/screenshots/README.md +6 -0
  374. data/templates/portfolio/studio.yml +89 -0
  375. data/templates/portfolio/template.yml +18 -0
  376. 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>