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,26 @@
1
+ {
2
+ "name": "website-studio",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@clerk/clerk-js": "^6.36.0",
13
+ "@tiptap/core": "^3.31.4",
14
+ "@tiptap/extension-color": "^3.31.4",
15
+ "@tiptap/extension-link": "^3.31.4",
16
+ "@tiptap/extension-placeholder": "^3.31.4",
17
+ "@tiptap/extension-text-style": "^3.31.4",
18
+ "@tiptap/pm": "^3.31.4",
19
+ "@tiptap/starter-kit": "^3.31.4",
20
+ "js-yaml": "^4.1.0",
21
+ "marked": "^15.0.7"
22
+ },
23
+ "devDependencies": {
24
+ "vite": "^6.2.0"
25
+ }
26
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "clerk-backend-api": {
5
+ "source": "clerk/skills",
6
+ "sourceType": "github",
7
+ "skillPath": "skills/core/clerk-backend-api/SKILL.md",
8
+ "computedHash": "723ee35b2727923bbc9050c103369985d3d2ced00c9d25d79ff6383a23d8fea9"
9
+ },
10
+ "clerk-cli": {
11
+ "source": "clerk/skills",
12
+ "sourceType": "github",
13
+ "skillPath": "skills/core/clerk-cli/SKILL.md",
14
+ "computedHash": "5a29712cce41e427fce609e0717a7d1868baf276014711f58a3f8e0fddde9006"
15
+ },
16
+ "clerk-custom-ui": {
17
+ "source": "clerk/skills",
18
+ "sourceType": "github",
19
+ "skillPath": "skills/core/clerk-custom-ui/SKILL.md",
20
+ "computedHash": "91291f48e2f1734d5395a2038385ad8ee39db2a66b29081d8b2f2b72d454e3f9"
21
+ },
22
+ "clerk-orgs": {
23
+ "source": "clerk/skills",
24
+ "sourceType": "github",
25
+ "skillPath": "skills/features/clerk-orgs/SKILL.md",
26
+ "computedHash": "82f66ef3c66f85c5241842fb2ab98c8970e1c812ede562576c922d5bbf5c3045"
27
+ },
28
+ "clerk-setup": {
29
+ "source": "clerk/skills",
30
+ "sourceType": "github",
31
+ "skillPath": "skills/core/clerk-setup/SKILL.md",
32
+ "computedHash": "72c5baafa847dd0e8ccb7310ca769825114c14abe0214a1dca6ba0bfbb3ca2a8"
33
+ },
34
+ "clerk-testing": {
35
+ "source": "clerk/skills",
36
+ "sourceType": "github",
37
+ "skillPath": "skills/features/clerk-testing/SKILL.md",
38
+ "computedHash": "2e2172e0ef81c58cec2bdb7ff5edefbd1ba424e9fca931e0cb80a1bd8bfd4e80"
39
+ },
40
+ "clerk-webhooks": {
41
+ "source": "clerk/skills",
42
+ "sourceType": "github",
43
+ "skillPath": "skills/features/clerk-webhooks/SKILL.md",
44
+ "computedHash": "e3c348bdc6dee1f28bc56577cd4aac605d3dfc3c63fcb10fc51e6c04c1fe3fb0"
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,71 @@
1
+ let getToken = async () => null;
2
+
3
+ export function setTokenGetter(fn) {
4
+ getToken = fn;
5
+ }
6
+
7
+ async function api(path, init = {}) {
8
+ const token = await getToken();
9
+ const headers = {
10
+ Accept: "application/json",
11
+ ...(init.headers || {}),
12
+ };
13
+ if (token) headers.Authorization = `Bearer ${token}`;
14
+ if (init.body && !headers["Content-Type"]) {
15
+ headers["Content-Type"] = "application/json";
16
+ }
17
+ const res = await fetch(`/api/studio/${path.replace(/^\//, "")}`, {
18
+ ...init,
19
+ headers,
20
+ });
21
+ const text = await res.text();
22
+ let data = null;
23
+ try {
24
+ data = text ? JSON.parse(text) : null;
25
+ } catch {
26
+ data = { raw: text };
27
+ }
28
+ if (!res.ok) {
29
+ // Prefer the sentence. Joining the code in front (`server_error: …`) is
30
+ // what a narrow toolbar ellipsizes down to the code alone.
31
+ const message = String(data?.message || data?.error || `HTTP ${res.status}`);
32
+ const err = new Error(message);
33
+ err.status = res.status;
34
+ err.code = data?.error || "";
35
+ err.data = data;
36
+ throw err;
37
+ }
38
+ return data;
39
+ }
40
+
41
+ export const studioApi = {
42
+ session: () => api("session"),
43
+ tree: (collectionPath) =>
44
+ api(`tree?path=${encodeURIComponent(collectionPath)}`),
45
+ getFile: (path) => api(`file?path=${encodeURIComponent(path)}`),
46
+ putFile: ({ path, content, message, sha }) =>
47
+ api("file", {
48
+ method: "PUT",
49
+ body: JSON.stringify({ path, content, message, sha }),
50
+ }),
51
+ listMedia: (dirPath) =>
52
+ api(`media?path=${encodeURIComponent(dirPath || "content/media")}`),
53
+ uploadMedia: ({ path, content_base64, message }) =>
54
+ api("media", {
55
+ method: "POST",
56
+ body: JSON.stringify({ path, content_base64, message }),
57
+ }),
58
+ createMediaFolder: ({ path, message }) =>
59
+ api("media/folder", {
60
+ method: "POST",
61
+ body: JSON.stringify({ path, message }),
62
+ }),
63
+ searchXcite: (q) => api(`search/xcite?q=${encodeURIComponent(q || "")}`),
64
+ searchCite: (q) => api(`search/cite?q=${encodeURIComponent(q || "")}`),
65
+ /** Correct / Refine — docs/features/studio-proof.md (AC-PRF-01). */
66
+ rewrite: (payload) =>
67
+ api("rewrite", {
68
+ method: "POST",
69
+ body: JSON.stringify(payload),
70
+ }),
71
+ };
@@ -0,0 +1,70 @@
1
+ import { Editor } from "@tiptap/core";
2
+ import StarterKit from "@tiptap/starter-kit";
3
+ import Link from "@tiptap/extension-link";
4
+ import Placeholder from "@tiptap/extension-placeholder";
5
+ import { TextStyle } from "@tiptap/extension-text-style";
6
+ import { Color } from "@tiptap/extension-color";
7
+ import {
8
+ INCLUDE_DEFS,
9
+ LiquidIncludeNode,
10
+ markdownToEditorContent,
11
+ serializeDocToMarkdown,
12
+ } from "./liquid.js";
13
+
14
+ export function createBodyEditor(element, { content = "", onUpdate } = {}) {
15
+ const liquidNodes = INCLUDE_DEFS.map(LiquidIncludeNode);
16
+
17
+ const editor = new Editor({
18
+ element,
19
+ extensions: [
20
+ StarterKit.configure({
21
+ heading: { levels: [1, 2, 3, 4] },
22
+ }),
23
+ TextStyle,
24
+ Color,
25
+ Link.configure({
26
+ autolink: false,
27
+ linkOnPaste: false,
28
+ openOnClick: false,
29
+ }),
30
+ Placeholder.configure({ placeholder: "Write… Use Insert for Liquid blocks." }),
31
+ ...liquidNodes,
32
+ ],
33
+ content: markdownToEditorContent(content),
34
+ onUpdate: ({ editor: ed }) => {
35
+ if (onUpdate) onUpdate(serializeDocToMarkdown(ed));
36
+ },
37
+ });
38
+
39
+ return {
40
+ editor,
41
+ getMarkdown: () => serializeDocToMarkdown(editor),
42
+ setMarkdown: (md) => {
43
+ editor.commands.setContent(markdownToEditorContent(md));
44
+ },
45
+ destroy: () => editor.destroy(),
46
+ insertInclude: (defName, attrs = {}) => {
47
+ const def = INCLUDE_DEFS.find((d) => d.name === defName);
48
+ if (!def) return;
49
+ editor.commands.insertContent({ type: def.name, attrs });
50
+ },
51
+ transformSelectionCase: (mode) => {
52
+ const { from, to, empty } = editor.state.selection;
53
+ if (empty) return;
54
+ const text = editor.state.doc.textBetween(from, to, "");
55
+ let next = text;
56
+ if (mode === "lower") next = text.toLocaleLowerCase();
57
+ else if (mode === "upper") next = text.toLocaleUpperCase();
58
+ else if (mode === "title") {
59
+ next = text.replace(/\S+/g, (word) => {
60
+ const first = [...word][0] || "";
61
+ const rest = [...word].slice(1).join("");
62
+ return first.toLocaleUpperCase() + rest.toLocaleLowerCase();
63
+ });
64
+ } else return;
65
+ editor.chain().focus().insertContentAt({ from, to }, next).run();
66
+ },
67
+ };
68
+ }
69
+
70
+ export { INCLUDE_DEFS };
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Pure Liquid include serialize/parse helpers (no TipTap).
3
+ * Used by the editor and by scripts/test/test-studio-liquid-roundtrip.mjs.
4
+ */
5
+
6
+ export const INCLUDE_DEFS = [
7
+ {
8
+ name: "figure",
9
+ include: "figure.html",
10
+ label: "Figure",
11
+ fields: ["src", "alt", "caption", "source", "width"],
12
+ },
13
+ {
14
+ name: "cutIn",
15
+ include: "cut-in.html",
16
+ label: "Cut-in",
17
+ fields: ["src", "alt", "side"],
18
+ },
19
+ {
20
+ name: "youtube",
21
+ include: "youtube.html",
22
+ label: "YouTube",
23
+ fields: ["id", "caption", "width"],
24
+ },
25
+ {
26
+ name: "youtubeShort",
27
+ include: "youtube-short.html",
28
+ label: "YouTube Short",
29
+ fields: ["src", "src2"],
30
+ },
31
+ {
32
+ name: "video",
33
+ include: "video.html",
34
+ label: "Video",
35
+ fields: ["src", "caption", "width"],
36
+ },
37
+ {
38
+ name: "plotly",
39
+ include: "plotly.html",
40
+ label: "Plotly",
41
+ fields: ["src", "caption", "width", "height", "source", "alt"],
42
+ },
43
+ {
44
+ name: "xcite",
45
+ include: "xcite.html",
46
+ label: "Xcite",
47
+ fields: ["key"],
48
+ inline: true,
49
+ },
50
+ {
51
+ name: "cite",
52
+ include: "cite.html",
53
+ label: "Cite",
54
+ fields: ["key"],
55
+ inline: true,
56
+ },
57
+ {
58
+ name: "articleNote",
59
+ include: "article-note",
60
+ label: "Article note",
61
+ fields: ["body"],
62
+ blockBody: true,
63
+ },
64
+ {
65
+ name: "poem",
66
+ include: "poem",
67
+ label: "Poem",
68
+ fields: ["body"],
69
+ blockBody: true,
70
+ },
71
+ ];
72
+
73
+ export function attrsToInclude(def, attrs) {
74
+ if (def.blockBody) {
75
+ const body = attrs.body || "";
76
+ return `{% include ${def.include} markdown="1" %}\n${body}\n{% endinclude %}`;
77
+ }
78
+ const parts = def.fields
79
+ .filter((f) => attrs[f] != null && String(attrs[f]).length)
80
+ .map((f) => `${f}="${String(attrs[f]).replace(/"/g, """)}"`);
81
+ if (!parts.length) return `{% include ${def.include} %}`;
82
+ return `{% include ${def.include} ${parts.join(" ")} %}`;
83
+ }
84
+
85
+ export function parseIncludes(markdown) {
86
+ const text = String(markdown || "");
87
+ const segments = [];
88
+ // Stop at `%}` only — attrs often contain `%` (width="80%", percent-encoded URLs).
89
+ const selfRe = /\{%-?\s*include\s+([\w.-]+)\s*((?:(?!%\})[\s\S])*?)\s*-?%\}/g;
90
+ let last = 0;
91
+ let m;
92
+ while ((m = selfRe.exec(text))) {
93
+ const includeName = m[1];
94
+ const def = INCLUDE_DEFS.find((d) => d.include === includeName);
95
+ if (!def || def.blockBody) continue;
96
+ if (m.index > last) {
97
+ segments.push({ type: "text", text: text.slice(last, m.index) });
98
+ }
99
+ const attrs = parseAttrString(m[2]);
100
+ segments.push({ type: "include", def, attrs, raw: m[0] });
101
+ last = m.index + m[0].length;
102
+ }
103
+ if (last < text.length) {
104
+ segments.push({ type: "text", text: text.slice(last) });
105
+ }
106
+ return segments;
107
+ }
108
+
109
+ function parseAttrString(raw) {
110
+ const attrs = {};
111
+ String(raw || "").replace(
112
+ /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g,
113
+ (_, key, d, s) => {
114
+ attrs[key] = d != null ? d : s;
115
+ return "";
116
+ }
117
+ );
118
+ return attrs;
119
+ }
120
+
121
+ const INCLUDE_RE =
122
+ /\{%-?\s*include\s+([\w.-]+)\s*((?:(?!%\})[\s\S])*?)\s*-?%\}/g;
123
+
124
+ function defForInclude(includeName) {
125
+ return INCLUDE_DEFS.find((d) => d.include === includeName);
126
+ }
127
+
128
+ /** A chunk that is one include and nothing else, newlines inside the tag allowed. */
129
+ function matchSoloInclude(chunk) {
130
+ const t = String(chunk || "").trim();
131
+ if (!t.startsWith("{%")) return null;
132
+ const block = t.match(
133
+ /^\{%-?\s*include\s+([\w.-]+)\b[^%]*?-?%\}\s*([\s\S]*?)\s*\{%-?\s*endinclude\s*-?%\}$/
134
+ );
135
+ if (block) {
136
+ const def = defForInclude(block[1]);
137
+ if (!def?.blockBody) return null;
138
+ return {
139
+ type: def.name,
140
+ attrs: { body: block[2].replace(/^\n|\n$/g, ""), raw: t },
141
+ };
142
+ }
143
+ const self = t.match(
144
+ /^\{%-?\s*include\s+([\w.-]+)\s*((?:(?!%\})[\s\S])*?)\s*-?%\}$/
145
+ );
146
+ if (!self) return null;
147
+ const def = defForInclude(self[1]);
148
+ if (!def || def.inline || def.blockBody) return null;
149
+ return includeNode(def, self[2], self[0]);
150
+ }
151
+
152
+ function includeNode(def, attrSource, raw) {
153
+ return {
154
+ type: def.name,
155
+ attrs: { ...parseAttrString(attrSource), raw },
156
+ };
157
+ }
158
+
159
+ function rawStillMatches(def, attrs) {
160
+ const raw = attrs?.raw;
161
+ if (typeof raw !== "string" || !raw) return false;
162
+ if (def.blockBody) {
163
+ const current = attrs.body == null ? "" : String(attrs.body);
164
+ return raw.includes(current);
165
+ }
166
+ const fromRaw = parseAttrString(raw);
167
+ for (const field of def.fields) {
168
+ const current = attrs[field] == null ? "" : String(attrs[field]);
169
+ const original = fromRaw[field] == null ? "" : String(fromRaw[field]);
170
+ if (current !== original) return false;
171
+ }
172
+ return true;
173
+ }
174
+
175
+ function includeMarkdown(def, attrs) {
176
+ if (rawStillMatches(def, attrs)) return attrs.raw;
177
+ return attrsToInclude(def, attrs);
178
+ }
179
+
180
+ function paragraphNodes(text) {
181
+ const src = String(text || "");
182
+ const nodes = [];
183
+ const re = new RegExp(INCLUDE_RE.source, "g");
184
+ let last = 0;
185
+ let m;
186
+ while ((m = re.exec(src))) {
187
+ if (m.index > last) {
188
+ nodes.push({ type: "text", text: src.slice(last, m.index) });
189
+ }
190
+ const def = defForInclude(m[1]);
191
+ if (def?.inline) {
192
+ nodes.push(includeNode(def, m[2], m[0]));
193
+ } else {
194
+ nodes.push({ type: "text", text: m[0] });
195
+ }
196
+ last = m.index + m[0].length;
197
+ }
198
+ if (last < src.length) {
199
+ nodes.push({ type: "text", text: src.slice(last) });
200
+ }
201
+ return nodes.filter((n) => n.type !== "text" || n.text !== "");
202
+ }
203
+
204
+ function stripOuterBlankLines(chunk) {
205
+ return String(chunk || "").replace(/^\n+|\n+$/g, "");
206
+ }
207
+
208
+ function pushPlainChunk(content, text) {
209
+ const line = stripOuterBlankLines(text);
210
+ if (!line) return;
211
+ const heading = line.match(/^(#{1,6})\s+([\s\S]*)$/);
212
+ if (heading && !line.includes("\n") && line.trim() === line) {
213
+ content.push({
214
+ type: "heading",
215
+ attrs: { level: heading[1].length },
216
+ content: paragraphNodes(heading[2]),
217
+ });
218
+ return;
219
+ }
220
+ content.push({ type: "paragraph", content: paragraphNodes(line) });
221
+ }
222
+
223
+ function pushRichChunk(content, chunk) {
224
+ const trimmed = stripOuterBlankLines(chunk);
225
+ if (!trimmed) return;
226
+ const solo = matchSoloInclude(trimmed);
227
+ if (solo) {
228
+ content.push(solo);
229
+ return;
230
+ }
231
+
232
+ const re = new RegExp(INCLUDE_RE.source, "g");
233
+ const parts = [];
234
+ let last = 0;
235
+ let m;
236
+ let sawBlock = false;
237
+ while ((m = re.exec(trimmed))) {
238
+ const def = defForInclude(m[1]);
239
+ if (def && !def.inline && !def.blockBody) {
240
+ sawBlock = true;
241
+ parts.push({ kind: "text", text: trimmed.slice(last, m.index) });
242
+ parts.push({
243
+ kind: "block",
244
+ node: includeNode(def, m[2], m[0]),
245
+ });
246
+ last = m.index + m[0].length;
247
+ }
248
+ }
249
+ if (!sawBlock) {
250
+ pushPlainChunk(content, trimmed);
251
+ return;
252
+ }
253
+ parts.push({ kind: "text", text: trimmed.slice(last) });
254
+ for (const part of parts) {
255
+ if (part.kind === "block") content.push(part.node);
256
+ else if (part.text.trim()) pushPlainChunk(content, part.text);
257
+ }
258
+ }
259
+
260
+ function pushMarkdownChunks(content, text) {
261
+ const fenceRe = /```([^\n`]*)\n([\s\S]*?)```/g;
262
+ let last = 0;
263
+ let m;
264
+ const src = String(text || "");
265
+ while ((m = fenceRe.exec(src))) {
266
+ if (m.index > last) pushProseChunks(content, src.slice(last, m.index));
267
+ const lang = String(m[1] || "").trim();
268
+ const code = String(m[2] || "").replace(/\n$/, "");
269
+ content.push({
270
+ type: "codeBlock",
271
+ attrs: { language: lang || null },
272
+ content: code ? [{ type: "text", text: code }] : [],
273
+ });
274
+ last = m.index + m[0].length;
275
+ }
276
+ if (last < src.length) pushProseChunks(content, src.slice(last));
277
+ }
278
+
279
+ function pushProseChunks(content, text) {
280
+ const chunks = String(text || "").split(/\n{2,}/);
281
+ for (const chunk of chunks) pushRichChunk(content, chunk);
282
+ }
283
+
284
+ export function markdownToEditorContent(markdown) {
285
+ const content = [];
286
+ pushMarkdownChunks(content, markdown);
287
+ if (!content.length) content.push({ type: "paragraph" });
288
+ return { type: "doc", content };
289
+ }
290
+
291
+ function inlineToMd(nodes) {
292
+ return (nodes || [])
293
+ .map((n) => {
294
+ if (n.type === "text") {
295
+ let t = n.text || "";
296
+ const marks = n.marks || [];
297
+ for (const mark of marks) {
298
+ if (mark.type === "bold") t = `**${t}**`;
299
+ if (mark.type === "italic") t = `*${t}*`;
300
+ if (mark.type === "strike") t = `~~${t}~~`;
301
+ if (mark.type === "code") t = `\`${t}\``;
302
+ if (mark.type === "link") t = `[${t}](${mark.attrs?.href || ""})`;
303
+ if (mark.type === "textStyle" && mark.attrs?.color) {
304
+ t = `<span style="color:${mark.attrs.color}">${t}</span>`;
305
+ }
306
+ }
307
+ return t;
308
+ }
309
+ if (n.type === "hardBreak") return " \n";
310
+ const def = INCLUDE_DEFS.find((d) => d.name === n.type);
311
+ if (def) return includeMarkdown(def, n.attrs || {});
312
+ return "";
313
+ })
314
+ .join("");
315
+ }
316
+
317
+ function nodesToMarkdown(nodes) {
318
+ return (nodes || [])
319
+ .map((node) => {
320
+ const def = INCLUDE_DEFS.find((d) => d.name === node.type);
321
+ if (def) {
322
+ const block = includeMarkdown(def, node.attrs || {});
323
+ return block.endsWith("\n") ? `${block}\n` : `${block}\n\n`;
324
+ }
325
+ switch (node.type) {
326
+ case "paragraph":
327
+ return `${inlineToMd(node.content || [])}\n\n`;
328
+ case "heading": {
329
+ const level = node.attrs?.level || 1;
330
+ return `${"#".repeat(level)} ${inlineToMd(node.content || [])}\n\n`;
331
+ }
332
+ case "blockquote":
333
+ return (
334
+ nodesToMarkdown(node.content || [])
335
+ .split("\n")
336
+ .map((l) => (l ? `> ${l}` : ">"))
337
+ .join("\n") + "\n\n"
338
+ );
339
+ case "bulletList":
340
+ return (
341
+ (node.content || [])
342
+ .map((li) => `- ${nodesToMarkdown(li.content || []).trim()}`)
343
+ .join("\n") + "\n\n"
344
+ );
345
+ case "orderedList":
346
+ return (
347
+ (node.content || [])
348
+ .map(
349
+ (li, i) =>
350
+ `${i + 1}. ${nodesToMarkdown(li.content || []).trim()}`
351
+ )
352
+ .join("\n") + "\n\n"
353
+ );
354
+ case "codeBlock": {
355
+ const lang = node.attrs?.language || "";
356
+ return (
357
+ "```" +
358
+ lang +
359
+ "\n" +
360
+ (node.content || []).map((c) => c.text || "").join("") +
361
+ "\n```\n\n"
362
+ );
363
+ }
364
+ case "horizontalRule":
365
+ return "---\n\n";
366
+ default:
367
+ if (node.content) return nodesToMarkdown(node.content);
368
+ return "";
369
+ }
370
+ })
371
+ .join("");
372
+ }
373
+
374
+ export function serializeContent(nodes) {
375
+ return nodesToMarkdown(nodes).trim() + "\n";
376
+ }
377
+
378
+ /** Parse then serialize. Includes stay inline or one line, with the spaces around them. */
379
+ export function roundTripMarkdown(markdown) {
380
+ const doc = markdownToEditorContent(markdown);
381
+ return serializeContent(doc.content || []);
382
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Atomic Liquid include blocks for TipTap.
3
+ * Spec: docs/features/studio.md §6 — AC-STU-03, AC-STU-04.
4
+ */
5
+
6
+ import { Node, mergeAttributes } from "@tiptap/core";
7
+ import {
8
+ INCLUDE_DEFS,
9
+ attrsToInclude,
10
+ parseIncludes,
11
+ markdownToEditorContent,
12
+ serializeContent,
13
+ } from "./liquid-core.js";
14
+
15
+ export {
16
+ INCLUDE_DEFS,
17
+ attrsToInclude,
18
+ parseIncludes,
19
+ markdownToEditorContent,
20
+ serializeContent,
21
+ };
22
+
23
+ export function LiquidIncludeNode(def) {
24
+ return Node.create({
25
+ name: def.name,
26
+ group: def.inline ? "inline" : "block",
27
+ inline: !!def.inline,
28
+ atom: true,
29
+ selectable: true,
30
+ draggable: true,
31
+ addAttributes() {
32
+ const attrs = {};
33
+ for (const f of def.fields) {
34
+ attrs[f] = { default: "" };
35
+ }
36
+ attrs.raw = { default: "" };
37
+ return attrs;
38
+ },
39
+ parseHTML() {
40
+ return [{ tag: `div[data-liquid="${def.name}"]` }];
41
+ },
42
+ renderHTML({ HTMLAttributes }) {
43
+ const summary = def.fields
44
+ .map((f) => HTMLAttributes[f])
45
+ .filter(Boolean)
46
+ .slice(0, 2)
47
+ .join(" | ");
48
+ const tag = def.inline ? "span" : "div";
49
+ return [
50
+ tag,
51
+ mergeAttributes(HTMLAttributes, {
52
+ "data-liquid": def.name,
53
+ class: def.inline ? "liquid-block liquid-block--inline" : "liquid-block",
54
+ "data-include": def.include,
55
+ }),
56
+ ["strong", {}, def.label],
57
+ [tag === "span" ? "span" : "div", {}, summary || "(empty)"],
58
+ ];
59
+ },
60
+ addCommands() {
61
+ return {
62
+ [`insert${def.name.charAt(0).toUpperCase()}${def.name.slice(1)}`]:
63
+ (attrs = {}) =>
64
+ ({ commands }) =>
65
+ commands.insertContent({
66
+ type: def.name,
67
+ attrs,
68
+ }),
69
+ };
70
+ },
71
+ });
72
+ }
73
+
74
+ export function serializeDocToMarkdown(editor) {
75
+ const json = editor.getJSON();
76
+ return serializeContent(json.content || []);
77
+ }