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,43 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ # This plugin automatically derives 'date' and 'last_updated' from Git history
5
+ # for articles and posts, ensuring Git is the single source of truth for chronology.
6
+ module Jekyll
7
+ class GitMetadataGenerator < Generator
8
+ safe true
9
+ priority :high
10
+
11
+ def generate(site)
12
+ return unless GitChronology.git_repo?
13
+
14
+ article_docs = site.collections["articles"]&.docs || []
15
+ post_docs = site.collections["posts"]&.docs || []
16
+ return if article_docs.empty? && post_docs.empty?
17
+
18
+ (article_docs + post_docs).each do |doc|
19
+ next unless doc.path.match?(/\.(md|markdown)$/)
20
+
21
+ doc.data['language'] = doc.collection.label == 'articles' ? 'enus' : 'ptbr'
22
+ GitChronology.apply!(doc.data, doc.path, doc.collection.label == 'articles' ? 'article' : 'post')
23
+
24
+ if doc.data['date']
25
+ begin
26
+ doc.data['date'] = Time.parse(doc.data['date'].to_s)
27
+ rescue ArgumentError
28
+ end
29
+ end
30
+
31
+ if doc.data['last_updated']
32
+ begin
33
+ doc.data['last_updated'] = Time.parse(doc.data['last_updated'].to_s)
34
+ rescue ArgumentError
35
+ end
36
+ end
37
+ end
38
+
39
+ article_docs.sort_by! { |d| d.data['last_updated'] || d.data['date'] || Time.now }.reverse!
40
+ post_docs.sort_by! { |d| d.data['last_updated'] || d.data['date'] || Time.now }.reverse!
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Renders the private CV job body fields to HTML for the print layouts,
4
+ # storing the result in doc.data["extended"/"single_page"].
5
+ # body_public is not rendered here: the public CV page reads it straight
6
+ # from the exported JSON through the client-side runtime.
7
+ module Jekyll
8
+ class JobSectionsGenerator < Generator
9
+ safe true
10
+ priority :high
11
+
12
+ def generate(site)
13
+ return if site.config["shell_content_agnostic"]
14
+
15
+ converter = site.find_converter_instance(Jekyll::Converters::Markdown)
16
+
17
+ (site.collections["jobs"]&.docs || []).each do |doc|
18
+ %w[extended single_page].each do |key|
19
+ markdown = doc.data["body_#{key}"].to_s.strip
20
+ doc.data[key] = converter.convert(markdown) unless markdown.empty?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require_relative "resource_validator"
6
+ require_relative "tag_validator"
7
+ require_relative "../writing_slug_limits"
8
+ require_relative "../content_keys"
9
+
10
+ # Single validation entry point for editorial content under content/.
11
+ module ContentValidators
12
+ module_function
13
+
14
+ def validate_all!(repo_root = Pathname.pwd)
15
+ root = Pathname.new(repo_root)
16
+ errors = []
17
+ errors.concat(ResourceValidator.collect_errors(root))
18
+ errors.concat(TagValidator.collect_errors(root))
19
+ errors.concat(WritingSlugLimits.collect_errors(root))
20
+
21
+ unless errors.empty?
22
+ raise ArgumentError, "Content validation failed:\n - #{errors.join("\n - ")}"
23
+ end
24
+
25
+ ContentKeys.validate!(root)
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+
6
+ # Shared resource collection validation (content/ source, not Jekyll collections).
7
+ module ResourceValidator
8
+ module_function
9
+
10
+ def split_front_matter(text)
11
+ text = text.to_s.sub(/\A\xEF\xBB\xBF/, "")
12
+ return [{}, text] unless text.start_with?("---\n")
13
+
14
+ parts = text.split(/^---\s*$\n?/, 3)
15
+ return [{}, text] unless parts.length == 3
16
+
17
+ data = YAML.safe_load(parts[1], permitted_classes: [Date, Time], aliases: true) || {}
18
+ [data, parts[2]]
19
+ end
20
+
21
+ def collect_errors(repo_root)
22
+ root = Pathname.new(repo_root)
23
+ resources_dir = root.join("content/collections/resources")
24
+ return [] unless resources_dir.directory?
25
+
26
+ errors = []
27
+ resources_dir.glob("*.{md,markdown}").sort.each do |source|
28
+ front_matter, = split_front_matter(source.read)
29
+ name = front_matter["name"] || source.basename(source.extname).to_s
30
+ type = front_matter["type"].to_s.downcase
31
+ link = front_matter["link"].to_s
32
+
33
+ if link.empty?
34
+ errors << "#{name}: missing required 'link' field"
35
+ next
36
+ end
37
+
38
+ case type
39
+ when "youtube"
40
+ unless link.match?(%r{\Ahttps?://(www\.)?(youtube\.com|youtu\.be)/}i)
41
+ errors << "#{name}: type 'youtube' but link does not point to youtube.com or youtu.be (got: #{link})"
42
+ end
43
+ when "spotify"
44
+ unless link.match?(%r{\Ahttps?://(open\.)?spotify\.com/}i)
45
+ errors << "#{name}: type 'spotify' but link does not point to spotify.com (got: #{link})"
46
+ end
47
+ when "pdf"
48
+ unless link.end_with?(".pdf")
49
+ errors << "#{name}: type 'pdf' but link does not end with .pdf (got: #{link})"
50
+ end
51
+ end
52
+ end
53
+
54
+ errors
55
+ end
56
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+ require "date"
6
+
7
+ # Tags replaced category as the single classification axis for articles and
8
+ # rascunhos. Vocabulary is closed per collection and loaded from
9
+ # `_config.yml` → `pandorga.taxonomies` (PLT Phase 1).
10
+ module TagValidator
11
+ DEFAULT_MAX_TAGS = 3
12
+
13
+ COLLECTIONS_WITHOUT_REQUIRED_TAGS = %w[posts].freeze
14
+
15
+ module_function
16
+
17
+ def load_vocabularies(repo_root = Pathname.pwd)
18
+ root = Pathname.new(repo_root)
19
+ config_path = root.join("_config.yml")
20
+ return {} unless config_path.file?
21
+
22
+ config = YAML.safe_load(config_path.read, permitted_classes: [Date, Time], aliases: true) || {}
23
+ vocab = {}
24
+
25
+ taxonomies = config.dig("pandorga", "taxonomies")
26
+ if taxonomies.is_a?(Hash)
27
+ taxonomies.each do |collection, spec|
28
+ next unless spec.is_a?(Hash)
29
+
30
+ tags = spec["tags"]
31
+ next unless tags.is_a?(Array)
32
+
33
+ vocab[collection.to_s] = tags.map(&:to_s).freeze
34
+ end
35
+ end
36
+
37
+ # Page-local taxonomy (pandorga.pages[].taxonomy) fills gaps.
38
+ pages = config.dig("pandorga", "pages")
39
+ if pages.is_a?(Array)
40
+ pages.each do |page|
41
+ next unless page.is_a?(Hash)
42
+
43
+ collection = page["collection"].to_s
44
+ next if collection.empty? || vocab.key?(collection)
45
+
46
+ spec = page["taxonomy"]
47
+ next unless spec.is_a?(Hash)
48
+
49
+ tags = spec["tags"]
50
+ next unless tags.is_a?(Array)
51
+
52
+ vocab[collection] = tags.map(&:to_s).freeze
53
+ end
54
+ end
55
+
56
+ vocab
57
+ end
58
+
59
+ def max_tags_for(collection, repo_root = Pathname.pwd)
60
+ root = Pathname.new(repo_root)
61
+ config_path = root.join("_config.yml")
62
+ return DEFAULT_MAX_TAGS unless config_path.file?
63
+
64
+ config = YAML.safe_load(config_path.read, permitted_classes: [Date, Time], aliases: true) || {}
65
+ max = config.dig("pandorga", "taxonomies", collection.to_s, "max")
66
+ if max.nil?
67
+ pages = config.dig("pandorga", "pages")
68
+ if pages.is_a?(Array)
69
+ page = pages.find { |p| p.is_a?(Hash) && p["collection"].to_s == collection.to_s }
70
+ max = page.dig("taxonomy", "max") if page.is_a?(Hash)
71
+ end
72
+ end
73
+ max.nil? ? DEFAULT_MAX_TAGS : Integer(max)
74
+ rescue ArgumentError, TypeError
75
+ DEFAULT_MAX_TAGS
76
+ end
77
+
78
+ def collect_errors(repo_root = Pathname.pwd)
79
+ root = Pathname.new(repo_root)
80
+ errors = []
81
+ vocabulary = load_vocabularies(root)
82
+
83
+ if vocabulary.empty?
84
+ errors << "_config.yml: pandorga.taxonomies is missing or empty"
85
+ return errors
86
+ end
87
+
88
+ vocabulary.each do |collection, allowed|
89
+ dir = root.join("content", "collections", collection)
90
+ next unless dir.directory?
91
+
92
+ tags_required = !COLLECTIONS_WITHOUT_REQUIRED_TAGS.include?(collection)
93
+ max_tags = max_tags_for(collection, root)
94
+
95
+ Dir.glob(dir.join("*.md")).sort.each do |path|
96
+ relative = Pathname.new(path).relative_path_from(root)
97
+ front_matter = read_front_matter(path)
98
+ next if front_matter.nil?
99
+
100
+ if front_matter.key?("category")
101
+ errors << "#{relative}: uses `category`; tags replaced it as the " \
102
+ "single classification axis"
103
+ end
104
+
105
+ tags = front_matter["tags"]
106
+ if tags.nil?
107
+ errors << "#{relative}: missing `tags`" if tags_required
108
+ next
109
+ end
110
+
111
+ unless tags.is_a?(Array)
112
+ errors << "#{relative}: `tags` must be a list, got #{tags.class}"
113
+ next
114
+ end
115
+
116
+ if tags.empty?
117
+ errors << "#{relative}: `tags` is empty; every entry needs at least one" if tags_required
118
+ next
119
+ end
120
+
121
+ if tags.size > max_tags
122
+ errors << "#{relative}: #{tags.size} tags exceeds the #{max_tags} " \
123
+ "allowed — pick the ones that would actually be filtered on"
124
+ end
125
+
126
+ if tags.uniq.size != tags.size
127
+ errors << "#{relative}: duplicate tags #{(tags - tags.uniq).uniq.inspect}"
128
+ end
129
+
130
+ unknown = tags.reject { |tag| allowed.include?(tag.to_s) }
131
+ next if unknown.empty?
132
+
133
+ errors << "#{relative}: unknown #{collection} tag(s) #{unknown.inspect}. " \
134
+ "Allowed: #{allowed.join(', ')}"
135
+ end
136
+ end
137
+
138
+ errors
139
+ end
140
+
141
+ def read_front_matter(path)
142
+ source = File.read(path)
143
+ return nil unless source.start_with?("---")
144
+
145
+ raw = source.split(/^---\s*$/, 3)[1]
146
+ return nil if raw.nil?
147
+
148
+ parsed = YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: true)
149
+ parsed.is_a?(Hash) ? parsed : nil
150
+ rescue Psych::SyntaxError
151
+ nil
152
+ end
153
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Plain-text listing excerpt from editorial Markdown.
4
+ # Mirrors textExcerpt() in _includes/content-runtime/40-listing.html closely
5
+ # enough for card previews. Liquid includes are stripped rather than rendered.
6
+ module TextExcerpt
7
+ LIMIT = 220
8
+
9
+ module_function
10
+
11
+ def from_markdown(markdown, limit: LIMIT)
12
+ text = markdown.to_s.sub(/\A\xEF\xBB\xBF/, "")
13
+ text = strip_non_prose(text)
14
+ text = flatten_markup(text)
15
+ text = text.gsub(/\s+/, " ").strip
16
+ truncate(text, limit)
17
+ end
18
+
19
+ def listing_fields(front_matter, body)
20
+ editorial = front_matter["description"].to_s.strip
21
+ excerpt = from_markdown(body)
22
+ {
23
+ "description" => editorial,
24
+ "excerpt" => excerpt
25
+ }
26
+ end
27
+
28
+ def strip_non_prose(text)
29
+ text = text.gsub(/<!--[\s\S]*?-->/, " ")
30
+ text = text.gsub(/\{%-?[\s\S]*?-?%\}/, " ")
31
+ text = text.gsub(/\$\$[\s\S]*?\$\$/, " ")
32
+ text = text.gsub(/\$[^$\n]+\$/, " ")
33
+ text = text.gsub(/\\\[[\s\S]*?\\\]/, " ")
34
+ text = text.gsub(/\\\([\s\S]*?\\\)/, " ")
35
+ text.gsub(/```[\s\S]*?```/, " ")
36
+ end
37
+
38
+ def flatten_markup(text)
39
+ text = text.gsub(/!\[([^\]]*)\]\([^)]+\)/, ' \1 ')
40
+ text = text.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
41
+ text = text.gsub(/^\#{1,6}\s+(.+?)\s*$/) { "#{Regexp.last_match(1).sub(/:+\s*\z/, "")}: " }
42
+ text = text.gsub(/<br\s*\/?>/i, " ")
43
+ text = text.gsub(/<[^>]+>/, " ")
44
+ text = text.gsub(/[*_~`]+/, "")
45
+ text = text.gsub(/^\s*>\s+/, "")
46
+ text = text.gsub(/^\s*[-*+]\s+/, "")
47
+ text = text.gsub(/^\s*\d+\.\s+/, "")
48
+ text.gsub(/\|/, " ")
49
+ end
50
+
51
+ def truncate(text, limit)
52
+ return text if text.length <= limit
53
+
54
+ "#{text[0, [limit - 3, 0].max].rstrip}..."
55
+ end
56
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "yaml"
5
+
6
+ require_relative "../git_chronology"
7
+ require_relative "../content_keys"
8
+ require_relative "../content_writing_paths"
9
+ require_relative "text_excerpt"
10
+
11
+ # Builds one writing-index record from a content Markdown file.
12
+ #
13
+ # Two callers need the identical record from the identical files:
14
+ # Jekyll::ContentWritingIndex (inside a Jekyll build) and ContentSiteAdapter
15
+ # (standalone, for scripts/content/export-content-json.rb). They differ only in how
16
+ # they derive the repo-relative path used for Git chronology, so each passes
17
+ # its own. Everything downstream lives here, because a field added or a bug
18
+ # fixed in one copy used to leave the other silently beNewsreader.
19
+ module WritingEntry
20
+ XCITE_RE = /\{%-?\s*include\s+xcite\.html\s+key=["']([^"']+)["'][^%]*-?%\}/
21
+
22
+ module_function
23
+
24
+ # Returns the record hash, or nil when the document is unpublished or keyless.
25
+ def build(absolute_path, collection, repo_relative_path)
26
+ text = File.read(absolute_path).sub(/\A\xEF\xBB\xBF/, "")
27
+ front_matter, body = split_front_matter(text)
28
+ return nil if front_matter["published"] == false
29
+
30
+ writing_type = ContentWritingPaths::INDEX_TYPE_FOR_COLLECTION.fetch(collection)
31
+ if %w[article post].include?(writing_type)
32
+ GitChronology.apply!(front_matter, repo_relative_path, writing_type)
33
+ publication_date = front_matter["date"]
34
+ else
35
+ publication_date = ContentKeys.normalize_date(front_matter["date"]) ||
36
+ ContentKeys.normalize_date(front_matter["start_date"])
37
+ end
38
+
39
+ key = ContentKeys.normalize_key(front_matter["key"])
40
+ return nil if key.empty?
41
+
42
+ path_segment = ContentKeys.path_segment(writing_type, key, date: publication_date)
43
+ xcites = Array(front_matter["xcites"]).map(&:to_s).reject(&:empty?)
44
+ xcites = body.scan(XCITE_RE).flatten.uniq if xcites.empty?
45
+
46
+ title = front_matter["title"].to_s
47
+ title = front_matter["product"].to_s if title.empty? && front_matter["product"]
48
+ title = front_matter["project"].to_s if title.empty? && front_matter["project"]
49
+ listing = TextExcerpt.listing_fields(front_matter, body)
50
+
51
+ {
52
+ "slug" => path_segment,
53
+ "key" => key,
54
+ "title" => title,
55
+ "date" => normalize_date(publication_date),
56
+ "last_updated" => normalize_date(front_matter["last_updated"]),
57
+ "url" => ContentKeys.detail_path(writing_type, key, date: publication_date),
58
+ "type" => collection,
59
+ "collection" => collection,
60
+ "category" => front_matter["category"].to_s,
61
+ "description" => listing["description"],
62
+ "excerpt" => listing["excerpt"],
63
+ "thumbnail" => front_matter["thumbnail"].to_s,
64
+ "xcites" => xcites,
65
+ "company" => front_matter["company"].to_s,
66
+ "product" => front_matter["product"].to_s,
67
+ "project" => front_matter["project"].to_s,
68
+ "tagline" => front_matter["tagline"].to_s,
69
+ "start_date" => front_matter["start_date"].to_s,
70
+ "end_date" => front_matter["end_date"].to_s,
71
+ "sort_order" => front_matter["sort_order"],
72
+ "status" => front_matter["status"].to_s,
73
+ "label" => front_matter["label"].to_s,
74
+ "icon" => front_matter["icon"].to_s,
75
+ "tags" => Array(front_matter["tags"]),
76
+ "external_url" => front_matter["external_url"].to_s,
77
+ "article_url" => front_matter["article_url"].to_s,
78
+ "github" => front_matter["github"].to_s,
79
+ "aliases" => ContentKeys.alias_slugs(
80
+ "legacy_segment" => File.basename(absolute_path, File.extname(absolute_path)),
81
+ "key" => key,
82
+ "redirects" => ContentKeys.normalize_redirects(front_matter),
83
+ "path_segment" => path_segment
84
+ )
85
+ }
86
+ end
87
+
88
+ def split_front_matter(text)
89
+ return [{}, text] unless text.start_with?("---\n")
90
+
91
+ parts = text.split(/^---\s*$\n?/, 3)
92
+ return [{}, text] unless parts.length == 3
93
+
94
+ data = YAML.safe_load(parts[1], permitted_classes: [Date, Time], aliases: true) || {}
95
+ [data, parts[2]]
96
+ end
97
+
98
+ def normalize_date(value)
99
+ case value
100
+ when Date then value.iso8601
101
+ when Time then value.to_date.iso8601
102
+ when String then value
103
+ else
104
+ value&.to_s
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jekyll/commands/serve"
4
+ require "json"
5
+ require "net/http"
6
+ require "uri"
7
+ require_relative "content_detail_paths"
8
+
9
+ # Mirrors Cloudflare _redirects detail rewrites during `jekyll serve` so
10
+ # /posts/:slug/ and /articles/:slug/ work locally without per-slug HTML files.
11
+ # Also handles the local dev re-export endpoint and proxies /api/studio/* to
12
+ # `wrangler pages dev` (./scripts/studio-api-dev.sh, default :8788).
13
+ module Jekyll
14
+ module ContentDetailRewrites
15
+ REEXPORT_PATHS = ["/__dev/reexport", "/api/dev/reexport"].freeze
16
+ STUDIO_API_PREFIX = "/api/studio"
17
+ STUDIO_API_DEFAULT_ORIGIN = "http://127.0.0.1:8788"
18
+
19
+ module_function
20
+
21
+ def studio_api_proxy_enabled?
22
+ env = ENV["STUDIO_API_PROXY"]
23
+ return false if env == "0" || env == "false"
24
+
25
+ true
26
+ end
27
+
28
+ def studio_api_path?(path)
29
+ path == STUDIO_API_PREFIX || path.start_with?("#{STUDIO_API_PREFIX}/")
30
+ end
31
+
32
+ def studio_api_origin
33
+ ENV.fetch("STUDIO_API_ORIGIN", STUDIO_API_DEFAULT_ORIGIN)
34
+ end
35
+
36
+ def proxy_studio_api!(req, res)
37
+ origin = studio_api_origin
38
+ target = URI.join(origin.end_with?("/") ? origin : "#{origin}/", req.path.sub(%r{\A/}, ""))
39
+ target.query = req.query_string unless req.query_string.to_s.empty?
40
+
41
+ http = Net::HTTP.new(target.host, target.port)
42
+ http.open_timeout = 2
43
+ http.read_timeout = 60
44
+ http.use_ssl = target.scheme == "https"
45
+
46
+ upstream =
47
+ case req.request_method
48
+ when "GET" then Net::HTTP::Get.new(target)
49
+ when "PUT" then Net::HTTP::Put.new(target)
50
+ when "POST" then Net::HTTP::Post.new(target)
51
+ when "OPTIONS" then Net::HTTP::Options.new(target)
52
+ when "HEAD" then Net::HTTP::Head.new(target)
53
+ else
54
+ res.status = 405
55
+ res["Content-Type"] = "application/json; charset=utf-8"
56
+ res.body = '{"error":"method_not_allowed"}'
57
+ return
58
+ end
59
+
60
+ %w[Authorization Content-Type Accept Origin].each do |name|
61
+ value = req[name]
62
+ upstream[name] = value if value && !value.empty?
63
+ end
64
+
65
+ if %w[PUT POST].include?(req.request_method)
66
+ body = req.body
67
+ upstream.body = body if body
68
+ end
69
+
70
+ begin
71
+ response = http.request(upstream)
72
+ rescue StandardError => e
73
+ res.status = 502
74
+ res["Content-Type"] = "application/json; charset=utf-8"
75
+ res.body = JSON.generate(
76
+ error: "studio_api_unavailable",
77
+ message: "Proxy to #{origin} failed: #{e.class}: #{e.message}. " \
78
+ "Start ./scripts/studio-api-dev.sh (wrangler pages dev)."
79
+ )
80
+ return
81
+ end
82
+
83
+ res.status = response.code.to_i
84
+ response.each_header do |key, value|
85
+ next if %w[transfer-encoding connection content-length].include?(key.downcase)
86
+
87
+ res[key] = value
88
+ end
89
+ res.body = response.body
90
+ end
91
+
92
+ def install_serve_patch!
93
+ require "jekyll/commands/serve/servlet"
94
+ servlet = Jekyll::Commands::Serve::Servlet
95
+ return if servlet.method_defined?(:content_detail_do_GET)
96
+
97
+ servlet.alias_method :content_detail_do_GET, :do_GET
98
+ servlet.class_eval do
99
+ def do_GET(req, res)
100
+ if ContentDetailRewrites.studio_api_proxy_enabled? &&
101
+ ContentDetailRewrites.studio_api_path?(req.path)
102
+ ContentDetailRewrites.proxy_studio_api!(req, res)
103
+ return
104
+ end
105
+
106
+ if ContentDetailRewrites::REEXPORT_PATHS.include?(req.path)
107
+ if defined?(ContentLocalServe)
108
+ ContentLocalServe.handle_reexport_request(req, res)
109
+ else
110
+ res.status = 501
111
+ res["Content-Type"] = "text/plain"
112
+ res.body = "Dev re-export handler not loaded"
113
+ end
114
+ return
115
+ end
116
+
117
+ target = ContentDetailPaths.rewrite_path(req.path)
118
+ if target
119
+ req.instance_variable_set(:@path, target)
120
+ req.instance_variable_set(:@path_info, target)
121
+ end
122
+ content_detail_do_GET(req, res)
123
+
124
+ if req.path.end_with?(".json")
125
+ res["Cache-Control"] = "no-cache, no-store, must-revalidate"
126
+ res["Pragma"] = "no-cache"
127
+ res["Expires"] = "0"
128
+ end
129
+ end
130
+
131
+ def do_POST(req, res)
132
+ if ContentDetailRewrites.studio_api_proxy_enabled? &&
133
+ ContentDetailRewrites.studio_api_path?(req.path)
134
+ ContentDetailRewrites.proxy_studio_api!(req, res)
135
+ return
136
+ end
137
+
138
+ if ContentDetailRewrites::REEXPORT_PATHS.include?(req.path)
139
+ if defined?(ContentLocalServe)
140
+ ContentLocalServe.handle_reexport_request(req, res)
141
+ else
142
+ res.status = 501
143
+ res["Content-Type"] = "text/plain"
144
+ res.body = "Dev re-export handler not loaded"
145
+ end
146
+ return
147
+ end
148
+
149
+ res.status = 405
150
+ res["Content-Type"] = "text/plain"
151
+ res.body = "Method Not Allowed"
152
+ end
153
+
154
+ def do_PUT(req, res)
155
+ if ContentDetailRewrites.studio_api_proxy_enabled? &&
156
+ ContentDetailRewrites.studio_api_path?(req.path)
157
+ ContentDetailRewrites.proxy_studio_api!(req, res)
158
+ return
159
+ end
160
+
161
+ res.status = 405
162
+ res["Content-Type"] = "text/plain"
163
+ res.body = "Method Not Allowed"
164
+ end
165
+
166
+ def do_OPTIONS(req, res)
167
+ if ContentDetailRewrites.studio_api_proxy_enabled? &&
168
+ ContentDetailRewrites.studio_api_path?(req.path)
169
+ ContentDetailRewrites.proxy_studio_api!(req, res)
170
+ return
171
+ end
172
+
173
+ res.status = 204
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ module Commands
180
+ class Serve
181
+ class << self
182
+ unless method_defined?(:content_detail_original_process)
183
+ alias_method :content_detail_original_process, :process
184
+
185
+ def process(options)
186
+ ContentLocalServe.store_serve_options(options) if defined?(ContentLocalServe)
187
+ ContentDetailRewrites.install_serve_patch!
188
+ content_detail_original_process(options)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end