@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/lib/menu.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The editor's menu as data: the two-level tree (menuTree) and which section/item/child is open for a route (currentPlace).
|
|
2
|
+
// In the Apps section the installed-apps list (`apps`) is not an item: each installed app is an item after the others.
|
|
3
|
+
// currentPart (which item is open for a route) is exported only for its tests; currentPlace uses it as its last rule.
|
|
4
|
+
// Pure, no browser; MainMenu.tsx draws the drawer from it. No dependencies.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ id: string, title: string, href?: string, ids: string[] }[]} groups
|
|
8
|
+
* @param {(id: string) => { label: string, href: string, count: number | null }} partOf
|
|
9
|
+
* @param {(id: string) => { id: string, label: string, href: string }[]} childrenOf
|
|
10
|
+
* @returns {{ id: string, label: string, href?: string, items: { id: string, label: string, href: string, count: number | null, children?: { id: string, label: string, href: string }[] }[] }[]}
|
|
11
|
+
*/
|
|
12
|
+
export function menuTree(groups, partOf, childrenOf) {
|
|
13
|
+
return groups.map((group) => {
|
|
14
|
+
const entry = { id: group.id, label: group.title };
|
|
15
|
+
if (group.href !== undefined) {
|
|
16
|
+
entry.href = group.href;
|
|
17
|
+
}
|
|
18
|
+
// The installed-apps list (#apps) is not a row of the Apps section: its apps are rows, after the other items.
|
|
19
|
+
const hoisted = group.id === 'apps' ? childrenOf('apps') : [];
|
|
20
|
+
const rows = group.id === 'apps' ? group.ids.filter((id) => id !== 'apps') : group.ids;
|
|
21
|
+
entry.items = rows.map((id) => {
|
|
22
|
+
const item = { id, ...partOf(id) };
|
|
23
|
+
const children = childrenOf(id);
|
|
24
|
+
if (children.length > 0) {
|
|
25
|
+
item.children = children;
|
|
26
|
+
}
|
|
27
|
+
return item;
|
|
28
|
+
});
|
|
29
|
+
for (const app of hoisted) {
|
|
30
|
+
entry.items.push({ id: app.id, label: app.label, href: app.href, count: null });
|
|
31
|
+
}
|
|
32
|
+
return entry;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PARENTS = ['virksomheden', 'blokke', 'udseende', 'seo', 'apps'];
|
|
37
|
+
|
|
38
|
+
const under = (path, prefix) => path === prefix || path.startsWith(prefix + '/');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} route
|
|
42
|
+
* @param {{ open?: string, lists: string[], header: string[], partScreens: string[], cookies: string, store: string }} known
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function currentPart(route, known) {
|
|
46
|
+
if (known.store && under(route, known.store)) {
|
|
47
|
+
return 'appstore';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const part = known.open || route.replace(/^(liste|ny|slettet)\/([^/]+).*/, '$2');
|
|
51
|
+
|
|
52
|
+
if (part === known.cookies) {
|
|
53
|
+
// The cookie list is a list, but its place is under Privacy.
|
|
54
|
+
return 'privatliv';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (known.lists.includes(part)) {
|
|
58
|
+
return 'lister';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (known.header.includes(part)) {
|
|
62
|
+
return 'header';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parent = PARENTS.find((id) => under(part, id));
|
|
66
|
+
if (parent) {
|
|
67
|
+
return parent;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return known.partScreens.find((id) => part.startsWith(id + '/')) ?? part;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} route
|
|
75
|
+
* @param {ReturnType<typeof menuTree>} tree
|
|
76
|
+
* @param {Parameters<typeof currentPart>[1]} known
|
|
77
|
+
* @returns {{ section: string | null, item?: string, child?: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function currentPlace(route, tree, known) {
|
|
80
|
+
if (route === '') {
|
|
81
|
+
return { section: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Rule 2: the first child whose href path matches wins.
|
|
85
|
+
for (const entry of tree) {
|
|
86
|
+
for (const item of entry.items) {
|
|
87
|
+
if (!item.children) continue;
|
|
88
|
+
for (const child of item.children) {
|
|
89
|
+
if (under(route, child.href.replace(/^#/, ''))) {
|
|
90
|
+
return { section: entry.id, item: item.id, child: child.id };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Rule 3: among items whose href path matches, the one with the longest href wins.
|
|
97
|
+
let best = null;
|
|
98
|
+
let bestHrefLength = -1;
|
|
99
|
+
for (const entry of tree) {
|
|
100
|
+
for (const item of entry.items) {
|
|
101
|
+
const path = item.href.replace(/^#/, '');
|
|
102
|
+
if (path !== '' && under(route, path) && item.href.length > bestHrefLength) {
|
|
103
|
+
best = { section: entry.id, item: item.id };
|
|
104
|
+
bestHrefLength = item.href.length;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (best) {
|
|
109
|
+
return best;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Rule 4: fall back to currentPart and look up the item by id.
|
|
113
|
+
const id = currentPart(route, known);
|
|
114
|
+
for (const entry of tree) {
|
|
115
|
+
for (const item of entry.items) {
|
|
116
|
+
if (item.id === id) {
|
|
117
|
+
return { section: entry.id, item: id };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A screen that is a section itself (Appearance, Privacy) and not one of its items: the section is open, no item.
|
|
123
|
+
if (tree.some((entry) => entry.id === id)) {
|
|
124
|
+
return { section: id };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Rule 5: nothing matched.
|
|
128
|
+
return { section: null };
|
|
129
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Tests lib/menu.mjs: which item is open for a route,
|
|
2
|
+
// the two-level tree (menuTree), and which section/item/child is open for a route (currentPlace).
|
|
3
|
+
// Run with `node --test packages/editor/lib/*.test.mjs` (part of npm run check).
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { currentPart, menuTree, currentPlace } from './menu.mjs';
|
|
8
|
+
|
|
9
|
+
const known = {
|
|
10
|
+
lists: ['udtalelser', 'cookies', 'priser'],
|
|
11
|
+
header: ['menu', 'mobilmenu'],
|
|
12
|
+
partScreens: ['privatliv'],
|
|
13
|
+
cookies: 'cookies',
|
|
14
|
+
store: 'indstillinger/appstore',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const treeGroups = [
|
|
18
|
+
{ id: 'home', title: 'Home', href: '#', ids: [] },
|
|
19
|
+
{ id: 'indhold', title: 'Content', ids: ['pages', 'lister', 'videresendelser'] },
|
|
20
|
+
{ id: 'udseende', title: 'Appearance', ids: ['udseende/farver', 'udseende/logo'] },
|
|
21
|
+
];
|
|
22
|
+
const treePartOf = (id) => ({ label: `L ${id}`, href: `#${id === 'videresendelser' ? 'seo/videresendelser' : id}`, count: null });
|
|
23
|
+
const treeChildren = (id) =>
|
|
24
|
+
id === 'videresendelser'
|
|
25
|
+
? [
|
|
26
|
+
{ id: 'alle', label: 'All', href: '#seo/videresendelser/alle' },
|
|
27
|
+
{ id: 'ikke-fundet', label: 'Not found', href: '#seo/videresendelser/ikke-fundet' },
|
|
28
|
+
]
|
|
29
|
+
: id === 'lister'
|
|
30
|
+
? [{ id: 'priser', label: 'Prices', href: '#liste/priser' }]
|
|
31
|
+
: [];
|
|
32
|
+
const tree = menuTree(treeGroups, treePartOf, treeChildren);
|
|
33
|
+
|
|
34
|
+
test('an open collection is its own item', () => {
|
|
35
|
+
assert.equal(currentPart('src/content/pages/om.md', { ...known, open: 'pages' }), 'pages');
|
|
36
|
+
assert.equal(currentPart('liste/blog', { ...known, open: 'blog' }), 'blog');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('a short list is under Lists', () => {
|
|
40
|
+
assert.equal(currentPart('liste/udtalelser', known), 'lister');
|
|
41
|
+
assert.equal(currentPart('ny/udtalelser', known), 'lister');
|
|
42
|
+
assert.equal(currentPart('slettet/priser', known), 'lister');
|
|
43
|
+
assert.equal(currentPart('src/data/prices/a.json', { ...known, open: 'priser' }), 'lister');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the cookie list is under Privacy, whatever the way to it', () => {
|
|
47
|
+
assert.equal(currentPart('liste/cookies', known), 'privatliv');
|
|
48
|
+
assert.equal(currentPart('liste/cookies/cookiescanner', known), 'privatliv');
|
|
49
|
+
assert.equal(currentPart('ny/cookies', known), 'privatliv');
|
|
50
|
+
assert.equal(currentPart('slettet/cookies', known), 'privatliv');
|
|
51
|
+
assert.equal(currentPart('src/data/cookie-list/a.json', { ...known, open: 'cookies' }), 'privatliv');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the menus are under Header', () => {
|
|
55
|
+
assert.equal(currentPart('src/data/menu.json', { ...known, open: 'menu' }), 'header');
|
|
56
|
+
assert.equal(currentPart('liste/mobilmenu', known), 'header');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a screen under a part is that part', () => {
|
|
60
|
+
assert.equal(currentPart('virksomheden', known), 'virksomheden');
|
|
61
|
+
assert.equal(currentPart('virksomheden/betaling/mobilepay', known), 'virksomheden');
|
|
62
|
+
assert.equal(currentPart('blokke', known), 'blokke');
|
|
63
|
+
assert.equal(currentPart('blokke/hero', known), 'blokke');
|
|
64
|
+
assert.equal(currentPart('udseende/farver', known), 'udseende');
|
|
65
|
+
assert.equal(currentPart('seo/videresendelser', known), 'seo');
|
|
66
|
+
assert.equal(currentPart('apps/calendly', known), 'apps');
|
|
67
|
+
assert.equal(currentPart('apps/calendly/indstillinger', known), 'apps');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('Block settings is not under Block templates, and a longer name is not under a shorter one', () => {
|
|
71
|
+
assert.equal(currentPart('blokvalg', known), 'blokvalg');
|
|
72
|
+
assert.equal(currentPart('appsx', known), 'appsx');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the screens with parts, and their parts, are the screen', () => {
|
|
76
|
+
assert.equal(currentPart('privatliv', known), 'privatliv');
|
|
77
|
+
assert.equal(currentPart('privatliv/cookiebanner', known), 'privatliv');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the app store, and its themes, are Find apps', () => {
|
|
81
|
+
assert.equal(currentPart('indstillinger/appstore', known), 'appstore');
|
|
82
|
+
assert.equal(currentPart('indstillinger/appstore/temaer', known), 'appstore');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('a screen of its own is itself, and the start screen is none', () => {
|
|
86
|
+
assert.equal(currentPart('avanceret', known), 'avanceret');
|
|
87
|
+
assert.equal(currentPart('licenser', known), 'licenser');
|
|
88
|
+
assert.equal(currentPart('', known), '');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('menuTree keeps the groups in the order written', () => {
|
|
92
|
+
assert.deepEqual(
|
|
93
|
+
tree.map((e) => e.id),
|
|
94
|
+
['home', 'indhold', 'udseende'],
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('menuTree copies href only when the group has it', () => {
|
|
99
|
+
assert.equal(tree[0].href, '#');
|
|
100
|
+
assert.deepEqual(tree[0].items, []);
|
|
101
|
+
assert.equal('href' in tree[1], false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('menuTree gives the items in the order written', () => {
|
|
105
|
+
assert.deepEqual(
|
|
106
|
+
tree[1].items.map((i) => i.id),
|
|
107
|
+
['pages', 'lister', 'videresendelser'],
|
|
108
|
+
);
|
|
109
|
+
assert.equal(tree[1].items[0].label, 'L pages');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('menuTree adds children only when there are some', () => {
|
|
113
|
+
assert.equal(tree[1].items[2].children.length, 2);
|
|
114
|
+
assert.equal('children' in tree[1].items[0], false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('currentPlace on the start screen is no section', () => {
|
|
118
|
+
assert.deepEqual(currentPlace('', tree, known), { section: null });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('currentPlace finds a child', () => {
|
|
122
|
+
assert.deepEqual(currentPlace('seo/videresendelser/alle', tree, known), { section: 'indhold', item: 'videresendelser', child: 'alle' });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('currentPlace finds a list as a child of Lists', () => {
|
|
126
|
+
assert.deepEqual(currentPlace('liste/priser', tree, known), { section: 'indhold', item: 'lister', child: 'priser' });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('currentPlace finds an item by its href', () => {
|
|
130
|
+
assert.deepEqual(currentPlace('seo/videresendelser', tree, known), { section: 'indhold', item: 'videresendelser' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('currentPlace finds a screen under an item', () => {
|
|
134
|
+
assert.deepEqual(currentPlace('udseende/farver/tekst', tree, known), { section: 'udseende', item: 'udseende/farver' });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('currentPlace falls back to currentPart', () => {
|
|
138
|
+
assert.deepEqual(currentPlace('rediger/pages/app-store', tree, { ...known, open: 'pages' }), { section: 'indhold', item: 'pages' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('currentPlace gives no section for an unknown route', () => {
|
|
142
|
+
assert.deepEqual(currentPlace('ukendt', tree, known), { section: null });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('currentPlace gives the section of a screen that is a section itself', () => {
|
|
146
|
+
assert.deepEqual(currentPlace('udseende', tree, known), { section: 'udseende' });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// The menu as defaults/editor.ts `groups` writes it, with the hrefs the editor gives.
|
|
150
|
+
const menuGroups = [
|
|
151
|
+
{ id: 'home', title: 'Home', href: '#', ids: [] },
|
|
152
|
+
{
|
|
153
|
+
id: 'indhold',
|
|
154
|
+
title: 'Content',
|
|
155
|
+
ids: ['pages', 'blog', 'lister', 'popups', 'kategorier', 'filer', 'interne-links', 'videresendelser'],
|
|
156
|
+
},
|
|
157
|
+
{ id: 'globalt', title: 'Global', ids: ['header', 'footer', 'virksomheden'] },
|
|
158
|
+
{ id: 'apps', title: 'Apps', ids: ['apps', 'appstore'] },
|
|
159
|
+
{ id: 'blokke', title: 'Blocks', ids: ['blokke', 'blokvalg'] },
|
|
160
|
+
{ id: 'udseende', title: 'Appearance', ids: ['farver', 'logo'] },
|
|
161
|
+
{ id: 'privatliv', title: 'Privacy', ids: ['cookiebanner', 'cookies'] },
|
|
162
|
+
{ id: 'indstillinger', title: 'Settings', ids: ['avanceret'] },
|
|
163
|
+
];
|
|
164
|
+
const hrefs = {
|
|
165
|
+
pages: '#liste/pages',
|
|
166
|
+
blog: '#liste/blog',
|
|
167
|
+
lister: '#lister',
|
|
168
|
+
popups: '#liste/popups',
|
|
169
|
+
kategorier: '#liste/kategorier',
|
|
170
|
+
filer: '#filer',
|
|
171
|
+
'interne-links': '#seo/interne-links',
|
|
172
|
+
videresendelser: '#seo/videresendelser',
|
|
173
|
+
header: '#header',
|
|
174
|
+
footer: '#src/data/footer.json',
|
|
175
|
+
virksomheden: '#virksomheden',
|
|
176
|
+
apps: '#apps',
|
|
177
|
+
appstore: '#indstillinger/appstore',
|
|
178
|
+
blokke: '#blokke',
|
|
179
|
+
blokvalg: '#blokvalg',
|
|
180
|
+
farver: '#udseende/farver',
|
|
181
|
+
logo: '#udseende/logo',
|
|
182
|
+
cookiebanner: '#privatliv/cookiebanner',
|
|
183
|
+
cookies: '#liste/cookies',
|
|
184
|
+
avanceret: '#avanceret',
|
|
185
|
+
};
|
|
186
|
+
const menu = menuTree(
|
|
187
|
+
menuGroups,
|
|
188
|
+
(id) => ({ label: id, href: hrefs[id], count: null }),
|
|
189
|
+
(id) =>
|
|
190
|
+
id === 'videresendelser'
|
|
191
|
+
? [
|
|
192
|
+
{ id: 'alle', label: 'All', href: '#seo/videresendelser/alle' },
|
|
193
|
+
{ id: 'ikke-fundet', label: 'Not found', href: '#seo/videresendelser/ikke-fundet' },
|
|
194
|
+
]
|
|
195
|
+
: id === 'apps'
|
|
196
|
+
? [{ id: 'calendly', label: 'Calendly', href: '#apps/calendly' }]
|
|
197
|
+
: [],
|
|
198
|
+
);
|
|
199
|
+
const menuKnown = { ...known, store: 'indstillinger/appstore' };
|
|
200
|
+
const allIds = menu.flatMap((e) => e.items.flatMap((i) => [i.id, ...(i.children ?? []).map((c) => c.id)]));
|
|
201
|
+
const allHrefs = menu.flatMap((e) => e.items.flatMap((i) => [i.href, ...(i.children ?? []).map((c) => c.href)]));
|
|
202
|
+
|
|
203
|
+
test('the menu has the eight sections in order', () => {
|
|
204
|
+
assert.deepEqual(
|
|
205
|
+
menu.map((e) => e.id),
|
|
206
|
+
['home', 'indhold', 'globalt', 'apps', 'blokke', 'udseende', 'privatliv', 'indstillinger'],
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('Content holds Internal links and Redirects after the files, in order', () => {
|
|
211
|
+
assert.deepEqual(
|
|
212
|
+
menu[1].items.map((i) => i.id),
|
|
213
|
+
['pages', 'blog', 'lister', 'popups', 'kategorier', 'filer', 'interne-links', 'videresendelser'],
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('Redirects has All and Not found as children', () => {
|
|
218
|
+
const redirects = menu[1].items.find((i) => i.id === 'videresendelser');
|
|
219
|
+
assert.deepEqual(
|
|
220
|
+
redirects.children.map((c) => c.href),
|
|
221
|
+
['#seo/videresendelser/alle', '#seo/videresendelser/ikke-fundet'],
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('Find apps is the first item under Apps, then the apps installed, and Advanced the only one under Settings', () => {
|
|
226
|
+
assert.deepEqual(
|
|
227
|
+
menu[3].items.map((i) => i.id),
|
|
228
|
+
['appstore', 'calendly'],
|
|
229
|
+
);
|
|
230
|
+
assert.deepEqual(
|
|
231
|
+
menu[7].items.map((i) => i.id),
|
|
232
|
+
['avanceret'],
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('Analytics, SEO, the SEO rating and Licenses are not in the menu', () => {
|
|
237
|
+
for (const gone of ['maaling', 'seo', 'vurdering', 'licenser']) {
|
|
238
|
+
assert.equal(allIds.includes(gone), false);
|
|
239
|
+
}
|
|
240
|
+
for (const gone of ['#maaling', '#seo', '#seo/vurdering', '#licenser']) {
|
|
241
|
+
assert.equal(allHrefs.includes(gone), false);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('currentPlace of a page being edited, a list, the SEO rating, an Appearance screen, an app, Find apps and Privacy', () => {
|
|
246
|
+
assert.deepEqual(currentPlace('liste/pages/app-store', menu, menuKnown), { section: 'indhold', item: 'pages' });
|
|
247
|
+
assert.deepEqual(currentPlace('src/content/pages/om.md', menu, { ...menuKnown, open: 'pages' }), { section: 'indhold', item: 'pages' });
|
|
248
|
+
assert.deepEqual(currentPlace('seo/videresendelser/alle', menu, menuKnown), {
|
|
249
|
+
section: 'indhold',
|
|
250
|
+
item: 'videresendelser',
|
|
251
|
+
child: 'alle',
|
|
252
|
+
});
|
|
253
|
+
assert.deepEqual(currentPlace('seo/interne-links', menu, menuKnown), { section: 'indhold', item: 'interne-links' });
|
|
254
|
+
assert.deepEqual(currentPlace('seo/vurdering', menu, menuKnown), { section: null });
|
|
255
|
+
assert.deepEqual(currentPlace('udseende/farver', menu, menuKnown), { section: 'udseende', item: 'farver' });
|
|
256
|
+
assert.deepEqual(currentPlace('apps/calendly', menu, menuKnown), { section: 'apps', item: 'calendly' });
|
|
257
|
+
assert.deepEqual(currentPlace('apps/mailchimp', menu, menuKnown), { section: 'apps' });
|
|
258
|
+
assert.deepEqual(currentPlace('indstillinger/appstore/temaer', menu, menuKnown), { section: 'apps', item: 'appstore' });
|
|
259
|
+
assert.deepEqual(currentPlace('privatliv/cookiebanner', menu, menuKnown), { section: 'privatliv', item: 'cookiebanner' });
|
|
260
|
+
assert.deepEqual(currentPlace('src/data/cookie-list/a.json', menu, { ...menuKnown, open: 'cookies' }), { section: 'privatliv' });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const appsGroups = [{ id: 'apps', title: 'Apps', ids: ['apps', 'appstore'] }];
|
|
264
|
+
const appsPartOf = (id) => ({ label: `L ${id}`, href: id === 'appstore' ? '#indstillinger/appstore' : '#apps', count: null });
|
|
265
|
+
const appsOf = (list) => (id) => (id === 'apps' ? list.map((a) => ({ id: a, label: `A ${a}`, href: `#apps/${a}` })) : []);
|
|
266
|
+
|
|
267
|
+
test('the Apps section lists Appstore, then each installed app, and not the apps list', () => {
|
|
268
|
+
const [section] = menuTree(appsGroups, appsPartOf, appsOf(['mailchimp', 'mobilepay']));
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
section.items.map((i) => i.id),
|
|
271
|
+
['appstore', 'mailchimp', 'mobilepay'],
|
|
272
|
+
);
|
|
273
|
+
assert.equal(section.items[1].href, '#apps/mailchimp');
|
|
274
|
+
assert.equal(section.items[1].count, null);
|
|
275
|
+
assert.ok(section.items.every((i) => i.children === undefined));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('the Apps section with no apps installed lists Appstore only', () => {
|
|
279
|
+
const [section] = menuTree(appsGroups, appsPartOf, appsOf([]));
|
|
280
|
+
assert.deepEqual(
|
|
281
|
+
section.items.map((i) => i.id),
|
|
282
|
+
['appstore'],
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('an app screen selects its row; the apps list and Appstore keep the Apps section', () => {
|
|
287
|
+
const appsTree = menuTree(appsGroups, appsPartOf, appsOf(['mailchimp', 'mobilepay']));
|
|
288
|
+
assert.deepEqual(currentPlace('apps/mailchimp', appsTree, known), { section: 'apps', item: 'mailchimp' });
|
|
289
|
+
assert.deepEqual(currentPlace('apps', appsTree, known), { section: 'apps' });
|
|
290
|
+
assert.deepEqual(currentPlace('indstillinger/appstore', appsTree, known), { section: 'apps', item: 'appstore' });
|
|
291
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Every bold word in a text the site shows (Markdown's **word**, HTML's <strong>) gets the class "emphasize", which a
|
|
2
|
+
// theme may style in its global.css; the standard theme leaves it bold. Exposes EMPHASIZE and rehypeEmphasize (a rehype
|
|
3
|
+
// plugin, in the site's pipeline in astro.mjs, in inline.ts and in the editor's in markdown.ts). No dependencies.
|
|
4
|
+
|
|
5
|
+
/** The class of a bold word, on the site and in the editor (strongAttr in src/markdown.mjs). */
|
|
6
|
+
export const EMPHASIZE = 'emphasize';
|
|
7
|
+
|
|
8
|
+
/** The rehype plugin: the class on every <strong>, its own classes kept. */
|
|
9
|
+
export function rehypeEmphasize() {
|
|
10
|
+
return (tree) => {
|
|
11
|
+
const walk = (node) => {
|
|
12
|
+
if (node.type === 'element' && node.tagName === 'strong') {
|
|
13
|
+
const names = [].concat(node.properties.className ?? []);
|
|
14
|
+
if (!names.includes(EMPHASIZE)) node.properties.className = [...names, EMPHASIZE];
|
|
15
|
+
}
|
|
16
|
+
node.children?.forEach(walk);
|
|
17
|
+
};
|
|
18
|
+
walk(tree);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Tests for lib/rehype-emphasize.mjs: every bold word in a text the site shows gets the class "emphasize".
|
|
2
|
+
// Run: node --test packages/editor/lib/*.test.mjs packages/editor/src/*.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { createMarkdownProcessor } from '@astrojs/markdown-remark';
|
|
6
|
+
import { EMPHASIZE, rehypeEmphasize } from './rehype-emphasize.mjs';
|
|
7
|
+
|
|
8
|
+
const strong = (properties = {}) => ({ type: 'element', tagName: 'strong', properties, children: [{ type: 'text', value: 'saving' }] });
|
|
9
|
+
const root = (...children) => ({ type: 'root', children: [{ type: 'element', tagName: 'p', properties: {}, children }] });
|
|
10
|
+
|
|
11
|
+
test('the class is emphasize', () => {
|
|
12
|
+
assert.equal(EMPHASIZE, 'emphasize');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('a strong gets the class', () => {
|
|
16
|
+
const tree = root(strong());
|
|
17
|
+
rehypeEmphasize()(tree);
|
|
18
|
+
assert.deepEqual(tree.children[0].children[0].properties.className, ['emphasize']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('a strong keeps its own classes, and gets the class once', () => {
|
|
22
|
+
const tree = root(strong({ className: ['color-accent'] }), strong({ className: ['emphasize'] }));
|
|
23
|
+
rehypeEmphasize()(tree);
|
|
24
|
+
assert.deepEqual(tree.children[0].children[0].properties.className, ['color-accent', 'emphasize']);
|
|
25
|
+
assert.deepEqual(tree.children[0].children[1].properties.className, ['emphasize']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('other elements are left as they are', () => {
|
|
29
|
+
const em = { type: 'element', tagName: 'em', properties: {}, children: [] };
|
|
30
|
+
const tree = root(em);
|
|
31
|
+
rehypeEmphasize()(tree);
|
|
32
|
+
assert.equal(em.properties.className, undefined);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('in Astro\'s pipeline, **word** is drawn as <strong class="emphasize">', async () => {
|
|
36
|
+
const processor = await createMarkdownProcessor({ rehypePlugins: [rehypeEmphasize] });
|
|
37
|
+
const { code } = await processor.render('How **saving** works');
|
|
38
|
+
assert.equal(code.trim(), '<p>How <strong class="emphasize">saving</strong> works</p>');
|
|
39
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} order the sections, from the top of the page down
|
|
3
|
+
* @param {Record<string, number | undefined>} tops each section's top, from the top of the window
|
|
4
|
+
* @param {number} line
|
|
5
|
+
* @returns {string | undefined} the last of `order` whose top is above `line`; with all of them below it, the first one that is drawn; a section with no top (`undefined`: not drawn) is skipped.
|
|
6
|
+
*/
|
|
7
|
+
export function sectionInView(order: string[], tops: Record<string, number | undefined>, line: number): string | undefined;
|
|
8
|
+
export function spyLine(height: number, atBottom: boolean): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Which of a form's sections is in view while the page scrolls (EntryForm.tsx marks it in the sections list): pure, so it
|
|
2
|
+
// is tested (section-in-view.test.mjs). Exports sectionInView and spyLine. No dependencies.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string[]} order the sections, from the top of the page down
|
|
6
|
+
* @param {Record<string, number | undefined>} tops each section's top, from the top of the window
|
|
7
|
+
* @param {number} line
|
|
8
|
+
* @returns {string | undefined} the last of `order` whose top is above `line`; with all of them below it, the first one that is drawn; a section with no top (`undefined`: not drawn) is skipped.
|
|
9
|
+
*/
|
|
10
|
+
export function sectionInView(order, tops, line) {
|
|
11
|
+
/** @type {string | undefined} */
|
|
12
|
+
let here;
|
|
13
|
+
for (const id of order) {
|
|
14
|
+
const top = tops[id];
|
|
15
|
+
if (top !== undefined && (here === undefined || top <= line)) here = id;
|
|
16
|
+
}
|
|
17
|
+
return here;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* where a section's top must be to count as in view: a third of the window's height; at the bottom of the page, 96px above the window's bottom, so the last section can be marked.
|
|
22
|
+
* @param {number} height the window's height
|
|
23
|
+
* @param {boolean} atBottom the page is scrolled to its end
|
|
24
|
+
*/
|
|
25
|
+
export const spyLine = (height, atBottom) => (atBottom ? height - 96 : height / 3);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tests of sectionInView and spyLine (section-in-view.mjs): which of a form's sections is marked while the page scrolls.
|
|
2
|
+
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { sectionInView, spyLine } from './section-in-view.mjs';
|
|
6
|
+
|
|
7
|
+
const order = ['content', 'status', 'seo'];
|
|
8
|
+
|
|
9
|
+
test('the last section whose top is above the line is in view', () => {
|
|
10
|
+
assert.equal(sectionInView(order, { content: -500, status: 100, seo: 900 }, 300), 'status');
|
|
11
|
+
assert.equal(sectionInView(order, { content: -500, status: -100, seo: 200 }, 300), 'seo');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('with every top below the line, the first section with a top is in view', () => {
|
|
15
|
+
assert.equal(sectionInView(order, { content: 400, status: 600, seo: 900 }, 300), 'content');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('a section without a top (not drawn) is skipped', () => {
|
|
19
|
+
assert.equal(sectionInView(order, { status: 500, seo: 900 }, 300), 'status');
|
|
20
|
+
assert.equal(sectionInView(order, { content: -50, status: undefined, seo: 100 }, 300), 'seo');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('no tops at all: nothing is in view', () => {
|
|
24
|
+
assert.equal(sectionInView(order, {}, 300), undefined);
|
|
25
|
+
assert.equal(sectionInView([], { content: 0 }, 300), undefined);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('the order given decides, not the tops', () => {
|
|
29
|
+
assert.equal(sectionInView(['seo', 'content'], { content: -10, seo: 20 }, 300), 'content');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('the line is a third of the screen, or near its bottom at the bottom of the page', () => {
|
|
33
|
+
assert.equal(spyLine(900, false), 300);
|
|
34
|
+
assert.equal(spyLine(900, true), 804);
|
|
35
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The colours, by the theme colour they are: --block-surface, --block-tint and --block-accent. */
|
|
2
|
+
export const SECTION_COLORS: string[];
|
|
3
|
+
export function sectionMarker(color: string): string;
|
|
4
|
+
export function markerColor(html: string): string | null;
|
|
5
|
+
export function isLabelLine(html: string): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The colour of a section of a page or blog post (the *Sektionsfarve* button on an H2): in the Markdown a line of HTML right before
|
|
2
|
+
// the H2 (or before the label above it), <div data-section="tint"></div>. Only the theme's own colours, by name. Exposes
|
|
3
|
+
// SECTION_COLORS, sectionMarker, markerColor and isLabelLine. Depends on lib/eyebrow.mjs; no React or browser.
|
|
4
|
+
import { eyebrowText } from './eyebrow.mjs';
|
|
5
|
+
|
|
6
|
+
/** The colours, by the theme colour they are: --block-surface, --block-tint and --block-accent. */
|
|
7
|
+
export const SECTION_COLORS = ['surface', 'tint', 'accent'];
|
|
8
|
+
|
|
9
|
+
const MARKER = new RegExp(`^<div data-section="(${SECTION_COLORS.join('|')})"></div>$`);
|
|
10
|
+
|
|
11
|
+
/** The marker line of a colour. @param {string} color */
|
|
12
|
+
export const sectionMarker = (color) => `<div data-section="${color}"></div>`;
|
|
13
|
+
|
|
14
|
+
/** The colour a piece of HTML is the marker of, or null (also for a colour the theme has not). @param {string} html @returns {string | null} */
|
|
15
|
+
export const markerColor = (html) => String(html).trim().match(MARKER)?.[1] ?? null;
|
|
16
|
+
|
|
17
|
+
/** Whether a piece of HTML is the line of a label above a heading (lib/eyebrow.mjs). @param {string} html */
|
|
18
|
+
export const isLabelLine = (html) => eyebrowText(html) !== null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tests for lib/section-marker.mjs: the marker line before an H2 that gives its section a colour.
|
|
2
|
+
// Run: node --test packages/editor/lib/section-marker.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { SECTION_COLORS, isLabelLine, markerColor, sectionMarker } from './section-marker.mjs';
|
|
6
|
+
|
|
7
|
+
test('the colours are the theme’s three', () => assert.deepEqual(SECTION_COLORS, ['surface', 'tint', 'accent']));
|
|
8
|
+
test('writes the marker', () => assert.equal(sectionMarker('tint'), '<div data-section="tint"></div>'));
|
|
9
|
+
test('reads it back', () => {
|
|
10
|
+
for (const c of SECTION_COLORS) assert.equal(markerColor(sectionMarker(c)), c);
|
|
11
|
+
});
|
|
12
|
+
test('reads it with space around', () => assert.equal(markerColor(' <div data-section="accent"></div>\n'), 'accent'));
|
|
13
|
+
test('an unknown colour is no marker', () => assert.equal(markerColor('<div data-section="red"></div>'), null));
|
|
14
|
+
test('other HTML is no marker', () => {
|
|
15
|
+
assert.equal(markerColor('<div data-section="tint">x</div>'), null);
|
|
16
|
+
assert.equal(markerColor('<div data-block="hero"></div>'), null);
|
|
17
|
+
assert.equal(markerColor(''), null);
|
|
18
|
+
});
|
|
19
|
+
test('a label line', () => {
|
|
20
|
+
assert.equal(isLabelLine('<p class="block-eyebrow">Step by step</p>'), true);
|
|
21
|
+
assert.equal(isLabelLine('<p class="intro">Hi</p>'), false);
|
|
22
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The kind of a part, from its ProseMirror node name and heading level; the label is the eyebrow line before a heading. */
|
|
2
|
+
export function partKind(typeName: any, level: any): "h1" | "h2" | "other" | "label";
|
|
3
|
+
/**
|
|
4
|
+
* The sections of a text: each H2 (with a label directly before it) and every part after it up to the next H2, the next H1
|
|
5
|
+
* or the end. `parts` is the top-level parts in order; the result has the index of each section's first and last part.
|
|
6
|
+
*/
|
|
7
|
+
export function sectionRanges(parts: any): any[];
|
|
8
|
+
/** Where a part is in its section: 'start', 'middle', 'end', or 'only' for a section of one part. */
|
|
9
|
+
export function sectionPart(index: any, { from, to }: {
|
|
10
|
+
from: any;
|
|
11
|
+
to: any;
|
|
12
|
+
}): "only" | "start" | "end" | "middle";
|