@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613
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 +31 -0
- package/astro.mjs +2 -1
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +59 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +9 -8
- package/scripts/check-content.mjs +12 -2
- package/scripts/check-language.mjs +0 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/editor-screens.mjs +3 -3
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +79 -78
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +39 -24
- package/src/BlockDialog.tsx +39 -18
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +10 -20
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +3 -3
- package/src/FooterSettings.tsx +2 -2
- package/src/FormFrame.tsx +4 -3
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +75 -82
- package/src/Licenses.tsx +8 -8
- package/src/ListFab.tsx +13 -0
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +11 -20
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +14 -22
- package/src/Screen.tsx +16 -6
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +6 -8
- package/src/ThemeStore.tsx +37 -16
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +7 -8
- package/src/checkSite.ts +86 -20
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +59 -38
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/licenseData.ts +3 -3
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuChildren.ts +5 -5
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +102 -21
- package/src/movedRoutes.test.mjs +128 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/schema.ts +2 -2
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +36 -31
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +33 -22
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/Base.astro +2 -2
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
package/lib/section-ranges.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The kind of a part, from its ProseMirror node name and heading level; the label is the eyebrow line before a heading. */
|
|
2
2
|
export function partKind(typeName: any, level: any): "h1" | "h2" | "other" | "label";
|
|
3
3
|
/**
|
|
4
|
-
* The sections of a text: each H2 (with a label directly before it) and every part after it up to the next
|
|
4
|
+
* The sections of a text: each H1 or H2 (with a label directly before it) and every part after it up to the next H1 or H2
|
|
5
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
6
|
*/
|
|
7
7
|
export function sectionRanges(parts: any): any[];
|
package/lib/section-ranges.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Which top-level parts of a page's or blog post's text form H2
|
|
1
|
+
// Which top-level parts of a page's or blog post's text form sections (an H1 or an H2 starts one), for the editor's dotted frame.
|
|
2
2
|
// Exposes sectionRanges, sectionPart and partKind. Pure, no dependencies (used by src/sectionFrame.ts).
|
|
3
3
|
|
|
4
4
|
/** The kind of a part, from its ProseMirror node name and heading level; the label is the eyebrow line before a heading. */
|
|
@@ -7,8 +7,10 @@ export function partKind(typeName, level) {
|
|
|
7
7
|
return typeName === 'eyebrow' ? 'label' : 'other';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const startsSection = (kind) => kind === 'h1' || kind === 'h2';
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
|
-
* The sections of a text: each H2 (with a label directly before it) and every part after it up to the next
|
|
13
|
+
* The sections of a text: each H1 or H2 (with a label directly before it) and every part after it up to the next H1 or H2
|
|
12
14
|
* or the end. `parts` is the top-level parts in order; the result has the index of each section's first and last part.
|
|
13
15
|
*/
|
|
14
16
|
export function sectionRanges(parts) {
|
|
@@ -19,18 +21,10 @@ export function sectionRanges(parts) {
|
|
|
19
21
|
from = -1;
|
|
20
22
|
};
|
|
21
23
|
parts.forEach((part, i) => {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
from = i;
|
|
27
|
-
} else if (next === 'h1') close(i - 1);
|
|
28
|
-
} else if (part.kind === 'h2') {
|
|
29
|
-
if (parts[i - 1]?.kind !== 'label') {
|
|
30
|
-
close(i - 1);
|
|
31
|
-
from = i;
|
|
32
|
-
}
|
|
33
|
-
} else if (part.kind === 'h1') close(i - 1);
|
|
24
|
+
const starts = part.kind === 'label' ? startsSection(parts[i + 1]?.kind) : startsSection(part.kind) && parts[i - 1]?.kind !== 'label';
|
|
25
|
+
if (!starts) return;
|
|
26
|
+
close(i - 1);
|
|
27
|
+
from = i;
|
|
34
28
|
});
|
|
35
29
|
close(parts.length - 1);
|
|
36
30
|
return ranges;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Tests lib/section-ranges.mjs: which top-level parts of a text form H2
|
|
1
|
+
// Tests lib/section-ranges.mjs: which top-level parts of a text form sections, H1 or H2 (the editor's dotted frame).
|
|
2
2
|
// Run with `node --test packages/editor/lib/*.test.mjs` (part of npm run check).
|
|
3
3
|
import { test } from 'node:test';
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
@@ -15,13 +15,26 @@ const ranges = (text) =>
|
|
|
15
15
|
);
|
|
16
16
|
|
|
17
17
|
test('no parts, no sections', () => assert.deepEqual(ranges(''), []));
|
|
18
|
-
test('no
|
|
18
|
+
test('no heading, no sections', () => assert.deepEqual(ranges('o o'), []));
|
|
19
|
+
test('an H1 starts a section', () => assert.deepEqual(ranges('1 o o'), [{ from: 0, to: 2 }]));
|
|
20
|
+
test('an H1 then an H2 are a section each', () =>
|
|
21
|
+
assert.deepEqual(ranges('1 o 2 o'), [
|
|
22
|
+
{ from: 0, to: 1 },
|
|
23
|
+
{ from: 2, to: 3 },
|
|
24
|
+
]));
|
|
25
|
+
test('a label before an H1 starts the section', () => assert.deepEqual(ranges('l 1 o'), [{ from: 0, to: 2 }]));
|
|
26
|
+
test('consecutive H1s are a section each', () =>
|
|
27
|
+
assert.deepEqual(ranges('o 1 1'), [
|
|
28
|
+
{ from: 1, to: 1 },
|
|
29
|
+
{ from: 2, to: 2 },
|
|
30
|
+
]));
|
|
19
31
|
test('an H2 at the start runs to the end', () => assert.deepEqual(ranges('2 o o'), [{ from: 0, to: 2 }]));
|
|
20
32
|
test('text before the first H2 is outside', () => assert.deepEqual(ranges('o 2 o'), [{ from: 1, to: 2 }]));
|
|
21
33
|
test('an H2 as the last part is a section of one', () => assert.deepEqual(ranges('o o 2'), [{ from: 2, to: 2 }]));
|
|
22
|
-
test('an H1 ends
|
|
34
|
+
test('an H1 ends the section above it and starts its own', () =>
|
|
23
35
|
assert.deepEqual(ranges('2 o 1 o 2 o'), [
|
|
24
36
|
{ from: 0, to: 1 },
|
|
37
|
+
{ from: 2, to: 3 },
|
|
25
38
|
{ from: 4, to: 5 },
|
|
26
39
|
]));
|
|
27
40
|
test('consecutive H2s are a section each', () =>
|
|
@@ -36,8 +49,16 @@ test('a label before an H2 ends the section above it', () =>
|
|
|
36
49
|
{ from: 0, to: 1 },
|
|
37
50
|
{ from: 2, to: 4 },
|
|
38
51
|
]));
|
|
39
|
-
test('a label before an H1 belongs to the H1', () =>
|
|
40
|
-
|
|
52
|
+
test('a label before an H1 belongs to the H1', () =>
|
|
53
|
+
assert.deepEqual(ranges('2 o l 1 o'), [
|
|
54
|
+
{ from: 0, to: 1 },
|
|
55
|
+
{ from: 2, to: 4 },
|
|
56
|
+
]));
|
|
57
|
+
test('a label at the start before an H1 starts its section', () =>
|
|
58
|
+
assert.deepEqual(ranges('l 1 2 o'), [
|
|
59
|
+
{ from: 0, to: 1 },
|
|
60
|
+
{ from: 2, to: 3 },
|
|
61
|
+
]));
|
|
41
62
|
test('a label not before a heading is an ordinary part', () => {
|
|
42
63
|
assert.deepEqual(ranges('2 l o'), [{ from: 0, to: 2 }]);
|
|
43
64
|
assert.deepEqual(ranges('l o 2'), [{ from: 2, to: 2 }]);
|
package/lib/storeEntries.d.mts
CHANGED
|
@@ -13,6 +13,8 @@ export function validTheme(x: any, api: number): boolean;
|
|
|
13
13
|
export function isText(v: unknown): v is string;
|
|
14
14
|
export function isPath(v: unknown): boolean;
|
|
15
15
|
export const ITEM_ID: RegExp;
|
|
16
|
-
export const
|
|
16
|
+
export const HANDLE: RegExp;
|
|
17
|
+
export const LEGACY: RegExp;
|
|
17
18
|
export const STANDARD: "saturn";
|
|
18
19
|
export const RECORD: "store.json";
|
|
20
|
+
export function strayStoreId(name: string, kind: "app" | "theme"): boolean;
|
package/lib/storeEntries.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure validation logic for store apps and themes.
|
|
3
|
-
* Exposes isText, isPath, ITEM_ID,
|
|
3
|
+
* Exposes isText, isPath, ITEM_ID, HANDLE, LEGACY, STANDARD, RECORD, validApp, validTheme, strayStoreId.
|
|
4
4
|
* No external dependencies.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -16,14 +16,28 @@ export const isText = (v) => typeof v === 'string' && v.trim() !== '';
|
|
|
16
16
|
*/
|
|
17
17
|
export const isPath = (v) => isText(v) && v.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// A permanent id: the kind, then 16 lowercase hex characters
|
|
20
|
+
export const ITEM_ID = /^(app|theme)_[0-9a-f]{16}$/;
|
|
21
|
+
// An item's address in the store: publisher/name
|
|
22
|
+
export const HANDLE = /^[a-z0-9][a-z0-9-]{0,39}\/[a-z0-9][a-z0-9-]{0,39}$/;
|
|
23
|
+
// An item's folder name before it had a permanent id
|
|
24
|
+
export const LEGACY = /^[a-z0-9][a-z0-9-]*$/;
|
|
22
25
|
// The standard theme, which every site has
|
|
23
26
|
export const STANDARD = 'saturn';
|
|
24
27
|
// The note in an installed folder of where it came from
|
|
25
28
|
export const RECORD = 'store.json';
|
|
26
29
|
|
|
30
|
+
const hasId = (x, kind) => ITEM_ID.test(x?.id ?? '') && x.id.startsWith(`${kind}_`);
|
|
31
|
+
const hasHandle = (x) => HANDLE.test(x?.handle ?? '') && (x.legacy === undefined || LEGACY.test(x.legacy));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when a site's own folder name looks like a store id of this kind (it starts with `app_` or `theme_`) but is not one.
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @param {'app' | 'theme'} kind
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
export const strayStoreId = (name, kind) => /^(app|theme)_/.test(name) && !(ITEM_ID.test(name) && name.startsWith(`${kind}_`));
|
|
40
|
+
|
|
27
41
|
const isAdd = (v) => isText(v?.label) && isText(v?.icon);
|
|
28
42
|
const isGoodFile = (f) => isText(f?.oid) && isPath(f?.path) && f.path !== RECORD;
|
|
29
43
|
|
|
@@ -34,8 +48,8 @@ const isGoodFile = (f) => isText(f?.oid) && isPath(f?.path) && f.path !== RECORD
|
|
|
34
48
|
*/
|
|
35
49
|
export function validApp(x, api) {
|
|
36
50
|
return (
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
hasId(x, 'app') &&
|
|
52
|
+
hasHandle(x) &&
|
|
39
53
|
isText(x.version) &&
|
|
40
54
|
x.api === api &&
|
|
41
55
|
[x.label, x.publisher, x.icon, x.hint].every(isText) &&
|
|
@@ -53,8 +67,8 @@ export function validApp(x, api) {
|
|
|
53
67
|
*/
|
|
54
68
|
export function validTheme(x, api) {
|
|
55
69
|
return (
|
|
56
|
-
|
|
57
|
-
x
|
|
70
|
+
hasId(x, 'theme') &&
|
|
71
|
+
hasHandle(x) &&
|
|
58
72
|
x.api === api &&
|
|
59
73
|
[x.version, x.name, x.author, x.icon].every(isText) &&
|
|
60
74
|
Array.isArray(x.files) &&
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for store entry validation logic (validApp, validTheme, isPath).
|
|
2
|
+
* Tests for store entry validation logic (validApp, validTheme, isPath, strayStoreId).
|
|
3
3
|
* Run with `node --test packages/editor/lib/*.test.mjs` (part of npm run check).
|
|
4
4
|
*/
|
|
5
5
|
import { test } from 'node:test';
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
|
-
import { validApp, validTheme, isPath } from './storeEntries.mjs';
|
|
7
|
+
import { validApp, validTheme, isPath, strayStoreId } from './storeEntries.mjs';
|
|
8
8
|
|
|
9
9
|
const theme = () => ({
|
|
10
|
-
id: '
|
|
10
|
+
id: 'theme_fa395ad84690acbd',
|
|
11
|
+
handle: 'morpheme/venus',
|
|
12
|
+
legacy: 'venus',
|
|
11
13
|
version: '1.0.0',
|
|
12
14
|
api: 4,
|
|
13
15
|
name: 'Venus',
|
|
@@ -16,7 +18,8 @@ const theme = () => ({
|
|
|
16
18
|
files: [{ path: 'theme.json', oid: 'a'.repeat(40) }],
|
|
17
19
|
});
|
|
18
20
|
const app = () => ({
|
|
19
|
-
id: '
|
|
21
|
+
id: 'app_a030dfba69793996',
|
|
22
|
+
handle: 'morpheme/calendly',
|
|
20
23
|
version: '1.0.0',
|
|
21
24
|
api: 2,
|
|
22
25
|
label: 'Calendly',
|
|
@@ -35,8 +38,30 @@ test('valid theme wrong api', () => {
|
|
|
35
38
|
assert.equal(validTheme({ ...theme(), api: 3 }, 4), false);
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
test('valid theme
|
|
39
|
-
assert.equal(validTheme({ ...theme(), id: '
|
|
41
|
+
test('valid theme old-style id', () => {
|
|
42
|
+
assert.equal(validTheme({ ...theme(), id: 'venus' }, 4), false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('valid theme app id (wrong kind)', () => {
|
|
46
|
+
assert.equal(validTheme({ ...theme(), id: 'app_a030dfba69793996' }, 4), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('valid theme without handle', () => {
|
|
50
|
+
const { handle, ...rest } = theme();
|
|
51
|
+
assert.equal(validTheme(rest, 4), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('valid theme bad handle', () => {
|
|
55
|
+
assert.equal(validTheme({ ...theme(), handle: 'venus' }, 4), false);
|
|
56
|
+
assert.equal(validTheme({ ...theme(), handle: 'Morpheme/venus' }, 4), false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('valid theme keeps legacy, and drops a bad one', () => {
|
|
60
|
+
assert.equal(validTheme(theme(), 4), true);
|
|
61
|
+
assert.equal(theme().legacy, 'venus');
|
|
62
|
+
assert.equal(validTheme({ ...theme(), legacy: 'Venus!' }, 4), false);
|
|
63
|
+
const { legacy, ...rest } = theme();
|
|
64
|
+
assert.equal(validTheme(rest, 4), true);
|
|
40
65
|
});
|
|
41
66
|
|
|
42
67
|
test('valid theme bad path', () => {
|
|
@@ -55,8 +80,26 @@ test('valid app', () => {
|
|
|
55
80
|
assert.equal(validApp(app(), 2), true);
|
|
56
81
|
});
|
|
57
82
|
|
|
58
|
-
test('valid app
|
|
83
|
+
test('valid app old-style id', () => {
|
|
84
|
+
assert.equal(validApp({ ...app(), id: 'calendly' }, 2), false);
|
|
59
85
|
assert.equal(validApp({ ...app(), id: 'butik' }, 2), false);
|
|
86
|
+
assert.equal(validApp({ ...app(), id: 'store' }, 2), false);
|
|
87
|
+
assert.equal(validApp({ ...app(), id: 'udvikler' }, 2), false);
|
|
88
|
+
assert.equal(validApp({ ...app(), id: 'developer' }, 2), false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('valid app theme id (wrong kind)', () => {
|
|
92
|
+
assert.equal(validApp({ ...app(), id: 'theme_fa395ad84690acbd' }, 2), false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('valid app without handle', () => {
|
|
96
|
+
const { handle, ...rest } = app();
|
|
97
|
+
assert.equal(validApp(rest, 2), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('valid app bad id length', () => {
|
|
101
|
+
assert.equal(validApp({ ...app(), id: 'app_a030dfba6979399' }, 2), false);
|
|
102
|
+
assert.equal(validApp({ ...app(), id: 'app_A030DFBA69793996' }, 2), false);
|
|
60
103
|
});
|
|
61
104
|
|
|
62
105
|
test('valid app missing adds kinds', () => {
|
|
@@ -69,3 +112,12 @@ test('isPath cases', () => {
|
|
|
69
112
|
assert.equal(isPath('./a'), false);
|
|
70
113
|
assert.equal(isPath(''), false);
|
|
71
114
|
});
|
|
115
|
+
|
|
116
|
+
test('strayStoreId: a store-looking name that is not a store id of the kind', () => {
|
|
117
|
+
assert.equal(strayStoreId('app_a030dfba69793996', 'app'), false);
|
|
118
|
+
assert.equal(strayStoreId('app_xyz', 'app'), true);
|
|
119
|
+
assert.equal(strayStoreId('theme_fa395ad84690acbd', 'app'), true);
|
|
120
|
+
assert.equal(strayStoreId('theme_xyz', 'theme'), true);
|
|
121
|
+
assert.equal(strayStoreId('calendly', 'app'), false);
|
|
122
|
+
assert.equal(strayStoreId('my_theme', 'theme'), false);
|
|
123
|
+
});
|
package/lib/text-sections.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The text's HTML as runs: { color, html } with the marker removed; color is null for text outside a coloured section.
|
|
2
|
-
* A marker with an unknown colour, or not followed by an H2, is dropped. Without any coloured section, the HTML is
|
|
2
|
+
* A marker with an unknown colour, or not followed by an H1 or H2, is dropped. Without any coloured section, the HTML is
|
|
3
3
|
* returned as it came, in one run.
|
|
4
4
|
* @param {string} html
|
|
5
5
|
* @returns {{ color: string | null, html: string }[]} */
|
|
@@ -7,4 +7,14 @@ export function sectionsOf(html: string): {
|
|
|
7
7
|
color: string | null;
|
|
8
8
|
html: string;
|
|
9
9
|
}[];
|
|
10
|
+
/** The text before its first section starts (a marker, a label or an H1 or H2), and the rest from there. Without a heading
|
|
11
|
+
* the whole text is the intro. For a page whose coloured H1 panel holds the header and this intro (PageLayout.astro).
|
|
12
|
+
* @param {string} html
|
|
13
|
+
* @returns {{ intro: string, rest: string }} */
|
|
14
|
+
export function introOf(html: string): {
|
|
15
|
+
intro: string;
|
|
16
|
+
rest: string;
|
|
17
|
+
};
|
|
18
|
+
export const MARKER: string;
|
|
19
|
+
export const LABEL: string;
|
|
10
20
|
export function sectionClass(color: any): string;
|
package/lib/text-sections.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// A page's or post's rendered text cut into runs for the site: a section with a colour (the marker before its H2,
|
|
2
|
-
// lib/section-marker.mjs) is one run from the marker to the next
|
|
3
|
-
// used by theme/components/RichText.astro
|
|
1
|
+
// A page's or post's rendered text cut into runs for the site: a section with a colour (the marker before its H1 or H2,
|
|
2
|
+
// lib/section-marker.mjs) is one run from the marker to the next H1, H2 or the end. Exposes sectionsOf, introOf,
|
|
3
|
+
// sectionClass and the marker and label patterns (MARKER, LABEL), used by theme/components/RichText.astro, lib/without-h1.mjs
|
|
4
|
+
// and src/pagePreview.tsx. Depends on section-marker.mjs.
|
|
4
5
|
import { SECTION_COLORS } from './section-marker.mjs';
|
|
5
6
|
|
|
6
7
|
// The marker as the site's HTML pipeline writes it (rehype-raw keeps it as written; a space before `>` is tolerated).
|
|
7
|
-
const MARKER = String.raw`<div data-section="([^"]*)"\s*><\/div>\s*`;
|
|
8
|
+
export const MARKER = String.raw`<div data-section="([^"]*)"\s*><\/div>\s*`;
|
|
8
9
|
// A label line (text-menu's <p class="block-eyebrow">) directly before a heading starts the heading's section.
|
|
9
|
-
const LABEL = String.raw`(?:<p class="block-eyebrow">(?:(?!<\/p>)[\s\S])*<\/p>\s*)?`;
|
|
10
|
-
// A marker followed by an H2 (with its label between): where a coloured section starts. Ends just after `<h2`.
|
|
11
|
-
const START = new RegExp(`${MARKER}${LABEL}<
|
|
10
|
+
export const LABEL = String.raw`(?:<p class="block-eyebrow">(?:(?!<\/p>)[\s\S])*<\/p>\s*)?`;
|
|
11
|
+
// A marker followed by an H1 or H2 (with its label between): where a coloured section starts. Ends just after `<h1` or `<h2`.
|
|
12
|
+
const START = new RegExp(`${MARKER}${LABEL}<h[12]\\b`, 'g');
|
|
12
13
|
// Where the next section (or an H1) starts: its marker and label if it has them, else its heading.
|
|
13
14
|
const NEXT = new RegExp(`(?:${MARKER})?${LABEL}<h[12]\\b`);
|
|
14
15
|
const MARKERS = new RegExp(MARKER, 'g');
|
|
@@ -16,7 +17,7 @@ const MARKERS = new RegExp(MARKER, 'g');
|
|
|
16
17
|
const withoutMarkers = (html) => html.replace(MARKERS, '');
|
|
17
18
|
|
|
18
19
|
/** The text's HTML as runs: { color, html } with the marker removed; color is null for text outside a coloured section.
|
|
19
|
-
* A marker with an unknown colour, or not followed by an H2, is dropped. Without any coloured section, the HTML is
|
|
20
|
+
* A marker with an unknown colour, or not followed by an H1 or H2, is dropped. Without any coloured section, the HTML is
|
|
20
21
|
* returned as it came, in one run.
|
|
21
22
|
* @param {string} html
|
|
22
23
|
* @returns {{ color: string | null, html: string }[]} */
|
|
@@ -41,5 +42,14 @@ export function sectionsOf(html) {
|
|
|
41
42
|
return runs;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** The text before its first section starts (a marker, a label or an H1 or H2), and the rest from there. Without a heading
|
|
46
|
+
* the whole text is the intro. For a page whose coloured H1 panel holds the header and this intro (PageLayout.astro).
|
|
47
|
+
* @param {string} html
|
|
48
|
+
* @returns {{ intro: string, rest: string }} */
|
|
49
|
+
export function introOf(html) {
|
|
50
|
+
const at = html.search(NEXT);
|
|
51
|
+
return at < 0 ? { intro: html, rest: '' } : { intro: html.slice(0, at), rest: html.slice(at) };
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/** The classes of a coloured section on the site (blocks.css). */
|
|
45
55
|
export const sectionClass = (color) => `text-section text-section-${color}`;
|
|
@@ -4,7 +4,7 @@ import { test } from 'node:test';
|
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
5
|
import { createMarkdownProcessor } from '@astrojs/markdown-remark';
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
|
-
import { sectionClass, sectionsOf } from './text-sections.mjs';
|
|
7
|
+
import { introOf, sectionClass, sectionsOf } from './text-sections.mjs';
|
|
8
8
|
|
|
9
9
|
const M = (color) => `<div data-section="${color}"></div>`;
|
|
10
10
|
|
|
@@ -36,6 +36,23 @@ test('a run ends at an h1', () => {
|
|
|
36
36
|
]);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
test('a marker before an h1 starts a coloured run that ends at the next h2', () => {
|
|
40
|
+
const html = `${M('tint')}<h1>A</h1><p>x</p><h2>B</h2>`;
|
|
41
|
+
assert.deepEqual(sectionsOf(html), [
|
|
42
|
+
{ color: 'tint', html: '<h1>A</h1><p>x</p>' },
|
|
43
|
+
{ color: null, html: '<h2>B</h2>' },
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a marker and a label before an h1 start a coloured run', () => {
|
|
48
|
+
const html = `<p>Intro</p>${M('accent')}<p class="block-eyebrow">Hi</p><h1>A</h1><p>x</p><h1>B</h1>`;
|
|
49
|
+
assert.deepEqual(sectionsOf(html), [
|
|
50
|
+
{ color: null, html: '<p>Intro</p>' },
|
|
51
|
+
{ color: 'accent', html: '<p class="block-eyebrow">Hi</p><h1>A</h1><p>x</p>' },
|
|
52
|
+
{ color: null, html: '<h1>B</h1>' },
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
test('two coloured sections after each other are two runs', () => {
|
|
40
57
|
const html = `${M('tint')}\n<h2>A</h2>\n<p>x</p>\n${M('accent')}\n<h2>B</h2>\n<p>y</p>`;
|
|
41
58
|
assert.deepEqual(sectionsOf(html), [
|
|
@@ -61,6 +78,22 @@ test('a marker not followed by an h2 is dropped', () => {
|
|
|
61
78
|
assert.deepEqual(sectionsOf(`<p>a</p>\n${M('tint')}\n<h3>c</h3>`), [{ color: null, html: '<p>a</p>\n<h3>c</h3>' }]);
|
|
62
79
|
});
|
|
63
80
|
|
|
81
|
+
test('the intro is the text before the first section starts', () => {
|
|
82
|
+
assert.deepEqual(introOf(`<p>a</p>\n<p>b</p>\n${M('tint')}\n<h2>c</h2>\n<p>d</p>`), {
|
|
83
|
+
intro: '<p>a</p>\n<p>b</p>\n',
|
|
84
|
+
rest: `${M('tint')}\n<h2>c</h2>\n<p>d</p>`,
|
|
85
|
+
});
|
|
86
|
+
assert.deepEqual(introOf('<p>a</p><p class="block-eyebrow">L</p><h1>c</h1>'), {
|
|
87
|
+
intro: '<p>a</p>',
|
|
88
|
+
rest: '<p class="block-eyebrow">L</p><h1>c</h1>',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('without a heading the text is all intro, and a text that starts with one has none', () => {
|
|
93
|
+
assert.deepEqual(introOf('<p>a</p>'), { intro: '<p>a</p>', rest: '' });
|
|
94
|
+
assert.deepEqual(introOf('<h2>a</h2><p>b</p>'), { intro: '', rest: '<h2>a</h2><p>b</p>' });
|
|
95
|
+
});
|
|
96
|
+
|
|
64
97
|
test('the class of a coloured section', () => {
|
|
65
98
|
assert.equal(sectionClass('tint'), 'text-section text-section-tint');
|
|
66
99
|
});
|
package/lib/translate.d.mts
CHANGED
|
@@ -6,3 +6,14 @@
|
|
|
6
6
|
* @returns {string}
|
|
7
7
|
*/
|
|
8
8
|
export function translate(dictionary: Record<string, string> | undefined, text: string, values?: Record<string, string | number>): string;
|
|
9
|
+
/**
|
|
10
|
+
* Splits the texts that have no translation in a language: the standard parts' (English texts of
|
|
11
|
+
* defaults/, whose translations the package must have: they stop the check) and the others (the site's and the apps' own).
|
|
12
|
+
* @param {Iterable<string>} missing The texts with no translation.
|
|
13
|
+
* @param {ReadonlySet<string>} standard The standard parts' texts (the keys of defaults/i18n/da-DK/).
|
|
14
|
+
* @returns {{ standard: string[], own: string[] }}
|
|
15
|
+
*/
|
|
16
|
+
export function splitMissing(missing: Iterable<string>, standard: ReadonlySet<string>): {
|
|
17
|
+
standard: string[];
|
|
18
|
+
own: string[];
|
|
19
|
+
};
|
package/lib/translate.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The text lookup of the editor: a dictionary maps a text as written to its translation.
|
|
2
|
-
// Exposes translate. Pure: no packages, no file access.
|
|
2
|
+
// Exposes translate, splitMissing. Pure: no packages, no file access.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Takes dictionary[text] when the dictionary has that own key, else the text. Then fills each {name} from values.
|
|
@@ -13,3 +13,15 @@ export function translate(dictionary, text, values) {
|
|
|
13
13
|
if (!values) return taken;
|
|
14
14
|
return taken.replace(/\{(\w+)\}/g, (whole, name) => (name in values ? String(values[name]) : whole));
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Splits the texts that have no translation in a language: the standard parts' (English texts of
|
|
19
|
+
* defaults/, whose translations the package must have: they stop the check) and the others (the site's and the apps' own).
|
|
20
|
+
* @param {Iterable<string>} missing The texts with no translation.
|
|
21
|
+
* @param {ReadonlySet<string>} standard The standard parts' texts (the keys of defaults/i18n/da-DK/).
|
|
22
|
+
* @returns {{ standard: string[], own: string[] }}
|
|
23
|
+
*/
|
|
24
|
+
export function splitMissing(missing, standard) {
|
|
25
|
+
const all = [...missing];
|
|
26
|
+
return { standard: all.filter((text) => standard.has(text)), own: all.filter((text) => !standard.has(text)) };
|
|
27
|
+
}
|
package/lib/translate.test.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Tests for lib/translate.mjs. Run: node --test lib/translate.test.mjs
|
|
1
|
+
// Tests for lib/translate.mjs (translate, splitMissing). Run: node --test lib/translate.test.mjs
|
|
2
2
|
// Danish letters are written as \u escapes.
|
|
3
3
|
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
5
|
import { test } from 'node:test';
|
|
6
|
-
import { translate } from './translate.mjs';
|
|
6
|
+
import { splitMissing, translate } from './translate.mjs';
|
|
7
7
|
|
|
8
8
|
test('takes the dictionary entry', () => {
|
|
9
9
|
assert.equal(translate({ 'Read more': 'L\u00e6s mere' }, 'Read more'), 'L\u00e6s mere');
|
|
@@ -36,3 +36,24 @@ test('ignores inherited keys', () => {
|
|
|
36
36
|
test('keeps a name when no values are given', () => {
|
|
37
37
|
assert.equal(translate({}, 'Hi {name}'), 'Hi {name}');
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
// splitMissing: a standard text with no translation stops the check, the site's own only warns.
|
|
41
|
+
test('splits the missing texts into the standard parts and the own', () => {
|
|
42
|
+
const split = splitMissing(['Content', 'Indhold', 'Pris'], new Set(['Content', 'Media']));
|
|
43
|
+
assert.deepEqual(split, { standard: ['Content'], own: ['Indhold', 'Pris'] });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('has nothing to split when nothing is missing', () => {
|
|
47
|
+
assert.deepEqual(splitMissing([], new Set(['Content'])), { standard: [], own: [] });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('an own text of an English site with no Danish only warns', () => {
|
|
51
|
+
const split = splitMissing(['Opening hours'], new Set(['Content']));
|
|
52
|
+
assert.deepEqual(split, { standard: [], own: ['Opening hours'] });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('a standard text whose Danish entry was removed stops the check', () => {
|
|
56
|
+
// The standard set is built from the standard definitions, not from the Danish dictionary.
|
|
57
|
+
const split = splitMissing(['Button text', 'Opening hours'], new Set(['Button text', 'Content']));
|
|
58
|
+
assert.deepEqual(split, { standard: ['Button text'], own: ['Opening hours'] });
|
|
59
|
+
});
|
package/lib/video-id.d.mts
CHANGED
|
@@ -7,8 +7,12 @@ export function videoOf(text: any): {
|
|
|
7
7
|
id: string;
|
|
8
8
|
hash: string | null;
|
|
9
9
|
} | null;
|
|
10
|
-
/** The address that plays the video, with the settings in the address of the iframe it came from.
|
|
11
|
-
|
|
10
|
+
/** The address that plays the video, with the settings in the address of the iframe it came from.
|
|
11
|
+
* @param {any} video
|
|
12
|
+
* @param {string | null} [frame]
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function player(video: any, frame?: string | null): string;
|
|
12
16
|
export function videoPage(video: any): string;
|
|
13
17
|
export function stillName(video: any): string;
|
|
14
18
|
export function vimeoInfo(video: any, width: any): string;
|
package/lib/video-id.mjs
CHANGED
|
@@ -43,7 +43,11 @@ export function videoOf(text) {
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/** The address that plays the video, with the settings in the address of the iframe it came from.
|
|
46
|
+
/** The address that plays the video, with the settings in the address of the iframe it came from.
|
|
47
|
+
* @param {any} video
|
|
48
|
+
* @param {string | null} [frame]
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
47
51
|
export function player(video, frame) {
|
|
48
52
|
let params;
|
|
49
53
|
try {
|
package/lib/video.d.mts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** What stands in for a video (videoOf) on the site: its page, the address that plays it (`frame`: the address of the
|
|
2
2
|
* iframe it replaces, if any), its title, and its still from the site, when it could be fetched. The video block's
|
|
3
|
-
* view too (src/standard/video/load.ts).
|
|
4
|
-
|
|
3
|
+
* view too (src/standard/video/load.ts).
|
|
4
|
+
* @param {any} video
|
|
5
|
+
* @param {any} [title]
|
|
6
|
+
* @param {string | null} [frame]
|
|
7
|
+
*/
|
|
8
|
+
export function facadeOf(video: any, title?: any, frame?: string | null): {
|
|
5
9
|
href: string;
|
|
6
10
|
player: string;
|
|
7
11
|
title: any;
|
package/lib/video.mjs
CHANGED
|
@@ -39,7 +39,11 @@ function facade(video, title, frame, play) {
|
|
|
39
39
|
|
|
40
40
|
/** What stands in for a video (videoOf) on the site: its page, the address that plays it (`frame`: the address of the
|
|
41
41
|
* iframe it replaces, if any), its title, and its still from the site, when it could be fetched. The video block's
|
|
42
|
-
* view too (src/standard/video/load.ts).
|
|
42
|
+
* view too (src/standard/video/load.ts).
|
|
43
|
+
* @param {any} video
|
|
44
|
+
* @param {any} [title]
|
|
45
|
+
* @param {string | null} [frame]
|
|
46
|
+
*/
|
|
43
47
|
export function facadeOf(video, title, frame) {
|
|
44
48
|
const still = `/images/_video/${stillName(video)}`;
|
|
45
49
|
return {
|
package/lib/without-h1.d.mts
CHANGED
package/lib/without-h1.mjs
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
// A page's text as HTML without its H1, which the layout draws in its own place (above the picture, PageLayout.astro)
|
|
2
|
-
//
|
|
1
|
+
// A page's text as HTML without its H1, which the layout draws in its own place (above the picture, PageLayout.astro), and the
|
|
2
|
+
// colour of the H1's section (the marker before it, lib/section-marker.mjs). Exposes withoutH1 and h1SectionColor (pure); the H1
|
|
3
|
+
// is the first one wherever it stands, as pageH1 (src/sections.mjs) reads it. Depends on text-sections.mjs and section-marker.mjs.
|
|
4
|
+
import { LABEL, MARKER } from './text-sections.mjs';
|
|
5
|
+
import { SECTION_COLORS } from './section-marker.mjs';
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
// The first <h1>…</h1> and the white space after it, with the marker before it and the label between them, if any.
|
|
8
|
+
const H1 = new RegExp(`(?:${MARKER})?(${LABEL})<h1\\b[^>]*>[\\s\\S]*?<\\/h1>\\s*`);
|
|
9
|
+
|
|
10
|
+
/** The html without its first <h1>…</h1> and the white space after it, and without the marker before it (a label above it
|
|
11
|
+
* stays in the text).
|
|
5
12
|
* @param {string} html
|
|
6
13
|
* @returns {string} */
|
|
7
|
-
export const withoutH1 = (html) => html.replace(
|
|
14
|
+
export const withoutH1 = (html) => html.replace(H1, (_, __, label) => label);
|
|
15
|
+
|
|
16
|
+
/** The colour of the section the first H1 starts (the marker before it, or before the label above it), or null.
|
|
17
|
+
* @param {string} html
|
|
18
|
+
* @returns {string | null} */
|
|
19
|
+
export const h1SectionColor = (html) => {
|
|
20
|
+
const color = html.match(H1)?.[1];
|
|
21
|
+
return SECTION_COLORS.includes(color) ? color : null;
|
|
22
|
+
};
|
package/lib/without-h1.test.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Run with `node --test packages/editor/lib/*.test.mjs` (part of npm run check).
|
|
3
3
|
import { test } from 'node:test';
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
|
-
import { withoutH1 } from './without-h1.mjs';
|
|
5
|
+
import { h1SectionColor, withoutH1 } from './without-h1.mjs';
|
|
6
6
|
|
|
7
7
|
test('an H1 at the top is taken out, with the space after it', () => {
|
|
8
8
|
assert.equal(withoutH1('<h1 id="title">Title</h1>\n<p>Text</p>'), '<p>Text</p>');
|
|
@@ -20,6 +20,35 @@ test('only the first H1 goes', () => {
|
|
|
20
20
|
assert.equal(withoutH1('<h1>A</h1><p>x</p><h1>B</h1>'), '<p>x</p><h1>B</h1>');
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
const M = (color) => `<div data-section="${color}"></div>`;
|
|
24
|
+
|
|
25
|
+
test('the marker before the H1 is taken out with it', () => {
|
|
26
|
+
assert.equal(withoutH1(`${M('tint')}\n<h1 id="t">Title</h1>\n<p>Text</p>`), '<p>Text</p>');
|
|
27
|
+
assert.equal(withoutH1(`<p>Intro</p>\n${M('tint')}\n<h1>Title</h1>\n<h2>A</h2>`), '<p>Intro</p>\n<h2>A</h2>');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('a label between the marker and the H1 stays in the text', () => {
|
|
31
|
+
const label = '<p class="block-eyebrow">Hello</p>';
|
|
32
|
+
assert.equal(withoutH1(`${M('accent')}\n${label}\n<h1>Title</h1>\n<p>Text</p>`), `${label}\n<p>Text</p>`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the marker of an H2 is not taken out', () => {
|
|
36
|
+
const html = `<h1>Title</h1>\n${M('tint')}\n<h2>A</h2>`;
|
|
37
|
+
assert.equal(withoutH1(html), `${M('tint')}\n<h2>A</h2>`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the colour of the H1 section is the marker before the H1', () => {
|
|
41
|
+
assert.equal(h1SectionColor(`${M('tint')}\n<h1>Title</h1>\n<p>x</p>`), 'tint');
|
|
42
|
+
assert.equal(h1SectionColor(`<p>a</p>${M('accent')}<p class="block-eyebrow">L</p><h1>T</h1>`), 'accent');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('without a marker before the H1 there is no colour', () => {
|
|
46
|
+
assert.equal(h1SectionColor('<h1>Title</h1><p>x</p>'), null);
|
|
47
|
+
assert.equal(h1SectionColor(`<h1>Title</h1>${M('tint')}<h2>A</h2>`), null);
|
|
48
|
+
assert.equal(h1SectionColor(`${M('red')}<h1>Title</h1>`), null);
|
|
49
|
+
assert.equal(h1SectionColor('<p>x</p>'), null);
|
|
50
|
+
});
|
|
51
|
+
|
|
23
52
|
test('without an H1 the html is as it was', () => {
|
|
24
53
|
assert.equal(withoutH1('<p>Text</p><h2>Section</h2>'), '<p>Text</p><h2>Section</h2>');
|
|
25
54
|
assert.equal(withoutH1('<header>x</header>'), '<header>x</header>');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphemecms/cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0-next-20261106203613",
|
|
4
4
|
"description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lars Adolfsen",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node scripts/demo-build.mjs",
|
|
43
|
-
"check": "npm run format:check && npm run check:language && npm test && npm run check:types && npm run build && npm run check:editor",
|
|
43
|
+
"check": "npm run format:check && npm run check:language && npm test && npm run check:types && npm run build && npm run check:site-types && npm run check:editor",
|
|
44
|
+
"check:site-types": "node scripts/check-site-types.mjs",
|
|
44
45
|
"check:types": "tsc -p tsconfig.types.json --noEmit",
|
|
45
46
|
"check:editor": "cd .morpheme/demo/site && node ../../../bin/morpheme.mjs check:editor",
|
|
46
47
|
"check:screens": "cd .morpheme/demo/site && node ../../../bin/morpheme.mjs check:screens",
|