erudit 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/app/assets/icons/update.svg +3 -0
  2. package/app/components/Prose.vue +7 -7
  3. package/app/components/SmartMedia.vue +4 -4
  4. package/app/components/aside/major/contentNav/PaneBookNav.vue +1 -4
  5. package/app/components/aside/minor/content/Toc.vue +24 -1
  6. package/app/components/aside/minor/content/TocItem.vue +2 -1
  7. package/app/components/aside/minor/news/AsideMinorNews.vue +2 -4
  8. package/app/components/aside/minor/news/NewsItem.vue +3 -4
  9. package/app/components/aside/minor/news/RenderNewsElement.vue +4 -11
  10. package/app/components/aside/minor/news/elements/Mix.vue +2 -3
  11. package/app/components/aside/minor/news/elements/P.vue +3 -3
  12. package/app/components/aside/minor/news/elements/Ref.vue +3 -3
  13. package/app/components/aside/minor/news/elements/Text.vue +2 -2
  14. package/app/components/main/MainContentChild.vue +3 -3
  15. package/app/components/main/{MainQuickLink.vue → MainKeyLink.vue} +11 -11
  16. package/app/components/main/{MainQuickLinks.vue → MainKeyLinks.vue} +7 -7
  17. package/app/components/main/MainQuoteLoader.vue +2 -4
  18. package/app/components/main/MainStickyHeader.vue +1 -1
  19. package/app/components/main/MainStickyHeaderPreamble.vue +6 -2
  20. package/app/components/main/MainTopicPartPage.vue +8 -3
  21. package/app/components/main/connections/DepUnique.vue +45 -0
  22. package/app/components/main/connections/Deps.vue +14 -2
  23. package/app/components/main/connections/Externals.vue +1 -1
  24. package/app/components/main/connections/MainConnections.vue +1 -0
  25. package/app/components/main/contentStats/ItemLastChanged.vue +68 -0
  26. package/app/components/main/contentStats/MainContentStats.vue +36 -28
  27. package/app/components/preview/PreviewScreen.vue +2 -2
  28. package/app/components/preview/screen/ContentPage.vue +1 -4
  29. package/app/components/preview/screen/Unique.vue +3 -5
  30. package/app/composables/appElements.ts +2 -4
  31. package/app/composables/asideMajorPane.ts +3 -3
  32. package/app/composables/fetchJson.ts +4 -0
  33. package/app/composables/lastChanged.ts +28 -0
  34. package/app/composables/mainContent.ts +1 -4
  35. package/app/composables/og.ts +43 -35
  36. package/app/composables/phrases.ts +2 -3
  37. package/app/composables/scrollUp.ts +1 -1
  38. package/app/pages/book/[...bookId].vue +5 -1
  39. package/app/pages/contributor/[contributorId].vue +3 -5
  40. package/app/pages/contributors.vue +1 -1
  41. package/app/pages/group/[...groupId].vue +5 -1
  42. package/app/pages/index.vue +1 -1
  43. package/app/pages/page/[...pageId].vue +8 -3
  44. package/app/pages/sponsors.vue +1 -1
  45. package/app/plugins/appSetup/index.ts +0 -5
  46. package/app/plugins/fetchJson.ts +11 -0
  47. package/app/plugins/prerender.server.ts +1 -1
  48. package/app/router.options.ts +1 -1
  49. package/modules/erudit/globals/prose.ts +3 -4
  50. package/modules/erudit/setup/elements/appTemplate.ts +6 -7
  51. package/modules/erudit/setup/elements/{globalTypes.ts → elementGlobalTypes.ts} +21 -21
  52. package/modules/erudit/setup/elements/globalTemplate.ts +29 -23
  53. package/modules/erudit/setup/elements/setup.ts +18 -16
  54. package/modules/erudit/setup/elements/shared.ts +2 -2
  55. package/modules/erudit/setup/elements/tagsTable.ts +1 -1
  56. package/modules/erudit/setup/runtimeConfig.ts +2 -0
  57. package/nuxt.config.ts +2 -2
  58. package/package.json +14 -13
  59. package/server/api/main/content/[...contentTypePath].ts +5 -4
  60. package/server/api/prerender/content.ts +1 -3
  61. package/server/api/preview/contentPage/[...contentTypePath].ts +1 -2
  62. package/server/api/preview/contentUnique/[...contentTypePathUnique].ts +16 -31
  63. package/server/api/problemScript/[...problemScriptPath].ts +81 -4
  64. package/server/erudit/content/global/build.ts +64 -10
  65. package/server/erudit/content/nav/build.ts +4 -4
  66. package/server/erudit/content/repository/children.ts +3 -3
  67. package/server/erudit/content/repository/deps.ts +110 -13
  68. package/server/erudit/content/repository/elementSnippets.ts +16 -16
  69. package/server/erudit/content/repository/stats.ts +30 -22
  70. package/server/erudit/content/repository/topicParts.ts +1 -1
  71. package/server/erudit/content/repository/unique.ts +14 -15
  72. package/server/erudit/content/resolve/page.ts +15 -35
  73. package/server/erudit/content/resolve/topic.ts +33 -164
  74. package/server/erudit/content/resolve/utils/insertContentResolved.ts +59 -31
  75. package/server/erudit/content/search.ts +5 -22
  76. package/server/erudit/contributors/build.ts +7 -8
  77. package/server/erudit/db/repository/pushFile.ts +10 -3
  78. package/server/erudit/db/repository/pushProblemScript.ts +14 -3
  79. package/server/erudit/db/schema/contentDeps.ts +3 -0
  80. package/server/erudit/db/schema/contentSnippets.ts +3 -3
  81. package/server/erudit/db/schema/contentUniques.ts +2 -2
  82. package/server/erudit/db/schema/contributors.ts +2 -2
  83. package/server/erudit/db/schema/news.ts +2 -2
  84. package/server/erudit/db/schema/pages.ts +2 -2
  85. package/server/erudit/db/schema/topics.ts +4 -4
  86. package/server/erudit/global.ts +4 -0
  87. package/server/erudit/importer.ts +16 -8
  88. package/server/erudit/index.ts +0 -3
  89. package/server/erudit/language/list/en.ts +1 -0
  90. package/server/erudit/language/list/ru.ts +1 -0
  91. package/server/erudit/news/build.ts +6 -6
  92. package/server/erudit/news/repository/batch.ts +2 -2
  93. package/server/erudit/prose/repository/finalize.ts +22 -25
  94. package/server/erudit/prose/repository/get.ts +3 -5
  95. package/server/erudit/prose/repository/rawToProse.ts +31 -0
  96. package/server/erudit/prose/storage/callout.ts +9 -7
  97. package/server/erudit/prose/storage/image.ts +8 -11
  98. package/server/erudit/prose/storage/link.ts +24 -32
  99. package/server/erudit/prose/storage/problemScript.ts +8 -14
  100. package/server/erudit/prose/storage/video.ts +9 -7
  101. package/server/erudit/repository.ts +4 -4
  102. package/server/routes/file/[...path].ts +1 -1
  103. package/shared/types/contentChildren.ts +5 -2
  104. package/shared/types/contentConnections.ts +9 -0
  105. package/shared/types/elementSnippet.ts +1 -1
  106. package/shared/types/indexPage.ts +3 -0
  107. package/shared/types/language.ts +1 -83
  108. package/shared/types/mainContent.ts +11 -5
  109. package/shared/types/news.ts +2 -2
  110. package/shared/types/preview.ts +3 -2
  111. package/shared/types/runtimeConfig.ts +1 -0
  112. package/shared/types/search.ts +2 -0
  113. package/shared/utils/pages.ts +4 -2
  114. package/shared/utils/stringColor.ts +16 -6
  115. package/server/erudit/prose/repository/resolve.ts +0 -17
  116. package/server/erudit/prose/transform/bundleProblemScript.ts +0 -6
@@ -9,6 +9,13 @@ let initialBuild = true;
9
9
 
10
10
  const contentRoot = () => ERUDIT.paths.project('content');
11
11
 
12
+ export let builtLinkObject: Record<string, any> | null = null;
13
+
14
+ /** All valid fully-qualified $CONTENT paths — content items, topic parts,
15
+ * public uniques, and internal (underscore) uniques.
16
+ * Used for server-side prose link validation. */
17
+ export let builtValidPaths: Set<string> | null = null;
18
+
12
19
  export async function buildGlobalContent() {
13
20
  ERUDIT.log.debug.start('Building global content...');
14
21
 
@@ -20,7 +27,9 @@ export async function buildGlobalContent() {
20
27
  return;
21
28
  }
22
29
 
23
- const linkObject = await buildLinkObject();
30
+ const { linkObject, validPaths } = await buildLinkObject();
31
+ builtLinkObject = linkObject;
32
+ builtValidPaths = validPaths;
24
33
 
25
34
  const linkTypes = linkObjectToTypes(linkObject);
26
35
  writeFileSync(
@@ -51,6 +60,10 @@ function linkObjectToTypes(linkObject: any): string {
51
60
  return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
52
61
  }
53
62
 
63
+ function isValidIdentifier(key: string): boolean {
64
+ return /^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(key);
65
+ }
66
+
54
67
  function processObject(obj: any, level: number): string {
55
68
  const lines: string[] = [];
56
69
 
@@ -58,6 +71,9 @@ function linkObjectToTypes(linkObject: any): string {
58
71
  if (key === '__jsdoc' || key === '__typeguard') continue;
59
72
 
60
73
  const camelKey = toCamelCase(key);
74
+ const outputKey = isValidIdentifier(camelKey)
75
+ ? camelKey
76
+ : `'${camelKey}'`;
61
77
 
62
78
  // Add JSDoc comment if present
63
79
  if (value && typeof value === 'object' && value.__jsdoc) {
@@ -77,11 +93,11 @@ function linkObjectToTypes(linkObject: any): string {
77
93
  const typeguard = value?.__typeguard || 'GlobalContentItemTypeguard';
78
94
 
79
95
  if (hasNestedProps) {
80
- lines.push(indent(level) + `${camelKey}: ${typeguard} & {`);
96
+ lines.push(indent(level) + `${outputKey}: ${typeguard} & {`);
81
97
  lines.push(processObject(value, level + 1));
82
98
  lines.push(indent(level) + `}`);
83
99
  } else {
84
- lines.push(indent(level) + `${camelKey}: ${typeguard} & {}`);
100
+ lines.push(indent(level) + `${outputKey}: ${typeguard} & {}`);
85
101
  }
86
102
  }
87
103
 
@@ -110,6 +126,7 @@ ${body}
110
126
  */
111
127
  async function buildLinkObject() {
112
128
  const linkTree: any = {};
129
+ const validPaths = new Set<string>();
113
130
 
114
131
  await ERUDIT.contentNav.walk((navItem) => {
115
132
  // Navigate to the correct position in the tree based on the full path
@@ -118,7 +135,7 @@ async function buildLinkObject() {
118
135
 
119
136
  // Navigate through parent parts
120
137
  for (let i = 0; i < pathParts.length - 1; i++) {
121
- cursor = cursor[pathParts[i]];
138
+ cursor = cursor[pathParts[i]!];
122
139
  }
123
140
 
124
141
  //
@@ -151,6 +168,11 @@ ${jsdoc}
151
168
  navItem.contentRelPath,
152
169
  ),
153
170
  };
171
+
172
+ validPaths.add(navItem.fullId);
173
+ for (const name of getAllUniqueNames(moduleContent)) {
174
+ validPaths.add(`${navItem.fullId}/$${name}`);
175
+ }
154
176
  } else if (navItem.type === 'topic') {
155
177
  const pathToTopicFile = ERUDIT.paths.project(
156
178
  `content/${navItem.contentRelPath}/topic.ts`,
@@ -173,6 +195,8 @@ ${jsdoc}
173
195
  `.trim(),
174
196
  };
175
197
 
198
+ validPaths.add(navItem.fullId);
199
+
176
200
  for (const part of topicParts) {
177
201
  try {
178
202
  const pathToFile = ERUDIT.paths.project(
@@ -200,6 +224,11 @@ ${jsdoc}
200
224
  navItem.contentRelPath,
201
225
  ),
202
226
  };
227
+
228
+ validPaths.add(`${navItem.fullId}/${part}`);
229
+ for (const name of getAllUniqueNames(partContent)) {
230
+ validPaths.add(`${navItem.fullId}/${part}/$${name}`);
231
+ }
203
232
  } catch {}
204
233
  }
205
234
  } else {
@@ -223,17 +252,19 @@ ${jsdoc}
223
252
  */
224
253
  `.trim(),
225
254
  };
255
+
256
+ validPaths.add(navItem.fullId);
226
257
  }
227
258
  });
228
259
 
229
- return linkTree;
260
+ return { linkObject: linkTree, validPaths };
230
261
  }
231
262
 
232
263
  function tryGetTitle(moduleContent: string) {
233
- const titleMatch = moduleContent.match(/title:\s*['"`](.*?)['"`]/);
264
+ const titleMatch = moduleContent.match(/title:\s*(['"`])(.*?)\1/);
234
265
 
235
266
  if (titleMatch) {
236
- return titleMatch[1].trim();
267
+ return titleMatch[2]!.trim();
237
268
  }
238
269
  }
239
270
 
@@ -244,6 +275,25 @@ function jsdocLines(lines: any[]) {
244
275
  .join('\n');
245
276
  }
246
277
 
278
+ /** Returns ALL unique names from a module — both public and internal. Used to
279
+ * populate builtValidPaths for server-side prose link validation. */
280
+ function getAllUniqueNames(moduleContent: string): string[] {
281
+ const uniquesMatch = moduleContent.match(/uniques:\s*\{([^}]*)\}/s);
282
+ if (!uniquesMatch) return [];
283
+
284
+ const names: string[] = [];
285
+ for (const line of uniquesMatch[1]!.split('\n')) {
286
+ if (line.trim().startsWith('//')) continue;
287
+
288
+ const pairMatch =
289
+ line.match(/\[['"](.*?)['"]\]:\s*(\w+)/) ||
290
+ line.match(/['"](.*?)['"]:\s*(\w+)/) ||
291
+ line.match(/(\w+):\s*(\w+)/);
292
+ if (pairMatch) names.push(pairMatch[1]!);
293
+ }
294
+ return names;
295
+ }
296
+
247
297
  function tryGetUniquesObject(
248
298
  moduleContent: string,
249
299
  pathToFile: string,
@@ -259,7 +309,7 @@ function tryGetUniquesObject(
259
309
  const uniquesContent = uniquesMatch[1];
260
310
 
261
311
  // Parse key-value pairs from uniques object
262
- const lines = uniquesContent.split('\n');
312
+ const lines = uniquesContent!.split('\n');
263
313
  const result: any = {};
264
314
 
265
315
  for (const line of lines) {
@@ -268,12 +318,16 @@ function tryGetUniquesObject(
268
318
  continue;
269
319
  }
270
320
 
271
- // Skip uniques starting with underscore
321
+ // Skip uniques starting with underscore (internal — excluded from $CONTENT types)
272
322
  if (line.trim().startsWith('_')) {
273
323
  continue;
274
324
  }
275
325
 
276
- const pairMatch = line.match(/(\w+):\s*(\w+)/);
326
+ // Support bracket notation ['any string'], quoted keys "any string" / 'any string', and plain identifiers
327
+ const pairMatch =
328
+ line.match(/\[['"](.*?)['"]\]:\s*(\w+)/) ||
329
+ line.match(/['"](.*?)['"]:\s*(\w+)/) ||
330
+ line.match(/(\w+):\s*(\w+)/);
277
331
  if (!pairMatch) {
278
332
  continue;
279
333
  }
@@ -216,16 +216,16 @@ function parseContentPath(relPath: string):
216
216
 
217
217
  for (let i = 0; i < parts.length; i++) {
218
218
  const part = parts[i];
219
- const typeDelimiterPos = part.search(/[+-]/);
219
+ const typeDelimiterPos = part!.search(/[+-]/);
220
220
  if (typeDelimiterPos <= 0) return;
221
221
 
222
- const posStr = part.slice(0, typeDelimiterPos);
222
+ const posStr = part!.slice(0, typeDelimiterPos);
223
223
  if (!/^\d+$/.test(posStr)) return;
224
224
  const posNum = Number(posStr);
225
225
 
226
- const typeDelimiter = part.charAt(typeDelimiterPos);
226
+ const typeDelimiter = part!.charAt(typeDelimiterPos);
227
227
  skip = typeDelimiter === '+';
228
- idPart = part.slice(typeDelimiterPos + 1);
228
+ idPart = part!.slice(typeDelimiterPos + 1);
229
229
  if (!idPart) return;
230
230
 
231
231
  fullId += `/${idPart}`;
@@ -24,7 +24,7 @@ export async function getContentChildren(
24
24
  const elementSnippets = await ERUDIT.repository.content.elementSnippets(
25
25
  childNode.fullId,
26
26
  );
27
- const quickLinks = elementSnippets?.filter((snippet) => snippet.quick);
27
+ const keyLinks = elementSnippets?.filter((snippet) => snippet.key);
28
28
  const stats = await ERUDIT.repository.content.stats(childNode.fullId);
29
29
 
30
30
  const child: MainContentChildrenItem = {
@@ -37,8 +37,8 @@ export async function getContentChildren(
37
37
  child.description = description;
38
38
  }
39
39
 
40
- if (quickLinks && quickLinks.length > 0) {
41
- child.quickLinks = quickLinks;
40
+ if (keyLinks && keyLinks.length > 0) {
41
+ child.keyLinks = keyLinks;
42
42
  }
43
43
 
44
44
  if (stats) {
@@ -27,7 +27,7 @@ export async function getContentDependencies(fullId: string) {
27
27
 
28
28
  for (const toFullId of hardToFullIds) {
29
29
  const reason = fullId2Reason.get(toFullId)!;
30
- const hardDep = await createContentDep('hard', toFullId, reason);
30
+ const hardDep = await createContentDep('hard', toFullId, undefined, reason);
31
31
  if (hardDep) {
32
32
  hardDependencies.push(hardDep);
33
33
  }
@@ -40,7 +40,7 @@ export async function getContentDependencies(fullId: string) {
40
40
  const autoDependencies: ContentAutoDep[] = [];
41
41
 
42
42
  const dbAutoDependencies = await ERUDIT.db.query.contentDeps.findMany({
43
- columns: { toFullId: true, hard: true },
43
+ columns: { toFullId: true, hard: true, uniqueNames: true },
44
44
  where: and(
45
45
  or(
46
46
  eq(ERUDIT.db.schema.contentDeps.fromFullId, fullId),
@@ -50,13 +50,30 @@ export async function getContentDependencies(fullId: string) {
50
50
  ),
51
51
  });
52
52
 
53
+ // Merge unique names across rows that share the same toFullId
54
+ // (can happen when a topic and its children both dep on the same target).
55
+ const autoUniqueMap = new Map<string, Set<string>>();
56
+ for (const row of dbAutoDependencies) {
57
+ if (!autoUniqueMap.has(row.toFullId)) {
58
+ autoUniqueMap.set(row.toFullId, new Set());
59
+ }
60
+ if (row.uniqueNames) {
61
+ for (const name of row.uniqueNames.split(',')) {
62
+ autoUniqueMap.get(row.toFullId)!.add(name);
63
+ }
64
+ }
65
+ }
66
+
53
67
  // Skip auto-dependency if a hard dependency from the same source exists
54
68
  const autoToFullIds = ERUDIT.contentNav
55
69
  .orderIds(externalToFullIds(dbAutoDependencies))
56
70
  .filter((toFullId) => !fullId2Reason.has(toFullId));
57
71
 
58
72
  for (const toFullId of autoToFullIds) {
59
- const autoDep = await createContentDep('auto', toFullId);
73
+ const uniquePairs = Array.from(autoUniqueMap.get(toFullId) ?? []).map(
74
+ (uniqueName) => ({ contentFullId: toFullId, uniqueName }),
75
+ );
76
+ const autoDep = await createContentDep('auto', toFullId, uniquePairs);
60
77
  if (autoDep) {
61
78
  autoDependencies.push(autoDep);
62
79
  }
@@ -92,32 +109,50 @@ export async function getContentDependents(
92
109
  fullId: string,
93
110
  ): Promise<ContentAutoDep[]> {
94
111
  const dbDependents = await ERUDIT.db.query.contentDeps.findMany({
95
- columns: { fromFullId: true, toFullId: true },
112
+ columns: { fromFullId: true, toFullId: true, uniqueNames: true },
96
113
  where: or(
97
114
  eq(ERUDIT.db.schema.contentDeps.toFullId, fullId),
98
115
  like(ERUDIT.db.schema.contentDeps.toFullId, `${fullId}/%`),
99
116
  ),
100
117
  });
101
118
 
102
- // Skip dependent if it originates from current content item or its child
103
- const externalFromFullIds = dbDependents.reduce((ids, dbDependent) => {
104
- const fromFullId = dbDependent.fromFullId;
119
+ // Group rows by fromFullId, collecting {contentFullId, uniqueName} pairs
120
+ // (toFullId can vary when a dependent references different child pages).
121
+ const fromUniquePairsMap = new Map<
122
+ string,
123
+ { contentFullId: string; uniqueName: string }[]
124
+ >();
125
+ const externalFromFullIds: string[] = [];
126
+
127
+ for (const row of dbDependents) {
128
+ const fromFullId = row.fromFullId;
105
129
  const isFromSelf = fromFullId === fullId;
106
130
  const isFromChild = fromFullId.startsWith(`${fullId}/`);
107
131
 
108
- if (isFromSelf || isFromChild) {
109
- return ids;
132
+ if (isFromSelf || isFromChild) continue;
133
+
134
+ if (!fromUniquePairsMap.has(fromFullId)) {
135
+ fromUniquePairsMap.set(fromFullId, []);
136
+ externalFromFullIds.push(fromFullId);
110
137
  }
111
138
 
112
- ids.push(fromFullId);
113
- return ids;
114
- }, [] as string[]);
139
+ if (row.uniqueNames) {
140
+ for (const name of row.uniqueNames.split(',')) {
141
+ fromUniquePairsMap
142
+ .get(fromFullId)!
143
+ .push({ contentFullId: row.toFullId, uniqueName: name });
144
+ }
145
+ }
146
+ }
115
147
 
116
148
  // Order sources according to nav structure
117
149
  const fromFullIds = ERUDIT.contentNav.orderIds(externalFromFullIds);
118
150
 
119
151
  const dependents = await Promise.all(
120
- fromFullIds.map((fromFullId) => createContentDep('auto', fromFullId)),
152
+ fromFullIds.map((fromFullId) => {
153
+ const uniquePairs = fromUniquePairsMap.get(fromFullId) ?? [];
154
+ return createContentDep('auto', fromFullId, uniquePairs);
155
+ }),
121
156
  );
122
157
 
123
158
  return dependents.filter((dep): dep is ContentAutoDep => dep !== undefined);
@@ -126,15 +161,18 @@ export async function getContentDependents(
126
161
  async function createContentDep(
127
162
  type: 'auto',
128
163
  fullId: string,
164
+ uniquePairs: { contentFullId: string; uniqueName: string }[],
129
165
  ): Promise<ContentAutoDep | undefined>;
130
166
  async function createContentDep(
131
167
  type: 'hard',
132
168
  fullId: string,
169
+ uniquePairs: undefined,
133
170
  reason: string,
134
171
  ): Promise<ContentHardDep | undefined>;
135
172
  async function createContentDep(
136
173
  type: 'auto' | 'hard',
137
174
  fullId: string,
175
+ uniquePairs?: { contentFullId: string; uniqueName: string }[],
138
176
  reason?: string,
139
177
  ): Promise<ContentDep | undefined> {
140
178
  const navNode = ERUDIT.contentNav.getNodeOrThrow(fullId);
@@ -160,10 +198,69 @@ async function createContentDep(
160
198
  };
161
199
  }
162
200
 
201
+ const uniques =
202
+ uniquePairs && uniquePairs.length > 0
203
+ ? await resolveUniqueEntries(uniquePairs)
204
+ : undefined;
205
+
163
206
  return {
164
207
  type: 'auto',
165
208
  contentType,
166
209
  title,
167
210
  link,
211
+ ...(uniques && uniques.length > 0 ? { uniques } : {}),
168
212
  };
169
213
  }
214
+
215
+ async function resolveUniqueEntries(
216
+ pairs: { contentFullId: string; uniqueName: string }[],
217
+ ): Promise<ContentDepUnique[]> {
218
+ // Deduplicate by "contentFullId/uniqueName" to avoid showing the same
219
+ // element twice when multiple prose types reference it.
220
+ const seen = new Set<string>();
221
+ const unique: typeof pairs = [];
222
+ for (const pair of pairs) {
223
+ const key = `${pair.contentFullId}/${pair.uniqueName}`;
224
+ if (!seen.has(key)) {
225
+ seen.add(key);
226
+ unique.push(pair);
227
+ }
228
+ }
229
+
230
+ const results = await Promise.all(
231
+ unique.map(async ({ contentFullId, uniqueName }) => {
232
+ const dbUnique = await ERUDIT.db.query.contentUniques.findFirst({
233
+ columns: { title: true, prose: true, contentProseType: true },
234
+ where: and(
235
+ eq(ERUDIT.db.schema.contentUniques.contentFullId, contentFullId),
236
+ eq(ERUDIT.db.schema.contentUniques.uniqueName, uniqueName),
237
+ ),
238
+ });
239
+
240
+ if (!dbUnique) return null;
241
+
242
+ const navNode = ERUDIT.contentNav.getNodeOrThrow(contentFullId);
243
+ const schemaName = dbUnique.prose.schema.name;
244
+
245
+ if (!schemaName) return null;
246
+
247
+ const link =
248
+ navNode.type === 'topic'
249
+ ? PAGES.topic(
250
+ dbUnique.contentProseType as any,
251
+ navNode.shortId,
252
+ dbUnique.prose.id,
253
+ )
254
+ : PAGES.page(navNode.shortId, dbUnique.prose.id);
255
+
256
+ return {
257
+ name: uniqueName,
258
+ title: dbUnique.title ?? undefined,
259
+ link,
260
+ schemaName,
261
+ } satisfies ContentDepUnique;
262
+ }),
263
+ );
264
+
265
+ return results.filter((r): r is ContentDepUnique => r !== null);
266
+ }
@@ -15,7 +15,7 @@ export async function getContentElementSnippets(
15
15
  where: and(
16
16
  eq(ERUDIT.db.schema.contentSnippets.contentFullId, fullId),
17
17
  or(
18
- eq(ERUDIT.db.schema.contentSnippets.quick, true),
18
+ eq(ERUDIT.db.schema.contentSnippets.key, true),
19
19
  eq(ERUDIT.db.schema.contentSnippets.seo, true),
20
20
  ),
21
21
  ),
@@ -45,27 +45,27 @@ export async function getContentElementSnippets(
45
45
  title: snippetData.title!,
46
46
  };
47
47
 
48
- if (snippetData.quick) {
49
- snippet.quick = {};
50
- let quickTitle: string | undefined;
51
- let quickDescription: string | undefined;
48
+ if (snippetData.key) {
49
+ snippet.key = {};
50
+ let keyTitle: string | undefined;
51
+ let keyDescription: string | undefined;
52
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;
53
+ if (typeof snippetData.key === 'string') {
54
+ keyTitle = snippetData.key;
55
+ } else if (typeof snippetData.key === 'object') {
56
+ if (snippetData.key.title) {
57
+ keyTitle = snippetData.key.title;
58
58
  }
59
- if (snippetData.quick.description) {
60
- quickDescription = snippetData.quick.description;
59
+ if (snippetData.key.description) {
60
+ keyDescription = snippetData.key.description;
61
61
  }
62
62
  }
63
63
 
64
- if (quickTitle) {
65
- snippet.quick.title = quickTitle;
64
+ if (keyTitle) {
65
+ snippet.key.title = keyTitle;
66
66
  }
67
- if (quickDescription) {
68
- snippet.quick.description = quickDescription;
67
+ if (keyDescription) {
68
+ snippet.key.description = keyDescription;
69
69
  }
70
70
  }
71
71
 
@@ -1,30 +1,38 @@
1
1
  import { and, eq, sql } from 'drizzle-orm';
2
2
 
3
- export async function addContentElementCount(
3
+ export async function updateContentSchemaCounts(
4
4
  fullId: string,
5
- schemaName: string,
6
- amount: number,
5
+ schemaCounts: Record<string, number>,
6
+ // schemaName: string,
7
+ // amount: number,
7
8
  ): Promise<void> {
8
- await ERUDIT.db
9
- .insert(ERUDIT.db.schema.contentElementStats)
10
- .values({
11
- fullId,
12
- schemaName,
13
- count: 0,
14
- })
15
- .onConflictDoNothing();
9
+ const schemasFilter = ERUDIT.config.countElements.flat();
10
+ const filteredSchemaCounts = Object.entries(schemaCounts).filter(
11
+ ([schemaName]) => schemasFilter.includes(schemaName),
12
+ );
16
13
 
17
- await ERUDIT.db
18
- .update(ERUDIT.db.schema.contentElementStats)
19
- .set({
20
- count: sql`${ERUDIT.db.schema.contentElementStats.count} + ${amount}`,
21
- })
22
- .where(
23
- and(
24
- eq(ERUDIT.db.schema.contentElementStats.fullId, fullId),
25
- eq(ERUDIT.db.schema.contentElementStats.schemaName, schemaName),
26
- ),
27
- );
14
+ for (const [schemaName, amount] of filteredSchemaCounts) {
15
+ await ERUDIT.db
16
+ .insert(ERUDIT.db.schema.contentElementStats)
17
+ .values({
18
+ fullId,
19
+ schemaName,
20
+ count: 0,
21
+ })
22
+ .onConflictDoNothing();
23
+
24
+ await ERUDIT.db
25
+ .update(ERUDIT.db.schema.contentElementStats)
26
+ .set({
27
+ count: sql`${ERUDIT.db.schema.contentElementStats.count} + ${amount}`,
28
+ })
29
+ .where(
30
+ and(
31
+ eq(ERUDIT.db.schema.contentElementStats.fullId, fullId),
32
+ eq(ERUDIT.db.schema.contentElementStats.schemaName, schemaName),
33
+ ),
34
+ );
35
+ }
28
36
  }
29
37
 
30
38
  export async function getContentStats(
@@ -35,5 +35,5 @@ export async function getTopicParts(fullId: string): Promise<TopicPart[]> {
35
35
 
36
36
  export async function getDefaultTopicPart(fullId: string): Promise<TopicPart> {
37
37
  const parts = await getTopicParts(fullId);
38
- return parts[0];
38
+ return parts[0]!;
39
39
  }
@@ -1,12 +1,13 @@
1
1
  import { and, eq } from 'drizzle-orm';
2
2
  import {
3
+ isProseElement,
4
+ makeProseElement,
3
5
  mixSchema,
4
- walkElements,
5
6
  WalkNoDeeper,
7
+ walkPreSync,
6
8
  WalkStop,
7
- type AnySchema,
8
9
  type ProseElement,
9
- } from '@jsprose/core';
10
+ } from 'tsprose';
10
11
  import type { ContentProseType } from '@erudit-js/core/content/prose';
11
12
  import { headingSchema } from '@erudit-js/prose/elements/heading/core';
12
13
 
@@ -36,21 +37,18 @@ export async function getContentUnique(
36
37
  export async function getContentHeadingUnique(
37
38
  fullId: string,
38
39
  proseType: ContentProseType,
39
- uniqueName: string,
40
+ uniqueId: string,
40
41
  ) {
41
42
  const contentProse = await ERUDIT.repository.prose.getContent(
42
43
  proseType,
43
44
  fullId,
44
45
  );
45
46
 
46
- const afterHeadingElements: ProseElement<AnySchema>[] = [];
47
+ const afterHeadingElements: ProseElement[] = [];
47
48
  let adding = false;
48
49
 
49
- await walkElements(contentProse, async (element) => {
50
- if (
51
- element.schemaName === headingSchema.name &&
52
- element.uniqueName === uniqueName
53
- ) {
50
+ walkPreSync(contentProse, (element) => {
51
+ if (isProseElement(element, headingSchema) && element.id === uniqueId) {
54
52
  adding = true;
55
53
  }
56
54
 
@@ -66,11 +64,12 @@ export async function getContentHeadingUnique(
66
64
  }
67
65
  });
68
66
 
69
- const mix: ProseElement<typeof mixSchema> = {
70
- __JSPROSE_element: true,
71
- schemaName: mixSchema.name,
72
- children: afterHeadingElements,
73
- } as ProseElement<typeof mixSchema>;
67
+ const mix = makeProseElement({
68
+ schema: mixSchema,
69
+ elementHandler: (proseElement) => {
70
+ proseElement.children = afterHeadingElements;
71
+ },
72
+ });
74
73
 
75
74
  return mix;
76
75
  }