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.
@@ -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
- <TreeItem
51
- icon="arrow-left"
52
- :label="pharse.to_index"
53
- @click="insideNavBook = false"
54
- />
55
- <TreeItem
56
- icon="book-question"
57
- :label="pharse.about_book"
58
- :active="contentRoute?.contentId === book.id"
59
- :link="`/book/${book.id}`"
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,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import type { ContentReferences } from '@package';
2
+ import type { ContentReferences } from '@erudit-js/cog/schema';
3
3
 
4
4
  import ReferenceGroup from './reference/ReferenceGroup.vue';
5
5
 
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import type { ContentReferenceGroup } from '@package';
2
+ import type { ContentReferenceGroup } from '@erudit-js/cog/schema';
3
3
 
4
4
  import ReferenceSource from './ReferenceSource.vue';
5
5
  import ReferenceItem from './ReferenceItem.vue';
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" setup>
2
- import type { ContentReferenceItem } from '@package';
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.12",
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.12",
19
- "@erudit-js/cli": "3.0.0-dev.12",
20
- "@erudit-js/bitran-elements": "3.0.0-dev.12"
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",
@@ -1,5 +1,5 @@
1
- import { ERUDIT_SERVER } from '@server/global';
1
+ import { getNavBookIds } from '@server/nav/utils';
2
2
 
3
3
  export default defineEventHandler(() => {
4
- return Object.keys(ERUDIT_SERVER.NAV_BOOKS || {});
4
+ return getNavBookIds('short');
5
5
  });
@@ -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 { getNavBookIds } from '@server/nav/utils';
18
-
19
- import { toAbsoluteLocation } from '@shared/bitran/contentId';
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 entryLocation = stringifyBitranLocation(
45
- parsePartialBitranLocation(includeNode.id, context.location),
45
+ const rawLocation = parsePartialBitranLocation(
46
+ includeNode.id,
47
+ context.location,
46
48
  );
47
49
 
48
- // Always use absolute locations as keys for travelMap to avoid infinite loop bugs
49
- const absEntryLocation = toAbsoluteLocation(
50
- entryLocation,
51
- context.location.path!,
52
- getNavBookIds(),
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 = stringifyBitranLocation(
98
- parsePartialBitranLocation(
101
+ includeTargetLocation = (() => {
102
+ const _location = parsePartialBitranLocation(
99
103
  tryReplaceAlias(includeNode.parseData.location, aliases),
100
104
  parsedLocation,
101
- ),
102
- );
105
+ );
103
106
 
104
- includeTargetLocation = toAbsoluteLocation(
105
- includeTargetLocation,
106
- parsedLocation.path!,
107
- getNavBookIds(),
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.id)}...`,
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.id.split('/').slice(0, -1).join('/')
97
- : parent.id;
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.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') : ''}]`)}`,
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 = await getFullContentId(contentId);
23
+ contentId = getFullContentId(contentId);
24
24
 
25
25
  const context: Context = [];
26
26
 
27
- for (const _contentId of (await getIdsUp(contentId)).reverse()) {
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,
@@ -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
 
@@ -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.fullId === 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) {
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, 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,
@@ -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.id,
70
+ navNode.shortId,
68
71
  );
69
72
 
70
- return createContentLink(navNode.type, navNode.id);
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 = strDependencies.map((rawDependency) =>
130
- toAbsoluteContentId(rawDependency, contentId, getNavBookIds()),
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, contentId);
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, contentId);
178
+ return createTopicPartLink(topicPart, shortContentId);
168
179
  }
169
180
 
170
181
  export async function getContentContributors(contentId: string) {
@@ -1,26 +1,40 @@
1
- import { DbContentId } from '@server/db/entities/ContentId';
2
- import { ERUDIT_SERVER } from '@server/global';
1
+ import { getNavBookIds, getNavNode } from '@server/nav/utils';
2
+ import { toAbsoluteContentPath } from '@shared/bitran/contentId';
3
3
 
4
- async function findContentId(contentId: string): Promise<DbContentId> {
5
- const dbContentId = await ERUDIT_SERVER.DB.manager.findOne(DbContentId, {
6
- where: [{ shortId: contentId }, { fullId: contentId }],
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
- return dbContentId;
9
+ export function getShortContentId(mixedContentId: string): string {
10
+ const navNode = getNavNode(mixedContentId);
11
+ return navNode.shortId;
16
12
  }
17
13
 
18
- export async function getFullContentId(maybeShortId: string): Promise<string> {
19
- const dbContentId = await findContentId(maybeShortId);
20
- return dbContentId.fullId;
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 async function getShortContentId(maybeFullId: string): Promise<string> {
24
- const dbContentId = await findContentId(maybeFullId);
25
- return dbContentId.shortId;
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.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
 
@@ -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 toAbsoluteLocation<T extends string | BitranLocation>(
10
- location: T,
11
- contextId: string,
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 (contentId.startsWith('/')) {
42
- return contentId;
9
+ if (contentPath.startsWith('/')) {
10
+ return contentPath;
43
11
  }
44
12
 
45
- if (contentId.startsWith('~/')) {
46
- const restPath = contentId.substring(2);
47
- const bookId = detectContentBookId(contextId, bookIds ?? []);
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 contextId + '/' + contentId;
25
+ return contextPath + '/' + contentPath;
58
26
  })();
59
27
 
60
28
  return resolveContentPath(unresolvedPath);
@@ -7,5 +7,6 @@ export function detectContentBookId(contentId: string, bookIds: string[]) {
7
7
  }
8
8
  }
9
9
  }
10
+
10
11
  return bestMatch;
11
12
  }
@@ -1,5 +1,5 @@
1
1
  import {
2
- toAbsoluteContentId,
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('toAbsoluteContentId', () => {
57
+ describe('toAbsoluteContentPath', () => {
58
58
  const bookIds = ['combinatorics', 'group/book'];
59
59
 
60
60
  it('should not use context on absolute paths', () => {
61
- expect(toAbsoluteContentId('/foo/bar/../baz', 'qux/vaz', bookIds)).toBe(
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(toAbsoluteContentId('~/foo/bar', 'unknown-book', bookIds)).toBe(
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(toAbsoluteContentId('~/foo/bar', 'group/book', bookIds)).toBe(
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(toAbsoluteContentId(contentId!, context!, bookIds)).toBe(
86
+ expect(toAbsoluteContentPath(contentId!, context!, bookIds)).toBe(
87
87
  expected,
88
88
  );
89
89
  }
@@ -1,11 +0,0 @@
1
- import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
2
-
3
- @Entity('contentId')
4
- export class DbContentId {
5
- @PrimaryColumn('varchar')
6
- fullId!: string;
7
-
8
- @Column('varchar')
9
- @Index({ unique: true })
10
- shortId!: string;
11
- }