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.
- package/app/assets/icons/update.svg +3 -0
- package/app/components/Prose.vue +7 -7
- package/app/components/SmartMedia.vue +4 -4
- package/app/components/aside/major/contentNav/PaneBookNav.vue +1 -4
- package/app/components/aside/minor/content/Toc.vue +24 -1
- package/app/components/aside/minor/content/TocItem.vue +2 -1
- package/app/components/aside/minor/news/AsideMinorNews.vue +2 -4
- package/app/components/aside/minor/news/NewsItem.vue +3 -4
- package/app/components/aside/minor/news/RenderNewsElement.vue +4 -11
- package/app/components/aside/minor/news/elements/Mix.vue +2 -3
- package/app/components/aside/minor/news/elements/P.vue +3 -3
- package/app/components/aside/minor/news/elements/Ref.vue +3 -3
- package/app/components/aside/minor/news/elements/Text.vue +2 -2
- package/app/components/main/MainContentChild.vue +3 -3
- package/app/components/main/{MainQuickLink.vue → MainKeyLink.vue} +11 -11
- package/app/components/main/{MainQuickLinks.vue → MainKeyLinks.vue} +7 -7
- package/app/components/main/MainQuoteLoader.vue +2 -4
- package/app/components/main/MainStickyHeader.vue +1 -1
- package/app/components/main/MainStickyHeaderPreamble.vue +6 -2
- package/app/components/main/MainTopicPartPage.vue +8 -3
- package/app/components/main/connections/DepUnique.vue +45 -0
- package/app/components/main/connections/Deps.vue +14 -2
- package/app/components/main/connections/Externals.vue +1 -1
- package/app/components/main/connections/MainConnections.vue +1 -0
- package/app/components/main/contentStats/ItemLastChanged.vue +68 -0
- package/app/components/main/contentStats/MainContentStats.vue +36 -28
- package/app/components/preview/PreviewScreen.vue +2 -2
- package/app/components/preview/screen/ContentPage.vue +1 -4
- package/app/components/preview/screen/Unique.vue +3 -5
- package/app/composables/appElements.ts +2 -4
- package/app/composables/asideMajorPane.ts +3 -3
- package/app/composables/fetchJson.ts +4 -0
- package/app/composables/lastChanged.ts +28 -0
- package/app/composables/mainContent.ts +1 -4
- package/app/composables/og.ts +43 -35
- package/app/composables/phrases.ts +2 -3
- package/app/composables/scrollUp.ts +1 -1
- package/app/pages/book/[...bookId].vue +5 -1
- package/app/pages/contributor/[contributorId].vue +3 -5
- package/app/pages/contributors.vue +1 -1
- package/app/pages/group/[...groupId].vue +5 -1
- package/app/pages/index.vue +1 -1
- package/app/pages/page/[...pageId].vue +8 -3
- package/app/pages/sponsors.vue +1 -1
- package/app/plugins/appSetup/index.ts +0 -5
- package/app/plugins/fetchJson.ts +11 -0
- package/app/plugins/prerender.server.ts +1 -1
- package/app/router.options.ts +1 -1
- package/modules/erudit/globals/prose.ts +3 -4
- package/modules/erudit/setup/elements/appTemplate.ts +6 -7
- package/modules/erudit/setup/elements/{globalTypes.ts → elementGlobalTypes.ts} +21 -21
- package/modules/erudit/setup/elements/globalTemplate.ts +29 -23
- package/modules/erudit/setup/elements/setup.ts +18 -16
- package/modules/erudit/setup/elements/shared.ts +2 -2
- package/modules/erudit/setup/elements/tagsTable.ts +1 -1
- package/modules/erudit/setup/runtimeConfig.ts +2 -0
- package/nuxt.config.ts +2 -2
- package/package.json +14 -13
- package/server/api/main/content/[...contentTypePath].ts +5 -4
- package/server/api/prerender/content.ts +1 -3
- package/server/api/preview/contentPage/[...contentTypePath].ts +1 -2
- package/server/api/preview/contentUnique/[...contentTypePathUnique].ts +16 -31
- package/server/api/problemScript/[...problemScriptPath].ts +81 -4
- package/server/erudit/content/global/build.ts +64 -10
- package/server/erudit/content/nav/build.ts +4 -4
- package/server/erudit/content/repository/children.ts +3 -3
- package/server/erudit/content/repository/deps.ts +110 -13
- package/server/erudit/content/repository/elementSnippets.ts +16 -16
- package/server/erudit/content/repository/stats.ts +30 -22
- package/server/erudit/content/repository/topicParts.ts +1 -1
- package/server/erudit/content/repository/unique.ts +14 -15
- package/server/erudit/content/resolve/page.ts +15 -35
- package/server/erudit/content/resolve/topic.ts +33 -164
- package/server/erudit/content/resolve/utils/insertContentResolved.ts +59 -31
- package/server/erudit/content/search.ts +5 -22
- package/server/erudit/contributors/build.ts +7 -8
- package/server/erudit/db/repository/pushFile.ts +10 -3
- package/server/erudit/db/repository/pushProblemScript.ts +14 -3
- package/server/erudit/db/schema/contentDeps.ts +3 -0
- package/server/erudit/db/schema/contentSnippets.ts +3 -3
- package/server/erudit/db/schema/contentUniques.ts +2 -2
- package/server/erudit/db/schema/contributors.ts +2 -2
- package/server/erudit/db/schema/news.ts +2 -2
- package/server/erudit/db/schema/pages.ts +2 -2
- package/server/erudit/db/schema/topics.ts +4 -4
- package/server/erudit/global.ts +4 -0
- package/server/erudit/importer.ts +16 -8
- package/server/erudit/index.ts +0 -3
- package/server/erudit/language/list/en.ts +1 -0
- package/server/erudit/language/list/ru.ts +1 -0
- package/server/erudit/news/build.ts +6 -6
- package/server/erudit/news/repository/batch.ts +2 -2
- package/server/erudit/prose/repository/finalize.ts +22 -25
- package/server/erudit/prose/repository/get.ts +3 -5
- package/server/erudit/prose/repository/rawToProse.ts +31 -0
- package/server/erudit/prose/storage/callout.ts +9 -7
- package/server/erudit/prose/storage/image.ts +8 -11
- package/server/erudit/prose/storage/link.ts +24 -32
- package/server/erudit/prose/storage/problemScript.ts +8 -14
- package/server/erudit/prose/storage/video.ts +9 -7
- package/server/erudit/repository.ts +4 -4
- package/server/routes/file/[...path].ts +1 -1
- package/shared/types/contentChildren.ts +5 -2
- package/shared/types/contentConnections.ts +9 -0
- package/shared/types/elementSnippet.ts +1 -1
- package/shared/types/indexPage.ts +3 -0
- package/shared/types/language.ts +1 -83
- package/shared/types/mainContent.ts +11 -5
- package/shared/types/news.ts +2 -2
- package/shared/types/preview.ts +3 -2
- package/shared/types/runtimeConfig.ts +1 -0
- package/shared/types/search.ts +2 -0
- package/shared/utils/pages.ts +4 -2
- package/shared/utils/stringColor.ts +16 -6
- package/server/erudit/prose/repository/resolve.ts +0 -17
- 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) + `${
|
|
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) + `${
|
|
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[
|
|
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
|
|
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
|
-
|
|
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
|
|
219
|
+
const typeDelimiterPos = part!.search(/[+-]/);
|
|
220
220
|
if (typeDelimiterPos <= 0) return;
|
|
221
221
|
|
|
222
|
-
const posStr = part
|
|
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
|
|
226
|
+
const typeDelimiter = part!.charAt(typeDelimiterPos);
|
|
227
227
|
skip = typeDelimiter === '+';
|
|
228
|
-
idPart = part
|
|
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
|
|
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 (
|
|
41
|
-
child.
|
|
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
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
132
|
+
if (isFromSelf || isFromChild) continue;
|
|
133
|
+
|
|
134
|
+
if (!fromUniquePairsMap.has(fromFullId)) {
|
|
135
|
+
fromUniquePairsMap.set(fromFullId, []);
|
|
136
|
+
externalFromFullIds.push(fromFullId);
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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) =>
|
|
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.
|
|
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.
|
|
49
|
-
snippet.
|
|
50
|
-
let
|
|
51
|
-
let
|
|
48
|
+
if (snippetData.key) {
|
|
49
|
+
snippet.key = {};
|
|
50
|
+
let keyTitle: string | undefined;
|
|
51
|
+
let keyDescription: string | undefined;
|
|
52
52
|
|
|
53
|
-
if (typeof snippetData.
|
|
54
|
-
|
|
55
|
-
} else if (typeof snippetData.
|
|
56
|
-
if (snippetData.
|
|
57
|
-
|
|
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.
|
|
60
|
-
|
|
59
|
+
if (snippetData.key.description) {
|
|
60
|
+
keyDescription = snippetData.key.description;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
if (
|
|
65
|
-
snippet.
|
|
64
|
+
if (keyTitle) {
|
|
65
|
+
snippet.key.title = keyTitle;
|
|
66
66
|
}
|
|
67
|
-
if (
|
|
68
|
-
snippet.
|
|
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
|
|
3
|
+
export async function updateContentSchemaCounts(
|
|
4
4
|
fullId: string,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
schemaCounts: Record<string, number>,
|
|
6
|
+
// schemaName: string,
|
|
7
|
+
// amount: number,
|
|
7
8
|
): Promise<void> {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
@@ -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 '
|
|
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
|
-
|
|
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
|
|
47
|
+
const afterHeadingElements: ProseElement[] = [];
|
|
47
48
|
let adding = false;
|
|
48
49
|
|
|
49
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
const mix = makeProseElement({
|
|
68
|
+
schema: mixSchema,
|
|
69
|
+
elementHandler: (proseElement) => {
|
|
70
|
+
proseElement.children = afterHeadingElements;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
74
73
|
|
|
75
74
|
return mix;
|
|
76
75
|
}
|