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,534 @@
1
+ ---
2
+ layout: shell
3
+ ---
4
+
5
+ <main class="writing-index{% if page.accent and page.accent != 'blue' %} page-accent--{{ page.accent }}{% endif %} relative max-w-7xl mx-auto px-4 sm:px-8 md:px-16 pt-10 pb-16"
6
+ {% include page/listing-runtime-attrs.html
7
+ default_collection="articles"
8
+ default_path="/pages/articles/"
9
+ default_detail="/articles/:slug/" %}>
10
+
11
+ {%- comment -%}
12
+ Title, header tags and the optional standfirst all come from the
13
+ `articles` entry of content/pages/headers.yml, the one file that carries
14
+ every page title. The standfirst slot is declared by the
15
+ shared include and stays hidden until that file declares one, so nothing
16
+ is invented here to fill it.
17
+ {%- endcomment -%}
18
+ {% include page/page-header.html key=page.key
19
+ header_class="writing-masthead"
20
+ tags_class="writing-masthead-tags code-technical block hidden" %}
21
+
22
+ <div id="articles-listing">
23
+
24
+ {%- comment -%}
25
+ Featured panel. The entry is whichever article runtime.pickFeatured drew
26
+ for this browsing session — a random pick from the articles that carry a
27
+ thumbnail, remembered in session storage so the home page leads on the
28
+ same piece and paging through the archive below does not reshuffle it.
29
+ No article carries a thumbnail, no panel: the panel is built around an
30
+ image column, and one that reserves space for a picture that does not
31
+ exist is worse than no panel at all.
32
+ {%- endcomment -%}
33
+ <div id="articles-lead"></div>
34
+
35
+ <div class="writing-body">
36
+
37
+ {%- comment -%}
38
+ The filter is a rail beside the archive, not a band above it. Six tags
39
+ plus the order and the keyword box read as a list in a column, they
40
+ stay visible while the archive is scanned, and they take the column the
41
+ mockup filled with invented marginalia. Below 1024px the rail collapses
42
+ into the shared disclosure — trigger plus panel, exactly as
43
+ /pages/sources/ — so a phone is not made to scroll past the controls.
44
+ The controller is the shared ListingFilter either way: same tag facet,
45
+ same ?tag= URLs, same keyword query, same sort.
46
+
47
+ This is the sidebar both writing listings wear; /pages/blog/ emits the
48
+ same aside with its own labels. Anything added here has to make sense
49
+ on a Portuguese notebook as well as on an English archive, which is why
50
+ the last group is the licence: the terms are a fact about the corpus,
51
+ not about either page.
52
+ {%- endcomment -%}
53
+ <aside class="writing-rail listing-filter" data-listing-filter-root hidden>
54
+ <button type="button" class="writing-rail-trigger listing-filter-trigger"
55
+ data-listing-filter-trigger aria-expanded="false"
56
+ aria-controls="writing-filter-panel">
57
+ <span class="material-symbols-outlined" aria-hidden="true">filter_list</span>
58
+ <span class="font-label writing-rail-trigger-label">Filter</span>
59
+ </button>
60
+
61
+ <div id="writing-filter-panel" class="writing-rail-panel" data-listing-filter-panel hidden>
62
+ <p class="listing-filter-group-label font-label text-on-surface-variant">Keyword</p>
63
+ <label class="writing-rail-search">
64
+ <span class="sr-only">Filter articles by keyword</span>
65
+ <input type="search" class="listing-filter-search writing-rail-input font-body"
66
+ data-listing-filter-q placeholder="Title, summary, tag…" autocomplete="off">
67
+ <span class="material-symbols-outlined writing-rail-search-icon" aria-hidden="true">search</span>
68
+ </label>
69
+ <div data-listing-filter-chips></div>
70
+ </div>
71
+
72
+ {%- comment -%}
73
+ Citation and reuse. Filled from the export manifest's licence record
74
+ and nothing else — no declared licence, no block.
75
+ {%- endcomment -%}
76
+ <section class="writing-rail-licence" data-writing-licence hidden></section>
77
+ </aside>
78
+
79
+ <div class="writing-main">
80
+ {%- comment -%}
81
+ Both halves of this line are read off the live list: the ordering is
82
+ the active sort and the tally is the length of the filtered
83
+ selection. Neither is a standing claim about the archive, and neither
84
+ is authored anywhere.
85
+ {%- endcomment -%}
86
+ <div class="writing-archive-head">
87
+ <h2 class="writing-archive-title code-technical">Archive</h2>
88
+ <span class="writing-archive-note code-technical" data-writing-order></span>
89
+ <span class="writing-archive-tally code-technical" data-writing-tally></span>
90
+ </div>
91
+
92
+ <div class="writing-archive" id="articles-list">
93
+ <section id="empty-state" class="writing-archive-state">
94
+ <p class="font-body text-on-surface-variant italic">Loading articles...</p>
95
+ </section>
96
+ <section id="no-results" class="writing-archive-state hidden">
97
+ <p class="font-body text-on-surface-variant italic">Nothing matches these filters yet.</p>
98
+ </section>
99
+ </div>
100
+
101
+ <div id="pagination-controls" class="writing-pagination hidden" aria-label="Article pages"></div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <div id="articles-detail" class="hidden">
107
+ <div class="grid grid-cols-1 gap-8 lg:gap-12 items-start text-detail-grid">
108
+ <aside id="articles-detail-sidebar"></aside>
109
+ <div id="articles-detail-main"></div>
110
+ </div>
111
+ </div>
112
+
113
+ </main>
114
+ {% include mathjax.html %}
115
+
116
+ <script>
117
+ (function () {
118
+ 'use strict';
119
+
120
+ var runtime = window.ContentRuntime;
121
+ if (!runtime || !runtime.ListingFilter) return;
122
+
123
+ var configRoot = document.querySelector('[data-listing-collection]');
124
+ var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'articles';
125
+ var listingPath = (configRoot && configRoot.getAttribute('data-listing-path')) || '/pages/articles/';
126
+ var detailPattern = (configRoot && configRoot.getAttribute('data-listing-detail')) || '/articles/:slug/';
127
+ if (listingPath.slice(-1) !== '/') listingPath += '/';
128
+
129
+ var listing = runtime.Listing;
130
+ var perPage = parseInt('{{ site.posts_per_page | default: 6 }}', 10);
131
+ var params = new URLSearchParams(window.location.search);
132
+ var selectedArticle = runtime.contentSlug('article') ||
133
+ runtime.slugFromDetailPattern(window.location.pathname, detailPattern);
134
+ var currentPage = Math.max(1, parseInt(params.get('page') || '1', 10));
135
+
136
+ var grid = document.getElementById('articles-list');
137
+ var emptyState = document.getElementById('empty-state');
138
+ var noResults = document.getElementById('no-results');
139
+ var controls = document.getElementById('pagination-controls');
140
+ var listingRoot = document.getElementById('articles-listing');
141
+ var leadRoot = document.getElementById('articles-lead');
142
+ var orderNote = document.querySelector('[data-writing-order]');
143
+ var tallyNote = document.querySelector('[data-writing-tally]');
144
+ var detailRoot = document.getElementById('articles-detail');
145
+ var detailSidebar = document.getElementById('articles-detail-sidebar');
146
+ var detailMain = document.getElementById('articles-detail-main');
147
+ var pageHeader = document.querySelector('[data-content-page-header="{{ page.key }}"]');
148
+ var filterRoot = document.querySelector('[data-listing-filter-root]');
149
+ var licenceEl = document.querySelector('[data-writing-licence]');
150
+
151
+ var SORT_LABELS = {
152
+ updated: 'Order: last revised',
153
+ published: 'Order: date published'
154
+ };
155
+
156
+ var allArticles = [];
157
+ var filterUi = null;
158
+
159
+ /* Drawn once, from the whole archive rather than the current selection:
160
+ * the point of the session-held pick is that the home page and this page
161
+ * agree, and they only can if both draw from the same pool. Recomputing it
162
+ * per filter would also let a tag whose entries have no thumbnail
163
+ * overwrite the stored slug and change what the home page leads with. */
164
+ var featured = null;
165
+
166
+ function entryUrl(slug) {
167
+ return runtime.detailUrl(detailPattern, slug);
168
+ }
169
+
170
+ function tagHref(tag) {
171
+ return listingPath + '?tag=' + encodeURIComponent(tag);
172
+ }
173
+
174
+ function loadArticles() {
175
+ return runtime.loadWritingIndex(collection);
176
+ }
177
+
178
+ function sortItems(a, b, sortKey) {
179
+ if (sortKey === 'random') {
180
+ var ak = (a && (a.slug || a.key)) || '';
181
+ var bk = (b && (b.slug || b.key)) || '';
182
+ return runtime.sessionRandomCompare(ak, bk, collection);
183
+ }
184
+ if (sortKey === 'updated') {
185
+ var au = (a.front_matter && a.front_matter.last_updated) || a.last_updated || (a.front_matter && a.front_matter.date) || a.date || '';
186
+ var bu = (b.front_matter && b.front_matter.last_updated) || b.last_updated || (b.front_matter && b.front_matter.date) || b.date || '';
187
+ return String(bu).localeCompare(String(au));
188
+ }
189
+ var ad = (a.front_matter && a.front_matter.date) || a.date || '';
190
+ var bd = (b.front_matter && b.front_matter.date) || b.date || '';
191
+ return String(bd).localeCompare(String(ad));
192
+ }
193
+
194
+ /* Tab counts are computed from the index, never authored, so a tab can
195
+ * never promise more entries than the archive holds. */
196
+ function tagCounts(articles) {
197
+ var counts = {};
198
+ articles.forEach(function (article) {
199
+ var list = (article.front_matter && article.front_matter.tags) || [];
200
+ (Array.isArray(list) ? list : []).forEach(function (tag) {
201
+ if (!tag) return;
202
+ counts[tag] = (counts[tag] || 0) + 1;
203
+ });
204
+ });
205
+ return counts;
206
+ }
207
+
208
+ function buildChipGroups(tags, counts, total) {
209
+ return [
210
+ {
211
+ key: 'tag',
212
+ label: 'Topic',
213
+ options: [{ value: null, label: 'All (' + total + ')' }].concat(tags.map(function (tag) {
214
+ return { value: tag, label: tag + ' (' + counts[tag] + ')' };
215
+ }))
216
+ },
217
+ {
218
+ key: 'sort',
219
+ label: 'Order by',
220
+ options: [
221
+ { value: 'published', label: 'Date published' },
222
+ { value: 'updated', label: 'Last revised' },
223
+ { value: 'random', label: 'Random' }
224
+ ]
225
+ }
226
+ ];
227
+ }
228
+
229
+ function visibleArticles(state) {
230
+ return runtime.ListingFilter.filterItems(allArticles, state, { sortItems: sortItems });
231
+ }
232
+
233
+ function thumbnailOf(article) {
234
+ var value = (article && article.front_matter && article.front_matter.thumbnail) || '';
235
+ return String(value).trim();
236
+ }
237
+
238
+ /* The default order is by revision, so a list showing publication dates
239
+ * alone reads as unsorted. `last_updated` is a real exported field: show
240
+ * it beside the publication date whenever the two differ, and nothing at
241
+ * all when they do not. */
242
+ function revisionPart(fm) {
243
+ var revised = runtime.formatDate(fm.last_updated, 'en');
244
+ if (!revised || revised === runtime.formatDate(fm.date, 'en')) return '';
245
+ return '<span class="writing-revised">Revised ' + runtime.escapeHtml(revised) + '</span>';
246
+ }
247
+
248
+ function setHidden(el, hidden) {
249
+ if (!el) return;
250
+ el.classList.toggle('hidden', !!hidden);
251
+ }
252
+
253
+ function renderListing(state) {
254
+ Array.from(grid.querySelectorAll('[data-dynamic-article]')).forEach(function (el) { el.remove(); });
255
+ if (emptyState) emptyState.remove();
256
+ if (leadRoot) leadRoot.innerHTML = '';
257
+
258
+ var visible = visibleArticles(state);
259
+ if (orderNote) orderNote.textContent = SORT_LABELS[state.sort] || SORT_LABELS.updated;
260
+ if (tallyNote) {
261
+ tallyNote.textContent = visible.length === 1 ? '1 entry' : visible.length + ' entries';
262
+ }
263
+
264
+ if (visible.length === 0) {
265
+ setHidden(noResults, false);
266
+ if (controls) controls.classList.add('hidden');
267
+ return;
268
+ }
269
+ setHidden(noResults, true);
270
+
271
+ /* The featured entry is dropped from the archive so nothing is listed
272
+ * twice — but only when the current selection actually contains it. The
273
+ * pick is a standing one, held for the session, so a tag filter that
274
+ * excludes it leaves the panel alone and takes nothing out of the list.
275
+ * Later pages are archive only. */
276
+ var rest = visible.slice();
277
+ if (featured) {
278
+ var at = rest.findIndex(function (item) { return item.slug === featured.slug; });
279
+ if (at !== -1) rest.splice(at, 1);
280
+ }
281
+
282
+ var totalPages = Math.max(1, Math.ceil(rest.length / perPage));
283
+ if (currentPage > totalPages) currentPage = totalPages;
284
+ if (currentPage === 1) renderLead(featured);
285
+
286
+ rest.slice((currentPage - 1) * perPage, currentPage * perPage).forEach(renderRow);
287
+ listing.renderPagination(controls, currentPage, totalPages);
288
+ }
289
+
290
+ /* A thumbnail that 404s would leave a hole where the image column is, so
291
+ * the frame removes itself and the panel or row falls back to the
292
+ * typographic layout it has without one. */
293
+ function dropPlateOnError(scope, frame, illustratedClass) {
294
+ var image = frame && frame.querySelector('img');
295
+ if (!image) return;
296
+ image.addEventListener('error', function () {
297
+ frame.remove();
298
+ scope.classList.remove(illustratedClass);
299
+ });
300
+ }
301
+
302
+ /* Lead panel. Binds title, date, revision, tags, description and the
303
+ * thumbnail — the same strip as the rows below it, language pill included
304
+ * in the omission. The mockup's topology figure, its DOI/arXiv line and its
305
+ * "Critical Notes" list have no field beNewsreader them, so none is rendered.
306
+ *
307
+ * The panel reads down the left and stops on the action: kicker, ledger
308
+ * line, title, summary, tags, then "Read article" — with the plate holding
309
+ * the right. The action used to sit on the same line as the tag chips,
310
+ * hard right, which put the one thing to press furthest from the sentence
311
+ * that argues for pressing it. */
312
+ function renderLead(article) {
313
+ if (!leadRoot || !article) return;
314
+ var fm = article.front_matter || {};
315
+ var href = entryUrl(article.slug);
316
+ var thumbnail = thumbnailOf(article);
317
+ var meta = runtime.renderMetaStrip([
318
+ runtime.formatDate(fm.date, 'en'),
319
+ revisionPart(fm)
320
+ ]);
321
+
322
+ var html = '<article class="writing-lead' + (thumbnail ? ' writing-lead--illustrated' : '') + '">';
323
+ html += '<span class="shard-accent writing-lead-shard" aria-hidden="true"></span>';
324
+ html += '<span class="shard-accent shard-accent-beta writing-lead-shard writing-lead-shard--beta" aria-hidden="true"></span>';
325
+ html += '<div class="writing-lead-inner">';
326
+ html += '<div class="writing-lead-body">';
327
+ html += '<div class="writing-lead-rail">';
328
+ html += '<span class="ledger-chip writing-lead-featured">Featured</span>';
329
+ if (meta) html += meta;
330
+ html += '</div>';
331
+ /* Thin category rule under the meta strip — accent hairline only; tags
332
+ remain in the foot chips. */
333
+ html += '<div class="writing-lead-category-bar" aria-hidden="true"></div>';
334
+ html += '<h2 class="section-title writing-lead-title"><a href="' +
335
+ runtime.escapeAttr(href) + '">' +
336
+ runtime.escapeHtml(fm.title || article.slug) + '</a></h2>';
337
+
338
+ var preview = runtime.listingPreview(article);
339
+ if (preview) {
340
+ html += '<p class="writing-lead-copy font-body">' +
341
+ runtime.escapeHtml(preview) + '</p>';
342
+ }
343
+
344
+ var chips = runtime.renderTagChips(fm.tags, { hrefFor: tagHref });
345
+ var cta = runtime.renderCtaLink(href, 'Read article', { className: 'writing-lead-cta' });
346
+ html += '<div class="writing-lead-foot">' + chips + cta + '</div>';
347
+ html += '</div>';
348
+
349
+ if (thumbnail) {
350
+ html += '<div class="writing-lead-media">' +
351
+ runtime.renderPosterMedia(runtime.buildUrl(thumbnail), fm.title || article.slug) +
352
+ '</div>';
353
+ }
354
+
355
+ html += '</div></article>';
356
+ leadRoot.innerHTML = html;
357
+
358
+ var panel = leadRoot.querySelector('.writing-lead');
359
+ if (panel) {
360
+ dropPlateOnError(panel, panel.querySelector('.writing-lead-media'), 'writing-lead--illustrated');
361
+ }
362
+ }
363
+
364
+ /* Archive entry. The shared ledger row carries exactly the fields that
365
+ * exist: title, description, tag chips, link, and the strip this page
366
+ * hands it — the date it was published and the day it was last revised.
367
+ * The language pill is not among them: every article here is in English,
368
+ * so on this page the pill separated the entries that declare the field
369
+ * from the entries that do not, which is a fact about the front matter
370
+ * and not about the writing. The ordinal, the reading time and the
371
+ * "MONOGRAPH #42" stamp the mockup drew around it are backed by nothing
372
+ * and are gone. The plate on the right is the `thumbnail` field, and only
373
+ * the entries that declare one get it. */
374
+ function renderRow(article) {
375
+ var fm = article.front_matter || {};
376
+ var row = runtime.renderLedgerRow(article, {
377
+ href: entryUrl(article.slug),
378
+ locale: 'en',
379
+ tagHref: tagHref,
380
+ ctaLabel: 'Read article',
381
+ metaParts: [
382
+ runtime.formatDate(fm.date, 'en'),
383
+ revisionPart(fm)
384
+ ]
385
+ });
386
+ row.setAttribute('data-dynamic-article', 'true');
387
+
388
+ var thumbnail = thumbnailOf(article);
389
+ if (thumbnail) {
390
+ var body = document.createElement('div');
391
+ body.className = 'writing-row-body';
392
+ while (row.firstChild) body.appendChild(row.firstChild);
393
+ row.appendChild(body);
394
+ row.insertAdjacentHTML('beforeend',
395
+ runtime.renderPosterMedia(runtime.buildUrl(thumbnail), fm.title || article.slug));
396
+ row.classList.add('writing-row--illustrated');
397
+ dropPlateOnError(row, row.querySelector('.ledger-poster'), 'writing-row--illustrated');
398
+ }
399
+
400
+ grid.insertBefore(row, noResults);
401
+ }
402
+
403
+ function showListing() {
404
+ if (listingRoot) listingRoot.classList.remove('hidden');
405
+ if (detailRoot) detailRoot.classList.add('hidden');
406
+ if (pageHeader) pageHeader.classList.remove('hidden');
407
+ if (filterUi) filterUi.setVisible(true);
408
+ if (detailMain) detailMain.innerHTML = '';
409
+ if (detailSidebar) detailSidebar.innerHTML = '';
410
+ runtime.removeRuntimeBackLink();
411
+ runtime.removeArticleToc();
412
+ }
413
+
414
+ async function renderArticleDetail(article, articles) {
415
+ showListing();
416
+ if (listingRoot) listingRoot.classList.add('hidden');
417
+ if (pageHeader) pageHeader.classList.add('hidden');
418
+ if (filterUi) filterUi.setVisible(false);
419
+ if (detailRoot) detailRoot.classList.remove('hidden');
420
+
421
+ var fm = article.front_matter || {};
422
+ var lang = fm.language || 'enus';
423
+
424
+ if (detailSidebar) {
425
+ detailSidebar.innerHTML = runtime.buildTextSidebar({
426
+ frontMatter: fm,
427
+ fallbackTitle: article.slug,
428
+ dateHtml: runtime.renderDateEvolution(fm.date, fm.last_updated, 'en')
429
+ });
430
+ }
431
+
432
+ var index = (articles || []).findIndex(function (item) { return item.slug === article.slug; });
433
+
434
+ await runtime.renderRuntimeDetailSection({
435
+ detailAttribute: 'data-dynamic-article-detail',
436
+ bodyMarkdown: article.body_markdown,
437
+ frontMatter: fm,
438
+ mainColumn: detailMain,
439
+ sidebarEl: detailSidebar,
440
+ footerNav: {
441
+ lang: lang,
442
+ previousUrl: index >= 0 && index < articles.length - 1 ? entryUrl(articles[index + 1].slug) : null,
443
+ nextUrl: index > 0 ? entryUrl(articles[index - 1].slug) : null
444
+ }
445
+ });
446
+ }
447
+
448
+ function initFilter() {
449
+ // One entry per tag actually in use, so the rail never offers a filter
450
+ // that would return nothing.
451
+ var tags = Array.from(new Set(allArticles.reduce(function (acc, article) {
452
+ var list = (article.front_matter && article.front_matter.tags) || [];
453
+ return acc.concat(Array.isArray(list) ? list : []);
454
+ }, []).filter(Boolean))).sort();
455
+
456
+ filterUi = runtime.ListingFilter.create({
457
+ root: filterRoot,
458
+ initialQuery: params.get('q') || '',
459
+ initialTag: params.get('tag') || params.get('category') || null,
460
+ initialSort: params.get('sort') || 'published',
461
+ defaultSort: 'published',
462
+ tagParam: 'tag',
463
+ chipGroups: buildChipGroups(tags, tagCounts(allArticles), allArticles.length),
464
+ onChange: function (state) {
465
+ currentPage = 1;
466
+ renderListing(state);
467
+ }
468
+ });
469
+ if (filterRoot) filterRoot.hidden = false;
470
+ renderListing(filterUi.getState());
471
+ }
472
+
473
+ /* Citation and reuse, the rail's last group. Every word is the exported
474
+ * licence record; the page states no terms of its own, and a manifest
475
+ * without a licence leaves no block. Twin of the rascunhos copy — the two
476
+ * listings wear the same sidebar, each in its own language. */
477
+ function mountLicence(manifest) {
478
+ if (!licenceEl) return;
479
+ var licence = manifest && manifest.content_license;
480
+ if (!licence || !licence.name) return;
481
+
482
+ var parts = ['<span class="writing-rail-licence-label font-label">Licensing &amp; reuse</span>'];
483
+ var named = licence.id ? licence.name + ' (' + licence.id + ')' : licence.name;
484
+ parts.push('<p class="writing-rail-licence-copy font-body">Articles published under ' +
485
+ runtime.escapeHtml(named) + '.</p>');
486
+ if (licence.rights_holder) {
487
+ parts.push('<p class="writing-rail-licence-copy font-body">Attribution to ' +
488
+ runtime.escapeHtml(licence.rights_holder) + '.</p>');
489
+ }
490
+ var href = licence.page_path || licence.url;
491
+ if (href) parts.push(runtime.renderCtaLink(href, 'Licence terms'));
492
+
493
+ licenceEl.innerHTML = parts.join('');
494
+ licenceEl.hidden = false;
495
+ }
496
+
497
+ function mountRailLicence() {
498
+ return runtime.loadJson('manifest.json')
499
+ .then(mountLicence)
500
+ .catch(function (error) { console.error(error); });
501
+ }
502
+
503
+ loadArticles().then(function (items) {
504
+ allArticles = items.slice();
505
+
506
+ var selected = selectedArticle && runtime.resolveWritingAlias(allArticles, selectedArticle);
507
+ if (selectedArticle) {
508
+ if (selected && selected.slug && selected.slug !== selectedArticle) {
509
+ runtime.adoptCanonicalDetailUrl(selected);
510
+ }
511
+ var slugToLoad = selected ? selected.slug : selectedArticle;
512
+ return runtime.loadCollectionItem(collection, slugToLoad).then(function (article) {
513
+ return renderArticleDetail(article, allArticles);
514
+ }).catch(function (error) {
515
+ runtime.reportDetailLoadError(error, {
516
+ emptyState: emptyState,
517
+ grid: grid,
518
+ noResults: noResults,
519
+ message: 'This article could not be loaded right now.'
520
+ });
521
+ });
522
+ }
523
+
524
+ featured = runtime.pickFeatured(allArticles);
525
+ initFilter();
526
+ return mountRailLicence();
527
+ }).catch(function (error) {
528
+ console.error(error);
529
+ if (emptyState) {
530
+ emptyState.querySelector('p').textContent = 'Articles could not be loaded right now.';
531
+ }
532
+ });
533
+ })();
534
+ </script>