@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/lib/crop.mjs
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
// A picture in the text cut to a format (its sides, as 2:3) in the editor (Crop.tsx), written after
|
|
2
|
-
// its address in the Markdown, so the file and the picture stay as they are:
|
|
3
|
-
// 
|
|
4
|
-
// crop: the part of the picture that is shown, in percent of its width and height: from the left,
|
|
5
|
-
// from the top, its width and its height. Shown so on the site (rehypeResponsiveImages, images.mjs)
|
|
6
|
-
// and on its card in the editor (MarkdownEditor.tsx). Without crop the middle of it is shown.
|
|
7
|
-
// A decorative picture ("Dekorativt billede": screen readers skip it) is marked there too, alone or after
|
|
8
|
-
// the format and crop, and drawn with alt="" on the site:
|
|
9
|
-
// 
|
|
10
|
-
// The mark is part of the address, so Milkdown keeps it as it is, and a visitor never sees it.
|
|
11
|
-
|
|
12
|
-
/** The formats a picture can be cut to, as width:height. */
|
|
13
|
-
export const FORMATS = ['1:1', '4:5', '3:4', '2:3', '3:2', '4:3', '16:9'];
|
|
14
|
-
|
|
15
|
-
const FORMAT = /^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/;
|
|
16
|
-
|
|
17
|
-
/** Width divided by height of a format ('2:3'), or 0 when it is none. */
|
|
18
|
-
export function ratioOf(format) {
|
|
19
|
-
const m = String(format ?? '').match(FORMAT);
|
|
20
|
-
return m && +m[1] > 0 && +m[2] > 0 ? +m[1] / +m[2] : 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DECORATIVE = 'decorative';
|
|
24
|
-
|
|
25
|
-
/** Whether a picture's address is marked decorative, and the address without the mark (else as it is). */
|
|
26
|
-
export function decorativeOf(address) {
|
|
27
|
-
const text = String(address);
|
|
28
|
-
const at = text.indexOf('#');
|
|
29
|
-
const params = at < 0 ? [] : text.slice(at + 1).split('&');
|
|
30
|
-
const rest = params.filter((p) => p !== DECORATIVE);
|
|
31
|
-
if (rest.length === params.length) return { address: text, decorative: false };
|
|
32
|
-
return { address: text.slice(0, at) + (rest.length ? `#${rest.join('&')}` : ''), decorative: true };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The address marked decorative, or without the mark. */
|
|
36
|
-
export function decorated(address, decorative) {
|
|
37
|
-
const plain = decorativeOf(address).address;
|
|
38
|
-
return decorative ? `${plain}${plain.includes('#') ? '&' : '#'}${DECORATIVE}` : plain;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** The picture's own address, its format and crop ([left, top, width, height]) when it has them (an
|
|
42
|
-
* address without a format is left as it is), and whether it is decorative. */
|
|
43
|
-
export function splitPicture(address) {
|
|
44
|
-
const { address: own, decorative } = decorativeOf(address);
|
|
45
|
-
const [src, fragment = ''] = own.split('#');
|
|
46
|
-
const params = new URLSearchParams(fragment);
|
|
47
|
-
const format = params.get('format');
|
|
48
|
-
if (!ratioOf(format)) return { src: own, format: '', crop: null, decorative };
|
|
49
|
-
const crop = (params.get('crop') ?? '').split(',').map(Number);
|
|
50
|
-
const fits = crop.length === 4 && crop.every((n) => Number.isFinite(n) && n >= 0 && n <= 100) && crop[2] > 0 && crop[3] > 0;
|
|
51
|
-
return { src, format, crop: fits ? crop : null, decorative };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const round = (n) => Math.round(n * 10) / 10;
|
|
55
|
-
|
|
56
|
-
/** The address with the format and crop after it (only the address without a format), marked decorative when it is. */
|
|
57
|
-
export function joinPicture(src, format, crop, decorative = false) {
|
|
58
|
-
if (!ratioOf(format)) return decorated(src, decorative);
|
|
59
|
-
return decorated(`${src}#format=${format}${crop ? `&crop=${crop.map(round).join(',')}` : ''}`, decorative);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** The largest part of a picture (aspect: its width divided by its height) in a format, in the middle. */
|
|
63
|
-
export function middleCrop(aspect, format) {
|
|
64
|
-
const ratio = ratioOf(format);
|
|
65
|
-
if (!ratio || !aspect) return null;
|
|
66
|
-
const [w, h] = aspect > ratio ? [(ratio / aspect) * 100, 100] : [100, (aspect / ratio) * 100];
|
|
67
|
-
return [(100 - w) / 2, (100 - h) / 2, w, h];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The styles of a cut picture: its box (in the format) and the picture in it, as CSS text; how much
|
|
71
|
-
* wider than the box the picture is (for its sizes). Without a crop the picture is its own box. */
|
|
72
|
-
export function cropStyle(format, crop) {
|
|
73
|
-
const [a, b] = format.split(':');
|
|
74
|
-
const aspect = `aspect-ratio: ${a} / ${b}`;
|
|
75
|
-
if (!crop) return { box: null, img: `${aspect}; object-fit: cover`, scale: 1 };
|
|
76
|
-
const [x, y, w] = crop;
|
|
77
|
-
return {
|
|
78
|
-
box: aspect,
|
|
79
|
-
// The picture's width in percent of the box's, moved by percent of its own size.
|
|
80
|
-
img: `width: ${Math.round((100 / w) * 100 * 100) / 100}%; transform: translate(${-round(x)}%, ${-round(y)}%)`,
|
|
81
|
-
scale: 100 / w,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
1
|
+
// A picture in the text cut to a format (its sides, as 2:3) in the editor (Crop.tsx), written after
|
|
2
|
+
// its address in the Markdown, so the file and the picture stay as they are:
|
|
3
|
+
// 
|
|
4
|
+
// crop: the part of the picture that is shown, in percent of its width and height: from the left,
|
|
5
|
+
// from the top, its width and its height. Shown so on the site (rehypeResponsiveImages, images.mjs)
|
|
6
|
+
// and on its card in the editor (MarkdownEditor.tsx). Without crop the middle of it is shown.
|
|
7
|
+
// A decorative picture ("Dekorativt billede": screen readers skip it) is marked there too, alone or after
|
|
8
|
+
// the format and crop, and drawn with alt="" on the site:
|
|
9
|
+
// 
|
|
10
|
+
// The mark is part of the address, so Milkdown keeps it as it is, and a visitor never sees it.
|
|
11
|
+
|
|
12
|
+
/** The formats a picture can be cut to, as width:height. */
|
|
13
|
+
export const FORMATS = ['1:1', '4:5', '3:4', '2:3', '3:2', '4:3', '16:9'];
|
|
14
|
+
|
|
15
|
+
const FORMAT = /^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/;
|
|
16
|
+
|
|
17
|
+
/** Width divided by height of a format ('2:3'), or 0 when it is none. */
|
|
18
|
+
export function ratioOf(format) {
|
|
19
|
+
const m = String(format ?? '').match(FORMAT);
|
|
20
|
+
return m && +m[1] > 0 && +m[2] > 0 ? +m[1] / +m[2] : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DECORATIVE = 'decorative';
|
|
24
|
+
|
|
25
|
+
/** Whether a picture's address is marked decorative, and the address without the mark (else as it is). */
|
|
26
|
+
export function decorativeOf(address) {
|
|
27
|
+
const text = String(address);
|
|
28
|
+
const at = text.indexOf('#');
|
|
29
|
+
const params = at < 0 ? [] : text.slice(at + 1).split('&');
|
|
30
|
+
const rest = params.filter((p) => p !== DECORATIVE);
|
|
31
|
+
if (rest.length === params.length) return { address: text, decorative: false };
|
|
32
|
+
return { address: text.slice(0, at) + (rest.length ? `#${rest.join('&')}` : ''), decorative: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The address marked decorative, or without the mark. */
|
|
36
|
+
export function decorated(address, decorative) {
|
|
37
|
+
const plain = decorativeOf(address).address;
|
|
38
|
+
return decorative ? `${plain}${plain.includes('#') ? '&' : '#'}${DECORATIVE}` : plain;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The picture's own address, its format and crop ([left, top, width, height]) when it has them (an
|
|
42
|
+
* address without a format is left as it is), and whether it is decorative. */
|
|
43
|
+
export function splitPicture(address) {
|
|
44
|
+
const { address: own, decorative } = decorativeOf(address);
|
|
45
|
+
const [src, fragment = ''] = own.split('#');
|
|
46
|
+
const params = new URLSearchParams(fragment);
|
|
47
|
+
const format = params.get('format');
|
|
48
|
+
if (!ratioOf(format)) return { src: own, format: '', crop: null, decorative };
|
|
49
|
+
const crop = (params.get('crop') ?? '').split(',').map(Number);
|
|
50
|
+
const fits = crop.length === 4 && crop.every((n) => Number.isFinite(n) && n >= 0 && n <= 100) && crop[2] > 0 && crop[3] > 0;
|
|
51
|
+
return { src, format, crop: fits ? crop : null, decorative };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
55
|
+
|
|
56
|
+
/** The address with the format and crop after it (only the address without a format), marked decorative when it is. */
|
|
57
|
+
export function joinPicture(src, format, crop, decorative = false) {
|
|
58
|
+
if (!ratioOf(format)) return decorated(src, decorative);
|
|
59
|
+
return decorated(`${src}#format=${format}${crop ? `&crop=${crop.map(round).join(',')}` : ''}`, decorative);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The largest part of a picture (aspect: its width divided by its height) in a format, in the middle. */
|
|
63
|
+
export function middleCrop(aspect, format) {
|
|
64
|
+
const ratio = ratioOf(format);
|
|
65
|
+
if (!ratio || !aspect) return null;
|
|
66
|
+
const [w, h] = aspect > ratio ? [(ratio / aspect) * 100, 100] : [100, (aspect / ratio) * 100];
|
|
67
|
+
return [(100 - w) / 2, (100 - h) / 2, w, h];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The styles of a cut picture: its box (in the format) and the picture in it, as CSS text; how much
|
|
71
|
+
* wider than the box the picture is (for its sizes). Without a crop the picture is its own box. */
|
|
72
|
+
export function cropStyle(format, crop) {
|
|
73
|
+
const [a, b] = format.split(':');
|
|
74
|
+
const aspect = `aspect-ratio: ${a} / ${b}`;
|
|
75
|
+
if (!crop) return { box: null, img: `${aspect}; object-fit: cover`, scale: 1 };
|
|
76
|
+
const [x, y, w] = crop;
|
|
77
|
+
return {
|
|
78
|
+
box: aspect,
|
|
79
|
+
// The picture's width in percent of the box's, moved by percent of its own size.
|
|
80
|
+
img: `width: ${Math.round((100 / w) * 100 * 100) / 100}%; transform: translate(${-round(x)}%, ${-round(y)}%)`,
|
|
81
|
+
scale: 100 / w,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/lib/csp.mjs
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
// The Content Security Policy of every page the site builds, as a <meta http-equiv> at the top of its <head>: the
|
|
2
|
-
// browser runs only the scripts the build put in the page (each inline one by its hash) and those from the site's own
|
|
3
|
-
// address, and loads from elsewhere only what is allowed below. So HTML that gets into a page after the build (in the
|
|
4
|
-
// editor's preview, by a fault in a script) can run nothing. What the build wrote is trusted: the addresses of the
|
|
5
|
-
// scripts, frames and pictures in a page (HTML written in a page's text, an embed) are allowed on that page.
|
|
6
|
-
// Allowed everywhere besides: the videos (video.mjs plays them from YouTube's and Vimeo's players), what the site gives
|
|
7
|
-
// (`site`: its measuring, packages/editor/site/siteCsp.mjs) and what each app installed gives (`src/apps/<app>/csp.json`, in
|
|
8
|
-
// packages/editor/site/siteCsp.mjs too), with the addresses allowed in the editor (the site's own, config.csp). The editor's page (/editor/) has its own, stricter policy: GitHub, the stills of the videos,
|
|
9
|
-
// and nothing of the site's or the apps'. Styles may be inline (the pages have style="…" and React sets them).
|
|
10
|
-
// A meta tag cannot say who may frame a page; X-Frame-Options in packages/editor/pages/_headers.txt does.
|
|
11
|
-
import { createHash } from 'node:crypto';
|
|
12
|
-
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { joined } from './cspSources.mjs';
|
|
16
|
-
|
|
17
|
-
// What a site or an app may add to the policy, and the checks of it, without Node, so the site's Worker can use them too.
|
|
18
|
-
export { allows, isSource, joined, KINDS, kindOf, sourcesProblems } from './cspSources.mjs';
|
|
19
|
-
|
|
20
|
-
/** @typedef {import('./cspSources.mjs').Sources} Sources */
|
|
21
|
-
|
|
22
|
-
/** What the videos need on the site: their players, put in place when a video is played (video.mjs). */
|
|
23
|
-
const VIDEOS = { frame: ['https://www.youtube-nocookie.com', 'https://player.vimeo.com'] };
|
|
24
|
-
|
|
25
|
-
/** What the editor needs (GitHub it asks through the site's Worker, on its own address: github.ts): Vimeo's details of a video and the stills of the videos
|
|
26
|
-
* (standard/video/view.ts), and what it makes in the browser (pictures, files) as blob: and data:. */
|
|
27
|
-
const EDITOR = {
|
|
28
|
-
img: ['blob:', 'https://i.ytimg.com', 'https://i.vimeocdn.com'],
|
|
29
|
-
connect: ['blob:', 'data:', 'https://vimeo.com'],
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// An inline script that runs (not JSON, JSON-LD or a template).
|
|
33
|
-
const RUNS = /^(module|text\/javascript|application\/javascript)?$/i;
|
|
34
|
-
const SCRIPT = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
35
|
-
const attr = (attrs, name) => new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`, 'i').exec(attrs)?.[1];
|
|
36
|
-
|
|
37
|
-
/** The hash of each inline script that runs in a page, as CSP writes it. @param {string} html */
|
|
38
|
-
export function scriptHashes(html) {
|
|
39
|
-
const hashes = new Set();
|
|
40
|
-
for (const [, attrs, code] of html.matchAll(SCRIPT))
|
|
41
|
-
if (attr(attrs, 'src') === undefined && RUNS.test(attr(attrs, 'type') ?? ''))
|
|
42
|
-
hashes.add(`'sha256-${createHash('sha256').update(code, 'utf8').digest('base64')}'`);
|
|
43
|
-
return [...hashes];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The other addresses (origins) a page's HTML loads its scripts, frames and pictures from.
|
|
47
|
-
* @param {string} html @returns {Required<Pick<Sources, 'script' | 'frame' | 'img'>>} */
|
|
48
|
-
export function pageOrigins(html) {
|
|
49
|
-
const found = { script: new Set(), frame: new Set(), img: new Set() };
|
|
50
|
-
const add = (set, url) => {
|
|
51
|
-
try {
|
|
52
|
-
const { protocol, origin } = new URL(url.replace(/&/g, '&'));
|
|
53
|
-
if (protocol === 'https:' || protocol === 'http:') set.add(origin);
|
|
54
|
-
} catch {}
|
|
55
|
-
};
|
|
56
|
-
for (const [, attrs] of html.matchAll(/<script\b([^>]*)>/gi)) add(found.script, attr(attrs, 'src') ?? '');
|
|
57
|
-
for (const [, attrs] of html.matchAll(/<iframe\b([^>]*)>/gi)) add(found.frame, attr(attrs, 'src') ?? attr(attrs, 'data-src') ?? '');
|
|
58
|
-
for (const [, attrs] of html.matchAll(/<(?:img|source)\b([^>]*)>/gi)) {
|
|
59
|
-
add(found.img, attr(attrs, 'src') ?? '');
|
|
60
|
-
for (const part of (attr(attrs, 'srcset') ?? '').split(',')) add(found.img, part.trim().split(/\s+/)[0]);
|
|
61
|
-
}
|
|
62
|
-
return { script: [...found.script], frame: [...found.frame], img: [...found.img] };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** The policy, from its directives (an empty one is left out, `none` when it must say so).
|
|
66
|
-
* @param {Record<string, (string | undefined)[]>} directives */
|
|
67
|
-
function policy(directives) {
|
|
68
|
-
return Object.entries(directives)
|
|
69
|
-
.map(([name, values]) => [name, [...new Set(values.filter(Boolean))]])
|
|
70
|
-
.map(([name, values]) => `${name} ${values.length ? values.join(' ') : "'none'"}`)
|
|
71
|
-
.join('; ');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** A page's policy: on the site, or the editor's (`editor`). @param {string} html
|
|
75
|
-
* @param {{ editor?: boolean, site?: Sources, images?: string[] }} options */
|
|
76
|
-
export function pagePolicy(html, { editor = false, site = {}, images = [] }) {
|
|
77
|
-
const page = pageOrigins(html);
|
|
78
|
-
const s = editor ? joined(EDITOR, { img: images }) : joined(VIDEOS, site, page);
|
|
79
|
-
return policy({
|
|
80
|
-
'default-src': ["'self'"],
|
|
81
|
-
'script-src': ["'self'", ...scriptHashes(html), ...s.script],
|
|
82
|
-
'style-src': ["'self'", "'unsafe-inline'", ...s.style],
|
|
83
|
-
'img-src': ["'self'", 'data:', ...s.img],
|
|
84
|
-
'font-src': ["'self'", 'data:'],
|
|
85
|
-
'connect-src': ["'self'", ...s.connect],
|
|
86
|
-
'media-src': ["'self'", ...(editor ? ['blob:', 'data:'] : [])],
|
|
87
|
-
// The editor shows a page in a frame of its own (srcdoc, PageFrame in ui.tsx).
|
|
88
|
-
'frame-src': editor ? ["'self'", 'blob:', 'data:'] : s.frame,
|
|
89
|
-
'worker-src': ["'self'"],
|
|
90
|
-
'form-action': ["'self'", ...s.form],
|
|
91
|
-
'object-src': [],
|
|
92
|
-
'base-uri': ["'self'"],
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The policy as the first thing in a page's <head> (after its charset, which must stay in its first bytes).
|
|
97
|
-
* @param {string} html @param {string} value */
|
|
98
|
-
export function withPolicy(html, value) {
|
|
99
|
-
const meta = `<meta http-equiv="Content-Security-Policy" content="${value.replace(/&/g, '&').replace(/"/g, '"')}">`;
|
|
100
|
-
const at = /<meta\s+charset\s*=\s*"?[\w-]+"?\s*\/?>/i.exec(html) ?? /<head\b[^>]*>/i.exec(html);
|
|
101
|
-
if (!at) return undefined;
|
|
102
|
-
const end = at.index + at[0].length;
|
|
103
|
-
return html.slice(0, end) + meta + html.slice(end);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** The Astro integration that writes the policy into every page built (packages/editor/astro.mjs).
|
|
107
|
-
* @param {{ site?: Sources }} [options] @returns {import('astro').AstroIntegration} */
|
|
108
|
-
export function csp({ site = {} } = {}) {
|
|
109
|
-
return {
|
|
110
|
-
name: 'content-security-policy',
|
|
111
|
-
hooks: {
|
|
112
|
-
'astro:build:done': ({ dir, logger }) => {
|
|
113
|
-
const root = fileURLToPath(dir);
|
|
114
|
-
const files = readdirSync(root, { recursive: true, encoding: 'utf8' }).filter((f) => f.endsWith('.html'));
|
|
115
|
-
const pages = files.map((file) => ({ file, html: readFileSync(join(root, file), 'utf8') }));
|
|
116
|
-
// The editor shows the site's pages in its preview, with their pictures.
|
|
117
|
-
const images = [...new Set(pages.flatMap((p) => pageOrigins(p.html).img))];
|
|
118
|
-
const missing = [];
|
|
119
|
-
for (const { file, html } of pages) {
|
|
120
|
-
const editor = file.split(/[\\/]/)[0] === 'editor';
|
|
121
|
-
const out = withPolicy(html, pagePolicy(html, { editor, site, images }));
|
|
122
|
-
if (out) writeFileSync(join(root, file), out);
|
|
123
|
-
else missing.push(file);
|
|
124
|
-
}
|
|
125
|
-
if (missing.length) throw new Error(`Content-Security-Policy: ingen <head> i ${missing.join(', ')}`);
|
|
126
|
-
logger.info(`${pages.length} sider har en Content-Security-Policy.`);
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
1
|
+
// The Content Security Policy of every page the site builds, as a <meta http-equiv> at the top of its <head>: the
|
|
2
|
+
// browser runs only the scripts the build put in the page (each inline one by its hash) and those from the site's own
|
|
3
|
+
// address, and loads from elsewhere only what is allowed below. So HTML that gets into a page after the build (in the
|
|
4
|
+
// editor's preview, by a fault in a script) can run nothing. What the build wrote is trusted: the addresses of the
|
|
5
|
+
// scripts, frames and pictures in a page (HTML written in a page's text, an embed) are allowed on that page.
|
|
6
|
+
// Allowed everywhere besides: the videos (video.mjs plays them from YouTube's and Vimeo's players), what the site gives
|
|
7
|
+
// (`site`: its measuring, packages/editor/site/siteCsp.mjs) and what each app installed gives (`src/apps/<app>/csp.json`, in
|
|
8
|
+
// packages/editor/site/siteCsp.mjs too), with the addresses allowed in the editor (the site's own, config.csp). The editor's page (/editor/) has its own, stricter policy: GitHub, the stills of the videos,
|
|
9
|
+
// and nothing of the site's or the apps'. Styles may be inline (the pages have style="…" and React sets them).
|
|
10
|
+
// A meta tag cannot say who may frame a page; X-Frame-Options in packages/editor/pages/_headers.txt does.
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { joined } from './cspSources.mjs';
|
|
16
|
+
|
|
17
|
+
// What a site or an app may add to the policy, and the checks of it, without Node, so the site's Worker can use them too.
|
|
18
|
+
export { allows, isSource, joined, KINDS, kindOf, sourcesProblems } from './cspSources.mjs';
|
|
19
|
+
|
|
20
|
+
/** @typedef {import('./cspSources.mjs').Sources} Sources */
|
|
21
|
+
|
|
22
|
+
/** What the videos need on the site: their players, put in place when a video is played (video.mjs). */
|
|
23
|
+
const VIDEOS = { frame: ['https://www.youtube-nocookie.com', 'https://player.vimeo.com'] };
|
|
24
|
+
|
|
25
|
+
/** What the editor needs (GitHub it asks through the site's Worker, on its own address: github.ts): Vimeo's details of a video and the stills of the videos
|
|
26
|
+
* (standard/video/view.ts), and what it makes in the browser (pictures, files) as blob: and data:. */
|
|
27
|
+
const EDITOR = {
|
|
28
|
+
img: ['blob:', 'https://i.ytimg.com', 'https://i.vimeocdn.com'],
|
|
29
|
+
connect: ['blob:', 'data:', 'https://vimeo.com'],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// An inline script that runs (not JSON, JSON-LD or a template).
|
|
33
|
+
const RUNS = /^(module|text\/javascript|application\/javascript)?$/i;
|
|
34
|
+
const SCRIPT = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
35
|
+
const attr = (attrs, name) => new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`, 'i').exec(attrs)?.[1];
|
|
36
|
+
|
|
37
|
+
/** The hash of each inline script that runs in a page, as CSP writes it. @param {string} html */
|
|
38
|
+
export function scriptHashes(html) {
|
|
39
|
+
const hashes = new Set();
|
|
40
|
+
for (const [, attrs, code] of html.matchAll(SCRIPT))
|
|
41
|
+
if (attr(attrs, 'src') === undefined && RUNS.test(attr(attrs, 'type') ?? ''))
|
|
42
|
+
hashes.add(`'sha256-${createHash('sha256').update(code, 'utf8').digest('base64')}'`);
|
|
43
|
+
return [...hashes];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The other addresses (origins) a page's HTML loads its scripts, frames and pictures from.
|
|
47
|
+
* @param {string} html @returns {Required<Pick<Sources, 'script' | 'frame' | 'img'>>} */
|
|
48
|
+
export function pageOrigins(html) {
|
|
49
|
+
const found = { script: new Set(), frame: new Set(), img: new Set() };
|
|
50
|
+
const add = (set, url) => {
|
|
51
|
+
try {
|
|
52
|
+
const { protocol, origin } = new URL(url.replace(/&/g, '&'));
|
|
53
|
+
if (protocol === 'https:' || protocol === 'http:') set.add(origin);
|
|
54
|
+
} catch {}
|
|
55
|
+
};
|
|
56
|
+
for (const [, attrs] of html.matchAll(/<script\b([^>]*)>/gi)) add(found.script, attr(attrs, 'src') ?? '');
|
|
57
|
+
for (const [, attrs] of html.matchAll(/<iframe\b([^>]*)>/gi)) add(found.frame, attr(attrs, 'src') ?? attr(attrs, 'data-src') ?? '');
|
|
58
|
+
for (const [, attrs] of html.matchAll(/<(?:img|source)\b([^>]*)>/gi)) {
|
|
59
|
+
add(found.img, attr(attrs, 'src') ?? '');
|
|
60
|
+
for (const part of (attr(attrs, 'srcset') ?? '').split(',')) add(found.img, part.trim().split(/\s+/)[0]);
|
|
61
|
+
}
|
|
62
|
+
return { script: [...found.script], frame: [...found.frame], img: [...found.img] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The policy, from its directives (an empty one is left out, `none` when it must say so).
|
|
66
|
+
* @param {Record<string, (string | undefined)[]>} directives */
|
|
67
|
+
function policy(directives) {
|
|
68
|
+
return Object.entries(directives)
|
|
69
|
+
.map(([name, values]) => [name, [...new Set(values.filter(Boolean))]])
|
|
70
|
+
.map(([name, values]) => `${name} ${values.length ? values.join(' ') : "'none'"}`)
|
|
71
|
+
.join('; ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A page's policy: on the site, or the editor's (`editor`). @param {string} html
|
|
75
|
+
* @param {{ editor?: boolean, site?: Sources, images?: string[] }} options */
|
|
76
|
+
export function pagePolicy(html, { editor = false, site = {}, images = [] }) {
|
|
77
|
+
const page = pageOrigins(html);
|
|
78
|
+
const s = editor ? joined(EDITOR, { img: images }) : joined(VIDEOS, site, page);
|
|
79
|
+
return policy({
|
|
80
|
+
'default-src': ["'self'"],
|
|
81
|
+
'script-src': ["'self'", ...scriptHashes(html), ...s.script],
|
|
82
|
+
'style-src': ["'self'", "'unsafe-inline'", ...s.style],
|
|
83
|
+
'img-src': ["'self'", 'data:', ...s.img],
|
|
84
|
+
'font-src': ["'self'", 'data:'],
|
|
85
|
+
'connect-src': ["'self'", ...s.connect],
|
|
86
|
+
'media-src': ["'self'", ...(editor ? ['blob:', 'data:'] : [])],
|
|
87
|
+
// The editor shows a page in a frame of its own (srcdoc, PageFrame in ui.tsx).
|
|
88
|
+
'frame-src': editor ? ["'self'", 'blob:', 'data:'] : s.frame,
|
|
89
|
+
'worker-src': ["'self'"],
|
|
90
|
+
'form-action': ["'self'", ...s.form],
|
|
91
|
+
'object-src': [],
|
|
92
|
+
'base-uri': ["'self'"],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The policy as the first thing in a page's <head> (after its charset, which must stay in its first bytes).
|
|
97
|
+
* @param {string} html @param {string} value */
|
|
98
|
+
export function withPolicy(html, value) {
|
|
99
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${value.replace(/&/g, '&').replace(/"/g, '"')}">`;
|
|
100
|
+
const at = /<meta\s+charset\s*=\s*"?[\w-]+"?\s*\/?>/i.exec(html) ?? /<head\b[^>]*>/i.exec(html);
|
|
101
|
+
if (!at) return undefined;
|
|
102
|
+
const end = at.index + at[0].length;
|
|
103
|
+
return html.slice(0, end) + meta + html.slice(end);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The Astro integration that writes the policy into every page built (packages/editor/astro.mjs).
|
|
107
|
+
* @param {{ site?: Sources }} [options] @returns {import('astro').AstroIntegration} */
|
|
108
|
+
export function csp({ site = {} } = {}) {
|
|
109
|
+
return {
|
|
110
|
+
name: 'content-security-policy',
|
|
111
|
+
hooks: {
|
|
112
|
+
'astro:build:done': ({ dir, logger }) => {
|
|
113
|
+
const root = fileURLToPath(dir);
|
|
114
|
+
const files = readdirSync(root, { recursive: true, encoding: 'utf8' }).filter((f) => f.endsWith('.html'));
|
|
115
|
+
const pages = files.map((file) => ({ file, html: readFileSync(join(root, file), 'utf8') }));
|
|
116
|
+
// The editor shows the site's pages in its preview, with their pictures.
|
|
117
|
+
const images = [...new Set(pages.flatMap((p) => pageOrigins(p.html).img))];
|
|
118
|
+
const missing = [];
|
|
119
|
+
for (const { file, html } of pages) {
|
|
120
|
+
const editor = file.split(/[\\/]/)[0] === 'editor';
|
|
121
|
+
const out = withPolicy(html, pagePolicy(html, { editor, site, images }));
|
|
122
|
+
if (out) writeFileSync(join(root, file), out);
|
|
123
|
+
else missing.push(file);
|
|
124
|
+
}
|
|
125
|
+
if (missing.length) throw new Error(`Content-Security-Policy: ingen <head> i ${missing.join(', ')}`);
|
|
126
|
+
logger.info(`${pages.length} sider har en Content-Security-Policy.`);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
package/lib/cspSources.mjs
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
// What a site or an app may add to the pages' Content Security Policy (csp.mjs), by kind (an app's csp.json, the
|
|
2
|
-
// site's own addresses), and the checks of it. No dependencies, so the site's Worker can use it too (the app
|
|
3
|
-
// "Cookiescanner" allows an address the policy stopped).
|
|
4
|
-
|
|
5
|
-
/** @typedef {{ script?: string[], style?: string[], img?: string[], connect?: string[], frame?: string[], form?: string[] }} Sources */
|
|
6
|
-
|
|
7
|
-
/** The kinds of sources a site or an app can add (a Sources' keys, csp.json's). */
|
|
8
|
-
export const KINDS = ['script', 'style', 'img', 'connect', 'frame', 'form'];
|
|
9
|
-
|
|
10
|
-
// A source as a site or an app gives it: an address (origin) on https, with `*.` for all under a domain. Nothing that
|
|
11
|
-
// lets everything through (`*`, `https:`, 'unsafe-inline', 'unsafe-eval').
|
|
12
|
-
const SOURCE = /^https:\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?$/;
|
|
13
|
-
|
|
14
|
-
/** Whether a source is one a site or an app may give (see SOURCE). @param {unknown} source */
|
|
15
|
-
export const isSource = (source) => typeof source === 'string' && SOURCE.test(source);
|
|
16
|
-
|
|
17
|
-
/** The kind of sources a directive of the policy takes, as a browser names it when it stops something
|
|
18
|
-
* (securitypolicyviolation's violatedDirective: img-src, script-src-elem …); undefined for one a site or an app cannot
|
|
19
|
-
* add to (font-src, media-src …). @param {string} directive */
|
|
20
|
-
export function kindOf(directive) {
|
|
21
|
-
if (directive === 'form-action') return 'form';
|
|
22
|
-
if (directive === 'child-src') return 'frame';
|
|
23
|
-
const kind = /^(script|style|img|connect|frame)-src(-elem|-attr)?$/.exec(directive)?.[1];
|
|
24
|
-
return kind;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** What is wrong with sources written in a file (an app's csp.json), each as a sentence; none when they are right.
|
|
28
|
-
* @param {unknown} sources @param {string} file */
|
|
29
|
-
export function sourcesProblems(sources, file) {
|
|
30
|
-
if (!sources || typeof sources !== 'object' || Array.isArray(sources)) return [`${file}: skal være et objekt med ${KINDS.join(', ')}.`];
|
|
31
|
-
return Object.entries(sources).flatMap(([kind, values]) =>
|
|
32
|
-
!KINDS.includes(kind)
|
|
33
|
-
? [`${file}: "${kind}" er ikke en af ${KINDS.join(', ')}.`]
|
|
34
|
-
: !Array.isArray(values)
|
|
35
|
-
? [`${file}: "${kind}" skal være en liste af adresser.`]
|
|
36
|
-
: values
|
|
37
|
-
.filter((v) => !isSource(v))
|
|
38
|
-
.map(
|
|
39
|
-
(v) =>
|
|
40
|
-
`${file}: ${JSON.stringify(v)} under "${kind}" er ikke en adresse som https://eksempel.dk eller https://*.eksempel.dk.`,
|
|
41
|
-
),
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Whether the sources allow an address (its origin, or one under a `*.` domain), as any kind or as `kind`.
|
|
46
|
-
* @param {Sources} sources @param {string} url @param {string} [kind] */
|
|
47
|
-
export function allows(sources, url, kind) {
|
|
48
|
-
const { origin, host } = new URL(url);
|
|
49
|
-
return (kind ? (sources[kind] ?? []) : Object.values(sources).flat()).some(
|
|
50
|
-
(s) => s === origin || (s.startsWith('https://*.') && host.endsWith(s.slice('https://*'.length))),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The sources together. @param {Sources[]} all @returns {Required<Sources>} */
|
|
55
|
-
export function joined(...all) {
|
|
56
|
-
/** @type {Required<Sources>} */
|
|
57
|
-
const sum = Object.fromEntries(KINDS.map((kind) => [kind, []]));
|
|
58
|
-
for (const sources of all) for (const kind of KINDS) sum[kind].push(...(sources[kind] ?? []));
|
|
59
|
-
return sum;
|
|
60
|
-
}
|
|
1
|
+
// What a site or an app may add to the pages' Content Security Policy (csp.mjs), by kind (an app's csp.json, the
|
|
2
|
+
// site's own addresses), and the checks of it. No dependencies, so the site's Worker can use it too (the app
|
|
3
|
+
// "Cookiescanner" allows an address the policy stopped).
|
|
4
|
+
|
|
5
|
+
/** @typedef {{ script?: string[], style?: string[], img?: string[], connect?: string[], frame?: string[], form?: string[] }} Sources */
|
|
6
|
+
|
|
7
|
+
/** The kinds of sources a site or an app can add (a Sources' keys, csp.json's). */
|
|
8
|
+
export const KINDS = ['script', 'style', 'img', 'connect', 'frame', 'form'];
|
|
9
|
+
|
|
10
|
+
// A source as a site or an app gives it: an address (origin) on https, with `*.` for all under a domain. Nothing that
|
|
11
|
+
// lets everything through (`*`, `https:`, 'unsafe-inline', 'unsafe-eval').
|
|
12
|
+
const SOURCE = /^https:\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?$/;
|
|
13
|
+
|
|
14
|
+
/** Whether a source is one a site or an app may give (see SOURCE). @param {unknown} source */
|
|
15
|
+
export const isSource = (source) => typeof source === 'string' && SOURCE.test(source);
|
|
16
|
+
|
|
17
|
+
/** The kind of sources a directive of the policy takes, as a browser names it when it stops something
|
|
18
|
+
* (securitypolicyviolation's violatedDirective: img-src, script-src-elem …); undefined for one a site or an app cannot
|
|
19
|
+
* add to (font-src, media-src …). @param {string} directive */
|
|
20
|
+
export function kindOf(directive) {
|
|
21
|
+
if (directive === 'form-action') return 'form';
|
|
22
|
+
if (directive === 'child-src') return 'frame';
|
|
23
|
+
const kind = /^(script|style|img|connect|frame)-src(-elem|-attr)?$/.exec(directive)?.[1];
|
|
24
|
+
return kind;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What is wrong with sources written in a file (an app's csp.json), each as a sentence; none when they are right.
|
|
28
|
+
* @param {unknown} sources @param {string} file */
|
|
29
|
+
export function sourcesProblems(sources, file) {
|
|
30
|
+
if (!sources || typeof sources !== 'object' || Array.isArray(sources)) return [`${file}: skal være et objekt med ${KINDS.join(', ')}.`];
|
|
31
|
+
return Object.entries(sources).flatMap(([kind, values]) =>
|
|
32
|
+
!KINDS.includes(kind)
|
|
33
|
+
? [`${file}: "${kind}" er ikke en af ${KINDS.join(', ')}.`]
|
|
34
|
+
: !Array.isArray(values)
|
|
35
|
+
? [`${file}: "${kind}" skal være en liste af adresser.`]
|
|
36
|
+
: values
|
|
37
|
+
.filter((v) => !isSource(v))
|
|
38
|
+
.map(
|
|
39
|
+
(v) =>
|
|
40
|
+
`${file}: ${JSON.stringify(v)} under "${kind}" er ikke en adresse som https://eksempel.dk eller https://*.eksempel.dk.`,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether the sources allow an address (its origin, or one under a `*.` domain), as any kind or as `kind`.
|
|
46
|
+
* @param {Sources} sources @param {string} url @param {string} [kind] */
|
|
47
|
+
export function allows(sources, url, kind) {
|
|
48
|
+
const { origin, host } = new URL(url);
|
|
49
|
+
return (kind ? (sources[kind] ?? []) : Object.values(sources).flat()).some(
|
|
50
|
+
(s) => s === origin || (s.startsWith('https://*.') && host.endsWith(s.slice('https://*'.length))),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The sources together. @param {Sources[]} all @returns {Required<Sources>} */
|
|
55
|
+
export function joined(...all) {
|
|
56
|
+
/** @type {Required<Sources>} */
|
|
57
|
+
const sum = Object.fromEntries(KINDS.map((kind) => [kind, []]));
|
|
58
|
+
for (const sources of all) for (const kind of KINDS) sum[kind].push(...(sources[kind] ?? []));
|
|
59
|
+
return sum;
|
|
60
|
+
}
|