erudit 3.0.0-dev.10 → 3.0.0-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/SiteMain.vue +1 -3
- package/app/components/aside/major/panes/Pages.vue +1 -1
- package/app/components/aside/major/panes/other/ItemContent.vue +1 -1
- package/app/components/aside/major/panes/other/ItemGenerator.vue +1 -1
- package/app/components/aside/major/panes/other/ItemTheme.vue +1 -1
- package/app/components/bitran/BitranContent.vue +1 -1
- package/app/components/preview/display/Unique.vue +1 -1
- package/app/composables/bitran.ts +1 -1
- package/app/composables/bitranContent.ts +28 -4
- package/app/scripts/preview/build.ts +13 -6
- package/module/imports.ts +5 -0
- package/module/index.ts +2 -0
- package/module/problemGenerators.ts +46 -0
- package/nuxt.config.ts +6 -0
- package/package.json +7 -7
- package/server/plugin/bitran/content.ts +42 -48
- package/server/plugin/bitran/elements/include.ts +39 -30
- package/server/plugin/bitran/toc.ts +14 -0
- package/server/plugin/build/close.ts +3 -1
- package/server/plugin/build/jobs/content/generic.ts +2 -2
- package/server/plugin/build/process.ts +2 -0
- package/server/plugin/build/rebuild.ts +12 -1
- package/server/plugin/db/reset.ts +12 -0
- package/server/plugin/db/setup.ts +14 -1
- package/shared/bitran/stringContent.ts +2 -2
- package/utils/contentPath.ts +67 -0
- package/server/plugin/build/jobs/content/files.ts +0 -79
|
@@ -56,7 +56,7 @@ const isServer = import.meta.server;
|
|
|
56
56
|
<style lang="scss" module>
|
|
57
57
|
@use '$/def/bp';
|
|
58
58
|
|
|
59
|
-
.eruditBitranContainer {
|
|
59
|
+
:global(.bitran-component).eruditBitranContainer {
|
|
60
60
|
padding: var(--_pMainY) var(--_pMainX);
|
|
61
61
|
padding-left: calc(var(--_pMainX) - var(--_bitran_asideWidth));
|
|
62
62
|
|
|
@@ -21,7 +21,7 @@ const root = await bitranTranspiler.parser.parse(data.bitran.content.biCode);
|
|
|
21
21
|
<BitranContent
|
|
22
22
|
:content="{
|
|
23
23
|
root,
|
|
24
|
-
|
|
24
|
+
renderDataStorage: data.bitran.content.renderDataStorage,
|
|
25
25
|
}"
|
|
26
26
|
:context="data.bitran.context"
|
|
27
27
|
/>
|
|
@@ -88,7 +88,7 @@ export async function useBitranRenderers() {
|
|
|
88
88
|
|
|
89
89
|
export async function useBitranElementRenderer(productName: string) {
|
|
90
90
|
const renderer =
|
|
91
|
-
(await
|
|
91
|
+
(await getRenderers())[productName] ||
|
|
92
92
|
getDefaultBitranRenderers()[productName];
|
|
93
93
|
|
|
94
94
|
if (!renderer)
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
stringifyBitranLocation,
|
|
8
8
|
type BitranLocation,
|
|
9
9
|
} from '@erudit-js/cog/schema';
|
|
10
|
+
|
|
10
11
|
import type { StringBitranContent } from '@erudit/shared/bitran/stringContent';
|
|
11
12
|
|
|
12
13
|
import eruditConfig from '#erudit/config';
|
|
@@ -33,9 +34,32 @@ export async function useBitranContent(
|
|
|
33
34
|
|
|
34
35
|
// @ts-ignore
|
|
35
36
|
contentPromise = (async () => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const locationString = encodeBitranLocation(
|
|
38
|
+
stringifyBitranLocation(location.value!),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const payloadKey = `content`;
|
|
42
|
+
const payload =
|
|
43
|
+
(nuxtApp.static.data[payloadKey] ||=
|
|
44
|
+
nuxtApp.payload.data[payloadKey] ||=
|
|
45
|
+
{});
|
|
46
|
+
|
|
47
|
+
let stringContent: StringBitranContent;
|
|
48
|
+
|
|
49
|
+
if (payload.location === locationString) {
|
|
50
|
+
stringContent = payload;
|
|
51
|
+
} else {
|
|
52
|
+
stringContent = await $fetch(apiRoute, {
|
|
53
|
+
responseType: 'json',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Clear the payload and set new data
|
|
57
|
+
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
58
|
+
Object.assign(payload, {
|
|
59
|
+
...stringContent,
|
|
60
|
+
location: locationString,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
39
63
|
|
|
40
64
|
const bitranTranspiler = await useBitranTranspiler();
|
|
41
65
|
|
|
@@ -58,7 +82,7 @@ export async function useBitranContent(
|
|
|
58
82
|
|
|
59
83
|
content.value = {
|
|
60
84
|
root,
|
|
61
|
-
|
|
85
|
+
renderDataStorage: stringContent.renderDataStorage,
|
|
62
86
|
};
|
|
63
87
|
|
|
64
88
|
return content;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { PreviewDataType, type PreviewData } from './data';
|
|
2
2
|
import { PreviewRequestType, type PreviewRequest } from './request';
|
|
3
|
-
|
|
4
|
-
import { buildGenericLink } from './data/genericLink';
|
|
5
|
-
import { buildPageLink } from './data/pageLink';
|
|
6
|
-
import { buildUnique } from './data/unique';
|
|
7
|
-
import { createPreviewError } from './data/alert';
|
|
8
3
|
import { PreviewThemeName } from './state';
|
|
9
4
|
|
|
10
|
-
const builders = [
|
|
5
|
+
const builders = [
|
|
6
|
+
async (request: PreviewRequest) =>
|
|
7
|
+
(await import('./data/genericLink')).buildGenericLink(request),
|
|
8
|
+
async (request: PreviewRequest) =>
|
|
9
|
+
(await import('./data/pageLink')).buildPageLink(request),
|
|
10
|
+
async (request: PreviewRequest) =>
|
|
11
|
+
(await import('./data/unique')).buildUnique(request),
|
|
12
|
+
];
|
|
11
13
|
|
|
12
14
|
export async function buildPreviewData(
|
|
13
15
|
request: PreviewRequest,
|
|
@@ -24,6 +26,8 @@ export async function buildPreviewData(
|
|
|
24
26
|
'expected_page_hash',
|
|
25
27
|
);
|
|
26
28
|
|
|
29
|
+
const { createPreviewError } = await import('./data/alert');
|
|
30
|
+
|
|
27
31
|
return createPreviewError({
|
|
28
32
|
title: phrase.preview_missing_title,
|
|
29
33
|
message: phrase.preview_missing_explain_mismatch,
|
|
@@ -36,6 +40,8 @@ export async function buildPreviewData(
|
|
|
36
40
|
'element_id',
|
|
37
41
|
);
|
|
38
42
|
|
|
43
|
+
const { createPreviewError } = await import('./data/alert');
|
|
44
|
+
|
|
39
45
|
return createPreviewError({
|
|
40
46
|
title: phrase.preview_missing_title,
|
|
41
47
|
message: phrase.preview_missing_explain,
|
|
@@ -66,6 +72,7 @@ export async function buildPreviewData(
|
|
|
66
72
|
if (result) return result;
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
const { createPreviewError } = await import('./data/alert');
|
|
69
76
|
throw createPreviewError({
|
|
70
77
|
message: `Unable to build preview data for request!`,
|
|
71
78
|
pre: JSON.stringify(request, null, 4),
|
package/module/imports.ts
CHANGED
|
@@ -33,6 +33,11 @@ export function setupGlobalImports() {
|
|
|
33
33
|
from: eruditPath(`globals/content`),
|
|
34
34
|
}));
|
|
35
35
|
})(),
|
|
36
|
+
// Problems Generation
|
|
37
|
+
{
|
|
38
|
+
name: 'defineProblemGenerator',
|
|
39
|
+
from: '@erudit-js/bitran-elements/problem/generator',
|
|
40
|
+
},
|
|
36
41
|
// Helper Asset Path Functions
|
|
37
42
|
...(() => {
|
|
38
43
|
const imports = [
|
package/module/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { logger } from '@erudit/module/logger';
|
|
|
7
7
|
import { setupGlobalImports } from '@erudit/module/imports';
|
|
8
8
|
import { setupBitranTemplates } from './bitran';
|
|
9
9
|
import { setupGlobalPaths } from './paths';
|
|
10
|
+
import { setupProblemGenerators } from './problemGenerators';
|
|
10
11
|
|
|
11
12
|
export default defineNuxtModule({
|
|
12
13
|
meta: { name: 'Erudit', configKey: 'erudit' },
|
|
@@ -18,6 +19,7 @@ export default defineNuxtModule({
|
|
|
18
19
|
|
|
19
20
|
const eruditConfig = await setupEruditConfig(_nuxt);
|
|
20
21
|
await setupBitranTemplates(_nuxt);
|
|
22
|
+
await setupProblemGenerators(_nuxt);
|
|
21
23
|
setupGlobalPaths(_nuxt);
|
|
22
24
|
|
|
23
25
|
if (eruditConfig.site?.baseUrl) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { addTemplate } from 'nuxt/kit';
|
|
2
|
+
import type { Nuxt } from 'nuxt/schema';
|
|
3
|
+
import { globSync } from 'glob';
|
|
4
|
+
|
|
5
|
+
import { eruditEndNuxtPath, projectPath } from '@erudit/globalPath';
|
|
6
|
+
import { getContentPath } from '@erudit/utils/contentPath';
|
|
7
|
+
|
|
8
|
+
export async function setupProblemGenerators(_nuxt: Nuxt) {
|
|
9
|
+
const templateName = '#erudit/problemGenerators.ts';
|
|
10
|
+
|
|
11
|
+
addTemplate({
|
|
12
|
+
filename: templateName,
|
|
13
|
+
write: true,
|
|
14
|
+
async getContents() {
|
|
15
|
+
const generatorFiles = findGenerators();
|
|
16
|
+
|
|
17
|
+
return `
|
|
18
|
+
export const loaders = {
|
|
19
|
+
${generatorFiles
|
|
20
|
+
.map((file) => {
|
|
21
|
+
const filePath = projectPath('content/' + file);
|
|
22
|
+
const contentPath = getContentPath(
|
|
23
|
+
file,
|
|
24
|
+
projectPath('content'),
|
|
25
|
+
).replace(/\.gen\.(js|ts)$/, '');
|
|
26
|
+
return ` '${contentPath}': async () => (await import('${filePath}')).default,`;
|
|
27
|
+
})
|
|
28
|
+
.join('\n')}
|
|
29
|
+
}
|
|
30
|
+
`.trim();
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const alias = (_nuxt.options.alias ||= {});
|
|
35
|
+
alias['#erudit/problemGenerators'] = eruditEndNuxtPath(
|
|
36
|
+
`.nuxt/${templateName}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findGenerators() {
|
|
41
|
+
const generatorFiles = globSync('**/*.gen.{js,ts}', {
|
|
42
|
+
cwd: projectPath('content'),
|
|
43
|
+
posix: true,
|
|
44
|
+
});
|
|
45
|
+
return generatorFiles;
|
|
46
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -18,11 +18,17 @@ export default defineNuxtConfig({
|
|
|
18
18
|
'@shared': eruditPath('shared'),
|
|
19
19
|
'@app': eruditPath('app'),
|
|
20
20
|
$: eruditPath('app/styles'),
|
|
21
|
+
//
|
|
22
|
+
'#project': projectPath(),
|
|
23
|
+
'#content': projectPath('content'),
|
|
21
24
|
},
|
|
22
25
|
modules: [eruditPath('module'), 'nuxt-my-icons'],
|
|
23
26
|
myicons: {
|
|
24
27
|
iconsDir: eruditPath('app/assets/icons'),
|
|
25
28
|
},
|
|
29
|
+
features: {
|
|
30
|
+
inlineStyles: false,
|
|
31
|
+
},
|
|
26
32
|
typescript: {
|
|
27
33
|
tsConfig: {
|
|
28
34
|
include: [eruditPath('**/*'), projectPath('**/*')],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "erudit",
|
|
3
|
-
"version": "3.0.0-dev.
|
|
3
|
+
"version": "3.0.0-dev.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "🤓 CMS for perfect educational sites.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
"erudit": "bin/erudit.mjs"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@erudit-js/cog": "3.0.0-dev.
|
|
19
|
-
"@erudit-js/cli": "3.0.0-dev.
|
|
20
|
-
"@erudit-js/bitran-elements": "3.0.0-dev.
|
|
18
|
+
"@erudit-js/cog": "3.0.0-dev.11",
|
|
19
|
+
"@erudit-js/cli": "3.0.0-dev.11",
|
|
20
|
+
"@erudit-js/bitran-elements": "3.0.0-dev.11"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bitran-js/core": "1.0.0-dev.
|
|
24
|
-
"@bitran-js/renderer-vue": "1.0.0-dev.
|
|
25
|
-
"@bitran-js/transpiler": "1.0.0-dev.
|
|
23
|
+
"@bitran-js/core": "1.0.0-dev.12",
|
|
24
|
+
"@bitran-js/renderer-vue": "1.0.0-dev.12",
|
|
25
|
+
"@bitran-js/transpiler": "1.0.0-dev.12",
|
|
26
26
|
"@floating-ui/vue": "^1.1.6",
|
|
27
27
|
"chalk": "^5.4.1",
|
|
28
28
|
"chokidar": "^4.0.3",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { createPreRenderData, type PreRenderData } from '@bitran-js/transpiler';
|
|
2
1
|
import {
|
|
3
2
|
BlockErrorNode,
|
|
4
3
|
BlockNode,
|
|
5
4
|
BlocksNode,
|
|
5
|
+
createRenderData,
|
|
6
|
+
ElementNode,
|
|
6
7
|
InlinerErrorNode,
|
|
8
|
+
walkDown,
|
|
9
|
+
type RenderDataStorage,
|
|
7
10
|
} from '@bitran-js/core';
|
|
8
|
-
|
|
9
11
|
import {
|
|
10
12
|
isTopicPart,
|
|
11
13
|
mergeAliases,
|
|
@@ -26,11 +28,10 @@ import { DbTopic } from '@server/db/entities/Topic';
|
|
|
26
28
|
import { DbGroup } from '@server/db/entities/Group';
|
|
27
29
|
import { traverseInclude } from '@server/bitran/elements/include';
|
|
28
30
|
import { createBitranTranspiler } from '@server/bitran/transpiler';
|
|
31
|
+
import { logger } from '@server/logger';
|
|
29
32
|
|
|
30
33
|
import type { StringBitranContent } from '@shared/bitran/stringContent';
|
|
31
34
|
|
|
32
|
-
import { logger } from '@server/logger';
|
|
33
|
-
|
|
34
35
|
interface RawBitranContent {
|
|
35
36
|
context: BitranContext;
|
|
36
37
|
biCode: string;
|
|
@@ -122,63 +123,56 @@ async function createBitranContent(
|
|
|
122
123
|
setEruditBitranRuntime(item, runtime),
|
|
123
124
|
);
|
|
124
125
|
|
|
126
|
+
const renderDataStorage: RenderDataStorage = {};
|
|
127
|
+
|
|
128
|
+
async function makePreRender(node: ElementNode) {
|
|
129
|
+
if (generatePrerenderData === false) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const transpiler = bitranTranspiler.transpilers[node.name];
|
|
134
|
+
await createRenderData({
|
|
135
|
+
storage: renderDataStorage,
|
|
136
|
+
node,
|
|
137
|
+
extra: runtime,
|
|
138
|
+
generator: transpiler?.renderDataGenerator,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
const root = await bitranTranspiler.parser.parse(biCode, {
|
|
126
143
|
async step(node) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
switch (true) {
|
|
145
|
+
case node instanceof AliasesNode:
|
|
146
|
+
mergeAliases(context.aliases, node.parseData);
|
|
147
|
+
break;
|
|
148
|
+
case node instanceof IncludeNode:
|
|
149
|
+
await resolveInclude(node, context);
|
|
150
|
+
break;
|
|
151
|
+
case node instanceof BlockErrorNode:
|
|
152
|
+
case node instanceof InlinerErrorNode:
|
|
153
|
+
logParseError(node.name, node.error);
|
|
154
|
+
return;
|
|
130
155
|
}
|
|
131
156
|
|
|
132
|
-
|
|
133
|
-
await resolveInclude(node, context);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
157
|
+
await makePreRender(node);
|
|
136
158
|
|
|
137
|
-
if (
|
|
138
|
-
node
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
if (node instanceof IncludeNode) {
|
|
160
|
+
for (const block of node.parseData.blocks?.children || []) {
|
|
161
|
+
await walkDown(block, async (child) => {
|
|
162
|
+
if (child instanceof ElementNode) {
|
|
163
|
+
await makePreRender(child);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
143
167
|
}
|
|
144
168
|
},
|
|
145
169
|
});
|
|
146
170
|
|
|
147
171
|
const finalContent = await bitranTranspiler.stringifier.stringify(root);
|
|
148
172
|
|
|
149
|
-
// Building render data
|
|
150
|
-
const preRenderDataMap: Record<string, PreRenderData> = {};
|
|
151
|
-
|
|
152
|
-
if (generatePrerenderData) {
|
|
153
|
-
await bitranTranspiler.parser.parse(finalContent, {
|
|
154
|
-
step: async (node) => {
|
|
155
|
-
const id = node.autoId;
|
|
156
|
-
const elementTranspiler =
|
|
157
|
-
bitranTranspiler.transpilers[node.name]!;
|
|
158
|
-
|
|
159
|
-
if (id) {
|
|
160
|
-
const preRenderData = await createPreRenderData(
|
|
161
|
-
node,
|
|
162
|
-
elementTranspiler,
|
|
163
|
-
runtime,
|
|
164
|
-
);
|
|
165
|
-
if (preRenderData) preRenderDataMap[id] = preRenderData;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
node instanceof BlockErrorNode ||
|
|
170
|
-
node instanceof InlinerErrorNode
|
|
171
|
-
) {
|
|
172
|
-
logParseError(node.name, node.error);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
173
|
return {
|
|
180
174
|
biCode: finalContent,
|
|
181
|
-
|
|
175
|
+
renderDataStorage,
|
|
182
176
|
};
|
|
183
177
|
}
|
|
184
178
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { BlockNode, type ElementNode } from '@bitran-js/core';
|
|
2
2
|
import type { BitranTranspiler } from '@bitran-js/transpiler';
|
|
3
|
-
|
|
4
3
|
import {
|
|
5
4
|
parseBitranLocation,
|
|
6
5
|
parsePartialBitranLocation,
|
|
@@ -34,11 +33,6 @@ export type TraverseStepFn = (payload: {
|
|
|
34
33
|
|
|
35
34
|
export type TraverseLeaveFn = (payload: { _location: string }) => Promise<any>;
|
|
36
35
|
|
|
37
|
-
/**
|
|
38
|
-
* This operation is heavy as fuck.
|
|
39
|
-
* It consumes a lot of memory and is slow.
|
|
40
|
-
* Use as rarely as possible!
|
|
41
|
-
*/
|
|
42
36
|
export async function traverseInclude(
|
|
43
37
|
includeNode: IncludeNode,
|
|
44
38
|
context: BitranContext,
|
|
@@ -52,27 +46,32 @@ export async function traverseInclude(
|
|
|
52
46
|
parsePartialBitranLocation(includeNode.id, context.location),
|
|
53
47
|
);
|
|
54
48
|
|
|
49
|
+
// Always use absolute locations as keys for travelMap to avoid infinite loop bugs
|
|
50
|
+
const absEntryLocation = toAbsoluteLocation(
|
|
51
|
+
entryLocation,
|
|
52
|
+
context.location.path!,
|
|
53
|
+
getNavBookIds(),
|
|
54
|
+
);
|
|
55
|
+
|
|
55
56
|
const travelMap: Record<string, string | null> = {
|
|
56
|
-
|
|
57
|
-
[entryLocation]: null,
|
|
57
|
+
[absEntryLocation]: null,
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
try {
|
|
61
61
|
await _traverseStep(
|
|
62
62
|
includeNode,
|
|
63
|
-
|
|
63
|
+
absEntryLocation,
|
|
64
64
|
context.aliases,
|
|
65
65
|
listeners,
|
|
66
66
|
travelMap,
|
|
67
67
|
);
|
|
68
68
|
} catch (message) {
|
|
69
|
-
let finalMessage = `Include Traversal Error!\n\n${message}\n\n`;
|
|
69
|
+
let finalMessage = `Include Traversal Error!\n\n${message instanceof Error ? message.message : message}\n\n`;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
// Print the traversal path in the order of traversal
|
|
72
|
+
for (const location of Object.keys(travelMap)) {
|
|
73
|
+
const includeTarget = travelMap[location];
|
|
74
74
|
if (includeTarget === null) continue;
|
|
75
|
-
|
|
76
75
|
finalMessage += `at "${printIncludeTarget(includeTarget)}" in "${location}"\n`;
|
|
77
76
|
}
|
|
78
77
|
|
|
@@ -110,11 +109,16 @@ async function _traverseStep(
|
|
|
110
109
|
);
|
|
111
110
|
} catch (error) {
|
|
112
111
|
travelMap[location] = includeNode.parseData.location;
|
|
113
|
-
throw
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Failed to resolve include target location "${includeNode.parseData.location}" from "${location}": ${error instanceof Error ? error.message : error}`,
|
|
114
|
+
);
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Use only absolute locations as keys for infinite loop detection
|
|
116
118
|
if (includeTargetLocation in travelMap)
|
|
117
|
-
throw
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Include "${printIncludeTarget(includeNode.parseData.location)}" targets "${includeTargetLocation}" which creates infinite loop!`,
|
|
121
|
+
);
|
|
118
122
|
|
|
119
123
|
travelMap[location] = includeNode.parseData.location;
|
|
120
124
|
|
|
@@ -127,7 +131,9 @@ async function _traverseStep(
|
|
|
127
131
|
});
|
|
128
132
|
|
|
129
133
|
if (!dbUnique)
|
|
130
|
-
throw
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Include "${printIncludeTarget(includeNode.parseData.location)}" is targeting non-existing unique "${includeTargetLocation}"!`,
|
|
136
|
+
);
|
|
131
137
|
|
|
132
138
|
//
|
|
133
139
|
// Creating Bitran core within loaded unique's location context.
|
|
@@ -148,18 +154,19 @@ async function _traverseStep(
|
|
|
148
154
|
}),
|
|
149
155
|
);
|
|
150
156
|
|
|
151
|
-
listeners.enter
|
|
152
|
-
|
|
157
|
+
if (listeners.enter) {
|
|
158
|
+
await listeners.enter({
|
|
153
159
|
_biCode: dbUnique.content,
|
|
154
160
|
_location: includeTargetLocation,
|
|
155
161
|
_bitranTranspiler: bitranTranspiler,
|
|
156
|
-
})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
157
164
|
|
|
158
165
|
//
|
|
159
166
|
// Parsing unique content and sub-traversing all includes if any.
|
|
160
167
|
//
|
|
161
168
|
|
|
162
|
-
let stepErrorMessage: string | undefined;
|
|
169
|
+
let stepErrorMessage: string | Error | undefined;
|
|
163
170
|
|
|
164
171
|
await bitranTranspiler.parser.parse(dbUnique.content, {
|
|
165
172
|
step: async (node) => {
|
|
@@ -175,13 +182,13 @@ async function _traverseStep(
|
|
|
175
182
|
.join('__');
|
|
176
183
|
|
|
177
184
|
if (!(node instanceof IncludeNode)) {
|
|
178
|
-
listeners.step
|
|
179
|
-
|
|
185
|
+
if (listeners.step) {
|
|
186
|
+
await listeners.step({
|
|
180
187
|
_location: includeTargetLocation,
|
|
181
188
|
_node: node,
|
|
182
189
|
_bitranTranspiler: bitranTranspiler,
|
|
183
|
-
})
|
|
184
|
-
|
|
190
|
+
});
|
|
191
|
+
}
|
|
185
192
|
return;
|
|
186
193
|
}
|
|
187
194
|
|
|
@@ -193,8 +200,9 @@ async function _traverseStep(
|
|
|
193
200
|
listeners,
|
|
194
201
|
travelMap,
|
|
195
202
|
);
|
|
196
|
-
} catch (
|
|
197
|
-
stepErrorMessage =
|
|
203
|
+
} catch (err: any) {
|
|
204
|
+
stepErrorMessage =
|
|
205
|
+
err instanceof Error ? err : new Error(String(err));
|
|
198
206
|
}
|
|
199
207
|
},
|
|
200
208
|
});
|
|
@@ -205,10 +213,11 @@ async function _traverseStep(
|
|
|
205
213
|
// Leaving from current Include
|
|
206
214
|
//
|
|
207
215
|
|
|
208
|
-
listeners.leave
|
|
209
|
-
|
|
216
|
+
if (listeners.leave) {
|
|
217
|
+
await listeners.leave({
|
|
210
218
|
_location: includeTargetLocation,
|
|
211
|
-
})
|
|
219
|
+
});
|
|
220
|
+
}
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
function printIncludeTarget(target: string) {
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
headingName,
|
|
5
5
|
HeadingNode,
|
|
6
6
|
} from '@erudit-js/bitran-elements/heading/shared';
|
|
7
|
+
import {
|
|
8
|
+
ProblemNode,
|
|
9
|
+
ProblemsNode,
|
|
10
|
+
} from '@erudit-js/bitran-elements/problem/shared';
|
|
7
11
|
|
|
8
12
|
import type { Toc } from '@erudit/shared/bitran/toc';
|
|
9
13
|
import { ERUDIT_SERVER } from '@server/global';
|
|
@@ -58,6 +62,16 @@ export async function getBitranToc(location: BitranLocation) {
|
|
|
58
62
|
return 0;
|
|
59
63
|
};
|
|
60
64
|
|
|
65
|
+
// Problems by default are in TOC
|
|
66
|
+
if (node instanceof ProblemsNode || node instanceof ProblemNode) {
|
|
67
|
+
toc.push({
|
|
68
|
+
...tocItemBase,
|
|
69
|
+
level: notHeadingLevel(),
|
|
70
|
+
title: node.meta?.title || node.parseData.info.title,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
if (
|
|
62
76
|
ERUDIT_SERVER.CONFIG.bitran?.toc?.includes(node.name) ||
|
|
63
77
|
node.meta?.toc
|
|
@@ -4,7 +4,9 @@ import { ERUDIT_SERVER } from '@server/global';
|
|
|
4
4
|
export async function close() {
|
|
5
5
|
debug.start('Shutting down server...');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
if (ERUDIT_SERVER.DB && ERUDIT_SERVER.DB.isInitialized) {
|
|
8
|
+
await ERUDIT_SERVER.DB.destroy();
|
|
9
|
+
}
|
|
8
10
|
|
|
9
11
|
logger.success('Server shut down gracefully!');
|
|
10
12
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { resolvePaths } from '@erudit-js/cog/kit';
|
|
12
12
|
|
|
13
13
|
import { PROJECT_DIR } from '#erudit/globalPaths';
|
|
14
|
+
import { scanContentPaths } from '@erudit/utils/contentPath';
|
|
14
15
|
import { stress } from '@erudit/utils/stress';
|
|
15
16
|
|
|
16
17
|
import { debug, logger } from '@server/logger';
|
|
@@ -31,7 +32,6 @@ import { contentItemPath } from './path';
|
|
|
31
32
|
import { buildBook } from './type/book';
|
|
32
33
|
import { buildGroup } from './type/group';
|
|
33
34
|
import { buildTopic } from './type/topic';
|
|
34
|
-
import { scanFiles } from './files';
|
|
35
35
|
|
|
36
36
|
const typeBuilders: Record<ContentType, Function> = {
|
|
37
37
|
book: buildBook,
|
|
@@ -69,7 +69,7 @@ export async function buildContent() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
async function scanContentFiles() {
|
|
72
|
-
const contentFiles =
|
|
72
|
+
const contentFiles = scanContentPaths(PROJECT_DIR + '/content');
|
|
73
73
|
for (const [path, fullPath] of Object.entries(contentFiles)) {
|
|
74
74
|
const dbFile = new DbFile();
|
|
75
75
|
dbFile.path = path;
|
|
@@ -5,6 +5,7 @@ import { setupLanguage } from './jobs/language';
|
|
|
5
5
|
import { buildContributors } from './jobs/contributors';
|
|
6
6
|
import { buildNav } from './jobs/nav';
|
|
7
7
|
import { buildContent } from './jobs/content/generic';
|
|
8
|
+
import { resetDatabase } from '../db/reset';
|
|
8
9
|
|
|
9
10
|
let initial = true;
|
|
10
11
|
|
|
@@ -16,6 +17,7 @@ export async function build() {
|
|
|
16
17
|
initial = false;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
await resetDatabase();
|
|
19
21
|
await setupLanguage();
|
|
20
22
|
await buildContributors();
|
|
21
23
|
await buildNav();
|
|
@@ -26,6 +26,7 @@ const rebuildDelay = 300;
|
|
|
26
26
|
let watcher: FSWatcher;
|
|
27
27
|
let rebuildTimeout: any;
|
|
28
28
|
let rebuildRequested = false;
|
|
29
|
+
let pendingRebuild = false;
|
|
29
30
|
|
|
30
31
|
export async function setupRebuildWatcher() {
|
|
31
32
|
if (watcher) return;
|
|
@@ -41,7 +42,11 @@ export async function setupRebuildWatcher() {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export async function requestServerRebuild(reason?: string) {
|
|
44
|
-
|
|
45
|
+
// If rebuild is in progress, set flag for another rebuild after completion
|
|
46
|
+
if (rebuildRequested) {
|
|
47
|
+
pendingRebuild = true;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
45
50
|
|
|
46
51
|
clearTimeout(rebuildTimeout);
|
|
47
52
|
rebuildTimeout = setTimeout(async () => {
|
|
@@ -51,5 +56,11 @@ export async function requestServerRebuild(reason?: string) {
|
|
|
51
56
|
logger.info(`Rebuilding server data!${reason ? ` ${reason}` : ''}\n\n`);
|
|
52
57
|
await build();
|
|
53
58
|
rebuildRequested = false;
|
|
59
|
+
|
|
60
|
+
// If changes were detected during rebuild, start another rebuild
|
|
61
|
+
if (pendingRebuild) {
|
|
62
|
+
pendingRebuild = false;
|
|
63
|
+
requestServerRebuild('Changes detected during previous rebuild');
|
|
64
|
+
}
|
|
54
65
|
}, rebuildDelay);
|
|
55
66
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
1
2
|
import { DataSource } from 'typeorm';
|
|
2
3
|
|
|
3
4
|
import { PROJECT_DIR } from '#erudit/globalPaths';
|
|
@@ -13,12 +14,17 @@ import { DbHash } from './entities/Hash';
|
|
|
13
14
|
import { DbTopic } from './entities/Topic';
|
|
14
15
|
import { DbUnique } from './entities/Unique';
|
|
15
16
|
import { DbFile } from './entities/File';
|
|
17
|
+
import { logger } from '../logger';
|
|
16
18
|
|
|
17
19
|
export async function setupDatabase() {
|
|
20
|
+
rmSync(PROJECT_DIR + '/.erudit/data.sqlite', { force: true });
|
|
21
|
+
|
|
18
22
|
ERUDIT_SERVER.DB = new DataSource({
|
|
19
23
|
type: 'sqlite',
|
|
24
|
+
enableWAL: true,
|
|
20
25
|
database: PROJECT_DIR + '/.erudit/data.sqlite',
|
|
21
26
|
synchronize: true,
|
|
27
|
+
dropSchema: true,
|
|
22
28
|
entities: [
|
|
23
29
|
DbBook,
|
|
24
30
|
DbContent,
|
|
@@ -32,5 +38,12 @@ export async function setupDatabase() {
|
|
|
32
38
|
],
|
|
33
39
|
});
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
try {
|
|
42
|
+
// Wait before creating connection in case fast server restarts
|
|
43
|
+
// Otherwise, it may crash the whole Node process
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
45
|
+
await ERUDIT_SERVER.DB.initialize();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error('Error creating database connection:', error);
|
|
48
|
+
}
|
|
36
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RenderDataStorage } from '@bitran-js/core';
|
|
2
2
|
|
|
3
3
|
export interface StringBitranContent {
|
|
4
4
|
biCode: string;
|
|
5
|
-
|
|
5
|
+
renderDataStorage: RenderDataStorage;
|
|
6
6
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
// A directory is considered a "content directory" if it contains book, group, or topic with a file having a .js or .ts extension.
|
|
4
|
+
// When a directory qualifies as a content directory, its name is simplified by removing any leading digits and a '+' or '-' (e.g., "29+foo" becomes "foo").
|
|
5
|
+
|
|
6
|
+
const contentFileRegex = /^(book|group|topic)\.(js|ts)$/i;
|
|
7
|
+
|
|
8
|
+
function normalizePath(path: string): string {
|
|
9
|
+
return path.replace(/\\/g, '/');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isContentDirectory(directory: string): boolean {
|
|
13
|
+
if (!statSync(directory).isDirectory()) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return readdirSync(directory).some((file) => {
|
|
18
|
+
return contentFileRegex.test(file);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getContentPath(path: string, cwd?: string): string {
|
|
23
|
+
path = normalizePath(path);
|
|
24
|
+
cwd = cwd ? normalizePath(cwd) : undefined;
|
|
25
|
+
|
|
26
|
+
const unresolvedParts = path.split('/').filter(Boolean);
|
|
27
|
+
const resolvedParts: string[] = [];
|
|
28
|
+
|
|
29
|
+
let currentPath = cwd || '';
|
|
30
|
+
while (unresolvedParts.length) {
|
|
31
|
+
const currentPart = unresolvedParts.shift()!;
|
|
32
|
+
currentPath += '/' + currentPart;
|
|
33
|
+
if (isContentDirectory(currentPath)) {
|
|
34
|
+
resolvedParts.push(currentPart.replace(/^\d+(?:\+|-)/, ''));
|
|
35
|
+
} else {
|
|
36
|
+
resolvedParts.push(currentPart);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return resolvedParts.join('/');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function scanContentPaths(directory: string): Record<string, string> {
|
|
44
|
+
directory = normalizePath(directory);
|
|
45
|
+
|
|
46
|
+
const map: Record<string, string> = {};
|
|
47
|
+
|
|
48
|
+
function traverse(currentPath: string) {
|
|
49
|
+
const files = readdirSync(currentPath);
|
|
50
|
+
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const fullPath = currentPath + '/' + file;
|
|
53
|
+
const relativePath = fullPath.slice(directory.length + 1);
|
|
54
|
+
const stat = statSync(fullPath);
|
|
55
|
+
|
|
56
|
+
if (stat.isDirectory()) {
|
|
57
|
+
traverse(fullPath);
|
|
58
|
+
} else {
|
|
59
|
+
map[getContentPath(relativePath, directory)] = relativePath;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
traverse(directory);
|
|
65
|
+
|
|
66
|
+
return map;
|
|
67
|
+
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
|
|
4
|
-
interface FileMap {
|
|
5
|
-
[simplifiedPath: string]: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// Regex to check if a directory is a "content directory".
|
|
9
|
-
const contentFileRegex = /^(book|group|topic)\.(js|ts)$/i;
|
|
10
|
-
|
|
11
|
-
// Regex to simplify directory names (remove leading digits + '+' or '-').
|
|
12
|
-
const simplifyDirRegex = /^\d+(?:\+|-)/;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Recursively scans files starting from `dirPath` and builds a mapping where:
|
|
16
|
-
* - The key is the "simplified" path using forward slashes.
|
|
17
|
-
* - The value is the relative filesystem path, also using forward slashes.
|
|
18
|
-
*
|
|
19
|
-
* A directory is considered a "content directory" if it contains any file that matches:
|
|
20
|
-
* book, group, or topic with a .js or .ts extension.
|
|
21
|
-
* When a directory qualifies as a content directory, its name is simplified
|
|
22
|
-
* by removing any leading digits and a '+' or '-' (e.g., "29+foo" becomes "foo").
|
|
23
|
-
*
|
|
24
|
-
* @param dirPath The root directory to scan.
|
|
25
|
-
* @returns An object mapping simplified file paths to relative file paths.
|
|
26
|
-
*/
|
|
27
|
-
export function scanFiles(dirPath: string): FileMap {
|
|
28
|
-
const result: FileMap = {};
|
|
29
|
-
const absoluteRoot = path.resolve(dirPath); // Ensure absolute root path
|
|
30
|
-
|
|
31
|
-
function isContentDirectory(directory: string): boolean {
|
|
32
|
-
try {
|
|
33
|
-
return fs
|
|
34
|
-
.readdirSync(directory)
|
|
35
|
-
.some((entry) => contentFileRegex.test(entry));
|
|
36
|
-
} catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function traverse(currentPath: string, simplifiedParts: string[]) {
|
|
42
|
-
let currentSimplifiedParts = simplifiedParts;
|
|
43
|
-
if (currentPath !== absoluteRoot) {
|
|
44
|
-
let dirName = path.basename(currentPath);
|
|
45
|
-
if (isContentDirectory(currentPath)) {
|
|
46
|
-
dirName = dirName.replace(simplifyDirRegex, '');
|
|
47
|
-
}
|
|
48
|
-
currentSimplifiedParts = [...simplifiedParts, dirName];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const entry of fs.readdirSync(currentPath)) {
|
|
52
|
-
const fullPath = path.join(currentPath, entry);
|
|
53
|
-
const relativePath = path
|
|
54
|
-
.relative(absoluteRoot, fullPath)
|
|
55
|
-
.replace(/\\/g, '/'); // Always forward slashes
|
|
56
|
-
const stat = fs.statSync(fullPath);
|
|
57
|
-
|
|
58
|
-
if (stat.isDirectory()) {
|
|
59
|
-
traverse(fullPath, currentSimplifiedParts);
|
|
60
|
-
} else if (stat.isFile()) {
|
|
61
|
-
const simplifiedFilePath = path.posix.join(
|
|
62
|
-
...currentSimplifiedParts,
|
|
63
|
-
entry,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
// Remove first repeating part in the key
|
|
67
|
-
const keyParts = simplifiedFilePath.split('/');
|
|
68
|
-
if (keyParts.length > 1 && keyParts[0] === keyParts[1]) {
|
|
69
|
-
keyParts.shift(); // Remove first repeating part
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
result[keyParts.join('/')] = relativePath;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
traverse(absoluteRoot, []);
|
|
78
|
-
return result;
|
|
79
|
-
}
|