erudit 3.0.0-dev.21 → 3.0.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/assets/icons/cameo-add.svg +3 -3
- package/app/assets/icons/diamond.svg +2 -2
- package/app/assets/icons/files.svg +5 -0
- package/app/assets/icons/list-squared.svg +3 -0
- package/app/assets/icons/rocket.svg +1 -0
- package/app/components/GroupLikePage.vue +70 -0
- package/app/components/QuickLinks.vue +99 -0
- package/app/components/aside/major/SiteInfo.vue +2 -2
- package/app/components/aside/major/panes/nav/NavBook.vue +1 -1
- package/app/components/aside/major/panes/other/ItemTheme.vue +25 -30
- package/app/components/index/IndexAvatars.vue +143 -0
- package/app/components/main/MainActionButton.vue +3 -1
- package/app/components/main/MainBitranContent.vue +13 -0
- package/app/components/main/MainDescription.vue +6 -0
- package/app/components/main/MainSectionTitle.vue +50 -0
- package/app/components/main/MainSourceUsages.vue +119 -0
- package/app/components/main/MainSourcesUsage.vue +60 -0
- package/app/components/main/MainToc.vue +135 -0
- package/app/components/main/content/ContentPopovers.vue +9 -2
- package/app/components/main/content/ContentReferences.vue +1 -27
- package/app/components/main/content/reference/ReferenceGroup.vue +10 -9
- package/app/components/main/content/reference/ReferenceSource.vue +1 -0
- package/app/components/main/topic/MainTopic.vue +5 -8
- package/app/components/main/topic/MainTopicQuickLinks.vue +24 -0
- package/app/components/stats/Stats.vue +21 -0
- package/app/components/stats/StatsGroupLike.vue +24 -0
- package/app/components/stats/StatsItem.vue +33 -0
- package/app/components/stats/StatsItemElement.vue +13 -0
- package/app/composables/bitranLocation.ts +2 -3
- package/app/composables/contentPage.ts +7 -6
- package/app/composables/theme.ts +26 -5
- package/app/pages/book/[...bookId].vue +6 -31
- package/app/pages/group/[...groupId].vue +5 -41
- package/app/pages/index.vue +189 -16
- package/app/pages/sponsors.vue +2 -2
- package/app/scripts/preview/data/pageLink.ts +4 -3
- package/app/scripts/preview/data/unique.ts +6 -6
- package/languages/en.ts +7 -1
- package/languages/ru.ts +10 -3
- package/package.json +4 -4
- package/server/api/content/data.ts +33 -11
- package/server/api/index/data.ts +46 -0
- package/server/plugin/bitran/content.ts +0 -14
- package/server/plugin/bitran/location.ts +3 -6
- package/server/plugin/build/jobs/content/parse.ts +95 -1
- package/server/plugin/build/jobs/content/type/group.ts +0 -21
- package/server/plugin/content/context.ts +3 -6
- package/server/plugin/db/entities/Group.ts +0 -3
- package/server/plugin/db/entities/QuickLink.ts +19 -0
- package/server/plugin/db/entities/Stat.ts +13 -0
- package/server/plugin/db/setup.ts +4 -0
- package/server/plugin/repository/content.ts +3 -3
- package/server/plugin/repository/contentToc.ts +90 -0
- package/server/plugin/repository/elementStats.ts +80 -0
- package/server/plugin/repository/link.ts +20 -0
- package/server/plugin/repository/quickLink.ts +36 -0
- package/server/plugin/repository/readLink.ts +17 -0
- package/server/plugin/repository/reference.ts +78 -0
- package/server/plugin/repository/topicCount.ts +19 -0
- package/shared/content/data/base.ts +2 -2
- package/shared/content/data/groupLike.ts +11 -0
- package/shared/content/data/type/book.ts +3 -1
- package/shared/content/data/type/group.ts +3 -1
- package/shared/content/data/type/topic.ts +3 -1
- package/shared/content/reference.ts +18 -0
- package/shared/content/toc.ts +35 -0
- package/shared/indexData.ts +10 -0
- package/shared/link.ts +3 -2
- package/shared/quickLink.ts +7 -0
- package/shared/stat.ts +23 -0
- package/shared/types/language.ts +6 -1
- package/utils/normalize.ts +7 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import type { GroupConfig } from '@erudit-js/cog/schema';
|
|
3
2
|
|
|
4
3
|
import { DbGroup } from '@server/db/entities/Group';
|
|
5
4
|
import { ERUDIT_SERVER } from '@erudit/server/plugin/global';
|
|
6
5
|
import type { BuilderFunctionArgs } from '../builderArgs';
|
|
7
|
-
import { contentItemPath } from '../path';
|
|
8
|
-
import { parseBitranContent } from '../parse';
|
|
9
6
|
|
|
10
7
|
export async function buildGroup({
|
|
11
8
|
navNode,
|
|
@@ -15,23 +12,5 @@ export async function buildGroup({
|
|
|
15
12
|
dbGroup.contentId = navNode.fullId;
|
|
16
13
|
dbGroup.type = config?.type || 'folder';
|
|
17
14
|
|
|
18
|
-
try {
|
|
19
|
-
const strContent = readFileSync(
|
|
20
|
-
contentItemPath(navNode, 'content.bi'),
|
|
21
|
-
'utf-8',
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
if (strContent) {
|
|
25
|
-
dbGroup.content = strContent;
|
|
26
|
-
await parseBitranContent(
|
|
27
|
-
{
|
|
28
|
-
type: 'group',
|
|
29
|
-
path: dbGroup.contentId,
|
|
30
|
-
},
|
|
31
|
-
strContent,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
} catch {}
|
|
35
|
-
|
|
36
15
|
await ERUDIT_SERVER.DB.manager.save(dbGroup);
|
|
37
16
|
}
|
|
@@ -12,11 +12,8 @@ import { DbContributor } from '@server/db/entities/Contributor';
|
|
|
12
12
|
import { getFullContentId } from '@server/repository/contentId';
|
|
13
13
|
|
|
14
14
|
import type { Context } from '@shared/content/context';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
createContributorLink,
|
|
18
|
-
createTopicPartLink,
|
|
19
|
-
} from '@shared/link';
|
|
15
|
+
import { createContributorLink, createTopicPartLink } from '@shared/link';
|
|
16
|
+
import { createContentLink } from '@server/repository/link';
|
|
20
17
|
import { CONTENT_TYPE_ICON, ICON, TOPIC_PART_ICON } from '@erudit/shared/icons';
|
|
21
18
|
|
|
22
19
|
export async function getContentContext(contentId: string): Promise<Context> {
|
|
@@ -35,7 +32,7 @@ export async function getContentContext(contentId: string): Promise<Context> {
|
|
|
35
32
|
? CONTENT_TYPE_ICON[dbContent.type]
|
|
36
33
|
: '',
|
|
37
34
|
title: dbContent.title || dbContent.contentId,
|
|
38
|
-
href: createContentLink(dbContent.
|
|
35
|
+
href: await createContentLink(dbContent.contentId),
|
|
39
36
|
hidden: await isSkipId(dbContent.contentId),
|
|
40
37
|
});
|
|
41
38
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
@Entity('quickLink')
|
|
4
|
+
export class DbQuickLink {
|
|
5
|
+
@PrimaryColumn('varchar')
|
|
6
|
+
label!: string;
|
|
7
|
+
|
|
8
|
+
@PrimaryColumn('varchar')
|
|
9
|
+
contentId!: string;
|
|
10
|
+
|
|
11
|
+
@Column('varchar')
|
|
12
|
+
contentType!: string;
|
|
13
|
+
|
|
14
|
+
@Column('varchar')
|
|
15
|
+
elementId!: string;
|
|
16
|
+
|
|
17
|
+
@Column('varchar')
|
|
18
|
+
elementName!: string;
|
|
19
|
+
}
|
|
@@ -15,6 +15,8 @@ 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 { DbQuickLink } from './entities/QuickLink';
|
|
19
|
+
import { DbStat } from './entities/Stat';
|
|
18
20
|
|
|
19
21
|
export async function setupDatabase() {
|
|
20
22
|
rmSync(PROJECT_DIR + '/.erudit/data.sqlite', { force: true });
|
|
@@ -35,6 +37,8 @@ export async function setupDatabase() {
|
|
|
35
37
|
DbTopic,
|
|
36
38
|
DbUnique,
|
|
37
39
|
DbFile,
|
|
40
|
+
DbQuickLink,
|
|
41
|
+
DbStat,
|
|
38
42
|
],
|
|
39
43
|
});
|
|
40
44
|
|
|
@@ -17,11 +17,11 @@ import type { NavNode } from '../nav/node';
|
|
|
17
17
|
import { createContentLink, createTopicPartLink } from '@erudit/shared/link';
|
|
18
18
|
import type { PreviousNextItem } from '@erudit/shared/content/previousNext';
|
|
19
19
|
import type { ContentContributor } from '@erudit/shared/contributor';
|
|
20
|
-
import type {
|
|
20
|
+
import type { ContentGeneric } from '@shared/content/data/base';
|
|
21
21
|
|
|
22
22
|
export async function getContentGenericData(
|
|
23
23
|
contentId: string,
|
|
24
|
-
): Promise<
|
|
24
|
+
): Promise<ContentGeneric> {
|
|
25
25
|
const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
|
|
26
26
|
where: { contentId },
|
|
27
27
|
});
|
|
@@ -37,7 +37,7 @@ export async function getContentGenericData(
|
|
|
37
37
|
const decoration = await getContentDecoration(contentId);
|
|
38
38
|
const flags = await getContentFlags(contentId);
|
|
39
39
|
|
|
40
|
-
const contentPage:
|
|
40
|
+
const contentPage: ContentGeneric = {
|
|
41
41
|
contentId,
|
|
42
42
|
type: dbContent.type,
|
|
43
43
|
title: dbContent.title || undefined,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ContentToc } from '@shared/content/toc';
|
|
2
|
+
|
|
3
|
+
import { DbContent } from '@server/db/entities/Content';
|
|
4
|
+
import { ERUDIT_SERVER } from '@server/global';
|
|
5
|
+
import { getNavNode } from '@server/nav/utils';
|
|
6
|
+
import { createContentLink } from '@server/repository/link';
|
|
7
|
+
import { getElementStats } from '@erudit/server/plugin/repository/elementStats';
|
|
8
|
+
import { getQuickLinks } from '@server/repository/quickLink';
|
|
9
|
+
import { countTopicsIn } from '@server/repository/topicCount';
|
|
10
|
+
|
|
11
|
+
async function processChildContent(childId: string): Promise<any> {
|
|
12
|
+
const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
|
|
13
|
+
select: ['title', 'description', 'type'],
|
|
14
|
+
where: {
|
|
15
|
+
contentId: childId,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!dbContent) {
|
|
20
|
+
throw createError({
|
|
21
|
+
statusCode: 404,
|
|
22
|
+
statusMessage: `Failed to create content TOC!\nContent with ID "${childId}" not found!`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const title = dbContent.title ?? childId;
|
|
27
|
+
const link = await createContentLink(childId);
|
|
28
|
+
const description = dbContent.description;
|
|
29
|
+
|
|
30
|
+
switch (dbContent.type) {
|
|
31
|
+
case 'book':
|
|
32
|
+
case 'group':
|
|
33
|
+
const stats = await getElementStats(childId);
|
|
34
|
+
const topicCount = await countTopicsIn(childId);
|
|
35
|
+
return {
|
|
36
|
+
type: dbContent.type,
|
|
37
|
+
link,
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
topicCount,
|
|
41
|
+
stats: stats || [],
|
|
42
|
+
};
|
|
43
|
+
case 'topic':
|
|
44
|
+
const quickLinks = await getQuickLinks(childId);
|
|
45
|
+
return {
|
|
46
|
+
type: 'topic',
|
|
47
|
+
link,
|
|
48
|
+
title,
|
|
49
|
+
description,
|
|
50
|
+
quickLinks,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getContentToc(
|
|
56
|
+
mixedContentId: string,
|
|
57
|
+
): Promise<ContentToc> {
|
|
58
|
+
const navNode = getNavNode(mixedContentId);
|
|
59
|
+
const childIds = navNode.children?.map((child) => child.fullId) || [];
|
|
60
|
+
const toc: ContentToc = [];
|
|
61
|
+
|
|
62
|
+
for (const childId of childIds) {
|
|
63
|
+
const tocItem = await processChildContent(childId);
|
|
64
|
+
if (tocItem) {
|
|
65
|
+
toc.push(tocItem);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return toc;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getFullContentToc(): Promise<ContentToc | undefined> {
|
|
73
|
+
const rootNode = ERUDIT_SERVER.NAV;
|
|
74
|
+
|
|
75
|
+
if (!rootNode) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const childIds = rootNode.children?.map((child) => child.fullId) || [];
|
|
80
|
+
const toc: ContentToc = [];
|
|
81
|
+
|
|
82
|
+
for (const childId of childIds) {
|
|
83
|
+
const tocItem = await processChildContent(childId);
|
|
84
|
+
if (tocItem) {
|
|
85
|
+
toc.push(tocItem);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return toc;
|
|
90
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Like } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
import type { ElementStats } from '@shared/stat';
|
|
4
|
+
|
|
5
|
+
import { DbStat } from '@server/db/entities/Stat';
|
|
6
|
+
import { ERUDIT_SERVER } from '@server/global';
|
|
7
|
+
import { getFullContentId } from '@server/repository/contentId';
|
|
8
|
+
|
|
9
|
+
function buildGroupMappings() {
|
|
10
|
+
const statNames = ERUDIT_SERVER.CONFIG.bitran?.stat || [];
|
|
11
|
+
const orderedGroups: string[] = [];
|
|
12
|
+
const elementToGroup = new Map<string, string>();
|
|
13
|
+
|
|
14
|
+
statNames.forEach((item) => {
|
|
15
|
+
if (typeof item === 'string') {
|
|
16
|
+
orderedGroups.push(item);
|
|
17
|
+
elementToGroup.set(item, item);
|
|
18
|
+
} else if (Array.isArray(item) && item.length > 0) {
|
|
19
|
+
const groupName = item[0];
|
|
20
|
+
orderedGroups.push(groupName);
|
|
21
|
+
item.forEach((elementName) => {
|
|
22
|
+
elementToGroup.set(elementName, groupName);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return { orderedGroups, elementToGroup };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function processStats(
|
|
31
|
+
dbStats: DbStat[],
|
|
32
|
+
orderedGroups: string[],
|
|
33
|
+
elementToGroup: Map<string, string>,
|
|
34
|
+
): ElementStats {
|
|
35
|
+
const groupCounts = dbStats.reduce((counts, stat) => {
|
|
36
|
+
const groupName =
|
|
37
|
+
elementToGroup.get(stat.elementName) || stat.elementName;
|
|
38
|
+
counts.set(groupName, (counts.get(groupName) || 0) + stat.count);
|
|
39
|
+
return counts;
|
|
40
|
+
}, new Map<string, number>());
|
|
41
|
+
|
|
42
|
+
return orderedGroups
|
|
43
|
+
.map((groupName) => ({
|
|
44
|
+
elementName: groupName,
|
|
45
|
+
count: groupCounts.get(groupName) || 0,
|
|
46
|
+
type: 'element' as const,
|
|
47
|
+
}))
|
|
48
|
+
.filter((stat) => stat.count > 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getElementStats(
|
|
52
|
+
mixedContentId: string,
|
|
53
|
+
): Promise<ElementStats | undefined> {
|
|
54
|
+
const contentId = getFullContentId(mixedContentId);
|
|
55
|
+
const { orderedGroups, elementToGroup } = buildGroupMappings();
|
|
56
|
+
|
|
57
|
+
const dbStats = await ERUDIT_SERVER.DB.manager.find(DbStat, {
|
|
58
|
+
where: {
|
|
59
|
+
contentId: Like(`${contentId}/%`),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!dbStats.length) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return processStats(dbStats, orderedGroups, elementToGroup);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getAllElementStats(): Promise<ElementStats> {
|
|
71
|
+
const { orderedGroups, elementToGroup } = buildGroupMappings();
|
|
72
|
+
|
|
73
|
+
const dbStats = await ERUDIT_SERVER.DB.manager.find(DbStat);
|
|
74
|
+
|
|
75
|
+
if (!dbStats.length) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return processStats(dbStats, orderedGroups, elementToGroup);
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getNavNode } from '@server/nav/utils';
|
|
2
|
+
import {
|
|
3
|
+
createContentLink as _createContentLink,
|
|
4
|
+
createTopicPartLink,
|
|
5
|
+
} from '@shared/link';
|
|
6
|
+
import { getTopicParts } from './topic';
|
|
7
|
+
|
|
8
|
+
export async function createContentLink(
|
|
9
|
+
mixedContentId: string,
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const navNode = getNavNode(mixedContentId);
|
|
12
|
+
|
|
13
|
+
if (navNode.type === 'topic') {
|
|
14
|
+
const parts = await getTopicParts(navNode.fullId);
|
|
15
|
+
const part = parts.shift()!;
|
|
16
|
+
return createTopicPartLink(part, navNode.shortId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return _createContentLink(navNode.type, navNode.shortId);
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { trailingSlash } from '@erudit/utils/url';
|
|
2
|
+
|
|
3
|
+
import type { QuickLinks } from '@shared/quickLink';
|
|
4
|
+
import { createContentLink } from '@shared/link';
|
|
5
|
+
|
|
6
|
+
import { ERUDIT_SERVER } from '@server/global';
|
|
7
|
+
import { DbQuickLink } from '@server/db/entities/QuickLink';
|
|
8
|
+
import { getNavNode } from '@server/nav/utils';
|
|
9
|
+
import { getFullContentId } from '@server/repository/contentId';
|
|
10
|
+
|
|
11
|
+
export async function getQuickLinks(
|
|
12
|
+
mixedContentId: string,
|
|
13
|
+
): Promise<QuickLinks> {
|
|
14
|
+
const fullContentId = getFullContentId(mixedContentId);
|
|
15
|
+
const dbTags = await ERUDIT_SERVER.DB.manager.find(DbQuickLink, {
|
|
16
|
+
where: { contentId: fullContentId },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return dbTags.map((dbTag) => {
|
|
20
|
+
const navNode = getNavNode(dbTag.contentId);
|
|
21
|
+
|
|
22
|
+
const link =
|
|
23
|
+
trailingSlash(
|
|
24
|
+
createContentLink(dbTag.contentType as any, navNode.shortId),
|
|
25
|
+
false,
|
|
26
|
+
) +
|
|
27
|
+
'#' +
|
|
28
|
+
dbTag.elementId;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
label: dbTag.label,
|
|
32
|
+
elementName: dbTag.elementName,
|
|
33
|
+
link,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getNavNode } from '@server/nav/utils';
|
|
2
|
+
import { createContentLink } from '@server/repository/link';
|
|
3
|
+
|
|
4
|
+
export async function getReadLink(mixedContentId: string): Promise<string> {
|
|
5
|
+
const navNode = getNavNode(mixedContentId);
|
|
6
|
+
|
|
7
|
+
if (!navNode.children || navNode.children.length === 0) {
|
|
8
|
+
throw createError({
|
|
9
|
+
statusCode: 404,
|
|
10
|
+
statusText: `Missing children to create read link for content ID "${mixedContentId}"!`,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const targetId = navNode.children[0]!.fullId;
|
|
15
|
+
|
|
16
|
+
return await createContentLink(targetId);
|
|
17
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Like } from 'typeorm';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ContentSourceUsage,
|
|
6
|
+
ContentSourceUsageSet,
|
|
7
|
+
ContentSourceUsageSetItem,
|
|
8
|
+
} from '@shared/content/reference';
|
|
9
|
+
import { ERUDIT_SERVER } from '@server/global';
|
|
10
|
+
import { DbContent } from '@server/db/entities/Content';
|
|
11
|
+
import { createContentLink } from '@server/repository/link';
|
|
12
|
+
import { getFullContentId } from '@server/repository/contentId';
|
|
13
|
+
|
|
14
|
+
export async function getContentSourceUsageSet(
|
|
15
|
+
mixedContentId: string,
|
|
16
|
+
): Promise<ContentSourceUsageSet> {
|
|
17
|
+
const contentFullId = getFullContentId(mixedContentId);
|
|
18
|
+
const dbContentItems = await ERUDIT_SERVER.DB.manager.find(DbContent, {
|
|
19
|
+
select: ['contentId'],
|
|
20
|
+
where: { contentId: Like(`${contentFullId}/%`), type: 'topic' },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const usageSetObj: Record<string, ContentSourceUsageSetItem> = {};
|
|
24
|
+
|
|
25
|
+
for (const childId of dbContentItems.map((item) => item.contentId)) {
|
|
26
|
+
const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
|
|
27
|
+
select: ['title', 'type', 'references'],
|
|
28
|
+
where: { contentId: childId },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!dbContent || !dbContent.references) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const referenceGroups = dbContent.references;
|
|
36
|
+
for (const referenceGroup of referenceGroups) {
|
|
37
|
+
const source = referenceGroup.source;
|
|
38
|
+
const sourceId = getObjectUuid(source);
|
|
39
|
+
|
|
40
|
+
if (!(sourceId in usageSetObj)) {
|
|
41
|
+
usageSetObj[sourceId] = {
|
|
42
|
+
source,
|
|
43
|
+
usages: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const usageSet = usageSetObj[sourceId];
|
|
48
|
+
|
|
49
|
+
usageSet.usages.push({
|
|
50
|
+
type: dbContent.type,
|
|
51
|
+
title: dbContent.title || childId,
|
|
52
|
+
count: referenceGroup.references.length,
|
|
53
|
+
link: await createContentLink(childId),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sortedEntries = Object.entries(usageSetObj).sort(([, a], [, b]) => {
|
|
59
|
+
const aFeatured = !!a.source.featured;
|
|
60
|
+
const bFeatured = !!b.source.featured;
|
|
61
|
+
|
|
62
|
+
if (aFeatured !== bFeatured) {
|
|
63
|
+
return bFeatured ? 1 : -1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const aSum = a.usages.reduce((sum, usage) => sum + usage.count, 0);
|
|
67
|
+
const bSum = b.usages.reduce((sum, usage) => sum + usage.count, 0);
|
|
68
|
+
|
|
69
|
+
return bSum - aSum;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return sortedEntries.map(([, value]) => value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getObjectUuid(obj: any): string {
|
|
76
|
+
const serialized = JSON.stringify(obj, Object.keys(obj).sort());
|
|
77
|
+
return createHash('sha256').update(serialized).digest('hex').slice(0, 8);
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Like } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
import { DbTopic } from '@server/db/entities/Topic';
|
|
4
|
+
import { ERUDIT_SERVER } from '@server/global';
|
|
5
|
+
import { getFullContentId } from '@server/repository/contentId';
|
|
6
|
+
|
|
7
|
+
export async function countTopicsIn(mixedContentId: string) {
|
|
8
|
+
const fullContentId = getFullContentId(mixedContentId);
|
|
9
|
+
return await ERUDIT_SERVER.DB.manager.count(DbTopic, {
|
|
10
|
+
where: [
|
|
11
|
+
{ contentId: fullContentId },
|
|
12
|
+
{ contentId: Like(`${fullContentId}/%`) },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function countAllTopics() {
|
|
18
|
+
return await ERUDIT_SERVER.DB.manager.count(DbTopic);
|
|
19
|
+
}
|
|
@@ -10,7 +10,7 @@ import type { Context } from '@shared/content/context';
|
|
|
10
10
|
import type { PreviousNext } from '@shared/content/previousNext';
|
|
11
11
|
import type { ImageData } from '@shared/image';
|
|
12
12
|
|
|
13
|
-
export interface
|
|
13
|
+
export interface ContentGeneric {
|
|
14
14
|
contentId: string;
|
|
15
15
|
title?: string;
|
|
16
16
|
description?: string;
|
|
@@ -28,5 +28,5 @@ export interface ContentGenericData {
|
|
|
28
28
|
|
|
29
29
|
export interface ContentBaseData {
|
|
30
30
|
type: ContentType;
|
|
31
|
-
generic:
|
|
31
|
+
generic: ContentGeneric;
|
|
32
32
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ElementStat } from '@shared/stat';
|
|
2
|
+
import type { ContentToc } from '@shared/content/toc';
|
|
3
|
+
import type { ContentSourceUsageSet } from '@shared/content/reference';
|
|
4
|
+
|
|
5
|
+
export interface ContentGroupLike {
|
|
6
|
+
contentToc: ContentToc;
|
|
7
|
+
readLink: string;
|
|
8
|
+
topicCount: number;
|
|
9
|
+
elementStats?: ElementStat[];
|
|
10
|
+
sourceUsageSet?: ContentSourceUsageSet;
|
|
11
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { ContentBaseData } from '
|
|
1
|
+
import type { ContentBaseData } from '@shared/content/data/base';
|
|
2
|
+
import type { ContentGroupLike } from '@shared/content/data/groupLike';
|
|
2
3
|
|
|
3
4
|
export interface ContentBookData extends ContentBaseData {
|
|
4
5
|
type: 'book';
|
|
6
|
+
groupLike: ContentGroupLike;
|
|
5
7
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { ContentBaseData } from '
|
|
1
|
+
import type { ContentBaseData } from '@shared/content/data/base';
|
|
2
|
+
import type { ContentGroupLike } from '@shared/content/data/groupLike';
|
|
2
3
|
|
|
3
4
|
export interface ContentGroupData extends ContentBaseData {
|
|
4
5
|
type: 'group';
|
|
6
|
+
groupLike: ContentGroupLike;
|
|
5
7
|
bookTitle?: string;
|
|
6
8
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TopicPart } from '@erudit-js/cog/schema';
|
|
2
|
+
import type { QuickLinks } from '@erudit/shared/quickLink';
|
|
2
3
|
|
|
3
|
-
import type { ContentBaseData } from '
|
|
4
|
+
import type { ContentBaseData } from '@shared/content/data/base';
|
|
4
5
|
|
|
5
6
|
export type TopicPartLinks = Partial<Record<TopicPart, string>>;
|
|
6
7
|
|
|
@@ -8,4 +9,5 @@ export interface ContentTopicData extends ContentBaseData {
|
|
|
8
9
|
type: 'topic';
|
|
9
10
|
topicPartLinks: TopicPartLinks;
|
|
10
11
|
bookTitle?: string;
|
|
12
|
+
quickLinks: QuickLinks;
|
|
11
13
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContentReferenceSource,
|
|
3
|
+
ContentType,
|
|
4
|
+
} from '@erudit-js/cog/schema';
|
|
5
|
+
|
|
6
|
+
export interface ContentSourceUsage {
|
|
7
|
+
type: ContentType;
|
|
8
|
+
title: string;
|
|
9
|
+
link: string;
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ContentSourceUsageSetItem {
|
|
14
|
+
source: ContentReferenceSource;
|
|
15
|
+
usages: ContentSourceUsage[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ContentSourceUsageSet = ContentSourceUsageSetItem[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ContentType } from '@erudit-js/cog/schema';
|
|
2
|
+
|
|
3
|
+
import type { ElementStats } from '../stat';
|
|
4
|
+
import type { QuickLinks } from '../quickLink';
|
|
5
|
+
|
|
6
|
+
interface ContentTocBaseItem {
|
|
7
|
+
type: ContentType;
|
|
8
|
+
link: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ContentTocBookItem extends ContentTocBaseItem {
|
|
14
|
+
type: 'book';
|
|
15
|
+
topicCount: number;
|
|
16
|
+
stats: ElementStats;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ContentTocGroupItem extends ContentTocBaseItem {
|
|
20
|
+
type: 'group';
|
|
21
|
+
topicCount: number;
|
|
22
|
+
stats: ElementStats;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ContentTocTopicItem extends ContentTocBaseItem {
|
|
26
|
+
type: 'topic';
|
|
27
|
+
quickLinks: QuickLinks;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ContentTocItem =
|
|
31
|
+
| ContentTocBookItem
|
|
32
|
+
| ContentTocGroupItem
|
|
33
|
+
| ContentTocTopicItem;
|
|
34
|
+
|
|
35
|
+
export type ContentToc = ContentTocItem[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContentToc } from './content/toc';
|
|
2
|
+
import type { ElementStats } from './stat';
|
|
3
|
+
|
|
4
|
+
export interface IndexData {
|
|
5
|
+
elementStats: ElementStats;
|
|
6
|
+
topicCount: number;
|
|
7
|
+
contributors: [string, string | undefined][];
|
|
8
|
+
sponsors: [string, string | undefined][];
|
|
9
|
+
contentToc?: ContentToc;
|
|
10
|
+
}
|
package/shared/link.ts
CHANGED
|
@@ -13,8 +13,9 @@ export function createBitranLocationLink(location: BitranLocation) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function createContentLink(contentType: ContentType, contentId: string) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if (contentType === 'topic') {
|
|
17
|
+
throw Error(`Use 'createTopicPartLink' to create links to topics!`);
|
|
18
|
+
}
|
|
18
19
|
|
|
19
20
|
return `/${contentType}/${contentId}/`;
|
|
20
21
|
}
|
package/shared/stat.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MyIconName } from '#my-icons';
|
|
2
|
+
|
|
3
|
+
export type StatType = 'custom' | 'element';
|
|
4
|
+
|
|
5
|
+
interface StatBase {
|
|
6
|
+
type: StatType;
|
|
7
|
+
count: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CustomStat extends StatBase {
|
|
11
|
+
type: 'custom';
|
|
12
|
+
icon: MyIconName | (string & {});
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ElementStat extends StatBase {
|
|
17
|
+
type: 'element';
|
|
18
|
+
elementName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Stat = ElementStat | CustomStat;
|
|
22
|
+
export type ElementStats = ElementStat[];
|
|
23
|
+
export type Stats = Stat[];
|