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
data/_layouts/cv.html ADDED
@@ -0,0 +1,393 @@
1
+ ---
2
+ layout: shell
3
+ ---
4
+
5
+ <main class="cv-page{% if page.accent and page.accent != 'blue' %} page-accent--{{ page.accent }}{% endif %} max-w-7xl mx-auto px-4 sm:px-8 md:px-16 pt-10 pb-16 min-h-screen">
6
+ <div id="cv-page-header">
7
+ <div class="cv-dossier">
8
+ <span class="shard-accent cv-dossier-shard" aria-hidden="true"></span>
9
+ <span class="shard-accent shard-accent-beta cv-dossier-shard cv-dossier-shard--beta" aria-hidden="true"></span>
10
+ <div class="cv-dossier-inner">
11
+ {% include page/page-header.html
12
+ key=page.key
13
+ header_class="cv-dossier-head"
14
+ tags_class="cv-dossier-tags code-technical hidden"
15
+ standfirst_class="cv-dossier-standfirst font-body hidden" %}
16
+ <div class="cv-dossier-aside">
17
+ <div class="cv-contact-cta" id="contact">
18
+ <span class="cv-contact-cta-label">Contact</span>
19
+ {% include page/professional-connections.html wrapper_class="cv-contacts" %}
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div id="cv-disclaimer" class="cv-disclaimer-box">
27
+ <span class="material-symbols-outlined cv-dossier-note-icon" aria-hidden="true">info</span>
28
+ <div class="cv-dossier-note font-body" data-content-fragment="cv/disclaimer">
29
+ <p class="font-body text-on-surface-variant italic">Loading...</p>
30
+ </div>
31
+ </div>
32
+
33
+ <div id="cv-listing">
34
+ {% capture cv_summary %}
35
+ <div class="cv-summary font-body" data-content-fragment="cv/summary">
36
+ <p class="font-body text-on-surface-variant italic">Loading...</p>
37
+ </div>
38
+ {% endcapture %}
39
+ {% include cv/cv-box.html id="summary" title="Summary" content=cv_summary %}
40
+
41
+ {% capture cv_experience %}
42
+ <div id="cv-experience-list" class="cv-entry-stack">
43
+ <p class="font-body text-on-surface-variant italic">Loading experience...</p>
44
+ </div>
45
+ {% endcapture %}
46
+ {% include cv/cv-box.html id="experience" title="Experience" content=cv_experience %}
47
+
48
+ {% capture cv_products %}
49
+ <div id="cv-products-grid" class="cv-product-grid">
50
+ <p class="font-body text-on-surface-variant italic">Loading products...</p>
51
+ </div>
52
+ {% endcapture %}
53
+ {% include cv/cv-box.html id="products" title="Products" content=cv_products %}
54
+
55
+ {% capture cv_education %}
56
+ <div id="cv-education-list" class="cv-entry-stack">
57
+ <p class="font-body text-on-surface-variant italic">Loading education...</p>
58
+ </div>
59
+ {% endcapture %}
60
+ {% include cv/cv-box.html id="education" title="Education" content=cv_education %}
61
+
62
+ {% capture cv_languages %}
63
+ <div id="cv-languages-list" class="cv-language-list">
64
+ <p class="font-body text-on-surface-variant italic">Loading languages...</p>
65
+ </div>
66
+ {% endcapture %}
67
+ {% include cv/cv-box.html id="languages" title="Languages" content=cv_languages %}
68
+
69
+ {% include page/timeline-section.html %}
70
+ </div>
71
+
72
+ <div id="cv-detail" class="hidden">
73
+ <div class="grid grid-cols-1 gap-8 lg:gap-12 items-start text-detail-grid">
74
+ <aside id="cv-detail-sidebar"></aside>
75
+ <div id="cv-detail-main"></div>
76
+ </div>
77
+ </div>
78
+ </main>
79
+
80
+ <script>
81
+ (function () {
82
+ 'use strict';
83
+
84
+ var runtime = window.ContentRuntime;
85
+ if (!runtime) return;
86
+
87
+ var experienceList = document.getElementById('cv-experience-list');
88
+ var educationList = document.getElementById('cv-education-list');
89
+ var languagesList = document.getElementById('cv-languages-list');
90
+ var productsGrid = document.getElementById('cv-products-grid');
91
+ var listing = document.getElementById('cv-listing');
92
+ var detail = document.getElementById('cv-detail');
93
+ var detailSidebar = document.getElementById('cv-detail-sidebar');
94
+ var detailMain = document.getElementById('cv-detail-main');
95
+ var pageHeader = document.getElementById('cv-page-header');
96
+ var disclaimer = document.getElementById('cv-disclaimer');
97
+ var selectedProduct = runtime.contentSlug('product');
98
+
99
+ var EN_DASH = '–';
100
+
101
+ function isProductOngoing(fm) {
102
+ fm = fm || {};
103
+ if (fm.status === 'ongoing') return true;
104
+ return !String(fm.end_date || '').trim();
105
+ }
106
+
107
+ function formatProductPeriod(fm) {
108
+ fm = fm || {};
109
+ if (!fm.start_date) return '';
110
+ if (isProductOngoing(fm)) return String(fm.start_date) + ' ' + EN_DASH + ' Ongoing';
111
+ return String(fm.start_date) + ' ' + EN_DASH + ' ' + fm.end_date;
112
+ }
113
+
114
+ /* An open-ended range is a fact about the role, not a missing field:
115
+ jobs and degrees both write the end date only once it exists. */
116
+ function formatSpan(start, end, openLabel) {
117
+ var from = String(start || '').trim();
118
+ var to = String(end || '').trim();
119
+ if (!from) return to;
120
+ return from + ' ' + EN_DASH + ' ' + (to || openLabel);
121
+ }
122
+
123
+ function compareProductsByEndDate(a, b) {
124
+ var afm = (a && a.front_matter) || {};
125
+ var bfm = (b && b.front_matter) || {};
126
+ var aOngoing = isProductOngoing(afm);
127
+ var bOngoing = isProductOngoing(bfm);
128
+ if (aOngoing !== bOngoing) return aOngoing ? -1 : 1;
129
+ if (aOngoing && bOngoing) {
130
+ return String(bfm.start_date || '').localeCompare(String(afm.start_date || ''));
131
+ }
132
+ return String(bfm.end_date || '').localeCompare(String(afm.end_date || ''));
133
+ }
134
+
135
+ /* Register entry: the one card shape the CV repeats. The label line
136
+ names the organisation and the unit inside it, the display line
137
+ names the post held there, and every part disappears when its
138
+ field is empty rather than leaving a dangling separator. */
139
+ function renderEntry(parts) {
140
+ var el = document.createElement('article');
141
+ el.className = 'cv-entry';
142
+
143
+ var ident = [];
144
+ if (parts.lead) {
145
+ ident.push('<span class="cv-entry-lead card-meta">' +
146
+ runtime.escapeHtml(parts.lead) + '</span>');
147
+ }
148
+ /* The separator is punctuation between two fields, so it needs
149
+ both of them: one job declares no department, and its label
150
+ line has to stop after the company. */
151
+ if (parts.unit) {
152
+ if (ident.length) {
153
+ ident.push('<span class="cv-entry-sep" aria-hidden="true">&#8250;</span>');
154
+ }
155
+ ident.push('<span class="cv-entry-unit card-meta">' +
156
+ runtime.escapeHtml(parts.unit) + '</span>');
157
+ }
158
+
159
+ var head = [
160
+ ident.length ? '<div class="cv-entry-ident">' + ident.join('') + '</div>' : '',
161
+ parts.span ? '<span class="cv-entry-span code-technical">' +
162
+ runtime.escapeHtml(parts.span) + '</span>' : ''
163
+ ].join('');
164
+
165
+ var html = [
166
+ head ? '<div class="cv-entry-head">' + head + '</div>' : '',
167
+ parts.title ? '<h3 class="card-title cv-entry-title">' +
168
+ runtime.escapeHtml(parts.title) + '</h3>' : '',
169
+ parts.place ? '<div class="cv-entry-place font-body">' +
170
+ runtime.escapeHtml(parts.place) + '</div>' : '',
171
+ parts.bodyHtml ? '<div class="cv-entry-copy font-body">' + parts.bodyHtml + '</div>' : '',
172
+ parts.footHtml || ''
173
+ ].join('');
174
+
175
+ el.innerHTML = html;
176
+ return el;
177
+ }
178
+
179
+ function renderJob(job, productMap) {
180
+ var fm = job.front_matter || {};
181
+ var description = fm.body_public
182
+ ? runtime.renderMarkdown(fm.body_public)
183
+ : runtime.renderMarkdown(job.body_markdown);
184
+
185
+ /* Role to product linkage. The chips carry the product name, so
186
+ the href lookup is keyed by the label the chip shows. */
187
+ var hrefByLabel = {};
188
+ var labels = (Array.isArray(fm.products) ? fm.products : [])
189
+ .filter(Boolean)
190
+ .map(function (productKey) {
191
+ var product = productMap[productKey];
192
+ var label = (product && product.front_matter && product.front_matter.product) || productKey;
193
+ hrefByLabel[label] = runtime.productUrl(product ? product.slug : productKey);
194
+ return label;
195
+ });
196
+ var chips = runtime.renderTagChips(labels, {
197
+ hrefFor: function (label) { return hrefByLabel[label] || '#'; }
198
+ });
199
+ var foot = chips
200
+ ? '<div class="cv-entry-foot"><span class="cv-entry-foot-label card-meta">Products</span>' +
201
+ chips + '</div>'
202
+ : '';
203
+
204
+ return renderEntry({
205
+ lead: fm.company,
206
+ unit: fm.department,
207
+ span: formatSpan(fm.start, fm.end, 'Present'),
208
+ title: fm.position,
209
+ place: fm.location,
210
+ bodyHtml: description,
211
+ footHtml: foot
212
+ });
213
+ }
214
+
215
+ /* A school has no department, and the campus is what tells two
216
+ entries of the same institution apart, so the location takes the
217
+ unit slot a job gives its department. */
218
+ function renderEducation(entry) {
219
+ return renderEntry({
220
+ lead: entry.institution,
221
+ unit: entry.location,
222
+ span: formatSpan(entry.start, entry.end, 'Ongoing'),
223
+ title: entry.degree,
224
+ bodyHtml: entry.detail
225
+ ? '<p>' + runtime.escapeHtml(entry.detail) + '</p>'
226
+ : ''
227
+ });
228
+ }
229
+
230
+ function renderLanguage(entry) {
231
+ var el = document.createElement('div');
232
+ el.className = 'cv-language';
233
+ el.innerHTML = [
234
+ '<span class="cv-language-name font-body">' + runtime.escapeHtml(entry.name || '') + '</span>',
235
+ '<span class="cv-language-rule" aria-hidden="true"></span>',
236
+ '<span class="cv-language-level code-technical">' + runtime.escapeHtml(entry.level || '') + '</span>'
237
+ ].join('');
238
+ return el;
239
+ }
240
+
241
+ function renderProduct(product, opts) {
242
+ opts = opts || {};
243
+ var fm = product.front_matter || {};
244
+ var card = document.createElement('article');
245
+ card.className = 'cv-product group';
246
+ card.setAttribute('data-dynamic-product', 'true');
247
+
248
+ var productUrl = runtime.productUrl(product.slug);
249
+ var productName = fm.product || fm.title || product.slug;
250
+ var period = formatProductPeriod(fm);
251
+ /* Several products carry the same string in `product` and
252
+ `title`; printing it twice reads as a rendering fault. */
253
+ var subtitle = fm.title && fm.title !== productName ? fm.title : '';
254
+
255
+ card.innerHTML = [
256
+ '<div class="cv-product-head">',
257
+ fm.company ? '<span class="cv-product-company card-meta">' +
258
+ runtime.escapeHtml(fm.company) + '</span>' : '<span></span>',
259
+ period ? '<time class="cv-product-span code-technical">' +
260
+ runtime.escapeHtml(period) + '</time>' : '',
261
+ '</div>',
262
+ '<h3 class="card-title cv-product-title"><a href="' + runtime.escapeAttr(productUrl) + '">' +
263
+ runtime.escapeHtml(productName) + '</a></h3>',
264
+ subtitle ? '<p class="cv-product-copy font-body">' + runtime.escapeHtml(subtitle) + '</p>' : '',
265
+ '<div class="cv-product-foot">' + runtime.renderCtaLink(productUrl, 'Details') + '</div>'
266
+ ].join('');
267
+ return card;
268
+ }
269
+
270
+ function showListing() {
271
+ if (listing) listing.classList.remove('hidden');
272
+ if (detail) detail.classList.add('hidden');
273
+ if (pageHeader) pageHeader.classList.remove('hidden');
274
+ if (disclaimer) disclaimer.classList.remove('hidden');
275
+ if (detailMain) detailMain.innerHTML = '';
276
+ if (detailSidebar) detailSidebar.innerHTML = '';
277
+ runtime.removeRuntimeBackLink();
278
+ }
279
+
280
+ async function renderProductDetail(item) {
281
+ var fm = item.front_matter || {};
282
+ if (!fm.layout) fm.layout = 'product';
283
+
284
+ if (listing) listing.classList.add('hidden');
285
+ if (pageHeader) pageHeader.classList.add('hidden');
286
+ if (disclaimer) disclaimer.classList.add('hidden');
287
+ if (detail) detail.classList.remove('hidden');
288
+
289
+ if (detailSidebar) {
290
+ detailSidebar.innerHTML = runtime.buildTextSidebar({
291
+ frontMatter: fm,
292
+ fallbackTitle: item.slug
293
+ });
294
+ }
295
+
296
+ var allItems = await runtime.loadWritingIndex('products');
297
+ var sorted = allItems.slice().sort(compareProductsByEndDate);
298
+ var index = sorted.findIndex(function (entry) { return entry.slug === item.slug; });
299
+
300
+ await runtime.renderRuntimeDetailSection({
301
+ detailAttribute: 'data-dynamic-product-detail',
302
+ bodyMarkdown: item.body_markdown,
303
+ frontMatter: fm,
304
+ mainColumn: detailMain,
305
+ footerNav: {
306
+ lang: 'enus',
307
+ previousUrl: index >= 0 && index < sorted.length - 1 ? runtime.productUrl(sorted[index + 1].slug) : null,
308
+ nextUrl: index > 0 ? runtime.productUrl(sorted[index - 1].slug) : null
309
+ }
310
+ });
311
+ }
312
+
313
+ function loadListing() {
314
+ showListing();
315
+
316
+ if (experienceList) {
317
+ runtime.Listing.showLoadingState(experienceList, 'Loading experience...');
318
+ Promise.all([
319
+ runtime.loadCollection('collections/jobs', 'jobs'),
320
+ runtime.loadCollection('collections/products', 'products')
321
+ ]).then(function (results) {
322
+ var jobs = results[0];
323
+ var productMap = {};
324
+ results[1].forEach(function (product) {
325
+ var key = product.key || (product.front_matter && product.front_matter.key) || product.slug;
326
+ productMap[key] = product;
327
+ productMap[product.slug] = product;
328
+ });
329
+ experienceList.innerHTML = '';
330
+ jobs.sort(function (a, b) {
331
+ var as = (a.front_matter && a.front_matter.start) || '';
332
+ var bs = (b.front_matter && b.front_matter.start) || '';
333
+ return String(bs).localeCompare(String(as));
334
+ }).forEach(function (job) {
335
+ experienceList.appendChild(renderJob(job, productMap));
336
+ });
337
+ }).catch(function (error) {
338
+ console.error(error);
339
+ runtime.Listing.showErrorState(experienceList, 'Experience could not be loaded right now.');
340
+ });
341
+ }
342
+
343
+ if (educationList) {
344
+ runtime.Listing.showLoadingState(educationList, 'Loading education...');
345
+ runtime.loadJson('data/education.json').then(function (entries) {
346
+ educationList.innerHTML = '';
347
+ entries.forEach(function (entry) { educationList.appendChild(renderEducation(entry)); });
348
+ }).catch(function (error) {
349
+ console.error(error);
350
+ runtime.Listing.showErrorState(educationList, 'Education could not be loaded right now.');
351
+ });
352
+ }
353
+
354
+ if (languagesList) {
355
+ runtime.Listing.showLoadingState(languagesList, 'Loading languages...');
356
+ runtime.loadJson('data/languages.json').then(function (entries) {
357
+ languagesList.innerHTML = '';
358
+ entries.forEach(function (entry) { languagesList.appendChild(renderLanguage(entry)); });
359
+ }).catch(function (error) {
360
+ console.error(error);
361
+ runtime.Listing.showErrorState(languagesList, 'Languages could not be loaded right now.');
362
+ });
363
+ }
364
+
365
+ if (productsGrid) {
366
+ runtime.Listing.showLoadingState(productsGrid, 'Loading products...');
367
+ runtime.loadWritingIndex('products').then(function (products) {
368
+ productsGrid.innerHTML = '';
369
+ var ordered = products.slice().sort(compareProductsByEndDate);
370
+ ordered.forEach(function (product) {
371
+ productsGrid.appendChild(renderProduct(product));
372
+ });
373
+ }).catch(function (error) {
374
+ console.error(error);
375
+ runtime.Listing.showErrorState(productsGrid, 'Products could not be loaded right now.');
376
+ });
377
+ }
378
+ }
379
+
380
+ if (selectedProduct) {
381
+ runtime.loadCollectionItem('products', selectedProduct).then(function (item) {
382
+ return renderProductDetail(item);
383
+ }).catch(function (error) {
384
+ console.error(error);
385
+ loadListing();
386
+ if (productsGrid) runtime.Listing.showErrorState(productsGrid, 'This product could not be loaded right now.');
387
+ });
388
+ return;
389
+ }
390
+
391
+ loadListing();
392
+ })();
393
+ </script>