pandorga 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +77 -0
  3. data/DESIGN.md +1434 -0
  4. data/LICENSE +21 -0
  5. data/README.md +81 -0
  6. data/_data/themes/dark.yml +97 -0
  7. data/_data/themes/light.yml +120 -0
  8. data/_data/themes/shared.yml +37 -0
  9. data/_data/translations.yml +85 -0
  10. data/_includes/chord-tab.html +38 -0
  11. data/_includes/content-runtime/00-config.html +22 -0
  12. data/_includes/content-runtime/10-utils.html +176 -0
  13. data/_includes/content-runtime/20-markdown.html +264 -0
  14. data/_includes/content-runtime/25-code-blocks.html +315 -0
  15. data/_includes/content-runtime/30-includes-bib.html +190 -0
  16. data/_includes/content-runtime/30-includes-xcite.html +122 -0
  17. data/_includes/content-runtime/30-includes.html +365 -0
  18. data/_includes/content-runtime/40-listing.html +225 -0
  19. data/_includes/content-runtime/41-listing-helpers.html +71 -0
  20. data/_includes/content-runtime/42-listing-filter.html +208 -0
  21. data/_includes/content-runtime/43-blocks.html +379 -0
  22. data/_includes/content-runtime/50-detail.html +861 -0
  23. data/_includes/content-runtime/55-toc.html +176 -0
  24. data/_includes/content-runtime/60-fragments.html +567 -0
  25. data/_includes/content-runtime/90-bootstrap.html +76 -0
  26. data/_includes/cut-in.html +10 -0
  27. data/_includes/cv/cv-box.html +18 -0
  28. data/_includes/dict-card.html +24 -0
  29. data/_includes/figure.html +36 -0
  30. data/_includes/home/band-index.html +43 -0
  31. data/_includes/home/band-listing.html +49 -0
  32. data/_includes/mathjax.html +10 -0
  33. data/_includes/ongoing-product.html +1 -0
  34. data/_includes/page/content-runtime.html +20 -0
  35. data/_includes/page/date_evolution.html +19 -0
  36. data/_includes/page/figure-lightbox.html +102 -0
  37. data/_includes/page/formatted_date.html +15 -0
  38. data/_includes/page/hero-default.html +18 -0
  39. data/_includes/page/listing-runtime-attrs.html +8 -0
  40. data/_includes/page/mermaid.html +101 -0
  41. data/_includes/page/network-graph.html +1253 -0
  42. data/_includes/page/page-header.html +41 -0
  43. data/_includes/page/professional-connections.html +2 -0
  44. data/_includes/page/site-footer.html +101 -0
  45. data/_includes/page/timeline-section.html +868 -0
  46. data/_includes/plotly.html +22 -0
  47. data/_includes/theme/dev-reexport.html +100 -0
  48. data/_includes/theme/favicons.html +4 -0
  49. data/_includes/theme/font-loader.html +55 -0
  50. data/_includes/theme/shell-theme-boot.html +115 -0
  51. data/_includes/theme/site-logo.html +6 -0
  52. data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
  53. data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
  54. data/_includes/theme/theme-context.liquid +20 -0
  55. data/_includes/theme/theme-toggle.html +94 -0
  56. data/_includes/theme/theme-vars-block.liquid +161 -0
  57. data/_includes/theme/theme-vars.html +16 -0
  58. data/_includes/transcription.html +80 -0
  59. data/_includes/tweet.html +22 -0
  60. data/_includes/video.html +11 -0
  61. data/_includes/xcite.html +3 -0
  62. data/_includes/youtube-short.html +26 -0
  63. data/_includes/youtube.html +12 -0
  64. data/_layouts/article.html +27 -0
  65. data/_layouts/articles.html +534 -0
  66. data/_layouts/bibliography.html +417 -0
  67. data/_layouts/blog.html +544 -0
  68. data/_layouts/cv.html +393 -0
  69. data/_layouts/home.html +319 -0
  70. data/_layouts/network.html +9 -0
  71. data/_layouts/pandorga/articles.html +4 -0
  72. data/_layouts/pandorga/bibliography.html +4 -0
  73. data/_layouts/pandorga/blog.html +4 -0
  74. data/_layouts/pandorga/cv.html +4 -0
  75. data/_layouts/pandorga/media.html +4 -0
  76. data/_layouts/pandorga/network.html +4 -0
  77. data/_layouts/pandorga/portfolio.html +4 -0
  78. data/_layouts/post.html +7 -0
  79. data/_layouts/product.html +5 -0
  80. data/_layouts/project.html +27 -0
  81. data/_layouts/projects.html +534 -0
  82. data/_layouts/resources.html +387 -0
  83. data/_layouts/shell.html +203 -0
  84. data/_layouts/text.html +246 -0
  85. data/_plugins/bibliography_artifact.rb +57 -0
  86. data/_plugins/content_backend.rb +37 -0
  87. data/_plugins/content_collections.rb +51 -0
  88. data/_plugins/content_detail_paths.rb +58 -0
  89. data/_plugins/content_headers.rb +73 -0
  90. data/_plugins/content_keys.rb +310 -0
  91. data/_plugins/content_local_serve.rb +168 -0
  92. data/_plugins/content_site_adapter.rb +159 -0
  93. data/_plugins/content_validators_hook.rb +20 -0
  94. data/_plugins/content_writing_index.rb +52 -0
  95. data/_plugins/content_writing_paths.rb +55 -0
  96. data/_plugins/force_utf8_encoding.rb +13 -0
  97. data/_plugins/git_chronology.rb +211 -0
  98. data/_plugins/git_metadata.rb +43 -0
  99. data/_plugins/job_sections.rb +25 -0
  100. data/_plugins/lib/content_validators.rb +27 -0
  101. data/_plugins/lib/resource_validator.rb +56 -0
  102. data/_plugins/lib/tag_validator.rb +153 -0
  103. data/_plugins/lib/text_excerpt.rb +56 -0
  104. data/_plugins/lib/writing_entry.rb +107 -0
  105. data/_plugins/local_detail_rewrites.rb +194 -0
  106. data/_plugins/timeline.rb +507 -0
  107. data/_plugins/writing_slug_limits.rb +70 -0
  108. data/assets/css/article.css +1330 -0
  109. data/assets/css/base.css +1432 -0
  110. data/assets/css/cv.css +13 -0
  111. data/assets/css/global-link.css +94 -0
  112. data/assets/css/ledger/bibliography.css +307 -0
  113. data/assets/css/ledger/chrome.css +118 -0
  114. data/assets/css/ledger/cv.css +836 -0
  115. data/assets/css/ledger/detail.css +1270 -0
  116. data/assets/css/ledger/footer.css +298 -0
  117. data/assets/css/ledger/home.css +1490 -0
  118. data/assets/css/ledger/network.css +106 -0
  119. data/assets/css/ledger/projects.css +716 -0
  120. data/assets/css/ledger/rascunhos.css +379 -0
  121. data/assets/css/ledger/sources.css +327 -0
  122. data/assets/css/ledger/writing.css +754 -0
  123. data/assets/tailwind-play.js +15 -0
  124. data/examples/full/Gemfile +7 -0
  125. data/examples/full/_config.yml +92 -0
  126. data/examples/full/_redirects +3 -0
  127. data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
  128. data/examples/full/content/collections/data/contacts.yml +3 -0
  129. data/examples/full/content/collections/data/education.yml +4 -0
  130. data/examples/full/content/collections/jobs/research-fellow.md +12 -0
  131. data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
  132. data/examples/full/content/collections/products/ledger-tool.md +6 -0
  133. data/examples/full/content/collections/projects/demo-kit.md +10 -0
  134. data/examples/full/content/collections/resources/style-guide.md +7 -0
  135. data/examples/full/content/pages/cv/summary.md +1 -0
  136. data/examples/full/content/pages/headers.yml +20 -0
  137. data/examples/full/index.html +4 -0
  138. data/examples/minimal/Gemfile +7 -0
  139. data/examples/minimal/_config.yml +47 -0
  140. data/examples/minimal/_redirects +3 -0
  141. data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
  142. data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
  143. data/examples/minimal/content/pages/headers.yml +6 -0
  144. data/examples/minimal/index.html +4 -0
  145. data/exe/pandorga +7 -0
  146. data/functions/api/studio/[[path]].js +562 -0
  147. data/functions/api/studio/_lib/auth.js +131 -0
  148. data/functions/api/studio/_lib/blocks.js +156 -0
  149. data/functions/api/studio/_lib/gemini.js +62 -0
  150. data/functions/api/studio/_lib/github.js +278 -0
  151. data/functions/api/studio/_lib/guidelines.js +143 -0
  152. data/functions/api/studio/_lib/paths.js +76 -0
  153. data/functions/api/studio/_lib/prompts.js +165 -0
  154. data/functions/api/studio/_lib/rate-limit.js +40 -0
  155. data/functions/api/studio/_lib/rewrite.js +136 -0
  156. data/functions/api/studio/_lib/sanitize.js +143 -0
  157. data/lib/pandorga/cli.rb +66 -0
  158. data/lib/pandorga/commands/doctor.rb +66 -0
  159. data/lib/pandorga/commands/export.rb +30 -0
  160. data/lib/pandorga/commands/install_functions.rb +41 -0
  161. data/lib/pandorga/commands/new.rb +113 -0
  162. data/lib/pandorga/commands/publish.rb +53 -0
  163. data/lib/pandorga/commands/serve.rb +34 -0
  164. data/lib/pandorga/commands/studio_schema.rb +309 -0
  165. data/lib/pandorga/jekyll/generator.rb +74 -0
  166. data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
  167. data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
  168. data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
  169. data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
  170. data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
  171. data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
  172. data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
  173. data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
  174. data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
  175. data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
  176. data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
  177. data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
  178. data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
  179. data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
  180. data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
  181. data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
  182. data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
  183. data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
  184. data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
  185. data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
  186. data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
  187. data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
  188. data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
  189. data/lib/pandorga/jekyll.rb +15 -0
  190. data/lib/pandorga/object_store_env.rb +51 -0
  191. data/lib/pandorga/registry/schema.yml +115 -0
  192. data/lib/pandorga/registry.rb +279 -0
  193. data/lib/pandorga/version.rb +5 -0
  194. data/lib/pandorga.rb +19 -0
  195. data/scripts/cdn-allowlist.json +19 -0
  196. data/scripts/content/backfill-content-keys.rb +99 -0
  197. data/scripts/content/band_art.rb +362 -0
  198. data/scripts/content/export-content-json.rb +741 -0
  199. data/scripts/content/flatten-ansi-capture.mjs +153 -0
  200. data/scripts/content/hero_portraits.rb +28 -0
  201. data/scripts/content/publish-content-to-object-store.sh +78 -0
  202. data/scripts/content/publish-content-to-r2.sh +5 -0
  203. data/scripts/content/sync-content-json-to-r2.rb +167 -0
  204. data/scripts/content/sync-content-redirects.rb +12 -0
  205. data/scripts/generate-configuration-doc.rb +127 -0
  206. data/scripts/lib/content_license.rb +50 -0
  207. data/scripts/lib/content_storage_config.rb +70 -0
  208. data/scripts/lib/normalize-display-math.mjs +52 -0
  209. data/scripts/lib/r2_json_sync.rb +76 -0
  210. data/scripts/lib/withdrawn_public_objects.rb +23 -0
  211. data/scripts/lib/writing_index_contract.rb +61 -0
  212. data/scripts/pdf/md-to-pdf.mjs +1400 -0
  213. data/scripts/pdf/md-to-pdf.sh +27 -0
  214. data/scripts/test/fixtures/dual-template/_config.yml +32 -0
  215. data/scripts/test/fixtures/dual-template/index.html +4 -0
  216. data/scripts/test/render-parity/article-note.json +5 -0
  217. data/scripts/test/render-parity/blockquote.json +5 -0
  218. data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
  219. data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
  220. data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
  221. data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
  222. data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
  223. data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
  224. data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
  225. data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
  226. data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
  227. data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
  228. data/scripts/test/render-parity/blocks/archive-card.json +32 -0
  229. data/scripts/test/render-parity/blocks/cta-link.json +13 -0
  230. data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
  231. data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
  232. data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
  233. data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
  234. data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
  235. data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
  236. data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
  237. data/scripts/test/render-parity/blocks/section-banner.json +13 -0
  238. data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
  239. data/scripts/test/render-parity/blocks/system-card.json +48 -0
  240. data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
  241. data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
  242. data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
  243. data/scripts/test/render-parity/cut-in-right.json +9 -0
  244. data/scripts/test/render-parity/cut-in.json +8 -0
  245. data/scripts/test/render-parity/figure.json +5 -0
  246. data/scripts/test/render-parity/load-runtime.mjs +337 -0
  247. data/scripts/test/render-parity/ongoing-product.json +5 -0
  248. data/scripts/test/render-parity/plotly.json +5 -0
  249. data/scripts/test/render-parity/poem.json +5 -0
  250. data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
  251. data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
  252. data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
  253. data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
  254. data/scripts/test/render-parity/run-fixture.mjs +159 -0
  255. data/scripts/test/render-parity/selectors.mjs +43 -0
  256. data/scripts/test/render-parity/video.json +5 -0
  257. data/scripts/test/render-parity/youtube-short.json +10 -0
  258. data/scripts/test/render-parity/youtube.json +8 -0
  259. data/scripts/test/test-absent-field-absent-element.rb +43 -0
  260. data/scripts/test/test-cdn-pins.rb +44 -0
  261. data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
  262. data/scripts/test/test-configuration-doc.rb +36 -0
  263. data/scripts/test/test-content-detail-urls.rb +346 -0
  264. data/scripts/test/test-content-keys.rb +110 -0
  265. data/scripts/test/test-content-license.rb +45 -0
  266. data/scripts/test/test-content-render-parity.rb +38 -0
  267. data/scripts/test/test-content-validators.rb +12 -0
  268. data/scripts/test/test-css-structure.rb +72 -0
  269. data/scripts/test/test-cv-export-contract.rb +158 -0
  270. data/scripts/test/test-detail-render-parity.rb +40 -0
  271. data/scripts/test/test-dev-reexport.rb +122 -0
  272. data/scripts/test/test-doctor.rb +39 -0
  273. data/scripts/test/test-dual-template-instances.rb +87 -0
  274. data/scripts/test/test-dual-template-runtime.rb +167 -0
  275. data/scripts/test/test-examples-build.rb +60 -0
  276. data/scripts/test/test-export-content-json-incremental.rb +138 -0
  277. data/scripts/test/test-export.rb +53 -0
  278. data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
  279. data/scripts/test/test-form-control-theming.rb +143 -0
  280. data/scripts/test/test-git-chronology.rb +414 -0
  281. data/scripts/test/test-hero-portraits.mjs +97 -0
  282. data/scripts/test/test-hero-portraits.rb +162 -0
  283. data/scripts/test/test-home-band-art.rb +135 -0
  284. data/scripts/test/test-install-functions.rb +39 -0
  285. data/scripts/test/test-language-export.rb +79 -0
  286. data/scripts/test/test-listing-filter-contract.rb +119 -0
  287. data/scripts/test/test-math-row-breaks.mjs +79 -0
  288. data/scripts/test/test-math-row-breaks.rb +18 -0
  289. data/scripts/test/test-new.rb +113 -0
  290. data/scripts/test/test-no-personal-data.rb +85 -0
  291. data/scripts/test/test-public-cv-export.rb +164 -0
  292. data/scripts/test/test-r2-config-consistency.rb +57 -0
  293. data/scripts/test/test-r2-json-sync.rb +193 -0
  294. data/scripts/test/test-registry-home.rb +34 -0
  295. data/scripts/test/test-runtime-js-syntax.rb +64 -0
  296. data/scripts/test/test-slim-writing-index.rb +126 -0
  297. data/scripts/test/test-studio-api-contract.rb +99 -0
  298. data/scripts/test/test-studio-content-files.rb +79 -0
  299. data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
  300. data/scripts/test/test-studio-rate-limit.mjs +70 -0
  301. data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
  302. data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
  303. data/scripts/test/test-studio-schema-composition.rb +126 -0
  304. data/scripts/test/test-studio-slug-config.rb +167 -0
  305. data/scripts/test/test-template-packages.rb +51 -0
  306. data/scripts/test/test-text-excerpt.rb +51 -0
  307. data/scripts/test/test-unwrap-media-src.mjs +39 -0
  308. data/scripts/test/test-unwrap-media-src.rb +19 -0
  309. data/scripts/test/test-withdrawn-content-export.rb +69 -0
  310. data/scripts/test/test-writing-index-export-slugs.rb +84 -0
  311. data/scripts/test/test-writing-slug-limits.rb +61 -0
  312. data/scripts/test/test-xcite-chronology.rb +106 -0
  313. data/scripts/validate.sh +74 -0
  314. data/scripts/visual/build-cv-pages-json.js +33 -0
  315. data/scripts/visual/build-site-pages-json.js +94 -0
  316. data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
  317. data/scripts/visual/take-screenshots.js +88 -0
  318. data/scripts/visual/visual-inspection-pages.json +113 -0
  319. data/studio/assets/index-B_j0pMD5.css +1 -0
  320. data/studio/assets/index-Ch4YEv9g.js +285 -0
  321. data/studio/index.html +27 -0
  322. data/studio/schema.yml +869 -0
  323. data/studio-app/README.md +27 -0
  324. data/studio-app/index.html +26 -0
  325. data/studio-app/package-lock.json +2499 -0
  326. data/studio-app/package.json +26 -0
  327. data/studio-app/skills-lock.json +47 -0
  328. data/studio-app/src/api/client.js +71 -0
  329. data/studio-app/src/editor/body-editor.js +70 -0
  330. data/studio-app/src/editor/liquid-core.js +382 -0
  331. data/studio-app/src/editor/liquid.js +77 -0
  332. data/studio-app/src/lib/document.js +207 -0
  333. data/studio-app/src/lib/list-controls.js +317 -0
  334. data/studio-app/src/lib/schema.js +66 -0
  335. data/studio-app/src/lib/yaml-doc.js +100 -0
  336. data/studio-app/src/main.js +3693 -0
  337. data/studio-app/src/media/browser.js +578 -0
  338. data/studio-app/src/preview/render.js +430 -0
  339. data/studio-app/src/styles/studio.css +2826 -0
  340. data/studio-app/vite.config.js +25 -0
  341. data/templates/articles/README.md +40 -0
  342. data/templates/articles/fixtures/article.md +9 -0
  343. data/templates/articles/screenshots/README.md +6 -0
  344. data/templates/articles/studio.yml +73 -0
  345. data/templates/articles/template.yml +21 -0
  346. data/templates/bibliography/README.md +41 -0
  347. data/templates/bibliography/fixtures/references.json +9 -0
  348. data/templates/bibliography/screenshots/README.md +6 -0
  349. data/templates/bibliography/studio.yml +66 -0
  350. data/templates/bibliography/template.yml +18 -0
  351. data/templates/blog/README.md +38 -0
  352. data/templates/blog/fixtures/post.md +9 -0
  353. data/templates/blog/screenshots/README.md +6 -0
  354. data/templates/blog/studio.yml +69 -0
  355. data/templates/blog/template.yml +20 -0
  356. data/templates/cv/README.md +34 -0
  357. data/templates/cv/fixtures/job.md +12 -0
  358. data/templates/cv/screenshots/README.md +6 -0
  359. data/templates/cv/studio.yml +200 -0
  360. data/templates/cv/template.yml +25 -0
  361. data/templates/media/README.md +38 -0
  362. data/templates/media/fixtures/resource.md +7 -0
  363. data/templates/media/screenshots/README.md +6 -0
  364. data/templates/media/studio.yml +58 -0
  365. data/templates/media/template.yml +17 -0
  366. data/templates/network/README.md +34 -0
  367. data/templates/network/fixtures/sources.json +3 -0
  368. data/templates/network/screenshots/README.md +6 -0
  369. data/templates/network/studio.yml +28 -0
  370. data/templates/network/template.yml +10 -0
  371. data/templates/portfolio/README.md +39 -0
  372. data/templates/portfolio/fixtures/project.md +10 -0
  373. data/templates/portfolio/screenshots/README.md +6 -0
  374. data/templates/portfolio/studio.yml +89 -0
  375. data/templates/portfolio/template.yml +18 -0
  376. metadata +505 -0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Pandorga contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Pandorga
2
+
3
+ [![CI](https://github.com/alexandrelheinen/pandorga/actions/workflows/validate.yml/badge.svg)](https://github.com/alexandrelheinen/pandorga/actions/workflows/validate.yml)
4
+ [![Release](https://img.shields.io/github/v/release/alexandrelheinen/pandorga)](https://github.com/alexandrelheinen/pandorga/releases/latest)
5
+ [![Gem Version](https://img.shields.io/gem/v/pandorga)](https://rubygems.org/gems/pandorga)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ <img src="docs/images/family_link_64dp_5084C1_FILL0_wght400_GRAD0_opsz48.svg" alt="Pandorga Logo" width="120" align="left" style="margin-right: 10%;">
9
+ An aesthetic Jekyll publishing platform for creative and technical writing. The site is your paper, your portfolio, and an extension of your own thoughts.
10
+
11
+ - **Headless and S3-compatible.** Your content lives independently of the Pandorga shell — write, store, and move it on your own terms.
12
+ - **Studio CMS.** Manage everything from quick notes to full diagrams without touching the repo.
13
+ - **Fully configurable.** Portfolio, CV, blog, articles, and bibliography — each with its own presentation.
14
+
15
+ ## Quickstart
16
+
17
+ Scaffold a site (Ada Example defaults, `content.backend: static`):
18
+
19
+ ```bash
20
+ gem install pandorga # once 1.1 is on RubyGems; or use a local checkout
21
+ pandorga new my-site
22
+ cd my-site
23
+ bundle install
24
+ bundle exec pandorga doctor
25
+ bundle exec pandorga serve
26
+ ```
27
+
28
+ Or run the packaged minimal example from a clone:
29
+
30
+ ```bash
31
+ git clone https://github.com/alexandrelheinen/pandorga.git
32
+ cd pandorga
33
+ git submodule update --init .guidelines # agent/prose guidelines (optional for runtime)
34
+ mise install # or use Ruby >= 3.3
35
+ bundle install
36
+ cd examples/minimal && bundle install && cd ../..
37
+ bundle exec pandorga export examples/minimal/content examples/minimal/_content_json
38
+ bundle exec pandorga doctor examples/minimal
39
+ mise exec -- ./scripts/validate.sh
40
+ ```
41
+
42
+ Serve from the example directory:
43
+
44
+ ```bash
45
+ cd examples/minimal
46
+ bundle exec pandorga serve
47
+ ```
48
+
49
+ ## CLI
50
+
51
+ | Command | Purpose |
52
+ |---|---|
53
+ | `pandorga new PATH` | Scaffold a site from `examples/minimal` |
54
+ | `pandorga doctor` | Validate `pandorga:` config |
55
+ | `pandorga export` | Content → JSON (`manifest.json` includes `schema_version`) |
56
+ | `pandorga serve` | Export + `jekyll serve` |
57
+ | `pandorga publish` | S3-compatible object-store sync (`S3_*` or `R2_*`) |
58
+ | `pandorga install-functions` | Copy `functions/` + `studio/` into the site root |
59
+ | `pandorga studio-schema` | Compose Studio schema from templates |
60
+
61
+ ## Documentation
62
+
63
+ | Doc | Topic |
64
+ |---|---|
65
+ | [docs/getting-started.md](docs/getting-started.md) | Minimal example locally |
66
+ | [docs/configuration.md](docs/configuration.md) | `pandorga:` reference |
67
+ | [docs/pages-and-templates.md](docs/pages-and-templates.md) | Registry and seven templates |
68
+ | [docs/theming.md](docs/theming.md) | `_data/themes` and tokens |
69
+ | [docs/extending.md](docs/extending.md) | Site overrides, hero art, Studio overrides |
70
+ | [docs/architecture.md](docs/architecture.md) | Shell, JSON, runtime, Studio |
71
+ | [docs/upgrading.md](docs/upgrading.md) | Semver, `schema_version`, gem pins |
72
+ | [docs/studio.md](docs/studio.md) | Studio env and schema compose |
73
+ | [docs/studio-android.md](docs/studio-android.md) | Optional Expo Android WebView shell |
74
+ | [docs/deploy/static.md](docs/deploy/static.md) | Static backend |
75
+ | [docs/deploy/cloudflare.md](docs/deploy/cloudflare.md) | Pages + object store |
76
+ | [docs/spec.md](docs/spec.md) | Migration plan and acceptance criteria |
77
+ | [docs/decisions.md](docs/decisions.md) | Frozen decisions |
78
+
79
+ ## License
80
+
81
+ MIT — Copyright (c) The Pandorga contributors.
@@ -0,0 +1,97 @@
1
+ # Brazilian Ledger — dark theme (runtime toggle companion to light.yml).
2
+ #
3
+ # 2026-09 vivid pass: same Ledger hues as light, chroma raised so accents
4
+ # read on the near-black ladder (Stitch Home Dark Mode intent: pigment on
5
+ # near-black, not a new triad).
6
+
7
+ colors:
8
+ primary: "#a4c8da"
9
+ secondary: "#95c8a6"
10
+ tertiary: "#ddd8a8"
11
+ primary_container: "#4a5c66"
12
+ secondary_container: "#435c4c"
13
+ tertiary_container: "#6a684f"
14
+ on_primary_container: "#c0d8e6"
15
+ on_secondary_container: "#b5d9c2"
16
+ on_tertiary_container: "#e8e4c0"
17
+ chart_project: "#98c8ac"
18
+
19
+ neutrals:
20
+ on_surface: "#ffffff"
21
+ on_surface_variant: "#b8b7ae"
22
+ on_background: "#ffffff"
23
+ outline: "#93938d"
24
+ # Neutralised with the rest of the ramp: this was #44453f (green highest),
25
+ # and every hairline in the build is an opacity step of it.
26
+ outline_variant: "#45454c"
27
+ background: "#0c0c0e"
28
+ background_secondary: "#141418"
29
+ surface: "#111114"
30
+ surface_container_low: "#17171b"
31
+ surface_container: "#1f1f24"
32
+ surface_container_high: "#26262b"
33
+ surface_variant: "#2e2e34"
34
+ # Architectural Ledger ladder. Depth comes from tonal steps, not shadow:
35
+ # canvas (background) -> panel -> card (transparent at rest) -> hover.
36
+ #
37
+ # Cards rest transparent so the panel/canvas and shards show through; the
38
+ # hover rung still lifts. Widened 2026-09: canvas near-black buys headroom
39
+ # the reference mockup uses (#0a0a0a canvas). Panel and hover keep the
40
+ # ~+4 L* / ~+6 L* steps; the resting card no longer inserts a mid plate.
41
+ panel_contrast: "#141418"
42
+ surface_card: "#0c0c0e"
43
+ surface_card_contrast: "#141418"
44
+ surface_high: "#26262b"
45
+ surface_high_contrast: "#26262b"
46
+
47
+ links:
48
+ default: "#b0d0e0"
49
+ hover: "#c4dce8"
50
+
51
+ # Fonts, motion, and geometry live in shared.yml — colour themes do not
52
+ # redeclare them.
53
+
54
+ static:
55
+ on_primary: "#0f1a22"
56
+ on_secondary: "#141a15"
57
+ on_tertiary: "#1b1c12"
58
+ surface_container_lowest: "#08080a"
59
+ surface_dim: "#060607"
60
+ inverse_primary: "#1c5a7a"
61
+ inverse_surface: "#ffffff"
62
+ inverse_on_surface: "#000000"
63
+ primary_fixed: "#a8c5d4"
64
+ primary_fixed_dim: "#a4c8da"
65
+ secondary_fixed: "#b4d4c0"
66
+ secondary_fixed_dim: "#95c8a6"
67
+ tertiary_fixed: "#d0ceb0"
68
+ tertiary_fixed_dim: "#ddd8a8"
69
+ on_primary_fixed: "#0a1419"
70
+ on_primary_fixed_variant: "#c0d8e6"
71
+ on_secondary_fixed: "#0c1710"
72
+ on_secondary_fixed_variant: "#b5d9c2"
73
+ on_tertiary_fixed: "#17160c"
74
+ on_tertiary_fixed_variant: "#e8e4c0"
75
+ error: "#ffb4ab"
76
+ on_error: "#690005"
77
+ error_container: "#93000a"
78
+ on_error_container: "#ffdad6"
79
+ res_youtube: "#de6461"
80
+ res_podcast: "#5dc462"
81
+ res_doc: "#e4b268"
82
+
83
+ sidebar_thumbnail:
84
+ background: "#1f1f24"
85
+ overlay_gradient_start: "rgba(23, 23, 27, 0.35)"
86
+ overlay_gradient_end: "rgba(12, 12, 14, 0.45)"
87
+
88
+ # Timeline lane palette — see light.yml for the rationale. Same six hues,
89
+ # lifted in lightness and eased in saturation so they carry against the
90
+ # near-black canvas instead of sinking into it. Chroma raised with the triad.
91
+ timeline:
92
+ employment: "#a4c8da" # petrol
93
+ education: "#95c8a6" # forest
94
+ product: "#d5b46e" # tobacco
95
+ project: "#a9a5dd" # indigo
96
+ article: "#da987e" # terracotta
97
+ post: "#7cc7b5" # sage teal
@@ -0,0 +1,120 @@
1
+ # Brazilian Ledger — light theme (production default).
2
+ # Add sibling files (e.g. dark.yml) and set shell_theme.active in _config.yml to switch palettes.
3
+ #
4
+ # 2026-09 vivid pass (second cut): slate / forest / ochre chroma raised again
5
+ # so brand ink reads on white paper without grey wash. Canvas and cards stay
6
+ # pure white; contrast bands are near-white paper rhythm (Stitch "Editorial
7
+ # Detour" intent, without adopting its alabaster #fcf9f6 canvas). Card margins
8
+ # use a darker outline_variant so hairlines read closer to Stitch paper rules.
9
+
10
+ colors:
11
+ primary: "#1c5a7a"
12
+ secondary: "#287445"
13
+ tertiary: "#746f30"
14
+ primary_container: "#a8c5d4"
15
+ secondary_container: "#b4d4c0"
16
+ tertiary_container: "#d0ceb0"
17
+ on_primary_container: "#0a1419"
18
+ on_secondary_container: "#0c1710"
19
+ on_tertiary_container: "#17160c"
20
+ chart_project: "#6fb387"
21
+
22
+ neutrals:
23
+ on_surface: "#000000"
24
+ on_surface_variant: "#5e5d59"
25
+ on_background: "#000000"
26
+ # Hairlines. Every border in the build is an opacity step of
27
+ # `outline_variant`. Stitch-like paper rules need a mid-grey base so the
28
+ # opacity ladder does not vanish on white cards. Neutral grey — no cream
29
+ # warmth and no slate cast in the fills.
30
+ outline: "#5c5c5c"
31
+ outline_variant: "#b8b8b8"
32
+ # Architectural Ledger ladder — white paper edition.
33
+ #
34
+ # Canvas is pure white. Depth is hairline + a near-neutral grey panel band
35
+ # for alternating rhythm. Cards rest transparent (panel/canvas shows
36
+ # through; shards stay visible) and only take a fill on hover. Cream washed
37
+ # ochre into the page; the blue-grey step still read as slate — this ladder
38
+ # is practically grey (equal RGB) so highlights stay quiet paper.
39
+ #
40
+ # Canvas (#ffffff) -> Panel contrast (#f4f4f4) -> Card (transparent)
41
+ # -> Hover (#ebebeb). Contrast-band cards stay transparent too, so they
42
+ # never cut a white plate out of the grey band.
43
+ background: "#ffffff"
44
+ background_secondary: "#f4f4f4"
45
+ panel_contrast: "#f4f4f4"
46
+ surface_card: "#ffffff"
47
+ surface_card_contrast: "#f4f4f4"
48
+ surface_high: "#ebebeb"
49
+ surface_high_contrast: "#e2e2e2"
50
+ surface: "#ffffff"
51
+ surface_container_low: "#f4f4f4"
52
+ surface_container: "#ebebeb"
53
+ surface_container_high: "#e2e2e2"
54
+ surface_variant: "#d8d8d8"
55
+
56
+ links:
57
+ default: "#1c5a7a"
58
+ hover: "#2a6f92"
59
+
60
+ # Fonts, motion, and geometry live in shared.yml — colour themes do not
61
+ # redeclare them.
62
+
63
+ # Static tokens shared across Tailwind and scoped CSS (not derived from the brand triad).
64
+ static:
65
+ on_primary: "#ffffff"
66
+ on_secondary: "#ffffff"
67
+ on_tertiary: "#ffffff"
68
+ surface_container_lowest: "#ffffff"
69
+ surface_dim: "#d8d8d8"
70
+ inverse_primary: "#a4c8da"
71
+ inverse_surface: "#000000"
72
+ inverse_on_surface: "#ffffff"
73
+ primary_fixed: "#a8c5d4"
74
+ primary_fixed_dim: "#7fa8bc"
75
+ secondary_fixed: "#b4d4c0"
76
+ secondary_fixed_dim: "#6fb387"
77
+ tertiary_fixed: "#d0ceb0"
78
+ tertiary_fixed_dim: "#c0bc90"
79
+ on_primary_fixed: "#0a1419"
80
+ on_primary_fixed_variant: "#163040"
81
+ on_secondary_fixed: "#0c1710"
82
+ on_secondary_fixed_variant: "#24382a"
83
+ on_tertiary_fixed: "#17160c"
84
+ on_tertiary_fixed_variant: "#3a381c"
85
+ error: "#ba1a1a"
86
+ on_error: "#ffffff"
87
+ error_container: "#ffdad6"
88
+ on_error_container: "#93000a"
89
+ res_youtube: "#c62828"
90
+ res_podcast: "#2e7d32"
91
+ res_doc: "#b45309"
92
+
93
+ # Detail-page sidebar thumbnail. Light had no block of its own and fell through
94
+ # to _config.yml, whose #eeeeee plate and grey scrim are cold neutrals from
95
+ # before the ramp was warmed (DESIGN.md known inconsistency 14). Same three
96
+ # keys as dark.yml; width, opacity and the filters still fall through.
97
+ #
98
+ # The plate sits a step below the card because a photo is inset into the page,
99
+ # not lifted off it, and the scrim runs from card-white down to deep paper so
100
+ # the image settles onto the sheet instead of floating. Dark can afford 0.35 /
101
+ # 0.45 on its scrim; a light scrim that heavy would wash the photograph out,
102
+ # which is the failure this palette pass exists to undo.
103
+ sidebar_thumbnail:
104
+ background: "#ebebeb"
105
+ overlay_gradient_start: "rgba(255, 255, 255, 0.18)"
106
+ overlay_gradient_end: "rgba(226, 226, 226, 0.28)"
107
+
108
+ # Timeline lane palette — a categorical scale for the CV chart, not part of
109
+ # the UI triad. Six kinds of event are drawn at once and have to be told
110
+ # apart, so this is the one place the site carries more than three inks.
111
+ # Held separately from `colors:` on purpose: a `.page-accent--*` class
112
+ # rebinds --color-primary, and the CV reads green, but a green chart is an
113
+ # unreadable chart. Same hues as before, chroma raised with the triad.
114
+ timeline:
115
+ employment: "#1c5a7a" # petrol — the triad primary, the anchor lane
116
+ education: "#287445" # forest — triad secondary
117
+ product: "#906c2a" # tobacco — deepened off the ochre tertiary
118
+ project: "#534da1" # indigo
119
+ article: "#a95031" # terracotta
120
+ post: "#478576" # sage teal
@@ -0,0 +1,37 @@
1
+ # Tokens shared by every colour theme. Light and dark only differ on colour
2
+ # roles (see light.yml / dark.yml); fonts, motion, and geometry live here so
3
+ # a palette swap cannot drift the type stack.
4
+ #
5
+ # Consumed by theme-context.liquid → theme-vars-block.liquid / font-loader.
6
+
7
+ fonts:
8
+ title: "Cinzel Decorative"
9
+ subtitle: "Newsreader"
10
+ label: "Marcellus SC"
11
+ body: "Newsreader"
12
+ mono: "Courier Prime"
13
+ code: "Courier Prime"
14
+ cv_body: "Newsreader"
15
+ cv_body_weight: 400
16
+ # Generic CSS fallback after the named face (serif | sans-serif | monospace).
17
+ body_fallback: "sans-serif"
18
+ subtitle_fallback: "serif"
19
+ title_fallback: "serif"
20
+ label_fallback: "serif"
21
+ google_title_spec: "wght@400;700;900"
22
+ google_subtitle_spec: "ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500"
23
+ google_label_spec: "wght@400;700"
24
+ google_body_spec: "wght@300;400;500;600;700"
25
+ google_mono_spec: "ital,wght@0,400;0,700;1,400;1,700"
26
+ google_code_spec: "ital,wght@0,300;0,400;0,700;1,400;1,700"
27
+ google_cv_body_spec: "wght@300;400;500;600;700"
28
+
29
+ motion:
30
+ duration_fast: "0.15s"
31
+ duration_base: "0.4s"
32
+ duration_slow: "0.7s"
33
+ ease_default: "ease"
34
+ ease_out: "ease-out"
35
+
36
+ geometry:
37
+ radius_card: "0"
@@ -0,0 +1,85 @@
1
+ updated:
2
+ enus: "Updated"
3
+ ptbr: "Atualizado"
4
+ frfr: "Mis à jour"
5
+ eses: "Actualizado"
6
+ published:
7
+ enus: "Published"
8
+ ptbr: "Publicado"
9
+ frfr: "Publié"
10
+ eses: "Publicado"
11
+ months:
12
+ enus: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
13
+ ptbr: ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"]
14
+ frfr: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembro", "décembre"]
15
+ eses: ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]
16
+ back:
17
+ enus: "Back"
18
+ ptbr: "Voltar"
19
+ frfr: "Retour"
20
+ eses: "Volver"
21
+ back_to_drafts:
22
+ enus: "Back to drafts"
23
+ ptbr: "Voltar aos rascunhos"
24
+ frfr: "Retour aux brouillons"
25
+ eses: "Volver a los borradores"
26
+ back_to_articles:
27
+ enus: "Back to articles"
28
+ ptbr: "Voltar aos artigos"
29
+ frfr: "Retour aux articles"
30
+ eses: "Volver a los artículos"
31
+ back_to_portfolio:
32
+ enus: "Back to portfolio"
33
+ ptbr: "Voltar ao portfólio"
34
+ frfr: "Retour au portfolio"
35
+ eses: "Volver al portafolio"
36
+ back_to_projects:
37
+ enus: "Back to projects"
38
+ ptbr: "Voltar aos projetos"
39
+ frfr: "Retour aux projets"
40
+ eses: "Volver a los proyectos"
41
+ back_to_cv:
42
+ enus: "Back to CV"
43
+ ptbr: "Voltar ao CV"
44
+ frfr: "Retour au CV"
45
+ eses: "Volver al CV"
46
+ previous_text:
47
+ enus: "Previous"
48
+ ptbr: "Anterior"
49
+ frfr: "Précédent"
50
+ eses: "Anterior"
51
+ next_text:
52
+ enus: "Next"
53
+ ptbr: "Próximo"
54
+ frfr: "Suivant"
55
+ eses: "Siguiente"
56
+ random_text:
57
+ enus: "Random"
58
+ ptbr: "Aleatório"
59
+ frfr: "Aléatoire"
60
+ eses: "Aleatorio"
61
+ toc_title:
62
+ enus: "In this text"
63
+ ptbr: "Neste texto"
64
+ frfr: "Dans ce texte"
65
+ eses: "En este texto"
66
+ cite_this_text:
67
+ enus: "Cite this text"
68
+ ptbr: "Citar este texto"
69
+ frfr: "Citer ce texte"
70
+ eses: "Citar este texto"
71
+ related_texts:
72
+ enus: "Related texts"
73
+ ptbr: "Textos relacionados"
74
+ frfr: "Textes liés"
75
+ eses: "Textos relacionados"
76
+ cites:
77
+ enus: "Cites"
78
+ ptbr: "Cita"
79
+ frfr: "Cite"
80
+ eses: "Cita"
81
+ cited_by:
82
+ enus: "Cited by"
83
+ ptbr: "Citado por"
84
+ frfr: "Cité par"
85
+ eses: "Citado por"
@@ -0,0 +1,38 @@
1
+ {% comment %}
2
+ chord-tab.html
3
+ Exibe uma cifra estruturada em N colunas monoespaçadas, usando toda a largura disponível.
4
+ Cada coluna respeita o limite de 79 caracteres por linha.
5
+ Uso: {% include chord-tab.html tab=tab_data %}
6
+ Onde tab_data tem:
7
+ intro: "linha de acordes do intro (opcional)"
8
+ sections:
9
+ - label: "rótulo da seção (opcional)"
10
+ content: |
11
+ linhas de cifra/letra
12
+ {% endcomment %}
13
+
14
+ <div class="chord-tab-container mt-16 mb-8"
15
+ style="width: 100%; box-sizing: border-box; padding-left: 0; padding-right: 0;">
16
+ <h2>Cifra</h2>
17
+
18
+ {% if include.tab.intro %}
19
+ <div class="mt-3 mb-6">
20
+ <pre class="font-mono text-tertiary text-[0.8em] leading-relaxed whitespace-pre m-0 p-0 bg-transparent border-0">{{ include.tab.intro }}</pre>
21
+ </div>
22
+ {% endif %}
23
+
24
+ <div class="mt-4 font-mono text-[0.8em] grid gap-x-2"
25
+ style="display: grid; grid-template-columns: repeat(auto-fill, min(79ch, 100%)); gap: 0 0.5rem; width: 100%;">
26
+ {% for section in include.tab.sections %}
27
+ <div style="overflow-y: auto;">
28
+ {% if section.label and section.label != '' %}
29
+ <div class="font-body text-[0.9em] uppercase tracking-widest text-tertiary mb-[0.35rem]">
30
+ {{ section.label }}</div>
31
+ {% endif %}
32
+ <pre
33
+ class="m-0 p-0 bg-transparent border-0 whitespace-pre overflow-x-auto leading-[1.55] text-on-surface">{{ section.content }}</pre>
34
+ <hr class="border-0 border-t border-gentle-divider my-[1.55em]" />
35
+ </div>
36
+ {% endfor %}
37
+ </div>
38
+ </div>
@@ -0,0 +1,22 @@
1
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
2
+ <script>
3
+ window.ContentRuntimeConfig = {
4
+ contentBase: '{{ site.content_api_base_url | default: "" }}',
5
+ includeUnpublished: {{ site.content_include_unpublished | default: false | jsonify }},
6
+ defaultAuthor: {{ site.pandorga.identity.name | default: site.author | jsonify }},
7
+ siteSuffix: {% assign _short = site.pandorga.identity.short_name | default: site.pandorga.identity.name | default: site.author %}{{ _short | prepend: ' | ' | jsonify }},
8
+ translations: {{ site.data.translations | jsonify }},
9
+ tocMinHeadings: 3,
10
+ expectedSchemaVersion: 1
11
+ };
12
+ // Template modules register listing/detail handlers here (PLT Phase 2).
13
+ window.ContentRuntime = window.ContentRuntime || {};
14
+ window.ContentRuntime.templates = window.ContentRuntime.templates || {
15
+ _map: Object.create(null),
16
+ register: function (name, api) {
17
+ if (!name) throw new Error('ContentRuntime.templates.register: name required');
18
+ this._map[name] = api || {};
19
+ },
20
+ get: function (name) { return this._map[name] || null; }
21
+ };
22
+ </script>
@@ -0,0 +1,176 @@
1
+ {% raw %}
2
+ var contentBase = (window.ContentRuntimeConfig && window.ContentRuntimeConfig.contentBase);
3
+ if (contentBase == null) contentBase = '';
4
+ contentBase = String(contentBase).replace(/\/$/, '');
5
+
6
+ function escapeHtml(value) {
7
+ return String(value == null ? '' : value)
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
13
+ }
14
+
15
+ function escapeAttr(value) {
16
+ return escapeHtml(value).replace(/`/g, '&#96;');
17
+ }
18
+
19
+ function shellOrigin() {
20
+ return (window.ContentRuntimeConfig && window.ContentRuntimeConfig.shellOrigin) || window.location.origin || '';
21
+ }
22
+
23
+ function buildUrl(path) {
24
+ var value = unwrapMediaSrc(path);
25
+ if (!value) return '';
26
+ if (/^https?:\/\//.test(value)) return value;
27
+ if (value.charAt(0) === '/') return encodeURI(contentBase + value);
28
+ return encodeURI(contentBase + '/' + value);
29
+ }
30
+
31
+ /* Pages CMS rich-text (Editor mode) rewrites bare URLs inside quoted
32
+ * Liquid attributes into Markdown autolinks, e.g.
33
+ * src="https://example.com/a.png"
34
+ * becomes
35
+ * src="[https://example.com/a.png](https://example.com/a.png)"
36
+ * which then breaks <img src>. Prefer the Markdown href when the whole
37
+ * attribute value is a single link; leave real captions alone. */
38
+ function unwrapMediaSrc(path) {
39
+ var value = String(path == null ? '' : path).trim();
40
+ if (!value) return '';
41
+ var md = value.match(/^\[([^\]]*)\]\(([^)\s]+)\)\s*$/);
42
+ if (md) return String(md[2] || '').trim();
43
+ var angle = value.match(/^<([^>\s]+)>\s*$/);
44
+ if (angle) return String(angle[1] || '').trim();
45
+ return value;
46
+ }
47
+
48
+ function resolveAssetUrl(path) {
49
+ if (!path) return '';
50
+ var value = unwrapMediaSrc(path);
51
+ if (!value) return '';
52
+ if (/^https?:\/\//i.test(value)) return value;
53
+ if (value.charAt(0) === '/') {
54
+ if (value.indexOf('/media/') === 0) {
55
+ return encodeURI(contentBase.replace(/\/$/, '') + value);
56
+ }
57
+ return encodeURI(shellOrigin().replace(/\/$/, '') + value);
58
+ }
59
+ if (value.indexOf('media/') === 0) {
60
+ return encodeURI(contentBase.replace(/\/$/, '') + '/' + value);
61
+ }
62
+ return encodeURI(contentBase.replace(/\/$/, '') + '/' + value);
63
+ }
64
+
65
+ function parseIncludeAttrs(raw) {
66
+ var attrs = {};
67
+ var text = String(raw || '');
68
+ // Allow newlines inside quoted values (dict-card def=, tweet body=/note=).
69
+ text = text.replace(/([\w-]+)\s*=\s*(?:"([\s\S]*?)"|'([\s\S]*?)')/g, function (_, key, dbl, sgl) {
70
+ attrs[key] = dbl != null ? dbl : sgl;
71
+ return '';
72
+ });
73
+ text.replace(/([\w-]+)\s*=\s*([^\s]+)/g, function (_, key, val) {
74
+ if (!(key in attrs)) attrs[key] = val;
75
+ return '';
76
+ });
77
+ return attrs;
78
+ }
79
+
80
+ function parseScalar(value) {
81
+ var raw = String(value || '').trim();
82
+ if ((raw.charAt(0) === '"' && raw.charAt(raw.length - 1) === '"') ||
83
+ (raw.charAt(0) === "'" && raw.charAt(raw.length - 1) === "'")) {
84
+ return raw.slice(1, -1);
85
+ }
86
+ if (raw === 'true') return true;
87
+ if (raw === 'false') return false;
88
+ return raw;
89
+ }
90
+
91
+ function hydrateFrontMatter(item) {
92
+ var markdown = String(item.body_markdown || '').replace(/^\uFEFF/, '');
93
+ var match = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
94
+
95
+ // The export strips the front-matter block out of body_markdown, so this
96
+ // never matches for exported content. The date promotion below therefore
97
+ // has to happen first: when it sat after the early return, no detail
98
+ // page ever received a date. The parse itself still matters for raw
99
+ // Markdown that has not been through the exporter.
100
+ item.front_matter = item.front_matter || {};
101
+ if (item.date) item.front_matter.date = item.date;
102
+ if (item.last_updated) item.front_matter.last_updated = item.last_updated;
103
+
104
+ if (!match) return item;
105
+
106
+ var parsed = {};
107
+ match[1].split(/\n/).forEach(function (line) {
108
+ var m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
109
+ if (m) parsed[m[1]] = parseScalar(m[2]);
110
+ });
111
+ item.front_matter = Object.assign({}, parsed, item.front_matter);
112
+ item.body_markdown = markdown.slice(match[0].length);
113
+ return item;
114
+ }
115
+
116
+ function stripFrontMatter(markdown) {
117
+ return String(markdown || '').replace(/^\uFEFF/, '').replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '');
118
+ }
119
+
120
+ function endsWithSentencePunctuation(text) {
121
+ var value = String(text || '').trim();
122
+ return /[.!?](\s*<\/[^>]+>)*\s*$/.test(value);
123
+ }
124
+
125
+ function ensureCaptionPeriod(text) {
126
+ var value = String(text || '').trim();
127
+ if (!value || endsWithSentencePunctuation(value)) return value;
128
+ return value + '.';
129
+ }
130
+
131
+ /* The opening sentence of a longer piece of copy, for the places that need
132
+ * one sentence and are given a paragraph. A project's `tagline` is the
133
+ * case that forced this: it is a standfirst on the page, and it is also
134
+ * what `_layouts/projects.html` hands to the meta description and the
135
+ * social card, where a search result truncated mid-clause reads worse than
136
+ * a shorter one that finishes its thought.
137
+ *
138
+ * 160 characters is roughly what a desktop search result renders; Open
139
+ * Graph is more generous and takes the same string. A first sentence longer
140
+ * than the budget is cut on a word boundary rather than mid-word, and any
141
+ * punctuation left dangling at the cut goes with it.
142
+ */
143
+ var LEAD_SENTENCE_MAX = 160;
144
+
145
+ function leadSentence(text, limit) {
146
+ var value = String(text || '').trim();
147
+ if (!value) return '';
148
+ var max = limit || LEAD_SENTENCE_MAX;
149
+ var match = value.match(/^[\s\S]*?[.!?](?=\s|$)/);
150
+ var lead = match ? match[0] : value;
151
+ if (lead.length <= max) return lead;
152
+ var cut = lead.slice(0, max);
153
+ var space = cut.lastIndexOf(' ');
154
+ if (space > 0) cut = cut.slice(0, space);
155
+ return cut.replace(/[\s,;:.]+$/, '');
156
+ }
157
+
158
+ function figureSourceHtml(raw) {
159
+ raw = String(raw || '').trim();
160
+ if (!raw) return '';
161
+ if (/<a\s/i.test(raw)) {
162
+ return ' Source: ' + raw;
163
+ }
164
+ // Captions already parse Markdown; sources do too so
165
+ // `[Parrot Website](https://…)` renders as a link, not escaped brackets.
166
+ return ' Source: ' + stripSingleParagraph(parseInlineMarkdown(raw));
167
+ }
168
+
169
+ function includeCaptionHtml(caption) {
170
+ var trimmed = ensureCaptionPeriod(String(caption || '').trim());
171
+ if (!trimmed) return '';
172
+ if (/<[a-z][\s\S]*>/i.test(trimmed)) return trimmed;
173
+ return stripSingleParagraph(parseInlineMarkdown(trimmed));
174
+ }
175
+
176
+ {% endraw %}