@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
package/src/markdown.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// The Milkdown setup shared by the editor (/editor/) and packages/editor/scripts/editor-roundtrip.mjs, so the check
|
|
2
2
|
// runs the same Markdown parser and writer as the browser.
|
|
3
3
|
import { Editor, editorViewCtx, rootCtx, defaultValueCtx, remarkStringifyOptionsCtx, serializerCtx } from '@milkdown/kit/core';
|
|
4
|
-
import { commonmark } from '@milkdown/kit/preset/commonmark';
|
|
4
|
+
import { commonmark, headingSchema, strongAttr } from '@milkdown/kit/preset/commonmark';
|
|
5
5
|
import { gfm } from '@milkdown/kit/preset/gfm';
|
|
6
6
|
import { $markSchema, $nodeSchema, $prose, $remark } from '@milkdown/kit/utils';
|
|
7
7
|
import { Plugin } from '@milkdown/kit/prose/state';
|
|
8
8
|
import { blockHtml, parseBlock } from '../lib/blocks.mjs';
|
|
9
9
|
import { COLOR_CLOSE, TEXT_COLORS, colorClass, colorOpen, colorOpened } from '../lib/text-colors.mjs';
|
|
10
|
+
import { SECTION_COLORS, isLabelLine, markerColor, sectionMarker } from '../lib/section-marker.mjs';
|
|
11
|
+
import { EMPHASIZE } from '../lib/rehype-emphasize.mjs';
|
|
12
|
+
import { eyebrowText } from '../lib/eyebrow.mjs';
|
|
13
|
+
import { eyebrowKeeper, eyebrowKeymap, eyebrowSchema } from './eyebrow.mjs';
|
|
10
14
|
|
|
11
15
|
// Write Markdown the way imported files are written: "- " bullets and
|
|
12
16
|
// "* * *" rules, so a save changes as few lines as possible.
|
|
@@ -18,11 +22,15 @@ const stringify = { bullet: '-', listItemIndent: 'tab', rule: '*', ruleSpaces: t
|
|
|
18
22
|
// of the fixed pages are blocks too (sections.mjs); the site shows only its own (RichText.astro).
|
|
19
23
|
// Other HTML on a line of its own (YouTube, Vimeo, Calendly, an imported table) is a block too, of the
|
|
20
24
|
// type HTML, with the HTML as it is as its `code`, and written back as it was.
|
|
25
|
+
// A label above a heading is read here too (eyebrow.mjs).
|
|
21
26
|
export const HTML = 'html';
|
|
22
27
|
const remarkSiteBlocks = $remark('remarkSiteBlocks', () => () => (tree) => {
|
|
23
|
-
tree.children = tree.children.map((child) => {
|
|
28
|
+
tree.children = tree.children.map((child, i) => {
|
|
24
29
|
const only = child.type === 'paragraph' && child.children?.length === 1 ? child.children[0] : child;
|
|
25
30
|
if (only.type !== 'html') return child;
|
|
31
|
+
// A label right before a heading (lib/eyebrow.mjs) is the node eyebrow (eyebrow.mjs), not a block.
|
|
32
|
+
const label = tree.children[i + 1]?.type === 'heading' ? eyebrowText(String(only.value)) : null;
|
|
33
|
+
if (label !== null) return { type: 'eyebrow', value: label };
|
|
26
34
|
const block = parseBlock(String(only.value), null);
|
|
27
35
|
return block
|
|
28
36
|
? { type: 'siteBlock', blockType: block.type, props: block.props }
|
|
@@ -122,6 +130,56 @@ export const textColorSchema = $markSchema('textColor', () => ({
|
|
|
122
130
|
},
|
|
123
131
|
}));
|
|
124
132
|
|
|
133
|
+
// The colour of a section (packages/editor/lib/section-marker.mjs): the marker line before an H2, or before the label above it
|
|
134
|
+
// (eyebrow), is read into the H2's attribute `section` and written back before it. It must be read before remarkSiteBlocks
|
|
135
|
+
// sees it, so it is the first plugin of that kind in makeEditor. A marker with nothing to colour is dropped.
|
|
136
|
+
const htmlOf = (node) => {
|
|
137
|
+
const only = node?.type === 'paragraph' && node.children?.length === 1 ? node.children[0] : node;
|
|
138
|
+
return only?.type === 'html' ? String(only.value) : '';
|
|
139
|
+
};
|
|
140
|
+
const remarkSectionMarkers = $remark('remarkSectionMarkers', () => () => (tree) => {
|
|
141
|
+
const parts = tree.children;
|
|
142
|
+
tree.children = parts.filter((child, i) => {
|
|
143
|
+
const color = markerColor(htmlOf(child));
|
|
144
|
+
if (color === null) return true;
|
|
145
|
+
const next = isLabelLine(htmlOf(parts[i + 1])) ? parts[i + 2] : parts[i + 1];
|
|
146
|
+
if (next?.type === 'heading' && next.depth === 2) next.section = color;
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// The commonmark heading with the section colour as an attribute (only an H2 has one). Written back before the heading, or
|
|
152
|
+
// before its label, which is already the last thing written (a line of HTML).
|
|
153
|
+
/** @type {import('@milkdown/kit/utils').$NodeSchema<'heading'>} */
|
|
154
|
+
export const headingWithSection = headingSchema.extendSchema((prev) => (ctx) => {
|
|
155
|
+
const base = prev(ctx);
|
|
156
|
+
return {
|
|
157
|
+
...base,
|
|
158
|
+
attrs: { ...base.attrs, section: { default: null, validate: 'string|null' } },
|
|
159
|
+
parseMarkdown: {
|
|
160
|
+
match: base.parseMarkdown.match,
|
|
161
|
+
runner: (state, node, type) => {
|
|
162
|
+
state.openNode(type, { level: node.depth, section: node.depth === 2 ? (node.section ?? null) : null });
|
|
163
|
+
state.next(node.children);
|
|
164
|
+
state.closeNode();
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
toMarkdown: {
|
|
168
|
+
match: base.toMarkdown.match,
|
|
169
|
+
runner: (state, node) => {
|
|
170
|
+
const { level, section } = node.attrs;
|
|
171
|
+
const written = state.top()?.children;
|
|
172
|
+
if (level === 2 && SECTION_COLORS.includes(section) && Array.isArray(written)) {
|
|
173
|
+
const last = written.at(-1);
|
|
174
|
+
const at = last?.type === 'html' && isLabelLine(last.value) ? written.length - 1 : written.length;
|
|
175
|
+
written.splice(at, 0, { type: 'html', value: sectionMarker(section) });
|
|
176
|
+
}
|
|
177
|
+
base.toMarkdown.runner(state, node);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
125
183
|
/** A Milkdown editor of the text, in root (not created yet). @param {HTMLElement | null} root @param {string} markdown
|
|
126
184
|
* @returns {import('@milkdown/kit/core').Editor} */
|
|
127
185
|
export function makeEditor(root, markdown) {
|
|
@@ -130,19 +188,27 @@ export function makeEditor(root, markdown) {
|
|
|
130
188
|
ctx.set(rootCtx, root);
|
|
131
189
|
ctx.set(defaultValueCtx, markdown);
|
|
132
190
|
ctx.update(remarkStringifyOptionsCtx, (options) => ({ ...options, ...stringify }));
|
|
191
|
+
// Bold as on the site (lib/rehype-emphasize.mjs).
|
|
192
|
+
ctx.set(strongAttr.key, () => ({ class: EMPHASIZE }));
|
|
133
193
|
})
|
|
134
194
|
.use(commonmark)
|
|
135
195
|
.use(gfm)
|
|
196
|
+
.use(headingWithSection)
|
|
197
|
+
.use(remarkSectionMarkers)
|
|
136
198
|
.use(remarkSiteBlocks)
|
|
137
199
|
.use(siteBlockSchema)
|
|
138
200
|
.use(remarkTextColors)
|
|
139
|
-
.use(textColorSchema)
|
|
201
|
+
.use(textColorSchema)
|
|
202
|
+
.use(eyebrowSchema)
|
|
203
|
+
.use(eyebrowKeeper)
|
|
204
|
+
.use(eyebrowKeymap);
|
|
140
205
|
}
|
|
141
206
|
|
|
142
207
|
// The text in parts, for a fixed page's sections (sections.mjs): each block, each heading at the top
|
|
143
208
|
// level ("Overskrift", H2), and the Markdown of the text between them. Empty paragraphs (the lines
|
|
144
209
|
// kept free between two blocks to write in), empty headings (left when one is deleted) and the page's H1
|
|
145
210
|
// (no section's) are left out.
|
|
211
|
+
// An H2's colour (its `section`) is the part's `color`.
|
|
146
212
|
export function partsOf(ctx) {
|
|
147
213
|
const doc = ctx.get(editorViewCtx).state.doc;
|
|
148
214
|
const serialize = ctx.get(serializerCtx);
|
|
@@ -160,7 +226,7 @@ export function partsOf(ctx) {
|
|
|
160
226
|
parts.push({ block: { type: node.attrs.type, props: node.attrs.props } });
|
|
161
227
|
} else if (node.type.name === 'heading' && node.attrs.level === 2) {
|
|
162
228
|
flush();
|
|
163
|
-
parts.push({ heading: node.textContent.trim() });
|
|
229
|
+
parts.push({ heading: node.textContent.trim(), ...(node.attrs.section ? { color: node.attrs.section } : {}) });
|
|
164
230
|
} else if (node.type.name === 'heading' && node.attrs.level === 1) {
|
|
165
231
|
flush();
|
|
166
232
|
} else if (!((node.type.name === 'paragraph' || node.type.name === 'heading') && !node.textContent.trim() && !node.childCount))
|
|
@@ -198,7 +264,9 @@ export function markdownOf(ctx) {
|
|
|
198
264
|
const doc = ctx.get(editorViewCtx).state.doc;
|
|
199
265
|
const kept = [];
|
|
200
266
|
doc.forEach((node) => {
|
|
201
|
-
|
|
267
|
+
// Nor an empty label (added, then left without a text).
|
|
268
|
+
const empty = node.content.size === 0 && (node.type.name === 'paragraph' || node.type.name === 'eyebrow');
|
|
269
|
+
if (!empty) kept.push(node);
|
|
202
270
|
});
|
|
203
271
|
return ctx.get(serializerCtx)(doc.type.create(null, kept));
|
|
204
272
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Tests for src/markdown.mjs: the section colour marker is read into the H2's `section` attribute and written back as it was
|
|
2
|
+
// (before the label, if any). Milkdown needs a DOM; jsdom stands in. Run: node --test packages/editor/src/markdown.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { JSDOM } from 'jsdom';
|
|
6
|
+
|
|
7
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>', { pretendToBeVisual: true });
|
|
8
|
+
globalThis.window = dom.window;
|
|
9
|
+
for (const key of [
|
|
10
|
+
'document',
|
|
11
|
+
'Node',
|
|
12
|
+
'Element',
|
|
13
|
+
'HTMLElement',
|
|
14
|
+
'MutationObserver',
|
|
15
|
+
'getComputedStyle',
|
|
16
|
+
'requestAnimationFrame',
|
|
17
|
+
'DOMParser',
|
|
18
|
+
'Event',
|
|
19
|
+
'CustomEvent',
|
|
20
|
+
])
|
|
21
|
+
globalThis[key] = dom.window[key];
|
|
22
|
+
for (const key of ['addEventListener', 'removeEventListener', 'dispatchEvent']) globalThis[key] = dom.window[key].bind(dom.window);
|
|
23
|
+
|
|
24
|
+
const { editorViewCtx } = await import('@milkdown/kit/core');
|
|
25
|
+
const { makeEditor, markdownOf } = await import('./markdown.mjs');
|
|
26
|
+
|
|
27
|
+
// The text loaded and written back, and the nodes it was read as ("heading:tint", "siteBlock", …).
|
|
28
|
+
async function through(markdown) {
|
|
29
|
+
const editor = await makeEditor(document.createElement('div'), markdown).create();
|
|
30
|
+
const nodes = [];
|
|
31
|
+
editor.action((ctx) =>
|
|
32
|
+
ctx
|
|
33
|
+
.get(editorViewCtx)
|
|
34
|
+
.state.doc.forEach((n) => nodes.push(n.type.name === 'heading' && n.attrs.section ? `heading:${n.attrs.section}` : n.type.name)),
|
|
35
|
+
);
|
|
36
|
+
const saved = editor.action(markdownOf);
|
|
37
|
+
await editor.destroy();
|
|
38
|
+
return { saved, nodes };
|
|
39
|
+
}
|
|
40
|
+
const marked = (color, rest) => `<div data-section="${color}"></div>\n\n${rest}`;
|
|
41
|
+
|
|
42
|
+
test('a coloured section comes back byte for byte', async () => {
|
|
43
|
+
const text = `# Title\n\nIntro\n\n${marked('tint', '## Opening hours\n\nText\n\n')}${marked('accent', '## Prices\n\nMore\n')}`;
|
|
44
|
+
const { saved, nodes } = await through(text);
|
|
45
|
+
assert.equal(saved, text);
|
|
46
|
+
assert.deepEqual(nodes, ['heading', 'paragraph', 'heading:tint', 'paragraph', 'heading:accent', 'paragraph']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a text without markers is written as before', async () => {
|
|
50
|
+
const text = '# Title\n\n## One\n\nText\n';
|
|
51
|
+
assert.equal((await through(text)).saved, text);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the marker comes before a label above the H2', async () => {
|
|
55
|
+
const text = marked('surface', '<p class="block-eyebrow">Step by step</p>\n\n## Hours\n\nText\n');
|
|
56
|
+
const { saved, nodes } = await through(text);
|
|
57
|
+
assert.equal(saved, text);
|
|
58
|
+
assert.ok(nodes.includes('heading:surface'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a marker not followed by an H2 is dropped', async () => {
|
|
62
|
+
assert.deepEqual(await through(marked('tint', 'Plain text\n\n## B\n')), {
|
|
63
|
+
saved: 'Plain text\n\n## B\n',
|
|
64
|
+
nodes: ['paragraph', 'heading'],
|
|
65
|
+
});
|
|
66
|
+
assert.equal((await through(marked('tint', '### Sub\n'))).saved, '### Sub\n');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('an unknown colour stays the HTML it is', async () => {
|
|
70
|
+
const text = marked('red', '## B\n');
|
|
71
|
+
const { saved, nodes } = await through(text);
|
|
72
|
+
assert.equal(saved, text);
|
|
73
|
+
assert.deepEqual(nodes, ['siteBlock', 'heading']);
|
|
74
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// All of Material Symbols (Outlined, regular and filled) as text, by their file, for the icons icon.tsx draws that are not on
|
|
2
|
+
// its list: one lazy chunk, loaded in the browser the first time one is drawn (the server render has them from
|
|
3
|
+
// virtual:material-symbols-ssr instead, packages/editor/site/materialSymbols.mjs). Imported by icon.tsx only dynamically.
|
|
4
|
+
export const materialIcons = import.meta.glob<string>('/node_modules/@material-symbols/svg-400/outlined/*.svg', {
|
|
5
|
+
query: '?raw',
|
|
6
|
+
import: 'default',
|
|
7
|
+
eager: true,
|
|
8
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The second level of the main menu (MainMenu.tsx, lib/menu.mjs menuTree): the lists under Lists, the menus under Header,
|
|
2
|
+
// the parts under Business, the apps installed under Apps and the two lists under Redirects. Exposes childrenOf.
|
|
3
|
+
// Uses site.ts (collections, apps, companyParts), Library.tsx (hrefOf, listsShown) and Redirects.tsx (TAB_ROUTES).
|
|
4
|
+
import { header } from '@site/editor';
|
|
5
|
+
import { installedApps } from './apps';
|
|
6
|
+
import { t } from './i18n';
|
|
7
|
+
import { hrefOf } from './Library';
|
|
8
|
+
import { TAB_ROUTES } from './Redirects';
|
|
9
|
+
import type { RepoFile } from './github';
|
|
10
|
+
import { apps, collections, companyParts, listsShown } from './site';
|
|
11
|
+
|
|
12
|
+
export type MenuChild = { id: string; label: string; href: string };
|
|
13
|
+
|
|
14
|
+
const listOf = (ids: string[]): MenuChild[] =>
|
|
15
|
+
ids.flatMap((id) => {
|
|
16
|
+
const c = collections.find((x) => x.id === id);
|
|
17
|
+
return c ? [{ id, label: c.label, href: hrefOf(c) }] : [];
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** The sub-items of a menu item, in their order: none for most. */
|
|
21
|
+
export function childrenOf(id: string, files: RepoFile[]): MenuChild[] {
|
|
22
|
+
if (id === 'lister') return listOf(listsShown(files));
|
|
23
|
+
if (id === 'header') return listOf(header);
|
|
24
|
+
if (id === 'virksomheden') return companyParts.map((p) => ({ id: p.id, label: p.label, href: `#virksomheden/${p.id}` }));
|
|
25
|
+
if (id === 'apps') {
|
|
26
|
+
const installed = installedApps(files);
|
|
27
|
+
return apps.filter((a) => installed.includes(a.id)).map((a) => ({ id: a.id, label: a.label, href: `#apps/${a.id}` }));
|
|
28
|
+
}
|
|
29
|
+
if (id === 'videresendelser') {
|
|
30
|
+
return [
|
|
31
|
+
{ id: 'alle', label: t.redirects.all, href: `#${TAB_ROUTES.redirects}` },
|
|
32
|
+
{ id: 'ikke-fundet', label: t.redirects.missing.title, href: `#${TAB_ROUTES.missing}` },
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
package/src/menuOpen.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Whether the main menu's drawer is open, for the drawer (MainMenu.tsx) and the menu buttons (Screen.tsx, the rail): at expanded
|
|
2
|
+
// and wider the standard drawer's remembered state (sidebar.ts), below it the modal drawer's, which is not remembered.
|
|
3
|
+
// Which one is on screen is lib/menu-layout.mjs. Exposes useMenuOpen.
|
|
4
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
5
|
+
import { setSidebar, useSidebar } from './sidebar';
|
|
6
|
+
import { useMenuLayout } from './ui';
|
|
7
|
+
|
|
8
|
+
let modalOpen = false;
|
|
9
|
+
const listeners = new Set<() => void>();
|
|
10
|
+
|
|
11
|
+
function setModal(next: boolean) {
|
|
12
|
+
modalOpen = next;
|
|
13
|
+
listeners.forEach((l) => l());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const useModalOpen = () =>
|
|
17
|
+
useSyncExternalStore(
|
|
18
|
+
(changed) => {
|
|
19
|
+
listeners.add(changed);
|
|
20
|
+
return () => listeners.delete(changed);
|
|
21
|
+
},
|
|
22
|
+
() => modalOpen,
|
|
23
|
+
() => false,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** The menu's open state for the drawer that is on screen now, and how to change it. */
|
|
27
|
+
export function useMenuOpen() {
|
|
28
|
+
const wide = useMenuLayout().expanded === 'standard';
|
|
29
|
+
const standard = useSidebar();
|
|
30
|
+
const modal = useModalOpen();
|
|
31
|
+
const open = wide ? standard : modal;
|
|
32
|
+
const setOpen = useCallback((next: boolean) => (wide ? setSidebar(next) : setModal(next)), [wide]);
|
|
33
|
+
return { open, setOpen, toggle: () => setOpen(!open) };
|
|
34
|
+
}
|
package/src/pagePreview.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// "Forhåndsvis" (FormFrame.tsx): a page or a blog post as it looks on the site with what is in its form
|
|
2
|
-
// now, at once and without saving, in a sheet,
|
|
2
|
+
// now, at once and without saving, in a sheet, at each of Material 3's window sizes
|
|
3
|
+
// (lib/breakpoints.mjs).
|
|
3
4
|
//
|
|
4
5
|
// The page as it is on the site gives everything around what the form changes: the header, the footer,
|
|
5
6
|
// the layout and the site's CSS. The site's layouts mark what the form changes (data-editor): the
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
// puts a block that ends the text (data-editor="end", with its type in data-editor-block). Only what
|
|
8
9
|
// was changed is put in, so the rest is exactly as on the site. The text is the site's Markdown
|
|
9
10
|
// (Astro's pipeline, with the HTML written in it), and its blocks are drawn as in the editor
|
|
10
|
-
// (registry.tsx preview), with the site's classes.
|
|
11
|
+
// (registry.tsx preview), with the site's classes; a coloured section is a panel as on the site (lib/text-sections.mjs).
|
|
11
12
|
//
|
|
12
13
|
// The page is shown in a frame without scripts (sandbox), so nothing of the site runs on the editor's
|
|
13
14
|
// address, where its login is kept, and its links lead nowhere. A new page or post is shown in the
|
|
@@ -17,8 +18,9 @@
|
|
|
17
18
|
import { useRef, useState, type ReactNode } from 'react';
|
|
18
19
|
import { createRoot } from 'react-dom/client';
|
|
19
20
|
import { flushSync } from 'react-dom';
|
|
20
|
-
import { DeviceMobile, Desktop } from '@phosphor-icons/react';
|
|
21
21
|
import { parseBlock, splitBlocks } from '../lib/blocks.mjs';
|
|
22
|
+
import { sectionClass, sectionsOf } from '../lib/text-sections.mjs';
|
|
23
|
+
import { BREAKPOINTS, breakpointOf, type Breakpoint } from '../lib/breakpoints.mjs';
|
|
22
24
|
import { processor } from '../lib/markdown';
|
|
23
25
|
import { splitPicture } from '../lib/crop.mjs';
|
|
24
26
|
import { preview as drawBlock, siteBlocks } from './registry';
|
|
@@ -134,11 +136,14 @@ export async function pageDocument(draft: PageDraft): Promise<string> {
|
|
|
134
136
|
end.innerHTML = blockHtml(block.type, block.props, draft.data);
|
|
135
137
|
} else end?.remove();
|
|
136
138
|
const text = doc.querySelector('[data-editor="text"]')!;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const types = siteBlocks.map((b) => b.type);
|
|
140
|
+
text.innerHTML = sectionsOf(html)
|
|
141
|
+
.map((run) => {
|
|
142
|
+
const inner = splitBlocks(run.html, types)
|
|
143
|
+
.map((p) => ('html' in p ? p.html : blockHtml(p.type, p.props, draft.data)))
|
|
144
|
+
.join('');
|
|
145
|
+
return run.color ? `<section class="${sectionClass(run.color)}">${inner}</section>` : inner;
|
|
146
|
+
})
|
|
142
147
|
.join('');
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -148,15 +153,17 @@ export async function pageDocument(draft: PageDraft): Promise<string> {
|
|
|
148
153
|
return `<!doctype html>\n${doc.documentElement.outerHTML}`;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
// The widths the page is shown at: Material 3's breakpoints, each at the narrowest window it has, so its tightest
|
|
157
|
+
// layout is seen; compact at a common phone's.
|
|
158
|
+
const SIZES: Breakpoint[] = ['compact', 'medium', 'expanded', 'large', 'extraLarge'];
|
|
159
|
+
const WIDTH: Record<Breakpoint, number> = { ...BREAKPOINTS, compact: 390 };
|
|
153
160
|
|
|
154
161
|
/** The sheet with the preview, and how to open it with a draft (made when it opens). */
|
|
155
162
|
export function usePagePreview(): [(draft: () => PageDraft | null) => void, ReactNode] {
|
|
156
163
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
157
164
|
const [html, setHtml] = useState<string | null>(null);
|
|
158
165
|
const [problem, setProblem] = useState('');
|
|
159
|
-
const [
|
|
166
|
+
const [size, setSize] = useState<Breakpoint>(() => breakpointOf(innerWidth));
|
|
160
167
|
const run = useRef(0);
|
|
161
168
|
|
|
162
169
|
function open(make: () => PageDraft | null) {
|
|
@@ -184,17 +191,14 @@ export function usePagePreview(): [(draft: () => PageDraft | null) => void, Reac
|
|
|
184
191
|
<SheetTitle title={t.preview.title} onClose={close} />
|
|
185
192
|
<div className="px-5 pb-3">
|
|
186
193
|
<SegmentedButton
|
|
187
|
-
label={t.preview.
|
|
188
|
-
value={
|
|
189
|
-
options={[
|
|
190
|
-
|
|
191
|
-
{ value: 'computer', label: t.preview.computer, icon: Desktop },
|
|
192
|
-
]}
|
|
193
|
-
onChange={setDevice}
|
|
194
|
+
label={t.preview.size}
|
|
195
|
+
value={size}
|
|
196
|
+
options={SIZES.map((s) => ({ value: s, label: t.preview.sizes[s] }))}
|
|
197
|
+
onChange={setSize}
|
|
194
198
|
/>
|
|
195
199
|
</div>
|
|
196
200
|
{html ? (
|
|
197
|
-
<PageFrame html={html} width={WIDTH[
|
|
201
|
+
<PageFrame html={html} width={WIDTH[size]} title={t.preview.title} />
|
|
198
202
|
) : (
|
|
199
203
|
<p className="px-5 text-muted">{problem || t.preview.making}</p>
|
|
200
204
|
)}
|
package/src/r2Caching.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Also here: planStatus, whether the account has Workers Paid (/api/plan, packages/editor/worker/plan-status.js), asked for "Betalt bygning".
|
|
1
2
|
// R2 caching: the Worker's copy of the site's files in R2 (packages/editor/worker/files.js), turned on and off under "Indstillinger" ›
|
|
2
3
|
// "Avanceret" (Advanced.tsx). It is the line of its bucket (`"r2_buckets": …`, the binding FILES) in the Worker's settings
|
|
3
4
|
// (config.wrangler): there when it is on, there as a comment ("// …") when it is off, so a deploy never asks for a bucket
|
|
@@ -39,6 +40,19 @@ export const withJurisdiction = (text: string, jurisdiction: string | null): str
|
|
|
39
40
|
/** A jurisdiction's name, as Cloudflare writes it. */
|
|
40
41
|
export const jurisdictionName = (jurisdiction: string): string => ({ eu: 'EU', fedramp: 'FedRAMP' })[jurisdiction] ?? jurisdiction;
|
|
41
42
|
|
|
43
|
+
/** Whether the account has Workers Paid, as the Worker says (packages/editor/worker/plan-status.js); 'failed' when it can't be asked. */
|
|
44
|
+
export type PaidState = 'yes' | 'no' | 'no-key' | 'refused' | 'failed';
|
|
45
|
+
|
|
46
|
+
export async function planStatus(): Promise<PaidState> {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch('/api/plan', { cache: 'no-store' });
|
|
49
|
+
if (!res.ok || !res.headers.get('content-type')?.includes('application/json')) return 'failed';
|
|
50
|
+
return (await res.json()).paid;
|
|
51
|
+
} catch {
|
|
52
|
+
return 'failed';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
/** The bucket at Cloudflare, as the Worker says (packages/editor/worker/r2-status.js). */
|
|
43
57
|
export type BucketState = 'ready' | 'no-bucket' | 'no-r2' | 'no-key' | 'refused' | 'failed';
|
|
44
58
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The dotted frame round each H2 section of a page or blog post: sets data-section-part (start, middle, end, only) and
|
|
2
|
+
// data-section-color (the H2's section colour) on the top-level nodes of a section, which editor.css draws. Exposes
|
|
3
|
+
// sectionFrame (a Milkdown plugin). Uses lib/section-ranges.mjs and src/sectionState.mjs.
|
|
4
|
+
import { $prose } from '@milkdown/kit/utils';
|
|
5
|
+
import type { Node } from '@milkdown/kit/prose/model';
|
|
6
|
+
import { Plugin } from '@milkdown/kit/prose/state';
|
|
7
|
+
import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
|
|
8
|
+
import { partKind, sectionPart, sectionRanges } from '../lib/section-ranges.mjs';
|
|
9
|
+
import { sectionColorOf } from './sectionState.mjs';
|
|
10
|
+
|
|
11
|
+
type Range = { from: number; to: number };
|
|
12
|
+
|
|
13
|
+
// The attributes of the part at `index` in a section: its place in the frame, and the section's colour (its H2's `section`).
|
|
14
|
+
function sectionAttrs(nodes: Node[], range: Range, index: number): Record<string, string> {
|
|
15
|
+
const color = sectionColorOf(nodes, range);
|
|
16
|
+
return { 'data-section-part': sectionPart(index, range), ...(color ? { 'data-section-color': color } : {}) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The decorations of a document's sections. */
|
|
20
|
+
function sectionDecorations(doc: Node): Decoration[] {
|
|
21
|
+
const parts: { node: Node; pos: number }[] = [];
|
|
22
|
+
doc.forEach((node, pos) => void parts.push({ node, pos }));
|
|
23
|
+
const nodes = parts.map(({ node }) => node);
|
|
24
|
+
const ranges: Range[] = sectionRanges(nodes.map((node) => ({ kind: partKind(node.type.name, node.attrs.level) })));
|
|
25
|
+
return ranges.flatMap((range) =>
|
|
26
|
+
parts
|
|
27
|
+
.slice(range.from, range.to + 1)
|
|
28
|
+
.map(({ node, pos }, i) => Decoration.node(pos, pos + node.nodeSize, sectionAttrs(nodes, range, range.from + i))),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** on: false for a fixed page's text (MarkdownEditor's `sections`), whose H2s are the names of its sections: no frame. */
|
|
33
|
+
export const sectionFrame = (on: boolean) =>
|
|
34
|
+
$prose(
|
|
35
|
+
() =>
|
|
36
|
+
new Plugin({
|
|
37
|
+
props: { decorations: on ? ({ doc }) => DecorationSet.create(doc, sectionDecorations(doc)) : () => null },
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The top-level H2 the cursor is in, with its colour, or null. @param {import('@milkdown/kit/prose/state').EditorState} state
|
|
2
|
+
* @returns {{ pos: number, color: string | null } | null} */
|
|
3
|
+
export function sectionHeading(state: import("@milkdown/kit/prose/state").EditorState): {
|
|
4
|
+
pos: number;
|
|
5
|
+
color: string | null;
|
|
6
|
+
} | null;
|
|
7
|
+
/** Gives the H2 the cursor is in this colour (null: none). False when it is not in an H2, or the colour is not one of the theme's.
|
|
8
|
+
* @param {{ state: import('@milkdown/kit/prose/state').EditorState, dispatch: (tr: import('@milkdown/kit/prose/state').Transaction) => void }} view
|
|
9
|
+
* @param {string | null} color */
|
|
10
|
+
export function setSectionColor(view: {
|
|
11
|
+
state: import("@milkdown/kit/prose/state").EditorState;
|
|
12
|
+
dispatch: (tr: import("@milkdown/kit/prose/state").Transaction) => void;
|
|
13
|
+
}, color: string | null): boolean;
|
|
14
|
+
/** The colour of the section made of nodes[range.from..range.to] (sectionRanges): its H2's, null without one.
|
|
15
|
+
* @param {import('@milkdown/kit/prose/model').Node[]} nodes @param {{ from: number, to: number }} range @returns {string | null} */
|
|
16
|
+
export function sectionColorOf(nodes: import("@milkdown/kit/prose/model").Node[], range: {
|
|
17
|
+
from: number;
|
|
18
|
+
to: number;
|
|
19
|
+
}): string | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The section colour in the editor's document (the *Sektionsfarve* button, MarkdownEditor.tsx; the frame, sectionFrame.ts): the
|
|
2
|
+
// top-level H2 the cursor is in (sectionHeading), setting its colour (setSectionColor) and the colour of a section (sectionColorOf).
|
|
3
|
+
// Only ProseMirror's state; depends on lib/section-marker.mjs. No Milkdown, no browser.
|
|
4
|
+
import { SECTION_COLORS } from '../lib/section-marker.mjs';
|
|
5
|
+
|
|
6
|
+
/** The top-level H2 the cursor is in, with its colour, or null. @param {import('@milkdown/kit/prose/state').EditorState} state
|
|
7
|
+
* @returns {{ pos: number, color: string | null } | null} */
|
|
8
|
+
export function sectionHeading(state) {
|
|
9
|
+
const { $from } = state.selection;
|
|
10
|
+
const node = $from.parent;
|
|
11
|
+
if ($from.depth !== 1 || node.type.name !== 'heading' || node.attrs.level !== 2) return null;
|
|
12
|
+
return { pos: $from.before(1), color: node.attrs.section ?? null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Gives the H2 the cursor is in this colour (null: none). False when it is not in an H2, or the colour is not one of the theme's.
|
|
16
|
+
* @param {{ state: import('@milkdown/kit/prose/state').EditorState, dispatch: (tr: import('@milkdown/kit/prose/state').Transaction) => void }} view
|
|
17
|
+
* @param {string | null} color */
|
|
18
|
+
export function setSectionColor(view, color) {
|
|
19
|
+
const at = sectionHeading(view.state);
|
|
20
|
+
if (!at || (color !== null && !SECTION_COLORS.includes(color))) return false;
|
|
21
|
+
const heading = view.state.doc.nodeAt(at.pos);
|
|
22
|
+
if (at.color !== color) view.dispatch(view.state.tr.setNodeMarkup(at.pos, undefined, { ...heading.attrs, section: color }));
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The colour of the section made of nodes[range.from..range.to] (sectionRanges): its H2's, null without one.
|
|
27
|
+
* @param {import('@milkdown/kit/prose/model').Node[]} nodes @param {{ from: number, to: number }} range @returns {string | null} */
|
|
28
|
+
export function sectionColorOf(nodes, range) {
|
|
29
|
+
const heading = nodes.slice(range.from, range.to + 1).find((n) => n.type.name === 'heading' && n.attrs.level === 2);
|
|
30
|
+
return heading?.attrs.section ?? null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Tests for src/sectionState.mjs: the H2 the cursor is in, setting its section colour, and the colour of a section.
|
|
2
|
+
// Run: node --test packages/editor/src/sectionState.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { Schema } from '@milkdown/kit/prose/model';
|
|
6
|
+
import { EditorState, TextSelection } from '@milkdown/kit/prose/state';
|
|
7
|
+
import { sectionColorOf, sectionHeading, setSectionColor } from './sectionState.mjs';
|
|
8
|
+
|
|
9
|
+
const schema = new Schema({
|
|
10
|
+
nodes: {
|
|
11
|
+
doc: { content: 'block+' },
|
|
12
|
+
paragraph: { group: 'block', content: 'text*' },
|
|
13
|
+
heading: { group: 'block', content: 'text*', attrs: { level: { default: 2 }, section: { default: null } } },
|
|
14
|
+
eyebrow: { group: 'block', content: 'text*' },
|
|
15
|
+
text: {},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const node = (type, text, attrs) => schema.node(type, attrs, text ? [schema.text(text)] : []);
|
|
19
|
+
// An H2 "Hours" (0-7, coloured tint), a paragraph (7-10), an H3 (10-14).
|
|
20
|
+
const doc = schema.node('doc', null, [
|
|
21
|
+
node('heading', 'Hours', { level: 2, section: 'tint' }),
|
|
22
|
+
node('paragraph', 'x'),
|
|
23
|
+
node('heading', 'Sub', { level: 3 }),
|
|
24
|
+
]);
|
|
25
|
+
const viewAt = (pos) => ({
|
|
26
|
+
state: EditorState.create({ doc, selection: TextSelection.create(doc, pos) }),
|
|
27
|
+
dispatch(tr) {
|
|
28
|
+
this.state = this.state.apply(tr);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('the H2 the cursor is in', () => assert.deepEqual(sectionHeading(viewAt(3).state), { pos: 0, color: 'tint' }));
|
|
33
|
+
test('none in a paragraph or an H3', () => {
|
|
34
|
+
assert.equal(sectionHeading(viewAt(8).state), null);
|
|
35
|
+
assert.equal(sectionHeading(viewAt(12).state), null);
|
|
36
|
+
});
|
|
37
|
+
test('sets another colour', () => {
|
|
38
|
+
const view = viewAt(3);
|
|
39
|
+
assert.equal(setSectionColor(view, 'accent'), true);
|
|
40
|
+
assert.equal(view.state.doc.child(0).attrs.section, 'accent');
|
|
41
|
+
assert.equal(view.state.doc.child(0).attrs.level, 2);
|
|
42
|
+
});
|
|
43
|
+
test('null takes the colour away', () => {
|
|
44
|
+
const view = viewAt(3);
|
|
45
|
+
setSectionColor(view, null);
|
|
46
|
+
assert.equal(view.state.doc.child(0).attrs.section, null);
|
|
47
|
+
});
|
|
48
|
+
test('a colour the theme has not is refused, and nothing happens outside an H2', () => {
|
|
49
|
+
const view = viewAt(3);
|
|
50
|
+
assert.equal(setSectionColor(view, 'red'), false);
|
|
51
|
+
assert.equal(view.state.doc.child(0).attrs.section, 'tint');
|
|
52
|
+
assert.equal(setSectionColor(viewAt(8), 'tint'), false);
|
|
53
|
+
});
|
|
54
|
+
test('the colour of a section is its H2’s, also after a label', () => {
|
|
55
|
+
const kids = [node('eyebrow', 'Label'), node('heading', 'Hours', { level: 2, section: 'surface' }), node('paragraph', 'x')];
|
|
56
|
+
assert.equal(sectionColorOf(kids, { from: 0, to: 2 }), 'surface');
|
|
57
|
+
assert.equal(sectionColorOf(kids, { from: 1, to: 2 }), 'surface');
|
|
58
|
+
assert.equal(sectionColorOf([node('heading', 'A', { level: 2 })], { from: 0, to: 0 }), null);
|
|
59
|
+
});
|
package/src/sections.d.mts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
export function sectionsToMarkdown(sections: any, leads: {} | undefined, intro: any): string;
|
|
4
4
|
/** The page's introduction (the text before the first heading or card) and the parts after it. */
|
|
5
5
|
export function introOf(parts: any): any[];
|
|
6
|
-
/** The sections for the parts of the text (partsOf in markdown.mjs); leads as for sectionsToMarkdown.
|
|
7
|
-
|
|
6
|
+
/** The sections for the parts of the text (partsOf in markdown.mjs); leads as for sectionsToMarkdown. `colors`: the front
|
|
7
|
+
* page: each section also has the colour of its H2 as `sectionColor` (the top, the "Banner", has none). */
|
|
8
|
+
export function sectionsFromParts(parts: any, leads?: {}, options?: {}): any;
|
|
8
9
|
/** The look of each part of the text (null: as text), from its parts in order: { kind: 'block', type }
|
|
9
10
|
* for a card, 'heading' (H2), 'p' (with image: a picture), 'gap' (a free line) or 'other'. The text a
|
|
10
11
|
* section takes as its own is found as in sectionsFromParts. A text after the first is marked "rest". */
|
|
@@ -15,9 +16,10 @@ export function looksOf(items: any, { file, leads, intro, sectionLooks, headingL
|
|
|
15
16
|
sectionLooks?: {} | undefined;
|
|
16
17
|
headingLooks?: {} | undefined;
|
|
17
18
|
}): any;
|
|
18
|
-
/** A page's H1: the heading "# …"
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
/** A page's H1: the first heading "# …" of its text outside fenced code, wherever it stands (the editor makes the first
|
|
20
|
+
* heading of a page its H1, src/firstHeading.mjs, but text may stand above it), as text, and the text without that line
|
|
21
|
+
* (what stood above it first, a blank line, then what stood below). Without one: no heading, and the text as it is.
|
|
22
|
+
* With a word or more in colour, `html` is the heading as HTML with its colours (coloredHtml), for the layout to draw. */
|
|
21
23
|
export function pageH1(markdown: any): {
|
|
22
24
|
heading: undefined;
|
|
23
25
|
rest: string;
|
|
@@ -30,11 +32,11 @@ export function pageH1(markdown: any): {
|
|
|
30
32
|
/** The parts of a page's Markdown as the editor reads them (partsOf in markdown.mjs), without the editor: each block,
|
|
31
33
|
* each heading at the top level (H2), and the Markdown between them; for the site, which reads the front page into
|
|
32
34
|
* sections (packages/editor/site/home.ts). The page's H1 ("# …") is no section's and is left out. `npm run check:editor` checks
|
|
33
|
-
* that it gives the editor's sections for every page. */
|
|
35
|
+
* that it gives the editor's sections for every page. A colour marker before an H2 is its part's `color`. */
|
|
34
36
|
export function partsOfMarkdown(markdown: any): ({
|
|
35
37
|
block: any;
|
|
36
|
-
heading?: undefined;
|
|
37
38
|
} | {
|
|
39
|
+
color?: string | undefined;
|
|
38
40
|
heading: any;
|
|
39
41
|
block?: undefined;
|
|
40
42
|
})[];
|