erudit 4.0.0-dev.1 → 4.0.0-dev.2
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/app/app.vue +1 -2
- package/app/components/FancyBold.vue +0 -1
- package/app/components/FancyCard.vue +1 -2
- package/app/components/ads/AdsBannerAside.vue +1 -2
- package/app/components/ads/AdsReplacer.vue +2 -2
- package/app/components/aside/AsideListItem.vue +1 -1
- package/app/components/aside/AsideSwitch.vue +18 -8
- package/app/components/aside/major/PaneSwitcher.vue +1 -1
- package/app/components/aside/minor/AsideMinorPlainHeader.vue +2 -3
- package/app/components/aside/minor/content/AsideMinorContentTopic.vue +1 -4
- package/app/components/aside/minor/content/ButtonPaneContributions.vue +2 -4
- package/app/components/aside/minor/content/ButtonPaneImprove.vue +2 -3
- package/app/components/aside/minor/content/Contribution.vue +1 -1
- package/app/components/aside/minor/content/TocItem.vue +35 -21
- package/app/components/aside/minor/news/AsideMinorNews.vue +1 -2
- package/app/components/aside/minor/news/NewsItem.vue +2 -2
- package/app/components/aside/minor/news/elements/Ref.vue +1 -1
- package/app/components/main/MainContentChild.vue +2 -3
- package/app/components/main/MainDescription.vue +1 -1
- package/app/components/main/MainQuickLink.vue +20 -5
- package/app/components/main/MainQuickLinks.vue +1 -3
- package/app/components/main/MainQuote.vue +3 -6
- package/app/components/main/MainSection.vue +6 -21
- package/app/components/main/MainTitle.vue +1 -2
- package/app/components/main/MainTopicPartSwitch.vue +1 -1
- package/app/components/main/connections/Deps.vue +1 -1
- package/app/components/main/connections/Externals.vue +92 -34
- package/app/components/main/connections/MainConnections.vue +61 -8
- package/app/components/main/connections/MainConnectionsButton.vue +3 -2
- package/app/components/main/connections/ScrollPane.vue +2 -3
- package/app/components/main/contentStats/Item.vue +1 -2
- package/app/components/main/contentStats/MainContentStats.vue +6 -3
- package/app/components/preview/Preview.vue +1 -2
- package/app/components/preview/PreviewScreen.vue +1 -2
- package/app/components/site/SiteAside.vue +2 -4
- package/app/components/site/SiteMain.vue +1 -4
- package/app/components/tree/TreeItem.vue +1 -1
- package/app/composables/og.ts +19 -2
- package/app/pages/contributor/[contributorId].vue +1 -2
- package/app/pages/index.vue +1 -4
- package/app/styles/main.css +0 -1
- package/package.json +4 -4
- package/server/erudit/cameos/build.ts +77 -27
- package/server/erudit/content/global/build.ts +27 -1
- package/server/erudit/content/nav/build.ts +36 -4
- package/server/erudit/content/repository/elementSnippets.ts +51 -11
- package/server/erudit/content/repository/externals.ts +38 -9
- package/server/erudit/content/resolve/index.ts +172 -21
- package/server/erudit/content/resolve/topic.ts +93 -32
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +48 -9
- package/server/erudit/content/search.ts +31 -3
- package/server/erudit/contributors/build.ts +106 -51
- package/server/erudit/db/repository/pushFile.ts +7 -4
- package/server/erudit/db/schema/content.ts +2 -2
- package/server/erudit/db/schema/contentSnippets.ts +3 -4
- package/server/erudit/language/list/en.ts +2 -0
- package/server/erudit/language/list/ru.ts +2 -0
- package/server/erudit/news/build.ts +85 -48
- package/server/erudit/sponsors/build.ts +77 -26
- package/shared/types/contentConnections.ts +2 -2
- package/shared/types/elementSnippet.ts +9 -3
- package/shared/types/language.ts +2 -0
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
import Deps from './Deps.vue';
|
|
3
3
|
import Externals from './Externals.vue';
|
|
4
4
|
|
|
5
|
-
defineProps<{ connections?: ContentConnections }>();
|
|
5
|
+
const { connections } = defineProps<{ connections?: ContentConnections }>();
|
|
6
6
|
|
|
7
7
|
const phrase = await usePhrases('connections');
|
|
8
8
|
|
|
9
9
|
const currentType = ref<keyof ContentConnections | undefined>(
|
|
10
10
|
'hardDependencies',
|
|
11
11
|
);
|
|
12
|
+
|
|
13
|
+
const ownExternalsCount = computed(() => {
|
|
14
|
+
return connections?.externals?.find((ext) => ext.type === 'own')?.items
|
|
15
|
+
.length;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const parentExternalsCount = computed(() => {
|
|
19
|
+
return connections?.externals
|
|
20
|
+
?.filter((ext) => ext.type === 'parent')
|
|
21
|
+
.reduce((sum, ext) => sum + ext.items.length, 0);
|
|
22
|
+
});
|
|
12
23
|
</script>
|
|
13
24
|
|
|
14
25
|
<template>
|
|
@@ -18,17 +29,59 @@ const currentType = ref<keyof ContentConnections | undefined>(
|
|
|
18
29
|
class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
|
|
19
30
|
justify-center"
|
|
20
31
|
>
|
|
32
|
+
<template
|
|
33
|
+
v-for="(items, type) of {
|
|
34
|
+
hardDependencies: connections.hardDependencies,
|
|
35
|
+
autoDependencies: connections.autoDependencies,
|
|
36
|
+
dependents: connections.dependents,
|
|
37
|
+
}"
|
|
38
|
+
>
|
|
39
|
+
<MainConnectionsButton
|
|
40
|
+
v-if="items && items.length > 0"
|
|
41
|
+
:type="type"
|
|
42
|
+
:count="items.length"
|
|
43
|
+
:active="currentType === type"
|
|
44
|
+
@click="
|
|
45
|
+
currentType === type
|
|
46
|
+
? (currentType = undefined)
|
|
47
|
+
: (currentType = type)
|
|
48
|
+
"
|
|
49
|
+
/>
|
|
50
|
+
</template>
|
|
21
51
|
<MainConnectionsButton
|
|
22
|
-
v-
|
|
23
|
-
|
|
24
|
-
:
|
|
25
|
-
:active="currentType === type"
|
|
52
|
+
v-if="connections.externals"
|
|
53
|
+
type="externals"
|
|
54
|
+
:active="currentType === 'externals'"
|
|
26
55
|
@click="
|
|
27
|
-
currentType ===
|
|
56
|
+
currentType === 'externals'
|
|
28
57
|
? (currentType = undefined)
|
|
29
|
-
: (currentType =
|
|
58
|
+
: (currentType = 'externals')
|
|
30
59
|
"
|
|
31
|
-
|
|
60
|
+
>
|
|
61
|
+
<template #after>
|
|
62
|
+
<div
|
|
63
|
+
v-if="connections.externals"
|
|
64
|
+
class="gap-small *:border-border *:pl-small flex
|
|
65
|
+
items-center font-bold *:border-l"
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
v-if="ownExternalsCount"
|
|
69
|
+
class="flex items-center gap-1 text-amber-600
|
|
70
|
+
dark:text-amber-400"
|
|
71
|
+
>
|
|
72
|
+
<MyIcon name="arrow/left" class="-scale-x-100" />
|
|
73
|
+
<span>{{ ownExternalsCount }}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div
|
|
76
|
+
v-if="parentExternalsCount"
|
|
77
|
+
class="flex items-center gap-1"
|
|
78
|
+
>
|
|
79
|
+
<MyIcon name="arrow/up-to-right" />
|
|
80
|
+
<span>{{ parentExternalsCount }}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
</MainConnectionsButton>
|
|
32
85
|
</div>
|
|
33
86
|
<template v-if="currentType && connections[currentType]">
|
|
34
87
|
<Deps
|
|
@@ -4,7 +4,7 @@ import type { MyIconName } from '#my-icons';
|
|
|
4
4
|
const { type, active } = defineProps<{
|
|
5
5
|
type: 'hardDependencies' | 'autoDependencies' | 'dependents' | 'externals';
|
|
6
6
|
active?: boolean;
|
|
7
|
-
count
|
|
7
|
+
count?: number;
|
|
8
8
|
}>();
|
|
9
9
|
|
|
10
10
|
const isHard = type === 'hardDependencies';
|
|
@@ -75,6 +75,7 @@ const dynamicClasses = computed(() => {
|
|
|
75
75
|
>
|
|
76
76
|
<MyIcon :name="icon" class="text-[1.2em]" />
|
|
77
77
|
<span>{{ formatText(title) }}</span>
|
|
78
|
-
<span class="font-bold">{{ count }}</span>
|
|
78
|
+
<span v-if="count" class="font-bold">{{ count }}</span>
|
|
79
|
+
<slot name="after"></slot>
|
|
79
80
|
</button>
|
|
80
81
|
</template>
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<template>
|
|
4
4
|
<div
|
|
5
|
-
class="nice-scrollbars border-border
|
|
6
|
-
|
|
7
|
-
transition-[border]"
|
|
5
|
+
class="nice-scrollbars border-border mt-normal relative overflow-auto
|
|
6
|
+
border-t border-b"
|
|
8
7
|
>
|
|
9
8
|
<slot></slot>
|
|
10
9
|
</div>
|
|
@@ -22,8 +22,7 @@ const { mode = 'detailed' } = defineProps<{
|
|
|
22
22
|
<div
|
|
23
23
|
v-else
|
|
24
24
|
class="gap-small px-small text-main-sm border-border bg-bg-aside flex
|
|
25
|
-
items-center rounded-xl border py-1
|
|
26
|
-
transition-[background,color,border]"
|
|
25
|
+
items-center rounded-xl border py-1"
|
|
27
26
|
>
|
|
28
27
|
<MaybeMyIcon
|
|
29
28
|
:name="icon"
|
|
@@ -15,8 +15,8 @@ const phrase = await usePhrases('stats');
|
|
|
15
15
|
<section v-if="mode === 'single'" class="px-main py-main-half">
|
|
16
16
|
<MainSubTitle :title="phrase.stats + ':'" />
|
|
17
17
|
<div
|
|
18
|
-
class="micro:justify-start gap-normal flex
|
|
19
|
-
justify-center"
|
|
18
|
+
class="micro:justify-start gap-small micro:gap-normal flex
|
|
19
|
+
flex-wrap justify-center"
|
|
20
20
|
>
|
|
21
21
|
<ItemMaterials
|
|
22
22
|
v-if="stats.materials"
|
|
@@ -32,7 +32,10 @@ const phrase = await usePhrases('stats');
|
|
|
32
32
|
/>
|
|
33
33
|
</div>
|
|
34
34
|
</section>
|
|
35
|
-
<div
|
|
35
|
+
<div
|
|
36
|
+
v-else
|
|
37
|
+
class="gap-small micro:gap-normal text-main-sm flex flex-wrap"
|
|
38
|
+
>
|
|
36
39
|
<ItemMaterials
|
|
37
40
|
v-if="stats.materials"
|
|
38
41
|
:count="stats.materials"
|
|
@@ -117,8 +117,7 @@ await usePhrases(
|
|
|
117
117
|
`border-border bg-bg-main micro:max-h-[70dvh]
|
|
118
118
|
pointer-events-auto absolute bottom-0 max-h-[90dvh] w-full
|
|
119
119
|
touch-auto overflow-hidden rounded-[25px] rounded-b-none
|
|
120
|
-
border-t
|
|
121
|
-
transition-[box-shadow,background,border,max-height,height,translate]`,
|
|
120
|
+
border-t transition-[max-height,height,translate]`,
|
|
122
121
|
previewState.opened
|
|
123
122
|
? `translate-y-0
|
|
124
123
|
shadow-[0px_-10px_15px_5px_light-dark(rgba(0,0,0,0.1),rgba(255,255,255,0.05))]`
|
|
@@ -19,8 +19,7 @@ const { closePreview, hasPreviousRequest, setPreviousPreview } = usePreview();
|
|
|
19
19
|
</div>
|
|
20
20
|
<div
|
|
21
21
|
class="border-border gap-small micro:gap-normal micro:h-[60px]
|
|
22
|
-
px-main flex h-[54px] shrink-0 items-center border-t
|
|
23
|
-
transition-[border]"
|
|
22
|
+
px-main flex h-[54px] shrink-0 items-center border-t"
|
|
24
23
|
>
|
|
25
24
|
<MaybeMyIcon
|
|
26
25
|
:name="icon"
|
|
@@ -31,8 +31,7 @@ const opened = computed(() => {
|
|
|
31
31
|
<div
|
|
32
32
|
:class="[
|
|
33
33
|
`bg-bg-aside absolute top-0 left-0 h-full w-full
|
|
34
|
-
border-[light-dark(var(--color-neutral-200),var(--color-neutral-800))]
|
|
35
|
-
transition-[background,border,backdrop-filter,box-shadow]`,
|
|
34
|
+
border-[light-dark(var(--color-neutral-200),var(--color-neutral-800))]`,
|
|
36
35
|
{
|
|
37
36
|
'border-e': isMajor,
|
|
38
37
|
'border-s': isMinor,
|
|
@@ -55,8 +54,7 @@ const opened = computed(() => {
|
|
|
55
54
|
`pointer-events-none absolute top-0 h-full w-full
|
|
56
55
|
touch-none bg-linear-to-l
|
|
57
56
|
from-[light-dark(rgba(0,0,0,0.02),rgba(0,0,0,0.1))]
|
|
58
|
-
via-transparent via-[3px] opacity-100
|
|
59
|
-
transition-opacity`,
|
|
57
|
+
via-transparent via-[3px] opacity-100`,
|
|
60
58
|
{
|
|
61
59
|
'-scale-100': isMinor,
|
|
62
60
|
'max-aside1:opacity-0': opened && isMajor,
|
|
@@ -16,7 +16,7 @@ defineProps<{
|
|
|
16
16
|
:style="{ '--level': level ? +level : 0 }"
|
|
17
17
|
:class="[
|
|
18
18
|
`px-normal py-small hocus:bg-bg-accent flex cursor-pointer
|
|
19
|
-
items-center gap-[calc(var(--spacing-normal)/
|
|
19
|
+
items-center gap-[calc(var(--spacing-normal)/2)] text-[.85em]
|
|
20
20
|
transition-[background,color]`,
|
|
21
21
|
'pl-[calc(var(--spacing-normal)*(var(--level)+1)-2px)]',
|
|
22
22
|
{
|
package/app/composables/og.ts
CHANGED
|
@@ -118,9 +118,26 @@ export async function useContentSeo(args: {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const elementPhrase = await getElementPhrase(snippet.schemaName);
|
|
121
|
+
|
|
122
|
+
const title = (() => {
|
|
123
|
+
if (snippet.seo?.title) {
|
|
124
|
+
return snippet.seo.title;
|
|
125
|
+
} else {
|
|
126
|
+
return snippet.title;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
|
|
130
|
+
const description = (() => {
|
|
131
|
+
if (snippet.seo?.description) {
|
|
132
|
+
return snippet.seo.description;
|
|
133
|
+
} else {
|
|
134
|
+
return snippet.description;
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
|
|
121
138
|
setupSeo({
|
|
122
|
-
title: `${
|
|
123
|
-
description:
|
|
139
|
+
title: `${title} [${elementPhrase.element_name}] - ${seoSiteTitle}`,
|
|
140
|
+
description: description || '',
|
|
124
141
|
urlPath: snippet.link,
|
|
125
142
|
});
|
|
126
143
|
},
|
|
@@ -86,8 +86,7 @@ useStandartSeo({
|
|
|
86
86
|
:style="{ '--mediaColor': color }"
|
|
87
87
|
class="border-bg-main micro:size-[110px] size-[80px]
|
|
88
88
|
rounded-full border-2
|
|
89
|
-
[box-shadow:0_0_16px_0_var(--color)]
|
|
90
|
-
transition-[border]"
|
|
89
|
+
[box-shadow:0_0_16px_0_var(--color)]"
|
|
91
90
|
/>
|
|
92
91
|
</div>
|
|
93
92
|
<div
|
package/app/pages/index.vue
CHANGED
|
@@ -38,10 +38,7 @@ const phrase = await usePhrases('x_contributors', 'x_sponsors');
|
|
|
38
38
|
:style="{
|
|
39
39
|
'max-width': `min(${indexPage.topImage.maxWidth || '100%'}, 100%)`,
|
|
40
40
|
}"
|
|
41
|
-
:class="[
|
|
42
|
-
'pt-main-half px-main mx-auto block transition-[filter]',
|
|
43
|
-
logotypeInvertClass,
|
|
44
|
-
]"
|
|
41
|
+
:class="['pt-main-half px-main mx-auto block', logotypeInvertClass]"
|
|
45
42
|
/>
|
|
46
43
|
|
|
47
44
|
<!-- Main Data -->
|
package/app/styles/main.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erudit",
|
|
3
|
-
"version": "4.0.0-dev.
|
|
3
|
+
"version": "4.0.0-dev.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "🤓 CMS for perfect educational sites.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"test": "bun vitest run"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@erudit-js/cli": "4.0.0-dev.
|
|
33
|
-
"@erudit-js/core": "4.0.0-dev.
|
|
34
|
-
"@erudit-js/prose": "4.0.0-dev.
|
|
32
|
+
"@erudit-js/cli": "4.0.0-dev.2",
|
|
33
|
+
"@erudit-js/core": "4.0.0-dev.2",
|
|
34
|
+
"@erudit-js/prose": "4.0.0-dev.2",
|
|
35
35
|
"@floating-ui/vue": "^1.1.9",
|
|
36
36
|
"@jsprose/core": "^1.0.0",
|
|
37
37
|
"@tailwindcss/vite": "^4.1.17",
|
|
@@ -1,44 +1,96 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
3
|
import type { Cameo, CameoConfig } from '@erudit-js/core/cameo';
|
|
4
4
|
|
|
5
|
+
let initialBuild = true;
|
|
6
|
+
|
|
7
|
+
const cameosRoot = () => `${ERUDIT.config.paths.project}/cameos`;
|
|
8
|
+
|
|
5
9
|
export async function buildCameos() {
|
|
6
10
|
ERUDIT.log.debug.start('Building cameos...');
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.delete(ERUDIT.db.schema.files)
|
|
11
|
-
.where(eq(ERUDIT.db.schema.files.role, 'cameo-avatar'));
|
|
12
|
-
|
|
13
|
-
let cameoIds: string[] = [];
|
|
12
|
+
const isInitial = initialBuild;
|
|
13
|
+
initialBuild = false;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
cameoIds = readdirSync(ERUDIT.config.paths.project + '/cameos', {
|
|
17
|
-
withFileTypes: true,
|
|
18
|
-
})
|
|
19
|
-
.filter((entry) => entry.isDirectory())
|
|
20
|
-
.map((entry) => entry.name);
|
|
21
|
-
} catch {}
|
|
15
|
+
const cameoIds = collectCameoIds(isInitial);
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
if (!cameoIds.size) {
|
|
18
|
+
ERUDIT.log.info(
|
|
19
|
+
isInitial
|
|
20
|
+
? 'Skipping cameos — no cameos found.'
|
|
21
|
+
: 'Skipping cameos — nothing changed.',
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
24
25
|
|
|
25
26
|
for (const cameoId of cameoIds) {
|
|
27
|
+
await cleanupCameo(cameoId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const existingIds = [...cameoIds].filter((id) =>
|
|
31
|
+
existsSync(`${cameosRoot()}/${id}`),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!existingIds.length) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const cameoId of existingIds) {
|
|
26
39
|
await buildCameo(cameoId);
|
|
27
|
-
cameoCount++;
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
ERUDIT.log.success(
|
|
31
|
-
|
|
43
|
+
isInitial
|
|
44
|
+
? `Cameos build complete! (${ERUDIT.log.stress(cameoIds.size)})`
|
|
45
|
+
: `Cameos updated: ${ERUDIT.log.stress(existingIds.join(', '))}`,
|
|
32
46
|
);
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
//
|
|
50
|
+
//
|
|
51
|
+
//
|
|
52
|
+
|
|
53
|
+
function collectCameoIds(initial: boolean): Set<string> {
|
|
54
|
+
if (initial) {
|
|
55
|
+
try {
|
|
56
|
+
return new Set(
|
|
57
|
+
readdirSync(cameosRoot(), { withFileTypes: true })
|
|
58
|
+
.filter((entry) => entry.isDirectory())
|
|
59
|
+
.map((entry) => entry.name),
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
return new Set();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ids = new Set<string>();
|
|
67
|
+
|
|
68
|
+
for (const file of ERUDIT.changedFiles.values()) {
|
|
69
|
+
if (!file.startsWith(`${cameosRoot()}/`)) continue;
|
|
70
|
+
const id = file.replace(`${cameosRoot()}/`, '').split('/')[0];
|
|
71
|
+
if (id) ids.add(id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return ids;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function cleanupCameo(cameoId: string) {
|
|
78
|
+
await ERUDIT.db
|
|
79
|
+
.delete(ERUDIT.db.schema.cameos)
|
|
80
|
+
.where(eq(ERUDIT.db.schema.cameos.cameoId, cameoId));
|
|
81
|
+
|
|
82
|
+
await ERUDIT.db
|
|
83
|
+
.delete(ERUDIT.db.schema.files)
|
|
84
|
+
.where(eq(ERUDIT.db.schema.files.role, 'cameo-avatar'));
|
|
85
|
+
}
|
|
86
|
+
|
|
35
87
|
async function buildCameo(cameoId: string) {
|
|
36
88
|
ERUDIT.log.debug.start(`Building cameo ${ERUDIT.log.stress(cameoId)}...`);
|
|
37
89
|
|
|
38
|
-
const
|
|
39
|
-
const
|
|
90
|
+
const dir = `${cameosRoot()}/${cameoId}`;
|
|
91
|
+
const files = readdirSync(dir);
|
|
40
92
|
|
|
41
|
-
const hasConfig =
|
|
93
|
+
const hasConfig = files.some(
|
|
42
94
|
(file) => file === 'cameo.ts' || file === 'cameo.js',
|
|
43
95
|
);
|
|
44
96
|
|
|
@@ -52,9 +104,7 @@ async function buildCameo(cameoId: string) {
|
|
|
52
104
|
let cameoConfig: CameoConfig;
|
|
53
105
|
|
|
54
106
|
try {
|
|
55
|
-
cameoConfig = (await ERUDIT.import(
|
|
56
|
-
`${cameoDirectory}/cameo`,
|
|
57
|
-
)) as CameoConfig;
|
|
107
|
+
cameoConfig = (await ERUDIT.import(`${dir}/cameo`)) as CameoConfig;
|
|
58
108
|
} catch (error) {
|
|
59
109
|
const message = error instanceof Error ? error.message : String(error);
|
|
60
110
|
ERUDIT.log.error(
|
|
@@ -63,7 +113,7 @@ async function buildCameo(cameoId: string) {
|
|
|
63
113
|
return;
|
|
64
114
|
}
|
|
65
115
|
|
|
66
|
-
const avatarExtension =
|
|
116
|
+
const avatarExtension = files
|
|
67
117
|
.find((file) => file.startsWith('avatar.'))
|
|
68
118
|
?.split('.')
|
|
69
119
|
.pop();
|
|
@@ -76,14 +126,14 @@ async function buildCameo(cameoId: string) {
|
|
|
76
126
|
}
|
|
77
127
|
|
|
78
128
|
await ERUDIT.repository.db.pushFile(
|
|
79
|
-
`${
|
|
129
|
+
`${dir}/avatar.${avatarExtension}`,
|
|
80
130
|
'cameo-avatar',
|
|
81
131
|
);
|
|
82
132
|
|
|
83
133
|
const icon = (() => {
|
|
84
|
-
const iconFile =
|
|
134
|
+
const iconFile = files.find((file) => file === 'icon.svg');
|
|
85
135
|
if (iconFile) {
|
|
86
|
-
return readFileSync(
|
|
136
|
+
return readFileSync(dir + '/' + iconFile, 'utf-8');
|
|
87
137
|
}
|
|
88
138
|
})();
|
|
89
139
|
|
|
@@ -5,9 +5,21 @@ import { $CONTENT } from './singleton';
|
|
|
5
5
|
// Call singleton to trigger initialization
|
|
6
6
|
$CONTENT;
|
|
7
7
|
|
|
8
|
+
let initialBuild = true;
|
|
9
|
+
|
|
10
|
+
const contentRoot = () => `${ERUDIT.config.paths.project}/content`;
|
|
11
|
+
|
|
8
12
|
export async function buildGlobalContent() {
|
|
9
13
|
ERUDIT.log.debug.start('Building global content...');
|
|
10
14
|
|
|
15
|
+
const isInitial = initialBuild;
|
|
16
|
+
initialBuild = false;
|
|
17
|
+
|
|
18
|
+
if (!isInitial && !hasContentChanges()) {
|
|
19
|
+
ERUDIT.log.info('Skipping global content — nothing changed.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
const linkObject = await buildLinkObject();
|
|
12
24
|
|
|
13
25
|
const linkTypes = linkObjectToTypes(linkObject);
|
|
@@ -17,7 +29,21 @@ export async function buildGlobalContent() {
|
|
|
17
29
|
'utf-8',
|
|
18
30
|
);
|
|
19
31
|
|
|
20
|
-
ERUDIT.log.success(
|
|
32
|
+
ERUDIT.log.success(
|
|
33
|
+
isInitial
|
|
34
|
+
? 'Global content build complete!'
|
|
35
|
+
: 'Global content updated!',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasContentChanges() {
|
|
40
|
+
for (const file of (ERUDIT.changedFiles || new Set<string>()).values()) {
|
|
41
|
+
if (file.startsWith(`${contentRoot()}/`)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
21
47
|
}
|
|
22
48
|
|
|
23
49
|
function linkObjectToTypes(linkObject: any): string {
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { globSync } from 'glob';
|
|
3
|
-
import { readdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { contentTypes, type ContentType } from '@erudit-js/core/content/type';
|
|
5
5
|
|
|
6
6
|
import type { ContentNavNode, ContentNavMap } from './types';
|
|
7
7
|
|
|
8
|
+
let initialBuild = true;
|
|
9
|
+
|
|
10
|
+
const contentRoot = () => `${ERUDIT.config.paths.project}/content`;
|
|
11
|
+
|
|
8
12
|
export async function buildContentNav() {
|
|
9
13
|
ERUDIT.log.debug.start('Building content navigation...');
|
|
10
14
|
|
|
15
|
+
const isInitial = initialBuild;
|
|
16
|
+
initialBuild = false;
|
|
17
|
+
|
|
18
|
+
if (!isInitial && !hasContentChanges()) {
|
|
19
|
+
ERUDIT.log.info('Skipping content navigation — nothing changed.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
ERUDIT.contentNav.id2Node = new Map();
|
|
12
24
|
ERUDIT.contentNav.id2Root = new Map();
|
|
13
25
|
ERUDIT.contentNav.id2Books = new Map();
|
|
14
26
|
ERUDIT.contentNav.short2Full = new Map();
|
|
15
27
|
|
|
16
|
-
const cwd =
|
|
28
|
+
const cwd = contentRoot();
|
|
17
29
|
const contentDirectories = globSync('**/*/', {
|
|
18
30
|
cwd,
|
|
19
31
|
posix: true,
|
|
@@ -122,7 +134,11 @@ export async function buildContentNav() {
|
|
|
122
134
|
? getContentNavStats(ERUDIT.contentNav.id2Node)
|
|
123
135
|
: chalk.gray('empty');
|
|
124
136
|
|
|
125
|
-
ERUDIT.log.success(
|
|
137
|
+
ERUDIT.log.success(
|
|
138
|
+
isInitial
|
|
139
|
+
? `Content navigation build complete! (${stats})`
|
|
140
|
+
: `Content navigation updated! (${stats})`,
|
|
141
|
+
);
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
/**
|
|
@@ -136,7 +152,13 @@ async function createStandaloneContentNavNode(
|
|
|
136
152
|
const parsedContentPath = parseContentPath(relPath);
|
|
137
153
|
if (!parsedContentPath) return;
|
|
138
154
|
|
|
139
|
-
const
|
|
155
|
+
const dirPath = `${cwd}/${relPath}`;
|
|
156
|
+
|
|
157
|
+
if (!existsSync(dirPath)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const files = readdirSync(dirPath).reduce<Record<string, null>>(
|
|
140
162
|
(acc, name) => {
|
|
141
163
|
acc[name] = null;
|
|
142
164
|
return acc;
|
|
@@ -271,6 +293,16 @@ function getContentNavStats(id2Node: ContentNavMap) {
|
|
|
271
293
|
return parts.join('; ');
|
|
272
294
|
}
|
|
273
295
|
|
|
296
|
+
function hasContentChanges() {
|
|
297
|
+
for (const file of (ERUDIT.changedFiles || new Set<string>()).values()) {
|
|
298
|
+
if (file.startsWith(`${contentRoot()}/`)) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
274
306
|
//
|
|
275
307
|
// Validators
|
|
276
308
|
//
|
|
@@ -10,10 +10,7 @@ export async function getContentElementSnippets(
|
|
|
10
10
|
contentProseType: true,
|
|
11
11
|
schemaName: true,
|
|
12
12
|
elementId: true,
|
|
13
|
-
|
|
14
|
-
description: true,
|
|
15
|
-
quick: true,
|
|
16
|
-
seo: true,
|
|
13
|
+
snippetData: true,
|
|
17
14
|
},
|
|
18
15
|
where: and(
|
|
19
16
|
eq(ERUDIT.db.schema.contentSnippets.contentFullId, fullId),
|
|
@@ -40,22 +37,65 @@ export async function getContentElementSnippets(
|
|
|
40
37
|
);
|
|
41
38
|
})();
|
|
42
39
|
|
|
40
|
+
const snippetData = dbSnippet.snippetData;
|
|
41
|
+
|
|
43
42
|
const snippet: ElementSnippet = {
|
|
44
43
|
link,
|
|
45
44
|
schemaName: dbSnippet.schemaName,
|
|
46
|
-
title:
|
|
45
|
+
title: snippetData.title!,
|
|
47
46
|
};
|
|
48
47
|
|
|
49
|
-
if (
|
|
50
|
-
snippet.quick =
|
|
48
|
+
if (snippetData.quick) {
|
|
49
|
+
snippet.quick = {};
|
|
50
|
+
let quickTitle: string | undefined;
|
|
51
|
+
let quickDescription: string | undefined;
|
|
52
|
+
|
|
53
|
+
if (typeof snippetData.quick === 'string') {
|
|
54
|
+
quickTitle = snippetData.quick;
|
|
55
|
+
} else if (typeof snippetData.quick === 'object') {
|
|
56
|
+
if (snippetData.quick.title) {
|
|
57
|
+
quickTitle = snippetData.quick.title;
|
|
58
|
+
}
|
|
59
|
+
if (snippetData.quick.description) {
|
|
60
|
+
quickDescription = snippetData.quick.description;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (quickTitle) {
|
|
65
|
+
snippet.quick.title = quickTitle;
|
|
66
|
+
}
|
|
67
|
+
if (quickDescription) {
|
|
68
|
+
snippet.quick.description = quickDescription;
|
|
69
|
+
}
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
if (
|
|
54
|
-
snippet.seo =
|
|
72
|
+
if (snippetData.seo) {
|
|
73
|
+
snippet.seo = {};
|
|
74
|
+
|
|
75
|
+
let seoTitle: string | undefined;
|
|
76
|
+
let seoDescription: string | undefined;
|
|
77
|
+
|
|
78
|
+
if (typeof snippetData.seo === 'string') {
|
|
79
|
+
seoTitle = snippetData.seo;
|
|
80
|
+
} else if (typeof snippetData.seo === 'object') {
|
|
81
|
+
if (snippetData.seo.title) {
|
|
82
|
+
seoTitle = snippetData.seo.title;
|
|
83
|
+
}
|
|
84
|
+
if (snippetData.seo.description) {
|
|
85
|
+
seoDescription = snippetData.seo.description;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (seoTitle) {
|
|
90
|
+
snippet.seo.title = seoTitle;
|
|
91
|
+
}
|
|
92
|
+
if (seoDescription) {
|
|
93
|
+
snippet.seo.description = seoDescription;
|
|
94
|
+
}
|
|
55
95
|
}
|
|
56
96
|
|
|
57
|
-
if (
|
|
58
|
-
snippet.description =
|
|
97
|
+
if (snippetData.description) {
|
|
98
|
+
snippet.description = snippetData.description;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
snippets.push(snippet);
|