@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211
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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -21
- package/README.md +51 -51
- package/astro.mjs +162 -162
- package/bin/morpheme.mjs +38 -38
- package/client.d.ts +10 -10
- package/defaults/blockTexts.ts +33 -33
- package/defaults/collections.ts +774 -774
- package/defaults/colors.ts +125 -125
- package/defaults/company.ts +20 -20
- package/defaults/editor.ts +178 -178
- package/defaults/internalLinks.ts +30 -30
- package/defaults/opens.ts +18 -18
- package/defaults/popup.tsx +93 -93
- package/defaults/privacy.ts +79 -79
- package/defaults/rules.ts +450 -450
- package/defaults/schema.ts +16 -16
- package/defaults/sitedata.ts +129 -129
- package/defaults/standard.ts +106 -106
- package/defaults/standardPages.ts +87 -87
- package/defaults/texts.ts +481 -481
- package/demo/README.md +26 -26
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
- package/demo/src/content/blog/every-save-is-a-version.md +21 -21
- package/demo/src/content/blog/schedule-a-post.md +17 -17
- package/demo/src/content/blog/secure-from-day-one.md +17 -17
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
- package/demo/src/content/blog/the-app-store.md +21 -21
- package/demo/src/content/pages/about.md +22 -22
- package/demo/src/content/pages/app-store.md +30 -30
- package/demo/src/content/pages/blocks.md +98 -98
- package/demo/src/content/pages/blog-page.md +11 -11
- package/demo/src/content/pages/contact.md +16 -16
- package/demo/src/content/pages/get-started.md +20 -20
- package/demo/src/content/pages/home.md +26 -26
- package/demo/src/content/pages/page-not-found.md +11 -11
- package/demo/src/content/pages/privacy-policy.md +24 -24
- package/demo/src/data/blocks.json +25 -25
- package/demo/src/data/booking.json +6 -6
- package/demo/src/data/categories/apps.json +6 -6
- package/demo/src/data/categories/design.json +6 -6
- package/demo/src/data/categories/editing.json +6 -6
- package/demo/src/data/categories/security.json +6 -6
- package/demo/src/data/categories/seo.json +6 -6
- package/demo/src/data/colors.json +75 -75
- package/demo/src/data/contact.json +8 -8
- package/demo/src/data/cookies.json +21 -21
- package/demo/src/data/footer.json +27 -27
- package/demo/src/data/menu.json +35 -35
- package/demo/src/data/mobile-menu.json +25 -25
- package/demo/src/data/popups/booking-bar.json +13 -13
- package/demo/src/data/popups/try-the-editor.json +16 -16
- package/demo/src/data/standard-pages.json +8 -8
- package/demo/src/data/testimonials/sample-bakery.json +10 -10
- package/demo/src/data/testimonials/sample-clinic.json +10 -10
- package/demo/src/data/testimonials/sample-studio.json +10 -10
- package/demo/src/site/colors.ts +11 -11
- package/demo/src/site/config.ts +19 -19
- package/icons/lucide/LICENSE +43 -43
- package/icons/lucide/address-book.svg +1 -1
- package/icons/lucide/app-window.svg +1 -1
- package/icons/lucide/arrow-counter-clockwise.svg +1 -1
- package/icons/lucide/arrow-down.svg +1 -1
- package/icons/lucide/arrow-left.svg +1 -1
- package/icons/lucide/arrow-right.svg +1 -1
- package/icons/lucide/arrow-square-out.svg +1 -1
- package/icons/lucide/arrow-u-up-left.svg +1 -1
- package/icons/lucide/arrow-u-up-right.svg +1 -1
- package/icons/lucide/arrow-up-right.svg +1 -1
- package/icons/lucide/arrow-up.svg +1 -1
- package/icons/lucide/arrows-down-up.svg +1 -1
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
- package/icons/lucide/article.svg +1 -1
- package/icons/lucide/bank.svg +1 -1
- package/icons/lucide/bell.svg +1 -1
- package/icons/lucide/bicycle.svg +1 -1
- package/icons/lucide/binoculars.svg +1 -1
- package/icons/lucide/book-open-text.svg +1 -1
- package/icons/lucide/book-open.svg +1 -1
- package/icons/lucide/browser.svg +1 -1
- package/icons/lucide/buildings.svg +1 -1
- package/icons/lucide/bus.svg +1 -1
- package/icons/lucide/calendar-blank.svg +1 -1
- package/icons/lucide/calendar-check.svg +1 -1
- package/icons/lucide/car.svg +1 -1
- package/icons/lucide/cards.svg +1 -1
- package/icons/lucide/caret-down.svg +1 -1
- package/icons/lucide/caret-left.svg +1 -1
- package/icons/lucide/caret-right.svg +1 -1
- package/icons/lucide/caret-up.svg +1 -1
- package/icons/lucide/certificate.svg +1 -1
- package/icons/lucide/chart-line-up.svg +1 -1
- package/icons/lucide/chats.svg +1 -1
- package/icons/lucide/check-circle.svg +1 -1
- package/icons/lucide/check.svg +1 -1
- package/icons/lucide/circle-half.svg +1 -1
- package/icons/lucide/circle.svg +1 -1
- package/icons/lucide/clock-counter-clockwise.svg +1 -1
- package/icons/lucide/clock.svg +1 -1
- package/icons/lucide/code.svg +1 -1
- package/icons/lucide/coffee.svg +1 -1
- package/icons/lucide/columns.svg +1 -1
- package/icons/lucide/cookie.svg +1 -1
- package/icons/lucide/copy.svg +1 -1
- package/icons/lucide/credit-card.svg +1 -1
- package/icons/lucide/currency-circle-dollar.svg +1 -1
- package/icons/lucide/cursor-click.svg +1 -1
- package/icons/lucide/database.svg +1 -1
- package/icons/lucide/desktop.svg +1 -1
- package/icons/lucide/device-mobile.svg +1 -1
- package/icons/lucide/door-open.svg +1 -1
- package/icons/lucide/download-simple.svg +1 -1
- package/icons/lucide/envelope-simple.svg +1 -1
- package/icons/lucide/eye-slash.svg +1 -1
- package/icons/lucide/eye.svg +1 -1
- package/icons/lucide/file-pdf.svg +1 -1
- package/icons/lucide/file-text.svg +1 -1
- package/icons/lucide/file-video.svg +1 -1
- package/icons/lucide/file-zip.svg +1 -1
- package/icons/lucide/flower-lotus.svg +1 -1
- package/icons/lucide/folder-simple.svg +1 -1
- package/icons/lucide/funnel.svg +1 -1
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear.svg +1 -1
- package/icons/lucide/github-logo.svg +1 -1
- package/icons/lucide/globe.svg +1 -1
- package/icons/lucide/google-logo.svg +1 -1
- package/icons/lucide/graduation-cap.svg +1 -1
- package/icons/lucide/hammer.svg +1 -1
- package/icons/lucide/hand-heart.svg +1 -1
- package/icons/lucide/handshake.svg +1 -1
- package/icons/lucide/hard-drives.svg +1 -1
- package/icons/lucide/heart.svg +1 -1
- package/icons/lucide/house.svg +1 -1
- package/icons/lucide/image-square.svg +1 -1
- package/icons/lucide/image.svg +1 -1
- package/icons/lucide/info.svg +1 -1
- package/icons/lucide/keyboard.svg +1 -1
- package/icons/lucide/leaf.svg +1 -1
- package/icons/lucide/lifebuoy.svg +1 -1
- package/icons/lucide/lightbulb.svg +1 -1
- package/icons/lucide/link-break.svg +1 -1
- package/icons/lucide/link-simple.svg +1 -1
- package/icons/lucide/list-bullets.svg +1 -1
- package/icons/lucide/list-checks.svg +1 -1
- package/icons/lucide/list-numbers.svg +1 -1
- package/icons/lucide/list.svg +1 -1
- package/icons/lucide/lock-simple-open.svg +1 -1
- package/icons/lucide/lock-simple.svg +1 -1
- package/icons/lucide/magnifying-glass.svg +1 -1
- package/icons/lucide/map-pin.svg +1 -1
- package/icons/lucide/map-trifold.svg +1 -1
- package/icons/lucide/medal.svg +1 -1
- package/icons/lucide/megaphone.svg +1 -1
- package/icons/lucide/minus.svg +1 -1
- package/icons/lucide/moon.svg +1 -1
- package/icons/lucide/newspaper.svg +1 -1
- package/icons/lucide/pack.json +23 -23
- package/icons/lucide/paint-brush-broad.svg +1 -1
- package/icons/lucide/palette.svg +1 -1
- package/icons/lucide/paper-plane-tilt.svg +1 -1
- package/icons/lucide/pencil-simple.svg +1 -1
- package/icons/lucide/phone.svg +1 -1
- package/icons/lucide/play-fill.svg +1 -1
- package/icons/lucide/plus.svg +1 -1
- package/icons/lucide/prohibit.svg +1 -1
- package/icons/lucide/puzzle-piece.svg +1 -1
- package/icons/lucide/question.svg +1 -1
- package/icons/lucide/quotes.svg +1 -1
- package/icons/lucide/rows.svg +1 -1
- package/icons/lucide/ruler.svg +1 -1
- package/icons/lucide/scan.svg +1 -1
- package/icons/lucide/shapes.svg +1 -1
- package/icons/lucide/share-network.svg +1 -1
- package/icons/lucide/shield-check.svg +1 -1
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sign-out.svg +1 -1
- package/icons/lucide/signpost.svg +1 -1
- package/icons/lucide/sliders-horizontal.svg +1 -1
- package/icons/lucide/smiley.svg +1 -1
- package/icons/lucide/sparkle.svg +1 -1
- package/icons/lucide/square-half-bottom.svg +1 -1
- package/icons/lucide/square.svg +1 -1
- package/icons/lucide/squares-four.svg +1 -1
- package/icons/lucide/sticker.svg +1 -1
- package/icons/lucide/storefront.svg +1 -1
- package/icons/lucide/sun.svg +1 -1
- package/icons/lucide/swatches.svg +1 -1
- package/icons/lucide/tag.svg +1 -1
- package/icons/lucide/text-aa.svg +1 -1
- package/icons/lucide/text-align-left.svg +1 -1
- package/icons/lucide/text-b.svg +1 -1
- package/icons/lucide/text-h-one.svg +1 -1
- package/icons/lucide/text-h-three.svg +1 -1
- package/icons/lucide/text-h-two.svg +1 -1
- package/icons/lucide/text-italic.svg +1 -1
- package/icons/lucide/text-t.svg +1 -1
- package/icons/lucide/ticket.svg +1 -1
- package/icons/lucide/toggle-right.svg +1 -1
- package/icons/lucide/train.svg +1 -1
- package/icons/lucide/trash.svg +1 -1
- package/icons/lucide/upload-simple.svg +1 -1
- package/icons/lucide/user-circle.svg +1 -1
- package/icons/lucide/user.svg +1 -1
- package/icons/lucide/users-three.svg +1 -1
- package/icons/lucide/users.svg +1 -1
- package/icons/lucide/video-camera.svg +1 -1
- package/icons/lucide/warning-circle.svg +1 -1
- package/icons/lucide/warning.svg +1 -1
- package/icons/lucide/wave-sine.svg +1 -1
- package/icons/lucide/wheelchair.svg +1 -1
- package/icons/lucide/wrench.svg +1 -1
- package/icons/lucide/x.svg +1 -1
- package/icons/material-symbols/LICENSE +201 -201
- package/icons/material-symbols/address-book.svg +1 -1
- package/icons/material-symbols/app-window.svg +1 -1
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
- package/icons/material-symbols/arrow-down.svg +1 -1
- package/icons/material-symbols/arrow-left.svg +1 -1
- package/icons/material-symbols/arrow-right.svg +1 -1
- package/icons/material-symbols/arrow-square-out.svg +1 -1
- package/icons/material-symbols/arrow-u-up-left.svg +1 -1
- package/icons/material-symbols/arrow-u-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up.svg +1 -1
- package/icons/material-symbols/arrows-down-up.svg +1 -1
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
- package/icons/material-symbols/article.svg +1 -1
- package/icons/material-symbols/bank.svg +1 -1
- package/icons/material-symbols/bell.svg +1 -1
- package/icons/material-symbols/bicycle.svg +1 -1
- package/icons/material-symbols/binoculars.svg +1 -1
- package/icons/material-symbols/book-open-text.svg +1 -1
- package/icons/material-symbols/book-open.svg +1 -1
- package/icons/material-symbols/browser.svg +1 -1
- package/icons/material-symbols/buildings.svg +1 -1
- package/icons/material-symbols/bus.svg +1 -1
- package/icons/material-symbols/calendar-blank.svg +1 -1
- package/icons/material-symbols/calendar-check.svg +1 -1
- package/icons/material-symbols/car.svg +1 -1
- package/icons/material-symbols/cards.svg +1 -1
- package/icons/material-symbols/caret-down.svg +1 -1
- package/icons/material-symbols/caret-left.svg +1 -1
- package/icons/material-symbols/caret-right.svg +1 -1
- package/icons/material-symbols/caret-up.svg +1 -1
- package/icons/material-symbols/certificate.svg +1 -1
- package/icons/material-symbols/chart-line-up.svg +1 -1
- package/icons/material-symbols/chats.svg +1 -1
- package/icons/material-symbols/check-circle.svg +1 -1
- package/icons/material-symbols/check.svg +1 -1
- package/icons/material-symbols/circle-half.svg +1 -1
- package/icons/material-symbols/circle.svg +1 -1
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
- package/icons/material-symbols/clock.svg +1 -1
- package/icons/material-symbols/code.svg +1 -1
- package/icons/material-symbols/coffee.svg +1 -1
- package/icons/material-symbols/columns.svg +1 -1
- package/icons/material-symbols/cookie.svg +1 -1
- package/icons/material-symbols/copy.svg +1 -1
- package/icons/material-symbols/credit-card.svg +1 -1
- package/icons/material-symbols/currency-circle-dollar.svg +1 -1
- package/icons/material-symbols/cursor-click.svg +1 -1
- package/icons/material-symbols/database.svg +1 -1
- package/icons/material-symbols/desktop.svg +1 -1
- package/icons/material-symbols/device-mobile.svg +1 -1
- package/icons/material-symbols/door-open.svg +1 -1
- package/icons/material-symbols/download-simple.svg +1 -1
- package/icons/material-symbols/envelope-simple.svg +1 -1
- package/icons/material-symbols/eye-slash.svg +1 -1
- package/icons/material-symbols/eye.svg +1 -1
- package/icons/material-symbols/file-pdf.svg +1 -1
- package/icons/material-symbols/file-text.svg +1 -1
- package/icons/material-symbols/file-video.svg +1 -1
- package/icons/material-symbols/file-zip.svg +1 -1
- package/icons/material-symbols/flower-lotus.svg +1 -1
- package/icons/material-symbols/folder-simple.svg +1 -1
- package/icons/material-symbols/funnel.svg +1 -1
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear.svg +1 -1
- package/icons/material-symbols/github-logo.svg +1 -1
- package/icons/material-symbols/globe.svg +1 -1
- package/icons/material-symbols/google-logo.svg +1 -1
- package/icons/material-symbols/graduation-cap.svg +1 -1
- package/icons/material-symbols/hammer.svg +1 -1
- package/icons/material-symbols/hand-heart.svg +1 -1
- package/icons/material-symbols/handshake.svg +1 -1
- package/icons/material-symbols/hard-drives.svg +1 -1
- package/icons/material-symbols/heart.svg +1 -1
- package/icons/material-symbols/house.svg +1 -1
- package/icons/material-symbols/image-square.svg +1 -1
- package/icons/material-symbols/image.svg +1 -1
- package/icons/material-symbols/info.svg +1 -1
- package/icons/material-symbols/keyboard.svg +1 -1
- package/icons/material-symbols/leaf.svg +1 -1
- package/icons/material-symbols/lifebuoy.svg +1 -1
- package/icons/material-symbols/lightbulb.svg +1 -1
- package/icons/material-symbols/link-break.svg +1 -1
- package/icons/material-symbols/link-simple.svg +1 -1
- package/icons/material-symbols/list-bullets.svg +1 -1
- package/icons/material-symbols/list-checks.svg +1 -1
- package/icons/material-symbols/list-numbers.svg +1 -1
- package/icons/material-symbols/list.svg +1 -1
- package/icons/material-symbols/lock-simple-open.svg +1 -1
- package/icons/material-symbols/lock-simple.svg +1 -1
- package/icons/material-symbols/magnifying-glass.svg +1 -1
- package/icons/material-symbols/map-pin.svg +1 -1
- package/icons/material-symbols/map-trifold.svg +1 -1
- package/icons/material-symbols/medal.svg +1 -1
- package/icons/material-symbols/megaphone.svg +1 -1
- package/icons/material-symbols/minus.svg +1 -1
- package/icons/material-symbols/moon.svg +1 -1
- package/icons/material-symbols/newspaper.svg +1 -1
- package/icons/material-symbols/pack.json +29 -29
- package/icons/material-symbols/paint-brush-broad.svg +1 -1
- package/icons/material-symbols/palette.svg +1 -1
- package/icons/material-symbols/paper-plane-tilt.svg +1 -1
- package/icons/material-symbols/pencil-simple.svg +1 -1
- package/icons/material-symbols/phone.svg +1 -1
- package/icons/material-symbols/play-fill.svg +1 -1
- package/icons/material-symbols/plus.svg +1 -1
- package/icons/material-symbols/prohibit.svg +1 -1
- package/icons/material-symbols/puzzle-piece.svg +1 -1
- package/icons/material-symbols/question.svg +1 -1
- package/icons/material-symbols/quotes.svg +1 -1
- package/icons/material-symbols/rows.svg +1 -1
- package/icons/material-symbols/ruler.svg +1 -1
- package/icons/material-symbols/scan.svg +1 -1
- package/icons/material-symbols/shapes.svg +1 -1
- package/icons/material-symbols/share-network.svg +1 -1
- package/icons/material-symbols/shield-check.svg +1 -1
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sign-out.svg +1 -1
- package/icons/material-symbols/signpost.svg +1 -1
- package/icons/material-symbols/sliders-horizontal.svg +1 -1
- package/icons/material-symbols/smiley.svg +1 -1
- package/icons/material-symbols/sparkle.svg +1 -1
- package/icons/material-symbols/square-half-bottom.svg +1 -1
- package/icons/material-symbols/square.svg +1 -1
- package/icons/material-symbols/squares-four.svg +1 -1
- package/icons/material-symbols/sticker.svg +1 -1
- package/icons/material-symbols/storefront.svg +1 -1
- package/icons/material-symbols/sun.svg +1 -1
- package/icons/material-symbols/swatches.svg +1 -1
- package/icons/material-symbols/tag.svg +1 -1
- package/icons/material-symbols/text-aa.svg +1 -1
- package/icons/material-symbols/text-align-left.svg +1 -1
- package/icons/material-symbols/text-b.svg +1 -1
- package/icons/material-symbols/text-h-one.svg +1 -1
- package/icons/material-symbols/text-h-three.svg +1 -1
- package/icons/material-symbols/text-h-two.svg +1 -1
- package/icons/material-symbols/text-italic.svg +1 -1
- package/icons/material-symbols/text-t.svg +1 -1
- package/icons/material-symbols/ticket.svg +1 -1
- package/icons/material-symbols/toggle-right.svg +1 -1
- package/icons/material-symbols/train.svg +1 -1
- package/icons/material-symbols/trash.svg +1 -1
- package/icons/material-symbols/upload-simple.svg +1 -1
- package/icons/material-symbols/user-circle.svg +1 -1
- package/icons/material-symbols/user.svg +1 -1
- package/icons/material-symbols/users-three.svg +1 -1
- package/icons/material-symbols/users.svg +1 -1
- package/icons/material-symbols/video-camera.svg +1 -1
- package/icons/material-symbols/warning-circle.svg +1 -1
- package/icons/material-symbols/warning.svg +1 -1
- package/icons/material-symbols/wave-sine.svg +1 -1
- package/icons/material-symbols/wheelchair.svg +1 -1
- package/icons/material-symbols/wrench.svg +1 -1
- package/icons/material-symbols/x.svg +1 -1
- package/icons/phosphor/pack.json +8 -8
- package/lib/apps.mjs +142 -139
- package/lib/apps.test.d.mts +1 -0
- package/lib/apps.test.mjs +50 -0
- package/lib/blocks.mjs +90 -90
- package/lib/caseCollisions.d.mts +4 -0
- package/lib/caseCollisions.mjs +62 -0
- package/lib/caseCollisions.test.d.mts +1 -0
- package/lib/caseCollisions.test.mjs +74 -0
- package/lib/colors.ts +86 -86
- package/lib/contactBar.ts +18 -18
- package/lib/contracts.d.mts +15 -0
- package/lib/contracts.mjs +17 -0
- package/lib/cookieCategories.mjs +28 -28
- package/lib/crop.mjs +83 -83
- package/lib/csp.mjs +130 -130
- package/lib/cspSources.mjs +60 -60
- package/lib/dynamic-colors.ts +426 -426
- package/lib/git.mjs +64 -64
- package/lib/hooks.ts +51 -51
- package/lib/inline.ts +20 -20
- package/lib/license-texts/Apache-2.0.txt +190 -190
- package/lib/license-texts/BSD-2-Clause.txt +24 -24
- package/lib/license-texts/BSD-3-Clause.txt +28 -28
- package/lib/license-texts/ISC.txt +15 -15
- package/lib/license-texts/MIT.txt +21 -21
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.mjs +683 -683
- package/lib/markdown.ts +42 -42
- package/lib/motion-schemes.mjs +5 -5
- package/lib/motion.ts +131 -131
- package/lib/notice.mjs +34 -34
- package/lib/scheduled.mjs +37 -37
- package/lib/snackbar.ts +149 -149
- package/lib/status.mjs +18 -18
- package/lib/storeEntries.d.mts +18 -0
- package/lib/storeEntries.mjs +64 -0
- package/lib/storeEntries.test.d.mts +1 -0
- package/lib/storeEntries.test.mjs +71 -0
- package/lib/svg.mjs +231 -231
- package/lib/text-colors.mjs +43 -43
- package/lib/video-id.mjs +72 -72
- package/lib/video.mjs +91 -91
- package/package.json +136 -131
- package/pages/404.astro +10 -10
- package/pages/[...slug].astro +51 -51
- package/pages/_headers.txt +31 -31
- package/pages/blog/index.astro +10 -10
- package/pages/blog/kategori/[category].astro +18 -18
- package/pages/editor/code-images.json.js +25 -25
- package/pages/editor/index.astro +75 -75
- package/pages/editor/manifest.webmanifest.js +23 -23
- package/pages/editor/sw.js +33 -33
- package/pages/editor/sw.js.js +4 -4
- package/pages/editor/thumbnails.json.js +21 -21
- package/pages/editor/version.json.js +22 -22
- package/pages/headers.js +4 -4
- package/pages/index.astro +10 -10
- package/pages/ny.astro +6 -6
- package/pages/robots.txt.js +3 -3
- package/pages/rss.xml.js +20 -20
- package/scripts/app-screenshots.mjs +164 -164
- package/scripts/app-workers.mjs +6 -6
- package/scripts/bin.mjs +12 -12
- package/scripts/built-site.mjs +134 -99
- package/scripts/check-consent.mjs +113 -113
- package/scripts/check-content.mjs +151 -149
- package/scripts/check-demo.mjs +24 -24
- package/scripts/check-store.mjs +160 -0
- package/scripts/check-themes.mjs +45 -45
- package/scripts/check-worker.mjs +569 -569
- package/scripts/compare-html.mjs +81 -81
- package/scripts/declarations.mjs +10 -0
- package/scripts/declarations.test.mjs +32 -0
- package/scripts/demo-site.mjs +42 -42
- package/scripts/editor-icons.mjs +56 -56
- package/scripts/editor-roundtrip.mjs +134 -134
- package/scripts/editor-screens.mjs +101 -101
- package/scripts/git-env.mjs +9 -9
- package/scripts/icon-packs.mjs +151 -151
- package/scripts/link-counts.mjs +22 -22
- package/scripts/page-colors.mjs +42 -42
- package/scripts/releases.mjs +80 -80
- package/scripts/responsive-images.mjs +94 -94
- package/scripts/scheduled-list.mjs +15 -15
- package/scripts/theme-screenshots.mjs +88 -88
- package/scripts/types.mjs +17 -21
- package/scripts/video-thumbnails.mjs +89 -89
- package/scripts/worker-modules.mjs +34 -34
- package/site/appCookies.mjs +74 -74
- package/site/apps.ts +21 -21
- package/site/autolinks.mjs +200 -200
- package/site/blockScripts.ts +31 -31
- package/site/categories.ts +16 -16
- package/site/consent.ts +26 -26
- package/site/content.config.ts +84 -84
- package/site/content.ts +104 -104
- package/site/cookieList.ts +17 -17
- package/site/defineSite.d.mts +2 -14
- package/site/defineSite.mjs +131 -136
- package/site/directives/interact.mjs +31 -31
- package/site/editor-version.mjs +26 -26
- package/site/fill.ts +13 -13
- package/site/format.ts +11 -11
- package/site/home.ts +66 -66
- package/site/iconPacks.mjs +43 -43
- package/site/icons.mjs +605 -605
- package/site/images.mjs +157 -157
- package/site/motion.ts +13 -13
- package/site/noindex.mjs +21 -21
- package/site/pageColors.ts +110 -110
- package/site/popups.ts +98 -98
- package/site/schema.ts +189 -189
- package/site/sections.ts +3 -3
- package/site/seo.ts +28 -28
- package/site/site.ts +76 -76
- package/site/siteConfig.mjs +8 -8
- package/site/siteCsp.mjs +75 -75
- package/site/siteModules.mjs +27 -27
- package/site/standardPages.ts +59 -59
- package/site/storeTheme.d.mts +6 -0
- package/site/storeTheme.mjs +29 -0
- package/site/storeTheme.test.d.mts +1 -0
- package/site/storeTheme.test.mjs +53 -0
- package/site/testimonials.ts +48 -48
- package/site/text.ts +43 -43
- package/site/theme.mjs +483 -472
- package/site/tracking.client.ts +225 -225
- package/site/tracking.ts +9 -9
- package/src/ActionParts.tsx +205 -205
- package/src/Advanced.tsx +152 -152
- package/src/App.tsx +901 -904
- package/src/AppList.tsx +1362 -1345
- package/src/BlockDialog.tsx +360 -360
- package/src/BlockParts.tsx +652 -652
- package/src/BlockSettings.tsx +533 -533
- package/src/BrandForm.tsx +164 -164
- package/src/Colors.tsx +476 -476
- package/src/CommunicationParts.tsx +199 -199
- package/src/CompanyForm.tsx +209 -209
- package/src/Consents.tsx +175 -175
- package/src/ContainmentParts.tsx +185 -185
- package/src/Crop.tsx +152 -152
- package/src/DataForm.tsx +434 -434
- package/src/Deleted.tsx +177 -177
- package/src/EntryForm.tsx +740 -740
- package/src/FileView.tsx +30 -30
- package/src/FormFrame.tsx +237 -237
- package/src/Keywords.tsx +225 -225
- package/src/Library.tsx +1563 -1563
- package/src/Licenses.tsx +336 -336
- package/src/LinkSheet.tsx +138 -138
- package/src/MarkdownEditor.tsx +1075 -1075
- package/src/Media.tsx +677 -677
- package/src/NavigationParts.tsx +350 -350
- package/src/NavigationRestParts.tsx +247 -247
- package/src/Notifications.tsx +59 -59
- package/src/PagePicker.tsx +136 -136
- package/src/Parts.tsx +991 -991
- package/src/PartsShown.tsx +168 -168
- package/src/PickerParts.tsx +183 -183
- package/src/Redirects.tsx +450 -450
- package/src/Screen.tsx +184 -184
- package/src/SectionsField.tsx +65 -65
- package/src/SelectionParts.tsx +134 -134
- package/src/SeoScores.tsx +184 -184
- package/src/Sheet.tsx +35 -35
- package/src/ThemeStore.tsx +376 -376
- package/src/Tokens.tsx +346 -346
- package/src/Versions.tsx +214 -214
- package/src/addresses.ts +75 -75
- package/src/allIcons.ts +7 -7
- package/src/appFiles.ts +29 -29
- package/src/apps.ts +281 -281
- package/src/ask.tsx +123 -123
- package/src/blocks.ts +251 -251
- package/src/brand.ts +153 -153
- package/src/cache.ts +42 -42
- package/src/check.ts +285 -285
- package/src/checkSite.ts +256 -256
- package/src/{consents.ts → consentData.ts} +26 -26
- package/src/cookieList.ts +70 -70
- package/src/countries.ts +59 -59
- package/src/deploy.ts +155 -155
- package/src/editor.css +602 -602
- package/src/examples.ts +85 -85
- package/src/fields.tsx +942 -942
- package/src/frontmatter.mjs +42 -42
- package/src/github.ts +376 -376
- package/src/i18n/da-DK/advanced.ts +28 -28
- package/src/i18n/da-DK/app.ts +71 -71
- package/src/i18n/da-DK/apps.ts +90 -90
- package/src/i18n/da-DK/blockSettings.ts +38 -38
- package/src/i18n/da-DK/blocks.ts +408 -408
- package/src/i18n/da-DK/check.ts +25 -25
- package/src/i18n/da-DK/colors.ts +64 -64
- package/src/i18n/da-DK/common.ts +118 -118
- package/src/i18n/da-DK/company.ts +24 -24
- package/src/i18n/da-DK/consents.ts +23 -23
- package/src/i18n/da-DK/forms.ts +116 -116
- package/src/i18n/da-DK/history.ts +35 -35
- package/src/i18n/da-DK/index.ts +49 -49
- package/src/i18n/da-DK/keywords.ts +19 -19
- package/src/i18n/da-DK/library.ts +135 -135
- package/src/i18n/da-DK/licenses.ts +25 -25
- package/src/i18n/da-DK/media.ts +79 -79
- package/src/i18n/da-DK/parts.ts +696 -696
- package/src/i18n/da-DK/preview.ts +13 -13
- package/src/i18n/da-DK/redirects.ts +51 -51
- package/src/i18n/da-DK/seo.ts +20 -20
- package/src/i18n/da-DK/text.ts +35 -35
- package/src/i18n/da-DK/theme.ts +41 -41
- package/src/i18n/en-US/advanced.ts +32 -32
- package/src/i18n/en-US/app.ts +84 -84
- package/src/i18n/en-US/apps.ts +134 -134
- package/src/i18n/en-US/blockSettings.ts +48 -48
- package/src/i18n/en-US/blocks.ts +441 -441
- package/src/i18n/en-US/check.ts +31 -31
- package/src/i18n/en-US/colors.ts +82 -82
- package/src/i18n/en-US/common.ts +144 -144
- package/src/i18n/en-US/company.ts +25 -25
- package/src/i18n/en-US/consents.ts +25 -25
- package/src/i18n/en-US/forms.ts +145 -145
- package/src/i18n/en-US/history.ts +43 -43
- package/src/i18n/en-US/index.ts +51 -51
- package/src/i18n/en-US/keywords.ts +24 -24
- package/src/i18n/en-US/library.ts +176 -176
- package/src/i18n/en-US/licenses.ts +36 -36
- package/src/i18n/en-US/media.ts +80 -80
- package/src/i18n/en-US/parts.ts +718 -718
- package/src/i18n/en-US/preview.ts +15 -15
- package/src/i18n/en-US/redirects.ts +66 -66
- package/src/i18n/en-US/seo.ts +26 -26
- package/src/i18n/en-US/text.ts +41 -41
- package/src/i18n/en-US/theme.ts +41 -41
- package/src/i18n/index.ts +124 -124
- package/src/icon.tsx +160 -160
- package/src/iconPacks.ts +89 -89
- package/src/images.ts +163 -163
- package/src/{licenses.ts → licenseData.ts} +79 -79
- package/src/markdown.mjs +204 -204
- package/src/nav.ts +68 -68
- package/src/notFound.ts +28 -28
- package/src/{notifications.ts → notificationData.ts} +223 -223
- package/src/pagePreview.tsx +204 -204
- package/src/r2Caching.ts +55 -55
- package/src/{redirects.ts → redirectData.ts} +149 -149
- package/src/registry.tsx +188 -188
- package/src/schema.ts +417 -417
- package/src/sections.mjs +303 -303
- package/src/seo.ts +333 -333
- package/src/shared.ts +44 -44
- package/src/sidebar.ts +31 -31
- package/src/site.ts +317 -317
- package/src/standard/Block.astro +19 -19
- package/src/standard/BlockSite.astro +18 -18
- package/src/standard/accordion/index.tsx +47 -47
- package/src/standard/accordion/load.ts +23 -23
- package/src/standard/accordion/shared.ts +15 -15
- package/src/standard/accordion/view.ts +32 -32
- package/src/standard/buttons/fields.ts +98 -98
- package/src/standard/buttons/index.tsx +23 -23
- package/src/standard/buttons/load.ts +34 -34
- package/src/standard/buttons/shared.ts +12 -12
- package/src/standard/buttons/view.ts +30 -30
- package/src/standard/cards/index.tsx +71 -71
- package/src/standard/cards/load.ts +17 -17
- package/src/standard/cards/shared.ts +27 -27
- package/src/standard/cards/view.ts +29 -29
- package/src/standard/checklist/index.tsx +27 -27
- package/src/standard/checklist/load.ts +5 -5
- package/src/standard/checklist/view.ts +13 -13
- package/src/standard/cookies/index.tsx +17 -17
- package/src/standard/cookies/list.ts +56 -56
- package/src/standard/cookies/load.ts +11 -11
- package/src/standard/cookies/shared.ts +20 -20
- package/src/standard/cookies/view.ts +28 -28
- package/src/standard/faq/index.tsx +29 -29
- package/src/standard/faq/load.ts +14 -14
- package/src/standard/faq/view.ts +21 -21
- package/src/standard/head.ts +23 -23
- package/src/standard/hero/fields.ts +48 -48
- package/src/standard/hero/index.tsx +57 -57
- package/src/standard/hero/load.ts +23 -23
- package/src/standard/hero/view.ts +38 -38
- package/src/standard/links/index.tsx +41 -41
- package/src/standard/links/load.ts +17 -17
- package/src/standard/links/view.ts +25 -25
- package/src/standard/list/index.tsx +50 -50
- package/src/standard/list/load.ts +5 -5
- package/src/standard/list/view.ts +18 -18
- package/src/standard/load.ts +7 -7
- package/src/standard/notice/index.tsx +30 -30
- package/src/standard/notice/load.ts +16 -16
- package/src/standard/notice/view.ts +10 -10
- package/src/standard/points/index.tsx +42 -42
- package/src/standard/points/load.ts +5 -5
- package/src/standard/points/view.ts +12 -12
- package/src/standard/posts/index.tsx +132 -132
- package/src/standard/posts/load.ts +36 -36
- package/src/standard/posts/shared.ts +21 -21
- package/src/standard/posts/view.ts +63 -63
- package/src/standard/pricecards/index.tsx +57 -57
- package/src/standard/pricecards/load.ts +11 -11
- package/src/standard/pricecards/view.ts +50 -50
- package/src/standard/sharedTexts.ts +6 -6
- package/src/standard/site.ts +124 -124
- package/src/standard/siteLooks.ts +19 -19
- package/src/standard/sites.ts +13 -13
- package/src/standard/steps/index.tsx +28 -28
- package/src/standard/steps/load.ts +5 -5
- package/src/standard/steps/view.ts +11 -11
- package/src/standard/testimonials/index.tsx +87 -87
- package/src/standard/testimonials/list.ts +43 -43
- package/src/standard/testimonials/load.ts +58 -58
- package/src/standard/testimonials/shared.ts +13 -13
- package/src/standard/testimonials/view.ts +76 -76
- package/src/standard/themeBlocks.ts +13 -13
- package/src/standard/video/index.tsx +23 -23
- package/src/standard/video/load.ts +10 -10
- package/src/standard/video/view.ts +42 -42
- package/src/standard/view.tsx +105 -105
- package/src/standard/views.ts +37 -37
- package/src/store.ts +479 -499
- package/src/theme.ts +90 -90
- package/src/themeUi.ts +97 -97
- package/src/themes.ts +91 -91
- package/src/ui.tsx +3402 -3402
- package/src/virtual.d.ts +5 -5
- package/src/when.ts +22 -22
- package/starter/.githooks/pre-push +4 -4
- package/starter/.nvmrc +1 -1
- package/starter/.prettierignore +11 -11
- package/starter/.prettierrc.json +7 -7
- package/starter/README.md +27 -26
- package/starter/astro.config.mjs +5 -5
- package/starter/package.json +67 -67
- package/starter/public/_redirects +1 -1
- package/starter/public/favicon.svg +1 -1
- package/starter/public/logo.svg +1 -1
- package/starter/src/content/pages/blog.md +8 -8
- package/starter/src/content/pages/forside.md +9 -9
- package/starter/src/content/pages/kontakt.md +7 -7
- package/starter/src/content/pages/privatlivspolitik.md +9 -9
- package/starter/src/content/pages/siden-findes-ikke.md +7 -7
- package/starter/src/content.config.ts +2 -2
- package/starter/src/data/apps.json +1 -1
- package/starter/src/data/block-settings.json +3 -3
- package/starter/src/data/blocks.json +1 -1
- package/starter/src/data/booking.json +6 -6
- package/starter/src/data/colors.json +70 -70
- package/starter/src/data/contact.json +8 -8
- package/starter/src/data/cookies.json +21 -21
- package/starter/src/data/csp.json +1 -1
- package/starter/src/data/footer.json +15 -15
- package/starter/src/data/internal-links.json +4 -4
- package/starter/src/data/menu.json +4 -4
- package/starter/src/data/mobile-menu.json +4 -4
- package/starter/src/data/old-addresses.json +1 -1
- package/starter/src/data/redirects.json +1 -1
- package/starter/src/data/standard-pages.json +8 -8
- package/starter/src/data/theme.json +1 -1
- package/starter/src/env.d.ts +2 -2
- package/starter/src/site/config.ts +13 -13
- package/starter/src/site/worker.js +14 -14
- package/starter/tsconfig.json +17 -17
- package/starter/worker/apps.js +6 -6
- package/starter/wrangler.jsonc +39 -39
- package/theme/LICENSE +21 -21
- package/theme/blocks/ArrowLink.tsx +14 -14
- package/theme/blocks/BlockHead.tsx +26 -26
- package/theme/blocks/Panel.tsx +32 -32
- package/theme/blocks/Person.tsx +26 -26
- package/theme/blocks/accordion/PageRow.tsx +25 -25
- package/theme/blocks/accordion/View.tsx +17 -17
- package/theme/blocks/buttons/Buttons.tsx +28 -28
- package/theme/blocks/buttons/View.tsx +12 -12
- package/theme/blocks/cards/LinkCards.tsx +72 -72
- package/theme/blocks/cards/View.tsx +20 -20
- package/theme/blocks/checklist/Checks.tsx +15 -15
- package/theme/blocks/checklist/View.tsx +14 -14
- package/theme/blocks/cookies/View.tsx +36 -36
- package/theme/blocks/faq/Accordion.tsx +34 -34
- package/theme/blocks/faq/View.tsx +16 -16
- package/theme/blocks/faq/script.ts +56 -56
- package/theme/blocks/hero/View.tsx +44 -44
- package/theme/blocks/links/LinkGroups.tsx +24 -24
- package/theme/blocks/links/View.tsx +17 -17
- package/theme/blocks/list/Rows.tsx +24 -24
- package/theme/blocks/list/View.tsx +14 -14
- package/theme/blocks/notice/View.tsx +13 -13
- package/theme/blocks/points/PointList.tsx +34 -34
- package/theme/blocks/points/View.tsx +14 -14
- package/theme/blocks/posts/View.tsx +26 -26
- package/theme/blocks/pricecards/View.tsx +40 -40
- package/theme/blocks/steps/StepList.tsx +23 -23
- package/theme/blocks/steps/View.tsx +14 -14
- package/theme/blocks/testimonials/List.tsx +46 -46
- package/theme/blocks/testimonials/Slider.tsx +44 -44
- package/theme/blocks/testimonials/Testimonial.tsx +124 -124
- package/theme/blocks/testimonials/View.tsx +21 -21
- package/theme/blocks/testimonials/script.ts +52 -52
- package/theme/blocks/video/View.tsx +41 -41
- package/theme/components/ActionBar.astro +63 -63
- package/theme/components/AppSlot.astro +21 -21
- package/theme/components/ArrowLink.tsx +21 -21
- package/theme/components/BlogIndex.astro +48 -48
- package/theme/components/BlogPosts.astro +18 -18
- package/theme/components/BlogPosts.tsx +45 -45
- package/theme/components/BookButton.tsx +22 -22
- package/theme/components/Container.astro +11 -11
- package/theme/components/CookieConsent.astro +121 -121
- package/theme/components/EndBlock.astro +21 -21
- package/theme/components/FontPreload.astro +6 -6
- package/theme/components/Footer.astro +85 -85
- package/theme/components/Header.astro +186 -186
- package/theme/components/Icon.astro +16 -16
- package/theme/components/Img.astro +29 -29
- package/theme/components/Img.tsx +33 -33
- package/theme/components/Inline.astro +11 -11
- package/theme/components/Logo.astro +23 -23
- package/theme/components/LogoMark.astro +10 -10
- package/theme/components/Markdown.astro +17 -17
- package/theme/components/MobileMenu.tsx +224 -224
- package/theme/components/MobileNav.astro +33 -33
- package/theme/components/PageHeader.astro +22 -22
- package/theme/components/PageSection.astro +24 -24
- package/theme/components/PhoneButton.tsx +11 -11
- package/theme/components/Popup.astro +145 -145
- package/theme/components/PopupButton.tsx +24 -24
- package/theme/components/PopupView.tsx +48 -48
- package/theme/components/PostList.tsx +58 -58
- package/theme/components/PostMeta.tsx +17 -17
- package/theme/components/RichText.astro +28 -28
- package/theme/components/Section.astro +29 -29
- package/theme/components/SectionHeading.tsx +8 -8
- package/theme/components/TextSection.astro +16 -16
- package/theme/components/home/PageBlock.astro +19 -19
- package/theme/components/home/Text.astro +13 -13
- package/theme/components/loadPopup.ts +22 -22
- package/theme/components/loadPosts.ts +16 -16
- package/theme/components/posts.ts +15 -15
- package/theme/editor/looks.mjs +7 -7
- package/theme/editor/previews.tsx +56 -56
- package/theme/layouts/Base.astro +273 -273
- package/theme/layouts/PageLayout.astro +126 -126
- package/theme/layouts/PostLayout.astro +137 -137
- package/theme/scripts/motion.ts +29 -29
- package/theme/styles/base.css +320 -320
- package/theme/styles/blocks.css +1808 -1808
- package/theme/styles/components.css +206 -206
- package/theme/styles/global.css +1302 -1302
- package/theme/styles/ui.css +6544 -6544
- package/theme/templates/Blog.astro +38 -38
- package/theme/templates/Category.astro +19 -19
- package/theme/templates/Home.astro +36 -36
- package/theme/templates/New.astro +68 -68
- package/theme/templates/NotFound.astro +117 -117
- package/theme/theme.json +28 -28
- package/theme/ui/Badge.tsx +45 -45
- package/theme/ui/BottomSheet.tsx +148 -148
- package/theme/ui/Button.tsx +79 -79
- package/theme/ui/ButtonGroup.tsx +31 -31
- package/theme/ui/Card.tsx +45 -45
- package/theme/ui/Carousel.tsx +124 -124
- package/theme/ui/Checkbox.tsx +59 -59
- package/theme/ui/Chip.tsx +101 -101
- package/theme/ui/ChipSet.tsx +27 -27
- package/theme/ui/DatePicker.tsx +149 -149
- package/theme/ui/DatePickerContainer.tsx +316 -316
- package/theme/ui/Dialog.tsx +100 -100
- package/theme/ui/Divider.tsx +27 -27
- package/theme/ui/ExposedDropdownMenu.tsx +77 -77
- package/theme/ui/Fab.tsx +88 -88
- package/theme/ui/FabMenu.tsx +67 -67
- package/theme/ui/IconButton.tsx +78 -78
- package/theme/ui/List.tsx +287 -287
- package/theme/ui/LoadingIndicator.tsx +29 -29
- package/theme/ui/Menu.tsx +278 -278
- package/theme/ui/NavigationBar.tsx +62 -62
- package/theme/ui/NavigationDrawer.tsx +80 -80
- package/theme/ui/NavigationRail.tsx +154 -154
- package/theme/ui/ProgressIndicator.tsx +162 -162
- package/theme/ui/RadioButton.tsx +50 -50
- package/theme/ui/Search.tsx +129 -129
- package/theme/ui/SegmentedButton.tsx +49 -49
- package/theme/ui/SideSheet.tsx +54 -54
- package/theme/ui/Slider.tsx +155 -155
- package/theme/ui/Snackbar.tsx +67 -67
- package/theme/ui/SplitButton.tsx +67 -67
- package/theme/ui/Switch.tsx +60 -60
- package/theme/ui/Tabs.tsx +114 -114
- package/theme/ui/TextField.tsx +278 -278
- package/theme/ui/TimePicker.tsx +128 -128
- package/theme/ui/TimePickerContainer.tsx +234 -234
- package/theme/ui/Toolbar.tsx +67 -67
- package/theme/ui/Tooltip.tsx +58 -58
- package/theme/ui/TopAppBar.tsx +77 -77
- package/theme/ui/bottomSheet.script.ts +8 -8
- package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
- package/theme/ui/carousel.script.ts +28 -28
- package/theme/ui/carouselLayout.ts +229 -229
- package/theme/ui/checkbox.script.ts +8 -8
- package/theme/ui/datePicker.script.ts +302 -302
- package/theme/ui/dates.ts +185 -185
- package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
- package/theme/ui/dropdownMenu.script.ts +166 -166
- package/theme/ui/fabMenu.script.ts +57 -57
- package/theme/ui/navigationRail.script.ts +47 -47
- package/theme/ui/ripple.script.ts +159 -159
- package/theme/ui/search.script.ts +92 -92
- package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
- package/theme/ui/slider.script.ts +12 -12
- package/theme/ui/sliderUpdate.ts +52 -52
- package/theme/ui/snackbar.script.ts +16 -16
- package/theme/ui/splitButton.script.ts +63 -63
- package/theme/ui/tabs.script.ts +48 -48
- package/theme/ui/textField.script.ts +17 -17
- package/theme/ui/textFieldUpdate.ts +44 -44
- package/theme/ui/timePicker.script.ts +248 -248
- package/theme/ui/toolbar.script.ts +43 -43
- package/theme/ui/tooltip.script.ts +68 -68
- package/worker/consent.js +141 -141
- package/worker/editor-github.js +167 -167
- package/worker/editor-host.js +74 -74
- package/worker/files.js +401 -401
- package/worker/github.js +36 -36
- package/worker/index.js +188 -184
- package/worker/not-found.js +97 -97
- package/worker/oauth.js +95 -95
- package/worker/publish.js +52 -52
- package/worker/push.js +270 -270
- package/worker/r2-status.js +50 -50
- package/worker/session.js +58 -58
- package/worker/store.js +46 -0
package/defaults/rules.ts
CHANGED
|
@@ -1,450 +1,450 @@
|
|
|
1
|
-
// The site's own rules for the editor: the lists the fields that pick something choose from (a page, a
|
|
2
|
-
// post, a tag, a testimonial), the prices and testimonials the blocks show, and why an entry cannot be
|
|
3
|
-
// deleted (something on the site uses it). The messages are shown in the editor's language (siteText,
|
|
4
|
-
// texts.ts).
|
|
5
|
-
import type { RepoFile } from '../src/github';
|
|
6
|
-
import type { Option, PageState, TestimonialRow } from '../src/fields';
|
|
7
|
-
import type { Status } from '../src/FormFrame';
|
|
8
|
-
import { blocksIn } from '../lib/blocks.mjs';
|
|
9
|
-
import { readFields, splitFrontmatter } from '../src/frontmatter.mjs';
|
|
10
|
-
import { siteText } from '../src/i18n';
|
|
11
|
-
import { byText, fixedName, slugOf, type Collection, type Relation } from '../src/schema';
|
|
12
|
-
import { collections } from '@site/collections';
|
|
13
|
-
import { config } from '@site/config';
|
|
14
|
-
import { opensOf } from '@site/opens';
|
|
15
|
-
import { differentPages, standardPages, standardPaths } from '@site/standardPages';
|
|
16
|
-
|
|
17
|
-
const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
|
|
18
|
-
// A page's address: `path`, or /<file name>/ for pages made in the editor.
|
|
19
|
-
const pagePath = (f: RepoFile) => String(fm(f).path ?? `/${slugOf(f.path, config.folders.pages)}/`);
|
|
20
|
-
const isPage = (f: RepoFile) => f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md');
|
|
21
|
-
// The addresses of the standard pages, by their key ("Indstillinger" › "Udseende" › "Standardsider", standardPages.ts).
|
|
22
|
-
const standardOf = (files: { path: string; text: string }[]) => standardPaths(files.find((f) => f.path === config.standardPages)?.text);
|
|
23
|
-
// The page that is a standard page (the front page: 'front'), if it is there.
|
|
24
|
-
const standardFile = (files: RepoFile[], key: string) => {
|
|
25
|
-
const path = standardOf(files)[key];
|
|
26
|
-
return path ? files.find((f) => isPage(f) && pagePath(f) === path) : undefined;
|
|
27
|
-
};
|
|
28
|
-
// Where a page is shown: the front page at /, the blog page at /blog/, the others at their own address.
|
|
29
|
-
const shownAt = (path: string, files: { path: string; text: string }[]) => {
|
|
30
|
-
const paths = standardOf(files);
|
|
31
|
-
return standardPages.find((p) => p.at && paths[p.key] === path)?.at ?? path;
|
|
32
|
-
};
|
|
33
|
-
const json = (file: RepoFile) => JSON.parse(file.text) as Record<string, unknown>;
|
|
34
|
-
const byLabel = (a: Option, b: Option) => byText(a.label, b.label);
|
|
35
|
-
// A page's or post's status (packages/editor/lib/status.mjs), for the filter of the sheet it is picked in (usePickTools).
|
|
36
|
-
const stateOf = (f: RepoFile): PageState => {
|
|
37
|
-
const s = fm(f).status;
|
|
38
|
-
return s === 'draft' || s === 'unlisted' || s === 'scheduled' ? s : 'published';
|
|
39
|
-
};
|
|
40
|
-
// The addresses the menu (config.menu) links to, at every level, without a part of a page (#…).
|
|
41
|
-
type MenuEntry = { href: string; children?: MenuEntry[] };
|
|
42
|
-
function menuAddresses(files: RepoFile[]): Set<string> {
|
|
43
|
-
let entries: MenuEntry[] = [];
|
|
44
|
-
try {
|
|
45
|
-
entries = JSON.parse(files.find((f) => f.path === config.menu)?.text ?? '{}').items ?? [];
|
|
46
|
-
} catch {}
|
|
47
|
-
const all = (e: MenuEntry[]): string[] => e.flatMap((x) => [String(x.href ?? '').replace(/#.*/, ''), ...all(x.children ?? [])]);
|
|
48
|
-
return new Set(all(entries));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// The choices for the fields that pick a page, a post, a category or a testimonial.
|
|
52
|
-
export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
53
|
-
const inFolder = (folder: string, ext: string) => files.filter((f) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext));
|
|
54
|
-
// A standard page is in the menu by its own address or where it is shown (the front page at /, the blog page at /blog/).
|
|
55
|
-
const menu = menuAddresses(files);
|
|
56
|
-
const page = (f: RepoFile): Option => ({
|
|
57
|
-
value: pagePath(f),
|
|
58
|
-
label: String(fm(f).title),
|
|
59
|
-
state: stateOf(f),
|
|
60
|
-
inMenu: menu.has(pagePath(f)) || menu.has(shownAt(pagePath(f), files)),
|
|
61
|
-
});
|
|
62
|
-
return {
|
|
63
|
-
// Pages are stored by their address: `path`, or /<file name>/ for pages made in the editor.
|
|
64
|
-
pages: inFolder(config.folders.pages, '.md').map(page).sort(byLabel),
|
|
65
|
-
// The pages that can be a standard page ("Indstillinger" › "Udseende" › "Standardsider"): those on the site, not a
|
|
66
|
-
// draft, one waiting for its time or a new version of a page (/ny/).
|
|
67
|
-
standardPages: inFolder(config.folders.pages, '.md')
|
|
68
|
-
.filter((f) => !['draft', 'scheduled'].includes(String(fm(f).status ?? '')) && !pagePath(f).startsWith('/ny/'))
|
|
69
|
-
.map(page)
|
|
70
|
-
.sort(byLabel),
|
|
71
|
-
// The front page (/) and the blog (/blog/), whichever pages they are, the blog by its page's title.
|
|
72
|
-
fixed: [
|
|
73
|
-
{ value: '/', label: siteText('Forside'), inMenu: menu.has('/') },
|
|
74
|
-
{
|
|
75
|
-
value: '/blog/',
|
|
76
|
-
label: String((standardFile(files, 'blog') && fm(standardFile(files, 'blog')!).title) || siteText('Blog')),
|
|
77
|
-
inMenu: menu.has('/blog/'),
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
blog: inFolder(config.folders.blog, '.md')
|
|
81
|
-
.map((f) => ({
|
|
82
|
-
value: slugOf(f.path, config.folders.blog),
|
|
83
|
-
label: String(fm(f).title),
|
|
84
|
-
state: stateOf(f),
|
|
85
|
-
date: String(fm(f).date ?? ''),
|
|
86
|
-
}))
|
|
87
|
-
.sort(byLabel),
|
|
88
|
-
categories: inFolder('src/data/categories', '.json')
|
|
89
|
-
.map((f) => ({ value: slugOf(f.path, 'src/data/categories'), label: String(json(f).name) }))
|
|
90
|
-
.sort(byLabel),
|
|
91
|
-
testimonials: inFolder('src/data/testimonials', '.json')
|
|
92
|
-
.map((f) => ({ value: slugOf(f.path, 'src/data/testimonials'), label: String(json(f).name) }))
|
|
93
|
-
.sort(byLabel),
|
|
94
|
-
// The promotions turned on that a button can open ("Knapper": "Åbn promovering", opens.ts), by what opens with
|
|
95
|
-
// the promotion's name ("Bookingarket (Calendly bookingark)"), where they are shown (a button offers those on its
|
|
96
|
-
// page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book en tid").
|
|
97
|
-
promotions: inFolder('src/data/popups', '.json')
|
|
98
|
-
.flatMap((f) => {
|
|
99
|
-
const d = json(f);
|
|
100
|
-
const opens = opensOf(d.type ? String(d.type) : undefined);
|
|
101
|
-
if (!opens || d.enabled === false) return [];
|
|
102
|
-
const name = String(d.heading ?? d.name ?? '');
|
|
103
|
-
return [
|
|
104
|
-
{
|
|
105
|
-
value: slugOf(f.path, 'src/data/popups'),
|
|
106
|
-
label: siteText('{hvad} ({navn})', { hvad: siteText(opens.label), navn: name }),
|
|
107
|
-
on: { everywhere: d.where === 'all', pages: d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [] },
|
|
108
|
-
...(opens.as ? { as: opens.as } : { text: String(d.heading ?? '') }),
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
})
|
|
112
|
-
.sort(byLabel),
|
|
113
|
-
// The PDFs in "Filer", by their address on the site (/filer/<name>.pdf), named by their file name.
|
|
114
|
-
files: inFolder(config.folders.files, '.pdf')
|
|
115
|
-
.map((f) => ({ value: f.path.replace(/^public/, ''), label: f.path.split('/').pop()! }))
|
|
116
|
-
.sort(byLabel),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const frontPage = (files: RepoFile[]) => ({
|
|
121
|
-
to: standardFile(files, 'front')?.path ?? config.standardPages,
|
|
122
|
-
text: siteText('Rediger forsiden'),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Where a button that opens a promotion is: a page (`path`), the shared buttons (on any page: neither), or an action bar
|
|
126
|
-
// (`bar`: the pages it is shown on, or none for every page or the posts with its tags).
|
|
127
|
-
type Opener = { file: string; title: string; path?: string; bar?: { pages?: string[] } };
|
|
128
|
-
|
|
129
|
-
// The buttons that open a promotion ("Knapper": "Åbn promovering", the button's `open`), where they are: the page with
|
|
130
|
-
// the block (its file, title and the address it is shown at: the front page's /), the shared buttons
|
|
131
|
-
// ("Standardblokke" › "Knapper", on any page: no address), or an action bar ("Promovering").
|
|
132
|
-
function openersOf(slug: string, files: { path: string; text: string }[]): Opener[] {
|
|
133
|
-
const opens = (items: unknown) =>
|
|
134
|
-
Array.isArray(items) &&
|
|
135
|
-
items.some(
|
|
136
|
-
(b) =>
|
|
137
|
-
(b as Record<string, unknown>)?.type !== 'text' &&
|
|
138
|
-
(b as Record<string, unknown>)?.to === 'promotion' &&
|
|
139
|
-
(b as Record<string, unknown>).open === slug,
|
|
140
|
-
);
|
|
141
|
-
const found: Opener[] = [];
|
|
142
|
-
for (const f of files) {
|
|
143
|
-
if (f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md')) {
|
|
144
|
-
// Also a "Knapper" block in a block's text (a "Banner"'s), where its quotes are written as ".
|
|
145
|
-
if (!/data-block=(?:"|\\")buttons/.test(f.text)) continue;
|
|
146
|
-
const d = fm(f as RepoFile);
|
|
147
|
-
if (
|
|
148
|
-
blocksIn(f.text, true).some((b: { type: string; props: Record<string, unknown> }) => b.type === 'buttons' && opens(b.props.items))
|
|
149
|
-
)
|
|
150
|
-
found.push({ file: f.path, title: String(d.title ?? ''), path: shownAt(pagePath(f as RepoFile), files) });
|
|
151
|
-
} else if (f.path.startsWith('src/data/popups/') && f.path.endsWith('.json')) {
|
|
152
|
-
const d = json(f as RepoFile);
|
|
153
|
-
if (d.type === 'bookingbar' && d.enabled !== false && opens(d.buttons))
|
|
154
|
-
found.push({
|
|
155
|
-
file: f.path,
|
|
156
|
-
title: siteText('Handlingslinje'),
|
|
157
|
-
bar: { pages: d.where === 'all' || d.where === 'tags' ? undefined : Array.isArray(d.pages) ? d.pages.map(String) : [] },
|
|
158
|
-
});
|
|
159
|
-
} else if (f.path === config.blockTexts) {
|
|
160
|
-
if (opens((json(f as RepoFile).buttons as Record<string, unknown> | undefined)?.items))
|
|
161
|
-
found.push({ file: 'blokke/buttons', title: siteText('Standardknapper') });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return found;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Whether a promotion as it is (or is about to be saved) is shown where a button opens it: turned on, of a kind a button
|
|
168
|
-
// can open, and on every page, or on the button's page (the shared buttons are on any page), or on every page the
|
|
169
|
-
// action bar is on.
|
|
170
|
-
function shownFor(d: Record<string, unknown>, at: Opener) {
|
|
171
|
-
if (d.enabled === false || !opensOf(d.type ? String(d.type) : undefined)) return false;
|
|
172
|
-
if (d.where === 'all') return true;
|
|
173
|
-
const pages = d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [];
|
|
174
|
-
if (at.bar) return !!at.bar.pages && at.bar.pages.every((p) => pages.includes(p));
|
|
175
|
-
return !!at.path && pages.includes(at.path);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const openerText = (at: Opener) =>
|
|
179
|
-
at.bar
|
|
180
|
-
? siteText(
|
|
181
|
-
'En knap i handlingslinjen åbner promoveringen, så den skal være slået til og vises alle steder, hvor linjen vises. Ret knappen først.',
|
|
182
|
-
)
|
|
183
|
-
: at.path
|
|
184
|
-
? siteText('En knap på siden "{side}" åbner promoveringen, så den skal være slået til og vises dér. Ret knappen først.', {
|
|
185
|
-
side: at.title,
|
|
186
|
-
})
|
|
187
|
-
: siteText('Standardknapperne åbner promoveringen, så den skal være slået til og vises på alle sider. Ret dem først.');
|
|
188
|
-
const openerEdit = (at: Opener) => ({
|
|
189
|
-
to: at.file,
|
|
190
|
-
text: siteText(at.bar ? 'Rediger handlingslinjen' : at.path ? 'Rediger siden' : 'Rediger standardknapperne'),
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
/** Why a promotion cannot be saved like this (turned off, or no longer shown on a page where a button opens it,
|
|
194
|
-
* "Knapper": "Åbn promovering"), with a link to the button, or the standard pages (the same page twice, or a page
|
|
195
|
-
* without the block it must have, with a link to it), or null. Before it is saved (DataForm.tsx; the standard pages in
|
|
196
|
-
* CompanyForm.tsx, without a collection). */
|
|
197
|
-
export function keptBy(collection: Collection | null, path: string, data: Record<string, unknown>, files: RepoFile[]): Status {
|
|
198
|
-
if (path === config.standardPages) return standardKept(data, files);
|
|
199
|
-
if (collection?.kind !== 'folder' || collection.id !== 'popups') return null;
|
|
200
|
-
const at = openersOf(slugOf(path, collection.folder), files).find((o) => !shownFor(data, o));
|
|
201
|
-
return at ? { text: openerText(at), error: true, edit: openerEdit(at) } : null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// The standard pages as they are about to be saved: different pages (the cookie policy may be the privacy policy), each
|
|
205
|
-
// with the block it must have.
|
|
206
|
-
function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status {
|
|
207
|
-
const chosen = standardPages.map((p) => ({ ...p, path: String(data[p.key] ?? '') })).filter((p) => p.path);
|
|
208
|
-
if (!differentPages(Object.fromEntries(chosen.map((p) => [p.key, p.path]))))
|
|
209
|
-
return { text: siteText('Standardsiderne skal være forskellige sider.'), error: true };
|
|
210
|
-
for (const p of chosen) {
|
|
211
|
-
const page = files.find((f) => isPage(f) && pagePath(f) === p.path);
|
|
212
|
-
if (!p.needs || !page || blocksIn(page.text).some((b: { type: string }) => b.type === p.needs!.type)) continue;
|
|
213
|
-
return {
|
|
214
|
-
text: siteText('Siden "{side}" har ikke blokken {blok}, som {hvad} skal have. Sæt den ind på siden først.', {
|
|
215
|
-
side: String(fm(page).title),
|
|
216
|
-
blok: siteText(p.needs.label),
|
|
217
|
-
hvad: siteText(p.the).toLowerCase(),
|
|
218
|
-
}),
|
|
219
|
-
error: true,
|
|
220
|
-
edit: { to: page.path, text: siteText('Rediger siden') },
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** What is wrong on the site by its own rules, a line for each, before it is built (checkSite.ts): a standard page that
|
|
227
|
-
* is not a page on the site, or the same page twice, and a button that opens a promotion that is gone, turned off or not
|
|
228
|
-
* shown on its page. */
|
|
229
|
-
export function siteProblems(files: { path: string; text: string }[]): string[] {
|
|
230
|
-
const problems: string[] = [];
|
|
231
|
-
// The standard pages are pages on the site ("Indstillinger" › "Udseende" › "Standardsider"), different ones (but for
|
|
232
|
-
// the cookie policy, which may be the privacy policy).
|
|
233
|
-
if (files.some((f) => f.path === config.standardPages)) {
|
|
234
|
-
for (const p of standardPages) {
|
|
235
|
-
const page = standardFile(files as RepoFile[], p.key);
|
|
236
|
-
if (!page || ['draft', 'scheduled'].includes(String(fm(page).status ?? '')))
|
|
237
|
-
problems.push(`${config.standardPages}: ${siteText('{side} skal være en side, der er udgivet.', { side: siteText(p.the) })}`);
|
|
238
|
-
}
|
|
239
|
-
if (!differentPages(standardOf(files)))
|
|
240
|
-
problems.push(`${config.standardPages}: ${siteText('Standardsiderne skal være forskellige sider.')}`);
|
|
241
|
-
}
|
|
242
|
-
const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
|
|
243
|
-
const slugs = new Set<string>();
|
|
244
|
-
for (const f of files) for (const m of f.text.matchAll(/(?:"|")open(?:"|"):\s*(?:"|")([^"&]+)/g)) slugs.add(m[1]);
|
|
245
|
-
for (const slug of slugs) {
|
|
246
|
-
const file = promotions.find((f) => slugOf(f.path, 'src/data/popups') === slug);
|
|
247
|
-
for (const at of openersOf(slug, files)) {
|
|
248
|
-
if (file && shownFor(json(file as RepoFile), at)) continue;
|
|
249
|
-
problems.push(
|
|
250
|
-
`${at.file.startsWith('blokke/') ? config.blockTexts : at.file}: ${
|
|
251
|
-
file ? openerText(at) : siteText('En knap åbner promoveringen "{navn}", som ikke findes.', { navn: slug })
|
|
252
|
-
}`,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return problems;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// The tags a promotion is shown on the posts of (Promovering, "Vises på": "Blogindlæg med tags").
|
|
260
|
-
const promotionTags = (d: Record<string, unknown>): string[] => (d.where === 'tags' && Array.isArray(d.tags) ? d.tags.map(String) : []);
|
|
261
|
-
|
|
262
|
-
// Why an entry cannot be deleted (something on the site uses it), with a link to where, or null.
|
|
263
|
-
export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]): Status {
|
|
264
|
-
if (collection.kind === 'files') return null;
|
|
265
|
-
const slug = slugOf(file.path, collection.folder);
|
|
266
|
-
// A promotion a button opens ("Knapper": "Åbn promovering").
|
|
267
|
-
if (collection.id === 'popups') {
|
|
268
|
-
const [at] = openersOf(slug, files);
|
|
269
|
-
if (at)
|
|
270
|
-
return {
|
|
271
|
-
text: at.bar
|
|
272
|
-
? siteText('En knap i handlingslinjen åbner promoveringen. Ret knappen først.')
|
|
273
|
-
: at.path
|
|
274
|
-
? siteText('En knap på siden "{side}" åbner promoveringen. Ret knappen først.', { side: at.title })
|
|
275
|
-
: siteText('Standardknapperne åbner promoveringen. Ret dem først.'),
|
|
276
|
-
error: true,
|
|
277
|
-
edit: openerEdit(at),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
const home = standardFile(files, 'front');
|
|
281
|
-
const front = Object.fromEntries(
|
|
282
|
-
(home ? (blocksIn(home.text) as { type: string; props: Record<string, unknown> }[]) : []).map((b) => [b.type, b.props]),
|
|
283
|
-
);
|
|
284
|
-
if (collection.id === 'kategorier') {
|
|
285
|
-
const posts = files.filter(
|
|
286
|
-
(f) => f.path.startsWith(`${config.folders.blog}/`) && ((fm(f).categories as string[]) ?? []).includes(slug),
|
|
287
|
-
);
|
|
288
|
-
if (posts.length)
|
|
289
|
-
return {
|
|
290
|
-
text:
|
|
291
|
-
posts.length === 1
|
|
292
|
-
? siteText('Tagget bruges på et blogindlæg. Fjern det fra det først.')
|
|
293
|
-
: siteText('Tagget bruges på {antal} blogindlæg. Fjern det fra dem først.', { antal: posts.length }),
|
|
294
|
-
error: true,
|
|
295
|
-
};
|
|
296
|
-
const quotes = files.filter(
|
|
297
|
-
(f) => f.path.startsWith('src/data/testimonials/') && ((json(f).categories as string[] | undefined) ?? []).includes(slug),
|
|
298
|
-
);
|
|
299
|
-
if (quotes.length)
|
|
300
|
-
return {
|
|
301
|
-
text:
|
|
302
|
-
quotes.length === 1
|
|
303
|
-
? siteText('Tagget bruges på en udtalelse. Fjern det fra den først.')
|
|
304
|
-
: siteText('Tagget bruges på {antal} udtalelser. Fjern det fra dem først.', { antal: quotes.length }),
|
|
305
|
-
error: true,
|
|
306
|
-
};
|
|
307
|
-
// A promotion shown on the posts with the tag (Promovering, "Blogindlæg med tags").
|
|
308
|
-
const promotion = files.find(
|
|
309
|
-
(f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json') && promotionTags(json(f)).includes(slug),
|
|
310
|
-
);
|
|
311
|
-
if (promotion) {
|
|
312
|
-
const d = json(promotion);
|
|
313
|
-
return {
|
|
314
|
-
text: siteText('Tagget bruges i promoveringen "{navn}". Fjern det fra den først.', { navn: String(d.heading ?? d.name ?? '') }),
|
|
315
|
-
error: true,
|
|
316
|
-
edit: { to: promotion.path, text: siteText('Rediger promoveringen') },
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
const fp = front.posts as Record<string, unknown> | undefined;
|
|
320
|
-
if ((fp?.categories as string[] | undefined)?.includes(slug) || fp?.link === slug)
|
|
321
|
-
return {
|
|
322
|
-
text: siteText('Tagget vælger indlæggene på forsiden under "Fra bloggen". Vælg et andet først.'),
|
|
323
|
-
error: true,
|
|
324
|
-
edit: frontPage(files),
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
if (collection.id === 'blog' && ((front.posts as Record<string, unknown>)?.picked as string[])?.includes(slug))
|
|
328
|
-
return {
|
|
329
|
-
text: siteText('Indlægget er valgt til forsiden under "Fra bloggen". Vælg et andet først.'),
|
|
330
|
-
error: true,
|
|
331
|
-
edit: frontPage(files),
|
|
332
|
-
};
|
|
333
|
-
// A "Blogindlæg" block in a page that picks the post (its props are in the text).
|
|
334
|
-
if (collection.id === 'blog') {
|
|
335
|
-
const page = files.find(
|
|
336
|
-
(f) =>
|
|
337
|
-
f.path.startsWith(`${config.folders.pages}/`) && f.text.includes('data-block="posts"') && f.text.includes(`"${slug}"`),
|
|
338
|
-
);
|
|
339
|
-
if (page)
|
|
340
|
-
return {
|
|
341
|
-
text: siteText('Indlægget er valgt i en blok med blogindlæg på siden "{side}". Vælg et andet først.', {
|
|
342
|
-
side: String(fm(page).title),
|
|
343
|
-
}),
|
|
344
|
-
error: true,
|
|
345
|
-
edit: { to: page.path, text: siteText('Rediger siden') },
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
// Any other place under "Bruges på" (usesOf): a "Blogindlæg" or "Udtalelser" block that picks the tag, on a
|
|
349
|
-
// page or in a post.
|
|
350
|
-
if (collection.id === 'kategorier') {
|
|
351
|
-
const use = usesOf(collection, files)(file)[0];
|
|
352
|
-
if (use) {
|
|
353
|
-
const post = use.path.startsWith(`${config.folders.blog}/`);
|
|
354
|
-
return {
|
|
355
|
-
text: siteText(
|
|
356
|
-
post
|
|
357
|
-
? 'Tagget er valgt i en blok i blogindlægget "{side}". Vælg et andet først.'
|
|
358
|
-
: 'Tagget er valgt i en blok på siden "{side}". Vælg et andet først.',
|
|
359
|
-
{ side: use.name },
|
|
360
|
-
),
|
|
361
|
-
error: true,
|
|
362
|
-
edit: { to: use.path, text: siteText(post ? 'Rediger indlægget' : 'Rediger siden') },
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// What a file that uses something is called: a page's or post's title, a setting's name, an entry's
|
|
370
|
-
// as its list shows it.
|
|
371
|
-
function nameOf(file: RepoFile): string {
|
|
372
|
-
for (const c of collections)
|
|
373
|
-
if (c.kind === 'files') {
|
|
374
|
-
const own = c.files.find((f) => f.file === file.path);
|
|
375
|
-
if (own) return fixedName({ ...own, label: siteText(own.label) }, file.text);
|
|
376
|
-
} else if (c.kind === 'folder' && file.path.startsWith(`${c.folder}/`) && file.path.endsWith('.json'))
|
|
377
|
-
return c.summary(json(file)) || siteText(c.label);
|
|
378
|
-
return String(fm(file).title ?? file.path);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export type Use = { path: string; name: string };
|
|
382
|
-
|
|
383
|
-
// The places on the site that use an entry, by the entry's file, each once (the tag's "Bruges på", DataForm.tsx,
|
|
384
|
-
// and the number at it in its list, Library.tsx); [] for none. For a tag: the blog posts, testimonials and
|
|
385
|
-
// promotions that have it, and the pages and posts with a "Blogindlæg" or "Udtalelser" block that
|
|
386
|
-
// picks it (its tags, or the tag "Alle indlæg" goes to). Made once for the whole list.
|
|
387
|
-
export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => Use[] {
|
|
388
|
-
if (collection.kind === 'files' || collection.id !== 'kategorier') return () => [];
|
|
389
|
-
const uses = tagUses(files);
|
|
390
|
-
return (file) =>
|
|
391
|
-
[...(uses.get(slugOf(file.path, collection.folder))?.values() ?? [])]
|
|
392
|
-
.map((f) => ({ path: f.path, name: nameOf(f) }))
|
|
393
|
-
.sort((a, b) => byText(a.name, b.name));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// The files that use each tag (usesOf), by the tag, each file once.
|
|
397
|
-
function tagUses(files: RepoFile[]): Map<string, Map<string, RepoFile>> {
|
|
398
|
-
const uses = new Map<string, Map<string, RepoFile>>();
|
|
399
|
-
const add = (f: RepoFile, slugs: unknown) => {
|
|
400
|
-
for (const slug of Array.isArray(slugs) ? slugs.map(String) : []) {
|
|
401
|
-
if (!uses.has(slug)) uses.set(slug, new Map());
|
|
402
|
-
uses.get(slug)!.set(f.path, f);
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
const block = (f: RepoFile, type: unknown, props: Record<string, unknown>) => {
|
|
406
|
-
if (type === 'posts' || type === 'testimonials')
|
|
407
|
-
add(f, [...(Array.isArray(props.categories) ? props.categories : []), ...(props.link ? [props.link] : [])]);
|
|
408
|
-
};
|
|
409
|
-
const inFolder = (f: RepoFile, folder: string, ext: string) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext);
|
|
410
|
-
for (const f of files) {
|
|
411
|
-
if (inFolder(f, config.folders.blog, '.md') || inFolder(f, config.folders.pages, '.md')) {
|
|
412
|
-
if (inFolder(f, config.folders.blog, '.md')) add(f, fm(f).categories);
|
|
413
|
-
for (const b of blocksIn(f.text) as { type: string; props: Record<string, unknown> }[]) block(f, b.type, b.props);
|
|
414
|
-
} else if (inFolder(f, 'src/data/testimonials', '.json')) add(f, json(f).categories);
|
|
415
|
-
else if (inFolder(f, 'src/data/popups', '.json')) add(f, promotionTags(json(f)));
|
|
416
|
-
}
|
|
417
|
-
return uses;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// The number at the right of an entry in the list of a part, by the entry's file, or null for none
|
|
421
|
-
// (Library.tsx): at each tag, the number of places that use it (usesOf).
|
|
422
|
-
export function countsOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => number | null {
|
|
423
|
-
if (collection.kind === 'files' || collection.id !== 'kategorier') return () => null;
|
|
424
|
-
const uses = tagUses(files);
|
|
425
|
-
return (file) => uses.get(slugOf(file.path, collection.folder))?.size ?? 0;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// The testimonials, in their order on the site, for the cards and previews of the blocks.
|
|
429
|
-
export const testimonialsOf = (files: RepoFile[]) =>
|
|
430
|
-
files
|
|
431
|
-
.filter((f) => f.path.startsWith('src/data/testimonials/') && f.path.endsWith('.json'))
|
|
432
|
-
.map((f) => ({ ...(json(f) as TestimonialRow & { order?: number }), slug: slugOf(f.path, 'src/data/testimonials') }))
|
|
433
|
-
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
434
|
-
|
|
435
|
-
/** What is at an address, as said in a message: "forsiden", 'siden "Om mig" (/om-mig/)',
|
|
436
|
-
* or the address (where a redirect sends to, App.tsx). */
|
|
437
|
-
export function whatIsAt(address: string, relations: Record<Relation, Option[]>): string {
|
|
438
|
-
const base = address.split('#')[0];
|
|
439
|
-
if (base === '/') return siteText('forsiden');
|
|
440
|
-
if (base === '/blog/') return siteText('bloggen');
|
|
441
|
-
const page = [...relations.fixed, ...relations.pages].find((o) => o.value === base);
|
|
442
|
-
if (page) return siteText('siden "{navn}" ({adresse})', { navn: page.label, adresse: address });
|
|
443
|
-
const post = relations.blog.find((o) => `/${o.value}/` === base);
|
|
444
|
-
if (post) return siteText('blogindlægget "{navn}" ({adresse})', { navn: post.label, adresse: address });
|
|
445
|
-
const tag = relations.categories.find((o) => `/blog/kategori/${o.value}/` === base);
|
|
446
|
-
if (tag) return siteText('tagget "{navn}" på bloggen ({adresse})', { navn: tag.label, adresse: address });
|
|
447
|
-
if (base.startsWith(config.folders.media.replace(/^public/, '') + '/'))
|
|
448
|
-
return siteText('billedet {navn}', { navn: base.split('/').pop()! });
|
|
449
|
-
return address;
|
|
450
|
-
}
|
|
1
|
+
// The site's own rules for the editor: the lists the fields that pick something choose from (a page, a
|
|
2
|
+
// post, a tag, a testimonial), the prices and testimonials the blocks show, and why an entry cannot be
|
|
3
|
+
// deleted (something on the site uses it). The messages are shown in the editor's language (siteText,
|
|
4
|
+
// texts.ts).
|
|
5
|
+
import type { RepoFile } from '../src/github';
|
|
6
|
+
import type { Option, PageState, TestimonialRow } from '../src/fields';
|
|
7
|
+
import type { Status } from '../src/FormFrame';
|
|
8
|
+
import { blocksIn } from '../lib/blocks.mjs';
|
|
9
|
+
import { readFields, splitFrontmatter } from '../src/frontmatter.mjs';
|
|
10
|
+
import { siteText } from '../src/i18n';
|
|
11
|
+
import { byText, fixedName, slugOf, type Collection, type Relation } from '../src/schema';
|
|
12
|
+
import { collections } from '@site/collections';
|
|
13
|
+
import { config } from '@site/config';
|
|
14
|
+
import { opensOf } from '@site/opens';
|
|
15
|
+
import { differentPages, standardPages, standardPaths } from '@site/standardPages';
|
|
16
|
+
|
|
17
|
+
const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
|
|
18
|
+
// A page's address: `path`, or /<file name>/ for pages made in the editor.
|
|
19
|
+
const pagePath = (f: RepoFile) => String(fm(f).path ?? `/${slugOf(f.path, config.folders.pages)}/`);
|
|
20
|
+
const isPage = (f: RepoFile) => f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md');
|
|
21
|
+
// The addresses of the standard pages, by their key ("Indstillinger" › "Udseende" › "Standardsider", standardPages.ts).
|
|
22
|
+
const standardOf = (files: { path: string; text: string }[]) => standardPaths(files.find((f) => f.path === config.standardPages)?.text);
|
|
23
|
+
// The page that is a standard page (the front page: 'front'), if it is there.
|
|
24
|
+
const standardFile = (files: RepoFile[], key: string) => {
|
|
25
|
+
const path = standardOf(files)[key];
|
|
26
|
+
return path ? files.find((f) => isPage(f) && pagePath(f) === path) : undefined;
|
|
27
|
+
};
|
|
28
|
+
// Where a page is shown: the front page at /, the blog page at /blog/, the others at their own address.
|
|
29
|
+
const shownAt = (path: string, files: { path: string; text: string }[]) => {
|
|
30
|
+
const paths = standardOf(files);
|
|
31
|
+
return standardPages.find((p) => p.at && paths[p.key] === path)?.at ?? path;
|
|
32
|
+
};
|
|
33
|
+
const json = (file: RepoFile) => JSON.parse(file.text) as Record<string, unknown>;
|
|
34
|
+
const byLabel = (a: Option, b: Option) => byText(a.label, b.label);
|
|
35
|
+
// A page's or post's status (packages/editor/lib/status.mjs), for the filter of the sheet it is picked in (usePickTools).
|
|
36
|
+
const stateOf = (f: RepoFile): PageState => {
|
|
37
|
+
const s = fm(f).status;
|
|
38
|
+
return s === 'draft' || s === 'unlisted' || s === 'scheduled' ? s : 'published';
|
|
39
|
+
};
|
|
40
|
+
// The addresses the menu (config.menu) links to, at every level, without a part of a page (#…).
|
|
41
|
+
type MenuEntry = { href: string; children?: MenuEntry[] };
|
|
42
|
+
function menuAddresses(files: RepoFile[]): Set<string> {
|
|
43
|
+
let entries: MenuEntry[] = [];
|
|
44
|
+
try {
|
|
45
|
+
entries = JSON.parse(files.find((f) => f.path === config.menu)?.text ?? '{}').items ?? [];
|
|
46
|
+
} catch {}
|
|
47
|
+
const all = (e: MenuEntry[]): string[] => e.flatMap((x) => [String(x.href ?? '').replace(/#.*/, ''), ...all(x.children ?? [])]);
|
|
48
|
+
return new Set(all(entries));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The choices for the fields that pick a page, a post, a category or a testimonial.
|
|
52
|
+
export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
53
|
+
const inFolder = (folder: string, ext: string) => files.filter((f) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext));
|
|
54
|
+
// A standard page is in the menu by its own address or where it is shown (the front page at /, the blog page at /blog/).
|
|
55
|
+
const menu = menuAddresses(files);
|
|
56
|
+
const page = (f: RepoFile): Option => ({
|
|
57
|
+
value: pagePath(f),
|
|
58
|
+
label: String(fm(f).title),
|
|
59
|
+
state: stateOf(f),
|
|
60
|
+
inMenu: menu.has(pagePath(f)) || menu.has(shownAt(pagePath(f), files)),
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
// Pages are stored by their address: `path`, or /<file name>/ for pages made in the editor.
|
|
64
|
+
pages: inFolder(config.folders.pages, '.md').map(page).sort(byLabel),
|
|
65
|
+
// The pages that can be a standard page ("Indstillinger" › "Udseende" › "Standardsider"): those on the site, not a
|
|
66
|
+
// draft, one waiting for its time or a new version of a page (/ny/).
|
|
67
|
+
standardPages: inFolder(config.folders.pages, '.md')
|
|
68
|
+
.filter((f) => !['draft', 'scheduled'].includes(String(fm(f).status ?? '')) && !pagePath(f).startsWith('/ny/'))
|
|
69
|
+
.map(page)
|
|
70
|
+
.sort(byLabel),
|
|
71
|
+
// The front page (/) and the blog (/blog/), whichever pages they are, the blog by its page's title.
|
|
72
|
+
fixed: [
|
|
73
|
+
{ value: '/', label: siteText('Forside'), inMenu: menu.has('/') },
|
|
74
|
+
{
|
|
75
|
+
value: '/blog/',
|
|
76
|
+
label: String((standardFile(files, 'blog') && fm(standardFile(files, 'blog')!).title) || siteText('Blog')),
|
|
77
|
+
inMenu: menu.has('/blog/'),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
blog: inFolder(config.folders.blog, '.md')
|
|
81
|
+
.map((f) => ({
|
|
82
|
+
value: slugOf(f.path, config.folders.blog),
|
|
83
|
+
label: String(fm(f).title),
|
|
84
|
+
state: stateOf(f),
|
|
85
|
+
date: String(fm(f).date ?? ''),
|
|
86
|
+
}))
|
|
87
|
+
.sort(byLabel),
|
|
88
|
+
categories: inFolder('src/data/categories', '.json')
|
|
89
|
+
.map((f) => ({ value: slugOf(f.path, 'src/data/categories'), label: String(json(f).name) }))
|
|
90
|
+
.sort(byLabel),
|
|
91
|
+
testimonials: inFolder('src/data/testimonials', '.json')
|
|
92
|
+
.map((f) => ({ value: slugOf(f.path, 'src/data/testimonials'), label: String(json(f).name) }))
|
|
93
|
+
.sort(byLabel),
|
|
94
|
+
// The promotions turned on that a button can open ("Knapper": "Åbn promovering", opens.ts), by what opens with
|
|
95
|
+
// the promotion's name ("Bookingarket (Calendly bookingark)"), where they are shown (a button offers those on its
|
|
96
|
+
// page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book en tid").
|
|
97
|
+
promotions: inFolder('src/data/popups', '.json')
|
|
98
|
+
.flatMap((f) => {
|
|
99
|
+
const d = json(f);
|
|
100
|
+
const opens = opensOf(d.type ? String(d.type) : undefined);
|
|
101
|
+
if (!opens || d.enabled === false) return [];
|
|
102
|
+
const name = String(d.heading ?? d.name ?? '');
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
value: slugOf(f.path, 'src/data/popups'),
|
|
106
|
+
label: siteText('{hvad} ({navn})', { hvad: siteText(opens.label), navn: name }),
|
|
107
|
+
on: { everywhere: d.where === 'all', pages: d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [] },
|
|
108
|
+
...(opens.as ? { as: opens.as } : { text: String(d.heading ?? '') }),
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
})
|
|
112
|
+
.sort(byLabel),
|
|
113
|
+
// The PDFs in "Filer", by their address on the site (/filer/<name>.pdf), named by their file name.
|
|
114
|
+
files: inFolder(config.folders.files, '.pdf')
|
|
115
|
+
.map((f) => ({ value: f.path.replace(/^public/, ''), label: f.path.split('/').pop()! }))
|
|
116
|
+
.sort(byLabel),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const frontPage = (files: RepoFile[]) => ({
|
|
121
|
+
to: standardFile(files, 'front')?.path ?? config.standardPages,
|
|
122
|
+
text: siteText('Rediger forsiden'),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Where a button that opens a promotion is: a page (`path`), the shared buttons (on any page: neither), or an action bar
|
|
126
|
+
// (`bar`: the pages it is shown on, or none for every page or the posts with its tags).
|
|
127
|
+
type Opener = { file: string; title: string; path?: string; bar?: { pages?: string[] } };
|
|
128
|
+
|
|
129
|
+
// The buttons that open a promotion ("Knapper": "Åbn promovering", the button's `open`), where they are: the page with
|
|
130
|
+
// the block (its file, title and the address it is shown at: the front page's /), the shared buttons
|
|
131
|
+
// ("Standardblokke" › "Knapper", on any page: no address), or an action bar ("Promovering").
|
|
132
|
+
function openersOf(slug: string, files: { path: string; text: string }[]): Opener[] {
|
|
133
|
+
const opens = (items: unknown) =>
|
|
134
|
+
Array.isArray(items) &&
|
|
135
|
+
items.some(
|
|
136
|
+
(b) =>
|
|
137
|
+
(b as Record<string, unknown>)?.type !== 'text' &&
|
|
138
|
+
(b as Record<string, unknown>)?.to === 'promotion' &&
|
|
139
|
+
(b as Record<string, unknown>).open === slug,
|
|
140
|
+
);
|
|
141
|
+
const found: Opener[] = [];
|
|
142
|
+
for (const f of files) {
|
|
143
|
+
if (f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md')) {
|
|
144
|
+
// Also a "Knapper" block in a block's text (a "Banner"'s), where its quotes are written as ".
|
|
145
|
+
if (!/data-block=(?:"|\\")buttons/.test(f.text)) continue;
|
|
146
|
+
const d = fm(f as RepoFile);
|
|
147
|
+
if (
|
|
148
|
+
blocksIn(f.text, true).some((b: { type: string; props: Record<string, unknown> }) => b.type === 'buttons' && opens(b.props.items))
|
|
149
|
+
)
|
|
150
|
+
found.push({ file: f.path, title: String(d.title ?? ''), path: shownAt(pagePath(f as RepoFile), files) });
|
|
151
|
+
} else if (f.path.startsWith('src/data/popups/') && f.path.endsWith('.json')) {
|
|
152
|
+
const d = json(f as RepoFile);
|
|
153
|
+
if (d.type === 'bookingbar' && d.enabled !== false && opens(d.buttons))
|
|
154
|
+
found.push({
|
|
155
|
+
file: f.path,
|
|
156
|
+
title: siteText('Handlingslinje'),
|
|
157
|
+
bar: { pages: d.where === 'all' || d.where === 'tags' ? undefined : Array.isArray(d.pages) ? d.pages.map(String) : [] },
|
|
158
|
+
});
|
|
159
|
+
} else if (f.path === config.blockTexts) {
|
|
160
|
+
if (opens((json(f as RepoFile).buttons as Record<string, unknown> | undefined)?.items))
|
|
161
|
+
found.push({ file: 'blokke/buttons', title: siteText('Standardknapper') });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return found;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Whether a promotion as it is (or is about to be saved) is shown where a button opens it: turned on, of a kind a button
|
|
168
|
+
// can open, and on every page, or on the button's page (the shared buttons are on any page), or on every page the
|
|
169
|
+
// action bar is on.
|
|
170
|
+
function shownFor(d: Record<string, unknown>, at: Opener) {
|
|
171
|
+
if (d.enabled === false || !opensOf(d.type ? String(d.type) : undefined)) return false;
|
|
172
|
+
if (d.where === 'all') return true;
|
|
173
|
+
const pages = d.where !== 'tags' && Array.isArray(d.pages) ? d.pages.map(String) : [];
|
|
174
|
+
if (at.bar) return !!at.bar.pages && at.bar.pages.every((p) => pages.includes(p));
|
|
175
|
+
return !!at.path && pages.includes(at.path);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const openerText = (at: Opener) =>
|
|
179
|
+
at.bar
|
|
180
|
+
? siteText(
|
|
181
|
+
'En knap i handlingslinjen åbner promoveringen, så den skal være slået til og vises alle steder, hvor linjen vises. Ret knappen først.',
|
|
182
|
+
)
|
|
183
|
+
: at.path
|
|
184
|
+
? siteText('En knap på siden "{side}" åbner promoveringen, så den skal være slået til og vises dér. Ret knappen først.', {
|
|
185
|
+
side: at.title,
|
|
186
|
+
})
|
|
187
|
+
: siteText('Standardknapperne åbner promoveringen, så den skal være slået til og vises på alle sider. Ret dem først.');
|
|
188
|
+
const openerEdit = (at: Opener) => ({
|
|
189
|
+
to: at.file,
|
|
190
|
+
text: siteText(at.bar ? 'Rediger handlingslinjen' : at.path ? 'Rediger siden' : 'Rediger standardknapperne'),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/** Why a promotion cannot be saved like this (turned off, or no longer shown on a page where a button opens it,
|
|
194
|
+
* "Knapper": "Åbn promovering"), with a link to the button, or the standard pages (the same page twice, or a page
|
|
195
|
+
* without the block it must have, with a link to it), or null. Before it is saved (DataForm.tsx; the standard pages in
|
|
196
|
+
* CompanyForm.tsx, without a collection). */
|
|
197
|
+
export function keptBy(collection: Collection | null, path: string, data: Record<string, unknown>, files: RepoFile[]): Status {
|
|
198
|
+
if (path === config.standardPages) return standardKept(data, files);
|
|
199
|
+
if (collection?.kind !== 'folder' || collection.id !== 'popups') return null;
|
|
200
|
+
const at = openersOf(slugOf(path, collection.folder), files).find((o) => !shownFor(data, o));
|
|
201
|
+
return at ? { text: openerText(at), error: true, edit: openerEdit(at) } : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// The standard pages as they are about to be saved: different pages (the cookie policy may be the privacy policy), each
|
|
205
|
+
// with the block it must have.
|
|
206
|
+
function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status {
|
|
207
|
+
const chosen = standardPages.map((p) => ({ ...p, path: String(data[p.key] ?? '') })).filter((p) => p.path);
|
|
208
|
+
if (!differentPages(Object.fromEntries(chosen.map((p) => [p.key, p.path]))))
|
|
209
|
+
return { text: siteText('Standardsiderne skal være forskellige sider.'), error: true };
|
|
210
|
+
for (const p of chosen) {
|
|
211
|
+
const page = files.find((f) => isPage(f) && pagePath(f) === p.path);
|
|
212
|
+
if (!p.needs || !page || blocksIn(page.text).some((b: { type: string }) => b.type === p.needs!.type)) continue;
|
|
213
|
+
return {
|
|
214
|
+
text: siteText('Siden "{side}" har ikke blokken {blok}, som {hvad} skal have. Sæt den ind på siden først.', {
|
|
215
|
+
side: String(fm(page).title),
|
|
216
|
+
blok: siteText(p.needs.label),
|
|
217
|
+
hvad: siteText(p.the).toLowerCase(),
|
|
218
|
+
}),
|
|
219
|
+
error: true,
|
|
220
|
+
edit: { to: page.path, text: siteText('Rediger siden') },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** What is wrong on the site by its own rules, a line for each, before it is built (checkSite.ts): a standard page that
|
|
227
|
+
* is not a page on the site, or the same page twice, and a button that opens a promotion that is gone, turned off or not
|
|
228
|
+
* shown on its page. */
|
|
229
|
+
export function siteProblems(files: { path: string; text: string }[]): string[] {
|
|
230
|
+
const problems: string[] = [];
|
|
231
|
+
// The standard pages are pages on the site ("Indstillinger" › "Udseende" › "Standardsider"), different ones (but for
|
|
232
|
+
// the cookie policy, which may be the privacy policy).
|
|
233
|
+
if (files.some((f) => f.path === config.standardPages)) {
|
|
234
|
+
for (const p of standardPages) {
|
|
235
|
+
const page = standardFile(files as RepoFile[], p.key);
|
|
236
|
+
if (!page || ['draft', 'scheduled'].includes(String(fm(page).status ?? '')))
|
|
237
|
+
problems.push(`${config.standardPages}: ${siteText('{side} skal være en side, der er udgivet.', { side: siteText(p.the) })}`);
|
|
238
|
+
}
|
|
239
|
+
if (!differentPages(standardOf(files)))
|
|
240
|
+
problems.push(`${config.standardPages}: ${siteText('Standardsiderne skal være forskellige sider.')}`);
|
|
241
|
+
}
|
|
242
|
+
const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
|
|
243
|
+
const slugs = new Set<string>();
|
|
244
|
+
for (const f of files) for (const m of f.text.matchAll(/(?:"|")open(?:"|"):\s*(?:"|")([^"&]+)/g)) slugs.add(m[1]);
|
|
245
|
+
for (const slug of slugs) {
|
|
246
|
+
const file = promotions.find((f) => slugOf(f.path, 'src/data/popups') === slug);
|
|
247
|
+
for (const at of openersOf(slug, files)) {
|
|
248
|
+
if (file && shownFor(json(file as RepoFile), at)) continue;
|
|
249
|
+
problems.push(
|
|
250
|
+
`${at.file.startsWith('blokke/') ? config.blockTexts : at.file}: ${
|
|
251
|
+
file ? openerText(at) : siteText('En knap åbner promoveringen "{navn}", som ikke findes.', { navn: slug })
|
|
252
|
+
}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return problems;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The tags a promotion is shown on the posts of (Promovering, "Vises på": "Blogindlæg med tags").
|
|
260
|
+
const promotionTags = (d: Record<string, unknown>): string[] => (d.where === 'tags' && Array.isArray(d.tags) ? d.tags.map(String) : []);
|
|
261
|
+
|
|
262
|
+
// Why an entry cannot be deleted (something on the site uses it), with a link to where, or null.
|
|
263
|
+
export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]): Status {
|
|
264
|
+
if (collection.kind === 'files') return null;
|
|
265
|
+
const slug = slugOf(file.path, collection.folder);
|
|
266
|
+
// A promotion a button opens ("Knapper": "Åbn promovering").
|
|
267
|
+
if (collection.id === 'popups') {
|
|
268
|
+
const [at] = openersOf(slug, files);
|
|
269
|
+
if (at)
|
|
270
|
+
return {
|
|
271
|
+
text: at.bar
|
|
272
|
+
? siteText('En knap i handlingslinjen åbner promoveringen. Ret knappen først.')
|
|
273
|
+
: at.path
|
|
274
|
+
? siteText('En knap på siden "{side}" åbner promoveringen. Ret knappen først.', { side: at.title })
|
|
275
|
+
: siteText('Standardknapperne åbner promoveringen. Ret dem først.'),
|
|
276
|
+
error: true,
|
|
277
|
+
edit: openerEdit(at),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const home = standardFile(files, 'front');
|
|
281
|
+
const front = Object.fromEntries(
|
|
282
|
+
(home ? (blocksIn(home.text) as { type: string; props: Record<string, unknown> }[]) : []).map((b) => [b.type, b.props]),
|
|
283
|
+
);
|
|
284
|
+
if (collection.id === 'kategorier') {
|
|
285
|
+
const posts = files.filter(
|
|
286
|
+
(f) => f.path.startsWith(`${config.folders.blog}/`) && ((fm(f).categories as string[]) ?? []).includes(slug),
|
|
287
|
+
);
|
|
288
|
+
if (posts.length)
|
|
289
|
+
return {
|
|
290
|
+
text:
|
|
291
|
+
posts.length === 1
|
|
292
|
+
? siteText('Tagget bruges på et blogindlæg. Fjern det fra det først.')
|
|
293
|
+
: siteText('Tagget bruges på {antal} blogindlæg. Fjern det fra dem først.', { antal: posts.length }),
|
|
294
|
+
error: true,
|
|
295
|
+
};
|
|
296
|
+
const quotes = files.filter(
|
|
297
|
+
(f) => f.path.startsWith('src/data/testimonials/') && ((json(f).categories as string[] | undefined) ?? []).includes(slug),
|
|
298
|
+
);
|
|
299
|
+
if (quotes.length)
|
|
300
|
+
return {
|
|
301
|
+
text:
|
|
302
|
+
quotes.length === 1
|
|
303
|
+
? siteText('Tagget bruges på en udtalelse. Fjern det fra den først.')
|
|
304
|
+
: siteText('Tagget bruges på {antal} udtalelser. Fjern det fra dem først.', { antal: quotes.length }),
|
|
305
|
+
error: true,
|
|
306
|
+
};
|
|
307
|
+
// A promotion shown on the posts with the tag (Promovering, "Blogindlæg med tags").
|
|
308
|
+
const promotion = files.find(
|
|
309
|
+
(f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json') && promotionTags(json(f)).includes(slug),
|
|
310
|
+
);
|
|
311
|
+
if (promotion) {
|
|
312
|
+
const d = json(promotion);
|
|
313
|
+
return {
|
|
314
|
+
text: siteText('Tagget bruges i promoveringen "{navn}". Fjern det fra den først.', { navn: String(d.heading ?? d.name ?? '') }),
|
|
315
|
+
error: true,
|
|
316
|
+
edit: { to: promotion.path, text: siteText('Rediger promoveringen') },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const fp = front.posts as Record<string, unknown> | undefined;
|
|
320
|
+
if ((fp?.categories as string[] | undefined)?.includes(slug) || fp?.link === slug)
|
|
321
|
+
return {
|
|
322
|
+
text: siteText('Tagget vælger indlæggene på forsiden under "Fra bloggen". Vælg et andet først.'),
|
|
323
|
+
error: true,
|
|
324
|
+
edit: frontPage(files),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (collection.id === 'blog' && ((front.posts as Record<string, unknown>)?.picked as string[])?.includes(slug))
|
|
328
|
+
return {
|
|
329
|
+
text: siteText('Indlægget er valgt til forsiden under "Fra bloggen". Vælg et andet først.'),
|
|
330
|
+
error: true,
|
|
331
|
+
edit: frontPage(files),
|
|
332
|
+
};
|
|
333
|
+
// A "Blogindlæg" block in a page that picks the post (its props are in the text).
|
|
334
|
+
if (collection.id === 'blog') {
|
|
335
|
+
const page = files.find(
|
|
336
|
+
(f) =>
|
|
337
|
+
f.path.startsWith(`${config.folders.pages}/`) && f.text.includes('data-block="posts"') && f.text.includes(`"${slug}"`),
|
|
338
|
+
);
|
|
339
|
+
if (page)
|
|
340
|
+
return {
|
|
341
|
+
text: siteText('Indlægget er valgt i en blok med blogindlæg på siden "{side}". Vælg et andet først.', {
|
|
342
|
+
side: String(fm(page).title),
|
|
343
|
+
}),
|
|
344
|
+
error: true,
|
|
345
|
+
edit: { to: page.path, text: siteText('Rediger siden') },
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Any other place under "Bruges på" (usesOf): a "Blogindlæg" or "Udtalelser" block that picks the tag, on a
|
|
349
|
+
// page or in a post.
|
|
350
|
+
if (collection.id === 'kategorier') {
|
|
351
|
+
const use = usesOf(collection, files)(file)[0];
|
|
352
|
+
if (use) {
|
|
353
|
+
const post = use.path.startsWith(`${config.folders.blog}/`);
|
|
354
|
+
return {
|
|
355
|
+
text: siteText(
|
|
356
|
+
post
|
|
357
|
+
? 'Tagget er valgt i en blok i blogindlægget "{side}". Vælg et andet først.'
|
|
358
|
+
: 'Tagget er valgt i en blok på siden "{side}". Vælg et andet først.',
|
|
359
|
+
{ side: use.name },
|
|
360
|
+
),
|
|
361
|
+
error: true,
|
|
362
|
+
edit: { to: use.path, text: siteText(post ? 'Rediger indlægget' : 'Rediger siden') },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// What a file that uses something is called: a page's or post's title, a setting's name, an entry's
|
|
370
|
+
// as its list shows it.
|
|
371
|
+
function nameOf(file: RepoFile): string {
|
|
372
|
+
for (const c of collections)
|
|
373
|
+
if (c.kind === 'files') {
|
|
374
|
+
const own = c.files.find((f) => f.file === file.path);
|
|
375
|
+
if (own) return fixedName({ ...own, label: siteText(own.label) }, file.text);
|
|
376
|
+
} else if (c.kind === 'folder' && file.path.startsWith(`${c.folder}/`) && file.path.endsWith('.json'))
|
|
377
|
+
return c.summary(json(file)) || siteText(c.label);
|
|
378
|
+
return String(fm(file).title ?? file.path);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export type Use = { path: string; name: string };
|
|
382
|
+
|
|
383
|
+
// The places on the site that use an entry, by the entry's file, each once (the tag's "Bruges på", DataForm.tsx,
|
|
384
|
+
// and the number at it in its list, Library.tsx); [] for none. For a tag: the blog posts, testimonials and
|
|
385
|
+
// promotions that have it, and the pages and posts with a "Blogindlæg" or "Udtalelser" block that
|
|
386
|
+
// picks it (its tags, or the tag "Alle indlæg" goes to). Made once for the whole list.
|
|
387
|
+
export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => Use[] {
|
|
388
|
+
if (collection.kind === 'files' || collection.id !== 'kategorier') return () => [];
|
|
389
|
+
const uses = tagUses(files);
|
|
390
|
+
return (file) =>
|
|
391
|
+
[...(uses.get(slugOf(file.path, collection.folder))?.values() ?? [])]
|
|
392
|
+
.map((f) => ({ path: f.path, name: nameOf(f) }))
|
|
393
|
+
.sort((a, b) => byText(a.name, b.name));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The files that use each tag (usesOf), by the tag, each file once.
|
|
397
|
+
function tagUses(files: RepoFile[]): Map<string, Map<string, RepoFile>> {
|
|
398
|
+
const uses = new Map<string, Map<string, RepoFile>>();
|
|
399
|
+
const add = (f: RepoFile, slugs: unknown) => {
|
|
400
|
+
for (const slug of Array.isArray(slugs) ? slugs.map(String) : []) {
|
|
401
|
+
if (!uses.has(slug)) uses.set(slug, new Map());
|
|
402
|
+
uses.get(slug)!.set(f.path, f);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const block = (f: RepoFile, type: unknown, props: Record<string, unknown>) => {
|
|
406
|
+
if (type === 'posts' || type === 'testimonials')
|
|
407
|
+
add(f, [...(Array.isArray(props.categories) ? props.categories : []), ...(props.link ? [props.link] : [])]);
|
|
408
|
+
};
|
|
409
|
+
const inFolder = (f: RepoFile, folder: string, ext: string) => f.path.startsWith(`${folder}/`) && f.path.endsWith(ext);
|
|
410
|
+
for (const f of files) {
|
|
411
|
+
if (inFolder(f, config.folders.blog, '.md') || inFolder(f, config.folders.pages, '.md')) {
|
|
412
|
+
if (inFolder(f, config.folders.blog, '.md')) add(f, fm(f).categories);
|
|
413
|
+
for (const b of blocksIn(f.text) as { type: string; props: Record<string, unknown> }[]) block(f, b.type, b.props);
|
|
414
|
+
} else if (inFolder(f, 'src/data/testimonials', '.json')) add(f, json(f).categories);
|
|
415
|
+
else if (inFolder(f, 'src/data/popups', '.json')) add(f, promotionTags(json(f)));
|
|
416
|
+
}
|
|
417
|
+
return uses;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The number at the right of an entry in the list of a part, by the entry's file, or null for none
|
|
421
|
+
// (Library.tsx): at each tag, the number of places that use it (usesOf).
|
|
422
|
+
export function countsOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => number | null {
|
|
423
|
+
if (collection.kind === 'files' || collection.id !== 'kategorier') return () => null;
|
|
424
|
+
const uses = tagUses(files);
|
|
425
|
+
return (file) => uses.get(slugOf(file.path, collection.folder))?.size ?? 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The testimonials, in their order on the site, for the cards and previews of the blocks.
|
|
429
|
+
export const testimonialsOf = (files: RepoFile[]) =>
|
|
430
|
+
files
|
|
431
|
+
.filter((f) => f.path.startsWith('src/data/testimonials/') && f.path.endsWith('.json'))
|
|
432
|
+
.map((f) => ({ ...(json(f) as TestimonialRow & { order?: number }), slug: slugOf(f.path, 'src/data/testimonials') }))
|
|
433
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
434
|
+
|
|
435
|
+
/** What is at an address, as said in a message: "forsiden", 'siden "Om mig" (/om-mig/)',
|
|
436
|
+
* or the address (where a redirect sends to, App.tsx). */
|
|
437
|
+
export function whatIsAt(address: string, relations: Record<Relation, Option[]>): string {
|
|
438
|
+
const base = address.split('#')[0];
|
|
439
|
+
if (base === '/') return siteText('forsiden');
|
|
440
|
+
if (base === '/blog/') return siteText('bloggen');
|
|
441
|
+
const page = [...relations.fixed, ...relations.pages].find((o) => o.value === base);
|
|
442
|
+
if (page) return siteText('siden "{navn}" ({adresse})', { navn: page.label, adresse: address });
|
|
443
|
+
const post = relations.blog.find((o) => `/${o.value}/` === base);
|
|
444
|
+
if (post) return siteText('blogindlægget "{navn}" ({adresse})', { navn: post.label, adresse: address });
|
|
445
|
+
const tag = relations.categories.find((o) => `/blog/kategori/${o.value}/` === base);
|
|
446
|
+
if (tag) return siteText('tagget "{navn}" på bloggen ({adresse})', { navn: tag.label, adresse: address });
|
|
447
|
+
if (base.startsWith(config.folders.media.replace(/^public/, '') + '/'))
|
|
448
|
+
return siteText('billedet {navn}', { navn: base.split('/').pop()! });
|
|
449
|
+
return address;
|
|
450
|
+
}
|