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
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { isDocument, type
|
|
1
|
+
import { isDocument, type Document } from 'tsprose';
|
|
2
2
|
import { isContentItem } from '@erudit-js/core/content/item';
|
|
3
3
|
import type { PageContentItem } from '@erudit-js/core/content/page';
|
|
4
|
-
import { isIncludedRawElement } from '@erudit-js/prose';
|
|
5
4
|
|
|
6
5
|
import type { ContentNavNode } from '../nav/types';
|
|
7
6
|
import { logContentError } from './utils/contentError';
|
|
8
7
|
import { insertContentItem } from './utils/insertContentItem';
|
|
9
|
-
import { resolveEruditProse } from '../../prose/repository/resolve';
|
|
10
8
|
import { insertContentResolved } from './utils/insertContentResolved';
|
|
11
9
|
|
|
12
10
|
export async function resolvePage(pageNode: ContentNavNode) {
|
|
@@ -16,7 +14,7 @@ export async function resolvePage(pageNode: ContentNavNode) {
|
|
|
16
14
|
|
|
17
15
|
try {
|
|
18
16
|
const pageModule = await ERUDIT.import<{
|
|
19
|
-
content:
|
|
17
|
+
content: Document;
|
|
20
18
|
page: PageContentItem;
|
|
21
19
|
}>(ERUDIT.paths.project(`content/${pageNode.contentRelPath}/page`));
|
|
22
20
|
|
|
@@ -30,49 +28,31 @@ export async function resolvePage(pageNode: ContentNavNode) {
|
|
|
30
28
|
|
|
31
29
|
await insertContentItem(pageNode, pageModule.page);
|
|
32
30
|
|
|
33
|
-
const elementsCount: Record<string, number> = {};
|
|
34
|
-
|
|
35
31
|
const proseDocument = pageModule.content;
|
|
36
|
-
const
|
|
37
|
-
proseDocument.
|
|
38
|
-
true,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (isIncludedRawElement(rawElement)) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
ERUDIT.config.countElements.flat().includes(rawElement.schemaName)
|
|
48
|
-
) {
|
|
49
|
-
elementsCount[rawElement.schemaName] =
|
|
50
|
-
(elementsCount[rawElement.schemaName] || 0) + 1;
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
);
|
|
32
|
+
const result = await ERUDIT.repository.prose.fromRaw({
|
|
33
|
+
rawProse: proseDocument.rawProse,
|
|
34
|
+
toc: { enabled: true },
|
|
35
|
+
snippets: { enabled: true },
|
|
36
|
+
});
|
|
54
37
|
|
|
55
|
-
if (
|
|
38
|
+
if (result.toc?.length) {
|
|
56
39
|
await ERUDIT.db.insert(ERUDIT.db.schema.contentToc).values({
|
|
57
40
|
fullId: pageNode.fullId,
|
|
58
|
-
toc:
|
|
41
|
+
toc: result.toc,
|
|
59
42
|
});
|
|
60
43
|
}
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
count,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
45
|
+
await ERUDIT.repository.content.updateSchemaCounts(
|
|
46
|
+
pageNode.fullId,
|
|
47
|
+
result.schemaCounts,
|
|
48
|
+
);
|
|
69
49
|
|
|
70
50
|
await ERUDIT.db.insert(ERUDIT.db.schema.pages).values({
|
|
71
51
|
fullId: pageNode.fullId,
|
|
72
|
-
prose:
|
|
52
|
+
prose: result.prose,
|
|
73
53
|
});
|
|
74
54
|
|
|
75
|
-
await insertContentResolved(pageNode.fullId, 'page',
|
|
55
|
+
await insertContentResolved(pageNode.fullId, 'page', result);
|
|
76
56
|
} catch (error) {
|
|
77
57
|
logContentError(pageNode);
|
|
78
58
|
throw error;
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import { eq } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
isRawElement,
|
|
5
|
-
walkElements,
|
|
6
|
-
type AnyDocument,
|
|
7
|
-
type AnySchema,
|
|
8
|
-
} from '@jsprose/core';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { isDocument, type Document } from 'tsprose';
|
|
9
4
|
import {
|
|
10
5
|
topicParts,
|
|
11
6
|
type TopicContentItem,
|
|
12
7
|
} from '@erudit-js/core/content/topic';
|
|
13
8
|
import { isContentItem } from '@erudit-js/core/content/item';
|
|
14
|
-
import {
|
|
15
|
-
isIncludedRawElement,
|
|
16
|
-
type EruditRawElement,
|
|
17
|
-
type ResolvedTocItem,
|
|
18
|
-
} from '@erudit-js/prose';
|
|
19
|
-
import { problemSchema } from '@erudit-js/prose/elements/problem/problem';
|
|
20
|
-
import { problemsSchema } from '@erudit-js/prose/elements/problem/problems';
|
|
21
9
|
|
|
22
10
|
import type { ContentNavNode } from '../nav/types';
|
|
23
11
|
import { logContentError } from './utils/contentError';
|
|
24
12
|
import { insertContentItem } from './utils/insertContentItem';
|
|
25
|
-
import { resolveEruditProse } from '../../prose/repository/resolve';
|
|
26
13
|
import { insertContentResolved } from './utils/insertContentResolved';
|
|
14
|
+
import { problemSchema } from '@erudit-js/prose/elements/problem/problem';
|
|
15
|
+
import { problemsSchema } from '@erudit-js/prose/elements/problem/problems';
|
|
27
16
|
|
|
28
17
|
export async function resolveTopic(topicNode: ContentNavNode) {
|
|
29
18
|
ERUDIT.log.debug.start(
|
|
@@ -44,179 +33,59 @@ export async function resolveTopic(topicNode: ContentNavNode) {
|
|
|
44
33
|
fullId: topicNode.fullId,
|
|
45
34
|
});
|
|
46
35
|
|
|
47
|
-
const elementsCount: Record<string, number> = {};
|
|
48
|
-
|
|
49
36
|
for (const topicPart of topicParts) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
}>(
|
|
53
|
-
ERUDIT.paths.project(
|
|
54
|
-
`content/${topicNode.contentRelPath}/${topicPart}`,
|
|
55
|
-
),
|
|
56
|
-
{ try: true },
|
|
37
|
+
const topicPartPath = ERUDIT.paths.project(
|
|
38
|
+
`content/${topicNode.contentRelPath}/${topicPart}`,
|
|
57
39
|
);
|
|
58
40
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const resolvedTopicPart = await resolveEruditProse(
|
|
63
|
-
topicPartDocument.default.content,
|
|
64
|
-
true,
|
|
65
|
-
async ({ rawElement, proseElement }) => {
|
|
66
|
-
//
|
|
67
|
-
// Auto-adding problems to TOC if in practice topic part
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
if (topicPart === 'practice') {
|
|
71
|
-
if (
|
|
72
|
-
(rawElement as EruditRawElement<AnySchema>).title &&
|
|
73
|
-
proseElement.id
|
|
74
|
-
) {
|
|
75
|
-
if (
|
|
76
|
-
isRawElement(rawElement, problemSchema) ||
|
|
77
|
-
isRawElement(rawElement, problemsSchema)
|
|
78
|
-
) {
|
|
79
|
-
practiceProblemsTocItems.push({
|
|
80
|
-
type: 'element',
|
|
81
|
-
elementId: proseElement.id,
|
|
82
|
-
schemaName: rawElement.schemaName,
|
|
83
|
-
title: (rawElement as EruditRawElement<AnySchema>).title!,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
41
|
+
const topicPartExists = ['.js', '.jsx', '.ts', '.tsx'].some((ext) =>
|
|
42
|
+
existsSync(topicPartPath + ext),
|
|
43
|
+
);
|
|
88
44
|
|
|
89
|
-
|
|
90
|
-
// Counting elements for statistics
|
|
91
|
-
//
|
|
45
|
+
if (!topicPartExists) continue;
|
|
92
46
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
47
|
+
const topicPartDocument = await ERUDIT.import<{
|
|
48
|
+
default: Document;
|
|
49
|
+
}>(topicPartPath, { try: true });
|
|
96
50
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
51
|
+
if (isDocument(topicPartDocument?.default)) {
|
|
52
|
+
const result = await ERUDIT.repository.prose.fromRaw({
|
|
53
|
+
rawProse: topicPartDocument.default.rawProse,
|
|
54
|
+
toc: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
addSchemas:
|
|
57
|
+
topicPart === 'practice'
|
|
58
|
+
? [problemSchema, problemsSchema]
|
|
59
|
+
: undefined,
|
|
103
60
|
},
|
|
104
|
-
|
|
61
|
+
snippets: { enabled: true },
|
|
62
|
+
});
|
|
105
63
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (topicPart === 'practice' && practiceProblemsTocItems.length) {
|
|
109
|
-
// Map elementId -> TocItem for both headings and problems
|
|
110
|
-
const itemMap = new Map<string, ResolvedTocItem>();
|
|
111
|
-
|
|
112
|
-
// Collect all existing TOC items (headings) recursively
|
|
113
|
-
const collectItems = (items: ResolvedTocItem[]) => {
|
|
114
|
-
for (const item of items) {
|
|
115
|
-
if (item.elementId) {
|
|
116
|
-
itemMap.set(item.elementId, item);
|
|
117
|
-
}
|
|
118
|
-
if (item.type === 'heading' && item.children?.length) {
|
|
119
|
-
collectItems(item.children);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
collectItems(resolvedTopicPart.tocItems || []);
|
|
124
|
-
|
|
125
|
-
// Add practice problems to the map
|
|
126
|
-
practiceProblemsTocItems.forEach((p) => {
|
|
127
|
-
if (p.elementId) {
|
|
128
|
-
itemMap.set(p.elementId, p);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Rebuild TOC in document order using walkElements
|
|
133
|
-
const result: ResolvedTocItem[] = [];
|
|
134
|
-
const stack: ResolvedTocItem[] = [];
|
|
135
|
-
|
|
136
|
-
await walkElements(resolvedTopicPart.proseElement, (element) => {
|
|
137
|
-
if (element.id && itemMap.has(element.id)) {
|
|
138
|
-
const item = itemMap.get(element.id)!;
|
|
139
|
-
|
|
140
|
-
if (item.type === 'heading') {
|
|
141
|
-
// Pop headings at same or deeper level
|
|
142
|
-
while (stack.length > 0) {
|
|
143
|
-
const last = stack[stack.length - 1]!;
|
|
144
|
-
if (last.type === 'heading' && last.level >= item.level) {
|
|
145
|
-
stack.pop();
|
|
146
|
-
} else {
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Create new heading with empty children array
|
|
152
|
-
const newItem: ResolvedTocItem = {
|
|
153
|
-
...item,
|
|
154
|
-
children: [],
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Add to parent heading or root
|
|
158
|
-
if (stack.length > 0) {
|
|
159
|
-
const parent = stack[stack.length - 1]!;
|
|
160
|
-
if (parent.type === 'heading') {
|
|
161
|
-
parent.children.push(newItem);
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
result.push(newItem);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
stack.push(newItem);
|
|
168
|
-
} else {
|
|
169
|
-
// Non-heading item (problems, etc.)
|
|
170
|
-
if (stack.length > 0) {
|
|
171
|
-
const parent = stack[stack.length - 1]!;
|
|
172
|
-
if (parent.type === 'heading') {
|
|
173
|
-
parent.children.push(item);
|
|
174
|
-
} else {
|
|
175
|
-
result.push(item);
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
result.push(item);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
finalTocItems = result;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (finalTocItems?.length) {
|
|
64
|
+
if (result.toc?.length) {
|
|
188
65
|
await ERUDIT.db.insert(ERUDIT.db.schema.contentToc).values({
|
|
189
66
|
fullId: topicNode.fullId,
|
|
190
67
|
topicPart,
|
|
191
|
-
toc:
|
|
68
|
+
toc: result.toc,
|
|
192
69
|
});
|
|
193
70
|
}
|
|
194
71
|
|
|
72
|
+
await ERUDIT.repository.content.updateSchemaCounts(
|
|
73
|
+
topicNode.fullId,
|
|
74
|
+
result.schemaCounts,
|
|
75
|
+
);
|
|
76
|
+
|
|
195
77
|
await ERUDIT.db
|
|
196
78
|
.update(ERUDIT.db.schema.topics)
|
|
197
79
|
.set({
|
|
198
|
-
[topicPart]:
|
|
80
|
+
[topicPart]: result.prose,
|
|
199
81
|
})
|
|
200
82
|
.where(eq(ERUDIT.db.schema.topics.fullId, topicNode.fullId));
|
|
201
83
|
|
|
202
|
-
await insertContentResolved(
|
|
203
|
-
topicNode.fullId,
|
|
204
|
-
topicPart,
|
|
205
|
-
resolvedTopicPart,
|
|
206
|
-
);
|
|
84
|
+
await insertContentResolved(topicNode.fullId, topicPart, result);
|
|
207
85
|
}
|
|
208
86
|
}
|
|
209
|
-
|
|
210
|
-
for (const [schemaName, count] of Object.entries(elementsCount)) {
|
|
211
|
-
await ERUDIT.repository.content.addElementCount(
|
|
212
|
-
topicNode.fullId,
|
|
213
|
-
schemaName,
|
|
214
|
-
count,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
87
|
} catch (error) {
|
|
218
88
|
logContentError(topicNode);
|
|
219
|
-
//console.log(error);
|
|
220
89
|
throw error;
|
|
221
90
|
}
|
|
222
91
|
}
|
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { sql } from 'drizzle-orm';
|
|
3
|
-
import { type ResolvedRawElement } from '@jsprose/core';
|
|
4
3
|
import type { ContentProseType } from '@erudit-js/core/content/prose';
|
|
5
|
-
import
|
|
4
|
+
import { builtValidPaths } from '../../global/build';
|
|
6
5
|
import type {
|
|
7
6
|
ContentLinks,
|
|
8
7
|
ContentLinkUsage,
|
|
9
|
-
} from '@erudit-js/prose/elements/link/
|
|
8
|
+
} from '@erudit-js/prose/elements/link/hook';
|
|
9
|
+
import {
|
|
10
|
+
toKeySnippet,
|
|
11
|
+
toSearchSnippet,
|
|
12
|
+
toSeoSnippet,
|
|
13
|
+
type EruditRawToProseResult,
|
|
14
|
+
} from '@erudit-js/prose';
|
|
10
15
|
|
|
11
16
|
export async function insertContentResolved(
|
|
12
17
|
contentFullId: string,
|
|
13
18
|
contentProseType: ContentProseType,
|
|
14
|
-
|
|
19
|
+
result: EruditRawToProseResult,
|
|
15
20
|
) {
|
|
16
|
-
for (const file of
|
|
21
|
+
for (const file of result.files) {
|
|
17
22
|
await ERUDIT.repository.db.pushFile(file, `content-item:${contentFullId}`);
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
for (const [uniqueName, unique] of Object.entries(
|
|
25
|
+
for (const [uniqueName, unique] of Object.entries(result.uniques)) {
|
|
21
26
|
await ERUDIT.db.insert(ERUDIT.db.schema.contentUniques).values({
|
|
22
27
|
contentFullId,
|
|
23
28
|
contentProseType,
|
|
24
29
|
uniqueName,
|
|
25
|
-
title:
|
|
30
|
+
title: result.uniqueTitles[uniqueName],
|
|
26
31
|
prose: unique,
|
|
27
32
|
});
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
for (const snippet of
|
|
35
|
+
for (const snippet of result.snippets) {
|
|
31
36
|
await ERUDIT.db.insert(ERUDIT.db.schema.contentSnippets).values({
|
|
32
37
|
contentFullId,
|
|
33
38
|
contentProseType,
|
|
34
39
|
elementId: snippet.elementId,
|
|
35
40
|
schemaName: snippet.schemaName,
|
|
36
|
-
snippetData: snippet.
|
|
37
|
-
search: !!snippet.
|
|
38
|
-
|
|
39
|
-
seo: !!snippet.
|
|
41
|
+
snippetData: snippet.snippet,
|
|
42
|
+
search: !!toSearchSnippet(snippet.snippet),
|
|
43
|
+
key: !!toKeySnippet(snippet.snippet),
|
|
44
|
+
seo: !!toSeoSnippet(snippet.snippet),
|
|
40
45
|
});
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -49,25 +54,26 @@ export async function insertContentResolved(
|
|
|
49
54
|
await deduplicateTopicSnippetsSearch(contentFullId);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
for (const problemScript of
|
|
57
|
+
for (const problemScript of result.problemScripts) {
|
|
53
58
|
await ERUDIT.repository.db.pushProblemScript(problemScript, contentFullId);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
const
|
|
57
|
-
contentFullId,
|
|
58
|
-
resolveResult.contentLinks,
|
|
59
|
-
);
|
|
61
|
+
const targetMap = filterTargetMap(contentFullId, result.contentLinks);
|
|
60
62
|
|
|
61
|
-
await insertContentDeps(contentFullId,
|
|
63
|
+
await insertContentDeps(contentFullId, targetMap);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
async function insertContentDeps(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
async function insertContentDeps(
|
|
67
|
+
fromFullId: string,
|
|
68
|
+
targetMap: Map<string, Set<string>>,
|
|
69
|
+
) {
|
|
70
|
+
const contentDeps = Array.from(targetMap.entries())
|
|
71
|
+
.filter(([toFullId]) => toFullId !== fromFullId)
|
|
72
|
+
.map(([toFullId, uniqueSet]) => ({
|
|
68
73
|
fromFullId,
|
|
69
74
|
toFullId,
|
|
70
75
|
hard: false,
|
|
76
|
+
uniqueNames: uniqueSet.size > 0 ? Array.from(uniqueSet).join(',') : null,
|
|
71
77
|
}));
|
|
72
78
|
|
|
73
79
|
if (contentDeps.length > 0) {
|
|
@@ -78,10 +84,12 @@ async function insertContentDeps(fromFullId: string, toFullIds: string[]) {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
// Returns a map from resolved toFullId → set of unique names targeted in that
|
|
88
|
+
// content item. An empty set means the dep targets the whole page (no unique).
|
|
89
|
+
function filterTargetMap(
|
|
82
90
|
contentFullId: string,
|
|
83
91
|
contentLinks: ContentLinks,
|
|
84
|
-
) {
|
|
92
|
+
): Map<string, Set<string>> {
|
|
85
93
|
const brokenLinkMessage = (message: string, metas: ContentLinkUsage[]) => {
|
|
86
94
|
let output = `${message} in ${ERUDIT.log.stress(contentFullId)}:\n`;
|
|
87
95
|
for (const { type, label } of metas) {
|
|
@@ -90,7 +98,7 @@ function filterTargetFullIds(
|
|
|
90
98
|
return output;
|
|
91
99
|
};
|
|
92
100
|
|
|
93
|
-
const
|
|
101
|
+
const targetMap = new Map<string, Set<string>>();
|
|
94
102
|
|
|
95
103
|
for (const [storageKey, metas] of contentLinks) {
|
|
96
104
|
if (storageKey.startsWith('<link:unknown>/')) {
|
|
@@ -103,6 +111,14 @@ function filterTargetFullIds(
|
|
|
103
111
|
} else if (storageKey.startsWith('<link:global>')) {
|
|
104
112
|
try {
|
|
105
113
|
const globalContentId = storageKey.replace('<link:global>/', '');
|
|
114
|
+
|
|
115
|
+
// Extract unique name before stripping it for nav resolution
|
|
116
|
+
const parts = globalContentId.split('/');
|
|
117
|
+
const lastPart = parts.at(-1);
|
|
118
|
+
const uniqueName = lastPart?.startsWith('$')
|
|
119
|
+
? lastPart.slice(1)
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
106
122
|
const targetFullId = globalContentToNavNode(globalContentId).fullId;
|
|
107
123
|
|
|
108
124
|
if (
|
|
@@ -110,7 +126,12 @@ function filterTargetFullIds(
|
|
|
110
126
|
(meta) => meta.type === 'Dep' || meta.type === 'Dependency',
|
|
111
127
|
)
|
|
112
128
|
) {
|
|
113
|
-
|
|
129
|
+
if (!targetMap.has(targetFullId)) {
|
|
130
|
+
targetMap.set(targetFullId, new Set());
|
|
131
|
+
}
|
|
132
|
+
if (uniqueName) {
|
|
133
|
+
targetMap.get(targetFullId)!.add(uniqueName);
|
|
134
|
+
}
|
|
114
135
|
}
|
|
115
136
|
} catch {
|
|
116
137
|
ERUDIT.log.warn(
|
|
@@ -123,21 +144,28 @@ function filterTargetFullIds(
|
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
return
|
|
147
|
+
return targetMap;
|
|
127
148
|
}
|
|
128
149
|
|
|
129
150
|
function globalContentToNavNode(globalContentPath: string) {
|
|
151
|
+
// Validate the full path (including any $unique suffix) against the complete
|
|
152
|
+
// set of known valid paths built from source files. This catches broken
|
|
153
|
+
// unique names and content paths before we ever touch the nav tree.
|
|
154
|
+
if (builtValidPaths && !builtValidPaths.has(globalContentPath)) {
|
|
155
|
+
throw new Error(`Path not found in \$CONTENT: ${globalContentPath}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
130
158
|
const parts = globalContentPath.split('/');
|
|
131
159
|
|
|
132
160
|
if (parts.at(-1)?.startsWith('$')) {
|
|
133
161
|
parts.pop();
|
|
134
162
|
}
|
|
135
163
|
|
|
136
|
-
|
|
164
|
+
// If the exact node isn't found the last segment is a topic part — fall back to parent.
|
|
165
|
+
return (
|
|
137
166
|
ERUDIT.contentNav.getNode(parts.join('/')) ??
|
|
138
|
-
ERUDIT.contentNav.getNodeOrThrow(parts.slice(0, -1).join('/'))
|
|
139
|
-
|
|
140
|
-
return navNode;
|
|
167
|
+
ERUDIT.contentNav.getNodeOrThrow(parts.slice(0, -1).join('/'))
|
|
168
|
+
);
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
async function deduplicateTopicSnippetsSearch(contentFullId: string) {
|
|
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm';
|
|
|
2
2
|
import { contentTypes } from '@erudit-js/core/content/type';
|
|
3
3
|
import { headingSchema } from '@erudit-js/prose/elements/heading/core';
|
|
4
4
|
import type { TopicPart } from '@erudit-js/core/content/topic';
|
|
5
|
+
import { toSearchSnippet } from '@erudit-js/prose';
|
|
5
6
|
|
|
6
7
|
export async function searchIndexContentTypes(): Promise<SearchEntriesList[]> {
|
|
7
8
|
const entryLists: SearchEntriesList[] = [];
|
|
@@ -119,33 +120,15 @@ export async function searchIndexSnippets(): Promise<SearchEntriesList[]> {
|
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
const
|
|
123
|
-
let searchTitle = dbSnippet.snippetData.title!;
|
|
124
|
-
let searchDescription = dbSnippet.snippetData.description;
|
|
125
|
-
let searchSynonyms: string[] | undefined = undefined;
|
|
126
|
-
if (snippetSearch) {
|
|
127
|
-
if (typeof snippetSearch === 'boolean') {
|
|
128
|
-
} else if (typeof snippetSearch === 'string') {
|
|
129
|
-
snippetSearch.trim() && (searchTitle = snippetSearch.trim());
|
|
130
|
-
} else if (Array.isArray(snippetSearch)) {
|
|
131
|
-
searchSynonyms = snippetSearch.length > 0 ? snippetSearch : undefined;
|
|
132
|
-
} else {
|
|
133
|
-
searchTitle = snippetSearch.title || searchTitle;
|
|
134
|
-
searchDescription = snippetSearch.description || searchDescription;
|
|
135
|
-
searchSynonyms =
|
|
136
|
-
snippetSearch.synonyms && snippetSearch.synonyms.length > 0
|
|
137
|
-
? snippetSearch.synonyms
|
|
138
|
-
: undefined;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
123
|
+
const searchSnippet = toSearchSnippet(dbSnippet.snippetData)!;
|
|
141
124
|
|
|
142
125
|
entryLists.get(dbSnippet.schemaName)!.entries.push({
|
|
143
126
|
category: 'element:' + dbSnippet.schemaName,
|
|
144
|
-
title:
|
|
145
|
-
description:
|
|
127
|
+
title: searchSnippet.title,
|
|
128
|
+
description: searchSnippet.description,
|
|
146
129
|
link,
|
|
147
130
|
location: locationTitle,
|
|
148
|
-
synonyms:
|
|
131
|
+
synonyms: searchSnippet.synonyms,
|
|
149
132
|
});
|
|
150
133
|
}
|
|
151
134
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { eq, like } from 'drizzle-orm';
|
|
3
3
|
import { globSync } from 'glob';
|
|
4
|
-
import { isRawElement, type
|
|
4
|
+
import { isRawElement, type ProseElement } from 'tsprose';
|
|
5
5
|
import {
|
|
6
6
|
contributorIdToPropertyName,
|
|
7
7
|
globalContributorsObject,
|
|
@@ -131,19 +131,18 @@ async function buildContributor(contributorId: string) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
let description: ProseElement
|
|
134
|
+
let description: ProseElement | undefined;
|
|
135
135
|
|
|
136
136
|
if (isRawElement(def?.description)) {
|
|
137
|
-
const
|
|
138
|
-
def.description,
|
|
139
|
-
|
|
140
|
-
);
|
|
137
|
+
const result = await ERUDIT.repository.prose.fromRaw({
|
|
138
|
+
rawProse: def.description,
|
|
139
|
+
});
|
|
141
140
|
|
|
142
|
-
for (const file of
|
|
141
|
+
for (const file of result.files) {
|
|
143
142
|
await ERUDIT.repository.db.pushFile(file, `contributor:${contributorId}`);
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
description =
|
|
145
|
+
description = result.prose;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
await ERUDIT.db.insert(ERUDIT.db.schema.contributors).values({
|
|
@@ -3,16 +3,23 @@ import { sn } from 'unslash';
|
|
|
3
3
|
export async function pushFile(filepath: string, role: string): Promise<void> {
|
|
4
4
|
filepath = sn(filepath);
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let relativePath: string;
|
|
7
|
+
|
|
8
|
+
if (filepath.startsWith(ERUDIT.paths.project() + '/')) {
|
|
9
|
+
// Absolute path inside the project – convert to relative
|
|
10
|
+
relativePath = filepath.slice(ERUDIT.paths.project().length + 1);
|
|
11
|
+
} else if (/^([A-Za-z]:\/|\/)/.test(filepath)) {
|
|
12
|
+
// Absolute path outside the project – reject
|
|
7
13
|
throw createError({
|
|
8
14
|
statusCode: 400,
|
|
9
15
|
statusMessage: 'File is outside of project directory!',
|
|
10
16
|
message: `Can not add ${filepath} as it is outside of the project directory!`,
|
|
11
17
|
});
|
|
18
|
+
} else {
|
|
19
|
+
// Already a project-relative path
|
|
20
|
+
relativePath = filepath;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
const relativePath = filepath.replace(ERUDIT.paths.project() + '/', '');
|
|
15
|
-
|
|
16
23
|
await ERUDIT.db
|
|
17
24
|
.insert(ERUDIT.db.schema.files)
|
|
18
25
|
.values({
|
|
@@ -1,19 +1,30 @@
|
|
|
1
|
+
import { sn } from 'unslash';
|
|
2
|
+
|
|
1
3
|
export async function pushProblemScript(
|
|
2
4
|
problemScriptSrc: string,
|
|
3
5
|
contentFullId: string,
|
|
4
6
|
): Promise<void> {
|
|
5
|
-
|
|
7
|
+
problemScriptSrc = sn(problemScriptSrc);
|
|
8
|
+
|
|
9
|
+
let relativePath: string;
|
|
6
10
|
|
|
7
|
-
if (
|
|
11
|
+
if (problemScriptSrc.startsWith(ERUDIT.paths.project() + '/')) {
|
|
12
|
+
// Absolute path inside the project – convert to relative
|
|
13
|
+
relativePath = problemScriptSrc.slice(ERUDIT.paths.project().length + 1);
|
|
14
|
+
} else if (/^([A-Za-z]:\/|\/)/.test(problemScriptSrc)) {
|
|
15
|
+
// Absolute path outside the project – reject
|
|
8
16
|
throw createError({
|
|
9
17
|
statusCode: 400,
|
|
10
18
|
statusMessage: 'Problem script is outside of project directory!',
|
|
11
19
|
message: `Can not add problem script from ${problemScriptSrc} as it is outside of the project directory!`,
|
|
12
20
|
});
|
|
21
|
+
} else {
|
|
22
|
+
// Already a project-relative path
|
|
23
|
+
relativePath = problemScriptSrc;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
await ERUDIT.db.insert(ERUDIT.db.schema.problemScripts).values({
|
|
16
|
-
problemScriptSrc,
|
|
27
|
+
problemScriptSrc: relativePath,
|
|
17
28
|
contentFullId,
|
|
18
29
|
});
|
|
19
30
|
}
|
|
@@ -12,6 +12,9 @@ export const contentDeps = sqliteTable(
|
|
|
12
12
|
toFullId: text().notNull(),
|
|
13
13
|
hard: integer({ mode: 'boolean' }).notNull(),
|
|
14
14
|
reason: text(),
|
|
15
|
+
// Comma-separated unique names that were specifically targeted via <Dep>.
|
|
16
|
+
// Only set for auto deps; null means the dep targets the whole page.
|
|
17
|
+
uniqueNames: text(),
|
|
15
18
|
},
|
|
16
19
|
(table) => [
|
|
17
20
|
primaryKey({
|