erudit 3.0.0-dev.11 → 3.0.0-dev.13
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/components/SiteMain.vue +1 -1
- package/app/components/aside/major/panes/nav/Nav.vue +11 -6
- package/app/components/aside/major/panes/nav/NavBook.vue +19 -11
- package/app/components/aside/major/panes/nav/fnav/FNavFlags.vue +1 -1
- package/app/components/aside/minor/Contribute.vue +2 -2
- package/app/components/aside/minor/content/AsideMinorContent.vue +1 -1
- package/app/components/aside/minor/topic/AsideMinorTopic.vue +1 -1
- package/app/components/aside/minor/topic/TopicContributors.vue +2 -2
- package/app/components/aside/minor/topic/TopicToc.vue +1 -6
- package/app/components/aside/minor/topic/TopicTocItem.vue +3 -1
- package/app/components/main/utils/Breadcrumb.vue +1 -1
- package/app/components/main/utils/ContentReferences.vue +1 -1
- package/app/components/main/utils/reference/ReferenceGroup.vue +1 -1
- package/app/components/main/utils/reference/ReferenceItem.vue +5 -3
- package/app/components/main/utils/reference/ReferenceSource.vue +6 -4
- package/app/components/preview/PreviewScreen.vue +2 -2
- package/app/components/preview/display/PageLink.vue +1 -1
- package/app/composables/bitranContent.ts +2 -3
- package/app/composables/contentPage.ts +8 -7
- package/app/composables/formatText.ts +21 -8
- package/app/composables/phrases.ts +0 -16
- package/app/plugins/prerender.server.ts +22 -0
- package/app/scripts/preview/build.ts +1 -5
- package/app/scripts/preview/data/pageLink.ts +1 -0
- package/app/styles/normalize.scss +0 -14
- package/globals/content.ts +5 -0
- package/module/imports.ts +1 -0
- package/nuxt.config.ts +10 -5
- package/package.json +8 -8
- package/server/api/aside/major/nav/bookIds.ts +2 -2
- package/server/api/aside/minor/path.ts +19 -11
- package/server/api/bitran/content/[...location].ts +4 -2
- package/server/api/bitran/toc/[...location].ts +4 -2
- package/server/api/content/data.ts +5 -2
- package/server/api/prerender.ts +120 -0
- package/server/api/preview/page/[...parts].ts +30 -4
- package/server/api/preview/unique/{[location].ts → [...location].ts} +4 -3
- package/server/plugin/bitran/content.ts +3 -16
- package/server/plugin/bitran/elements/include.ts +40 -36
- package/server/plugin/bitran/location.ts +24 -10
- package/server/plugin/bitran/toc.ts +7 -2
- package/server/plugin/bitran/transpiler.ts +10 -1
- package/server/plugin/build/jobs/content/generic.ts +5 -6
- package/server/plugin/build/jobs/content/parse.ts +5 -1
- package/server/plugin/build/jobs/content/type/group.ts +2 -2
- package/server/plugin/build/jobs/content/type/topic.ts +2 -2
- package/server/plugin/build/jobs/nav.ts +28 -15
- package/server/plugin/content/context.ts +5 -2
- package/server/plugin/db/entities/Content.ts +0 -4
- package/server/plugin/db/setup.ts +1 -1
- package/server/plugin/importer.ts +5 -1
- package/server/plugin/nav/node.ts +2 -1
- package/server/plugin/nav/utils.ts +66 -24
- package/server/plugin/repository/content.ts +26 -23
- package/server/plugin/repository/contentId.ts +40 -0
- package/server/plugin/repository/frontNav.ts +6 -9
- package/server/plugin/repository/topic.ts +4 -1
- package/shared/aside/minor.ts +2 -2
- package/shared/bitran/contentId.ts +12 -44
- package/shared/content/bookId.ts +12 -0
- package/shared/frontNav.ts +1 -1
- package/test/contentId.test.ts +10 -10
|
@@ -12,7 +12,7 @@ export async function buildGroup({
|
|
|
12
12
|
config,
|
|
13
13
|
}: BuilderFunctionArgs<GroupConfig>) {
|
|
14
14
|
const dbGroup = new DbGroup();
|
|
15
|
-
dbGroup.contentId = navNode.
|
|
15
|
+
dbGroup.contentId = navNode.fullId;
|
|
16
16
|
dbGroup.type = config?.type || 'folder';
|
|
17
17
|
|
|
18
18
|
try {
|
|
@@ -26,7 +26,7 @@ export async function buildGroup({
|
|
|
26
26
|
await parseBitranContent(
|
|
27
27
|
{
|
|
28
28
|
type: 'group',
|
|
29
|
-
path:
|
|
29
|
+
path: dbGroup.contentId,
|
|
30
30
|
},
|
|
31
31
|
strContent,
|
|
32
32
|
);
|
|
@@ -9,7 +9,7 @@ import { parseBitranContent } from '../parse';
|
|
|
9
9
|
|
|
10
10
|
export async function buildTopic({ navNode }: BuilderFunctionArgs) {
|
|
11
11
|
const dbTopic = new DbTopic();
|
|
12
|
-
dbTopic.contentId = navNode.
|
|
12
|
+
dbTopic.contentId = navNode.fullId;
|
|
13
13
|
const existingTopicParts: TopicPart[] = [];
|
|
14
14
|
|
|
15
15
|
for (const topicPart of topicParts) {
|
|
@@ -25,7 +25,7 @@ export async function buildTopic({ navNode }: BuilderFunctionArgs) {
|
|
|
25
25
|
await parseBitranContent(
|
|
26
26
|
{
|
|
27
27
|
type: topicPart,
|
|
28
|
-
path:
|
|
28
|
+
path: dbTopic.contentId,
|
|
29
29
|
},
|
|
30
30
|
strContent,
|
|
31
31
|
);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { globSync } from 'glob';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { resolvePaths } from '@erudit-js/cog/kit';
|
|
4
3
|
import {
|
|
5
4
|
contentTypes,
|
|
6
5
|
topicParts,
|
|
@@ -54,13 +53,15 @@ async function scanChildNodes(
|
|
|
54
53
|
parent: NavNode | RootNavNode,
|
|
55
54
|
insideBook: boolean,
|
|
56
55
|
): Promise<{ children: NavNode[] | undefined; newIds: Ids }> {
|
|
57
|
-
const currentFsPath = isRootNode(parent) ? '' : parent.path;
|
|
56
|
+
const currentFsPath = isRootNode(parent) ? '' : parent.path + '/';
|
|
58
57
|
|
|
59
58
|
const nodeFsPaths = globSync(
|
|
60
|
-
`${
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
`${currentFsPath}*/{${contentTypes.join(',')}}.{ts,js}`,
|
|
60
|
+
{
|
|
61
|
+
cwd: PROJECT_DIR + '/content',
|
|
62
|
+
posix: true,
|
|
63
|
+
},
|
|
64
|
+
).sort();
|
|
64
65
|
|
|
65
66
|
let newIds: Ids = {};
|
|
66
67
|
const children: NavNode[] = [];
|
|
@@ -77,11 +78,7 @@ async function scanChildNodes(
|
|
|
77
78
|
|
|
78
79
|
if (!pathParts) continue; // Wrong path pattern
|
|
79
80
|
|
|
80
|
-
const nodePath = nodeFsPath
|
|
81
|
-
.replace(PROJECT_DIR + '/content/', '')
|
|
82
|
-
.split('/')
|
|
83
|
-
.slice(0, -1)
|
|
84
|
-
.join('/');
|
|
81
|
+
const nodePath = nodeFsPath.split('/').slice(0, -1).join('/');
|
|
85
82
|
|
|
86
83
|
if (pathParts.type === 'book' && insideBook) {
|
|
87
84
|
logger.warn(
|
|
@@ -96,8 +93,8 @@ async function scanChildNodes(
|
|
|
96
93
|
const parentId = isRootNode(parent)
|
|
97
94
|
? ''
|
|
98
95
|
: parent.skip
|
|
99
|
-
? parent.
|
|
100
|
-
: parent.
|
|
96
|
+
? parent.fullId.split('/').slice(0, -1).join('/')
|
|
97
|
+
: parent.fullId;
|
|
101
98
|
|
|
102
99
|
// Regular id might not include parent id part if it is skipped
|
|
103
100
|
const id = parentId ? `${parentId}/${pathParts.id}` : pathParts.id;
|
|
@@ -117,14 +114,30 @@ async function scanChildNodes(
|
|
|
117
114
|
? pathParts.id
|
|
118
115
|
: `${parent.fullId}/${pathParts.id}`;
|
|
119
116
|
|
|
117
|
+
// Short id skips parent ids for nodes with skip=true, except current node
|
|
118
|
+
const shortId = isRootNode(parent)
|
|
119
|
+
? pathParts.id
|
|
120
|
+
: (() => {
|
|
121
|
+
// Traverse up, skipping parent ids where skip=true
|
|
122
|
+
let ids: string[] = [];
|
|
123
|
+
let p: NavNode | RootNavNode | undefined = parent;
|
|
124
|
+
while (p && !isRootNode(p)) {
|
|
125
|
+
if (!p.skip) ids.unshift(p.idPart);
|
|
126
|
+
p = p.parent;
|
|
127
|
+
}
|
|
128
|
+
ids.push(pathParts.id);
|
|
129
|
+
return ids.join('/');
|
|
130
|
+
})();
|
|
131
|
+
|
|
120
132
|
const skip = pathParts.sep === '+';
|
|
121
133
|
|
|
122
134
|
const childNode: NavNode = {
|
|
123
135
|
parent,
|
|
124
136
|
type: pathParts.type,
|
|
125
137
|
path: nodePath,
|
|
126
|
-
id,
|
|
138
|
+
idPart: pathParts.id,
|
|
127
139
|
fullId,
|
|
140
|
+
shortId,
|
|
128
141
|
skip,
|
|
129
142
|
};
|
|
130
143
|
|
|
@@ -203,7 +216,7 @@ function debugPrintNav(node: RootNavNode) {
|
|
|
203
216
|
console.log(
|
|
204
217
|
isRootNode(node)
|
|
205
218
|
? chalk.dim('#root')
|
|
206
|
-
: `${' '.repeat(indent)}${node.
|
|
219
|
+
: `${' '.repeat(indent)}${node.idPart} ${chalk.dim(`[${node.type}${node.skip ? ', ' + chalk.yellow('skip') : ''}]`)}`,
|
|
207
220
|
);
|
|
208
221
|
|
|
209
222
|
if (node.children)
|
|
@@ -9,6 +9,7 @@ import { ERUDIT_SERVER } from '@server/global';
|
|
|
9
9
|
import { getIdsUp, isSkipId } from '@server/nav/utils';
|
|
10
10
|
import { DbContent } from '@server/db/entities/Content';
|
|
11
11
|
import { DbContributor } from '@server/db/entities/Contributor';
|
|
12
|
+
import { getFullContentId } from '@server/repository/contentId';
|
|
12
13
|
|
|
13
14
|
import type { Context } from '@shared/content/context';
|
|
14
15
|
import {
|
|
@@ -19,9 +20,11 @@ import {
|
|
|
19
20
|
import { CONTENT_TYPE_ICON, ICON, TOPIC_PART_ICON } from '@erudit/shared/icons';
|
|
20
21
|
|
|
21
22
|
export async function getContentContext(contentId: string): Promise<Context> {
|
|
23
|
+
contentId = getFullContentId(contentId);
|
|
24
|
+
|
|
22
25
|
const context: Context = [];
|
|
23
26
|
|
|
24
|
-
for (const _contentId of
|
|
27
|
+
for (const _contentId of getIdsUp(contentId).reverse()) {
|
|
25
28
|
const dbContent = await getDbContent(_contentId);
|
|
26
29
|
|
|
27
30
|
context.push({
|
|
@@ -106,7 +109,7 @@ export async function getLocationContext(
|
|
|
106
109
|
|
|
107
110
|
async function getDbContent(contentId: string): Promise<DbContent> {
|
|
108
111
|
const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
|
|
109
|
-
select: ['type', 'title', '
|
|
112
|
+
select: ['type', 'title', 'contentId'],
|
|
110
113
|
where: { contentId },
|
|
111
114
|
});
|
|
112
115
|
|
|
@@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
|
|
|
3
3
|
|
|
4
4
|
import { PROJECT_DIR } from '#erudit/globalPaths';
|
|
5
5
|
import { ERUDIT_SERVER } from '@server/global';
|
|
6
|
+
import { logger } from '../logger';
|
|
6
7
|
|
|
7
8
|
// Database Entities
|
|
8
9
|
import { DbContributor } from './entities/Contributor';
|
|
@@ -14,7 +15,6 @@ import { DbHash } from './entities/Hash';
|
|
|
14
15
|
import { DbTopic } from './entities/Topic';
|
|
15
16
|
import { DbUnique } from './entities/Unique';
|
|
16
17
|
import { DbFile } from './entities/File';
|
|
17
|
-
import { logger } from '../logger';
|
|
18
18
|
|
|
19
19
|
export async function setupDatabase() {
|
|
20
20
|
rmSync(PROJECT_DIR + '/.erudit/data.sqlite', { force: true });
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createJiti } from 'jiti';
|
|
2
|
-
import { ERUDIT_DIR } from '#erudit/globalPaths';
|
|
2
|
+
import { ERUDIT_DIR, PROJECT_DIR } from '#erudit/globalPaths';
|
|
3
3
|
|
|
4
4
|
const jiti = createJiti(ERUDIT_DIR, {
|
|
5
5
|
// Enable reimporting same files during process in development mode
|
|
6
6
|
fsCache: !import.meta.dev,
|
|
7
7
|
moduleCache: !import.meta.dev,
|
|
8
|
+
alias: {
|
|
9
|
+
'#project': PROJECT_DIR,
|
|
10
|
+
'#content': PROJECT_DIR + '/content',
|
|
11
|
+
},
|
|
8
12
|
});
|
|
9
13
|
|
|
10
14
|
export async function IMPORT(...args: Parameters<typeof jiti.import>) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ERUDIT_SERVER } from '@server/global';
|
|
2
|
-
import { isRootNode, type NavNode } from '@server/nav/node';
|
|
2
|
+
import { isRootNode, type NavNode, type RootNavNode } from '@server/nav/node';
|
|
3
3
|
|
|
4
4
|
export async function walkNav(
|
|
5
5
|
step: (node: NavNode) => Promise<void | false>,
|
|
@@ -18,12 +18,22 @@ export async function walkNav(
|
|
|
18
18
|
//
|
|
19
19
|
//
|
|
20
20
|
|
|
21
|
-
export function getNavBookIds() {
|
|
22
|
-
|
|
21
|
+
export function getNavBookIds(mode: 'full' | 'short'): string[] {
|
|
22
|
+
const bookIds: string[] = [];
|
|
23
|
+
|
|
24
|
+
if (!ERUDIT_SERVER.NAV_BOOKS) {
|
|
25
|
+
return bookIds;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const navBook of Object.values(ERUDIT_SERVER.NAV_BOOKS)) {
|
|
29
|
+
bookIds.push(mode === 'full' ? navBook.fullId : navBook.shortId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return bookIds;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export function getNavBookOf(target: string | NavNode): NavNode | undefined {
|
|
26
|
-
const id = typeof target === 'string' ? target : target.
|
|
36
|
+
const id = typeof target === 'string' ? target : target.fullId;
|
|
27
37
|
|
|
28
38
|
if (!id || !ERUDIT_SERVER.NAV_BOOKS) return undefined;
|
|
29
39
|
|
|
@@ -46,7 +56,7 @@ export async function getPreviousNextNav(contentId: string) {
|
|
|
46
56
|
let finish = false;
|
|
47
57
|
|
|
48
58
|
await walkNav(async (navNode) => {
|
|
49
|
-
if (navNode.
|
|
59
|
+
if (navNode.fullId === contentId) {
|
|
50
60
|
book = getNavBookOf(navNode);
|
|
51
61
|
finish = true;
|
|
52
62
|
return;
|
|
@@ -88,32 +98,64 @@ export async function getPreviousNextNav(contentId: string) {
|
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Find navigation node by mixed content ID.
|
|
103
|
+
* It can be full ID, short ID or any combination of present and missing skipped parts.
|
|
104
|
+
*/
|
|
105
|
+
export function getNavNode(mixedContentId: string): NavNode {
|
|
106
|
+
const parts = mixedContentId.split('/');
|
|
107
|
+
let foundNode: NavNode | undefined;
|
|
108
|
+
|
|
109
|
+
function search(node: NavNode, partIdx: number): NavNode | undefined {
|
|
110
|
+
const targetIdPart = parts[partIdx];
|
|
111
|
+
const nodeIdPart = node.idPart;
|
|
112
|
+
|
|
113
|
+
if (nodeIdPart === targetIdPart) {
|
|
114
|
+
if (partIdx === parts.length - 1) {
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const child of node.children || []) {
|
|
119
|
+
const deepResult = search(child, partIdx + 1);
|
|
120
|
+
if (deepResult) return deepResult;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
95
123
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
if (node.skip) {
|
|
125
|
+
for (const child of node.children || []) {
|
|
126
|
+
const deepResult = search(child, partIdx);
|
|
127
|
+
if (deepResult) return deepResult;
|
|
128
|
+
}
|
|
100
129
|
}
|
|
101
|
-
});
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const child of ERUDIT_SERVER.NAV?.children || []) {
|
|
135
|
+
foundNode = search(child, 0);
|
|
136
|
+
if (foundNode) break;
|
|
137
|
+
}
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
139
|
+
if (!foundNode) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Failed to find navigation content node for ID: ${mixedContentId}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
108
144
|
|
|
109
|
-
|
|
145
|
+
return foundNode;
|
|
146
|
+
}
|
|
110
147
|
|
|
148
|
+
export function getIdsUp(contentId: string): string[] {
|
|
111
149
|
const ids: string[] = [];
|
|
150
|
+
const startNavNode = getNavNode(contentId);
|
|
151
|
+
|
|
152
|
+
if (!startNavNode) return ids;
|
|
153
|
+
|
|
154
|
+
let currentNavNode: NavNode = startNavNode;
|
|
112
155
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
currentNavNode = currentNavNode.parent;
|
|
156
|
+
while (currentNavNode?.idPart) {
|
|
157
|
+
ids.push(currentNavNode.fullId);
|
|
158
|
+
currentNavNode = currentNavNode.parent as NavNode;
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
return ids;
|
|
@@ -123,7 +165,7 @@ export async function isSkipId(contentId: string) {
|
|
|
123
165
|
let hidden = false;
|
|
124
166
|
|
|
125
167
|
await walkNav(async (navNode) => {
|
|
126
|
-
if (navNode.
|
|
168
|
+
if (navNode.fullId === contentId) {
|
|
127
169
|
hidden = navNode.skip;
|
|
128
170
|
return false;
|
|
129
171
|
}
|
|
@@ -6,7 +6,11 @@ import { DbContent } from '@server/db/entities/Content';
|
|
|
6
6
|
import { DbContribution } from '@server/db/entities/Contribution';
|
|
7
7
|
import { getContentContext } from '@server/content/context';
|
|
8
8
|
import { DbContributor } from '@server/db/entities/Contributor';
|
|
9
|
-
import { getIdsUp,
|
|
9
|
+
import { getIdsUp, getPreviousNextNav } from '@server/nav/utils';
|
|
10
|
+
import {
|
|
11
|
+
getShortContentId,
|
|
12
|
+
resolveClientContentId,
|
|
13
|
+
} from '@server/repository/contentId';
|
|
10
14
|
import { getTopicPart } from './topic';
|
|
11
15
|
import type { NavNode } from '../nav/node';
|
|
12
16
|
|
|
@@ -14,7 +18,6 @@ import { createContentLink, createTopicPartLink } from '@erudit/shared/link';
|
|
|
14
18
|
import type { PreviousNextItem } from '@erudit/shared/content/previousNext';
|
|
15
19
|
import type { ContentContributor } from '@erudit/shared/contributor';
|
|
16
20
|
import type { ContentGenericData } from '@shared/content/data/base';
|
|
17
|
-
import { toAbsoluteContentId } from '@erudit/shared/bitran/contentId';
|
|
18
21
|
|
|
19
22
|
export async function getContentGenericData(
|
|
20
23
|
contentId: string,
|
|
@@ -23,11 +26,12 @@ export async function getContentGenericData(
|
|
|
23
26
|
where: { contentId },
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
if (!dbContent)
|
|
29
|
+
if (!dbContent) {
|
|
27
30
|
throw createError({
|
|
28
31
|
statusCode: 404,
|
|
29
32
|
message: `Content item "${contentId}" not found!`,
|
|
30
33
|
});
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
const previousNext = await getPreviousNext(contentId);
|
|
33
37
|
const decoration = await getContentDecoration(contentId);
|
|
@@ -57,16 +61,16 @@ export async function getPreviousNext(contentId: string) {
|
|
|
57
61
|
const { previousNav, nextNav } = await getPreviousNextNav(contentId);
|
|
58
62
|
|
|
59
63
|
async function getItemData(navNode: NavNode): Promise<PreviousNextItem> {
|
|
60
|
-
const title = await getContentTitle(navNode.
|
|
64
|
+
const title = await getContentTitle(navNode.fullId);
|
|
61
65
|
|
|
62
66
|
const link = await (async () => {
|
|
63
67
|
if (navNode.type === 'topic')
|
|
64
68
|
return createTopicPartLink(
|
|
65
|
-
await getTopicPart(navNode.
|
|
66
|
-
navNode.
|
|
69
|
+
await getTopicPart(navNode.fullId),
|
|
70
|
+
navNode.shortId,
|
|
67
71
|
);
|
|
68
72
|
|
|
69
|
-
return createContentLink(navNode.type, navNode.
|
|
73
|
+
return createContentLink(navNode.type, navNode.shortId);
|
|
70
74
|
})();
|
|
71
75
|
|
|
72
76
|
return {
|
|
@@ -125,9 +129,13 @@ export async function getContentDependencies(
|
|
|
125
129
|
contentId: string,
|
|
126
130
|
strDependencies: string[],
|
|
127
131
|
) {
|
|
128
|
-
const dependencyIds =
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
const dependencyIds: string[] = [];
|
|
133
|
+
for (const rawDependency of strDependencies) {
|
|
134
|
+
dependencyIds.push(
|
|
135
|
+
resolveClientContentId(rawDependency, contentId, 'full'),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
const dependencies: Record<string, string> = {};
|
|
132
140
|
|
|
133
141
|
for (const dependencyId of dependencyIds)
|
|
@@ -152,27 +160,22 @@ export async function getContentLink(contentId: string) {
|
|
|
152
160
|
where: { contentId },
|
|
153
161
|
});
|
|
154
162
|
|
|
155
|
-
if (!dbContent)
|
|
163
|
+
if (!dbContent) {
|
|
156
164
|
throw createError({
|
|
157
165
|
statusCode: 404,
|
|
158
166
|
statusText: `Missing "${contentId}" content item!`,
|
|
159
167
|
});
|
|
168
|
+
}
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
return createContentLink(dbContent.type, contentId);
|
|
163
|
-
|
|
164
|
-
const topicPart = await getTopicPart(contentId);
|
|
170
|
+
const shortContentId = getShortContentId(contentId);
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
if (dbContent.type !== 'topic') {
|
|
173
|
+
return createContentLink(dbContent.type, shortContentId);
|
|
174
|
+
}
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
|
|
171
|
-
select: ['fullId'],
|
|
172
|
-
where: { contentId },
|
|
173
|
-
});
|
|
176
|
+
const topicPart = await getTopicPart(contentId);
|
|
174
177
|
|
|
175
|
-
return
|
|
178
|
+
return createTopicPartLink(topicPart, shortContentId);
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
export async function getContentContributors(contentId: string) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getNavBookIds, getNavNode } from '@server/nav/utils';
|
|
2
|
+
import { toAbsoluteContentPath } from '@shared/bitran/contentId';
|
|
3
|
+
|
|
4
|
+
export function getFullContentId(mixedContentId: string): string {
|
|
5
|
+
const navNode = getNavNode(mixedContentId);
|
|
6
|
+
return navNode.fullId;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getShortContentId(mixedContentId: string): string {
|
|
10
|
+
const navNode = getNavNode(mixedContentId);
|
|
11
|
+
return navNode.shortId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function serverAbsolutizeContentPath(
|
|
15
|
+
relativePath: string,
|
|
16
|
+
contextPath: string,
|
|
17
|
+
) {
|
|
18
|
+
const absolutePath = toAbsoluteContentPath(
|
|
19
|
+
relativePath,
|
|
20
|
+
contextPath,
|
|
21
|
+
getNavBookIds('full'),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return absolutePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveClientContentId(
|
|
28
|
+
clientContentId: string,
|
|
29
|
+
contextContentId: string,
|
|
30
|
+
mode: 'full' | 'short',
|
|
31
|
+
): string {
|
|
32
|
+
const absoluteContentId = serverAbsolutizeContentPath(
|
|
33
|
+
clientContentId,
|
|
34
|
+
contextContentId,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return mode === 'full'
|
|
38
|
+
? getFullContentId(absoluteContentId)
|
|
39
|
+
: getShortContentId(absoluteContentId);
|
|
40
|
+
}
|
|
@@ -46,12 +46,12 @@ async function toFrontNavItem(arg: ToFuncArg): Promise<FrontNavItem> {
|
|
|
46
46
|
case 'group':
|
|
47
47
|
const dbGroup = await ERUDIT_SERVER.DB.manager.findOne(DbGroup, {
|
|
48
48
|
select: ['type'],
|
|
49
|
-
where: { contentId: arg.node.
|
|
49
|
+
where: { contentId: arg.node.fullId },
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
if (!dbGroup)
|
|
53
53
|
throw new Error(
|
|
54
|
-
`Missing group content item "${arg.node.
|
|
54
|
+
`Missing group content item "${arg.node.fullId}" when creating front nav!`,
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
if (dbGroup.type === 'folder') return await toFrontNavFolder(arg);
|
|
@@ -107,7 +107,7 @@ async function toFrontNavTopic({
|
|
|
107
107
|
node,
|
|
108
108
|
level,
|
|
109
109
|
}: ToFuncArg): Promise<FrontNavTopic> {
|
|
110
|
-
const topicPart = await getTopicPart(node.
|
|
110
|
+
const topicPart = await getTopicPart(node.fullId);
|
|
111
111
|
return {
|
|
112
112
|
type: 'topic',
|
|
113
113
|
part: topicPart,
|
|
@@ -121,18 +121,15 @@ async function toFrontNavBase({
|
|
|
121
121
|
}: ToFuncArg): Promise<Omit<FrontNavBase, 'type'>> {
|
|
122
122
|
const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
|
|
123
123
|
select: ['title', 'navTitle', 'flags'],
|
|
124
|
-
where: { contentId: node.
|
|
124
|
+
where: { contentId: node.fullId },
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
return {
|
|
128
|
-
id: node.
|
|
128
|
+
id: node.shortId,
|
|
129
129
|
fullId: node.fullId,
|
|
130
130
|
level,
|
|
131
131
|
flags: dbContent?.flags,
|
|
132
|
-
label:
|
|
133
|
-
dbContent?.navTitle ||
|
|
134
|
-
dbContent?.title ||
|
|
135
|
-
node.id.split('/').pop()!,
|
|
132
|
+
label: dbContent?.navTitle || dbContent?.title || node.idPart,
|
|
136
133
|
};
|
|
137
134
|
}
|
|
138
135
|
|
|
@@ -2,6 +2,8 @@ import type { TopicPart } from '@erudit-js/cog/schema';
|
|
|
2
2
|
|
|
3
3
|
import { ERUDIT_SERVER } from '@server/global';
|
|
4
4
|
import { DbTopic } from '@server/db/entities/Topic';
|
|
5
|
+
import { getShortContentId } from '@server/repository/contentId';
|
|
6
|
+
|
|
5
7
|
import type { TopicPartLinks } from '@shared/content/data/type/topic';
|
|
6
8
|
import { createTopicPartLink } from '@shared/link';
|
|
7
9
|
|
|
@@ -23,10 +25,11 @@ export async function getTopicPart(contentId: string): Promise<TopicPart> {
|
|
|
23
25
|
|
|
24
26
|
export async function getTopicPartsLinks(topicId: string) {
|
|
25
27
|
const existingTopicParts = await getTopicParts(topicId);
|
|
28
|
+
const shortTopicId = await getShortContentId(topicId);
|
|
26
29
|
const links: TopicPartLinks = {};
|
|
27
30
|
|
|
28
31
|
for (const topicPart of existingTopicParts)
|
|
29
|
-
links[topicPart] = createTopicPartLink(topicPart,
|
|
32
|
+
links[topicPart] = createTopicPartLink(topicPart, shortTopicId);
|
|
30
33
|
|
|
31
34
|
return links;
|
|
32
35
|
}
|
package/shared/aside/minor.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type AsideMinorType =
|
|
|
17
17
|
|
|
18
18
|
export interface AsideMinorTopic extends AsideMinorBase {
|
|
19
19
|
type: 'topic';
|
|
20
|
-
|
|
20
|
+
topicId: string;
|
|
21
21
|
location: BitranLocation;
|
|
22
22
|
nav: Partial<{
|
|
23
23
|
previous: PreviousNextItem;
|
|
@@ -33,7 +33,7 @@ export interface AsideMinorTopic extends AsideMinorBase {
|
|
|
33
33
|
|
|
34
34
|
export interface AsideMinorContent extends AsideMinorBase {
|
|
35
35
|
type: 'group' | 'book';
|
|
36
|
-
|
|
36
|
+
contentId: string;
|
|
37
37
|
nav: Partial<{
|
|
38
38
|
previous: PreviousNextItem;
|
|
39
39
|
next: PreviousNextItem;
|
|
@@ -1,60 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isContentType,
|
|
3
|
-
parseBitranLocation,
|
|
4
|
-
stringifyBitranLocation,
|
|
5
|
-
type BitranLocation,
|
|
6
|
-
} from '@erudit-js/cog/schema';
|
|
1
|
+
import { detectContentBookId } from '../content/bookId';
|
|
7
2
|
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
bookIds?: string[],
|
|
12
|
-
) {
|
|
13
|
-
const isStringLocation = typeof location === 'string';
|
|
14
|
-
const parsedLocation = isStringLocation
|
|
15
|
-
? parseBitranLocation(location)
|
|
16
|
-
: (location as BitranLocation);
|
|
17
|
-
|
|
18
|
-
if (isContentType(parsedLocation.type)) {
|
|
19
|
-
parsedLocation.path = toAbsoluteContentId(
|
|
20
|
-
parsedLocation.path!,
|
|
21
|
-
contextId,
|
|
22
|
-
bookIds,
|
|
23
|
-
);
|
|
24
|
-
return (
|
|
25
|
-
isStringLocation
|
|
26
|
-
? stringifyBitranLocation(parsedLocation)
|
|
27
|
-
: parsedLocation
|
|
28
|
-
) as T;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return location;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function toAbsoluteContentId(
|
|
35
|
-
contentId: string,
|
|
36
|
-
contextId: string,
|
|
3
|
+
export function toAbsoluteContentPath(
|
|
4
|
+
contentPath: string,
|
|
5
|
+
contextPath: string,
|
|
37
6
|
bookIds?: string[],
|
|
38
7
|
) {
|
|
39
8
|
const unresolvedPath = (() => {
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
9
|
+
if (contentPath.startsWith('/')) {
|
|
10
|
+
return contentPath;
|
|
42
11
|
}
|
|
43
12
|
|
|
44
|
-
if (
|
|
45
|
-
const restPath =
|
|
13
|
+
if (contentPath.startsWith('~/')) {
|
|
14
|
+
const restPath = contentPath.substring(2);
|
|
15
|
+
const bookId = detectContentBookId(contextPath, bookIds ?? []);
|
|
46
16
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return bookId + '/' + restPath;
|
|
50
|
-
}
|
|
17
|
+
if (bookId) {
|
|
18
|
+
return bookId + '/' + restPath;
|
|
51
19
|
}
|
|
52
20
|
|
|
53
21
|
// Not in any book, convert to absolute path
|
|
54
22
|
return '/' + restPath;
|
|
55
23
|
}
|
|
56
24
|
|
|
57
|
-
return
|
|
25
|
+
return contextPath + '/' + contentPath;
|
|
58
26
|
})();
|
|
59
27
|
|
|
60
28
|
return resolveContentPath(unresolvedPath);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function detectContentBookId(contentId: string, bookIds: string[]) {
|
|
2
|
+
let bestMatch: string | undefined = undefined;
|
|
3
|
+
for (const bookId of bookIds) {
|
|
4
|
+
if (contentId.startsWith(bookId)) {
|
|
5
|
+
if (!bestMatch || bookId.length > bestMatch.length) {
|
|
6
|
+
bestMatch = bookId;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return bestMatch;
|
|
12
|
+
}
|