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,861 @@
1
+ {% raw %}
2
+ function buildTextSidebar(options) {
3
+ var fm = options.frontMatter || {};
4
+ var title = fm.title || options.fallbackTitle || '';
5
+ var dateHtml = options.dateHtml || '';
6
+ var description = fm.description || fm.tagline || '';
7
+ var parts = ['<div class="md:sticky md:top-32 text-detail-sidebar">'];
8
+
9
+ if (title) {
10
+ parts.push('<div class="article-sidebar-heading mb-8">');
11
+ if (fm.icon) {
12
+ /* The page accent, not a fixed triad role: this glyph marks a
13
+ project, and the projects page reads blue. text-secondary pinned
14
+ it green there regardless. */
15
+ parts.push('<span class="material-symbols-outlined text-5xl md:text-6xl text-primary mb-4 block leading-none">' + escapeHtml(fm.icon) + '</span>');
16
+ }
17
+ parts.push('<h1 class="text-title">' + escapeHtml(title) + '</h1>');
18
+ if (fm.layout === 'post' && fm.thumbnail) {
19
+ parts.push(
20
+ '<figure class="article-sidebar-thumbnail">',
21
+ '<img src="', escapeAttr(buildUrl(fm.thumbnail)), '" alt="" loading="lazy">',
22
+ '</figure>'
23
+ );
24
+ } else if (description) {
25
+ parts.push('<p class="article-sidebar-description">' + escapeHtml(description) + '</p>');
26
+ } else if (fm.thumbnail) {
27
+ parts.push(
28
+ '<figure class="article-sidebar-thumbnail">',
29
+ '<img src="', escapeAttr(buildUrl(fm.thumbnail)), '" alt="" loading="lazy">',
30
+ '</figure>'
31
+ );
32
+ }
33
+ parts.push('</div>');
34
+ }
35
+
36
+ parts.push('<div class="flex flex-col gap-6 text-tertiary font-label text-xs article-meta mb-8">');
37
+ parts.push('<div class="flex flex-col gap-2">');
38
+ // Tags replaced category on articles and posts; resources still classify
39
+ // on category, so both are read.
40
+ if (fm.category) parts.push('<span class="text-tertiary font-bold">' + escapeHtml(fm.category) + '</span>');
41
+ // The private Liquid path renders these as links into the listing
42
+ // filter; rendering them as inert spans here gave the same page two
43
+ // different affordances depending on which path drew it.
44
+ if (typeof renderTagChips === 'function') {
45
+ var tagBase = { posts: '/pages/blog/', articles: '/pages/articles/',
46
+ project: '/pages/projects/' }[fm.layout === 'project' ? 'project' :
47
+ (fm.layout === 'post' ? 'posts' : (fm.layout === 'article' ? 'articles' : ''))];
48
+ parts.push(renderTagChips(fm.tags, tagBase ? {
49
+ hrefFor: function (tag) { return tagBase + '?tag=' + encodeURIComponent(tag); }
50
+ } : undefined));
51
+ }
52
+ if (dateHtml) parts.push('<div class="mt-1">' + dateHtml + '</div>');
53
+ if (fm.layout === 'product') {
54
+ if (fm.company) parts.push('<span class="text-secondary font-bold">' + escapeHtml(fm.company) + '</span>');
55
+ if (fm.product) parts.push('<span>' + escapeHtml(fm.product) + '</span>');
56
+ if (fm.start_date) {
57
+ var productOngoing = fm.status === 'ongoing' || !String(fm.end_date || '').trim();
58
+ var productEnd = productOngoing ? ' – Ongoing' : (fm.end_date ? ' – ' + escapeHtml(fm.end_date) : '');
59
+ parts.push('<span>' + escapeHtml(fm.start_date) + productEnd + '</span>');
60
+ }
61
+ }
62
+ if (fm.layout === 'project') {
63
+ if (fm.label) parts.push('<span class="text-tertiary font-bold">' + escapeHtml(fm.label) + '</span>');
64
+ if (fm.start_date) {
65
+ var end = fm.status === 'ongoing' ? ' – Ongoing' : (fm.end_date ? ' – ' + escapeHtml(fm.end_date) : '');
66
+ parts.push('<span>' + escapeHtml(fm.start_date) + end + '</span>');
67
+ }
68
+ }
69
+ parts.push('</div>');
70
+ parts.push(buildDetailActions(fm));
71
+ parts.push('</div></div>');
72
+ return parts.join('');
73
+ }
74
+
75
+ /* Reading size. Nine pt offsets (−4…+4) from the default prose size.
76
+ The shell applies the saved step before paint; this control only changes it. */
77
+ function proseSizeLabel(step) {
78
+ return step > 0 ? '+' + step : String(step);
79
+ }
80
+
81
+ function buildProseSizeControl() {
82
+ var api = window.ShellProseSize;
83
+ var steps = api ? api.offsets : [-4, -3, -2, -1, 0, 1, 2, 3, 4];
84
+ var current = api ? api.read() : 0;
85
+ var currentLabel = proseSizeLabel(current);
86
+ var parts = [
87
+ '<div class="detail-type-size" data-prose-size>',
88
+ '<button type="button" class="detail-type-size-trigger" aria-haspopup="listbox" aria-expanded="false" aria-label="Reading size ', currentLabel, ' pt">',
89
+ '<span class="material-symbols-outlined" aria-hidden="true">format_size</span>',
90
+ '<span class="detail-type-size-value" data-prose-size-label>', currentLabel, '</span>',
91
+ '<span class="material-symbols-outlined detail-type-size-caret" aria-hidden="true">expand_more</span>',
92
+ '</button>',
93
+ '<div class="detail-type-size-menu" role="listbox" aria-label="Reading size" hidden>'
94
+ ];
95
+ for (var i = 0; i < steps.length; i++) {
96
+ var step = steps[i];
97
+ var label = proseSizeLabel(step);
98
+ var on = step === current;
99
+ parts.push(
100
+ '<button type="button" class="detail-type-size-step" data-prose-offset="', String(step),
101
+ '" role="option" aria-selected="', on ? 'true' : 'false',
102
+ '" aria-label="', label, ' pt" tabindex="-1">',
103
+ label,
104
+ '</button>'
105
+ );
106
+ }
107
+ parts.push('</div></div>');
108
+ return parts.join('');
109
+ }
110
+
111
+ function bindProseSizeControl(group) {
112
+ if (!group || group._proseSizeBound) return;
113
+ group._proseSizeBound = true;
114
+ var api = window.ShellProseSize;
115
+ var trigger = group.querySelector('.detail-type-size-trigger');
116
+ var menu = group.querySelector('.detail-type-size-menu');
117
+ var valueEl = group.querySelector('[data-prose-size-label]');
118
+ if (!trigger || !menu) return;
119
+
120
+ function options() {
121
+ return menu.querySelectorAll('[data-prose-offset]');
122
+ }
123
+
124
+ function setOpen(open) {
125
+ trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
126
+ if (open) menu.removeAttribute('hidden');
127
+ else menu.setAttribute('hidden', '');
128
+ }
129
+
130
+ function isOpen() {
131
+ return trigger.getAttribute('aria-expanded') === 'true';
132
+ }
133
+
134
+ function paint(current) {
135
+ var label = proseSizeLabel(current);
136
+ if (valueEl) valueEl.textContent = label;
137
+ trigger.setAttribute('aria-label', 'Reading size ' + label + ' pt');
138
+ var buttons = options();
139
+ for (var i = 0; i < buttons.length; i++) {
140
+ var on = Number(buttons[i].getAttribute('data-prose-offset')) === current;
141
+ buttons[i].setAttribute('aria-selected', on ? 'true' : 'false');
142
+ }
143
+ }
144
+
145
+ function selectedIndex() {
146
+ var buttons = options();
147
+ for (var i = 0; i < buttons.length; i++) {
148
+ if (buttons[i].getAttribute('aria-selected') === 'true') return i;
149
+ }
150
+ return 0;
151
+ }
152
+
153
+ function choose(n, closeAfter) {
154
+ if (!api || api.offsets.indexOf(n) === -1) return;
155
+ paint(api.set(n));
156
+ if (closeAfter) {
157
+ setOpen(false);
158
+ trigger.focus();
159
+ }
160
+ }
161
+
162
+ paint(api ? api.read() : 0);
163
+
164
+ trigger.addEventListener('click', function (event) {
165
+ event.preventDefault();
166
+ setOpen(!isOpen());
167
+ if (isOpen()) {
168
+ var buttons = options();
169
+ var idx = selectedIndex();
170
+ if (buttons[idx]) {
171
+ buttons[idx].focus();
172
+ if (buttons[idx].scrollIntoView) {
173
+ buttons[idx].scrollIntoView({ block: 'nearest' });
174
+ }
175
+ }
176
+ }
177
+ });
178
+
179
+ menu.addEventListener('click', function (event) {
180
+ var btn = event.target.closest ? event.target.closest('[data-prose-offset]') : null;
181
+ if (!btn || !menu.contains(btn)) return;
182
+ choose(Number(btn.getAttribute('data-prose-offset')), true);
183
+ });
184
+
185
+ group.addEventListener('keydown', function (event) {
186
+ var buttons = options();
187
+ if (!buttons.length) return;
188
+
189
+ if (event.key === 'Escape') {
190
+ if (!isOpen()) return;
191
+ event.preventDefault();
192
+ setOpen(false);
193
+ trigger.focus();
194
+ return;
195
+ }
196
+
197
+ if (!isOpen()) {
198
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
199
+ event.preventDefault();
200
+ setOpen(true);
201
+ var openIdx = selectedIndex();
202
+ if (buttons[openIdx]) buttons[openIdx].focus();
203
+ }
204
+ return;
205
+ }
206
+
207
+ var dir = 0;
208
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') dir = 1;
209
+ else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') dir = -1;
210
+ else if (event.key === 'Home') {
211
+ event.preventDefault();
212
+ buttons[0].focus();
213
+ return;
214
+ } else if (event.key === 'End') {
215
+ event.preventDefault();
216
+ buttons[buttons.length - 1].focus();
217
+ return;
218
+ } else if (event.key === 'Enter' || event.key === ' ') {
219
+ var focused = document.activeElement;
220
+ if (focused && menu.contains(focused) && focused.hasAttribute('data-prose-offset')) {
221
+ event.preventDefault();
222
+ choose(Number(focused.getAttribute('data-prose-offset')), true);
223
+ }
224
+ return;
225
+ } else {
226
+ return;
227
+ }
228
+
229
+ var index = 0;
230
+ for (var i = 0; i < buttons.length; i++) {
231
+ if (buttons[i] === document.activeElement) index = i;
232
+ }
233
+ var next = buttons[index + dir];
234
+ if (!next) return;
235
+ event.preventDefault();
236
+ next.focus();
237
+ });
238
+
239
+ document.addEventListener('click', function (event) {
240
+ if (!isOpen()) return;
241
+ if (group.contains(event.target)) return;
242
+ setOpen(false);
243
+ });
244
+ }
245
+
246
+ function bindProseSizeControls(root) {
247
+ if (!root || !root.querySelectorAll) return;
248
+ if (root.getAttribute && root.getAttribute('data-prose-size') !== null) {
249
+ bindProseSizeControl(root);
250
+ }
251
+ var groups = root.querySelectorAll('[data-prose-size]');
252
+ for (var i = 0; i < groups.length; i++) bindProseSizeControl(groups[i]);
253
+ }
254
+
255
+ /* Back · Share · Cite · PDF. Back returns to the collection listing.
256
+ Share/Cite use the live URL and front-matter fields; PDF renders only if `pdf_url` is set.
257
+ Projects and products only get the Back button: BibTeX citation does not apply to portfolio
258
+ items, and they carry their own outbound link actions in the hero actions row.
259
+ The reading-size pill sits with them on every detail page. */
260
+ function buildDetailActions(fm) {
261
+ fm = fm || {};
262
+ var layout = fm.layout || '';
263
+ var isArticleOrPost = layout === 'article' || layout === 'post';
264
+ var isProjectOrProduct = layout === 'project' || layout === 'product';
265
+ if (!isArticleOrPost && !isProjectOrProduct) return '';
266
+ var lang = fm.language || (layout === 'post' ? 'ptbr' : 'enus');
267
+ var backUrl = layout === 'post' ? '/pages/blog/'
268
+ : layout === 'project' ? '/pages/projects/'
269
+ : layout === 'product' ? '/pages/cv/#products'
270
+ : '/pages/articles/';
271
+ var parts = ['<div class="detail-actions" data-detail-actions>'];
272
+ parts.push(
273
+ '<a class="detail-action" href="', escapeAttr(backUrl), '" aria-label="Back">',
274
+ '<span class="material-symbols-outlined text-sm" aria-hidden="true">chevron_left</span>',
275
+ '<span class="detail-action-label">Back</span>',
276
+ '</a>'
277
+ );
278
+ if (isArticleOrPost) {
279
+ parts.push(
280
+ '<button type="button" class="detail-action" data-detail-share aria-label="Share">',
281
+ '<span class="material-symbols-outlined text-sm" aria-hidden="true">share</span>',
282
+ '<span class="detail-action-label">Share</span>',
283
+ '</button>'
284
+ );
285
+ parts.push(
286
+ '<button type="button" class="detail-action" data-detail-cite aria-label="Cite">',
287
+ '<span class="material-symbols-outlined text-sm" aria-hidden="true">format_quote</span>',
288
+ '<span class="detail-action-label">Cite</span>',
289
+ '</button>'
290
+ );
291
+ }
292
+ parts.push(buildProseSizeControl());
293
+ if (isArticleOrPost && fm.pdf_url) {
294
+ parts.push(
295
+ '<a class="detail-action" href="', escapeAttr(String(fm.pdf_url)),
296
+ '" rel="noopener" target="_blank" aria-label="PDF">',
297
+ '<span class="material-symbols-outlined text-sm" aria-hidden="true">picture_as_pdf</span>',
298
+ '<span class="detail-action-label">PDF</span>',
299
+ '</a>'
300
+ );
301
+ }
302
+ parts.push('</div>');
303
+ return parts.join('');
304
+ }
305
+
306
+ function articleCiteBibtex(fm, pageUrl) {
307
+ fm = fm || {};
308
+ var year = String(fm.date || '').slice(0, 4);
309
+ var key = String(fm.slug || fm.title || 'entry')
310
+ .toLowerCase()
311
+ .replace(/[^a-z0-9]+/g, '_')
312
+ .replace(/^_|_$/g, '')
313
+ .slice(0, 48) || 'entry';
314
+ var lines = [
315
+ '@misc{' + key + ',',
316
+ ' title = {' + String(fm.title || key).replace(/[{}]/g, '') + '},',
317
+ ' author = {' + String(fm.author || (window.ContentRuntimeConfig && window.ContentRuntimeConfig.defaultAuthor) || '').replace(/[{}]/g, '') + '},'
318
+ ];
319
+ if (year) lines.push(' year = {' + year + '},');
320
+ lines.push(' url = {' + String(pageUrl || '') + '}');
321
+ lines.push('}');
322
+ return lines.join('\n');
323
+ }
324
+
325
+ /* End-matter BibTeX plate for citing this page — same fields as Cite. */
326
+ function buildCiteBox(fm, lang) {
327
+ fm = fm || {};
328
+ if (fm.layout !== 'article' && fm.layout !== 'post') return '';
329
+ var bibtex = articleCiteBibtex(fm, absoluteUrl(window.location.pathname));
330
+ var label = translate('cite_this_text', lang || fm.language || 'enus');
331
+ return [
332
+ '<section class="article-cite-block text-reading-column w-full" data-article-cite-block>',
333
+ '<p class="article-cite-label font-label">', escapeHtml(label), '</p>',
334
+ '<pre><code class="language-bibtex">', escapeHtml(bibtex), '</code></pre>',
335
+ '</section>'
336
+ ].join('');
337
+ }
338
+
339
+ function copyTextToClipboard(text) {
340
+ if (navigator.clipboard && navigator.clipboard.writeText) {
341
+ return navigator.clipboard.writeText(text);
342
+ }
343
+ return new Promise(function (resolve, reject) {
344
+ try {
345
+ var ta = document.createElement('textarea');
346
+ ta.value = text;
347
+ ta.setAttribute('readonly', '');
348
+ ta.style.position = 'fixed';
349
+ ta.style.left = '-9999px';
350
+ document.body.appendChild(ta);
351
+ ta.select();
352
+ document.execCommand('copy');
353
+ document.body.removeChild(ta);
354
+ resolve();
355
+ } catch (err) {
356
+ reject(err);
357
+ }
358
+ });
359
+ }
360
+
361
+ function bindDetailActions(root, fm) {
362
+ if (!root) return;
363
+ var shareBtn = root.querySelector('[data-detail-share]');
364
+ var citeBtn = root.querySelector('[data-detail-cite]');
365
+ var pageUrl = absoluteUrl(window.location.pathname);
366
+
367
+ if (shareBtn) {
368
+ shareBtn.addEventListener('click', function () {
369
+ var payload = {
370
+ title: (fm && fm.title) || document.title,
371
+ text: (fm && fm.description) || '',
372
+ url: pageUrl
373
+ };
374
+ var labelEl = shareBtn.querySelector('.detail-action-label') || shareBtn;
375
+ var done = function () {
376
+ labelEl.textContent = 'Copied';
377
+ window.setTimeout(function () { labelEl.textContent = 'Share'; }, 1600);
378
+ };
379
+ if (navigator.share) {
380
+ navigator.share(payload).catch(function () {
381
+ copyTextToClipboard(pageUrl).then(done);
382
+ });
383
+ } else {
384
+ copyTextToClipboard(pageUrl).then(done);
385
+ }
386
+ });
387
+ }
388
+
389
+ if (citeBtn) {
390
+ citeBtn.addEventListener('click', function () {
391
+ var labelEl = citeBtn.querySelector('.detail-action-label') || citeBtn;
392
+ copyTextToClipboard(articleCiteBibtex(fm, pageUrl)).then(function () {
393
+ labelEl.textContent = 'Copied';
394
+ window.setTimeout(function () { labelEl.textContent = 'Cite'; }, 1600);
395
+ });
396
+ });
397
+ }
398
+
399
+ bindProseSizeControls(root);
400
+
401
+ var citeBoxBtn = root.querySelector('[data-detail-cite-box]');
402
+ if (citeBoxBtn) {
403
+ citeBoxBtn.addEventListener('click', function () {
404
+ copyTextToClipboard(articleCiteBibtex(fm, pageUrl)).then(function () {
405
+ citeBoxBtn.textContent = 'Copied';
406
+ window.setTimeout(function () { citeBoxBtn.textContent = 'Copy BibTeX'; }, 1600);
407
+ });
408
+ });
409
+ }
410
+ }
411
+
412
+ function buildTextBackLink(options) {
413
+ var lang = options.lang || 'enus';
414
+ var label = options.backLabel || translate(options.backLabelKey || 'back', lang);
415
+ return [
416
+ '<a href="', escapeAttr(options.backUrl || '#'), '" ',
417
+ 'class="back-link text-sm font-label inline-flex items-center gap-2 no-underline hover:underline text-primary mb-8 block" ',
418
+ 'id="', escapeAttr(options.id || 'runtime-back-link'), '">',
419
+ '<span class="material-symbols-outlined text-sm">arrow_left_alt</span>',
420
+ escapeHtml(label),
421
+ '</a>'
422
+ ].join('');
423
+ }
424
+
425
+ function buildTextFooterNav(options) {
426
+ var lang = options.lang || 'enus';
427
+ var parts = ['<nav class="text-page-nav text-reading-column w-full" aria-label="Article navigation">'];
428
+
429
+ if (options.previousUrl) {
430
+ parts.push(
431
+ '<a href="', escapeAttr(options.previousUrl), '">',
432
+ '<span class="material-symbols-outlined text-sm">arrow_left_alt</span>',
433
+ escapeHtml(translate('previous_text', lang)),
434
+ '</a>'
435
+ );
436
+ } else {
437
+ parts.push('<span></span>');
438
+ }
439
+
440
+ if (options.nextUrl) {
441
+ parts.push(
442
+ '<a href="', escapeAttr(options.nextUrl), '">',
443
+ escapeHtml(translate('next_text', lang)),
444
+ '<span class="material-symbols-outlined text-sm">arrow_right_alt</span>',
445
+ '</a>'
446
+ );
447
+ } else if (options.showRandom) {
448
+ parts.push(
449
+ '<a href="#" id="random-post-link">',
450
+ escapeHtml(translate('random_text', lang)),
451
+ '<span class="material-symbols-outlined text-sm">shuffle</span>',
452
+ '</a>'
453
+ );
454
+ } else {
455
+ parts.push('<span></span>');
456
+ }
457
+
458
+ parts.push('</nav>');
459
+ return parts.join('');
460
+ }
461
+
462
+ async function enhanceArticleContent(root, frontMatter) {
463
+ if (!root) return;
464
+ decorateCodeBlocks(root);
465
+ await typesetMath(root);
466
+ if (!frontMatter || !frontMatter.mermaid) return;
467
+ try {
468
+ await ensureMermaid();
469
+ root.querySelectorAll('pre code.language-mermaid').forEach(function (code) {
470
+ var div = document.createElement('div');
471
+ div.className = 'mermaid';
472
+ div.textContent = code.textContent;
473
+ // Keep the source so the diagram can be redrawn on a theme change.
474
+ div.dataset.mermaidSource = code.textContent;
475
+ code.closest('pre').replaceWith(div);
476
+ });
477
+ if (typeof window.mermaid.run === 'function') {
478
+ await window.mermaid.run({ nodes: root.querySelectorAll('.mermaid') });
479
+ }
480
+ } catch (error) {
481
+ console.error('Mermaid rendering failed:', error);
482
+ }
483
+ }
484
+
485
+ function removeRuntimeBackLink() {
486
+ var existing = document.getElementById('runtime-back-link');
487
+ if (existing) existing.remove();
488
+ }
489
+
490
+ function insertRuntimeBackLink(beforeElement, options) {
491
+ removeRuntimeBackLink();
492
+ if (!beforeElement || !beforeElement.parentElement) return;
493
+ var wrapper = document.createElement('div');
494
+ wrapper.innerHTML = buildTextBackLink(options);
495
+ beforeElement.parentElement.insertBefore(wrapper.firstChild, beforeElement);
496
+ }
497
+
498
+ function absoluteUrl(path) {
499
+ if (!path) return window.location.href;
500
+ var value = String(path);
501
+ if (value.indexOf('://') !== -1) return value;
502
+ return window.location.origin + (value.charAt(0) === '/' ? value : '/' + value);
503
+ }
504
+
505
+ function setMetaTag(name, content, attr) {
506
+ if (!content) return;
507
+ attr = attr || 'name';
508
+ var selector = attr + '="' + name + '"';
509
+ var el = document.head.querySelector('meta[' + selector + ']');
510
+ if (!el) {
511
+ el = document.createElement('meta');
512
+ el.setAttribute(attr, name);
513
+ document.head.appendChild(el);
514
+ }
515
+ el.setAttribute('content', content);
516
+ }
517
+
518
+ function applyDocumentMeta(meta) {
519
+ meta = meta || {};
520
+ var siteSuffix = (window.ContentRuntimeConfig && window.ContentRuntimeConfig.siteSuffix) || '';
521
+ var title = meta.title ? String(meta.title) + siteSuffix : document.title;
522
+ document.title = title;
523
+ if (meta.description) setMetaTag('description', meta.description);
524
+ setMetaTag('og:title', title, 'property');
525
+ if (meta.description) setMetaTag('og:description', meta.description, 'property');
526
+ if (meta.url) setMetaTag('og:url', absoluteUrl(meta.url), 'property');
527
+
528
+ var defaultOgImage = '/assets/images/og-default-dark.png';
529
+ var imagePath = meta.image || defaultOgImage;
530
+ var imageUrl = resolveAssetUrl(imagePath);
531
+ setMetaTag('og:image', imageUrl, 'property');
532
+ setMetaTag('image', imageUrl, 'name');
533
+ setMetaTag('twitter:image', imageUrl);
534
+
535
+ var ext = imageUrl.split('?')[0].split('.').pop().toLowerCase();
536
+ var imageType = (ext === 'jpg' || ext === 'jpeg') ? 'image/jpeg' : (ext === 'webp' ? 'image/webp' : 'image/png');
537
+ setMetaTag('og:image:type', imageType, 'property');
538
+ setMetaTag('og:image:width', '1200', 'property');
539
+ setMetaTag('og:image:height', '630', 'property');
540
+
541
+ setMetaTag('twitter:title', title);
542
+ if (meta.description) setMetaTag('twitter:description', meta.description);
543
+ }
544
+
545
+ /* Related Texts: neighbors from writing_index.json xcites (same graph the
546
+ Network of Ideas reads). Two sections — Cites, then Cited by — each
547
+ ordered projects → articles → posts. */
548
+ var RELATED_COLLECTION_ORDER = { projects: 0, articles: 1, posts: 2 };
549
+
550
+ /* Site language codes (enus / ptbr) are not BCP-47 tags. Intl.DateTimeFormat
551
+ rejects them; map to a real locale before any date render. */
552
+ function relatedDateLocale(lang) {
553
+ if (lang === 'ptbr') return 'pt-BR';
554
+ if (lang === 'frfr') return 'fr';
555
+ if (lang === 'eses') return 'es';
556
+ return 'en';
557
+ }
558
+
559
+ function relatedCollection(item) {
560
+ var raw = (item && (item.collection || item.type)) || '';
561
+ if (raw === 'article') return 'articles';
562
+ if (raw === 'post') return 'posts';
563
+ if (raw === 'project') return 'projects';
564
+ return raw;
565
+ }
566
+
567
+ function relatedDateLine(date, lastUpdated, locale) {
568
+ var created = formatDate(date, locale);
569
+ var updated = formatDate(lastUpdated, locale);
570
+ var hasUpdate = dateKey(lastUpdated) && dateKey(lastUpdated) !== dateKey(date);
571
+ if (!created && !(hasUpdate && updated)) return '';
572
+ if (created && hasUpdate && updated) {
573
+ return '<div class="article-related-card-dates">' +
574
+ '<span title="Published">' + escapeHtml(created) + '</span>' +
575
+ '<span class="article-related-card-dates-sep" aria-hidden="true"> / </span>' +
576
+ '<span title="Updated">' + escapeHtml(updated) + '</span>' +
577
+ '</div>';
578
+ }
579
+ return '<div class="article-related-card-dates"><span title="Published">' +
580
+ escapeHtml(created || updated) + '</span></div>';
581
+ }
582
+
583
+ function relatedCardHtml(neighbor, dateLocale) {
584
+ var item = neighbor.item;
585
+ var dateHtml = relatedDateLine(item.date, item.last_updated, dateLocale);
586
+ return [
587
+ '<article class="article-related-card article-related-card--', neighbor.collection, '">',
588
+ '<h3 class="article-related-card-title"><a href="', escapeAttr(xciteUrl(item)), '">',
589
+ escapeHtml(xciteLabel(item)), '</a></h3>',
590
+ dateHtml,
591
+ '</article>'
592
+ ].join('');
593
+ }
594
+
595
+ function relatedSectionHtml(type, items, lang, dateLocale) {
596
+ if (!items.length) return '';
597
+ var label = type === 'out' ? translate('cites', lang) : translate('cited_by', lang);
598
+ var parts = [
599
+ '<section class="article-related-section">',
600
+ '<h3 class="article-related-section-pill">', escapeHtml(label), '</h3>',
601
+ '<div class="article-related-list">'
602
+ ];
603
+ items.forEach(function (neighbor) {
604
+ parts.push(relatedCardHtml(neighbor, dateLocale));
605
+ });
606
+ parts.push('</div></section>');
607
+ return parts.join('');
608
+ }
609
+
610
+ function buildRelatedBox(currentKey, currentItem, xciteIndex, langHint) {
611
+ if (!currentKey || !xciteIndex) return '';
612
+
613
+ var outKeys = Array.isArray(currentItem && currentItem.xcites)
614
+ ? currentItem.xcites
615
+ : [];
616
+ var seenOut = {};
617
+ var seenIn = {};
618
+ var outbound = [];
619
+ var inbound = [];
620
+
621
+ outKeys.forEach(function (key) {
622
+ if (seenOut[key]) return;
623
+ seenOut[key] = true;
624
+ var item = xciteIndex[key];
625
+ if (!item) return;
626
+ var coll = relatedCollection(item);
627
+ if (!(coll in RELATED_COLLECTION_ORDER)) return;
628
+ outbound.push({ type: 'out', item: item, collection: coll });
629
+ });
630
+
631
+ Object.keys(xciteIndex).forEach(function (key) {
632
+ if (key === currentKey) return;
633
+ var item = xciteIndex[key];
634
+ var cites = Array.isArray(item.xcites) ? item.xcites : [];
635
+ if (cites.indexOf(currentKey) === -1) return;
636
+ var coll = relatedCollection(item);
637
+ if (!(coll in RELATED_COLLECTION_ORDER)) return;
638
+ if (seenIn[key]) return;
639
+ seenIn[key] = true;
640
+ inbound.push({ type: 'in', item: item, collection: coll });
641
+ });
642
+
643
+ if (!outbound.length && !inbound.length) return '';
644
+
645
+ function byCollection(a, b) {
646
+ return RELATED_COLLECTION_ORDER[a.collection] - RELATED_COLLECTION_ORDER[b.collection];
647
+ }
648
+ outbound.sort(byCollection);
649
+ inbound.sort(byCollection);
650
+
651
+ var lang = langHint
652
+ || (currentItem && currentItem.language)
653
+ || ((currentItem && relatedCollection(currentItem) === 'posts') ? 'ptbr' : 'enus');
654
+ var dateLocale = relatedDateLocale(lang);
655
+
656
+ return [
657
+ '<div class="article-related-box">',
658
+ '<h2 class="article-related-title">', escapeHtml(translate('related_texts', lang)), '</h2>',
659
+ relatedSectionHtml('out', outbound, lang, dateLocale),
660
+ relatedSectionHtml('in', inbound, lang, dateLocale),
661
+ '</div>'
662
+ ].join('');
663
+ }
664
+
665
+ function mountRelatedTexts(sidebarEl, currentKey, xciteIndex, langHint) {
666
+ if (!sidebarEl || !currentKey || !xciteIndex) return;
667
+ var currentItem = xciteIndex[currentKey] || null;
668
+ var relatedHtml = buildRelatedBox(currentKey, currentItem, xciteIndex, langHint);
669
+ if (!relatedHtml) return;
670
+ var sticky = sidebarEl.querySelector('.md\\:sticky') || sidebarEl.firstElementChild || sidebarEl;
671
+ var wrap = document.createElement('div');
672
+ wrap.innerHTML = relatedHtml;
673
+ if (wrap.firstElementChild) sticky.appendChild(wrap.firstElementChild);
674
+ }
675
+
676
+ /* Sticky `top` percentages resolve against the stretched <aside> (the full
677
+ reading-column height), not the dossier — so the tall-sidebar CSS trick
678
+ cannot see the plate's own height. Measure after mount and set top so a
679
+ short dossier parks under the nav, and a tall one scrolls with the page
680
+ until its bottom meets the viewport. */
681
+ function syncDetailSidebarSticky(sidebarEl) {
682
+ var el = sidebarEl && (
683
+ sidebarEl.querySelector('.text-detail-sidebar') ||
684
+ sidebarEl.querySelector('.md\\:sticky') ||
685
+ sidebarEl.firstElementChild
686
+ );
687
+ if (!el || window.matchMedia('(max-width: 767px)').matches) return;
688
+
689
+ function apply() {
690
+ var navPx = 7 * 16;
691
+ var height = el.offsetHeight;
692
+ var view = window.innerHeight;
693
+ if (height <= view - navPx) {
694
+ el.style.top = navPx + 'px';
695
+ } else {
696
+ el.style.top = (view - height) + 'px';
697
+ }
698
+ }
699
+
700
+ apply();
701
+ if (el._detailStickyBound) return;
702
+ el._detailStickyBound = true;
703
+ window.addEventListener('resize', apply);
704
+ if (typeof ResizeObserver !== 'undefined') {
705
+ var ro = new ResizeObserver(apply);
706
+ ro.observe(el);
707
+ }
708
+ }
709
+
710
+ function articleContentOpensWithHeading(root) {
711
+ if (!root || !root.firstElementChild) return false;
712
+ return /^H[1-6]$/i.test(root.firstElementChild.tagName);
713
+ }
714
+
715
+ function applyDropCapClass(body, frontMatter) {
716
+ if (!body || !frontMatter || frontMatter.drop_cap !== true) return;
717
+ if (!articleContentOpensWithHeading(body)) {
718
+ body.classList.add('has-drop-cap');
719
+ }
720
+ }
721
+
722
+ async function renderRuntimeDetailSection(options) {
723
+ var fm = options.frontMatter || {};
724
+ if (options.documentMeta !== false) {
725
+ applyDocumentMeta({
726
+ title: fm.title,
727
+ description: fm.description,
728
+ url: window.location.pathname,
729
+ image: fm.og_image || fm.thumbnail
730
+ });
731
+ }
732
+ var section = document.createElement('section');
733
+ section.className = 'text-page-main text-reading-column w-full';
734
+ section.setAttribute(options.detailAttribute, 'true');
735
+
736
+ var body = document.createElement('div');
737
+ body.className = 'article-content';
738
+ body.innerHTML = await renderRichContent(options.bodyMarkdown, fm);
739
+ applyDropCapClass(body, fm);
740
+ section.appendChild(body);
741
+
742
+ if (options.mainColumn) options.mainColumn.appendChild(section);
743
+
744
+ await enhanceArticleContent(body, fm);
745
+
746
+ assignHeadingIds(body);
747
+ mountArticleToc({
748
+ entries: buildTocEntries(body),
749
+ lang: resolveTocLang(fm),
750
+ sidebarEl: options.sidebarEl,
751
+ detailSection: section
752
+ });
753
+
754
+ /* Related Texts sit below the TOC. The index is already warm from
755
+ renderRichContent; ensureXciteIndex resolves immediately. A failure
756
+ here must not abort the cite box that follows. */
757
+ try {
758
+ var xciteIndex = await ensureXciteIndex();
759
+ var relatedLang = fm.language || (fm.layout === 'post' ? 'ptbr' : 'enus');
760
+ mountRelatedTexts(options.sidebarEl, fm.key, xciteIndex, relatedLang);
761
+ } catch (relatedError) {
762
+ console.error(relatedError);
763
+ }
764
+ syncDetailSidebarSticky(options.sidebarEl);
765
+
766
+ if (options.sidebarEl) bindDetailActions(options.sidebarEl, fm);
767
+ bindDetailActions(section, fm);
768
+
769
+ if (options.footerNav) {
770
+ var footer = document.createElement('div');
771
+ footer.innerHTML = buildTextFooterNav(options.footerNav);
772
+ if (footer.firstElementChild) {
773
+ if (options.mainColumn) {
774
+ options.mainColumn.appendChild(footer.firstElementChild);
775
+ } else {
776
+ section.appendChild(footer.firstElementChild);
777
+ }
778
+ }
779
+ }
780
+
781
+ var citeBoxHtml = buildCiteBox(fm, options.footerNav ? options.footerNav.lang : null);
782
+ if (citeBoxHtml) {
783
+ var citeWrap = document.createElement('div');
784
+ citeWrap.innerHTML = citeBoxHtml;
785
+ if (citeWrap.firstElementChild) {
786
+ var citeBlock = citeWrap.firstElementChild;
787
+ decorateCodeBlocks(citeBlock);
788
+ if (options.mainColumn) {
789
+ options.mainColumn.appendChild(citeBlock);
790
+ } else {
791
+ section.appendChild(citeBlock);
792
+ }
793
+ }
794
+ }
795
+
796
+ return section;
797
+ }
798
+
799
+ function reportDetailLoadError(error, options) {
800
+ console.error(error);
801
+ if (!options || !options.emptyState || !options.grid) return;
802
+ options.emptyState.classList.remove('hidden');
803
+ var message = options.emptyState.querySelector('p');
804
+ if (message) message.textContent = options.message || 'Content could not be loaded right now.';
805
+ options.grid.insertBefore(options.emptyState, options.noResults || null);
806
+ }
807
+
808
+ async function renderRichContent(markdown, frontMatter) {
809
+ var fm = frontMatter || {};
810
+ var index = await ensureXciteIndex();
811
+ var bibIndex = await ensureBibliographyIndex();
812
+ var assignPrep = preprocessTranscriptionAssigns(stripFrontMatter(markdown));
813
+ var prepared = assignPrep.text;
814
+ var transcriptionAssigns = assignPrep.assignMap;
815
+ var replacements = [];
816
+ prepared = prepared.replace(/\{%-?\s*include\s+transcription\.html\s+([\s\S]*?)-?%\}/g, function (match, raw) {
817
+ var replacementIndex = replacements.length;
818
+ var attrs = parseIncludeAttrs(raw);
819
+ var path = resolveTranscriptionPath(attrs, fm, transcriptionAssigns);
820
+ replacements.push(path ? loadJson(path).then(function (data) { return transcriptionHtml(data, attrs); }) : Promise.resolve(''));
821
+ return '%%CONTENT_RUNTIME_REPLACEMENT_' + replacementIndex + '%%';
822
+ });
823
+ prepared = prepared.replace(/\{%-?\s*include\s+chord-tab\.html\s+([\s\S]*?)-?%\}/g, function (match, raw) {
824
+ var replacementIndex = replacements.length;
825
+ var attrs = parseIncludeAttrs(raw);
826
+ var path = normalizeDataPath(attrs.path || attrs.file || attrs.src || attrs.tab, 'data/tabs');
827
+ replacements.push(path ? loadJson(path).then(chordTabHtml) : Promise.resolve(''));
828
+ return '%%CONTENT_RUNTIME_REPLACEMENT_' + replacementIndex + '%%';
829
+ });
830
+ var citationPrep = preprocessScholarCites(prepared, bibIndex);
831
+ var html = renderMarkdown(citationPrep.text, {
832
+ xciteIndex: index,
833
+ bibIndex: bibIndex,
834
+ citedKeys: citationPrep.citedKeys,
835
+ lang: fm.language
836
+ });
837
+ html = resolveXcitePlaceholders(html, index);
838
+ var resolved = await Promise.all(replacements);
839
+ resolved.forEach(function (replacement, index) {
840
+ html = html.replace('<p>%%CONTENT_RUNTIME_REPLACEMENT_' + index + '%%</p>', replacement);
841
+ html = html.replace('%%CONTENT_RUNTIME_REPLACEMENT_' + index + '%%', replacement);
842
+ });
843
+ if (fm.tab && !/\{%-?\s*include\s+chord-tab\.html/.test(String(markdown || ''))) {
844
+ try {
845
+ html += chordTabHtml(await loadJson(normalizeDataPath(fm.tab, 'data/tabs')));
846
+ } catch (e) {
847
+ console.error(e);
848
+ }
849
+ }
850
+ return html;
851
+ }
852
+
853
+ if (document.readyState === 'loading') {
854
+ document.addEventListener('DOMContentLoaded', function () {
855
+ bindProseSizeControls(document);
856
+ });
857
+ } else {
858
+ bindProseSizeControls(document);
859
+ }
860
+
861
+ {% endraw %}