erudit 4.3.3 → 4.3.4

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.
@@ -1,28 +1,36 @@
1
1
  <script lang="ts" setup>
2
2
  defineProps<{ breadcrumbs: Breadcrumbs }>();
3
+
4
+ const phrase = await usePhrases('breadcrumb');
3
5
  </script>
4
6
 
5
7
  <template>
6
- <section
8
+ <nav
7
9
  v-if="breadcrumbs.length > 0"
8
- class="gap-small max-micro:justify-center px-main py-main-half flex
9
- flex-wrap"
10
+ :aria-label="phrase.breadcrumb"
11
+ class="px-main py-main-half"
10
12
  >
11
- <EruditLink
12
- v-for="(breadcrumb, i) of breadcrumbs"
13
- :to="breadcrumb.link"
14
- class="gap-small text-text-dimmed hocus:text-text-muted flex items-center
15
- transition-[color]"
13
+ <ol
14
+ class="gap-small max-micro:justify-center m-0 flex list-none flex-wrap
15
+ p-0"
16
16
  >
17
- <MaybeMyIcon :name="breadcrumb.icon" class="text-[1.2em]" />
18
- <span>{{ formatText(breadcrumb.title) }}</span>
19
- <MyIcon
20
- name="chevron-right"
21
- :class="{
22
- 'relative -left-[3px]': true,
23
- 'rotate-90': i === breadcrumbs.length - 1,
24
- }"
25
- />
26
- </EruditLink>
27
- </section>
17
+ <li v-for="(breadcrumb, i) of breadcrumbs">
18
+ <EruditLink
19
+ :to="breadcrumb.link"
20
+ class="gap-small text-text-dimmed hocus:text-text-muted flex
21
+ items-center transition-[color]"
22
+ >
23
+ <MaybeMyIcon :name="breadcrumb.icon" class="text-[1.2em]" />
24
+ <span>{{ formatText(breadcrumb.title) }}</span>
25
+ <MyIcon
26
+ name="chevron-right"
27
+ :class="{
28
+ 'relative -left-[3px]': true,
29
+ 'rotate-90': i === breadcrumbs.length - 1,
30
+ }"
31
+ />
32
+ </EruditLink>
33
+ </li>
34
+ </ol>
35
+ </nav>
28
36
  </template>
@@ -19,16 +19,22 @@ const phrase = await usePhrases('key_elements');
19
19
 
20
20
  <template>
21
21
  <template v-if="keyLinks">
22
- <section v-if="mode === 'single'" class="px-main py-main-half">
22
+ <nav
23
+ v-if="mode === 'single'"
24
+ :aria-label="phrase.key_elements"
25
+ class="px-main py-main-half"
26
+ >
23
27
  <MainSubTitle :title="phrase.key_elements + ':'" />
24
- <div
28
+ <ul
25
29
  :style="{ '--keyBg': 'var(--color-bg-aside)' }"
26
- class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
27
- justify-center"
30
+ class="gap-small micro:gap-normal micro:justify-start m-0 flex list-none
31
+ flex-wrap justify-center p-0"
28
32
  >
29
- <MainKeyLink v-for="keyLink of keyLinks" :keyLink />
30
- </div>
31
- </section>
33
+ <li v-for="keyLink of keyLinks">
34
+ <MainKeyLink :keyLink />
35
+ </li>
36
+ </ul>
37
+ </nav>
32
38
  <div
33
39
  v-else
34
40
  :style="{ '--keyBg': 'var(--color-bg-main)' }"
@@ -3,9 +3,10 @@ defineProps<{ title: string }>();
3
3
  </script>
4
4
 
5
5
  <template>
6
- <div
7
- class="text-main-sm micro:text-left pb-main-half text-center font-semibold"
6
+ <h2
7
+ class="text-main-sm micro:text-left pb-main-half m-0 text-center
8
+ font-semibold"
8
9
  >
9
10
  {{ formatText(title) }}
10
- </div>
11
+ </h2>
11
12
  </template>
@@ -49,6 +49,9 @@ await useContentSeo({
49
49
  snippets: mainContent.snippets,
50
50
  breadcrumbs: mainContent.breadcrumbs,
51
51
  lastmod: mainContent.lastmod,
52
+ contributions: mainContent.contributions,
53
+ flags: mainContent.flags,
54
+ connections: mainContent.connections,
52
55
  });
53
56
  </script>
54
57
 
@@ -27,11 +27,15 @@ const parentExternalsCount = computed(() => {
27
27
  </script>
28
28
 
29
29
  <template>
30
- <section v-if="connections" class="px-main py-main-half">
30
+ <section
31
+ v-if="connections"
32
+ :aria-label="phrase.connections"
33
+ class="px-main py-main-half"
34
+ >
31
35
  <MainSubTitle :title="phrase.connections + ':'" />
32
- <div
33
- class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
34
- justify-center"
36
+ <ul
37
+ class="gap-small micro:gap-normal micro:justify-start m-0 flex list-none
38
+ flex-wrap justify-center p-0"
35
39
  >
36
40
  <template
37
41
  v-for="(items, type) of {
@@ -40,49 +44,52 @@ const parentExternalsCount = computed(() => {
40
44
  dependents: connections.dependents,
41
45
  }"
42
46
  >
47
+ <li v-if="items && items.length > 0">
48
+ <MainConnectionsButton
49
+ :type="type"
50
+ :count="items.length"
51
+ :active="currentType === type"
52
+ @click="
53
+ currentType === type
54
+ ? (currentType = undefined)
55
+ : (currentType = type)
56
+ "
57
+ />
58
+ </li>
59
+ </template>
60
+ <li v-if="connections.externals">
43
61
  <MainConnectionsButton
44
- v-if="items && items.length > 0"
45
- :type="type"
46
- :count="items.length"
47
- :active="currentType === type"
62
+ type="externals"
63
+ :active="currentType === 'externals'"
48
64
  @click="
49
- currentType === type
65
+ currentType === 'externals'
50
66
  ? (currentType = undefined)
51
- : (currentType = type)
67
+ : (currentType = 'externals')
52
68
  "
53
- />
54
- </template>
55
- <MainConnectionsButton
56
- v-if="connections.externals"
57
- type="externals"
58
- :active="currentType === 'externals'"
59
- @click="
60
- currentType === 'externals'
61
- ? (currentType = undefined)
62
- : (currentType = 'externals')
63
- "
64
- >
65
- <template #after>
66
- <div
67
- v-if="connections.externals"
68
- class="gap-small *:border-border *:pl-small flex items-center
69
- font-bold *:border-l"
70
- >
69
+ >
70
+ <template #after>
71
71
  <div
72
- v-if="ownExternalsCount"
73
- class="flex items-center gap-1 text-amber-600 dark:text-amber-400"
72
+ v-if="connections.externals"
73
+ class="gap-small *:border-border *:pl-small flex items-center
74
+ font-bold *:border-l"
74
75
  >
75
- <MyIcon name="arrow/left" class="-scale-x-100" />
76
- <span>{{ ownExternalsCount }}</span>
77
- </div>
78
- <div v-if="parentExternalsCount" class="flex items-center gap-1">
79
- <MyIcon name="arrow/up-to-right" />
80
- <span>{{ parentExternalsCount }}</span>
76
+ <div
77
+ v-if="ownExternalsCount"
78
+ class="flex items-center gap-1 text-amber-600
79
+ dark:text-amber-400"
80
+ >
81
+ <MyIcon name="arrow/left" class="-scale-x-100" />
82
+ <span>{{ ownExternalsCount }}</span>
83
+ </div>
84
+ <div v-if="parentExternalsCount" class="flex items-center gap-1">
85
+ <MyIcon name="arrow/up-to-right" />
86
+ <span>{{ parentExternalsCount }}</span>
87
+ </div>
81
88
  </div>
82
- </div>
83
- </template>
84
- </MainConnectionsButton>
85
- </div>
89
+ </template>
90
+ </MainConnectionsButton>
91
+ </li>
92
+ </ul>
86
93
  <template v-if="currentType && connections[currentType]">
87
94
  <Deps
88
95
  v-if="currentType !== 'externals'"
@@ -15,27 +15,26 @@ const phrase = await usePhrases('stats');
15
15
  <template>
16
16
  <section
17
17
  v-if="mode === 'single' && (stats || lastChangedDate)"
18
+ :aria-label="phrase.stats"
18
19
  class="px-main py-main-half"
19
20
  >
20
21
  <MainSubTitle :title="phrase.stats + ':'" />
21
- <div
22
- class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
23
- justify-center"
22
+ <ul
23
+ class="micro:justify-start gap-small micro:gap-normal m-0 flex list-none
24
+ flex-wrap justify-center p-0"
24
25
  >
25
- <ItemMaterials
26
- v-if="stats?.materials"
27
- :count="stats.materials"
28
- mode="detailed"
29
- />
30
- <ItemElement
31
- v-if="stats?.elements"
32
- v-for="(count, schemaName) of stats.elements"
33
- :schemaName
34
- :count
35
- mode="detailed"
36
- />
37
- <ItemLastChanged v-if="lastChangedDate" :date="lastChangedDate" />
38
- </div>
26
+ <li v-if="stats?.materials">
27
+ <ItemMaterials :count="stats.materials" mode="detailed" />
28
+ </li>
29
+ <template v-if="stats?.elements">
30
+ <li v-for="(count, schemaName) of stats.elements">
31
+ <ItemElement :schemaName :count mode="detailed" />
32
+ </li>
33
+ </template>
34
+ <li v-if="lastChangedDate">
35
+ <ItemLastChanged :date="lastChangedDate" />
36
+ </li>
37
+ </ul>
39
38
  </section>
40
39
  <div
41
40
  v-else-if="mode === 'children' && stats"
@@ -112,7 +112,7 @@ export function initFavicon() {
112
112
  let stopWatchingRoute: ReturnType<typeof watch> | undefined;
113
113
  onMounted(() => {
114
114
  stopWatchingRoute = watch(
115
- route,
115
+ () => route.path,
116
116
  () => {
117
117
  clearTimeout(contentFaviconChangeTimeout);
118
118
 
@@ -1,4 +1,14 @@
1
+ import type { ContentContribution } from '@erudit-js/core/content/contributions';
2
+ import type { ContentFlags } from '@erudit-js/core/content/flags';
3
+
1
4
  import type { Breadcrumbs } from '../../shared/types/breadcrumbs';
5
+ import type { ContentConnections } from '../../shared/types/contentConnections';
6
+ import type { ElementSnippet } from '../../shared/types/elementSnippet';
7
+ import type { MainContentChildrenItem } from '../../shared/types/mainContent';
8
+
9
+ function contentTypeToSchemaType(type: string): string {
10
+ return type === 'book' ? 'Book' : type === 'group' ? 'Course' : 'Article';
11
+ }
2
12
 
3
13
  export function useJsonLd(key: string, data: Record<string, unknown>) {
4
14
  useHead({
@@ -60,18 +70,19 @@ export function useContentArticleJsonLd(args: {
60
70
  urlPath: string;
61
71
  contentType: string;
62
72
  lastmod?: string;
73
+ keyElements?: ElementSnippet[];
74
+ breadcrumbs?: Breadcrumbs;
75
+ children?: MainContentChildrenItem[];
76
+ contributions?: ContentContribution[];
77
+ flags?: ContentFlags;
78
+ connections?: ContentConnections;
63
79
  }) {
64
80
  const withSiteUrl = useSiteUrl();
65
81
 
66
82
  const siteTitle =
67
83
  ERUDIT.config.seo?.siteTitle || ERUDIT.config.asideMajor?.siteInfo?.title;
68
84
 
69
- const schemaType =
70
- args.contentType === 'book'
71
- ? 'Book'
72
- : args.contentType === 'group'
73
- ? 'Course'
74
- : 'Article';
85
+ const schemaType = contentTypeToSchemaType(args.contentType);
75
86
 
76
87
  const data: Record<string, unknown> = {
77
88
  '@context': 'https://schema.org',
@@ -80,6 +91,7 @@ export function useContentArticleJsonLd(args: {
80
91
  ? { headline: formatText(args.title) }
81
92
  : { name: formatText(args.title) }),
82
93
  url: withSiteUrl(args.urlPath),
94
+ inLanguage: ERUDIT.config.language?.current || 'en',
83
95
  };
84
96
 
85
97
  if (args.description) {
@@ -90,12 +102,82 @@ export function useContentArticleJsonLd(args: {
90
102
  data.dateModified = args.lastmod;
91
103
  }
92
104
 
93
- if (siteTitle) {
105
+ const parentBreadcrumb =
106
+ args.breadcrumbs && args.breadcrumbs.length >= 1
107
+ ? args.breadcrumbs[args.breadcrumbs.length - 1]
108
+ : undefined;
109
+
110
+ if (parentBreadcrumb) {
111
+ data.isPartOf = {
112
+ '@type': 'WebPage',
113
+ name: formatText(parentBreadcrumb.title),
114
+ url: withSiteUrl(parentBreadcrumb.link),
115
+ };
116
+ } else if (siteTitle) {
94
117
  data.isPartOf = {
95
118
  '@type': 'WebSite',
96
119
  name: siteTitle,
97
120
  };
98
121
  }
99
122
 
123
+ const keyElementTerms = args.keyElements?.length
124
+ ? args.keyElements.map((el) => ({
125
+ '@type': 'DefinedTerm',
126
+ name: formatText(el.seo?.title || el.key?.title || el.title),
127
+ url: withSiteUrl(el.link),
128
+ }))
129
+ : [];
130
+
131
+ if (keyElementTerms.length > 0) {
132
+ data.about = keyElementTerms;
133
+ }
134
+
135
+ const hasPart: Record<string, unknown>[] = [];
136
+
137
+ if (args.children?.length) {
138
+ for (const child of args.children) {
139
+ const part: Record<string, unknown> = {
140
+ '@type': contentTypeToSchemaType(child.type),
141
+ name: formatText(child.title),
142
+ url: withSiteUrl(child.link),
143
+ };
144
+ if (child.description) {
145
+ part.description = formatText(child.description);
146
+ }
147
+ hasPart.push(part);
148
+ }
149
+ }
150
+
151
+ hasPart.push(...keyElementTerms);
152
+
153
+ if (hasPart.length > 0) {
154
+ data.hasPart = hasPart;
155
+ }
156
+
157
+ if (args.contributions?.length) {
158
+ data.author = args.contributions.map((c) => {
159
+ const person: Record<string, unknown> = {
160
+ '@type': 'Person',
161
+ name: c.name || c.contributorId,
162
+ };
163
+ if (c.avatarUrl) {
164
+ person.image = withSiteUrl(c.avatarUrl);
165
+ }
166
+ return person;
167
+ });
168
+ }
169
+
170
+ if (args.flags?.advanced) {
171
+ data.educationalLevel = 'Advanced';
172
+ }
173
+
174
+ if (args.connections?.hardDependencies?.length) {
175
+ data.isBasedOn = args.connections.hardDependencies.map((dep) => ({
176
+ '@type': contentTypeToSchemaType(dep.contentType),
177
+ name: formatText(dep.title),
178
+ url: withSiteUrl(dep.link),
179
+ }));
180
+ }
181
+
100
182
  useJsonLd('jsonld-content', data);
101
183
  }
@@ -95,6 +95,10 @@ export async function useContentSeo(args: {
95
95
  snippets?: ElementSnippet[];
96
96
  breadcrumbs?: Breadcrumbs;
97
97
  lastmod?: string;
98
+ children?: MainContentChildrenItem[];
99
+ contributions?: ContentContribution[];
100
+ flags?: ContentFlags;
101
+ connections?: ContentConnections;
98
102
  }) {
99
103
  const canUseBookTitle = ERUDIT.config.seo?.useBookSiteTitle;
100
104
 
@@ -161,6 +165,12 @@ export async function useContentSeo(args: {
161
165
  ? 'article'
162
166
  : args.contentTypePath.type,
163
167
  lastmod: args.lastmod,
168
+ keyElements: args.snippets?.filter((snippet) => !!snippet.key),
169
+ breadcrumbs: args.breadcrumbs,
170
+ children: args.children,
171
+ contributions: args.contributions,
172
+ flags: args.flags,
173
+ connections: args.connections,
164
174
  });
165
175
 
166
176
  //
@@ -31,6 +31,10 @@ await useContentSeo({
31
31
  seo: mainContent.seo,
32
32
  breadcrumbs: mainContent.breadcrumbs,
33
33
  lastmod: mainContent.lastmod,
34
+ children: mainContent.children,
35
+ contributions: mainContent.contributions,
36
+ flags: mainContent.flags,
37
+ connections: mainContent.connections,
34
38
  });
35
39
  </script>
36
40
 
@@ -33,6 +33,10 @@ await useContentSeo({
33
33
  seo: mainContent.seo,
34
34
  breadcrumbs: mainContent.breadcrumbs,
35
35
  lastmod: mainContent.lastmod,
36
+ children: mainContent.children,
37
+ contributions: mainContent.contributions,
38
+ flags: mainContent.flags,
39
+ connections: mainContent.connections,
36
40
  });
37
41
  </script>
38
42
 
@@ -31,6 +31,9 @@ await useContentSeo({
31
31
  snippets: mainContent.snippets,
32
32
  breadcrumbs: mainContent.breadcrumbs,
33
33
  lastmod: mainContent.lastmod,
34
+ contributions: mainContent.contributions,
35
+ flags: mainContent.flags,
36
+ connections: mainContent.connections,
34
37
  });
35
38
  </script>
36
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -24,9 +24,9 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@erudit-js/cli": "4.3.3",
28
- "@erudit-js/core": "4.3.3",
29
- "@erudit-js/prose": "4.3.3",
27
+ "@erudit-js/cli": "4.3.4",
28
+ "@erudit-js/core": "4.3.4",
29
+ "@erudit-js/prose": "4.3.4",
30
30
  "@floating-ui/vue": "^1.1.11",
31
31
  "@resvg/resvg-js": "^2.6.2",
32
32
  "@tailwindcss/vite": "^4.2.1",
@@ -34,7 +34,7 @@
34
34
  "chokidar": "^5.0.0",
35
35
  "consola": "^3.4.2",
36
36
  "drizzle-kit": "^0.31.10",
37
- "drizzle-orm": "^0.45.1",
37
+ "drizzle-orm": "^0.45.2",
38
38
  "esbuild": "^0.27.4",
39
39
  "flexsearch": "^0.8.212",
40
40
  "glob": "^13.0.6",
@@ -43,7 +43,7 @@
43
43
  "nuxt": "4.4.2",
44
44
  "nuxt-my-icons": "1.2.2",
45
45
  "perfect-debounce": "^2.1.0",
46
- "satori": "^0.25.0",
46
+ "satori": "^0.26.0",
47
47
  "sharp": "^0.34.5",
48
48
  "tailwindcss": "^4.2.1",
49
49
  "ts-xor": "^1.3.0",
@@ -61,6 +61,14 @@ export async function buildServerErudit() {
61
61
  // Watcher
62
62
  //
63
63
 
64
+ const watchedProjectDirs = [
65
+ 'content',
66
+ 'contributors',
67
+ 'cameos',
68
+ 'sponsors',
69
+ 'news',
70
+ ] as const;
71
+
64
72
  export async function tryServerWatchProject() {
65
73
  if (ERUDIT.mode === 'static') {
66
74
  return;
@@ -93,39 +101,14 @@ export async function tryServerWatchProject() {
93
101
  }
94
102
  }, 300);
95
103
 
96
- function isWatched(path: string) {
97
- if (path.startsWith(ERUDIT.paths.project('content') + '/')) {
98
- return true;
99
- }
100
-
101
- if (path.startsWith(ERUDIT.paths.project('contributors') + '/')) {
102
- return true;
103
- }
104
-
105
- if (path.startsWith(ERUDIT.paths.project('cameos') + '/')) {
106
- return true;
107
- }
108
-
109
- if (path.startsWith(ERUDIT.paths.project('sponsors') + '/')) {
110
- return true;
111
- }
112
-
113
- if (path.startsWith(ERUDIT.paths.project('news') + '/')) {
114
- return true;
115
- }
116
- }
117
-
118
- const watcher = chokidar.watch(ERUDIT.paths.project(), {
119
- ignoreInitial: true,
120
- });
104
+ const watcher = chokidar.watch(
105
+ watchedProjectDirs.map((dir) => ERUDIT.paths.project(dir)),
106
+ { ignoreInitial: true },
107
+ );
121
108
 
122
109
  watcher.on('all', (_, path) => {
123
110
  path = sn(path);
124
111
 
125
- if (!isWatched(path)) {
126
- return;
127
- }
128
-
129
112
  if (pendingRebuild) {
130
113
  return;
131
114
  }
@@ -23,8 +23,11 @@ export async function pushProblemScript(
23
23
  relativePath = problemScriptSrc;
24
24
  }
25
25
 
26
- await ERUDIT.db.insert(ERUDIT.db.schema.problemScripts).values({
27
- problemScriptSrc: relativePath,
28
- contentFullId,
29
- });
26
+ await ERUDIT.db
27
+ .insert(ERUDIT.db.schema.problemScripts)
28
+ .values({
29
+ problemScriptSrc: relativePath,
30
+ contentFullId,
31
+ })
32
+ .onConflictDoNothing();
30
33
  }
@@ -46,6 +46,7 @@ export const phrases: LanguagePhrases = {
46
46
  flag_secondary: 'Additional',
47
47
  flag_secondary_description:
48
48
  'This is an optional material is for learners who want to dive deeper and gain additional knowledge and context.',
49
+ breadcrumb: 'Breadcrumb',
49
50
  key_elements: 'Key elements',
50
51
  stats: 'Statistics',
51
52
  connections: 'Connections',
@@ -47,6 +47,7 @@ export const phrases: LanguagePhrases = {
47
47
  flag_secondary: 'Дополнение',
48
48
  flag_secondary_description:
49
49
  'Это дополнительный материал для тех, кто хочет глубже погрузиться в предмет и получить дополнительные знания и контекст.',
50
+ breadcrumb: 'Путь',
50
51
  key_elements: 'Ключевые элементы',
51
52
  stats: 'Статистика',
52
53
  connections: 'Связи',
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  fillProseStorage,
3
3
  isProseElement,
4
+ isRawElement,
4
5
  type ProseElement,
6
+ type ProseStorage,
5
7
  type ProseWithStorage,
8
+ type RawElement,
6
9
  } from 'tsprose';
7
10
  import { imageSchema } from '@erudit-js/prose/elements/image/core';
8
11
  import { videoSchema } from '@erudit-js/prose/elements/video/core';
@@ -17,6 +20,7 @@ import {
17
20
  } from '@erudit-js/prose/elements/link/dependency/core';
18
21
  import { problemSchema } from '@erudit-js/prose/elements/problem/problem';
19
22
  import { subProblemSchema } from '@erudit-js/prose/elements/problem/problems';
23
+ import { problemCheckSchema } from '@erudit-js/prose/elements/problem/problemCheck';
20
24
 
21
25
  import { createImageStorage } from '../storage/image';
22
26
  import { createVideoStorage } from '../storage/video';
@@ -26,6 +30,68 @@ import { createProblemScriptStorage } from '../storage/problemScript';
26
30
 
27
31
  import { coreElements } from '#erudit/prose/global';
28
32
 
33
+ async function createStorageForRawElement(
34
+ rawElement: RawElement,
35
+ storageKey: string,
36
+ ) {
37
+ switch (true) {
38
+ case isRawElement(rawElement, imageSchema):
39
+ return await createImageStorage(rawElement as any);
40
+ case isRawElement(rawElement, videoSchema):
41
+ return createVideoStorage(rawElement as any);
42
+ case isRawElement(rawElement, calloutSchema):
43
+ return createCalloutStorage(rawElement as any);
44
+ case isRawElement(rawElement, refSchema):
45
+ case isRawElement(rawElement, referenceSchema):
46
+ case isRawElement(rawElement, depSchema):
47
+ case isRawElement(rawElement, dependencySchema):
48
+ return await createLinkStorage(rawElement as any, storageKey);
49
+ case isRawElement(rawElement, problemSchema):
50
+ case isRawElement(rawElement, subProblemSchema):
51
+ return createProblemScriptStorage(rawElement as any, storageKey);
52
+ }
53
+ }
54
+
55
+ async function collectEnsureStorage(
56
+ rawElements: RawElement[],
57
+ storage: ProseStorage,
58
+ ) {
59
+ for (const rawElement of rawElements) {
60
+ if (rawElement.storageKey && !(rawElement.storageKey in storage)) {
61
+ const value = await createStorageForRawElement(
62
+ rawElement,
63
+ rawElement.storageKey,
64
+ );
65
+ if (value !== undefined) {
66
+ storage[rawElement.storageKey] = value;
67
+ }
68
+ }
69
+ if (rawElement.children) {
70
+ await collectEnsureStorage(rawElement.children as RawElement[], storage);
71
+ }
72
+ }
73
+ }
74
+
75
+ async function processEnsureStorage(
76
+ element: ProseElement,
77
+ storage: ProseStorage,
78
+ ) {
79
+ if (
80
+ (isProseElement(element, problemSchema) ||
81
+ isProseElement(element, subProblemSchema) ||
82
+ isProseElement(element, problemCheckSchema)) &&
83
+ element.data.ensureStorage
84
+ ) {
85
+ await collectEnsureStorage(element.data.ensureStorage, storage);
86
+ delete element.data.ensureStorage;
87
+ }
88
+ if (element.children) {
89
+ for (const child of element.children as ProseElement[]) {
90
+ await processEnsureStorage(child, storage);
91
+ }
92
+ }
93
+ }
94
+
29
95
  export async function finalizeProse(
30
96
  prose: ProseElement,
31
97
  ): Promise<ProseWithStorage> {
@@ -58,6 +124,8 @@ export async function finalizeProse(
58
124
  },
59
125
  });
60
126
 
127
+ await processEnsureStorage(prose, storage);
128
+
61
129
  return {
62
130
  prose,
63
131
  storage,
@@ -52,6 +52,7 @@ export type LanguagePhrases = Phrases<{
52
52
  flag_advanced_description: string;
53
53
  flag_secondary: string;
54
54
  flag_secondary_description: string;
55
+ breadcrumb: string;
55
56
  key_elements: string;
56
57
  stats: string;
57
58
  connections: string;