@morphemecms/cms 0.5.0 → 0.7.0
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 +16 -0
- package/astro.mjs +3 -0
- package/bin/morpheme.mjs +3 -1
- package/defaults/blockTexts.ts +2 -2
- package/defaults/collections.ts +10 -0
- package/defaults/editor.ts +57 -53
- package/defaults/privacy.ts +4 -1
- package/defaults/rules.ts +36 -38
- package/defaults/standardPages.ts +0 -3
- package/defaults/texts.ts +11 -2
- package/demo/src/content/pages/about.md +2 -0
- package/icons/lucide/browser-fill.svg +1 -0
- package/icons/lucide/buildings-fill.svg +1 -0
- package/icons/lucide/certificate-fill.svg +1 -0
- package/icons/lucide/chart-line-up-fill.svg +1 -0
- package/icons/lucide/chats-fill.svg +1 -0
- package/icons/lucide/envelope-simple-fill.svg +1 -0
- package/icons/lucide/file-text-fill.svg +1 -0
- package/icons/lucide/folder-simple-fill.svg +1 -0
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear-six.svg +1 -0
- package/icons/lucide/globe-simple.svg +1 -0
- package/icons/lucide/heart-fill.svg +1 -0
- package/icons/lucide/layout.svg +1 -0
- package/icons/lucide/list-checks-fill.svg +1 -0
- package/icons/lucide/magnifying-glass-fill.svg +1 -0
- package/icons/lucide/megaphone-fill.svg +1 -0
- package/icons/lucide/newspaper-fill.svg +1 -0
- package/icons/lucide/paint-brush-broad-fill.svg +1 -0
- package/icons/lucide/puzzle-piece-fill.svg +1 -0
- package/icons/lucide/shield-check-fill.svg +1 -0
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sidebar.svg +1 -0
- package/icons/lucide/square-half-bottom-fill.svg +1 -0
- package/icons/lucide/square-half.svg +1 -0
- package/icons/lucide/squares-four-fill.svg +1 -0
- package/icons/lucide/storefront-fill.svg +1 -0
- package/icons/lucide/tag-fill.svg +1 -0
- package/icons/lucide/toggle-right-fill.svg +1 -0
- package/icons/lucide/trash-fill.svg +1 -0
- package/icons/lucide/user-fill.svg +1 -0
- package/icons/lucide/wrench-fill.svg +1 -0
- package/icons/material-symbols/browser-fill.svg +1 -0
- package/icons/material-symbols/buildings-fill.svg +1 -0
- package/icons/material-symbols/certificate-fill.svg +1 -0
- package/icons/material-symbols/chart-line-up-fill.svg +1 -0
- package/icons/material-symbols/chats-fill.svg +1 -0
- package/icons/material-symbols/envelope-simple-fill.svg +1 -0
- package/icons/material-symbols/file-text-fill.svg +1 -0
- package/icons/material-symbols/folder-simple-fill.svg +1 -0
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear-six.svg +1 -0
- package/icons/material-symbols/globe-simple.svg +1 -0
- package/icons/material-symbols/heart-fill.svg +1 -0
- package/icons/material-symbols/layout.svg +1 -0
- package/icons/material-symbols/list-checks-fill.svg +1 -0
- package/icons/material-symbols/magnifying-glass-fill.svg +1 -0
- package/icons/material-symbols/megaphone-fill.svg +1 -0
- package/icons/material-symbols/newspaper-fill.svg +1 -0
- package/icons/material-symbols/paint-brush-broad-fill.svg +1 -0
- package/icons/material-symbols/puzzle-piece-fill.svg +1 -0
- package/icons/material-symbols/shield-check-fill.svg +1 -0
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sidebar.svg +1 -0
- package/icons/material-symbols/square-half-bottom-fill.svg +1 -0
- package/icons/material-symbols/square-half.svg +1 -0
- package/icons/material-symbols/squares-four-fill.svg +1 -0
- package/icons/material-symbols/storefront-fill.svg +1 -0
- package/icons/material-symbols/tag-fill.svg +1 -0
- package/icons/material-symbols/toggle-right-fill.svg +1 -0
- package/icons/material-symbols/trash-fill.svg +1 -0
- package/icons/material-symbols/user-fill.svg +1 -0
- package/icons/material-symbols/wrench-fill.svg +1 -0
- package/icons/phosphor/browser-fill.svg +1 -0
- package/icons/phosphor/buildings-fill.svg +1 -0
- package/icons/phosphor/certificate-fill.svg +1 -0
- package/icons/phosphor/chart-line-up-fill.svg +1 -0
- package/icons/phosphor/chats-fill.svg +1 -0
- package/icons/phosphor/envelope-simple-fill.svg +1 -0
- package/icons/phosphor/file-text-fill.svg +1 -0
- package/icons/phosphor/folder-simple-fill.svg +1 -0
- package/icons/phosphor/gear-six.svg +1 -0
- package/icons/phosphor/globe-simple.svg +1 -0
- package/icons/phosphor/heart-fill.svg +1 -0
- package/icons/phosphor/layout.svg +1 -0
- package/icons/phosphor/list-checks-fill.svg +1 -0
- package/icons/phosphor/magnifying-glass-fill.svg +1 -0
- package/icons/phosphor/megaphone-fill.svg +1 -0
- package/icons/phosphor/newspaper-fill.svg +1 -0
- package/icons/phosphor/paint-brush-broad-fill.svg +1 -0
- package/icons/phosphor/puzzle-piece-fill.svg +1 -0
- package/icons/phosphor/shield-check-fill.svg +1 -0
- package/icons/phosphor/sidebar.svg +1 -0
- package/icons/phosphor/square-half-bottom-fill.svg +1 -0
- package/icons/phosphor/square-half.svg +1 -0
- package/icons/phosphor/squares-four-fill.svg +1 -0
- package/icons/phosphor/storefront-fill.svg +1 -0
- package/icons/phosphor/tag-fill.svg +1 -0
- package/icons/phosphor/toggle-right-fill.svg +1 -0
- package/icons/phosphor/trash-fill.svg +1 -0
- package/icons/phosphor/user-fill.svg +1 -0
- package/icons/phosphor/wrench-fill.svg +1 -0
- package/lib/breakpoints.d.mts +13 -0
- package/lib/breakpoints.mjs +20 -0
- package/lib/breakpoints.test.mjs +39 -0
- package/lib/build-lane.d.mts +15 -0
- package/lib/build-lane.mjs +23 -0
- package/lib/build-lane.test.mjs +74 -0
- package/lib/build-settings.d.mts +16 -0
- package/lib/build-settings.mjs +39 -0
- package/lib/build-settings.test.mjs +41 -0
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/drawer-swipe.d.mts +10 -0
- package/lib/drawer-swipe.mjs +17 -0
- package/lib/drawer-swipe.test.mjs +33 -0
- package/lib/drawerKeys.d.mts +9 -0
- package/lib/drawerKeys.mjs +26 -0
- package/lib/drawerKeys.test.mjs +21 -0
- package/lib/eyebrow.d.mts +6 -0
- package/lib/eyebrow.mjs +24 -0
- package/lib/eyebrow.test.mjs +20 -0
- package/lib/front-sections.test.mjs +82 -0
- package/lib/heading-levels.d.mts +7 -0
- package/lib/heading-levels.mjs +16 -0
- package/lib/heading-levels.test.mjs +37 -0
- package/lib/inline.ts +3 -2
- package/lib/markdown.ts +2 -0
- package/lib/materialIcon.d.mts +21 -0
- package/lib/materialIcon.mjs +29 -0
- package/lib/materialIcon.test.mjs +45 -0
- package/lib/menu-layout.d.mts +11 -0
- package/lib/menu-layout.mjs +14 -0
- package/lib/menu-layout.test.mjs +23 -0
- package/lib/menu.d.mts +59 -0
- package/lib/menu.mjs +129 -0
- package/lib/menu.test.mjs +291 -0
- package/lib/rehype-emphasize.d.mts +4 -0
- package/lib/rehype-emphasize.mjs +20 -0
- package/lib/rehype-emphasize.test.mjs +39 -0
- package/lib/section-in-view.d.mts +8 -0
- package/lib/section-in-view.mjs +25 -0
- package/lib/section-in-view.test.mjs +35 -0
- package/lib/section-marker.d.mts +5 -0
- package/lib/section-marker.mjs +18 -0
- package/lib/section-marker.test.mjs +22 -0
- package/lib/section-ranges.d.mts +12 -0
- package/lib/section-ranges.mjs +43 -0
- package/lib/section-ranges.test.mjs +61 -0
- package/lib/sheet-stops.d.mts +68 -0
- package/lib/sheet-stops.mjs +198 -0
- package/lib/sheet-stops.test.mjs +155 -0
- package/lib/still-timer.d.mts +5 -0
- package/lib/still-timer.mjs +17 -0
- package/lib/still-timer.test.mjs +42 -0
- package/lib/text-sections.d.mts +10 -0
- package/lib/text-sections.mjs +45 -0
- package/lib/text-sections.test.mjs +78 -0
- package/lib/video.mjs +2 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +7 -0
- package/lib/without-h1.test.mjs +26 -0
- package/lib/word-at.d.mts +6 -0
- package/lib/word-at.mjs +20 -0
- package/lib/word-at.test.mjs +19 -0
- package/package.json +3 -1
- package/pages/editor/version.json.js +22 -2
- package/scripts/already-checked.mjs +11 -0
- package/scripts/already-checked.test.mjs +18 -0
- package/scripts/build-settings-problems.mjs +27 -0
- package/scripts/build-settings-problems.test.mjs +35 -0
- package/scripts/check-content.mjs +42 -4
- package/scripts/check-worker.mjs +96 -4
- package/scripts/ci-build.mjs +85 -0
- package/scripts/ci-build.test.mjs +137 -0
- package/scripts/editor-roundtrip.mjs +62 -3
- package/scripts/editor-screens.mjs +21 -7
- package/scripts/git-env.mjs +6 -0
- package/scripts/step-command.mjs +34 -0
- package/scripts/step-command.test.mjs +50 -0
- package/scripts/text-problems.mjs +83 -0
- package/scripts/text-problems.test.mjs +74 -0
- package/scripts/worker-modules.mjs +11 -1
- package/site/breakpoints.d.mts +6 -0
- package/site/breakpoints.mjs +52 -0
- package/site/breakpoints.test.mjs +36 -0
- package/site/casing.d.mts +3 -0
- package/site/casing.mjs +26 -0
- package/site/content.config.ts +2 -0
- package/site/content.ts +2 -4
- package/site/defineSite.d.mts +1 -0
- package/site/defineSite.mjs +3 -0
- package/site/home.ts +3 -2
- package/site/icons.d.mts +116 -4
- package/site/icons.mjs +62 -6
- package/site/images.mjs +1 -1
- package/site/live-diff.d.mts +22 -0
- package/site/live-diff.mjs +98 -0
- package/site/live-diff.test.mjs +235 -0
- package/site/materialSymbols.d.mts +5 -0
- package/site/materialSymbols.mjs +19 -0
- package/site/sections.ts +1 -1
- package/site/theme.d.mts +1 -1
- package/site/theme.mjs +7 -1
- package/src/ActionParts.tsx +1 -1
- package/src/Advanced.tsx +101 -20
- package/src/App.tsx +47 -121
- package/src/AppList.tsx +1 -1
- package/src/BlockParts.tsx +3 -3
- package/src/Colors.tsx +1 -1
- package/src/CommunicationParts.tsx +1 -1
- package/src/ContainmentParts.tsx +80 -28
- package/src/Deleted.tsx +18 -4
- package/src/EditorFooter.tsx +14 -0
- package/src/EntryForm.tsx +75 -39
- package/src/FormFrame.tsx +3 -8
- package/src/Library.tsx +46 -51
- package/src/Licenses.tsx +27 -11
- package/src/MainDrawerParts.tsx +60 -0
- package/src/MainMenu.tsx +136 -0
- package/src/MarkdownEditor.tsx +175 -32
- package/src/Media.tsx +69 -16
- package/src/MediaTags.tsx +91 -0
- package/src/ModalSheetSample.tsx +47 -0
- package/src/NavigationParts.tsx +67 -21
- package/src/NavigationRestParts.tsx +1 -1
- package/src/Parts.tsx +71 -0
- package/src/PartsShown.tsx +1 -1
- package/src/PickerParts.tsx +2 -2
- package/src/Redirects.tsx +3 -2
- package/src/Screen.tsx +93 -90
- package/src/SelectionParts.tsx +22 -6
- package/src/SiteLogoLink.tsx +14 -0
- package/src/StartFab.tsx +22 -0
- package/src/StartStatus.tsx +1 -1
- package/src/autoTags.d.mts +28 -0
- package/src/autoTags.mjs +68 -0
- package/src/autoTags.test.mjs +109 -0
- package/src/buildSettings.test.mjs +20 -0
- package/src/buildSettings.ts +20 -0
- package/src/checkSite.ts +14 -1
- package/src/deploy.ts +7 -9
- package/src/editor.css +150 -17
- package/src/eyebrow.d.mts +5 -0
- package/src/eyebrow.mjs +53 -0
- package/src/eyebrowState.d.mts +21 -0
- package/src/eyebrowState.mjs +62 -0
- package/src/eyebrowState.test.mjs +78 -0
- package/src/fileRecords.d.mts +115 -0
- package/src/fileRecords.mjs +224 -0
- package/src/fileRecords.test.mjs +224 -0
- package/src/fileTagFilter.d.mts +30 -0
- package/src/fileTagFilter.mjs +44 -0
- package/src/fileTagFilter.test.mjs +57 -0
- package/src/fileTags.d.mts +56 -0
- package/src/fileTags.mjs +100 -0
- package/src/fileTags.test.mjs +127 -0
- package/src/firstHeading.d.mts +6 -0
- package/src/firstHeading.mjs +37 -0
- package/src/i18n/da-DK/advanced.ts +27 -12
- package/src/i18n/da-DK/app.ts +9 -7
- package/src/i18n/da-DK/apps.ts +16 -16
- package/src/i18n/da-DK/blockSettings.ts +9 -9
- package/src/i18n/da-DK/blocks.ts +2 -2
- package/src/i18n/da-DK/check.ts +5 -2
- package/src/i18n/da-DK/cms.ts +14 -12
- package/src/i18n/da-DK/colors.ts +5 -5
- package/src/i18n/da-DK/common.ts +2 -2
- package/src/i18n/da-DK/company.ts +3 -3
- package/src/i18n/da-DK/consents.ts +6 -6
- package/src/i18n/da-DK/forms.ts +6 -6
- package/src/i18n/da-DK/history.ts +6 -6
- package/src/i18n/da-DK/keywords.ts +2 -2
- package/src/i18n/da-DK/library.ts +17 -15
- package/src/i18n/da-DK/licenses.ts +4 -4
- package/src/i18n/da-DK/media.ts +10 -4
- package/src/i18n/da-DK/parts.ts +62 -25
- package/src/i18n/da-DK/preview.ts +5 -6
- package/src/i18n/da-DK/redirects.ts +7 -7
- package/src/i18n/da-DK/seo.ts +1 -1
- package/src/i18n/da-DK/text.ts +2 -0
- package/src/i18n/da-DK/theme.ts +5 -5
- package/src/i18n/en-US/advanced.ts +34 -18
- package/src/i18n/en-US/app.ts +7 -5
- package/src/i18n/en-US/apps.ts +9 -9
- package/src/i18n/en-US/blockSettings.ts +4 -4
- package/src/i18n/en-US/blocks.ts +6 -6
- package/src/i18n/en-US/check.ts +6 -3
- package/src/i18n/en-US/cms.ts +15 -14
- package/src/i18n/en-US/colors.ts +18 -18
- package/src/i18n/en-US/common.ts +3 -3
- package/src/i18n/en-US/consents.ts +4 -4
- package/src/i18n/en-US/forms.ts +5 -5
- package/src/i18n/en-US/history.ts +2 -2
- package/src/i18n/en-US/library.ts +21 -15
- package/src/i18n/en-US/licenses.ts +6 -6
- package/src/i18n/en-US/media.ts +7 -1
- package/src/i18n/en-US/parts.ts +60 -21
- package/src/i18n/en-US/preview.ts +5 -6
- package/src/i18n/en-US/redirects.ts +3 -3
- package/src/i18n/en-US/seo.ts +1 -1
- package/src/i18n/en-US/text.ts +3 -0
- package/src/i18n/en-US/theme.ts +6 -6
- package/src/icon.tsx +90 -27
- package/src/licenseData.test.mjs +28 -0
- package/src/licenseData.ts +12 -1
- package/src/markdown.d.mts +2 -0
- package/src/markdown.mjs +73 -5
- package/src/markdown.test.mjs +74 -0
- package/src/materialIcons.ts +8 -0
- package/src/menuChildren.ts +36 -0
- package/src/menuOpen.ts +34 -0
- package/src/pagePreview.tsx +23 -19
- package/src/r2Caching.ts +14 -0
- package/src/sectionFrame.ts +39 -0
- package/src/sectionState.d.mts +19 -0
- package/src/sectionState.mjs +31 -0
- package/src/sectionState.test.mjs +59 -0
- package/src/sections.d.mts +9 -7
- package/src/sections.mjs +74 -27
- package/src/sections.test.mjs +59 -0
- package/src/sidebar.ts +2 -2
- package/src/site.ts +27 -17
- package/src/stillCursor.d.mts +8 -0
- package/src/stillCursor.mjs +19 -0
- package/src/stillCursor.test.mjs +26 -0
- package/src/tagUses.d.mts +47 -0
- package/src/tagUses.mjs +121 -0
- package/src/tagUses.test.mjs +141 -0
- package/src/themeUi.ts +10 -0
- package/src/topBarSlot.tsx +37 -0
- package/src/ui.tsx +654 -150
- package/src/virtual.d.ts +5 -0
- package/starter/README.md +2 -1
- package/starter/package.json +2 -1
- package/starter/wrangler.jsonc +3 -2
- package/theme/blocks/cards/LinkCards.tsx +2 -1
- package/theme/blocks/hero/View.tsx +2 -1
- package/theme/blocks/video/View.tsx +2 -1
- package/theme/components/BlogIndex.astro +2 -2
- package/theme/components/BlogPosts.astro +1 -1
- package/theme/components/BlogPosts.tsx +9 -6
- package/theme/components/Container.astro +1 -1
- package/theme/components/CookieConsent.astro +1 -1
- package/theme/components/EndBlock.astro +1 -1
- package/theme/components/Footer.astro +5 -5
- package/theme/components/Header.astro +3 -3
- package/theme/components/Icon.astro +2 -2
- package/theme/components/MobileMenu.tsx +72 -61
- package/theme/components/MobileNav.astro +2 -2
- package/theme/components/PageHeader.astro +2 -2
- package/theme/components/PageSection.astro +1 -1
- package/theme/components/PopupButton.tsx +2 -2
- package/theme/components/PopupView.tsx +2 -2
- package/theme/components/PostList.tsx +7 -3
- package/theme/components/RichText.astro +22 -7
- package/theme/components/Section.astro +17 -10
- package/theme/components/SectionHeading.tsx +1 -1
- package/theme/components/home/PageBlock.astro +4 -4
- package/theme/components/home/Text.astro +3 -3
- package/theme/layouts/PageLayout.astro +3 -3
- package/theme/layouts/PostLayout.astro +9 -8
- package/theme/styles/base.css +13 -3
- package/theme/styles/blocks.css +97 -10
- package/theme/styles/components.css +9 -9
- package/theme/styles/elevation.test.mjs +168 -0
- package/theme/styles/global.css +65 -59
- package/theme/styles/ui.css +460 -131
- package/theme/templates/Home.astro +1 -1
- package/theme/templates/New.astro +5 -5
- package/theme/templates/NotFound.astro +2 -2
- package/theme/ui/BottomSheet.tsx +118 -21
- package/theme/ui/BottomSheetResizeIcon.tsx +13 -0
- package/theme/ui/Button.tsx +4 -3
- package/theme/ui/Dialog.tsx +1 -1
- package/theme/ui/FabMenu.tsx +13 -12
- package/theme/ui/LoadingIndicator.tsx +1 -1
- package/theme/ui/NavigationDrawer.tsx +116 -15
- package/theme/ui/NavigationRail.tsx +22 -13
- package/theme/ui/Search.tsx +1 -1
- package/theme/ui/SettingRow.tsx +31 -0
- package/theme/ui/Switch.tsx +11 -11
- package/theme/ui/TextField.tsx +17 -6
- package/theme/ui/bottomSheet.script.ts +3 -2
- package/theme/ui/bottomSheetControl.ts +131 -46
- package/theme/ui/fabMenu.script.ts +69 -53
- package/theme/ui/modalSheetSize.ts +214 -0
- package/theme/ui/navigationDrawerControl.ts +35 -0
- package/theme/ui/search.script.ts +4 -2
- package/worker/index.js +3 -0
- package/worker/plan-status.js +46 -0
- package/worker/push.js +7 -5
- package/worker/r2-status.js +2 -1
- package/icons/lucide/desktop.svg +0 -1
- package/icons/material-symbols/desktop.svg +0 -1
- package/icons/phosphor/desktop.svg +0 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Tests lib/text-sections.mjs: a text's HTML cut into coloured and uncoloured runs, and the marker as the site's real
|
|
2
|
+
// Markdown pipeline (rehype-raw, as in astro.mjs) writes it. Run with `node --test packages/editor/lib/text-sections.test.mjs`.
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { createMarkdownProcessor } from '@astrojs/markdown-remark';
|
|
6
|
+
import rehypeRaw from 'rehype-raw';
|
|
7
|
+
import { sectionClass, sectionsOf } from './text-sections.mjs';
|
|
8
|
+
|
|
9
|
+
const M = (color) => `<div data-section="${color}"></div>`;
|
|
10
|
+
|
|
11
|
+
test('html without markers passes through unchanged, in one run', () => {
|
|
12
|
+
const html = '<h1>T</h1>\n<p>a</p>\n<h2 id="x">X</h2>\n<p>b</p>';
|
|
13
|
+
assert.deepEqual(sectionsOf(html), [{ color: null, html }]);
|
|
14
|
+
assert.deepEqual(sectionsOf(''), [{ color: null, html: '' }]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('a coloured run goes from its marker to the next h2, markers removed', () => {
|
|
18
|
+
const html = `<p>Intro</p>\n${M('tint')}\n<h2 id="a">A</h2>\n<p>One</p>\n<h2 id="b">B</h2>\n<p>Two</p>`;
|
|
19
|
+
assert.deepEqual(sectionsOf(html), [
|
|
20
|
+
{ color: null, html: '<p>Intro</p>\n' },
|
|
21
|
+
{ color: 'tint', html: '<h2 id="a">A</h2>\n<p>One</p>\n' },
|
|
22
|
+
{ color: null, html: '<h2 id="b">B</h2>\n<p>Two</p>' },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('a run goes on to the end of the text, and over an h3 and blocks', () => {
|
|
27
|
+
const html = `${M('surface')}\n<h2>A</h2>\n<h3>s</h3>\n<div data-block="faq"></div>\n<p>x</p>`;
|
|
28
|
+
assert.deepEqual(sectionsOf(html), [{ color: 'surface', html: '<h2>A</h2>\n<h3>s</h3>\n<div data-block="faq"></div>\n<p>x</p>' }]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('a run ends at an h1', () => {
|
|
32
|
+
const html = `${M('tint')}\n<h2>A</h2>\n<p>x</p>\n<h1>Big</h1>\n<p>y</p>`;
|
|
33
|
+
assert.deepEqual(sectionsOf(html), [
|
|
34
|
+
{ color: 'tint', html: '<h2>A</h2>\n<p>x</p>\n' },
|
|
35
|
+
{ color: null, html: '<h1>Big</h1>\n<p>y</p>' },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('two coloured sections after each other are two runs', () => {
|
|
40
|
+
const html = `${M('tint')}\n<h2>A</h2>\n<p>x</p>\n${M('accent')}\n<h2>B</h2>\n<p>y</p>`;
|
|
41
|
+
assert.deepEqual(sectionsOf(html), [
|
|
42
|
+
{ color: 'tint', html: '<h2>A</h2>\n<p>x</p>\n' },
|
|
43
|
+
{ color: 'accent', html: '<h2>B</h2>\n<p>y</p>' },
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a label line between the marker and the h2 belongs to the section, and the next label starts the next', () => {
|
|
48
|
+
const html = `${M('tint')}\n<p class="block-eyebrow">Hours</p>\n<h2>A</h2>\n<p>x</p>\n<p class="block-eyebrow">Next</p>\n<h2>B</h2>`;
|
|
49
|
+
assert.deepEqual(sectionsOf(html), [
|
|
50
|
+
{ color: 'tint', html: '<p class="block-eyebrow">Hours</p>\n<h2>A</h2>\n<p>x</p>\n' },
|
|
51
|
+
{ color: null, html: '<p class="block-eyebrow">Next</p>\n<h2>B</h2>' },
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('an unknown colour is dropped, the text stays', () => {
|
|
56
|
+
assert.deepEqual(sectionsOf(`${M('pink')}\n<h2>A</h2>\n<p>x</p>`), [{ color: null, html: '<h2>A</h2>\n<p>x</p>' }]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a marker not followed by an h2 is dropped', () => {
|
|
60
|
+
assert.deepEqual(sectionsOf(`<p>a</p>\n${M('tint')}\n<p>b</p>\n<h2>c</h2>`), [{ color: null, html: '<p>a</p>\n<p>b</p>\n<h2>c</h2>' }]);
|
|
61
|
+
assert.deepEqual(sectionsOf(`<p>a</p>\n${M('tint')}\n<h3>c</h3>`), [{ color: null, html: '<p>a</p>\n<h3>c</h3>' }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('the class of a coloured section', () => {
|
|
65
|
+
assert.equal(sectionClass('tint'), 'text-section text-section-tint');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("in Astro's pipeline the marker comes out as the matcher reads it", async () => {
|
|
69
|
+
const processor = await createMarkdownProcessor({ rehypePlugins: [rehypeRaw] });
|
|
70
|
+
const { code } = await processor.render(`Intro\n\n${M('tint')}\n\n## Opening hours\n\nMon–Fri\n\n## Contact\n\nCall`);
|
|
71
|
+
assert.ok(code.includes(M('tint')), code);
|
|
72
|
+
const runs = sectionsOf(code);
|
|
73
|
+
assert.deepEqual(
|
|
74
|
+
runs.map((r) => r.color),
|
|
75
|
+
[null, 'tint', null],
|
|
76
|
+
);
|
|
77
|
+
assert.ok(runs[1].html.startsWith('<h2') && runs[1].html.includes('Mon–Fri') && !runs[1].html.includes('Contact'), runs[1].html);
|
|
78
|
+
});
|
package/lib/video.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// from the video, from the site itself (packages/editor/scripts/video-thumbnails.mjs), when it could be fetched.
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { BREAKPOINTS } from './breakpoints.mjs';
|
|
11
12
|
import { svgHtml } from './svg.mjs';
|
|
12
13
|
import { player, stillName, videoOf, videoPage } from './video-id.mjs';
|
|
13
14
|
|
|
@@ -26,7 +27,7 @@ const unescapeHtml = (s) => s.replace(/&(amp|lt|gt|quot|#39);/g, (_, e) => ({ am
|
|
|
26
27
|
function facade(video, title, frame, play) {
|
|
27
28
|
const { href, player, title: name, still } = facadeOf(video, title, frame);
|
|
28
29
|
const image = still
|
|
29
|
-
? `<img class="video-facade-image" src="${still.src}" srcset="${still.srcset}" sizes="(min-width:
|
|
30
|
+
? `<img class="video-facade-image" src="${still.src}" srcset="${still.srcset}" sizes="(min-width: ${BREAKPOINTS.expanded}px) 720px, 100vw" alt="" loading="lazy" decoding="async">`
|
|
30
31
|
: '';
|
|
31
32
|
return (
|
|
32
33
|
`<a class="video-facade${image ? ' video-facade-still' : ''}" href="${escapeHtml(href)}" data-video="${escapeHtml(player)}" data-title="${escapeHtml(name)}">` +
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function withoutH1(html: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// A page's text as HTML without its H1, which the layout draws in its own place (above the picture, PageLayout.astro).
|
|
2
|
+
// Exposes withoutH1 (pure); the H1 is the first one wherever it stands, as pageH1 (src/sections.mjs) reads it. No dependencies.
|
|
3
|
+
|
|
4
|
+
/** The html without its first <h1>…</h1> and the white space after it.
|
|
5
|
+
* @param {string} html
|
|
6
|
+
* @returns {string} */
|
|
7
|
+
export const withoutH1 = (html) => html.replace(/<h1\b[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Tests lib/without-h1.mjs: the first <h1> of a page's rendered text is taken out, wherever it is.
|
|
2
|
+
// Run with `node --test packages/editor/lib/*.test.mjs` (part of npm run check).
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { withoutH1 } from './without-h1.mjs';
|
|
6
|
+
|
|
7
|
+
test('an H1 at the top is taken out, with the space after it', () => {
|
|
8
|
+
assert.equal(withoutH1('<h1 id="title">Title</h1>\n<p>Text</p>'), '<p>Text</p>');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('an H1 after other parts is taken out where it is', () => {
|
|
12
|
+
assert.equal(withoutH1('<p>Intro</p>\n<h1 id="t">Title</h1>\n<h2>Section</h2>'), '<p>Intro</p>\n<h2>Section</h2>');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('an H1 with a coloured word is taken out whole', () => {
|
|
16
|
+
assert.equal(withoutH1('<h1>One <span class="color-accent">two</span></h1><p>x</p>'), '<p>x</p>');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('only the first H1 goes', () => {
|
|
20
|
+
assert.equal(withoutH1('<h1>A</h1><p>x</p><h1>B</h1>'), '<p>x</p><h1>B</h1>');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('without an H1 the html is as it was', () => {
|
|
24
|
+
assert.equal(withoutH1('<p>Text</p><h2>Section</h2>'), '<p>Text</p><h2>Section</h2>');
|
|
25
|
+
assert.equal(withoutH1('<header>x</header>'), '<header>x</header>');
|
|
26
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The word `text` has at `offset` (a place between two characters), as offsets in `text`, or null when there is no
|
|
2
|
+
* word on either side of it. wordAt('How saving works', 6) is { from: 4, to: 10 }. */
|
|
3
|
+
export function wordAt(text: any, offset: any): {
|
|
4
|
+
from: any;
|
|
5
|
+
to: any;
|
|
6
|
+
} | null;
|
package/lib/word-at.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The word around a place in a line of text (the menu over a text at a still cursor, src/stillCursor.mjs): letters,
|
|
2
|
+
// digits, apostrophes and hyphens, without the apostrophes and hyphens at its ends. Exposes wordAt. No dependencies,
|
|
3
|
+
// no browser.
|
|
4
|
+
|
|
5
|
+
const IN_WORD = /[\p{L}\p{M}\p{N}_'’-]/u;
|
|
6
|
+
const LETTER = /[\p{L}\p{N}]/u;
|
|
7
|
+
|
|
8
|
+
/** The word `text` has at `offset` (a place between two characters), as offsets in `text`, or null when there is no
|
|
9
|
+
* word on either side of it. wordAt('How saving works', 6) is { from: 4, to: 10 }. */
|
|
10
|
+
export function wordAt(text, offset) {
|
|
11
|
+
const inWord = (i) => i >= 0 && i < text.length && IN_WORD.test(text[i]);
|
|
12
|
+
if (!inWord(offset - 1) && !inWord(offset)) return null;
|
|
13
|
+
let from = offset;
|
|
14
|
+
let to = offset;
|
|
15
|
+
while (inWord(from - 1)) from--;
|
|
16
|
+
while (inWord(to)) to++;
|
|
17
|
+
while (from < to && !LETTER.test(text[from])) from++;
|
|
18
|
+
while (to > from && !LETTER.test(text[to - 1])) to--;
|
|
19
|
+
return from < to ? { from, to } : null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Tests for lib/word-at.mjs: the word around a place in a line of text. Run: node --test packages/editor/lib/*.test.mjs packages/editor/src/*.test.mjs
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { wordAt } from './word-at.mjs';
|
|
5
|
+
|
|
6
|
+
test('inside a word', () => assert.deepEqual(wordAt('How saving works', 6), { from: 4, to: 10 }));
|
|
7
|
+
test('at the start of a word', () => assert.deepEqual(wordAt('How saving works', 4), { from: 4, to: 10 }));
|
|
8
|
+
test('at the end of a word', () => assert.deepEqual(wordAt('How saving works', 10), { from: 4, to: 10 }));
|
|
9
|
+
test('between two spaces', () => assert.equal(wordAt('How saving', 4), null));
|
|
10
|
+
test('after a full stop', () => assert.equal(wordAt('works.', 6), null));
|
|
11
|
+
test('before a full stop', () => assert.deepEqual(wordAt('works.', 5), { from: 0, to: 5 }));
|
|
12
|
+
test('an apostrophe and a hyphen are inside a word', () => {
|
|
13
|
+
assert.deepEqual(wordAt("don't", 2), { from: 0, to: 5 });
|
|
14
|
+
assert.deepEqual(wordAt('e-mail', 1), { from: 0, to: 6 });
|
|
15
|
+
});
|
|
16
|
+
test('quotes around a word are not part of it', () => assert.deepEqual(wordAt("'hi'", 2), { from: 1, to: 3 }));
|
|
17
|
+
test('Danish letters', () => assert.deepEqual(wordAt('Gem ændringer', 6), { from: 4, to: 13 }));
|
|
18
|
+
test('a lone hyphen is no word', () => assert.equal(wordAt('a - b', 3), null));
|
|
19
|
+
test('an empty text', () => assert.equal(wordAt('', 0), null));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphemecms/cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lars Adolfsen",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@astrojs/rss": "^4.0.19",
|
|
54
54
|
"@astrojs/sitemap": "^3.7.4",
|
|
55
55
|
"@fontsource-variable/roboto": "^5.3.0",
|
|
56
|
+
"@material-symbols/svg-400": "0.48.2",
|
|
56
57
|
"@material/material-color-utilities": "^0.3.0",
|
|
57
58
|
"@milkdown/kit": "^7.22.3",
|
|
58
59
|
"@phosphor-icons/core": "^2.1.1",
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
"@changesets/changelog-git": "^1.0.0",
|
|
109
110
|
"@changesets/cli": "^3.0.3",
|
|
110
111
|
"@fontsource-variable/roboto": "^5.3.0",
|
|
112
|
+
"@material-symbols/svg-400": "0.48.2",
|
|
111
113
|
"@material/material-color-utilities": "^0.3.0",
|
|
112
114
|
"@milkdown/kit": "^7.22.3",
|
|
113
115
|
"@phosphor-icons/core": "^2.1.1",
|
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
// until it is the saved commit (or a later one), and then says that the site has been updated.
|
|
4
4
|
// Cloudflare's builds set WORKERS_CI_COMMIT_SHA; a build elsewhere asks git. `editor` is the
|
|
5
5
|
// fingerprint of the editor's code (packages/editor/site/editor-version.mjs): an open editor with another one
|
|
6
|
-
// reloads itself.
|
|
6
|
+
// reloads itself. `paidBuilds` (src/data/build.json, packages/editor/lib/build-settings.mjs) is how long the editor waits for
|
|
7
|
+
// a build before it says it failed: 20 minutes on the free plan, 10 on Workers Paid.
|
|
7
8
|
import { execSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
8
11
|
import { editorVersion } from '@morphemecms/cms/site/editor-version.mjs';
|
|
12
|
+
import { BUILD_SETTINGS_PATH, readBuildSettings } from '../../lib/build-settings.mjs';
|
|
13
|
+
|
|
14
|
+
// From where the build runs, as packages/editor/site/images.mjs (this file is bundled elsewhere first).
|
|
15
|
+
const SETTINGS = path.resolve(BUILD_SETTINGS_PATH);
|
|
16
|
+
|
|
17
|
+
function paidBuilds() {
|
|
18
|
+
try {
|
|
19
|
+
return readBuildSettings(JSON.parse(fs.readFileSync(SETTINGS, 'utf8'))).paidBuilds;
|
|
20
|
+
} catch {
|
|
21
|
+
return readBuildSettings(undefined).paidBuilds;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
function head() {
|
|
11
26
|
try {
|
|
@@ -17,6 +32,11 @@ function head() {
|
|
|
17
32
|
|
|
18
33
|
export function GET() {
|
|
19
34
|
return new Response(
|
|
20
|
-
JSON.stringify({
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
commit: process.env.WORKERS_CI_COMMIT_SHA || head(),
|
|
37
|
+
editor: editorVersion(),
|
|
38
|
+
builtAt: new Date().toISOString(),
|
|
39
|
+
paidBuilds: paidBuilds(),
|
|
40
|
+
}),
|
|
21
41
|
);
|
|
22
42
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Whether check:content already ran for this commit (morpheme ci-build sets MORPHEME_CONTENT_CHECKED before the build).
|
|
2
|
+
// Exposes alreadyChecked. No dependencies; apart from check-content.mjs so a test can import it without running the check.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string | undefined} envValue the MORPHEME_CONTENT_CHECKED variable
|
|
6
|
+
* @param {string} head `git rev-parse HEAD`
|
|
7
|
+
* @returns {boolean} true only when the variable is the commit being built
|
|
8
|
+
*/
|
|
9
|
+
export function alreadyChecked(envValue, head) {
|
|
10
|
+
return typeof envValue === 'string' && envValue !== '' && envValue === head;
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Tests for scripts/already-checked.mjs: alreadyChecked() skips check:content in the build only when ci-build has run it
|
|
2
|
+
// for exactly this commit. Run: node --test packages/editor/scripts/*.test.mjs
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { alreadyChecked } from './already-checked.mjs';
|
|
6
|
+
|
|
7
|
+
const head = 'a'.repeat(40);
|
|
8
|
+
|
|
9
|
+
test('alreadyChecked is true for the same sha', () => {
|
|
10
|
+
assert.equal(alreadyChecked(head, head), true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('alreadyChecked is false for another sha, an empty string or undefined', () => {
|
|
14
|
+
assert.equal(alreadyChecked('b'.repeat(40), head), false);
|
|
15
|
+
assert.equal(alreadyChecked('', head), false);
|
|
16
|
+
assert.equal(alreadyChecked(undefined, head), false);
|
|
17
|
+
assert.equal(alreadyChecked('', ''), false);
|
|
18
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The check of the build settings file (src/data/build.json, Indstillinger › Avanceret): an object with only the
|
|
2
|
+
// boolean keys contentLane and paidBuilds. Exposes buildSettingsProblems; used by check-content.mjs. No dependencies.
|
|
3
|
+
|
|
4
|
+
const KEYS = ['contentLane', 'paidBuilds'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} path the file's path, for the messages
|
|
8
|
+
* @param {string | undefined} text the file's text, or undefined when it is missing (which passes)
|
|
9
|
+
* @returns {string[]} what is wrong with the file, one line each
|
|
10
|
+
*/
|
|
11
|
+
export function buildSettingsProblems(path, text) {
|
|
12
|
+
if (text === undefined) return [];
|
|
13
|
+
let json;
|
|
14
|
+
try {
|
|
15
|
+
json = JSON.parse(text);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return [`${path}: er ikke gyldig JSON (${e.message})`];
|
|
18
|
+
}
|
|
19
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json)) return [`${path}: skal være et objekt med ${KEYS.join(' og ')}`];
|
|
20
|
+
return Object.entries(json).flatMap(([key, value]) =>
|
|
21
|
+
!KEYS.includes(key)
|
|
22
|
+
? [`${path}: kender ikke nøglen "${key}" (kun ${KEYS.join(' og ')})`]
|
|
23
|
+
: typeof value !== 'boolean'
|
|
24
|
+
? [`${path}: "${key}" skal være true eller false`]
|
|
25
|
+
: [],
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tests buildSettingsProblems (build-settings-problems.mjs): the build settings file passes when it is missing or an
|
|
2
|
+
// object with only boolean contentLane and paidBuilds, and is reported otherwise.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { buildSettingsProblems } from './build-settings-problems.mjs';
|
|
6
|
+
|
|
7
|
+
const PATH = 'src/data/build.json';
|
|
8
|
+
|
|
9
|
+
test('a missing file passes', () => {
|
|
10
|
+
assert.deepEqual(buildSettingsProblems(PATH, undefined), []);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('both keys, one key and an empty object pass', () => {
|
|
14
|
+
assert.deepEqual(buildSettingsProblems(PATH, '{ "contentLane": true, "paidBuilds": false }'), []);
|
|
15
|
+
assert.deepEqual(buildSettingsProblems(PATH, '{ "paidBuilds": true }'), []);
|
|
16
|
+
assert.deepEqual(buildSettingsProblems(PATH, '{}'), []);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('an unknown key is reported', () => {
|
|
20
|
+
const problems = buildSettingsProblems(PATH, '{ "contentLane": true, "fastLane": true }');
|
|
21
|
+
assert.equal(problems.length, 1);
|
|
22
|
+
assert.match(problems[0], /fastLane/);
|
|
23
|
+
assert.match(problems[0], new RegExp(PATH));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('a value that is not a boolean is reported', () => {
|
|
27
|
+
const problems = buildSettingsProblems(PATH, '{ "contentLane": "yes", "paidBuilds": 1 }');
|
|
28
|
+
assert.equal(problems.length, 2);
|
|
29
|
+
assert.match(problems[0], /contentLane/);
|
|
30
|
+
assert.match(problems[1], /paidBuilds/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('JSON that is not an object, or not JSON, is reported', () => {
|
|
34
|
+
for (const text of ['[]', 'true', 'null', '"x"', '{ nope']) assert.equal(buildSettingsProblems(PATH, text).length, 1, text);
|
|
35
|
+
});
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// (checkSite.ts in the editor, packages/editor/). Stops with what is wrong, so a broken page never goes
|
|
4
4
|
// out; the site that is out stays as it is; among it every theme (themeProblems, with the icon packs it brings) and
|
|
5
5
|
// the system's icon packs and the choice of one (iconPackProblems), and so an icon the site or the editor draws that a
|
|
6
|
-
// pack has none for, and the choice of motion scheme (motionProblems)
|
|
6
|
+
// pack has none for, and the choice of motion scheme (motionProblems), and names in a folder that differ only in case
|
|
7
|
+
// (casingProblems). Says too which of the site's texts the
|
|
7
8
|
// editor has no translation of (@site/texts, a block's texts.ts); that does not stop the build. The editor's
|
|
8
|
-
// code is TypeScript and TSX, so it is loaded with Vite, as the site's own build does.
|
|
9
|
+
// code is TypeScript and TSX, so it is loaded with Vite, as the site's own build does. Exits early, without checking, when
|
|
10
|
+
// MORPHEME_CONTENT_CHECKED names the commit being built (morpheme ci-build already ran it; already-checked.mjs).
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
9
12
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
13
|
import { join, relative } from 'node:path';
|
|
11
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -24,7 +27,26 @@ import {
|
|
|
24
27
|
import { appFoldersPlugin, appIds, appWorkersProblems } from '@morphemecms/cms/lib/apps.mjs';
|
|
25
28
|
import { caseCollisionProblems } from '@morphemecms/cms/lib/caseCollisions.mjs';
|
|
26
29
|
import { codePackageProblems, declarationProblems, ownerId } from '@morphemecms/cms/lib/licenses.mjs';
|
|
30
|
+
import { breakpointProblems } from '@morphemecms/cms/site/breakpoints.mjs';
|
|
31
|
+
import { casingProblems } from '../site/casing.mjs';
|
|
32
|
+
import { BUILD_SETTINGS_PATH } from '../lib/build-settings.mjs';
|
|
33
|
+
import { buildSettingsProblems } from './build-settings-problems.mjs';
|
|
34
|
+
import { textProblems } from './text-problems.mjs';
|
|
27
35
|
import { notesOf, releaseProblems } from './releases.mjs';
|
|
36
|
+
import { alreadyChecked } from './already-checked.mjs';
|
|
37
|
+
|
|
38
|
+
// morpheme ci-build has run this check for the commit being built.
|
|
39
|
+
// Git is asked only then; no repository or no commit yet means "not checked".
|
|
40
|
+
if (process.env.MORPHEME_CONTENT_CHECKED) {
|
|
41
|
+
let head = '';
|
|
42
|
+
try {
|
|
43
|
+
head = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
44
|
+
} catch {}
|
|
45
|
+
if (alreadyChecked(process.env.MORPHEME_CONTENT_CHECKED, head)) {
|
|
46
|
+
console.log(`check:content already ran for ${head.slice(0, 7)}`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
28
50
|
|
|
29
51
|
const root = `${process.cwd()}/`;
|
|
30
52
|
// This package's own code, from the site's root (packages/editor/ or node_modules/@morphemecms/cms/).
|
|
@@ -38,7 +60,7 @@ const files = ['src/content', 'src/data']
|
|
|
38
60
|
.flatMap((dir) =>
|
|
39
61
|
readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
40
62
|
.filter((e) => e.isFile() && /\.(md|json)$/.test(e.name))
|
|
41
|
-
.map((e) => join(e.parentPath, e.name)),
|
|
63
|
+
.map((e) => join(e.parentPath, e.name).replaceAll('\\', '/')),
|
|
42
64
|
)
|
|
43
65
|
.concat([config.siteRedirects.rules, config.siteRedirects.map])
|
|
44
66
|
.map((path) => ({ path, text: readFileSync(path, 'utf8') }));
|
|
@@ -52,7 +74,7 @@ const server = await createServer({
|
|
|
52
74
|
resolve: { alias: aliases() },
|
|
53
75
|
server: { middlewareMode: true, hmr: false, ws: false },
|
|
54
76
|
});
|
|
55
|
-
let result, missing;
|
|
77
|
+
let result, missing, enTexts;
|
|
56
78
|
// The licences ("Licenser" under "Indstillinger", @morphemecms/cms/lib/licenses.mjs): each app's own and those of the
|
|
57
79
|
// third-party work it brings (its defineApp), installed or not, as each theme's (themeProblems), with the packages they
|
|
58
80
|
// declare, which they give what a package lacks for.
|
|
@@ -65,6 +87,8 @@ try {
|
|
|
65
87
|
const { problemsOf, untranslated } = await server.ssrLoadModule('@morphemecms/cms/src/checkSite.ts');
|
|
66
88
|
result = problemsOf(files);
|
|
67
89
|
missing = untranslated();
|
|
90
|
+
// The editor's English texts against Material 3's rules for writing (text-problems.mjs).
|
|
91
|
+
enTexts = textProblems((await server.ssrLoadModule('@morphemecms/cms/src/i18n/en-US/index.ts')).enUS);
|
|
68
92
|
if (existsSync('store/catalog.json')) catalog = JSON.parse(readFileSync('store/catalog.json', 'utf8'));
|
|
69
93
|
for (const id of appIds(root)) {
|
|
70
94
|
const app = (await server.ssrLoadModule(`/src/apps/${id}/index.ts`)).default;
|
|
@@ -103,11 +127,14 @@ for (const [locale, files] of Object.entries(missing)) {
|
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
129
|
|
|
130
|
+
// The editor's English texts follow Material 3's rules for writing (text-problems.mjs).
|
|
131
|
+
result.problems.push(...enTexts);
|
|
106
132
|
// Every theme installed has what a theme must have (the standard theme all of it), so the owner can swap to any of them
|
|
107
133
|
// on "Udseende".
|
|
108
134
|
result.problems.push(...installedThemes().flatMap(themeProblems), ...iconPackProblems(), ...motionProblems());
|
|
109
135
|
// Every block is drawn from its view, on the site and in the editor alike.
|
|
110
136
|
result.problems.push(...blockProblems());
|
|
137
|
+
result.problems.push(...breakpointProblems(root));
|
|
111
138
|
// What each app loads from elsewhere is in its csp.json, so the pages' Content-Security-Policy lets it through.
|
|
112
139
|
result.problems.push(...appCspProblems());
|
|
113
140
|
// The addresses allowed in the editor after the policy stopped them are written as an app's csp.json.
|
|
@@ -141,6 +168,17 @@ for (const theme of installedThemes()) {
|
|
|
141
168
|
);
|
|
142
169
|
}
|
|
143
170
|
result.problems.push(...releases);
|
|
171
|
+
// No two names in a folder differ only in case, so the site builds on Windows and macOS too (casing.mjs). From git's
|
|
172
|
+
// list of the files; a site that is no git repository (a fresh starter) has none to give.
|
|
173
|
+
let tracked = [];
|
|
174
|
+
try {
|
|
175
|
+
tracked = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
176
|
+
} catch {}
|
|
177
|
+
result.problems.push(...casingProblems(tracked));
|
|
178
|
+
// The build settings the editor writes under "Avanceret": only boolean contentLane and paidBuilds; a missing file passes.
|
|
179
|
+
result.problems.push(
|
|
180
|
+
...buildSettingsProblems(BUILD_SETTINGS_PATH, existsSync(BUILD_SETTINGS_PATH) ? readFileSync(BUILD_SETTINGS_PATH, 'utf8') : undefined),
|
|
181
|
+
);
|
|
144
182
|
|
|
145
183
|
if (result.problems.length) {
|
|
146
184
|
console.error(`\nIndholdet følger ikke editorens regler (${result.problems.length}):\n`);
|
package/scripts/check-worker.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// as GitHub's GraphQL and REST APIs give it), with R2 in memory. With the cache, without it (no binding FILES) and
|
|
5
5
|
// with an R2 that fails, the editor must get exactly the files git has, save, see the versions and the deleted files
|
|
6
6
|
// and restore them; with the cache, an opening after a save may ask GitHub for nothing but the branch's head, and
|
|
7
|
-
// everything in R2 must give its git id. Nothing goes to GitHub or Cloudflare. Run: npm run check:worker
|
|
7
|
+
// everything in R2 must give its git id. /api/r2 and /api/plan must say what Cloudflare says. Nothing goes to GitHub or Cloudflare. Run: npm run check:worker
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
@@ -58,7 +58,8 @@ const out = (...args) => git(args).toString().trim();
|
|
|
58
58
|
execFileSync('git', ['init', '-q', '-b', branch, REPO], { env: ownRepoEnv() });
|
|
59
59
|
const tarball = join(REPO, '.git', 'head.tar');
|
|
60
60
|
execFileSync('git', ['-C', ROOT, 'archive', '-o', tarball, 'HEAD']);
|
|
61
|
-
|
|
61
|
+
// From inside the folder, by a relative path: GNU tar (Git's on Windows) reads C:\… as a remote host's file.
|
|
62
|
+
execFileSync('tar', ['-x', '-f', join('.git', 'head.tar')], { cwd: REPO });
|
|
62
63
|
rmSync(tarball);
|
|
63
64
|
git(['add', '-A']);
|
|
64
65
|
git(['commit', '-q', '-m', 'Start']);
|
|
@@ -213,6 +214,8 @@ const requests = [];
|
|
|
213
214
|
let cloudflare = [500, {}];
|
|
214
215
|
let cloudflareJurisdiction = null;
|
|
215
216
|
let r2Env = {};
|
|
217
|
+
// Cloudflare's answer about the account's subscriptions, for /api/plan ([status, body]).
|
|
218
|
+
let subscriptions = [500, {}];
|
|
216
219
|
async function github(url, init) {
|
|
217
220
|
asked++;
|
|
218
221
|
requests.push(`${url} ${init.body ?? ''}`);
|
|
@@ -228,7 +231,12 @@ async function github(url, init) {
|
|
|
228
231
|
// ---------------------------------------------------------------------------------------------------
|
|
229
232
|
// The Worker, with R2 in memory (or none, or one that fails), and the editor's github.ts in a "browser" whose
|
|
230
233
|
// requests to /api/github/ go to it, signed in.
|
|
231
|
-
const [{ handleGitHub }, { readFiles, commitFiles }, { handleR2 }] = await workerModules(
|
|
234
|
+
const [{ handleGitHub }, { readFiles, commitFiles }, { handleR2 }, { handlePlan, paidFrom }] = await workerModules(
|
|
235
|
+
'editor-github.js',
|
|
236
|
+
'github.js',
|
|
237
|
+
'r2-status.js',
|
|
238
|
+
'plan-status.js',
|
|
239
|
+
);
|
|
232
240
|
|
|
233
241
|
function memoryBucket() {
|
|
234
242
|
const objects = new Map();
|
|
@@ -280,6 +288,8 @@ globalThis.fetch = async (input, init = {}) => {
|
|
|
280
288
|
return new Headers(init.headers).get('cf-r2-jurisdiction') === cloudflareJurisdiction
|
|
281
289
|
? Response.json(cloudflare[1], { status: cloudflare[0] })
|
|
282
290
|
: Response.json({ success: false, errors: [{ code: 10006, message: 'The specified bucket does not exist.' }] }, { status: 404 });
|
|
291
|
+
if (url === 'https://api.cloudflare.com/client/v4/accounts/account/subscriptions')
|
|
292
|
+
return Response.json(subscriptions[1], { status: subscriptions[0] });
|
|
283
293
|
if (!url.startsWith('/api/github/')) return new Response(null, { status: 404 });
|
|
284
294
|
const request = new Request(`https://editor.example${url}`, {
|
|
285
295
|
...init,
|
|
@@ -313,7 +323,7 @@ await build({
|
|
|
313
323
|
],
|
|
314
324
|
});
|
|
315
325
|
let browsers = 0;
|
|
316
|
-
const browser = () => import(`${bundle}?${browsers++}`);
|
|
326
|
+
const browser = () => import(`${pathToFileURL(bundle).href}?${browsers++}`);
|
|
317
327
|
|
|
318
328
|
// ---------------------------------------------------------------------------------------------------
|
|
319
329
|
// What git has, as the editor had it before: each file in a folder `depth` levels down, with its text ('' for binary).
|
|
@@ -542,6 +552,44 @@ try {
|
|
|
542
552
|
expect(other.status === 403, `/api/r2 svarede en anden side med ${other.status}, ikke 403`);
|
|
543
553
|
}
|
|
544
554
|
|
|
555
|
+
// /api/plan (packages/editor/worker/plan-status.js): whether the account has Workers Paid, for "Betalt bygning".
|
|
556
|
+
{
|
|
557
|
+
const plan = (id, state) => ({ rate_plan: { id }, ...(state === undefined ? {} : { state }) });
|
|
558
|
+
expect(paidFrom([plan('WORKERS_PAID', 'Paid')]) === true, 'paidFrom: WORKERS_PAID med store bogstaver er ikke betalt');
|
|
559
|
+
expect(paidFrom([plan('workers_paid')]) === true, 'paidFrom: workers_paid uden tilstand er ikke betalt');
|
|
560
|
+
expect(paidFrom([plan('workers_paid', 'trial')]) === true, 'paidFrom: en prøveperiode er ikke betalt');
|
|
561
|
+
expect(paidFrom([plan('workers_paid', 'Cancelled')]) === false, 'paidFrom: en opsagt plan er betalt');
|
|
562
|
+
expect(paidFrom([plan('cf_free', 'Paid'), plan('zone_pro', 'Paid')]) === false, 'paidFrom: en zoneplan er Workers Paid');
|
|
563
|
+
expect(
|
|
564
|
+
paidFrom(undefined) === false && paidFrom({}) === false && paidFrom(null) === false,
|
|
565
|
+
'paidFrom: noget, der ikke er en liste, er betalt',
|
|
566
|
+
);
|
|
567
|
+
const signedIn = { cookie: 'editor_session=token', 'sec-fetch-site': 'same-origin' };
|
|
568
|
+
const askPlan = (headers = signedIn, method = 'GET') =>
|
|
569
|
+
handlePlan(new Request('https://editor.example/api/plan', { headers, method }), r2Env).then(async (r) => ({
|
|
570
|
+
status: r.status,
|
|
571
|
+
...(await r.json()),
|
|
572
|
+
}));
|
|
573
|
+
const keyed = { CLOUDFLARE_ACCOUNT_ID: 'account', CLOUDFLARE_R2_TOKEN: 'key' };
|
|
574
|
+
r2Env = {};
|
|
575
|
+
expect((await askPlan()).paid === 'no-key', '/api/plan uden Workerens nøgle sagde ikke no-key');
|
|
576
|
+
r2Env = keyed;
|
|
577
|
+
subscriptions = [403, { success: false, errors: [{ code: 10000, message: 'Authentication error' }] }];
|
|
578
|
+
expect((await askPlan()).paid === 'refused', '/api/plan sagde ikke refused, da Cloudflare svarede 403');
|
|
579
|
+
subscriptions = [500, { success: false, errors: [] }];
|
|
580
|
+
expect((await askPlan()).paid === 'failed', '/api/plan sagde ikke failed, da Cloudflare svarede 500');
|
|
581
|
+
subscriptions = [200, { success: true, errors: [], result: [plan('WORKERS_PAID', 'Paid')] }];
|
|
582
|
+
expect((await askPlan()).paid === 'yes', '/api/plan sagde ikke yes med Workers Paid');
|
|
583
|
+
subscriptions = [200, { success: true, errors: [], result: [plan('cf_free', 'Paid')] }];
|
|
584
|
+
expect((await askPlan()).paid === 'no', '/api/plan sagde ikke no uden Workers Paid');
|
|
585
|
+
expect(
|
|
586
|
+
(await askPlan({ cookie: 'editor_session=token', 'sec-fetch-site': 'same-site' })).status === 403,
|
|
587
|
+
'/api/plan svarede en anden side ikke med 403',
|
|
588
|
+
);
|
|
589
|
+
expect((await askPlan(signedIn, 'POST')).status === 405, '/api/plan svarede en POST ikke med 405');
|
|
590
|
+
r2Env = {};
|
|
591
|
+
}
|
|
592
|
+
|
|
545
593
|
// Everything in R2 is the repository's, gives its git id, and was written once.
|
|
546
594
|
for (const [key, value] of bucket.objects) {
|
|
547
595
|
const [o, r, kind, oid] = key.split('/');
|
|
@@ -554,6 +602,50 @@ try {
|
|
|
554
602
|
for (const [key, n] of bucket.puts) expect(n === 1, `R2: ${key} blev skrevet ${n} gange`);
|
|
555
603
|
for (const oid of truncated)
|
|
556
604
|
expect(!bucket.objects.has(`${owner}/${name}/blob/${oid}`), `R2: en fil, GitHub kun gav en del af, blev gemt`);
|
|
605
|
+
|
|
606
|
+
// How long a wait for a build lasts (packages/editor/worker/push.js waitForBuilds, build-settings.mjs giveUpAfter): a wait of
|
|
607
|
+
// 15 minutes is still waiting when version.json has no paidBuilds (free plan: 20 minutes), is given up with
|
|
608
|
+
// paidBuilds: true (10 minutes), and every wait is given up after 20.
|
|
609
|
+
{
|
|
610
|
+
const [{ waitForBuilds }] = await workerModules('push.js');
|
|
611
|
+
const minute = 60_000;
|
|
612
|
+
const now = Date.now();
|
|
613
|
+
const commit = 'a'.repeat(40);
|
|
614
|
+
const kv = (waits) => {
|
|
615
|
+
const store = new Map(waits.map(([key, at]) => [key, { metadata: { commit, device: key, at: new Date(at).toISOString() } }]));
|
|
616
|
+
store.set('waiting', { value: String(now + 30 * minute) });
|
|
617
|
+
return {
|
|
618
|
+
store,
|
|
619
|
+
get: async (key) => store.get(key)?.value ?? null,
|
|
620
|
+
put: async () => {},
|
|
621
|
+
delete: async (key) => void store.delete(key),
|
|
622
|
+
list: async ({ prefix }) => ({
|
|
623
|
+
keys: [...store].filter(([key]) => key.startsWith(prefix)).map(([name, { metadata }]) => ({ name, metadata })),
|
|
624
|
+
}),
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
// A build from before the saves, so none of them is on the site.
|
|
628
|
+
const assets = (paid) => ({
|
|
629
|
+
fetch: async () =>
|
|
630
|
+
Response.json({
|
|
631
|
+
commit: 'b'.repeat(40),
|
|
632
|
+
builtAt: new Date(now - 60 * minute).toISOString(),
|
|
633
|
+
...(paid === undefined ? {} : { paidBuilds: paid }),
|
|
634
|
+
}),
|
|
635
|
+
});
|
|
636
|
+
for (const [paid, minutes, waiting] of [
|
|
637
|
+
[undefined, 15, true],
|
|
638
|
+
[false, 15, true],
|
|
639
|
+
[true, 15, false],
|
|
640
|
+
[undefined, 25, false],
|
|
641
|
+
[true, 25, false],
|
|
642
|
+
]) {
|
|
643
|
+
const PUSH = kv([['wait:x', now - minutes * minute]]);
|
|
644
|
+
await waitForBuilds({ PUSH, ASSETS: assets(paid) }, now);
|
|
645
|
+
const label = `et ventende gem på ${minutes} minutter, paidBuilds ${paid}`;
|
|
646
|
+
expect(PUSH.store.has('wait:x') === waiting, `${label}: ${waiting ? 'blev opgivet for tidligt' : 'blev ikke opgivet'}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
557
649
|
} finally {
|
|
558
650
|
console.log = log;
|
|
559
651
|
rmSync(WORK, { recursive: true, force: true });
|