erudit 2.0.0-dev.7

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 (314) hide show
  1. package/.nuxtrc +1 -0
  2. package/app/app.vue +172 -0
  3. package/app/assets/icons/alert.svg +3 -0
  4. package/app/assets/icons/angle-right.svg +3 -0
  5. package/app/assets/icons/arrow-in-text.svg +3 -0
  6. package/app/assets/icons/arrow-left.svg +3 -0
  7. package/app/assets/icons/arrow-up-to-right.svg +3 -0
  8. package/app/assets/icons/aside-open.svg +3 -0
  9. package/app/assets/icons/asterisk.svg +3 -0
  10. package/app/assets/icons/book-question.svg +3 -0
  11. package/app/assets/icons/book.svg +3 -0
  12. package/app/assets/icons/bug.svg +3 -0
  13. package/app/assets/icons/chip.svg +3 -0
  14. package/app/assets/icons/circle-help.svg +3 -0
  15. package/app/assets/icons/construction.svg +3 -0
  16. package/app/assets/icons/cross.svg +3 -0
  17. package/app/assets/icons/draw.svg +3 -0
  18. package/app/assets/icons/ellipsis-vertical.svg +3 -0
  19. package/app/assets/icons/file-check.svg +3 -0
  20. package/app/assets/icons/file-lines.svg +3 -0
  21. package/app/assets/icons/file-star.svg +3 -0
  22. package/app/assets/icons/folder-open.svg +3 -0
  23. package/app/assets/icons/folder.svg +3 -0
  24. package/app/assets/icons/globe.svg +3 -0
  25. package/app/assets/icons/house.svg +3 -0
  26. package/app/assets/icons/link-external.svg +3 -0
  27. package/app/assets/icons/link.svg +3 -0
  28. package/app/assets/icons/moon.svg +3 -0
  29. package/app/assets/icons/outline/book.svg +3 -0
  30. package/app/assets/icons/outline/file-lines.svg +3 -0
  31. package/app/assets/icons/pirate.svg +3 -0
  32. package/app/assets/icons/plus-circle.svg +3 -0
  33. package/app/assets/icons/plus.svg +3 -0
  34. package/app/assets/icons/puzzle.svg +3 -0
  35. package/app/assets/icons/search.svg +3 -0
  36. package/app/assets/icons/shuffle.svg +3 -0
  37. package/app/assets/icons/star.svg +3 -0
  38. package/app/assets/icons/sun-moon.svg +3 -0
  39. package/app/assets/icons/sun.svg +3 -0
  40. package/app/assets/icons/user.svg +3 -0
  41. package/app/assets/icons/users.svg +3 -0
  42. package/app/components/Loading.vue +23 -0
  43. package/app/components/SiteAside.vue +382 -0
  44. package/app/components/SiteMain.vue +35 -0
  45. package/app/components/ads/BannerTemplate.vue +51 -0
  46. package/app/components/ads/BottomBanner.vue +45 -0
  47. package/app/components/ads/LeftBanner.vue +50 -0
  48. package/app/components/aside/AsideListItem.vue +74 -0
  49. package/app/components/aside/AsideMajor.vue +56 -0
  50. package/app/components/aside/AsideMinor.vue +71 -0
  51. package/app/components/aside/major/PaneContentScroll.vue +23 -0
  52. package/app/components/aside/major/PaneSwitch.vue +54 -0
  53. package/app/components/aside/major/PaneSwitchButton.vue +63 -0
  54. package/app/components/aside/major/SiteInfo.vue +85 -0
  55. package/app/components/aside/major/panes/Language.vue +79 -0
  56. package/app/components/aside/major/panes/Pages.vue +34 -0
  57. package/app/components/aside/major/panes/Search.vue +3 -0
  58. package/app/components/aside/major/panes/nav/Nav.vue +91 -0
  59. package/app/components/aside/major/panes/nav/NavBook.vue +86 -0
  60. package/app/components/aside/major/panes/nav/NavBookLoading.vue +24 -0
  61. package/app/components/aside/major/panes/nav/NavGlobal.vue +16 -0
  62. package/app/components/aside/major/panes/nav/fnav/FNav.vue +105 -0
  63. package/app/components/aside/major/panes/nav/fnav/FNavBook.vue +32 -0
  64. package/app/components/aside/major/panes/nav/fnav/FNavFlags.vue +40 -0
  65. package/app/components/aside/major/panes/nav/fnav/FNavFolder.vue +60 -0
  66. package/app/components/aside/major/panes/nav/fnav/FNavItem.vue +34 -0
  67. package/app/components/aside/major/panes/nav/fnav/FNavSeparator.vue +80 -0
  68. package/app/components/aside/major/panes/nav/fnav/FNavTopic.vue +24 -0
  69. package/app/components/aside/major/panes/other/ItemContent.vue +29 -0
  70. package/app/components/aside/major/panes/other/ItemGenerator.vue +15 -0
  71. package/app/components/aside/major/panes/other/ItemTheme.vue +54 -0
  72. package/app/components/aside/major/panes/other/Other.vue +16 -0
  73. package/app/components/aside/minor/AsideMinorContributor.vue +5 -0
  74. package/app/components/aside/minor/AsideMinorNews.vue +11 -0
  75. package/app/components/aside/minor/AsideMinorPane.vue +16 -0
  76. package/app/components/aside/minor/AsideMinorTopLink.vue +67 -0
  77. package/app/components/aside/minor/Contribute.vue +145 -0
  78. package/app/components/aside/minor/content/AsideMinorContent.vue +92 -0
  79. package/app/components/aside/minor/topic/AsideMinorTopic.vue +32 -0
  80. package/app/components/aside/minor/topic/TopicContributors.vue +177 -0
  81. package/app/components/aside/minor/topic/TopicNav.vue +49 -0
  82. package/app/components/aside/minor/topic/TopicToc.vue +202 -0
  83. package/app/components/aside/minor/topic/TopicTocItem.vue +31 -0
  84. package/app/components/aside/utils/AsideOverlayPane.vue +40 -0
  85. package/app/components/bitran/BitranContent.vue +64 -0
  86. package/app/components/bitran/RenderWrapper.vue +12 -0
  87. package/app/components/contributor/ContributorAvatar.vue +43 -0
  88. package/app/components/contributor/ContributorListItem.vue +35 -0
  89. package/app/components/main/topic/MainTopic.vue +79 -0
  90. package/app/components/main/topic/TopicPartSwitch.vue +118 -0
  91. package/app/components/main/utils/Breadcrumb.vue +75 -0
  92. package/app/components/main/utils/ContentDecoration.vue +29 -0
  93. package/app/components/main/utils/ContentDescription.vue +20 -0
  94. package/app/components/main/utils/ContentFlag.vue +16 -0
  95. package/app/components/main/utils/ContentPopover.vue +176 -0
  96. package/app/components/main/utils/ContentPopovers.vue +105 -0
  97. package/app/components/main/utils/ContentReferences.vue +76 -0
  98. package/app/components/main/utils/ContentSection.vue +42 -0
  99. package/app/components/main/utils/ContentTitle.vue +37 -0
  100. package/app/components/main/utils/reference/ReferenceGroup.vue +41 -0
  101. package/app/components/main/utils/reference/ReferenceItem.vue +64 -0
  102. package/app/components/main/utils/reference/ReferenceSource.vue +110 -0
  103. package/app/components/preview/Preview.vue +177 -0
  104. package/app/components/preview/PreviewDisplay.vue +139 -0
  105. package/app/components/preview/PreviewFooterAction.vue +73 -0
  106. package/app/components/preview/PreviewLoading.vue +15 -0
  107. package/app/components/preview/PreviewScreen.vue +99 -0
  108. package/app/components/preview/display/Alert.vue +50 -0
  109. package/app/components/preview/display/Custom.vue +18 -0
  110. package/app/components/preview/display/GenericLink.vue +48 -0
  111. package/app/components/preview/display/PageLink.vue +20 -0
  112. package/app/components/preview/display/Unique.vue +49 -0
  113. package/app/components/transition/Fade.vue +22 -0
  114. package/app/components/tree/TreeContainer.vue +12 -0
  115. package/app/components/tree/TreeItem.vue +89 -0
  116. package/app/composables/bitran.ts +132 -0
  117. package/app/composables/bitranContent.ts +36 -0
  118. package/app/composables/bitranLocation.ts +7 -0
  119. package/app/composables/contentData.ts +36 -0
  120. package/app/composables/contentPage.ts +156 -0
  121. package/app/composables/contentRoute.ts +45 -0
  122. package/app/composables/darkMagic.ts +24 -0
  123. package/app/composables/externalApi.ts +63 -0
  124. package/app/composables/favicon.ts +8 -0
  125. package/app/composables/formatText.ts +86 -0
  126. package/app/composables/majorPane.ts +60 -0
  127. package/app/composables/phrases.ts +80 -0
  128. package/app/composables/theme.ts +29 -0
  129. package/app/composables/url.ts +33 -0
  130. package/app/pages/_test/preview.vue +110 -0
  131. package/app/pages/article/[...articleId].vue +3 -0
  132. package/app/pages/book/[...bookId].vue +47 -0
  133. package/app/pages/group/[...groupId].vue +64 -0
  134. package/app/pages/index.vue +32 -0
  135. package/app/pages/members.vue +7 -0
  136. package/app/pages/practice/[...practice].vue +3 -0
  137. package/app/pages/summary/[...summaryId].vue +3 -0
  138. package/app/public/favicon/article.svg +10 -0
  139. package/app/public/favicon/default.svg +10 -0
  140. package/app/public/favicon/practice.svg +10 -0
  141. package/app/public/favicon/summary.svg +10 -0
  142. package/app/public/logotype.svg +17 -0
  143. package/app/public/og-default.png +0 -0
  144. package/app/public/user.svg +10 -0
  145. package/app/scripts/_immediate.js +4 -0
  146. package/app/scripts/aside/index.ts +59 -0
  147. package/app/scripts/aside/major/nav.ts +26 -0
  148. package/app/scripts/aside/minor/state.ts +37 -0
  149. package/app/scripts/aside/minor/topic.ts +3 -0
  150. package/app/scripts/flag.ts +28 -0
  151. package/app/scripts/og.ts +27 -0
  152. package/app/scripts/preview/build.ts +84 -0
  153. package/app/scripts/preview/data/alert.ts +19 -0
  154. package/app/scripts/preview/data/custom.ts +8 -0
  155. package/app/scripts/preview/data/genericLink.ts +24 -0
  156. package/app/scripts/preview/data/pageLink.ts +20 -0
  157. package/app/scripts/preview/data/unique.ts +70 -0
  158. package/app/scripts/preview/data.ts +26 -0
  159. package/app/scripts/preview/display.ts +39 -0
  160. package/app/scripts/preview/footer.ts +9 -0
  161. package/app/scripts/preview/request.ts +51 -0
  162. package/app/scripts/preview/state.ts +63 -0
  163. package/app/styles/_immediate.css +3 -0
  164. package/app/styles/_util.scss +50 -0
  165. package/app/styles/_utils.scss +44 -0
  166. package/app/styles/app.scss +91 -0
  167. package/app/styles/def/_bp.scss +24 -0
  168. package/app/styles/def/_size.scss +7 -0
  169. package/app/styles/def/_z.scss +5 -0
  170. package/app/styles/default.scss +85 -0
  171. package/app/styles/normalize.scss +63 -0
  172. package/app/styles/partials/_darkMagic.scss +7 -0
  173. package/app/styles/partials/_fnav.scss +18 -0
  174. package/app/styles/partials/_preview.scss +7 -0
  175. package/globalPath.ts +24 -0
  176. package/globals/bitran.ts +39 -0
  177. package/globals/content.ts +22 -0
  178. package/globals/contributor.ts +5 -0
  179. package/globals/erudit.ts +5 -0
  180. package/globals/register.ts +18 -0
  181. package/languages/en.ts +95 -0
  182. package/languages/ru.ts +99 -0
  183. package/module/bitran.ts +34 -0
  184. package/module/config.ts +34 -0
  185. package/module/imports.ts +46 -0
  186. package/module/index.ts +35 -0
  187. package/module/logger.ts +10 -0
  188. package/module/paths.ts +22 -0
  189. package/module/restart.ts +61 -0
  190. package/nuxt.config.ts +96 -0
  191. package/package.json +32 -0
  192. package/server/api/aside/major/nav/bookIds.ts +5 -0
  193. package/server/api/aside/major/nav/bookNav/[...bookId].ts +20 -0
  194. package/server/api/aside/major/nav/global.ts +7 -0
  195. package/server/api/aside/minor/news.ts +7 -0
  196. package/server/api/aside/minor/path.ts +78 -0
  197. package/server/api/bitran/content/[location].ts +7 -0
  198. package/server/api/bitran/toc/[location].ts +7 -0
  199. package/server/api/content/data.ts +72 -0
  200. package/server/api/contributor/count.ts +6 -0
  201. package/server/api/fake/content.ts +11 -0
  202. package/server/api/fake/shared/languages.ts +12 -0
  203. package/server/api/language/functions.ts +12 -0
  204. package/server/api/language/phrase/[phraseId].ts +19 -0
  205. package/server/api/language/phraseIds.ts +8 -0
  206. package/server/api/preview/page/[...parts].ts +51 -0
  207. package/server/api/preview/unique/[location].ts +55 -0
  208. package/server/plugin/bitran/content.ts +176 -0
  209. package/server/plugin/bitran/core.ts +51 -0
  210. package/server/plugin/bitran/location.ts +25 -0
  211. package/server/plugin/bitran/products/include.ts +229 -0
  212. package/server/plugin/bitran/products/link.ts +114 -0
  213. package/server/plugin/bitran/setup.ts +10 -0
  214. package/server/plugin/bitran/toc.ts +82 -0
  215. package/server/plugin/build/close.ts +10 -0
  216. package/server/plugin/build/jobs/content/builderArgs.ts +8 -0
  217. package/server/plugin/build/jobs/content/generic.ts +176 -0
  218. package/server/plugin/build/jobs/content/parse.ts +90 -0
  219. package/server/plugin/build/jobs/content/path.ts +6 -0
  220. package/server/plugin/build/jobs/content/type/book.ts +9 -0
  221. package/server/plugin/build/jobs/content/type/group.ts +37 -0
  222. package/server/plugin/build/jobs/content/type/topic.ts +36 -0
  223. package/server/plugin/build/jobs/contributors.ts +66 -0
  224. package/server/plugin/build/jobs/language.ts +36 -0
  225. package/server/plugin/build/jobs/nav.ts +209 -0
  226. package/server/plugin/build/process.ts +25 -0
  227. package/server/plugin/build/rebuild.ts +55 -0
  228. package/server/plugin/build/setup.ts +21 -0
  229. package/server/plugin/content/absoluteId.ts +94 -0
  230. package/server/plugin/content/context.ts +112 -0
  231. package/server/plugin/db/entities/Book.ts +7 -0
  232. package/server/plugin/db/entities/Content.ts +49 -0
  233. package/server/plugin/db/entities/Contribution.ts +10 -0
  234. package/server/plugin/db/entities/Contributor.ts +16 -0
  235. package/server/plugin/db/entities/Group.ts +14 -0
  236. package/server/plugin/db/entities/Hash.ts +15 -0
  237. package/server/plugin/db/entities/Topic.ts +20 -0
  238. package/server/plugin/db/entities/Unique.ts +21 -0
  239. package/server/plugin/db/setup.ts +34 -0
  240. package/server/plugin/global.ts +18 -0
  241. package/server/plugin/importer.ts +12 -0
  242. package/server/plugin/index.ts +9 -0
  243. package/server/plugin/logger.ts +23 -0
  244. package/server/plugin/nav/node.ts +26 -0
  245. package/server/plugin/nav/utils.ts +129 -0
  246. package/server/plugin/repository/book.ts +21 -0
  247. package/server/plugin/repository/content.ts +238 -0
  248. package/server/plugin/repository/contributor.ts +8 -0
  249. package/server/plugin/repository/frontNav.ts +148 -0
  250. package/server/plugin/repository/topic.ts +32 -0
  251. package/server/tsconfig.json +7 -0
  252. package/shared/aside/minor.ts +50 -0
  253. package/shared/asset.ts +15 -0
  254. package/shared/bitran/alias.ts +17 -0
  255. package/shared/bitran/context.ts +7 -0
  256. package/shared/bitran/location.ts +166 -0
  257. package/shared/bitran/products/alias/core/factory.ts +46 -0
  258. package/shared/bitran/products/alias/core/index.ts +13 -0
  259. package/shared/bitran/products/alias/render/Alias.vue +10 -0
  260. package/shared/bitran/products/alias/render/icon.svg +3 -0
  261. package/shared/bitran/products/alias/render/index.ts +17 -0
  262. package/shared/bitran/products/alias/render/languages/en.ts +5 -0
  263. package/shared/bitran/products/alias/render/languages/ru.ts +5 -0
  264. package/shared/bitran/products/alias/shared.ts +11 -0
  265. package/shared/bitran/products/heading/core/factory.ts +53 -0
  266. package/shared/bitran/products/heading/core/index.ts +19 -0
  267. package/shared/bitran/products/heading/render/Heading.vue +47 -0
  268. package/shared/bitran/products/heading/render/icon.svg +3 -0
  269. package/shared/bitran/products/heading/render/index.ts +17 -0
  270. package/shared/bitran/products/heading/render/languages/en.ts +5 -0
  271. package/shared/bitran/products/heading/render/languages/ru.ts +5 -0
  272. package/shared/bitran/products/heading/shared.ts +13 -0
  273. package/shared/bitran/products/include/core/factory.ts +61 -0
  274. package/shared/bitran/products/include/core/index.ts +13 -0
  275. package/shared/bitran/products/include/render/Include.vue +13 -0
  276. package/shared/bitran/products/include/render/icon.svg +3 -0
  277. package/shared/bitran/products/include/render/index.ts +18 -0
  278. package/shared/bitran/products/include/render/languages/en.ts +5 -0
  279. package/shared/bitran/products/include/render/languages/ru.ts +5 -0
  280. package/shared/bitran/products/include/shared.ts +15 -0
  281. package/shared/bitran/products/link/core/factory.ts +20 -0
  282. package/shared/bitran/products/link/core/index.ts +17 -0
  283. package/shared/bitran/products/link/render/Link.vue +174 -0
  284. package/shared/bitran/products/link/render/icon.svg +3 -0
  285. package/shared/bitran/products/link/render/index.ts +17 -0
  286. package/shared/bitran/products/link/render/languages/en.ts +5 -0
  287. package/shared/bitran/products/link/render/languages/ru.ts +5 -0
  288. package/shared/bitran/products/link/shared.ts +15 -0
  289. package/shared/bitran/products/link/target.ts +134 -0
  290. package/shared/bitran/toc.ts +8 -0
  291. package/shared/content/context.ts +9 -0
  292. package/shared/content/data/base.ts +32 -0
  293. package/shared/content/data/index.ts +5 -0
  294. package/shared/content/data/type/book.ts +5 -0
  295. package/shared/content/data/type/group.ts +6 -0
  296. package/shared/content/data/type/topic.ts +11 -0
  297. package/shared/content/previousNext.ts +9 -0
  298. package/shared/contributor.ts +5 -0
  299. package/shared/frontNav.ts +41 -0
  300. package/shared/icons.ts +38 -0
  301. package/shared/image.ts +5 -0
  302. package/shared/link.ts +25 -0
  303. package/shared/types/language.ts +75 -0
  304. package/shared/utils/objectsEqual.ts +4 -0
  305. package/shared/utils/stringColor.ts +9 -0
  306. package/test/bitran/alias.test.ts +44 -0
  307. package/test/bitran/location.test.ts +143 -0
  308. package/test/bitran/products/alias.test.ts +83 -0
  309. package/test/bitran/products/heading.test.ts +119 -0
  310. package/test/bitran/products/include.test.ts +77 -0
  311. package/test/bitran/products/link/factory.test.ts +30 -0
  312. package/test/bitran/products/link/target.test.ts +138 -0
  313. package/tsconfig.json +8 -0
  314. package/utils/stress.ts +9 -0
@@ -0,0 +1,209 @@
1
+ import { globSync } from 'glob';
2
+ import chalk from 'chalk';
3
+ import { resolvePaths } from 'erudit-cog/kit';
4
+ import { contentTypes, topicParts, type ContentType } from 'erudit-cog/schema';
5
+
6
+ import { PROJECT_DIR } from '#erudit/globalPaths';
7
+ import { stress } from '@erudit/utils/stress';
8
+ import { debug, logger } from '@server/logger';
9
+ import {
10
+ createRootNode,
11
+ isRootNode,
12
+ type NavNode,
13
+ type RootNavNode,
14
+ } from '@server/nav/node';
15
+ import { ERUDIT_SERVER } from '../../global';
16
+
17
+ type Ids = Record<string, string>;
18
+ let ids: Ids;
19
+
20
+ const contentTargets = ERUDIT_SERVER.CONFIG?.contentTargets || [];
21
+
22
+ const nodePathRegexp = new RegExp(
23
+ `(?<pos>\\d+)(?<sep>-|\\+)(?<id>[\\w-]+)\\/(?<type>${contentTypes.join('|')})\\..*`,
24
+ );
25
+
26
+ export async function buildNav() {
27
+ debug.start('Building navigation tree...');
28
+
29
+ ids = {};
30
+
31
+ const rootNode = createRootNode();
32
+ rootNode.children = (await scanChildNodes(rootNode, false)).children;
33
+ ERUDIT_SERVER.NAV = rootNode.children ? rootNode : undefined;
34
+
35
+ const nodeCount = Object.values(ids).length;
36
+
37
+ if (nodeCount === 0) {
38
+ logger.warn('No content nodes detected! The site will be empty!');
39
+ } else {
40
+ if (ERUDIT_SERVER.CONFIG?.debug?.log) debugPrintNav(rootNode);
41
+
42
+ logger.success(
43
+ 'Navigation tree built successfully!',
44
+ chalk.dim(`(${nodeCount})`),
45
+ );
46
+ }
47
+ }
48
+
49
+ async function scanChildNodes(
50
+ parent: NavNode | RootNavNode,
51
+ insideBook: boolean,
52
+ ): Promise<{ children: NavNode[] | undefined; newIds: Ids }> {
53
+ const currentFsPath = isRootNode(parent) ? '' : parent.path;
54
+
55
+ const nodeFsPaths = globSync(
56
+ `${PROJECT_DIR}/content/${currentFsPath}/*/{${contentTypes.join(',')}}.{ts,js}`,
57
+ )
58
+ .sort()
59
+ .map((path) => resolvePaths(path));
60
+
61
+ let newIds: Ids = {};
62
+ const children: NavNode[] = [];
63
+
64
+ for (const nodeFsPath of nodeFsPaths) {
65
+ const pathParts:
66
+ | {
67
+ pos: string;
68
+ sep: string;
69
+ id: string;
70
+ type: ContentType;
71
+ }
72
+ | undefined = nodeFsPath.match(nodePathRegexp)?.groups as any;
73
+
74
+ if (!pathParts) continue; // Wrong path pattern
75
+
76
+ const nodePath = nodeFsPath
77
+ .replace(PROJECT_DIR + '/content/', '')
78
+ .split('/')
79
+ .slice(0, -1)
80
+ .join('/');
81
+
82
+ if (pathParts.type === 'book' && insideBook) {
83
+ logger.warn(
84
+ 'Books inside books are not allowed!\n',
85
+ `Skipping ${stress(nodePath)} nav node!`,
86
+ );
87
+ continue;
88
+ }
89
+
90
+ if (!satisfiesContentTargets(nodePath)) continue; // Not a content target
91
+
92
+ const parentId = isRootNode(parent)
93
+ ? ''
94
+ : parent.skip
95
+ ? parent.id.split('/').slice(0, -1).join('/')
96
+ : parent.id;
97
+
98
+ // Regular id might not include parent id part if it is skipped
99
+ const id = parentId ? `${parentId}/${pathParts.id}` : pathParts.id;
100
+
101
+ if (ids[id]) {
102
+ logger.warn(
103
+ `Nav node ${stress(id)} ID collision!\n\n`,
104
+ `This ID belongs to ${stress(ids[id], chalk.greenBright)} nav node.\n`,
105
+ `Nav node ${stress(nodePath, chalk.redBright)} tries to use the same ID!\n`,
106
+ 'Skipping nav node which causes collision!',
107
+ );
108
+ continue;
109
+ }
110
+
111
+ // Full id always includes parent id
112
+ const fullId = isRootNode(parent)
113
+ ? pathParts.id
114
+ : `${parent.fullId}/${pathParts.id}`;
115
+
116
+ const skip = pathParts.sep === '+';
117
+
118
+ const childNode: NavNode = {
119
+ type: pathParts.type,
120
+ path: nodePath,
121
+ id,
122
+ fullId,
123
+ skip,
124
+ };
125
+
126
+ if (!validNode(childNode)) continue;
127
+
128
+ ids[id] = nodePath;
129
+ newIds[id] = nodePath;
130
+
131
+ const scanResult = await scanChildNodes(
132
+ childNode,
133
+ insideBook || childNode.type === 'book',
134
+ );
135
+
136
+ if (
137
+ ['book', 'group'].includes(childNode.type) &&
138
+ !scanResult.children
139
+ ) {
140
+ delete ids[id];
141
+ delete newIds[id];
142
+ for (const childNewId of Object.keys(scanResult.newIds))
143
+ delete ids[childNewId];
144
+ continue;
145
+ }
146
+
147
+ if (childNode.type === 'book') {
148
+ ERUDIT_SERVER.NAV_BOOKS ||= {};
149
+ ERUDIT_SERVER.NAV_BOOKS[id] = childNode;
150
+ }
151
+
152
+ childNode.children = scanResult.children;
153
+ children.push(childNode);
154
+ }
155
+
156
+ return {
157
+ children: children.length > 0 ? children : undefined,
158
+ newIds,
159
+ };
160
+ }
161
+
162
+ function satisfiesContentTargets(nodePath: string): boolean {
163
+ if (contentTargets.length === 0) return true;
164
+
165
+ for (const target of contentTargets)
166
+ if (nodePath.startsWith(target) || target.search(nodePath) === 0)
167
+ return true;
168
+
169
+ return false;
170
+ }
171
+
172
+ function validNode(node: NavNode): boolean {
173
+ switch (node.type) {
174
+ case 'topic':
175
+ const partPaths = globSync(
176
+ PROJECT_DIR +
177
+ `/content/${node.path}` +
178
+ `/{${topicParts.join(',')}}.bi`,
179
+ );
180
+ if (partPaths.length === 0) return false; // Topic is empty
181
+ break;
182
+ case 'book':
183
+ if (node.skip) {
184
+ logger.warn(
185
+ `Books can't skip their ID part!\n`,
186
+ `Skipping ${stress(node.path)} nav node!`,
187
+ );
188
+ return false;
189
+ }
190
+ break;
191
+ }
192
+
193
+ return true;
194
+ }
195
+
196
+ function debugPrintNav(node: RootNavNode) {
197
+ const logNode = (node: NavNode | RootNavNode, indent: number) => {
198
+ console.log(
199
+ isRootNode(node)
200
+ ? chalk.dim('#root')
201
+ : `${' '.repeat(indent)}${node.id.split('/').pop()} ${chalk.dim(`[${node.type}${node.skip ? ', ' + chalk.yellow('skip') : ''}]`)}`,
202
+ );
203
+
204
+ if (node.children)
205
+ for (const child of node.children) logNode(child, indent + 1);
206
+ };
207
+
208
+ logNode(node, 0);
209
+ }
@@ -0,0 +1,25 @@
1
+ import { debug, logger } from '@server/logger';
2
+
3
+ import { setup } from './setup';
4
+ import { setupLanguage } from './jobs/language';
5
+ import { buildContributors } from './jobs/contributors';
6
+ import { buildNav } from './jobs/nav';
7
+ import { buildContent } from './jobs/content/generic';
8
+
9
+ let initial = true;
10
+
11
+ export async function build() {
12
+ debug.start('Building data...');
13
+
14
+ if (initial) {
15
+ await setup();
16
+ initial = false;
17
+ }
18
+
19
+ await setupLanguage();
20
+ await buildContributors();
21
+ await buildNav();
22
+ await buildContent();
23
+
24
+ logger.success('Build successful!');
25
+ }
@@ -0,0 +1,55 @@
1
+ import chokidar, { type FSWatcher } from 'chokidar';
2
+
3
+ import { ERUDIT_DIR, PROJECT_DIR } from '#erudit/globalPaths';
4
+ import { ERUDIT_SERVER } from '@server/global';
5
+ import { logger } from '@server/logger';
6
+ import { build } from '@server/build/process';
7
+ import { stress } from '@erudit/utils/stress';
8
+
9
+ const watchTargets: string[] = [
10
+ // Languages
11
+ ERUDIT_DIR + '/languages',
12
+ // Content directory
13
+ PROJECT_DIR + '/content',
14
+ // Contributors directory
15
+ PROJECT_DIR + '/contributors',
16
+ ];
17
+
18
+ const ignoreTargets: string[] = [];
19
+
20
+ const rebuildDelay = 300;
21
+
22
+ //
23
+ //
24
+ //
25
+
26
+ let watcher: FSWatcher;
27
+ let rebuildTimeout: any;
28
+ let rebuildRequested = false;
29
+
30
+ export async function setupRebuildWatcher() {
31
+ if (watcher) return;
32
+
33
+ watcher = chokidar.watch(watchTargets, {
34
+ ignored: ignoreTargets,
35
+ ignoreInitial: true,
36
+ });
37
+
38
+ watcher.on('all', (event, path) =>
39
+ requestServerRebuild(`File change: ${stress(path)}`),
40
+ );
41
+ }
42
+
43
+ export async function requestServerRebuild(reason?: string) {
44
+ if (rebuildRequested) return;
45
+
46
+ clearTimeout(rebuildTimeout);
47
+ rebuildTimeout = setTimeout(async () => {
48
+ rebuildRequested = true;
49
+ await ERUDIT_SERVER.BUILD_PROMISE;
50
+ console.log('\n');
51
+ logger.info(`Rebuilding server data!${reason ? ` ${reason}` : ''}\n\n`);
52
+ await build();
53
+ rebuildRequested = false;
54
+ }, rebuildDelay);
55
+ }
@@ -0,0 +1,21 @@
1
+ import eruditConfig from '#erudit/config';
2
+
3
+ import { registerGlobals } from '@erudit/globals/register';
4
+ import { debug } from '@server/logger';
5
+ import { ERUDIT_SERVER } from '@server/global';
6
+
7
+ import { setupRebuildWatcher } from './rebuild';
8
+ import { setupDatabase } from '../db/setup';
9
+ import { setupBitranConfig } from '../bitran/setup';
10
+
11
+ export async function setup() {
12
+ debug.start('Running initial setup procedures...');
13
+
14
+ if (import.meta.dev) await setupRebuildWatcher();
15
+
16
+ registerGlobals();
17
+ ERUDIT_SERVER.CONFIG = eruditConfig;
18
+
19
+ await setupBitranConfig();
20
+ await setupDatabase();
21
+ }
@@ -0,0 +1,94 @@
1
+ import { isTopicPart } from 'erudit-cog/schema';
2
+
3
+ import {
4
+ parseBitranLocation,
5
+ stringifyBitranLocation,
6
+ type BitranLocation,
7
+ } from '@shared/bitran/location';
8
+ import { getNavBookOf } from '../nav/utils';
9
+
10
+ export function toAbsoluteContentLocation<T extends string | BitranLocation>(
11
+ location: T,
12
+ contextContentId: string,
13
+ ): T {
14
+ const isLocationStr = typeof location === 'string';
15
+ const parsedLocation = isLocationStr
16
+ ? parseBitranLocation(location)
17
+ : (location as BitranLocation);
18
+
19
+ if (
20
+ isTopicPart(parsedLocation.type) ||
21
+ ['book', 'group', 'topic'].includes(parsedLocation.type)
22
+ ) {
23
+ parsedLocation.path = toAbsoluteContentId(
24
+ parsedLocation.path!,
25
+ contextContentId,
26
+ );
27
+ return (
28
+ isLocationStr
29
+ ? stringifyBitranLocation(parsedLocation)
30
+ : parsedLocation
31
+ ) as T;
32
+ }
33
+
34
+ return location;
35
+ }
36
+
37
+ export function toAbsoluteContentId(
38
+ path: string,
39
+ contextContentId: string = '',
40
+ ) {
41
+ const fullPath = (() => {
42
+ if (path.startsWith('..'))
43
+ return (contextContentId ? contextContentId + '/' : '') + path;
44
+
45
+ if (path.startsWith('.'))
46
+ return trimEdgeSlashes(contextContentId + path.substring(1));
47
+
48
+ if (path.startsWith('~'))
49
+ return trimEdgeSlashes(
50
+ tryGetParentBook(contextContentId) + path.substring(1),
51
+ );
52
+
53
+ return path;
54
+ })();
55
+
56
+ const reversePathParts = fullPath.split('/').reverse();
57
+ const finalParts: string[] = [];
58
+
59
+ let skipCounter = 0;
60
+
61
+ for (let i = 0; i < reversePathParts.length; i++) {
62
+ const part = reversePathParts[i];
63
+
64
+ if (part === '..') {
65
+ skipCounter++;
66
+ continue;
67
+ }
68
+
69
+ if (skipCounter) {
70
+ skipCounter--;
71
+ continue;
72
+ }
73
+
74
+ finalParts.unshift(part!);
75
+ }
76
+
77
+ return finalParts.join('/');
78
+ }
79
+
80
+ function trimEdgeSlashes(path: string) {
81
+ if (path.startsWith('/')) path = path.substring(1);
82
+
83
+ if (path.endsWith('/')) path = path.substring(0, path.length - 1);
84
+
85
+ return path;
86
+ }
87
+
88
+ function tryGetParentBook(contentId: string): string {
89
+ const navBook = getNavBookOf(contentId);
90
+
91
+ if (!navBook) return '';
92
+
93
+ return navBook.id;
94
+ }
@@ -0,0 +1,112 @@
1
+ import { isContentType, isTopicPart, type TopicPart } from 'erudit-cog/schema';
2
+
3
+ import { ERUDIT_SERVER } from '@server/global';
4
+ import { getIdsUp, isSkipId } from '@server/nav/utils';
5
+ import { DbContent } from '@server/db/entities/Content';
6
+ import { DbContributor } from '@server/db/entities/Contributor';
7
+
8
+ import type { Context } from '@shared/content/context';
9
+ import {
10
+ createContentLink,
11
+ createContributorLink,
12
+ createTopicPartLink,
13
+ } from '@shared/link';
14
+ import { CONTENT_TYPE_ICON, ICON, TOPIC_PART_ICON } from '@erudit/shared/icons';
15
+ import type { BitranLocation } from '@erudit/shared/bitran/location';
16
+
17
+ export async function getContentContext(contentId: string): Promise<Context> {
18
+ const context: Context = [];
19
+
20
+ for (const _contentId of (await getIdsUp(contentId)).reverse()) {
21
+ const dbContent = await getDbContent(_contentId);
22
+
23
+ context.push({
24
+ type: ERUDIT_SERVER.LANGUAGE.phrases[dbContent.type],
25
+ icon: isTopicPart(dbContent.type)
26
+ ? TOPIC_PART_ICON[dbContent.type]
27
+ : isContentType(dbContent.type)
28
+ ? CONTENT_TYPE_ICON[dbContent.type]
29
+ : '',
30
+ title: dbContent.title || dbContent.contentId,
31
+ href: createContentLink(dbContent.type, dbContent.contentId),
32
+ hidden: await isSkipId(dbContent.contentId),
33
+ });
34
+ }
35
+
36
+ return context;
37
+ }
38
+
39
+ export async function getTopicPartContext(
40
+ topicPart: TopicPart,
41
+ contentId: string,
42
+ ): Promise<Context> {
43
+ const context = await getContentContext(contentId);
44
+ const topicItem = context.pop()!;
45
+
46
+ topicItem.type = ERUDIT_SERVER.LANGUAGE.phrases[topicPart];
47
+ topicItem.icon = TOPIC_PART_ICON[topicPart];
48
+ topicItem.href = createTopicPartLink(topicPart, contentId);
49
+
50
+ return context.concat([topicItem]);
51
+ }
52
+
53
+ export async function getContributorContext(
54
+ contributorId: string,
55
+ ): Promise<Context> {
56
+ const dbContributor = await ERUDIT_SERVER.DB.manager.findOne(
57
+ DbContributor,
58
+ {
59
+ select: ['displayName'],
60
+ where: { contributorId },
61
+ },
62
+ );
63
+
64
+ if (!dbContributor)
65
+ throw new Error(`Contributor "${contributorId}" not found!`);
66
+
67
+ return [
68
+ {
69
+ type: ERUDIT_SERVER.LANGUAGE.phrases.contributor,
70
+ icon: ICON.contributor,
71
+ title: dbContributor.displayName || contributorId,
72
+ href: createContributorLink(contributorId),
73
+ hidden: false,
74
+ },
75
+ ];
76
+ }
77
+
78
+ export async function getLocationContext(
79
+ location: BitranLocation,
80
+ ): Promise<Context> {
81
+ const contentId = location.path!;
82
+
83
+ switch (location.type) {
84
+ case 'article':
85
+ case 'summary':
86
+ case 'practice':
87
+ return await getTopicPartContext(location.type, contentId);
88
+ case 'group':
89
+ return await getContentContext(contentId);
90
+ case 'contributor':
91
+ return await getContributorContext(contentId);
92
+ }
93
+
94
+ throw new Error(
95
+ `Cant' build context for location unknown type "${location.type}"!`,
96
+ );
97
+ }
98
+
99
+ //
100
+ //
101
+ //
102
+
103
+ async function getDbContent(contentId: string): Promise<DbContent> {
104
+ const dbContent = await ERUDIT_SERVER.DB.manager.findOne(DbContent, {
105
+ select: ['type', 'title', 'fullId', 'contentId'],
106
+ where: { contentId },
107
+ });
108
+
109
+ if (!dbContent) throw new Error(`Content "${contentId}" not found!`);
110
+
111
+ return dbContent;
112
+ }
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryColumn } from 'typeorm';
2
+
3
+ @Entity('book')
4
+ export class DbBook {
5
+ @PrimaryColumn('varchar')
6
+ contentId!: string;
7
+ }
@@ -0,0 +1,49 @@
1
+ import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
2
+ import type {
3
+ ContentFlag,
4
+ ContentReferences,
5
+ ContentSeo,
6
+ ContentType,
7
+ } from 'erudit-cog/schema';
8
+
9
+ import type { ImageData } from '@erudit/shared/image';
10
+
11
+ @Entity('content')
12
+ export class DbContent {
13
+ @PrimaryColumn('varchar')
14
+ contentId!: string;
15
+
16
+ @Column('varchar')
17
+ @Index({ unique: true })
18
+ fullId!: string;
19
+
20
+ @Column('varchar')
21
+ type!: ContentType;
22
+
23
+ @Column('varchar', { nullable: true })
24
+ title?: string;
25
+
26
+ @Column('varchar', { nullable: true })
27
+ navTitle?: string;
28
+
29
+ @Column('text', { nullable: true })
30
+ description?: string;
31
+
32
+ @Column('simple-json', { nullable: true })
33
+ flags?: Record<ContentFlag, boolean>;
34
+
35
+ @Column('varchar', { nullable: true })
36
+ decoration?: string;
37
+
38
+ @Column('simple-json', { nullable: true })
39
+ seo?: Partial<ContentSeo>;
40
+
41
+ @Column('simple-json', { nullable: true })
42
+ ogImage?: ImageData;
43
+
44
+ @Column('simple-array', { nullable: true })
45
+ dependencies?: string[];
46
+
47
+ @Column('simple-json', { nullable: true })
48
+ references?: ContentReferences;
49
+ }
@@ -0,0 +1,10 @@
1
+ import { Entity, PrimaryColumn } from 'typeorm';
2
+
3
+ @Entity('contribution')
4
+ export class DbContribution {
5
+ @PrimaryColumn('varchar')
6
+ contentId!: string;
7
+
8
+ @PrimaryColumn('varchar')
9
+ contributorId!: string;
10
+ }
@@ -0,0 +1,16 @@
1
+ import { Column, Entity, PrimaryColumn } from 'typeorm';
2
+
3
+ @Entity('contributor')
4
+ export class DbContributor {
5
+ @PrimaryColumn('varchar')
6
+ contributorId!: string;
7
+
8
+ @Column('varchar', { nullable: true })
9
+ displayName?: string;
10
+
11
+ @Column('text', { nullable: true })
12
+ description?: string;
13
+
14
+ @Column('varchar', { nullable: true })
15
+ avatar?: string;
16
+ }
@@ -0,0 +1,14 @@
1
+ import { Column, Entity, PrimaryColumn } from 'typeorm';
2
+ import type { GroupType } from 'erudit-cog/schema';
3
+
4
+ @Entity('group')
5
+ export class DbGroup {
6
+ @PrimaryColumn('varchar')
7
+ contentId!: string;
8
+
9
+ @Column('varchar')
10
+ type!: GroupType;
11
+
12
+ @Column('text', { nullable: true })
13
+ content?: string;
14
+ }
@@ -0,0 +1,15 @@
1
+ import { Column, Entity, PrimaryColumn } from 'typeorm';
2
+
3
+ /**
4
+ * TODO:
5
+ * Create hash of Bitran content of content items and use it to distinguish between content item version.
6
+ */
7
+
8
+ @Entity('hash')
9
+ export class DbHash {
10
+ @PrimaryColumn('varchar')
11
+ id!: string;
12
+
13
+ @Column('varchar')
14
+ hash!: string;
15
+ }
@@ -0,0 +1,20 @@
1
+ import { Column, Entity, PrimaryColumn } from 'typeorm';
2
+ import type { TopicPart } from 'erudit-cog/schema';
3
+
4
+ @Entity('topic')
5
+ export class DbTopic {
6
+ @PrimaryColumn('varchar')
7
+ contentId!: string;
8
+
9
+ @Column('simple-array')
10
+ parts!: TopicPart[];
11
+
12
+ @Column('text', { nullable: true })
13
+ article?: string;
14
+
15
+ @Column('text', { nullable: true })
16
+ summary?: string;
17
+
18
+ @Column('text', { nullable: true })
19
+ practice?: string;
20
+ }
@@ -0,0 +1,21 @@
1
+ import { Column, Entity, PrimaryColumn } from 'typeorm';
2
+
3
+ import type { BitranContext } from '@erudit/shared/bitran/context';
4
+
5
+ @Entity('unique')
6
+ export class DbUnique {
7
+ @PrimaryColumn('varchar')
8
+ location!: string;
9
+
10
+ @Column('varchar')
11
+ productName!: string;
12
+
13
+ @Column('text')
14
+ content!: string;
15
+
16
+ @Column('simple-json')
17
+ context!: BitranContext;
18
+
19
+ @Column('varchar', { nullable: true })
20
+ title?: string;
21
+ }