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.
Files changed (62) hide show
  1. package/app/components/SiteMain.vue +1 -1
  2. package/app/components/aside/major/panes/nav/Nav.vue +11 -6
  3. package/app/components/aside/major/panes/nav/NavBook.vue +19 -11
  4. package/app/components/aside/major/panes/nav/fnav/FNavFlags.vue +1 -1
  5. package/app/components/aside/minor/Contribute.vue +2 -2
  6. package/app/components/aside/minor/content/AsideMinorContent.vue +1 -1
  7. package/app/components/aside/minor/topic/AsideMinorTopic.vue +1 -1
  8. package/app/components/aside/minor/topic/TopicContributors.vue +2 -2
  9. package/app/components/aside/minor/topic/TopicToc.vue +1 -6
  10. package/app/components/aside/minor/topic/TopicTocItem.vue +3 -1
  11. package/app/components/main/utils/Breadcrumb.vue +1 -1
  12. package/app/components/main/utils/ContentReferences.vue +1 -1
  13. package/app/components/main/utils/reference/ReferenceGroup.vue +1 -1
  14. package/app/components/main/utils/reference/ReferenceItem.vue +5 -3
  15. package/app/components/main/utils/reference/ReferenceSource.vue +6 -4
  16. package/app/components/preview/PreviewScreen.vue +2 -2
  17. package/app/components/preview/display/PageLink.vue +1 -1
  18. package/app/composables/bitranContent.ts +2 -3
  19. package/app/composables/contentPage.ts +8 -7
  20. package/app/composables/formatText.ts +21 -8
  21. package/app/composables/phrases.ts +0 -16
  22. package/app/plugins/prerender.server.ts +22 -0
  23. package/app/scripts/preview/build.ts +1 -5
  24. package/app/scripts/preview/data/pageLink.ts +1 -0
  25. package/app/styles/normalize.scss +0 -14
  26. package/globals/content.ts +5 -0
  27. package/module/imports.ts +1 -0
  28. package/nuxt.config.ts +10 -5
  29. package/package.json +8 -8
  30. package/server/api/aside/major/nav/bookIds.ts +2 -2
  31. package/server/api/aside/minor/path.ts +19 -11
  32. package/server/api/bitran/content/[...location].ts +4 -2
  33. package/server/api/bitran/toc/[...location].ts +4 -2
  34. package/server/api/content/data.ts +5 -2
  35. package/server/api/prerender.ts +120 -0
  36. package/server/api/preview/page/[...parts].ts +30 -4
  37. package/server/api/preview/unique/{[location].ts → [...location].ts} +4 -3
  38. package/server/plugin/bitran/content.ts +3 -16
  39. package/server/plugin/bitran/elements/include.ts +40 -36
  40. package/server/plugin/bitran/location.ts +24 -10
  41. package/server/plugin/bitran/toc.ts +7 -2
  42. package/server/plugin/bitran/transpiler.ts +10 -1
  43. package/server/plugin/build/jobs/content/generic.ts +5 -6
  44. package/server/plugin/build/jobs/content/parse.ts +5 -1
  45. package/server/plugin/build/jobs/content/type/group.ts +2 -2
  46. package/server/plugin/build/jobs/content/type/topic.ts +2 -2
  47. package/server/plugin/build/jobs/nav.ts +28 -15
  48. package/server/plugin/content/context.ts +5 -2
  49. package/server/plugin/db/entities/Content.ts +0 -4
  50. package/server/plugin/db/setup.ts +1 -1
  51. package/server/plugin/importer.ts +5 -1
  52. package/server/plugin/nav/node.ts +2 -1
  53. package/server/plugin/nav/utils.ts +66 -24
  54. package/server/plugin/repository/content.ts +26 -23
  55. package/server/plugin/repository/contentId.ts +40 -0
  56. package/server/plugin/repository/frontNav.ts +6 -9
  57. package/server/plugin/repository/topic.ts +4 -1
  58. package/shared/aside/minor.ts +2 -2
  59. package/shared/bitran/contentId.ts +12 -44
  60. package/shared/content/bookId.ts +12 -0
  61. package/shared/frontNav.ts +1 -1
  62. 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.id;
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: navNode.id,
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.id;
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: navNode.id,
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
- `${PROJECT_DIR}/content/${currentFsPath}/*/{${contentTypes.join(',')}}.{ts,js}`,
61
- )
62
- .sort()
63
- .map((path) => resolvePaths(path));
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.id.split('/').slice(0, -1).join('/')
100
- : parent.id;
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.id.split('/').pop()} ${chalk.dim(`[${node.type}${node.skip ? ', ' + chalk.yellow('skip') : ''}]`)}`,
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 (await getIdsUp(contentId)).reverse()) {
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', 'fullId', 'contentId'],
112
+ select: ['type', 'title', 'contentId'],
110
113
  where: { contentId },
111
114
  });
112
115
 
@@ -13,10 +13,6 @@ export class DbContent {
13
13
  @PrimaryColumn('varchar')
14
14
  contentId!: string;
15
15
 
16
- @Column('varchar')
17
- @Index({ unique: true })
18
- fullId!: string;
19
-
20
16
  @Column('varchar')
21
17
  type!: ContentType;
22
18
 
@@ -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>) {
@@ -3,7 +3,8 @@ import type { ContentType } from '@erudit-js/cog/schema';
3
3
  export interface NavNode {
4
4
  type: ContentType;
5
5
  path: string;
6
- id: string;
6
+ idPart: string;
7
+ shortId: string;
7
8
  fullId: string;
8
9
  skip: boolean;
9
10
  children?: NavNode[];
@@ -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
- return Object.keys(ERUDIT_SERVER.NAV_BOOKS || {});
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.id;
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.id === contentId) {
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
- export async function getNavNode(
92
- contentId: string,
93
- ): Promise<NavNode | undefined> {
94
- let navNode: NavNode | undefined;
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
- await walkNav(async (_navNode) => {
97
- if (_navNode.id === contentId) {
98
- navNode = _navNode;
99
- return false;
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
- return navNode;
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
- export async function getIdsUp(contentId: string): Promise<string[]> {
107
- const startNavNode = await getNavNode(contentId);
139
+ if (!foundNode) {
140
+ throw new Error(
141
+ `Failed to find navigation content node for ID: ${mixedContentId}`,
142
+ );
143
+ }
108
144
 
109
- if (!startNavNode) return [];
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
- let currentNavNode: any = startNavNode;
114
- while (currentNavNode?.id) {
115
- ids.push(currentNavNode.id);
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.id === contentId) {
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, getNavBookIds, getPreviousNextNav } from '@server/nav/utils';
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.id);
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.id),
66
- navNode.id,
69
+ await getTopicPart(navNode.fullId),
70
+ navNode.shortId,
67
71
  );
68
72
 
69
- return createContentLink(navNode.type, navNode.id);
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 = strDependencies.map((rawDependency) =>
129
- toAbsoluteContentId(rawDependency, contentId, getNavBookIds()),
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
- if (dbContent.type !== 'topic')
162
- return createContentLink(dbContent.type, contentId);
163
-
164
- const topicPart = await getTopicPart(contentId);
170
+ const shortContentId = getShortContentId(contentId);
165
171
 
166
- return createTopicPartLink(topicPart, contentId);
167
- }
172
+ if (dbContent.type !== 'topic') {
173
+ return createContentLink(dbContent.type, shortContentId);
174
+ }
168
175
 
169
- export async function getFullContentId(contentId: string) {
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 dbContent?.fullId;
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.id },
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.id}" when creating front nav!`,
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.id);
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.id },
124
+ where: { contentId: node.fullId },
125
125
  });
126
126
 
127
127
  return {
128
- id: node.id,
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, topicId);
32
+ links[topicPart] = createTopicPartLink(topicPart, shortTopicId);
30
33
 
31
34
  return links;
32
35
  }
@@ -17,7 +17,7 @@ export type AsideMinorType =
17
17
 
18
18
  export interface AsideMinorTopic extends AsideMinorBase {
19
19
  type: 'topic';
20
- fullContentId: string;
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
- fullContentId: string;
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 toAbsoluteLocation<T extends string | BitranLocation>(
9
- location: T,
10
- contextId: string,
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 (contentId.startsWith('/')) {
41
- return contentId;
9
+ if (contentPath.startsWith('/')) {
10
+ return contentPath;
42
11
  }
43
12
 
44
- if (contentId.startsWith('~/')) {
45
- const restPath = contentId.substring(2);
13
+ if (contentPath.startsWith('~/')) {
14
+ const restPath = contentPath.substring(2);
15
+ const bookId = detectContentBookId(contextPath, bookIds ?? []);
46
16
 
47
- for (const bookId of bookIds ?? []) {
48
- if (contextId.startsWith(bookId)) {
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 contextId + '/' + contentId;
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
+ }