erudit 3.0.0-dev.12 → 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/aside/major/panes/nav/Nav.vue +2 -2
- package/app/components/aside/major/panes/nav/NavBook.vue +19 -11
- 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/composables/bitranContent.ts +0 -1
- package/nuxt.config.ts +9 -0
- package/package.json +4 -4
- package/server/api/aside/major/nav/bookIds.ts +2 -2
- package/server/plugin/bitran/elements/include.ts +27 -20
- package/server/plugin/build/jobs/content/generic.ts +1 -7
- package/server/plugin/build/jobs/content/parse.ts +5 -1
- package/server/plugin/build/jobs/nav.ts +20 -4
- package/server/plugin/content/context.ts +2 -2
- package/server/plugin/db/setup.ts +0 -2
- package/server/plugin/nav/node.ts +2 -1
- package/server/plugin/nav/utils.ts +63 -21
- package/server/plugin/repository/content.ts +22 -11
- package/server/plugin/repository/contentId.ts +33 -19
- package/server/plugin/repository/frontNav.ts +2 -5
- package/shared/bitran/contentId.ts +9 -41
- package/shared/content/bookId.ts +1 -0
- package/test/contentId.test.ts +10 -10
- package/server/plugin/db/entities/ContentId.ts +0 -11
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { FrontNav } from '@shared/frontNav';
|
|
3
|
+
import { detectContentBookId } from '@shared/content/bookId';
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
getAsideMajorNavPayload,
|
|
5
7
|
insideNavBook,
|
|
@@ -7,8 +9,6 @@ import {
|
|
|
7
9
|
navBookVisible,
|
|
8
10
|
} from '@app/scripts/aside/major/nav';
|
|
9
11
|
|
|
10
|
-
import { detectContentBookId } from '@shared/content/bookId';
|
|
11
|
-
|
|
12
12
|
import NavGlobal from './NavGlobal.vue';
|
|
13
13
|
import NavBook from './NavBook.vue';
|
|
14
14
|
|
|
@@ -47,17 +47,19 @@ const pharse = await usePhrases('to_index', 'about_book');
|
|
|
47
47
|
</section>
|
|
48
48
|
<FNav :nav="book.children!" :contentId="contentRoute?.contentId">
|
|
49
49
|
<template v-slot:before>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
<section :class="$style.bookActions">
|
|
51
|
+
<TreeItem
|
|
52
|
+
icon="arrow-left"
|
|
53
|
+
:label="pharse.to_index"
|
|
54
|
+
@click="insideNavBook = false"
|
|
55
|
+
/>
|
|
56
|
+
<TreeItem
|
|
57
|
+
icon="book-question"
|
|
58
|
+
:label="pharse.about_book"
|
|
59
|
+
:active="contentRoute?.contentId === book.id"
|
|
60
|
+
:link="`/book/${book.id}`"
|
|
61
|
+
/>
|
|
62
|
+
</section>
|
|
61
63
|
</template>
|
|
62
64
|
</FNav>
|
|
63
65
|
</PaneContentScroll>
|
|
@@ -84,4 +86,10 @@ const pharse = await usePhrases('to_index', 'about_book');
|
|
|
84
86
|
flex-shrink: 0;
|
|
85
87
|
}
|
|
86
88
|
}
|
|
89
|
+
|
|
90
|
+
.bookActions {
|
|
91
|
+
border-bottom: 1px solid var(--border);
|
|
92
|
+
padding-bottom: calc(var(--gap)/2);
|
|
93
|
+
margin-bottom: calc(var(--gap)/2);
|
|
94
|
+
}
|
|
87
95
|
</style>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import type { ContentReferenceItem } from '@
|
|
2
|
+
import type { ContentReferenceItem } from '@erudit-js/cog/schema';
|
|
3
3
|
|
|
4
4
|
defineProps<{ reference: ContentReferenceItem }>();
|
|
5
|
+
|
|
6
|
+
const pretty = useFormatText();
|
|
5
7
|
</script>
|
|
6
8
|
|
|
7
9
|
<template>
|
|
@@ -10,7 +12,7 @@ defineProps<{ reference: ContentReferenceItem }>();
|
|
|
10
12
|
<div :class="$style.body">
|
|
11
13
|
<div :class="$style.header">
|
|
12
14
|
<a :href="reference.link" target="_blank">{{
|
|
13
|
-
reference.title
|
|
15
|
+
pretty(reference.title)
|
|
14
16
|
}}</a>
|
|
15
17
|
<MyIcon
|
|
16
18
|
v-if="reference.link"
|
|
@@ -19,7 +21,7 @@ defineProps<{ reference: ContentReferenceItem }>();
|
|
|
19
21
|
/>
|
|
20
22
|
</div>
|
|
21
23
|
<div v-if="reference.description" :class="$style.description">
|
|
22
|
-
{{ reference.description }}
|
|
24
|
+
{{ pretty(reference.description) }}
|
|
23
25
|
</div>
|
|
24
26
|
</div>
|
|
25
27
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import type { ContentReferenceSource } from '@erudit-js/cog/schema';
|
|
3
|
+
|
|
2
4
|
import { type MyIconName } from '#my-icons';
|
|
3
|
-
import type { ContentReferenceSource } from '@package';
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{ source: ContentReferenceSource }>();
|
|
6
7
|
|
|
@@ -15,6 +16,7 @@ const typeIcon = computed<MyIconName>(() => {
|
|
|
15
16
|
}
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
const pretty = useFormatText();
|
|
18
20
|
const phrase = await usePhrases('reference_source_featured');
|
|
19
21
|
</script>
|
|
20
22
|
|
|
@@ -26,7 +28,7 @@ const phrase = await usePhrases('reference_source_featured');
|
|
|
26
28
|
<div :class="$style.body">
|
|
27
29
|
<div :class="$style.header">
|
|
28
30
|
<MyIcon :name="typeIcon" :class="$style.headerTypeIcon" />
|
|
29
|
-
<a :href="source.link" target="_blank">{{ source.title }}</a>
|
|
31
|
+
<a :href="source.link" target="_blank">{{ pretty(source.title) }}</a>
|
|
30
32
|
<MyIcon
|
|
31
33
|
v-if="source.featured"
|
|
32
34
|
name="star"
|
|
@@ -40,10 +42,10 @@ const phrase = await usePhrases('reference_source_featured');
|
|
|
40
42
|
/>
|
|
41
43
|
</div>
|
|
42
44
|
<div v-if="source.description" :class="$style.description">
|
|
43
|
-
{{ source.description }}
|
|
45
|
+
{{ pretty(source.description) }}
|
|
44
46
|
</div>
|
|
45
47
|
<div v-if="source.comment" :class="$style.comment">
|
|
46
|
-
{{ source.comment }}
|
|
48
|
+
{{ pretty(source.comment) }}
|
|
47
49
|
</div>
|
|
48
50
|
</div>
|
|
49
51
|
</div>
|
|
@@ -30,7 +30,6 @@ export async function useBitranContent(
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const contentApiRoute = `/api/bitran/content/${encodeBitranLocation(stringifyBitranLocation(location.value!))}`;
|
|
33
|
-
nuxtApp.runWithContext(() => prerenderRoutes(contentApiRoute));
|
|
34
33
|
|
|
35
34
|
// @ts-ignore
|
|
36
35
|
contentPromise = (async () => {
|
package/nuxt.config.ts
CHANGED
|
@@ -57,7 +57,16 @@ export default defineNuxtConfig({
|
|
|
57
57
|
preset: 'github-pages',
|
|
58
58
|
plugins: [eruditPath('server/plugin')],
|
|
59
59
|
prerender: {
|
|
60
|
+
crawlLinks: true,
|
|
60
61
|
failOnError: false,
|
|
62
|
+
concurrency: 10,
|
|
63
|
+
ignore: [
|
|
64
|
+
(path: string) => {
|
|
65
|
+
const regexps = [/\?element=/gm, /#/gm, /\.json\?/gm];
|
|
66
|
+
const shouldIgnore = regexps.some((re) => re.test(path));
|
|
67
|
+
return shouldIgnore;
|
|
68
|
+
},
|
|
69
|
+
],
|
|
61
70
|
},
|
|
62
71
|
output: {
|
|
63
72
|
publicDir: projectPath('dist'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erudit",
|
|
3
|
-
"version": "3.0.0-dev.
|
|
3
|
+
"version": "3.0.0-dev.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "🤓 CMS for perfect educational sites.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"erudit": "bin/erudit.mjs"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@erudit-js/cog": "3.0.0-dev.
|
|
19
|
-
"@erudit-js/cli": "3.0.0-dev.
|
|
20
|
-
"@erudit-js/bitran-elements": "3.0.0-dev.
|
|
18
|
+
"@erudit-js/cog": "3.0.0-dev.13",
|
|
19
|
+
"@erudit-js/cli": "3.0.0-dev.13",
|
|
20
|
+
"@erudit-js/bitran-elements": "3.0.0-dev.13"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@bitran-js/core": "1.0.0-dev.13",
|
|
@@ -14,9 +14,10 @@ import { AliasesNode } from '@erudit-js/bitran-elements/aliases/shared';
|
|
|
14
14
|
import { createBitranTranspiler } from '@server/bitran/transpiler';
|
|
15
15
|
import { ERUDIT_SERVER } from '@server/global';
|
|
16
16
|
import { DbUnique } from '@server/db/entities/Unique';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import {
|
|
18
|
+
resolveClientContentId,
|
|
19
|
+
serverAbsolutizeContentPath,
|
|
20
|
+
} from '@server/repository/contentId';
|
|
20
21
|
|
|
21
22
|
export type TraverseEnterFn = (payload: {
|
|
22
23
|
_location: string;
|
|
@@ -41,16 +42,19 @@ export async function traverseInclude(
|
|
|
41
42
|
leave?: TraverseLeaveFn;
|
|
42
43
|
},
|
|
43
44
|
): Promise<BlockNode[]> {
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const rawLocation = parsePartialBitranLocation(
|
|
46
|
+
includeNode.id,
|
|
47
|
+
context.location,
|
|
46
48
|
);
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
if (rawLocation.path) {
|
|
51
|
+
rawLocation.path = serverAbsolutizeContentPath(
|
|
52
|
+
rawLocation.path,
|
|
53
|
+
context.location.path!,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const absEntryLocation = stringifyBitranLocation(rawLocation);
|
|
54
58
|
|
|
55
59
|
const travelMap: Record<string, string | null> = {
|
|
56
60
|
[absEntryLocation]: null,
|
|
@@ -94,18 +98,21 @@ async function _traverseInclude(
|
|
|
94
98
|
try {
|
|
95
99
|
const parsedLocation = parseBitranLocation(location);
|
|
96
100
|
|
|
97
|
-
includeTargetLocation =
|
|
98
|
-
parsePartialBitranLocation(
|
|
101
|
+
includeTargetLocation = (() => {
|
|
102
|
+
const _location = parsePartialBitranLocation(
|
|
99
103
|
tryReplaceAlias(includeNode.parseData.location, aliases),
|
|
100
104
|
parsedLocation,
|
|
101
|
-
)
|
|
102
|
-
);
|
|
105
|
+
);
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if (_location.path) {
|
|
108
|
+
_location.path = serverAbsolutizeContentPath(
|
|
109
|
+
_location.path,
|
|
110
|
+
parsedLocation.path!,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return stringifyBitranLocation(_location);
|
|
115
|
+
})();
|
|
109
116
|
} catch (error) {
|
|
110
117
|
travelMap[location] = includeNode.parseData.location;
|
|
111
118
|
throw new Error(
|
|
@@ -23,7 +23,6 @@ import { IMPORT } from '@server/importer';
|
|
|
23
23
|
import { contributorExists } from '@server/repository/contributor';
|
|
24
24
|
import { DbContribution } from '@server/db/entities/Contribution';
|
|
25
25
|
import { DbFile } from '@server/db/entities/File';
|
|
26
|
-
import { DbContentId } from '@server/db/entities/ContentId';
|
|
27
26
|
|
|
28
27
|
import { contentAsset } from '@erudit/shared/asset';
|
|
29
28
|
import type { ImageData } from '@erudit/shared/image';
|
|
@@ -81,14 +80,9 @@ async function scanContentFiles() {
|
|
|
81
80
|
|
|
82
81
|
async function addContentItem(navNode: NavNode) {
|
|
83
82
|
debug.start(
|
|
84
|
-
`Adding ${stress(navNode.type)} content item ${stress(navNode.
|
|
83
|
+
`Adding ${stress(navNode.type)} content item ${stress(navNode.shortId)}...`,
|
|
85
84
|
);
|
|
86
85
|
|
|
87
|
-
const dbContentId = new DbContentId();
|
|
88
|
-
dbContentId.fullId = navNode.fullId;
|
|
89
|
-
dbContentId.shortId = navNode.id;
|
|
90
|
-
await ERUDIT_SERVER.DB.manager.save(dbContentId);
|
|
91
|
-
|
|
92
86
|
const dbContent = new DbContent();
|
|
93
87
|
dbContent.contentId = navNode.fullId;
|
|
94
88
|
dbContent.type = navNode.type;
|
|
@@ -29,7 +29,11 @@ export async function parseBitranContent(
|
|
|
29
29
|
context.location = location;
|
|
30
30
|
context.aliases = NO_ALIASES();
|
|
31
31
|
|
|
32
|
-
bitranTranspiler ||= await createBitranTranspiler(
|
|
32
|
+
bitranTranspiler ||= await createBitranTranspiler({
|
|
33
|
+
context,
|
|
34
|
+
eruditConfig: ERUDIT_SERVER.CONFIG,
|
|
35
|
+
insideInclude: false,
|
|
36
|
+
});
|
|
33
37
|
|
|
34
38
|
// Tracking heading nodes to deal with them later
|
|
35
39
|
const headings: HeadingNode[] = [];
|
|
@@ -93,8 +93,8 @@ async function scanChildNodes(
|
|
|
93
93
|
const parentId = isRootNode(parent)
|
|
94
94
|
? ''
|
|
95
95
|
: parent.skip
|
|
96
|
-
? parent.
|
|
97
|
-
: parent.
|
|
96
|
+
? parent.fullId.split('/').slice(0, -1).join('/')
|
|
97
|
+
: parent.fullId;
|
|
98
98
|
|
|
99
99
|
// Regular id might not include parent id part if it is skipped
|
|
100
100
|
const id = parentId ? `${parentId}/${pathParts.id}` : pathParts.id;
|
|
@@ -114,14 +114,30 @@ async function scanChildNodes(
|
|
|
114
114
|
? pathParts.id
|
|
115
115
|
: `${parent.fullId}/${pathParts.id}`;
|
|
116
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
|
+
|
|
117
132
|
const skip = pathParts.sep === '+';
|
|
118
133
|
|
|
119
134
|
const childNode: NavNode = {
|
|
120
135
|
parent,
|
|
121
136
|
type: pathParts.type,
|
|
122
137
|
path: nodePath,
|
|
123
|
-
id,
|
|
138
|
+
idPart: pathParts.id,
|
|
124
139
|
fullId,
|
|
140
|
+
shortId,
|
|
125
141
|
skip,
|
|
126
142
|
};
|
|
127
143
|
|
|
@@ -200,7 +216,7 @@ function debugPrintNav(node: RootNavNode) {
|
|
|
200
216
|
console.log(
|
|
201
217
|
isRootNode(node)
|
|
202
218
|
? chalk.dim('#root')
|
|
203
|
-
: `${' '.repeat(indent)}${node.
|
|
219
|
+
: `${' '.repeat(indent)}${node.idPart} ${chalk.dim(`[${node.type}${node.skip ? ', ' + chalk.yellow('skip') : ''}]`)}`,
|
|
204
220
|
);
|
|
205
221
|
|
|
206
222
|
if (node.children)
|
|
@@ -20,11 +20,11 @@ import {
|
|
|
20
20
|
import { CONTENT_TYPE_ICON, ICON, TOPIC_PART_ICON } from '@erudit/shared/icons';
|
|
21
21
|
|
|
22
22
|
export async function getContentContext(contentId: string): Promise<Context> {
|
|
23
|
-
contentId =
|
|
23
|
+
contentId = getFullContentId(contentId);
|
|
24
24
|
|
|
25
25
|
const context: Context = [];
|
|
26
26
|
|
|
27
|
-
for (const _contentId of
|
|
27
|
+
for (const _contentId of getIdsUp(contentId).reverse()) {
|
|
28
28
|
const dbContent = await getDbContent(_contentId);
|
|
29
29
|
|
|
30
30
|
context.push({
|
|
@@ -15,7 +15,6 @@ import { DbHash } from './entities/Hash';
|
|
|
15
15
|
import { DbTopic } from './entities/Topic';
|
|
16
16
|
import { DbUnique } from './entities/Unique';
|
|
17
17
|
import { DbFile } from './entities/File';
|
|
18
|
-
import { DbContentId } from './entities/ContentId';
|
|
19
18
|
|
|
20
19
|
export async function setupDatabase() {
|
|
21
20
|
rmSync(PROJECT_DIR + '/.erudit/data.sqlite', { force: true });
|
|
@@ -28,7 +27,6 @@ export async function setupDatabase() {
|
|
|
28
27
|
dropSchema: true,
|
|
29
28
|
entities: [
|
|
30
29
|
DbBook,
|
|
31
|
-
DbContentId,
|
|
32
30
|
DbContent,
|
|
33
31
|
DbContribution,
|
|
34
32
|
DbContributor,
|
|
@@ -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
|
|
|
@@ -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
|
-
while (currentNavNode?.id) {
|
|
156
|
+
while (currentNavNode?.idPart) {
|
|
115
157
|
ids.push(currentNavNode.fullId);
|
|
116
|
-
currentNavNode = currentNavNode.parent;
|
|
158
|
+
currentNavNode = currentNavNode.parent as NavNode;
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
return ids;
|
|
@@ -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,
|
|
@@ -64,10 +67,10 @@ export async function getPreviousNext(contentId: string) {
|
|
|
64
67
|
if (navNode.type === 'topic')
|
|
65
68
|
return createTopicPartLink(
|
|
66
69
|
await getTopicPart(navNode.fullId),
|
|
67
|
-
navNode.
|
|
70
|
+
navNode.shortId,
|
|
68
71
|
);
|
|
69
72
|
|
|
70
|
-
return createContentLink(navNode.type, navNode.
|
|
73
|
+
return createContentLink(navNode.type, navNode.shortId);
|
|
71
74
|
})();
|
|
72
75
|
|
|
73
76
|
return {
|
|
@@ -126,9 +129,13 @@ export async function getContentDependencies(
|
|
|
126
129
|
contentId: string,
|
|
127
130
|
strDependencies: string[],
|
|
128
131
|
) {
|
|
129
|
-
const dependencyIds =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
const dependencyIds: string[] = [];
|
|
133
|
+
for (const rawDependency of strDependencies) {
|
|
134
|
+
dependencyIds.push(
|
|
135
|
+
resolveClientContentId(rawDependency, contentId, 'full'),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
132
139
|
const dependencies: Record<string, string> = {};
|
|
133
140
|
|
|
134
141
|
for (const dependencyId of dependencyIds)
|
|
@@ -153,18 +160,22 @@ export async function getContentLink(contentId: string) {
|
|
|
153
160
|
where: { contentId },
|
|
154
161
|
});
|
|
155
162
|
|
|
156
|
-
if (!dbContent)
|
|
163
|
+
if (!dbContent) {
|
|
157
164
|
throw createError({
|
|
158
165
|
statusCode: 404,
|
|
159
166
|
statusText: `Missing "${contentId}" content item!`,
|
|
160
167
|
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const shortContentId = getShortContentId(contentId);
|
|
161
171
|
|
|
162
|
-
if (dbContent.type !== 'topic')
|
|
163
|
-
return createContentLink(dbContent.type,
|
|
172
|
+
if (dbContent.type !== 'topic') {
|
|
173
|
+
return createContentLink(dbContent.type, shortContentId);
|
|
174
|
+
}
|
|
164
175
|
|
|
165
176
|
const topicPart = await getTopicPart(contentId);
|
|
166
177
|
|
|
167
|
-
return createTopicPartLink(topicPart,
|
|
178
|
+
return createTopicPartLink(topicPart, shortContentId);
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
export async function getContentContributors(contentId: string) {
|
|
@@ -1,26 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getNavBookIds, getNavNode } from '@server/nav/utils';
|
|
2
|
+
import { toAbsoluteContentPath } from '@shared/bitran/contentId';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (!dbContentId) {
|
|
10
|
-
throw new Error(
|
|
11
|
-
`Can't find both short or full content id: ${contentId}!`,
|
|
12
|
-
);
|
|
13
|
-
}
|
|
4
|
+
export function getFullContentId(mixedContentId: string): string {
|
|
5
|
+
const navNode = getNavNode(mixedContentId);
|
|
6
|
+
return navNode.fullId;
|
|
7
|
+
}
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
export function getShortContentId(mixedContentId: string): string {
|
|
10
|
+
const navNode = getNavNode(mixedContentId);
|
|
11
|
+
return navNode.shortId;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
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);
|
|
26
40
|
}
|
|
@@ -125,14 +125,11 @@ async function toFrontNavBase({
|
|
|
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
|
|
|
@@ -1,50 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isContentType,
|
|
3
|
-
parseBitranLocation,
|
|
4
|
-
stringifyBitranLocation,
|
|
5
|
-
type BitranLocation,
|
|
6
|
-
} from '@erudit-js/cog/schema';
|
|
7
1
|
import { detectContentBookId } from '../content/bookId';
|
|
8
2
|
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
bookIds?: string[],
|
|
13
|
-
) {
|
|
14
|
-
const isStringLocation = typeof location === 'string';
|
|
15
|
-
const parsedLocation = isStringLocation
|
|
16
|
-
? parseBitranLocation(location)
|
|
17
|
-
: (location as BitranLocation);
|
|
18
|
-
|
|
19
|
-
if (isContentType(parsedLocation.type)) {
|
|
20
|
-
parsedLocation.path = toAbsoluteContentId(
|
|
21
|
-
parsedLocation.path!,
|
|
22
|
-
contextId,
|
|
23
|
-
bookIds,
|
|
24
|
-
);
|
|
25
|
-
return (
|
|
26
|
-
isStringLocation
|
|
27
|
-
? stringifyBitranLocation(parsedLocation)
|
|
28
|
-
: parsedLocation
|
|
29
|
-
) as T;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return location;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function toAbsoluteContentId(
|
|
36
|
-
contentId: string,
|
|
37
|
-
contextId: string,
|
|
3
|
+
export function toAbsoluteContentPath(
|
|
4
|
+
contentPath: string,
|
|
5
|
+
contextPath: string,
|
|
38
6
|
bookIds?: string[],
|
|
39
7
|
) {
|
|
40
8
|
const unresolvedPath = (() => {
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
9
|
+
if (contentPath.startsWith('/')) {
|
|
10
|
+
return contentPath;
|
|
43
11
|
}
|
|
44
12
|
|
|
45
|
-
if (
|
|
46
|
-
const restPath =
|
|
47
|
-
const bookId = detectContentBookId(
|
|
13
|
+
if (contentPath.startsWith('~/')) {
|
|
14
|
+
const restPath = contentPath.substring(2);
|
|
15
|
+
const bookId = detectContentBookId(contextPath, bookIds ?? []);
|
|
48
16
|
|
|
49
17
|
if (bookId) {
|
|
50
18
|
return bookId + '/' + restPath;
|
|
@@ -54,7 +22,7 @@ export function toAbsoluteContentId(
|
|
|
54
22
|
return '/' + restPath;
|
|
55
23
|
}
|
|
56
24
|
|
|
57
|
-
return
|
|
25
|
+
return contextPath + '/' + contentPath;
|
|
58
26
|
})();
|
|
59
27
|
|
|
60
28
|
return resolveContentPath(unresolvedPath);
|
package/shared/content/bookId.ts
CHANGED
package/test/contentId.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
toAbsoluteContentPath,
|
|
3
3
|
resolveContentPath,
|
|
4
4
|
} from '@erudit/shared/bitran/contentId';
|
|
5
5
|
|
|
@@ -54,23 +54,23 @@ describe('resolveContentPath', () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
describe('
|
|
57
|
+
describe('toAbsoluteContentPath', () => {
|
|
58
58
|
const bookIds = ['combinatorics', 'group/book'];
|
|
59
59
|
|
|
60
60
|
it('should not use context on absolute paths', () => {
|
|
61
|
-
expect(
|
|
62
|
-
'foo/baz',
|
|
63
|
-
);
|
|
61
|
+
expect(
|
|
62
|
+
toAbsoluteContentPath('/foo/bar/../baz', 'qux/vaz', bookIds),
|
|
63
|
+
).toBe('foo/baz');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it('should fallback to absolute path if not inside book', () => {
|
|
67
|
-
expect(
|
|
68
|
-
'foo/bar',
|
|
69
|
-
);
|
|
67
|
+
expect(
|
|
68
|
+
toAbsoluteContentPath('~/foo/bar', 'unknown-book', bookIds),
|
|
69
|
+
).toBe('foo/bar');
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it('should correctly handle book-relative paths', () => {
|
|
73
|
-
expect(
|
|
73
|
+
expect(toAbsoluteContentPath('~/foo/bar', 'group/book', bookIds)).toBe(
|
|
74
74
|
'group/book/foo/bar',
|
|
75
75
|
);
|
|
76
76
|
});
|
|
@@ -83,7 +83,7 @@ describe('toAbsoluteContentId', () => {
|
|
|
83
83
|
];
|
|
84
84
|
|
|
85
85
|
for (const [contentId, context, expected] of testCases) {
|
|
86
|
-
expect(
|
|
86
|
+
expect(toAbsoluteContentPath(contentId!, context!, bookIds)).toBe(
|
|
87
87
|
expected,
|
|
88
88
|
);
|
|
89
89
|
}
|