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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +77 -0
- data/DESIGN.md +1434 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/_data/themes/dark.yml +97 -0
- data/_data/themes/light.yml +120 -0
- data/_data/themes/shared.yml +37 -0
- data/_data/translations.yml +85 -0
- data/_includes/chord-tab.html +38 -0
- data/_includes/content-runtime/00-config.html +22 -0
- data/_includes/content-runtime/10-utils.html +176 -0
- data/_includes/content-runtime/20-markdown.html +264 -0
- data/_includes/content-runtime/25-code-blocks.html +315 -0
- data/_includes/content-runtime/30-includes-bib.html +190 -0
- data/_includes/content-runtime/30-includes-xcite.html +122 -0
- data/_includes/content-runtime/30-includes.html +365 -0
- data/_includes/content-runtime/40-listing.html +225 -0
- data/_includes/content-runtime/41-listing-helpers.html +71 -0
- data/_includes/content-runtime/42-listing-filter.html +208 -0
- data/_includes/content-runtime/43-blocks.html +379 -0
- data/_includes/content-runtime/50-detail.html +861 -0
- data/_includes/content-runtime/55-toc.html +176 -0
- data/_includes/content-runtime/60-fragments.html +567 -0
- data/_includes/content-runtime/90-bootstrap.html +76 -0
- data/_includes/cut-in.html +10 -0
- data/_includes/cv/cv-box.html +18 -0
- data/_includes/dict-card.html +24 -0
- data/_includes/figure.html +36 -0
- data/_includes/home/band-index.html +43 -0
- data/_includes/home/band-listing.html +49 -0
- data/_includes/mathjax.html +10 -0
- data/_includes/ongoing-product.html +1 -0
- data/_includes/page/content-runtime.html +20 -0
- data/_includes/page/date_evolution.html +19 -0
- data/_includes/page/figure-lightbox.html +102 -0
- data/_includes/page/formatted_date.html +15 -0
- data/_includes/page/hero-default.html +18 -0
- data/_includes/page/listing-runtime-attrs.html +8 -0
- data/_includes/page/mermaid.html +101 -0
- data/_includes/page/network-graph.html +1253 -0
- data/_includes/page/page-header.html +41 -0
- data/_includes/page/professional-connections.html +2 -0
- data/_includes/page/site-footer.html +101 -0
- data/_includes/page/timeline-section.html +868 -0
- data/_includes/plotly.html +22 -0
- data/_includes/theme/dev-reexport.html +100 -0
- data/_includes/theme/favicons.html +4 -0
- data/_includes/theme/font-loader.html +55 -0
- data/_includes/theme/shell-theme-boot.html +115 -0
- data/_includes/theme/site-logo.html +6 -0
- data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
- data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
- data/_includes/theme/theme-context.liquid +20 -0
- data/_includes/theme/theme-toggle.html +94 -0
- data/_includes/theme/theme-vars-block.liquid +161 -0
- data/_includes/theme/theme-vars.html +16 -0
- data/_includes/transcription.html +80 -0
- data/_includes/tweet.html +22 -0
- data/_includes/video.html +11 -0
- data/_includes/xcite.html +3 -0
- data/_includes/youtube-short.html +26 -0
- data/_includes/youtube.html +12 -0
- data/_layouts/article.html +27 -0
- data/_layouts/articles.html +534 -0
- data/_layouts/bibliography.html +417 -0
- data/_layouts/blog.html +544 -0
- data/_layouts/cv.html +393 -0
- data/_layouts/home.html +319 -0
- data/_layouts/network.html +9 -0
- data/_layouts/pandorga/articles.html +4 -0
- data/_layouts/pandorga/bibliography.html +4 -0
- data/_layouts/pandorga/blog.html +4 -0
- data/_layouts/pandorga/cv.html +4 -0
- data/_layouts/pandorga/media.html +4 -0
- data/_layouts/pandorga/network.html +4 -0
- data/_layouts/pandorga/portfolio.html +4 -0
- data/_layouts/post.html +7 -0
- data/_layouts/product.html +5 -0
- data/_layouts/project.html +27 -0
- data/_layouts/projects.html +534 -0
- data/_layouts/resources.html +387 -0
- data/_layouts/shell.html +203 -0
- data/_layouts/text.html +246 -0
- data/_plugins/bibliography_artifact.rb +57 -0
- data/_plugins/content_backend.rb +37 -0
- data/_plugins/content_collections.rb +51 -0
- data/_plugins/content_detail_paths.rb +58 -0
- data/_plugins/content_headers.rb +73 -0
- data/_plugins/content_keys.rb +310 -0
- data/_plugins/content_local_serve.rb +168 -0
- data/_plugins/content_site_adapter.rb +159 -0
- data/_plugins/content_validators_hook.rb +20 -0
- data/_plugins/content_writing_index.rb +52 -0
- data/_plugins/content_writing_paths.rb +55 -0
- data/_plugins/force_utf8_encoding.rb +13 -0
- data/_plugins/git_chronology.rb +211 -0
- data/_plugins/git_metadata.rb +43 -0
- data/_plugins/job_sections.rb +25 -0
- data/_plugins/lib/content_validators.rb +27 -0
- data/_plugins/lib/resource_validator.rb +56 -0
- data/_plugins/lib/tag_validator.rb +153 -0
- data/_plugins/lib/text_excerpt.rb +56 -0
- data/_plugins/lib/writing_entry.rb +107 -0
- data/_plugins/local_detail_rewrites.rb +194 -0
- data/_plugins/timeline.rb +507 -0
- data/_plugins/writing_slug_limits.rb +70 -0
- data/assets/css/article.css +1330 -0
- data/assets/css/base.css +1432 -0
- data/assets/css/cv.css +13 -0
- data/assets/css/global-link.css +94 -0
- data/assets/css/ledger/bibliography.css +307 -0
- data/assets/css/ledger/chrome.css +118 -0
- data/assets/css/ledger/cv.css +836 -0
- data/assets/css/ledger/detail.css +1270 -0
- data/assets/css/ledger/footer.css +298 -0
- data/assets/css/ledger/home.css +1490 -0
- data/assets/css/ledger/network.css +106 -0
- data/assets/css/ledger/projects.css +716 -0
- data/assets/css/ledger/rascunhos.css +379 -0
- data/assets/css/ledger/sources.css +327 -0
- data/assets/css/ledger/writing.css +754 -0
- data/assets/tailwind-play.js +15 -0
- data/examples/full/Gemfile +7 -0
- data/examples/full/_config.yml +92 -0
- data/examples/full/_redirects +3 -0
- data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
- data/examples/full/content/collections/data/contacts.yml +3 -0
- data/examples/full/content/collections/data/education.yml +4 -0
- data/examples/full/content/collections/jobs/research-fellow.md +12 -0
- data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
- data/examples/full/content/collections/products/ledger-tool.md +6 -0
- data/examples/full/content/collections/projects/demo-kit.md +10 -0
- data/examples/full/content/collections/resources/style-guide.md +7 -0
- data/examples/full/content/pages/cv/summary.md +1 -0
- data/examples/full/content/pages/headers.yml +20 -0
- data/examples/full/index.html +4 -0
- data/examples/minimal/Gemfile +7 -0
- data/examples/minimal/_config.yml +47 -0
- data/examples/minimal/_redirects +3 -0
- data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
- data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
- data/examples/minimal/content/pages/headers.yml +6 -0
- data/examples/minimal/index.html +4 -0
- data/exe/pandorga +7 -0
- data/functions/api/studio/[[path]].js +562 -0
- data/functions/api/studio/_lib/auth.js +131 -0
- data/functions/api/studio/_lib/blocks.js +156 -0
- data/functions/api/studio/_lib/gemini.js +62 -0
- data/functions/api/studio/_lib/github.js +278 -0
- data/functions/api/studio/_lib/guidelines.js +143 -0
- data/functions/api/studio/_lib/paths.js +76 -0
- data/functions/api/studio/_lib/prompts.js +165 -0
- data/functions/api/studio/_lib/rate-limit.js +40 -0
- data/functions/api/studio/_lib/rewrite.js +136 -0
- data/functions/api/studio/_lib/sanitize.js +143 -0
- data/lib/pandorga/cli.rb +66 -0
- data/lib/pandorga/commands/doctor.rb +66 -0
- data/lib/pandorga/commands/export.rb +30 -0
- data/lib/pandorga/commands/install_functions.rb +41 -0
- data/lib/pandorga/commands/new.rb +113 -0
- data/lib/pandorga/commands/publish.rb +53 -0
- data/lib/pandorga/commands/serve.rb +34 -0
- data/lib/pandorga/commands/studio_schema.rb +309 -0
- data/lib/pandorga/jekyll/generator.rb +74 -0
- data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
- data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
- data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
- data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
- data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
- data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
- data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
- data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
- data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
- data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
- data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
- data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
- data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
- data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
- data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
- data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
- data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
- data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
- data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
- data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
- data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
- data/lib/pandorga/jekyll.rb +15 -0
- data/lib/pandorga/object_store_env.rb +51 -0
- data/lib/pandorga/registry/schema.yml +115 -0
- data/lib/pandorga/registry.rb +279 -0
- data/lib/pandorga/version.rb +5 -0
- data/lib/pandorga.rb +19 -0
- data/scripts/cdn-allowlist.json +19 -0
- data/scripts/content/backfill-content-keys.rb +99 -0
- data/scripts/content/band_art.rb +362 -0
- data/scripts/content/export-content-json.rb +741 -0
- data/scripts/content/flatten-ansi-capture.mjs +153 -0
- data/scripts/content/hero_portraits.rb +28 -0
- data/scripts/content/publish-content-to-object-store.sh +78 -0
- data/scripts/content/publish-content-to-r2.sh +5 -0
- data/scripts/content/sync-content-json-to-r2.rb +167 -0
- data/scripts/content/sync-content-redirects.rb +12 -0
- data/scripts/generate-configuration-doc.rb +127 -0
- data/scripts/lib/content_license.rb +50 -0
- data/scripts/lib/content_storage_config.rb +70 -0
- data/scripts/lib/normalize-display-math.mjs +52 -0
- data/scripts/lib/r2_json_sync.rb +76 -0
- data/scripts/lib/withdrawn_public_objects.rb +23 -0
- data/scripts/lib/writing_index_contract.rb +61 -0
- data/scripts/pdf/md-to-pdf.mjs +1400 -0
- data/scripts/pdf/md-to-pdf.sh +27 -0
- data/scripts/test/fixtures/dual-template/_config.yml +32 -0
- data/scripts/test/fixtures/dual-template/index.html +4 -0
- data/scripts/test/render-parity/article-note.json +5 -0
- data/scripts/test/render-parity/blockquote.json +5 -0
- data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
- data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
- data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
- data/scripts/test/render-parity/blocks/archive-card.json +32 -0
- data/scripts/test/render-parity/blocks/cta-link.json +13 -0
- data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
- data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
- data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
- data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
- data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
- data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
- data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
- data/scripts/test/render-parity/blocks/section-banner.json +13 -0
- data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
- data/scripts/test/render-parity/blocks/system-card.json +48 -0
- data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
- data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
- data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
- data/scripts/test/render-parity/cut-in-right.json +9 -0
- data/scripts/test/render-parity/cut-in.json +8 -0
- data/scripts/test/render-parity/figure.json +5 -0
- data/scripts/test/render-parity/load-runtime.mjs +337 -0
- data/scripts/test/render-parity/ongoing-product.json +5 -0
- data/scripts/test/render-parity/plotly.json +5 -0
- data/scripts/test/render-parity/poem.json +5 -0
- data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
- data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
- data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
- data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
- data/scripts/test/render-parity/run-fixture.mjs +159 -0
- data/scripts/test/render-parity/selectors.mjs +43 -0
- data/scripts/test/render-parity/video.json +5 -0
- data/scripts/test/render-parity/youtube-short.json +10 -0
- data/scripts/test/render-parity/youtube.json +8 -0
- data/scripts/test/test-absent-field-absent-element.rb +43 -0
- data/scripts/test/test-cdn-pins.rb +44 -0
- data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
- data/scripts/test/test-configuration-doc.rb +36 -0
- data/scripts/test/test-content-detail-urls.rb +346 -0
- data/scripts/test/test-content-keys.rb +110 -0
- data/scripts/test/test-content-license.rb +45 -0
- data/scripts/test/test-content-render-parity.rb +38 -0
- data/scripts/test/test-content-validators.rb +12 -0
- data/scripts/test/test-css-structure.rb +72 -0
- data/scripts/test/test-cv-export-contract.rb +158 -0
- data/scripts/test/test-detail-render-parity.rb +40 -0
- data/scripts/test/test-dev-reexport.rb +122 -0
- data/scripts/test/test-doctor.rb +39 -0
- data/scripts/test/test-dual-template-instances.rb +87 -0
- data/scripts/test/test-dual-template-runtime.rb +167 -0
- data/scripts/test/test-examples-build.rb +60 -0
- data/scripts/test/test-export-content-json-incremental.rb +138 -0
- data/scripts/test/test-export.rb +53 -0
- data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
- data/scripts/test/test-form-control-theming.rb +143 -0
- data/scripts/test/test-git-chronology.rb +414 -0
- data/scripts/test/test-hero-portraits.mjs +97 -0
- data/scripts/test/test-hero-portraits.rb +162 -0
- data/scripts/test/test-home-band-art.rb +135 -0
- data/scripts/test/test-install-functions.rb +39 -0
- data/scripts/test/test-language-export.rb +79 -0
- data/scripts/test/test-listing-filter-contract.rb +119 -0
- data/scripts/test/test-math-row-breaks.mjs +79 -0
- data/scripts/test/test-math-row-breaks.rb +18 -0
- data/scripts/test/test-new.rb +113 -0
- data/scripts/test/test-no-personal-data.rb +85 -0
- data/scripts/test/test-public-cv-export.rb +164 -0
- data/scripts/test/test-r2-config-consistency.rb +57 -0
- data/scripts/test/test-r2-json-sync.rb +193 -0
- data/scripts/test/test-registry-home.rb +34 -0
- data/scripts/test/test-runtime-js-syntax.rb +64 -0
- data/scripts/test/test-slim-writing-index.rb +126 -0
- data/scripts/test/test-studio-api-contract.rb +99 -0
- data/scripts/test/test-studio-content-files.rb +79 -0
- data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
- data/scripts/test/test-studio-rate-limit.mjs +70 -0
- data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
- data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
- data/scripts/test/test-studio-schema-composition.rb +126 -0
- data/scripts/test/test-studio-slug-config.rb +167 -0
- data/scripts/test/test-template-packages.rb +51 -0
- data/scripts/test/test-text-excerpt.rb +51 -0
- data/scripts/test/test-unwrap-media-src.mjs +39 -0
- data/scripts/test/test-unwrap-media-src.rb +19 -0
- data/scripts/test/test-withdrawn-content-export.rb +69 -0
- data/scripts/test/test-writing-index-export-slugs.rb +84 -0
- data/scripts/test/test-writing-slug-limits.rb +61 -0
- data/scripts/test/test-xcite-chronology.rb +106 -0
- data/scripts/validate.sh +74 -0
- data/scripts/visual/build-cv-pages-json.js +33 -0
- data/scripts/visual/build-site-pages-json.js +94 -0
- data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
- data/scripts/visual/take-screenshots.js +88 -0
- data/scripts/visual/visual-inspection-pages.json +113 -0
- data/studio/assets/index-B_j0pMD5.css +1 -0
- data/studio/assets/index-Ch4YEv9g.js +285 -0
- data/studio/index.html +27 -0
- data/studio/schema.yml +869 -0
- data/studio-app/README.md +27 -0
- data/studio-app/index.html +26 -0
- data/studio-app/package-lock.json +2499 -0
- data/studio-app/package.json +26 -0
- data/studio-app/skills-lock.json +47 -0
- data/studio-app/src/api/client.js +71 -0
- data/studio-app/src/editor/body-editor.js +70 -0
- data/studio-app/src/editor/liquid-core.js +382 -0
- data/studio-app/src/editor/liquid.js +77 -0
- data/studio-app/src/lib/document.js +207 -0
- data/studio-app/src/lib/list-controls.js +317 -0
- data/studio-app/src/lib/schema.js +66 -0
- data/studio-app/src/lib/yaml-doc.js +100 -0
- data/studio-app/src/main.js +3693 -0
- data/studio-app/src/media/browser.js +578 -0
- data/studio-app/src/preview/render.js +430 -0
- data/studio-app/src/styles/studio.css +2826 -0
- data/studio-app/vite.config.js +25 -0
- data/templates/articles/README.md +40 -0
- data/templates/articles/fixtures/article.md +9 -0
- data/templates/articles/screenshots/README.md +6 -0
- data/templates/articles/studio.yml +73 -0
- data/templates/articles/template.yml +21 -0
- data/templates/bibliography/README.md +41 -0
- data/templates/bibliography/fixtures/references.json +9 -0
- data/templates/bibliography/screenshots/README.md +6 -0
- data/templates/bibliography/studio.yml +66 -0
- data/templates/bibliography/template.yml +18 -0
- data/templates/blog/README.md +38 -0
- data/templates/blog/fixtures/post.md +9 -0
- data/templates/blog/screenshots/README.md +6 -0
- data/templates/blog/studio.yml +69 -0
- data/templates/blog/template.yml +20 -0
- data/templates/cv/README.md +34 -0
- data/templates/cv/fixtures/job.md +12 -0
- data/templates/cv/screenshots/README.md +6 -0
- data/templates/cv/studio.yml +200 -0
- data/templates/cv/template.yml +25 -0
- data/templates/media/README.md +38 -0
- data/templates/media/fixtures/resource.md +7 -0
- data/templates/media/screenshots/README.md +6 -0
- data/templates/media/studio.yml +58 -0
- data/templates/media/template.yml +17 -0
- data/templates/network/README.md +34 -0
- data/templates/network/fixtures/sources.json +3 -0
- data/templates/network/screenshots/README.md +6 -0
- data/templates/network/studio.yml +28 -0
- data/templates/network/template.yml +10 -0
- data/templates/portfolio/README.md +39 -0
- data/templates/portfolio/fixtures/project.md +10 -0
- data/templates/portfolio/screenshots/README.md +6 -0
- data/templates/portfolio/studio.yml +89 -0
- data/templates/portfolio/template.yml +18 -0
- metadata +505 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Front matter + body serialization helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
|
|
7
|
+
export function splitDocument(raw) {
|
|
8
|
+
const text = String(raw || "");
|
|
9
|
+
if (!text.startsWith("---")) {
|
|
10
|
+
return { fmLines: [], fm: {}, body: text, hasFm: false };
|
|
11
|
+
}
|
|
12
|
+
const end = text.indexOf("\n---", 3);
|
|
13
|
+
if (end < 0) return { fmLines: [], fm: {}, body: text, hasFm: false };
|
|
14
|
+
const rawFm = text.slice(4, end).replace(/^\r?\n/, "");
|
|
15
|
+
const afterClose = text.slice(end + 4);
|
|
16
|
+
const blankAfterFm = /^\r?\n\r?\n/.test(afterClose);
|
|
17
|
+
const body = afterClose.replace(/^(?:\r?\n)+/, "");
|
|
18
|
+
const fmLines = rawFm.split(/\r?\n/);
|
|
19
|
+
const fm = parseFrontMatter(rawFm);
|
|
20
|
+
return { fmLines, fm, body, hasFm: true, rawFm, blankAfterFm };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseFrontMatter(rawFm) {
|
|
24
|
+
try {
|
|
25
|
+
const doc = yaml.load(rawFm, { schema: yaml.JSON_SCHEMA });
|
|
26
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc)) return doc;
|
|
27
|
+
} catch {
|
|
28
|
+
/* fall through to the line parser */
|
|
29
|
+
}
|
|
30
|
+
const fm = {};
|
|
31
|
+
for (const line of String(rawFm || "").split(/\r?\n/)) {
|
|
32
|
+
const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
33
|
+
if (!m) continue;
|
|
34
|
+
let v = m[2];
|
|
35
|
+
if (v === "true") v = true;
|
|
36
|
+
else if (v === "false") v = false;
|
|
37
|
+
else if (
|
|
38
|
+
(v.startsWith('"') && v.endsWith('"')) ||
|
|
39
|
+
(v.startsWith("'") && v.endsWith("'"))
|
|
40
|
+
) {
|
|
41
|
+
v = v.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
fm[m[1]] = v;
|
|
44
|
+
}
|
|
45
|
+
return fm;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function yamlScalar(value) {
|
|
49
|
+
if (value === true || value === false) return String(value);
|
|
50
|
+
if (typeof value === "number") return String(value);
|
|
51
|
+
if (value == null) return '""';
|
|
52
|
+
const s = String(value);
|
|
53
|
+
if (s === "") return '""';
|
|
54
|
+
if (/^(true|false|null|yes|no|~)$/i.test(s)) return JSON.stringify(s);
|
|
55
|
+
if (/[:#&*!|>%@`]/.test(s) || /^[\s\-?]|:\s|\s$/.test(s) || /[\n"']/.test(s)) {
|
|
56
|
+
return JSON.stringify(s);
|
|
57
|
+
}
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isEmptyFm(value) {
|
|
62
|
+
if (value === "" || value == null) return true;
|
|
63
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sameFmValue(a, b) {
|
|
68
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function topLevelSpans(rawFm) {
|
|
72
|
+
const lines = String(rawFm || "").replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n");
|
|
73
|
+
const spans = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < lines.length) {
|
|
76
|
+
if (lines[i].trim() === "") {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const m = lines[i].match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
81
|
+
if (!m) {
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const start = i;
|
|
86
|
+
i++;
|
|
87
|
+
while (i < lines.length) {
|
|
88
|
+
const nxt = lines[i];
|
|
89
|
+
if (nxt === "") {
|
|
90
|
+
const after = lines[i + 1];
|
|
91
|
+
if (after == null || /^[A-Za-z0-9_]+:\s*/.test(after)) break;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (/^\s/.test(nxt)) {
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
spans.push({ key: m[1], lines: lines.slice(start, i) });
|
|
102
|
+
}
|
|
103
|
+
return spans;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatKeyLines(key, value) {
|
|
107
|
+
if (Array.isArray(value) && value.every((v) => v == null || typeof v !== "object")) {
|
|
108
|
+
return [`${key}:`, ...value.map((v) => ` - ${yamlScalar(v)}`)];
|
|
109
|
+
}
|
|
110
|
+
if (value && typeof value === "object") {
|
|
111
|
+
const dumped = yaml.dump(
|
|
112
|
+
{ [key]: value },
|
|
113
|
+
{ schema: yaml.JSON_SCHEMA, lineWidth: -1, noRefs: true }
|
|
114
|
+
);
|
|
115
|
+
return dumped.replace(/\n$/, "").split("\n");
|
|
116
|
+
}
|
|
117
|
+
return [`${key}: ${yamlScalar(value)}`];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function frontMatterChanged(original, fields) {
|
|
121
|
+
const next = { ...original };
|
|
122
|
+
for (const [key, val] of Object.entries(fields || {})) {
|
|
123
|
+
if (isEmptyFm(val)) {
|
|
124
|
+
if (!isEmptyFm(original[key])) delete next[key];
|
|
125
|
+
} else {
|
|
126
|
+
next[key] = val;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const keys = new Set([...Object.keys(original), ...Object.keys(next)]);
|
|
130
|
+
for (const key of keys) {
|
|
131
|
+
if (!sameFmValue(original[key], next[key])) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rebuildFrontMatter(rawFm, fields) {
|
|
137
|
+
const original = parseFrontMatter(rawFm);
|
|
138
|
+
const next = { ...original };
|
|
139
|
+
for (const [key, val] of Object.entries(fields || {})) {
|
|
140
|
+
if (isEmptyFm(val)) {
|
|
141
|
+
if (!isEmptyFm(original[key])) delete next[key];
|
|
142
|
+
} else {
|
|
143
|
+
next[key] = val;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const originalKeys = Object.keys(original);
|
|
147
|
+
const nextKeys = Object.keys(next);
|
|
148
|
+
const unchanged =
|
|
149
|
+
originalKeys.length === nextKeys.length &&
|
|
150
|
+
nextKeys.every((key) => sameFmValue(original[key], next[key]));
|
|
151
|
+
if (unchanged) return String(rawFm || "").replace(/\n$/, "");
|
|
152
|
+
|
|
153
|
+
const spans = topLevelSpans(rawFm);
|
|
154
|
+
const used = new Set();
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const span of spans) {
|
|
157
|
+
if (!Object.prototype.hasOwnProperty.call(next, span.key)) continue;
|
|
158
|
+
used.add(span.key);
|
|
159
|
+
if (sameFmValue(original[span.key], next[span.key])) out.push(...span.lines);
|
|
160
|
+
else out.push(...formatKeyLines(span.key, next[span.key]));
|
|
161
|
+
}
|
|
162
|
+
for (const [key, val] of Object.entries(next)) {
|
|
163
|
+
if (used.has(key)) continue;
|
|
164
|
+
out.push(...formatKeyLines(key, val));
|
|
165
|
+
}
|
|
166
|
+
return out.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Rebuild file content from original FM lines, overlaying edited scalar fields
|
|
171
|
+
* and replacing the body. Preserves unknown YAML structure where possible.
|
|
172
|
+
*/
|
|
173
|
+
export function rebuildDocument(originalRaw, { fields, body, format }) {
|
|
174
|
+
if (format === "raw") {
|
|
175
|
+
return body.endsWith("\n") ? body : body + "\n";
|
|
176
|
+
}
|
|
177
|
+
if (format === "yaml") {
|
|
178
|
+
// YAML collections use dumpYaml / structured editors in main.js — not this path.
|
|
179
|
+
return body.endsWith("\n") ? body : body + "\n";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const split = splitDocument(originalRaw);
|
|
183
|
+
const { rawFm, hasFm, blankAfterFm } = split;
|
|
184
|
+
const bodyNorm = String(body || "").replace(/^(?:\r?\n)+/, "");
|
|
185
|
+
if (
|
|
186
|
+
hasFm &&
|
|
187
|
+
!frontMatterChanged(split.fm, fields) &&
|
|
188
|
+
bodyNorm === String(split.body || "").replace(/^(?:\r?\n)+/, "")
|
|
189
|
+
) {
|
|
190
|
+
return originalRaw;
|
|
191
|
+
}
|
|
192
|
+
const out = ["---"];
|
|
193
|
+
if (hasFm) {
|
|
194
|
+
const fmText = rebuildFrontMatter(rawFm, fields);
|
|
195
|
+
if (fmText) out.push(fmText);
|
|
196
|
+
} else {
|
|
197
|
+
for (const [key, val] of Object.entries(fields || {})) {
|
|
198
|
+
if (isEmptyFm(val)) continue;
|
|
199
|
+
out.push(...formatKeyLines(key, val));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
out.push("---");
|
|
203
|
+
const bodyText = String(body || "").replace(/^(?:\r?\n)+/, "");
|
|
204
|
+
const withNl = bodyText.endsWith("\n") ? bodyText : bodyText + "\n";
|
|
205
|
+
const gap = blankAfterFm ? "\n\n" : "\n";
|
|
206
|
+
return out.join("\n") + gap + withNl;
|
|
207
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modular search + sort + pagination for Studio collection entry lists.
|
|
3
|
+
* Sort / page-size options are config-driven; render code only looks up by id.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function cmpStr(a, b, dir = 1) {
|
|
7
|
+
return (
|
|
8
|
+
String(a || "").localeCompare(String(b || ""), undefined, {
|
|
9
|
+
sensitivity: "base",
|
|
10
|
+
numeric: true,
|
|
11
|
+
}) * dir
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Empty dates sink to the end for both newest-first and oldest-first. */
|
|
16
|
+
function cmpDate(a, b, dir = 1) {
|
|
17
|
+
const aa = String(a || "");
|
|
18
|
+
const bb = String(b || "");
|
|
19
|
+
if (!aa && !bb) return 0;
|
|
20
|
+
if (!aa) return 1;
|
|
21
|
+
if (!bb) return -1;
|
|
22
|
+
return aa.localeCompare(bb) * dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @typedef {{ id: string, label: string, compare: (a: object, b: object) => number }} ListSortOption */
|
|
26
|
+
|
|
27
|
+
/** @type {ListSortOption[]} */
|
|
28
|
+
export const LIST_SORT_OPTIONS = [
|
|
29
|
+
{
|
|
30
|
+
id: "date-desc",
|
|
31
|
+
label: "Date | newest first",
|
|
32
|
+
compare: (a, b) =>
|
|
33
|
+
cmpDate(a.date, b.date, -1) || cmpStr(a.title || a.name, b.title || b.name),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "date-asc",
|
|
37
|
+
label: "Date | oldest first",
|
|
38
|
+
compare: (a, b) =>
|
|
39
|
+
cmpDate(a.date, b.date, 1) || cmpStr(a.title || a.name, b.title || b.name),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "title-asc",
|
|
43
|
+
label: "Title | A–Z",
|
|
44
|
+
compare: (a, b) => cmpStr(a.title || a.name, b.title || b.name, 1),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "title-desc",
|
|
48
|
+
label: "Title | Z–A",
|
|
49
|
+
compare: (a, b) => cmpStr(a.title || a.name, b.title || b.name, -1),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "path-asc",
|
|
53
|
+
label: "Path | A–Z",
|
|
54
|
+
compare: (a, b) => cmpStr(a.path || a.name, b.path || b.name, 1),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "path-desc",
|
|
58
|
+
label: "Path | Z–A",
|
|
59
|
+
compare: (a, b) => cmpStr(a.path || a.name, b.path || b.name, -1),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "key-asc",
|
|
63
|
+
label: "Key | A–Z",
|
|
64
|
+
compare: (a, b) => cmpStr(a.key || a.name, b.key || b.name, 1),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export const DEFAULT_SORT_ID = "date-desc";
|
|
69
|
+
|
|
70
|
+
/** @typedef {{ id: string, label: string, size: number | null }} ListPageSizeOption */
|
|
71
|
+
|
|
72
|
+
/** `size: null` means ALL (no slice). */
|
|
73
|
+
export const LIST_PAGE_SIZE_OPTIONS = [
|
|
74
|
+
{ id: "10", label: "10", size: 10 },
|
|
75
|
+
{ id: "20", label: "20", size: 20 },
|
|
76
|
+
{ id: "30", label: "30", size: 30 },
|
|
77
|
+
{ id: "all", label: "ALL", size: null },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export const DEFAULT_PAGE_SIZE_ID = "10";
|
|
81
|
+
|
|
82
|
+
const LS_SORT_PREFIX = "studio-list-sort:";
|
|
83
|
+
const LS_PAGE_SIZE_PREFIX = "studio-list-page-size:";
|
|
84
|
+
|
|
85
|
+
export function sortOptionById(id) {
|
|
86
|
+
return LIST_SORT_OPTIONS.find((o) => o.id === id) || LIST_SORT_OPTIONS[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function pageSizeOptionById(id) {
|
|
90
|
+
return (
|
|
91
|
+
LIST_PAGE_SIZE_OPTIONS.find((o) => o.id === id) || LIST_PAGE_SIZE_OPTIONS[0]
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Normalize peeked tags (array, legacy string, or absent) to a string list. */
|
|
96
|
+
export function itemTags(item) {
|
|
97
|
+
const raw = item?.tags;
|
|
98
|
+
if (Array.isArray(raw)) {
|
|
99
|
+
return raw.map((t) => String(t == null ? "" : t).trim()).filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
if (typeof raw === "string" && raw.trim()) return [raw.trim()];
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function filterItems(items, query) {
|
|
106
|
+
const q = String(query || "").trim().toLowerCase();
|
|
107
|
+
if (!q) return items.slice();
|
|
108
|
+
return items.filter((item) => {
|
|
109
|
+
const hay = [
|
|
110
|
+
item.title,
|
|
111
|
+
item.name,
|
|
112
|
+
item.key,
|
|
113
|
+
item.path,
|
|
114
|
+
item.date,
|
|
115
|
+
itemTags(item).join(" "),
|
|
116
|
+
]
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join("\n")
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
return hay.includes(q);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function filterItemsByTag(items, tag) {
|
|
125
|
+
const tagQ = String(tag || "").trim().toLowerCase();
|
|
126
|
+
if (!tagQ) return items.slice();
|
|
127
|
+
return items.filter((item) =>
|
|
128
|
+
itemTags(item).some((t) => t.toLowerCase() === tagQ)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Distinct tags across items, sorted case-insensitively. */
|
|
133
|
+
export function collectDistinctTags(items) {
|
|
134
|
+
const seen = new Map();
|
|
135
|
+
for (const item of items || []) {
|
|
136
|
+
for (const tag of itemTags(item)) {
|
|
137
|
+
const key = tag.toLowerCase();
|
|
138
|
+
if (!seen.has(key)) seen.set(key, tag);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...seen.values()].sort((a, b) =>
|
|
142
|
+
a.localeCompare(b, undefined, { sensitivity: "base" })
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function applyListControls(
|
|
147
|
+
items,
|
|
148
|
+
{ query = "", sortId = DEFAULT_SORT_ID, tag = "" } = {}
|
|
149
|
+
) {
|
|
150
|
+
const filtered = filterItemsByTag(filterItems(items, query), tag);
|
|
151
|
+
const opt = sortOptionById(sortId);
|
|
152
|
+
return filtered.sort((a, b) => opt.compare(a, b));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Slice a filtered+sorted list into a page.
|
|
157
|
+
* @returns {{ items: object[], page: number, pageCount: number, pageSize: number | null, total: number, from: number, to: number }}
|
|
158
|
+
*/
|
|
159
|
+
export function paginateItems(
|
|
160
|
+
items,
|
|
161
|
+
{ pageSizeId = DEFAULT_PAGE_SIZE_ID, page = 1 } = {}
|
|
162
|
+
) {
|
|
163
|
+
const opt = pageSizeOptionById(pageSizeId);
|
|
164
|
+
const total = items.length;
|
|
165
|
+
const size = opt.size;
|
|
166
|
+
|
|
167
|
+
if (size == null || size <= 0) {
|
|
168
|
+
return {
|
|
169
|
+
items: items.slice(),
|
|
170
|
+
page: 1,
|
|
171
|
+
pageCount: 1,
|
|
172
|
+
pageSize: null,
|
|
173
|
+
total,
|
|
174
|
+
from: total ? 1 : 0,
|
|
175
|
+
to: total,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const pageCount = Math.max(1, Math.ceil(total / size));
|
|
180
|
+
const safePage = Math.min(Math.max(1, Number(page) || 1), pageCount);
|
|
181
|
+
const start = (safePage - 1) * size;
|
|
182
|
+
const slice = items.slice(start, start + size);
|
|
183
|
+
return {
|
|
184
|
+
items: slice,
|
|
185
|
+
page: safePage,
|
|
186
|
+
pageCount,
|
|
187
|
+
pageSize: size,
|
|
188
|
+
total,
|
|
189
|
+
from: total ? start + 1 : 0,
|
|
190
|
+
to: start + slice.length,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function loadSortPreference(collectionName, fallback = DEFAULT_SORT_ID) {
|
|
195
|
+
try {
|
|
196
|
+
const v = localStorage.getItem(LS_SORT_PREFIX + collectionName);
|
|
197
|
+
if (v && LIST_SORT_OPTIONS.some((o) => o.id === v)) return v;
|
|
198
|
+
} catch {
|
|
199
|
+
/* private mode / blocked storage */
|
|
200
|
+
}
|
|
201
|
+
return fallback;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function saveSortPreference(collectionName, sortId) {
|
|
205
|
+
try {
|
|
206
|
+
localStorage.setItem(LS_SORT_PREFIX + collectionName, sortId);
|
|
207
|
+
} catch {
|
|
208
|
+
/* ignore */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function loadPageSizePreference(
|
|
213
|
+
collectionName,
|
|
214
|
+
fallback = DEFAULT_PAGE_SIZE_ID
|
|
215
|
+
) {
|
|
216
|
+
try {
|
|
217
|
+
const v = localStorage.getItem(LS_PAGE_SIZE_PREFIX + collectionName);
|
|
218
|
+
if (v && LIST_PAGE_SIZE_OPTIONS.some((o) => o.id === v)) return v;
|
|
219
|
+
} catch {
|
|
220
|
+
/* private mode / blocked storage */
|
|
221
|
+
}
|
|
222
|
+
return fallback;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function savePageSizePreference(collectionName, pageSizeId) {
|
|
226
|
+
try {
|
|
227
|
+
localStorage.setItem(LS_PAGE_SIZE_PREFIX + collectionName, pageSizeId);
|
|
228
|
+
} catch {
|
|
229
|
+
/* ignore */
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Default ledger-meta field lists when schema view.list_meta_fields is absent.
|
|
235
|
+
* Values are tree-item keys (peekMeta / yaml-only listing). Missing values skip.
|
|
236
|
+
* Filename (basename + ext) is always appended by formatEntryListMeta — do not list it here.
|
|
237
|
+
*/
|
|
238
|
+
export const DEFAULT_LIST_META_FIELDS = {
|
|
239
|
+
posts: ["date"],
|
|
240
|
+
articles: ["date"],
|
|
241
|
+
resources: ["category", "type", "language"],
|
|
242
|
+
bibliography: ["key"],
|
|
243
|
+
products: ["company", "start_date", "end_date"],
|
|
244
|
+
projects: ["status", "start_date", "end_date"],
|
|
245
|
+
jobs: ["company", "engagement", "start", "end"],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** Fields that mean "filename" — reserved; always forced as the last segment. */
|
|
249
|
+
const FILENAME_FIELD_ALIASES = new Set(["name", "filename"]);
|
|
250
|
+
|
|
251
|
+
function basenameFromPath(path) {
|
|
252
|
+
const parts = String(path || "")
|
|
253
|
+
.split("/")
|
|
254
|
+
.filter(Boolean);
|
|
255
|
+
return parts.length ? parts[parts.length - 1] : "";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatMetaFieldValue(field, item) {
|
|
259
|
+
if (field === "path") {
|
|
260
|
+
const p = String(item.path || "")
|
|
261
|
+
.split("/")
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join(" | ");
|
|
264
|
+
return p || "";
|
|
265
|
+
}
|
|
266
|
+
const raw = item?.[field];
|
|
267
|
+
if (raw == null) return "";
|
|
268
|
+
if (Array.isArray(raw)) {
|
|
269
|
+
return raw
|
|
270
|
+
.map((v) => String(v == null ? "" : v).trim())
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.join(", ");
|
|
273
|
+
}
|
|
274
|
+
return String(raw).trim();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Resolve configured list-meta fields for a collection.
|
|
279
|
+
* @param {{ name?: string, listMetaFields?: string[] | null } | null | undefined} collection
|
|
280
|
+
* @returns {string[]}
|
|
281
|
+
*/
|
|
282
|
+
export function listMetaFieldsFor(collection) {
|
|
283
|
+
if (Array.isArray(collection?.listMetaFields) && collection.listMetaFields.length) {
|
|
284
|
+
return collection.listMetaFields.map(String);
|
|
285
|
+
}
|
|
286
|
+
const name = collection?.name;
|
|
287
|
+
if (name && DEFAULT_LIST_META_FIELDS[name]) {
|
|
288
|
+
return DEFAULT_LIST_META_FIELDS[name].slice();
|
|
289
|
+
}
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Build the ledger-meta line: configured fields joined by ` | `, filename always last.
|
|
295
|
+
* Skips empty/missing fields. Does not invent data.
|
|
296
|
+
* @param {object} item tree list item
|
|
297
|
+
* @param {{ name?: string, listMetaFields?: string[] | null } | string[] | null | undefined} collectionOrFields
|
|
298
|
+
*/
|
|
299
|
+
export function formatEntryListMeta(item, collectionOrFields) {
|
|
300
|
+
const fields = Array.isArray(collectionOrFields)
|
|
301
|
+
? collectionOrFields.map(String)
|
|
302
|
+
: listMetaFieldsFor(collectionOrFields);
|
|
303
|
+
|
|
304
|
+
const parts = [];
|
|
305
|
+
for (const field of fields) {
|
|
306
|
+
if (FILENAME_FIELD_ALIASES.has(field)) continue;
|
|
307
|
+
const value = formatMetaFieldValue(field, item);
|
|
308
|
+
if (value) parts.push(value);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const filename =
|
|
312
|
+
String(item?.name || "").trim() || basenameFromPath(item?.path);
|
|
313
|
+
if (filename && parts[parts.length - 1] !== filename) {
|
|
314
|
+
parts.push(filename);
|
|
315
|
+
}
|
|
316
|
+
return parts.join(" | ");
|
|
317
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
|
|
3
|
+
/** Load schema from the committed studio/schema.yml (same-origin in prod). */
|
|
4
|
+
export async function loadSchema() {
|
|
5
|
+
for (const url of ["/studio/schema.yml", "./schema.yml"]) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(url, { cache: "no-store" });
|
|
8
|
+
if (res.ok) {
|
|
9
|
+
const text = await res.text();
|
|
10
|
+
return normalizeSchema(yaml.load(text));
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
/* try next */
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
throw new Error("Could not load studio/schema.yml");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeSchema(raw) {
|
|
20
|
+
const groups = [];
|
|
21
|
+
const collections = [];
|
|
22
|
+
|
|
23
|
+
function walk(nodes, groupLabel) {
|
|
24
|
+
for (const entry of nodes || []) {
|
|
25
|
+
if (!entry || typeof entry !== "object") continue;
|
|
26
|
+
if (entry.type === "group") {
|
|
27
|
+
groups.push({ name: entry.name, label: entry.label || entry.name });
|
|
28
|
+
walk(entry.items, entry.label || entry.name);
|
|
29
|
+
} else if (entry.type === "collection" || entry.type === "file") {
|
|
30
|
+
const view = entry.view || {};
|
|
31
|
+
collections.push({
|
|
32
|
+
name: entry.name,
|
|
33
|
+
label: entry.label || entry.name,
|
|
34
|
+
group: groupLabel || "Content",
|
|
35
|
+
type: entry.type,
|
|
36
|
+
path: entry.path,
|
|
37
|
+
format: entry.format || "yaml-frontmatter",
|
|
38
|
+
filename: entry.filename,
|
|
39
|
+
root_key: entry.root_key || null,
|
|
40
|
+
description: entry.description || "",
|
|
41
|
+
list: !!entry.list,
|
|
42
|
+
fields: entry.fields || [],
|
|
43
|
+
view,
|
|
44
|
+
/** @type {string[] | null} */
|
|
45
|
+
listMetaFields: Array.isArray(view.list_meta_fields)
|
|
46
|
+
? view.list_meta_fields.map(String)
|
|
47
|
+
: null,
|
|
48
|
+
operations: entry.operations || { delete: false },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
walk(raw?.content, null);
|
|
55
|
+
return { media: raw?.media || {}, groups, collections, raw };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function fieldIsBody(field) {
|
|
59
|
+
const name = String(field?.name || "");
|
|
60
|
+
return (
|
|
61
|
+
field?.component === "markdown_body" ||
|
|
62
|
+
name === "body" ||
|
|
63
|
+
name.startsWith("body_") ||
|
|
64
|
+
(name === "description" && field?.component === "markdown_body")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
|
|
3
|
+
/** js-yaml dump options shared by Studio YAML saves. */
|
|
4
|
+
export const YAML_DUMP_OPTS = {
|
|
5
|
+
lineWidth: 100,
|
|
6
|
+
noRefs: true,
|
|
7
|
+
sortKeys: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How a schema collection should be edited in Studio.
|
|
12
|
+
* @returns {"list"|"map"|"source"|null}
|
|
13
|
+
* - list: root is a YAML sequence of objects (Profile contacts/education/…)
|
|
14
|
+
* - map: root (or root_key subtree) is an object with schema fields
|
|
15
|
+
* - source: free-form YAML — monospace editor only
|
|
16
|
+
* - null: not a YAML collection
|
|
17
|
+
*/
|
|
18
|
+
export function yamlEditKind(collection) {
|
|
19
|
+
if (!collection || collection.format !== "yaml") return null;
|
|
20
|
+
if (collection.list) return "list";
|
|
21
|
+
if (collection.root_key) return "map";
|
|
22
|
+
if ((collection.fields || []).length) return "map";
|
|
23
|
+
return "source";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function dumpYaml(value) {
|
|
27
|
+
const text = yaml.dump(value ?? null, YAML_DUMP_OPTS);
|
|
28
|
+
return text.endsWith("\n") ? text : text + "\n";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadYaml(content) {
|
|
32
|
+
try {
|
|
33
|
+
return yaml.load(content);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const error = new Error(err?.message || "Invalid YAML");
|
|
36
|
+
error.cause = err;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build an empty list item from schema field defs (defaults when set). */
|
|
42
|
+
export function emptyItemFromFields(fields) {
|
|
43
|
+
const item = {};
|
|
44
|
+
for (const field of fields || []) {
|
|
45
|
+
if (!field?.name || field.hidden) continue;
|
|
46
|
+
if (Object.prototype.hasOwnProperty.call(field, "default")) {
|
|
47
|
+
item[field.name] = structuredCloneSafe(field.default);
|
|
48
|
+
} else if (field.list) {
|
|
49
|
+
item[field.name] = [];
|
|
50
|
+
} else if (field.type === "boolean") {
|
|
51
|
+
item[field.name] = false;
|
|
52
|
+
} else {
|
|
53
|
+
item[field.name] = "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return item;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function structuredCloneSafe(value) {
|
|
60
|
+
if (value == null || typeof value !== "object") return value;
|
|
61
|
+
try {
|
|
62
|
+
return structuredClone(value);
|
|
63
|
+
} catch {
|
|
64
|
+
return JSON.parse(JSON.stringify(value));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Drop empty optional scalars/arrays from a map before dump, keeping unknown keys.
|
|
70
|
+
* Empty string / null / undefined / [] are omitted for known schema fields.
|
|
71
|
+
*/
|
|
72
|
+
export function pruneEmptyFields(doc, fields) {
|
|
73
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc;
|
|
74
|
+
const known = new Set((fields || []).map((f) => f.name).filter(Boolean));
|
|
75
|
+
const out = { ...doc };
|
|
76
|
+
for (const key of Object.keys(out)) {
|
|
77
|
+
if (!known.has(key)) continue;
|
|
78
|
+
const val = out[key];
|
|
79
|
+
if (val === "" || val == null) {
|
|
80
|
+
delete out[key];
|
|
81
|
+
} else if (Array.isArray(val) && val.length === 0) {
|
|
82
|
+
delete out[key];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Normalize a loaded YAML root into a list of plain objects. */
|
|
89
|
+
export function asObjectList(doc) {
|
|
90
|
+
if (!Array.isArray(doc)) return [];
|
|
91
|
+
return doc.map((item) =>
|
|
92
|
+
item && typeof item === "object" && !Array.isArray(item) ? { ...item } : {}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Normalize a loaded YAML root into a plain object map. */
|
|
97
|
+
export function asObjectMap(doc) {
|
|
98
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc)) return { ...doc };
|
|
99
|
+
return {};
|
|
100
|
+
}
|