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,430 @@
1
+ /**
2
+ * Studio side-preview: include → HTML (content-runtime–aligned where practical).
3
+ * Spec: AC-STU-05. Cite/xcite pills start neutral; main.js resolves existence.
4
+ */
5
+
6
+ import { marked } from "marked";
7
+
8
+ function escapeAttr(value) {
9
+ return String(value ?? "")
10
+ .replace(/&/g, "&")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;");
14
+ }
15
+
16
+ function escapeHtml(value) {
17
+ return String(value ?? "")
18
+ .replace(/&/g, "&amp;")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;");
21
+ }
22
+
23
+ function unwrapMediaSrc(path) {
24
+ const value = String(path == null ? "" : path).trim();
25
+ if (!value) return "";
26
+ const md = value.match(/^\[([^\]]*)\]\(([^)\s]+)\)\s*$/);
27
+ if (md) return String(md[2] || "").trim();
28
+ return value;
29
+ }
30
+
31
+ /** Percent width from Liquid attrs; strips a trailing % so "80" and "80%" both work. */
32
+ function maxWidthCss(width, fallback = "100") {
33
+ let w = String(width == null || width === "" ? fallback : width).trim();
34
+ if (!w) w = fallback;
35
+ w = w.replace(/%$/, "");
36
+ return `max-width:${escapeAttr(w)}%;`;
37
+ }
38
+
39
+ function parseIncludeAttrs(raw) {
40
+ const attrs = {};
41
+ let text = String(raw || "");
42
+ // Allow newlines inside quoted values (dict-card def=, tweet body=/note=).
43
+ text = text.replace(
44
+ /([\w-]+)\s*=\s*(?:"([\s\S]*?)"|'([\s\S]*?)')/g,
45
+ (_, key, dbl, sgl) => {
46
+ attrs[key] = dbl != null ? dbl : sgl;
47
+ return "";
48
+ }
49
+ );
50
+ text.replace(/([\w-]+)\s*=\s*([^\s%]+)/g, (_, key, val) => {
51
+ if (!(key in attrs)) attrs[key] = val;
52
+ return "";
53
+ });
54
+ return attrs;
55
+ }
56
+
57
+ function figureHtml(attrs) {
58
+ const src = unwrapMediaSrc(attrs.src);
59
+ if (!src) return "";
60
+ const width = maxWidthCss(attrs.width);
61
+ const caption = attrs.caption || "";
62
+ return [
63
+ `<figure class="my-8 mx-auto text-center w-full" style="display:block;${width}">`,
64
+ `<img class="figure-lightbox-trigger" src="${escapeAttr(src)}" alt="${escapeAttr(attrs.alt || caption)}" loading="lazy">`,
65
+ caption ? `<figcaption class="mt-3">${escapeHtml(caption)}</figcaption>` : "",
66
+ "</figure>",
67
+ ].join("");
68
+ }
69
+
70
+ function cutInHtml(attrs) {
71
+ const src = unwrapMediaSrc(attrs.src);
72
+ if (!src) return "";
73
+ const sideClass = attrs.side === "right" ? " article-cut-in--right" : "";
74
+ return `<figure class="article-cut-in-plate${sideClass}"><img class="article-cut-in${sideClass}" src="${escapeAttr(src)}" alt="${escapeAttr(attrs.alt || "")}" width="120" loading="lazy"></figure>`;
75
+ }
76
+
77
+ /** Basename for labels (`/media/plots/goo.html` → `goo.html`). */
78
+ function fileLabel(path) {
79
+ const raw = unwrapMediaSrc(path);
80
+ if (!raw) return "";
81
+ const cleaned = raw.split(/[?#]/)[0];
82
+ const parts = cleaned.split(/[/\\]/).filter(Boolean);
83
+ return parts[parts.length - 1] || cleaned;
84
+ }
85
+
86
+ /** YouTube id from bare id or embed/watch/shorts URL. */
87
+ function youtubeIdFrom(value) {
88
+ const raw = unwrapMediaSrc(value);
89
+ if (!raw) return "";
90
+ const m = raw.match(
91
+ /(?:youtube\.com\/(?:embed\/|shorts\/|watch\?v=)|youtu\.be\/)([A-Za-z0-9_-]{6,})/
92
+ );
93
+ if (m) return m[1];
94
+ if (/^[A-Za-z0-9_-]{6,}$/.test(raw)) return raw;
95
+ return raw;
96
+ }
97
+
98
+ /**
99
+ * Sized rectangle for embeds Studio must not load live (Plotly HTML, YouTube, video).
100
+ * Images stay real; this is for network / heavy external media only.
101
+ */
102
+ function embedPlaceholderHtml(label, { width, height, aspect, caption } = {}) {
103
+ const text = String(label || "").trim() || "Embed";
104
+ const styleParts = [maxWidthCss(width)];
105
+ if (height) styleParts.push(`min-height:${escapeAttr(String(height))}px`);
106
+ else if (aspect) styleParts.push(`aspect-ratio:${escapeAttr(aspect)}`);
107
+ else styleParts.push("min-height:12rem");
108
+ const fig = [
109
+ `<figure class="studio-embed-placeholder" style="display:block;margin:1.5rem auto;text-align:center;${styleParts.join("")}">`,
110
+ `<div class="studio-embed-placeholder__box" role="img" aria-label="${escapeAttr(text)}">`,
111
+ `<code class="studio-embed-placeholder__label">${escapeHtml(text)}</code>`,
112
+ `</div>`,
113
+ ];
114
+ if (caption) fig.push(`<figcaption>${escapeHtml(caption)}</figcaption>`);
115
+ fig.push("</figure>");
116
+ return fig.join("");
117
+ }
118
+
119
+ function youtubeHtml(attrs) {
120
+ const id = youtubeIdFrom(attrs.id || attrs.src);
121
+ if (!id) return "";
122
+ return embedPlaceholderHtml(`YouTube video: ${id}`, {
123
+ width: attrs.width,
124
+ aspect: "16 / 9",
125
+ caption: attrs.caption,
126
+ });
127
+ }
128
+
129
+ function youtubeShortHtml(attrs) {
130
+ const id = youtubeIdFrom(attrs.src || attrs.id);
131
+ if (!id) return "";
132
+ const id2 = youtubeIdFrom(attrs.src2 || attrs.id2);
133
+ if (id2) {
134
+ return [
135
+ embedPlaceholderHtml(`YouTube short: ${id}`, {
136
+ width: "45",
137
+ aspect: "9 / 16",
138
+ }),
139
+ embedPlaceholderHtml(`YouTube short: ${id2}`, {
140
+ width: "45",
141
+ aspect: "9 / 16",
142
+ }),
143
+ ].join("");
144
+ }
145
+ return embedPlaceholderHtml(`YouTube short: ${id}`, {
146
+ width: attrs.width || "45",
147
+ aspect: "9 / 16",
148
+ });
149
+ }
150
+
151
+ function videoHtml(attrs) {
152
+ const src = unwrapMediaSrc(attrs.src);
153
+ if (!src) return "";
154
+ const label = fileLabel(src) || src;
155
+ return embedPlaceholderHtml(`Video: ${label}`, {
156
+ width: attrs.width,
157
+ aspect: "16 / 9",
158
+ caption: attrs.caption,
159
+ });
160
+ }
161
+
162
+ function plotlyHtml(attrs) {
163
+ const src = unwrapMediaSrc(attrs.src);
164
+ if (!src) return "";
165
+ const name = fileLabel(src) || src;
166
+ return embedPlaceholderHtml(`Plotly: ${name}`, {
167
+ width: attrs.width,
168
+ height: attrs.height || "480",
169
+ caption: attrs.caption,
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Shared preview pill (cite / xcite / latex). One box — no nested wrappers.
175
+ * main.js paints cite/xcite green/red after debounced existence check.
176
+ */
177
+ function pillHtml(text, { kind, key, variant, display } = {}) {
178
+ const label = String(text || "").trim() || "?";
179
+ const classes = ["studio-ref-pill"];
180
+ if (variant) classes.push(`studio-ref-pill--${variant}`);
181
+ if (display) classes.push("studio-ref-pill--display");
182
+ const attrs = [`class="${classes.join(" ")}"`];
183
+ if (kind) {
184
+ attrs.push(`data-cite-check="${escapeAttr(kind)}"`);
185
+ attrs.push(`data-key="${escapeAttr(String(key || "").trim())}"`);
186
+ }
187
+ if (variant === "latex") {
188
+ attrs.push('title="LaTeX (MathJax not available in Studio preview)"');
189
+ }
190
+ return `<span ${attrs.join(" ")}>${escapeHtml(label)}</span>`;
191
+ }
192
+
193
+ function refPillHtml(kind, key) {
194
+ const k = String(key || "").trim();
195
+ return pillHtml(k || "?", { kind, key: k });
196
+ }
197
+
198
+ function xciteHtml(attrs) {
199
+ return refPillHtml("xcite", attrs.key);
200
+ }
201
+
202
+ function citeHtml(attrs) {
203
+ return refPillHtml("cite", attrs.key);
204
+ }
205
+
206
+ function formatDefHtml(def) {
207
+ return escapeHtml(String(def || "").trim()).replace(/\r\n|\r|\n/g, "<br>");
208
+ }
209
+
210
+ /** Side-preview for `_includes/dict-card.html` (content-runtime–aligned). */
211
+ function dictCardHtml(attrs) {
212
+ const lemma = String(attrs.lemma || "").trim();
213
+ if (!lemma) return "";
214
+ const locale = String(attrs.locale || "").trim().toLowerCase();
215
+ const meta = String(attrs.meta || "").trim();
216
+ const label = String(attrs.country || meta || locale).trim();
217
+ const note = String(attrs.note || "").trim();
218
+ const source = String(attrs.source || "").trim();
219
+ const url = String(attrs.url || "").trim();
220
+ const accessed = String(attrs.accessed || "").trim();
221
+ const citeExtra = String(attrs.cite_extra || "").trim();
222
+ const linkLabel = String(attrs.link_label || url).trim();
223
+ const parts = [
224
+ `<article class="sov-card${locale ? ` sov-card--${escapeAttr(locale)}` : ""}" role="listitem">`,
225
+ `<div class="sov-card__head">`,
226
+ ];
227
+ if (locale) {
228
+ parts.push(
229
+ `<span class="sov-card__flag" title="${escapeAttr(label)}">` +
230
+ `<span class="fi fi-${escapeAttr(locale)}" role="img" aria-label="${escapeAttr(label)}"></span>` +
231
+ `</span>`
232
+ );
233
+ }
234
+ if (meta) {
235
+ parts.push(`<span class="sov-card__meta">${escapeHtml(meta)}</span>`);
236
+ }
237
+ parts.push(`</div>`);
238
+ parts.push(`<p class="sov-card__lemma">${escapeHtml(lemma)}</p>`);
239
+ if (attrs.def) {
240
+ parts.push(`<div class="sov-card__def">${formatDefHtml(attrs.def)}</div>`);
241
+ }
242
+ if (note) {
243
+ // Note may carry intentional inline HTML (e.g. <em>), matching site Liquid.
244
+ parts.push(`<p class="sov-card__note">${note}</p>`);
245
+ }
246
+ if (source) {
247
+ let cite =
248
+ `${escapeHtml(source)}, s.v. &ldquo;${escapeHtml(lemma)},&rdquo;`;
249
+ if (url) {
250
+ cite += ` <a href="${escapeAttr(url)}">${escapeHtml(linkLabel)}</a>`;
251
+ }
252
+ if (accessed) cite += `, accessed ${escapeHtml(accessed)}`;
253
+ cite += ".";
254
+ if (citeExtra) cite += ` ${escapeHtml(citeExtra)}`;
255
+ parts.push(`<p class="sov-card__cite">${cite}</p>`);
256
+ }
257
+ parts.push(`</article>`);
258
+ return parts.join("");
259
+ }
260
+
261
+ /** Wrap runs of dictionary cards in the same grid the public runtime uses. */
262
+ function wrapDictCardGrid(html) {
263
+ return String(html || "").replace(
264
+ /(?:<article\b[^>]*\bsov-card\b[\s\S]*?<\/article>\s*){2,}/g,
265
+ (block) => {
266
+ if (/\bsov-grid\b/.test(block)) return block;
267
+ return `<div class="sov-grid" role="list">${block.trim()}</div>\n`;
268
+ }
269
+ );
270
+ }
271
+
272
+ function tweetHtml(attrs) {
273
+ const body = String(attrs.body || "").trim();
274
+ if (!body) return "";
275
+ const handle = String(attrs.handle || "")
276
+ .trim()
277
+ .replace(/^@/, "");
278
+ const icon = String(attrs.icon || "chat").trim() || "chat";
279
+ const url = String(attrs.url || "").trim();
280
+ const date = String(attrs.date || "").trim();
281
+ const note = String(attrs.note || "").trim();
282
+ const name = String(attrs.name || "").trim();
283
+ const linkLabel = url.replace(/^https?:\/\//, "");
284
+ const parts = [
285
+ `<article class="sov-tweet article-tweet">`,
286
+ `<div class="sov-tweet__head article-tweet__head">`,
287
+ `<span class="sov-tweet__avatar article-tweet__avatar" aria-hidden="true"><span class="material-symbols-outlined">${escapeHtml(icon)}</span></span>`,
288
+ `<span class="sov-tweet__id article-tweet__id">`,
289
+ ];
290
+ if (name) {
291
+ parts.push(
292
+ `<span class="sov-tweet__name article-tweet__name">${escapeHtml(name)}</span>`
293
+ );
294
+ }
295
+ if (handle) {
296
+ parts.push(
297
+ `<span class="sov-tweet__handle article-tweet__handle">@${escapeHtml(handle)}</span>`
298
+ );
299
+ }
300
+ parts.push(`</span></div>`);
301
+ // Body/note may carry intentional inline HTML (e.g. <em>), matching site runtime.
302
+ // Local card only — no X/Twitter widget script.
303
+ parts.push(`<p class="sov-tweet__body article-tweet__body">${body}</p>`);
304
+ if (url || date) {
305
+ let cite = "";
306
+ if (url) cite += `<a href="${escapeAttr(url)}">${escapeHtml(linkLabel)}</a>`;
307
+ if (url && date) cite += " | ";
308
+ if (date) cite += escapeHtml(date);
309
+ parts.push(`<p class="sov-tweet__cite article-tweet__cite">${cite}</p>`);
310
+ }
311
+ if (note) {
312
+ parts.push(`<p class="sov-tweet__note article-tweet__note">${note}</p>`);
313
+ }
314
+ parts.push(`</article>`);
315
+ return parts.join("");
316
+ }
317
+
318
+ function articleNoteHtml(innerMarkdown) {
319
+ const inner = String(innerMarkdown || "").trim();
320
+ if (!inner) return `<div class="article-note"></div>`;
321
+ let bodyHtml = "";
322
+ try {
323
+ bodyHtml = marked.parse(inner, { gfm: true, breaks: false });
324
+ } catch {
325
+ bodyHtml = `<p>${escapeHtml(inner)}</p>`;
326
+ }
327
+ return `<div class="article-note">${bodyHtml}</div>`;
328
+ }
329
+
330
+ function latexPillHtml(tex, display) {
331
+ return pillHtml(String(tex || "").trim(), {
332
+ variant: "latex",
333
+ display: Boolean(display),
334
+ });
335
+ }
336
+
337
+ function normalizeDisplayMathTex(math) {
338
+ // Keep light parity with scripts/lib/normalize-display-math.mjs (row breaks).
339
+ return String(math || "").replace(/\\\s/g, "\\\\ ");
340
+ }
341
+
342
+ function protectMath(markdown) {
343
+ const store = [];
344
+ let text = String(markdown || "");
345
+ const push = (display, math) => {
346
+ const i = store.length;
347
+ store.push({ display, math });
348
+ return `\uE000MATH${i}\uE001`;
349
+ };
350
+ text = text.replace(/\$\$([\s\S]+?)\$\$/g, (_, math) =>
351
+ push(true, normalizeDisplayMathTex(math))
352
+ );
353
+ text = text.replace(/\\\[([\s\S]+?)\\\]/g, (_, math) =>
354
+ push(true, normalizeDisplayMathTex(math))
355
+ );
356
+ text = text.replace(/\\\(([\s\S]+?)\\\)/g, (_, math) => push(false, math));
357
+ text = text.replace(
358
+ /(^|[^\\$])\$(?!\$)([^$\n]+?)\$(?!\$)/g,
359
+ (match, prefix, math) => prefix + push(false, math)
360
+ );
361
+ return { text, store };
362
+ }
363
+
364
+ function restoreMathAsPills(html, store) {
365
+ return String(html || "").replace(/\uE000MATH(\d+)\uE001/g, (_, idx) => {
366
+ const entry = store[Number(idx)];
367
+ if (!entry) return "";
368
+ return latexPillHtml(entry.math, entry.display);
369
+ });
370
+ }
371
+
372
+ const RENDERERS = {
373
+ "figure.html": figureHtml,
374
+ "cut-in.html": cutInHtml,
375
+ "youtube.html": youtubeHtml,
376
+ "youtube-short.html": youtubeShortHtml,
377
+ "video.html": videoHtml,
378
+ "plotly.html": plotlyHtml,
379
+ "xcite.html": xciteHtml,
380
+ "cite.html": citeHtml,
381
+ "dict-card.html": dictCardHtml,
382
+ "tweet.html": tweetHtml,
383
+ };
384
+
385
+ export function preprocessIncludes(markdown) {
386
+ let text = String(markdown || "");
387
+
388
+ // Block article-note include (Studio TipTap / Liquid body form).
389
+ text = text.replace(
390
+ /\{%-?\s*include\s+article-note(?:\.html)?\b(?:(?!%\})[\s\S])*?-?%\}\s*([\s\S]*?)\s*\{%-?\s*endinclude\s*-?%\}/gi,
391
+ (_, body) => articleNoteHtml(body)
392
+ );
393
+
394
+ // Authored HTML callout (most published articles/posts).
395
+ text = text.replace(
396
+ /<div\s+([^>]*\barticle-note\b[^>]*)>([\s\S]*?)<\/div>/gi,
397
+ (_, _open, inner) => articleNoteHtml(inner)
398
+ );
399
+
400
+ // Bibliography cite tag used in published Markdown: {% cite key %}
401
+ text = text.replace(
402
+ /\{%-?\s*cite\s+([^\s%]+)[\s\S]*?-?%\}/g,
403
+ (_, key) => refPillHtml("cite", key)
404
+ );
405
+
406
+ // Stop at `%}` only — attrs often contain `%` (width="80%", percent-encoded URLs).
407
+ text = text.replace(
408
+ /\{%-?\s*include\s+([\w.-]+)\s*((?:(?!%\})[\s\S])*?)\s*-?%\}/g,
409
+ (_, name, rawAttrs) => {
410
+ const renderer = RENDERERS[name];
411
+ if (!renderer) {
412
+ return `<div class="studio-unknown-include" data-include="${escapeAttr(name)}"></div>`;
413
+ }
414
+ return renderer(parseIncludeAttrs(rawAttrs));
415
+ }
416
+ );
417
+ return text;
418
+ }
419
+
420
+ export function renderPreview(markdown) {
421
+ const math = protectMath(String(markdown || ""));
422
+ const pre = preprocessIncludes(math.text);
423
+ let html;
424
+ try {
425
+ html = marked.parse(pre, { gfm: true, breaks: false });
426
+ } catch {
427
+ html = `<p>${escapeHtml(pre)}</p>`;
428
+ }
429
+ return wrapDictCardGrid(restoreMathAsPills(html, math.store));
430
+ }