@janga/norna 0.7.23 → 0.7.25
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/README.md +14 -18
- package/astro.config.mjs +14 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +25 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +80 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +16 -1
- package/scripts/dev-local.mjs +106 -11
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +3 -0
- package/scripts/lib/code-fence-metadata.mjs +228 -0
- package/scripts/lib/edit-source-link.mjs +102 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +164 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +141 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +30 -5
- package/scripts/lib/schema-editor-metadata.mjs +36 -4
- package/scripts/lib/schema-value-definitions.mjs +4 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/table-render-plugin.mjs +33 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +11 -5
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +9 -7
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageContentsNavigation.astro +2 -3
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +45 -7
- package/src/components/SiteNavigation.astro +102 -51
- package/src/components/SitePage.astro +107 -39
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +11 -1
- package/src/components/TableOverflowScript.astro +251 -0
- package/src/components/TreeNavigationScript.astro +271 -34
- package/src/layouts/BaseLayout.astro +83 -2
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/sectionContent.ts +36 -30
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +6 -3
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +538 -39
- package/src/styles/media.css +187 -8
- package/src/styles/navigation.css +199 -3
- package/src/styles/page-layout.css +481 -100
- package/src/styles/responsive.css +144 -28
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export const semanticCalloutTypes = Object.freeze([
|
|
2
|
+
'NOTE',
|
|
3
|
+
'TIP',
|
|
4
|
+
'IMPORTANT',
|
|
5
|
+
'WARNING',
|
|
6
|
+
'CAUTION',
|
|
7
|
+
'DANGER',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const semanticCalloutTypeSet = new Set(semanticCalloutTypes);
|
|
11
|
+
const markerPattern = /^\[!([A-Za-z][A-Za-z0-9-]*)\]([^\n]*)(?:\n|$)/;
|
|
12
|
+
|
|
13
|
+
const hasMeaningfulContent = (node) => {
|
|
14
|
+
if (!node || typeof node !== 'object') return false;
|
|
15
|
+
if ('value' in node && typeof node.value === 'string') return node.value.trim().length > 0;
|
|
16
|
+
if (node.type === 'image') return Boolean(node.alt);
|
|
17
|
+
return Array.isArray(node.children) && node.children.some(hasMeaningfulContent);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getSemanticCalloutMarker = (node) => {
|
|
21
|
+
if (node?.type !== 'blockquote') return null;
|
|
22
|
+
const firstParagraph = node.children?.[0];
|
|
23
|
+
const firstText = firstParagraph?.type === 'paragraph' ? firstParagraph.children?.[0] : null;
|
|
24
|
+
if (firstText?.type !== 'text') return null;
|
|
25
|
+
|
|
26
|
+
const match = firstText.value.match(markerPattern);
|
|
27
|
+
if (!match) {
|
|
28
|
+
return firstText.value.startsWith('[!')
|
|
29
|
+
? { malformed: true, firstParagraph, firstText }
|
|
30
|
+
: null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rawType = match[1] ?? '';
|
|
34
|
+
const type = rawType.toUpperCase();
|
|
35
|
+
const markerSource = match[0];
|
|
36
|
+
const title = (match[2] ?? '').trim();
|
|
37
|
+
const firstTextRemainder = firstText.value.slice(markerSource.length);
|
|
38
|
+
const firstParagraphRemainder = [
|
|
39
|
+
{ ...firstText, value: firstTextRemainder },
|
|
40
|
+
...(firstParagraph.children?.slice(1) ?? []),
|
|
41
|
+
];
|
|
42
|
+
const hasBody = firstParagraphRemainder.some(hasMeaningfulContent)
|
|
43
|
+
|| (node.children?.slice(1) ?? []).some(hasMeaningfulContent);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
firstParagraph,
|
|
47
|
+
firstText,
|
|
48
|
+
firstTextRemainder,
|
|
49
|
+
hasBody,
|
|
50
|
+
malformed: false,
|
|
51
|
+
markerSource,
|
|
52
|
+
rawType,
|
|
53
|
+
supported: semanticCalloutTypeSet.has(type),
|
|
54
|
+
title,
|
|
55
|
+
type,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const formatDiagnostic = ({ code, fix, label, line, message, offset }) => ({
|
|
60
|
+
code,
|
|
61
|
+
fix,
|
|
62
|
+
line,
|
|
63
|
+
message: `${label} line ${line}: ${message}`,
|
|
64
|
+
offset,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const getSemanticCalloutDiagnostics = (tree, options = {}) => {
|
|
68
|
+
const diagnostics = [];
|
|
69
|
+
const label = options.label ?? 'Markdown';
|
|
70
|
+
const lineOffset = options.lineOffset ?? 0;
|
|
71
|
+
|
|
72
|
+
const visit = (node, blockquoteDepth = 0) => {
|
|
73
|
+
if (!node || typeof node !== 'object') return;
|
|
74
|
+
const nextBlockquoteDepth = blockquoteDepth + (node.type === 'blockquote' ? 1 : 0);
|
|
75
|
+
const marker = getSemanticCalloutMarker(node);
|
|
76
|
+
|
|
77
|
+
if (marker) {
|
|
78
|
+
const line = lineOffset + (node.position?.start.line ?? 1);
|
|
79
|
+
const offset = node.position?.start.offset ?? 0;
|
|
80
|
+
let issue = null;
|
|
81
|
+
|
|
82
|
+
if (blockquoteDepth > 0) {
|
|
83
|
+
issue = {
|
|
84
|
+
code: 'nested-semantic-callout',
|
|
85
|
+
message: 'Semantic callouts cannot be nested inside another blockquote or callout.',
|
|
86
|
+
fix: 'Move the inner callout after the surrounding blockquote.',
|
|
87
|
+
};
|
|
88
|
+
} else if (marker.malformed) {
|
|
89
|
+
issue = {
|
|
90
|
+
code: 'invalid-semantic-callout-marker',
|
|
91
|
+
message: 'This blockquote starts like a semantic callout but its marker is incomplete.',
|
|
92
|
+
fix: 'Write the marker on its own first line, for example "> [!WARNING]".',
|
|
93
|
+
};
|
|
94
|
+
} else if (marker.rawType !== marker.type) {
|
|
95
|
+
issue = {
|
|
96
|
+
code: 'invalid-semantic-callout-type-case',
|
|
97
|
+
message: `Semantic callout type "${marker.rawType}" must use uppercase ASCII letters.`,
|
|
98
|
+
fix: `Write "> [!${marker.type}]".`,
|
|
99
|
+
};
|
|
100
|
+
} else if (!marker.supported) {
|
|
101
|
+
issue = {
|
|
102
|
+
code: 'unknown-semantic-callout-type',
|
|
103
|
+
message: `Unknown semantic callout type "${marker.rawType}".`,
|
|
104
|
+
fix: `Use one of: ${semanticCalloutTypes.join(', ')}.`,
|
|
105
|
+
};
|
|
106
|
+
} else if (marker.title) {
|
|
107
|
+
issue = {
|
|
108
|
+
code: 'unsupported-semantic-callout-title',
|
|
109
|
+
message: `Semantic callout markers cannot contain a custom title ("${marker.title}").`,
|
|
110
|
+
fix: `Keep "> [!${marker.type}]" on its own line. Begin the callout body with bold text when local emphasis is useful.`,
|
|
111
|
+
};
|
|
112
|
+
} else if (!marker.hasBody) {
|
|
113
|
+
issue = {
|
|
114
|
+
code: 'empty-semantic-callout',
|
|
115
|
+
message: `${marker.type} callout has no content.`,
|
|
116
|
+
fix: 'Add at least one quoted content line after the marker.',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (issue) diagnostics.push(formatDiagnostic({ ...issue, label, line, offset }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const child of node.children ?? []) visit(child, nextBlockquoteDepth);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
visit(tree);
|
|
127
|
+
return diagnostics;
|
|
128
|
+
};
|
|
@@ -30,6 +30,7 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
30
30
|
'align',
|
|
31
31
|
'aliases',
|
|
32
32
|
'alt',
|
|
33
|
+
'baseUrl',
|
|
33
34
|
'body',
|
|
34
35
|
'blockGap',
|
|
35
36
|
'caption',
|
|
@@ -152,7 +153,7 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
152
153
|
} else if (fileKind === 'content' && key === 'description') {
|
|
153
154
|
fix = 'Indent "description:" under "page:". The optional meta description belongs in the page object.';
|
|
154
155
|
} else if (fileKind === 'content' && key === 'images') {
|
|
155
|
-
fix = 'Put local image references in
|
|
156
|
+
fix = 'Put local image references in image-stack or image-carousel blocks in the Markdown body.';
|
|
156
157
|
} else if (fileKind === 'content' && knownThemeTopLevelFrontmatterKeys.has(key)) {
|
|
157
158
|
fix = `Move "${key}:" to theme.yaml. Visual settings do not belong in content frontmatter.`;
|
|
158
159
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'navigation') {
|
|
@@ -2,6 +2,7 @@ import { readdir, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
createPageAliasModel,
|
|
5
|
+
generatedSiteRoutes,
|
|
5
6
|
} from './page-aliases.mjs';
|
|
6
7
|
import { parsePageMarkdownSource } from './page-markdown.mjs';
|
|
7
8
|
import {
|
|
@@ -117,6 +118,9 @@ const createPageRecord = ({ contentFile, data = {}, document }) => {
|
|
|
117
118
|
anchors,
|
|
118
119
|
contentFile,
|
|
119
120
|
document,
|
|
121
|
+
navigation: {
|
|
122
|
+
listed: data.navigation?.listed ?? true,
|
|
123
|
+
},
|
|
120
124
|
pathname,
|
|
121
125
|
title: document.pageTitle?.title ?? contentFile.pageId,
|
|
122
126
|
};
|
|
@@ -140,7 +144,13 @@ const looksLikePublicFile = (pathname) => {
|
|
|
140
144
|
return (pathname.split('/').at(-1) ?? '').includes('.');
|
|
141
145
|
};
|
|
142
146
|
|
|
143
|
-
export const createSiteLinkGraph = ({
|
|
147
|
+
export const createSiteLinkGraph = ({
|
|
148
|
+
siteStructure,
|
|
149
|
+
pageDocuments,
|
|
150
|
+
publicFiles = [],
|
|
151
|
+
generatedRoutes,
|
|
152
|
+
}) => {
|
|
153
|
+
const activeGeneratedRoutes = generatedRoutes ?? generatedSiteRoutes;
|
|
144
154
|
const documentsByDirectory = new Map(pageDocuments.map(({ contentFile, data, document }) => [
|
|
145
155
|
contentFile.pageDirectory,
|
|
146
156
|
{ contentFile, data, document },
|
|
@@ -167,7 +177,11 @@ export const createSiteLinkGraph = ({ siteStructure, pageDocuments, publicFiles
|
|
|
167
177
|
categories: siteStructure.categories,
|
|
168
178
|
pages,
|
|
169
179
|
publicFiles,
|
|
180
|
+
generatedRoutes: activeGeneratedRoutes,
|
|
170
181
|
});
|
|
182
|
+
const generatedRoutesByPathname = new Map(
|
|
183
|
+
activeGeneratedRoutes.map((route) => [route.pathname, route]),
|
|
184
|
+
);
|
|
171
185
|
|
|
172
186
|
const references = [];
|
|
173
187
|
const referencesByTarget = new Map();
|
|
@@ -249,6 +263,18 @@ export const createSiteLinkGraph = ({ siteStructure, pageDocuments, publicFiles
|
|
|
249
263
|
continue;
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
const generatedRoute = generatedRoutesByPathname.get(
|
|
267
|
+
target.pageLookupPathname ?? target.pathname,
|
|
268
|
+
);
|
|
269
|
+
if (generatedRoute && !target.fragment) {
|
|
270
|
+
reference.resolution = {
|
|
271
|
+
kind: 'generated-route',
|
|
272
|
+
pathname: generatedRoute.pathname,
|
|
273
|
+
route: generatedRoute,
|
|
274
|
+
};
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
252
278
|
const targetCategory = target.pageLookupPathname
|
|
253
279
|
? categoriesByPathname.get(target.pageLookupPathname)
|
|
254
280
|
: null;
|
|
@@ -320,7 +346,12 @@ export const getSiteLinkGraph = async (options = {}) => {
|
|
|
320
346
|
readPublicFiles(options.publicDir ?? sitePublicDir),
|
|
321
347
|
]);
|
|
322
348
|
|
|
323
|
-
return createSiteLinkGraph({
|
|
349
|
+
return createSiteLinkGraph({
|
|
350
|
+
pageDocuments,
|
|
351
|
+
publicFiles,
|
|
352
|
+
siteStructure,
|
|
353
|
+
generatedRoutes: options.generatedRoutes,
|
|
354
|
+
});
|
|
324
355
|
};
|
|
325
356
|
|
|
326
357
|
export const getSitePublicFiles = async (publicDir = sitePublicDir) => readPublicFiles(publicDir);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const getSiteNavigationTree = (entries) => {
|
|
2
|
+
const nodes = new Map();
|
|
3
|
+
const roots = [];
|
|
4
|
+
|
|
5
|
+
for (const entry of entries) {
|
|
6
|
+
nodes.set(entry.node.pagePath, { ...entry, children: [] });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const node = nodes.get(entry.node.pagePath);
|
|
11
|
+
if (!node) continue;
|
|
12
|
+
|
|
13
|
+
if (entry.node.isHome || entry.node.parentPagePath === null) {
|
|
14
|
+
roots.push(node);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parent = nodes.get(entry.node.parentPagePath);
|
|
19
|
+
if (!parent) {
|
|
20
|
+
throw new Error(`Site node "${entry.node.pagePath}" has no parent node "${entry.node.parentPagePath}".`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
parent.children.push(node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return roots;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getListedSiteNavigationTree = (entries) => {
|
|
30
|
+
const filterListed = (nodes) => nodes.flatMap((node) => {
|
|
31
|
+
if (!node.node.isHome && !node.node.navigation.listed) return [];
|
|
32
|
+
const children = filterListed(node.children);
|
|
33
|
+
if (node.node.kind === 'category' && children.length === 0) return [];
|
|
34
|
+
return [{ ...node, children }];
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return filterListed(getSiteNavigationTree(entries));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getFirstPageInNavigationNode = (node) => {
|
|
41
|
+
if (node.node.kind === 'page') return node.node;
|
|
42
|
+
|
|
43
|
+
for (const child of node.children) {
|
|
44
|
+
const page = getFirstPageInNavigationNode(child);
|
|
45
|
+
if (page) return page;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const flattenSiteNavigationTree = (nodes) => nodes.flatMap((node) => [
|
|
52
|
+
node,
|
|
53
|
+
...flattenSiteNavigationTree(node.children),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export const getSequentialPageNavigation = (nodes, currentPagePath) => {
|
|
57
|
+
const activeRoot = nodes.find((root) => (
|
|
58
|
+
flattenSiteNavigationTree([root]).some(({ node }) => node.pagePath === currentPagePath)
|
|
59
|
+
));
|
|
60
|
+
if (!activeRoot) return { previous: null, next: null };
|
|
61
|
+
|
|
62
|
+
const pages = flattenSiteNavigationTree([activeRoot])
|
|
63
|
+
.filter(({ node }) => node.kind === 'page')
|
|
64
|
+
.map(({ node }) => node);
|
|
65
|
+
const currentIndex = pages.findIndex(({ pagePath }) => pagePath === currentPagePath);
|
|
66
|
+
if (currentIndex < 0) return { previous: null, next: null };
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
previous: pages[currentIndex - 1] ?? null,
|
|
70
|
+
next: pages[currentIndex + 1] ?? null,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const getDirectChildPages = (nodes, currentPagePath) => {
|
|
75
|
+
const current = flattenSiteNavigationTree(nodes)
|
|
76
|
+
.find(({ node }) => node.pagePath === currentPagePath);
|
|
77
|
+
if (!current) return [];
|
|
78
|
+
const children = current.node.isHome
|
|
79
|
+
? nodes.filter(({ node }) => !node.isHome)
|
|
80
|
+
: current.children;
|
|
81
|
+
|
|
82
|
+
return children
|
|
83
|
+
.filter(({ node }) => node.kind === 'page')
|
|
84
|
+
.map(({ node }) => node);
|
|
85
|
+
};
|
|
@@ -5,6 +5,7 @@ import { homePageDirectory } from './site-conventions.mjs';
|
|
|
5
5
|
|
|
6
6
|
const siteDirectoryEnvName = 'NORNA_SITE_DIR';
|
|
7
7
|
const invocationRootEnvName = 'NORNA_INVOCATION_ROOT';
|
|
8
|
+
const stateDirectoryEnvName = 'NORNA_INTERNAL_STATE_DIR';
|
|
8
9
|
const defaultSiteDirectory = 'site';
|
|
9
10
|
export { homePageDirectory };
|
|
10
11
|
|
|
@@ -16,12 +17,19 @@ export const siteDirectoryEnv = siteDirectoryEnvName;
|
|
|
16
17
|
|
|
17
18
|
const normalizeSiteDirectory = (value) => String(value ?? '').trim();
|
|
18
19
|
const normalizeInvocationRoot = (value) => String(value ?? '').trim();
|
|
20
|
+
const normalizeStateDirectory = (value) => String(value ?? '').trim();
|
|
19
21
|
|
|
20
22
|
const configuredInvocationRoot = normalizeInvocationRoot(process.env[invocationRootEnvName]);
|
|
21
23
|
export const invocationRoot = configuredInvocationRoot
|
|
22
24
|
? path.resolve(configuredInvocationRoot)
|
|
23
25
|
: process.cwd();
|
|
24
26
|
|
|
27
|
+
const hasStateDirectoryEnv = Object.hasOwn(process.env, stateDirectoryEnvName);
|
|
28
|
+
const configuredStateDirectory = normalizeStateDirectory(process.env[stateDirectoryEnvName]);
|
|
29
|
+
if (hasStateDirectoryEnv && !configuredStateDirectory) {
|
|
30
|
+
throw new Error(`${stateDirectoryEnvName} must not be empty.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
const hasSiteFilesInDirectory = (siteDir) => {
|
|
26
34
|
return (
|
|
27
35
|
existsSync(path.join(siteDir, 'config.yaml'))
|
|
@@ -126,9 +134,13 @@ export const siteHomePageDir = path.join(sitePagesDir, homePageDirectory);
|
|
|
126
134
|
export const siteContentPath = path.join(siteHomePageDir, 'content.md');
|
|
127
135
|
export const siteImagesDir = path.join(siteHomePageDir, 'images');
|
|
128
136
|
export const sitePublicDir = path.join(siteDir, 'public');
|
|
129
|
-
export const siteStateDir =
|
|
137
|
+
export const siteStateDir = configuredStateDirectory
|
|
138
|
+
? path.resolve(configuredStateDirectory)
|
|
139
|
+
: path.join(siteDir, '.norna');
|
|
130
140
|
export const astroPublicDir = path.join(siteStateDir, 'public');
|
|
131
|
-
export const astroDistDir =
|
|
141
|
+
export const astroDistDir = configuredStateDirectory
|
|
142
|
+
? path.join(siteStateDir, 'dist')
|
|
143
|
+
: path.join(siteProjectRoot, 'dist');
|
|
132
144
|
export const astroRootDir = siteStateDir;
|
|
133
145
|
export const astroCacheDir = path.join(astroRootDir, '.astro');
|
|
134
146
|
export const generatedImagesDir = path.join(astroPublicDir, 'images', 'generated');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getPublicAssetInspection } from './logo-assets.mjs';
|
|
2
|
+
import {
|
|
3
|
+
socialImageAssetDefinitions,
|
|
4
|
+
socialImageAssetFilenames,
|
|
5
|
+
} from './public-asset-conventions.mjs';
|
|
6
|
+
import { sitePublicLabel } from './site-paths.mjs';
|
|
7
|
+
|
|
8
|
+
const definitionsByFilename = new Map(
|
|
9
|
+
socialImageAssetDefinitions.map((definition) => [definition.filename, definition]),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const getSocialImageAssets = () => getPublicAssetInspection().socialImages
|
|
13
|
+
.map((filename) => ({
|
|
14
|
+
...definitionsByFilename.get(filename),
|
|
15
|
+
filename,
|
|
16
|
+
href: `/${filename}`,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
export const getSocialImageAsset = () => {
|
|
20
|
+
const assets = getSocialImageAssets();
|
|
21
|
+
|
|
22
|
+
if (assets.length > 1) {
|
|
23
|
+
throw new Error([
|
|
24
|
+
`Found multiple social sharing images in ${sitePublicLabel}. Keep exactly one of ${socialImageAssetFilenames.join(', ')}.`,
|
|
25
|
+
...assets.map(({ filename }) => `- ${sitePublicLabel}/${filename}`),
|
|
26
|
+
].join('\n'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return assets[0] ?? null;
|
|
30
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineHastPlugin } from 'satteri';
|
|
2
|
+
|
|
3
|
+
export const nornaTableRenderPlugin = defineHastPlugin({
|
|
4
|
+
name: 'norna-table-render',
|
|
5
|
+
element: {
|
|
6
|
+
filter: ['table'],
|
|
7
|
+
visit(node, context) {
|
|
8
|
+
context.replaceNode(node, {
|
|
9
|
+
type: 'element',
|
|
10
|
+
tagName: 'div',
|
|
11
|
+
properties: {
|
|
12
|
+
className: ['norna-table-frame', 'content-block-note-lane-boundary'],
|
|
13
|
+
'data-table-frame': '',
|
|
14
|
+
},
|
|
15
|
+
children: [{
|
|
16
|
+
type: 'element',
|
|
17
|
+
tagName: 'div',
|
|
18
|
+
properties: {
|
|
19
|
+
className: ['norna-table-scroll'],
|
|
20
|
+
'data-table-scroll': '',
|
|
21
|
+
tabIndex: 0,
|
|
22
|
+
},
|
|
23
|
+
children: [{
|
|
24
|
+
type: 'element',
|
|
25
|
+
tagName: node.tagName,
|
|
26
|
+
properties: { ...(node.properties ?? {}) },
|
|
27
|
+
children: [...(node.children ?? [])],
|
|
28
|
+
}],
|
|
29
|
+
}],
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -4,7 +4,7 @@ import { resolveThemeProfileRecipe } from './theme-profiles.mjs';
|
|
|
4
4
|
|
|
5
5
|
export const themePresetRecipes = Object.freeze({
|
|
6
6
|
portfolio: Object.freeze({
|
|
7
|
-
color: 'near-monochrome-
|
|
7
|
+
color: 'near-monochrome-adaptive',
|
|
8
8
|
typography: 'restrained-sans',
|
|
9
9
|
rhythm: 'balanced',
|
|
10
10
|
geometry: 'image-led',
|
|
@@ -231,7 +231,7 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml
|
|
|
231
231
|
'blocks:',
|
|
232
232
|
' cardList:',
|
|
233
233
|
' # Alternatives: text, narrow, normal, wide.',
|
|
234
|
-
' # A width written in a
|
|
234
|
+
' # A width written in a card-list block overrides this default.',
|
|
235
235
|
` width: ${blocks.cardList.width}`,
|
|
236
236
|
'',
|
|
237
237
|
'typography:',
|
|
@@ -2,10 +2,6 @@ import { freezeDeep, mergeDeep } from './object.mjs';
|
|
|
2
2
|
|
|
3
3
|
export const themeProfileDefinitions = freezeDeep({
|
|
4
4
|
color: {
|
|
5
|
-
'near-monochrome-dark': {
|
|
6
|
-
appearance: { default: 'dark' },
|
|
7
|
-
palette: 'near-monochrome',
|
|
8
|
-
},
|
|
9
5
|
'near-monochrome-adaptive': {
|
|
10
6
|
appearance: { default: 'system' },
|
|
11
7
|
palette: 'near-monochrome',
|