erudit 4.1.0 → 4.2.0-dev.1

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 (118) hide show
  1. package/app/assets/icons/update.svg +3 -0
  2. package/app/components/Prose.vue +7 -7
  3. package/app/components/SmartMedia.vue +4 -4
  4. package/app/components/aside/major/contentNav/PaneBookNav.vue +1 -4
  5. package/app/components/aside/minor/content/Toc.vue +24 -1
  6. package/app/components/aside/minor/content/TocItem.vue +2 -1
  7. package/app/components/aside/minor/news/AsideMinorNews.vue +1 -3
  8. package/app/components/aside/minor/news/NewsItem.vue +3 -4
  9. package/app/components/aside/minor/news/RenderNewsElement.vue +4 -11
  10. package/app/components/aside/minor/news/elements/Mix.vue +2 -3
  11. package/app/components/aside/minor/news/elements/P.vue +3 -3
  12. package/app/components/aside/minor/news/elements/Ref.vue +3 -3
  13. package/app/components/aside/minor/news/elements/Text.vue +2 -2
  14. package/app/components/main/MainContentChild.vue +3 -3
  15. package/app/components/main/{MainQuickLink.vue → MainKeyLink.vue} +11 -11
  16. package/app/components/main/{MainQuickLinks.vue → MainKeyLinks.vue} +7 -7
  17. package/app/components/main/MainQuoteLoader.vue +2 -4
  18. package/app/components/main/MainStickyHeader.vue +1 -1
  19. package/app/components/main/MainStickyHeaderPreamble.vue +6 -2
  20. package/app/components/main/MainTopicPartPage.vue +8 -3
  21. package/app/components/main/connections/DepUnique.vue +45 -0
  22. package/app/components/main/connections/Deps.vue +15 -5
  23. package/app/components/main/connections/Externals.vue +4 -4
  24. package/app/components/main/connections/MainConnections.vue +1 -0
  25. package/app/components/main/contentStats/ItemLastChanged.vue +68 -0
  26. package/app/components/main/contentStats/MainContentStats.vue +36 -28
  27. package/app/components/preview/PreviewScreen.vue +2 -2
  28. package/app/components/preview/screen/ContentPage.vue +1 -4
  29. package/app/components/preview/screen/Unique.vue +3 -5
  30. package/app/composables/appElements.ts +2 -4
  31. package/app/composables/asideMajorPane.ts +3 -3
  32. package/app/composables/fetchJson.ts +4 -0
  33. package/app/composables/lastChanged.ts +28 -0
  34. package/app/composables/mainContent.ts +1 -4
  35. package/app/composables/og.ts +43 -35
  36. package/app/composables/phrases.ts +0 -3
  37. package/app/composables/scrollUp.ts +1 -1
  38. package/app/pages/book/[...bookId].vue +5 -1
  39. package/app/pages/contributor/[contributorId].vue +3 -5
  40. package/app/pages/contributors.vue +1 -1
  41. package/app/pages/group/[...groupId].vue +5 -1
  42. package/app/pages/index.vue +1 -1
  43. package/app/pages/page/[...pageId].vue +8 -3
  44. package/app/pages/sponsors.vue +1 -1
  45. package/app/plugins/appSetup/index.ts +0 -5
  46. package/app/plugins/fetchJson.ts +11 -0
  47. package/app/plugins/prerender.server.ts +1 -1
  48. package/app/router.options.ts +1 -1
  49. package/modules/erudit/globals/prose.ts +3 -4
  50. package/modules/erudit/setup/elements/appTemplate.ts +6 -7
  51. package/modules/erudit/setup/elements/{globalTypes.ts → elementGlobalTypes.ts} +21 -21
  52. package/modules/erudit/setup/elements/globalTemplate.ts +29 -23
  53. package/modules/erudit/setup/elements/setup.ts +18 -16
  54. package/modules/erudit/setup/elements/shared.ts +2 -2
  55. package/modules/erudit/setup/elements/tagsTable.ts +1 -1
  56. package/modules/erudit/setup/runtimeConfig.ts +2 -0
  57. package/nuxt.config.ts +2 -2
  58. package/package.json +14 -13
  59. package/server/api/main/content/[...contentTypePath].ts +5 -4
  60. package/server/api/prerender/content.ts +1 -3
  61. package/server/api/preview/contentPage/[...contentTypePath].ts +1 -2
  62. package/server/api/preview/contentUnique/[...contentTypePathUnique].ts +16 -31
  63. package/server/api/problemScript/[...problemScriptPath].ts +73 -4
  64. package/server/erudit/content/global/build.ts +21 -7
  65. package/server/erudit/content/nav/build.ts +4 -4
  66. package/server/erudit/content/repository/children.ts +3 -3
  67. package/server/erudit/content/repository/deps.ts +127 -39
  68. package/server/erudit/content/repository/elementSnippets.ts +16 -16
  69. package/server/erudit/content/repository/stats.ts +30 -22
  70. package/server/erudit/content/repository/topicParts.ts +1 -1
  71. package/server/erudit/content/repository/unique.ts +14 -15
  72. package/server/erudit/content/resolve/index.ts +6 -1
  73. package/server/erudit/content/resolve/page.ts +15 -35
  74. package/server/erudit/content/resolve/topic.ts +33 -164
  75. package/server/erudit/content/resolve/utils/insertContentItem.ts +2 -2
  76. package/server/erudit/content/resolve/utils/insertContentResolved.ts +82 -31
  77. package/server/erudit/content/search.ts +5 -22
  78. package/server/erudit/contributors/build.ts +7 -8
  79. package/server/erudit/db/repository/pushFile.ts +10 -3
  80. package/server/erudit/db/repository/pushProblemScript.ts +14 -3
  81. package/server/erudit/db/schema/contentDeps.ts +3 -0
  82. package/server/erudit/db/schema/contentSnippets.ts +3 -3
  83. package/server/erudit/db/schema/contentUniques.ts +2 -2
  84. package/server/erudit/db/schema/contributors.ts +2 -2
  85. package/server/erudit/db/schema/news.ts +2 -2
  86. package/server/erudit/db/schema/pages.ts +2 -2
  87. package/server/erudit/db/schema/topics.ts +4 -4
  88. package/server/erudit/global.ts +4 -0
  89. package/server/erudit/importer.ts +16 -8
  90. package/server/erudit/index.ts +0 -3
  91. package/server/erudit/language/list/en.ts +1 -0
  92. package/server/erudit/language/list/ru.ts +1 -0
  93. package/server/erudit/news/build.ts +6 -6
  94. package/server/erudit/news/repository/batch.ts +2 -2
  95. package/server/erudit/prose/repository/finalize.ts +22 -25
  96. package/server/erudit/prose/repository/get.ts +3 -5
  97. package/server/erudit/prose/repository/rawToProse.ts +31 -0
  98. package/server/erudit/prose/storage/callout.ts +9 -7
  99. package/server/erudit/prose/storage/image.ts +8 -11
  100. package/server/erudit/prose/storage/link.ts +24 -32
  101. package/server/erudit/prose/storage/problemScript.ts +8 -14
  102. package/server/erudit/prose/storage/video.ts +9 -7
  103. package/server/erudit/repository.ts +4 -4
  104. package/server/routes/file/[...path].ts +1 -1
  105. package/shared/types/contentChildren.ts +5 -2
  106. package/shared/types/contentConnections.ts +9 -0
  107. package/shared/types/elementSnippet.ts +1 -1
  108. package/shared/types/indexPage.ts +3 -0
  109. package/shared/types/language.ts +1 -83
  110. package/shared/types/mainContent.ts +11 -5
  111. package/shared/types/news.ts +2 -2
  112. package/shared/types/preview.ts +3 -2
  113. package/shared/types/runtimeConfig.ts +1 -0
  114. package/shared/types/search.ts +2 -0
  115. package/shared/utils/pages.ts +4 -2
  116. package/shared/utils/stringColor.ts +16 -6
  117. package/server/erudit/prose/repository/resolve.ts +0 -17
  118. package/server/erudit/prose/transform/bundleProblemScript.ts +0 -6
@@ -22,9 +22,8 @@ export function createGlobalTemplate(nuxt: Nuxt, elementsData: ElementData[]) {
22
22
  }
23
23
 
24
24
  const template = `
25
- import { PROSE_REGISTRY } from '@jsprose/core';
26
- import { jsx, jsxs, Fragment } from '@jsprose/core/jsx-runtime';
27
- import type { EruditProseCoreElement } from '@erudit-js/prose';
25
+ import { jsx, jsxs, Fragment } from 'tsprose/jsx-runtime';
26
+ import type { ProseCoreElements } from '@erudit-js/prose';
28
27
  import { defineProblemScript } from '@erudit-js/prose/elements/problem/problemScript';
29
28
 
30
29
  ${Object.entries(cores)
@@ -41,32 +40,39 @@ ${Object.entries(globals)
41
40
  )
42
41
  .join('\n')}
43
42
 
44
- const coreElements: EruditProseCoreElement[] = [
45
- ${Object.keys(cores).join(',\n ')}
46
- ].flatMap(element => (Array.isArray(element) ? element : [element]) as any);
43
+ export const coreElements: ProseCoreElements = Object.fromEntries([
44
+ ${Object.keys(cores).join(',\n ')}
45
+ ]
46
+ .flatMap((element: any) => (Array.isArray(element) ? element : [element]))
47
+ .map((element: any) => [element.schema.name, element])
48
+ );
47
49
 
48
- const elementsGlobals = {
49
- ${Object.keys(globals)
50
- .map((key) => `...${key}`)
51
- .join(',\n ')}
50
+ export const elementsGlobals = {
51
+ ${Object.keys(globals)
52
+ .map((key) => `...${key}`)
53
+ .join(',\n ')}
52
54
  }
53
55
 
54
56
  export function registerProseGlobals() {
55
- for (const element of coreElements) {
56
- PROSE_REGISTRY.addItem(element.registryItem);
57
- Object.assign(globalThis, element.registryItem.tags || {});
57
+ for (const coreElement of Object.values(coreElements)) {
58
+ const tags = coreElement.tags || [];
59
+ for (const tag of tags) {
60
+ Object.assign(globalThis, {
61
+ [tag.tagName]: tag,
62
+ });
58
63
  }
64
+ }
59
65
 
60
- Object.assign(globalThis, {
61
- // Make jsx runtime globally available (for prose generation in isolated modules like problem scripts)
62
- jsx,
63
- jsxs,
64
- Fragment,
65
- // Problem globals
66
- defineProblemScript,
67
- // Elements globals
68
- ...elementsGlobals
69
- });
66
+ Object.assign(globalThis, {
67
+ // Make jsx runtime globally available (for prose generation in isolated modules like problem scripts)
68
+ jsx,
69
+ jsxs,
70
+ Fragment,
71
+ // Problem globals
72
+ defineProblemScript,
73
+ // Elements globals
74
+ ...elementsGlobals
75
+ });
70
76
  }
71
77
  `.trim();
72
78
 
@@ -1,13 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import type { Nuxt } from 'nuxt/schema';
3
3
  import { findPath } from 'nuxt/kit';
4
- import type { EruditProseCoreElement } from '@erudit-js/prose';
4
+
5
+ import type { ProseCoreElement } from '@erudit-js/prose';
5
6
 
6
7
  import type { EruditRuntimeConfig } from '../../../../shared/types/runtimeConfig';
7
8
  import { moduleLogger } from '../../logger';
8
9
  import type { ElementData } from './shared';
9
10
  import { createTagsTable } from './tagsTable';
10
- import { createGlobalTypes } from './globalTypes';
11
+ import { createElementGlobalTypes } from './elementGlobalTypes';
11
12
  import { createGlobalTemplate } from './globalTemplate';
12
13
  import { createAppTemplate } from './appTemplate';
13
14
  import { PROJECT_PATH } from '../../env';
@@ -60,7 +61,7 @@ export async function setupProseElements(
60
61
 
61
62
  const elementData: ElementData = {
62
63
  name: uniqueElementName,
63
- registryItems: [],
64
+ coreElements: [],
64
65
  absDirectory: '',
65
66
  absCorePath: '',
66
67
  absAppPath: undefined,
@@ -91,7 +92,7 @@ export async function setupProseElements(
91
92
  elementData.absAppPath = appAbsPath;
92
93
  }
93
94
 
94
- const coreDefault: EruditProseCoreElement | EruditProseCoreElement[] = (
95
+ const coreDefault: ProseCoreElement | ProseCoreElement[] = (
95
96
  await import(coreAbsPath)
96
97
  ).default;
97
98
 
@@ -100,7 +101,7 @@ export async function setupProseElements(
100
101
  : [coreDefault];
101
102
 
102
103
  for (const coreElement of coreElements) {
103
- const schemaName = coreElement.registryItem.schema.name;
104
+ const schemaName = coreElement.schema.name;
104
105
 
105
106
  if (seenSchemas.has(schemaName)) {
106
107
  throw new Error(
@@ -109,23 +110,24 @@ export async function setupProseElements(
109
110
  }
110
111
 
111
112
  seenSchemas.add(schemaName);
112
- elementData.registryItems.push({
113
+ elementData.coreElements.push({
113
114
  schemaName,
114
- tagNames: [],
115
+ tags: {},
115
116
  });
116
117
 
117
- if (coreElement.registryItem.tags) {
118
- for (const tagName of Object.keys(coreElement.registryItem.tags)) {
119
- if (seenTags.has(tagName)) {
118
+ if (coreElement.tags) {
119
+ for (let i = 0; i < coreElement.tags.length; i++) {
120
+ const tag = coreElement.tags[i]!;
121
+ if (seenTags.has(tag.tagName)) {
120
122
  throw new Error(
121
- `Prose element tag name "<${tagName}>" is already registered by another element!`,
123
+ `Prose element tag name "<${tag.tagName}>" is already registered by another element!`,
122
124
  );
123
125
  }
124
126
 
125
- seenTags.add(tagName);
126
- elementData.registryItems[
127
- elementData.registryItems.length - 1
128
- ]!.tagNames.push(tagName);
127
+ seenTags.add(tag.tagName);
128
+ elementData.coreElements[elementData.coreElements.length - 1]!.tags[
129
+ tag.tagName
130
+ ] = i;
129
131
  }
130
132
  }
131
133
 
@@ -144,7 +146,7 @@ export async function setupProseElements(
144
146
  elementsData.push(elementData);
145
147
  }
146
148
 
147
- createGlobalTypes(elementsData);
149
+ createElementGlobalTypes(elementsData);
148
150
  createGlobalTemplate(nuxt, elementsData);
149
151
  createAppTemplate(nuxt, elementsData);
150
152
 
@@ -1,8 +1,8 @@
1
1
  export interface ElementData {
2
2
  name: string;
3
- registryItems: {
3
+ coreElements: {
4
4
  schemaName: string;
5
- tagNames: string[];
5
+ tags: Record<string, number>;
6
6
  }[];
7
7
  absDirectory: string;
8
8
  absCorePath: string;
@@ -2,7 +2,7 @@ import type { ElementData } from './shared';
2
2
 
3
3
  export function createTagsTable(elementsData: ElementData[], columns = 4) {
4
4
  const tagNames = elementsData.flatMap((data) =>
5
- data.registryItems.flatMap((item) => item.tagNames),
5
+ data.coreElements.flatMap((coreElement) => Object.keys(coreElement.tags)),
6
6
  );
7
7
 
8
8
  if (tagNames.length === 0) return '';
@@ -43,6 +43,8 @@ export async function setupEruditRuntimeConfig(nuxt: Nuxt) {
43
43
  slowTransition: eruditConfig.debug?.slowTransition ?? false,
44
44
  fakeApi: {
45
45
  repository: eruditConfig.debug?.fakeApi?.repository ?? nuxt.options.dev,
46
+ lastChanged:
47
+ eruditConfig.debug?.fakeApi?.lastChanged ?? nuxt.options.dev,
46
48
  },
47
49
  analytics: eruditConfig.debug?.analytics,
48
50
  },
package/nuxt.config.ts CHANGED
@@ -50,9 +50,9 @@ export default defineNuxtConfig({
50
50
  },
51
51
  },
52
52
  rollupConfig: {
53
- // Prevent inlining some packages to avoid singleton and Symbol duplication issues
53
+ // Prevent inlining some packages
54
54
  external(source) {
55
- const ignore = ['jiti', '@jsprose'];
55
+ const ignore = ['jiti', 'tsprose'];
56
56
 
57
57
  for (const ignoreItem of ignore) {
58
58
  if (source.includes(ignoreItem)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "4.1.0",
3
+ "version": "4.2.0-dev.1",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -24,30 +24,31 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@erudit-js/cli": "4.1.0",
28
- "@erudit-js/core": "4.1.0",
29
- "@erudit-js/prose": "4.1.0",
27
+ "@erudit-js/cli": "4.2.0-dev.1",
28
+ "@erudit-js/core": "4.2.0-dev.1",
29
+ "@erudit-js/prose": "4.2.0-dev.1",
30
30
  "unslash": "^2.0.0",
31
31
  "@floating-ui/vue": "^1.1.10",
32
- "@jsprose/core": "^1.0.0",
33
- "@tailwindcss/vite": "^4.1.18",
32
+ "tsprose": "^1.0.0",
33
+ "@tailwindcss/vite": "^4.2.0",
34
34
  "better-sqlite3": "^12.6.2",
35
35
  "chalk": "^5.6.2",
36
36
  "chokidar": "^5.0.0",
37
37
  "consola": "^3.4.2",
38
- "drizzle-kit": "^0.31.8",
38
+ "drizzle-kit": "^0.31.9",
39
39
  "drizzle-orm": "^0.45.1",
40
- "esbuild": "^0.27.2",
40
+ "esbuild": "^0.27.3",
41
41
  "flexsearch": "^0.8.212",
42
- "glob": "^13.0.0",
42
+ "glob": "^13.0.6",
43
43
  "image-size": "^2.0.2",
44
44
  "jiti": "^2.6.1",
45
- "nuxt": "4.3.0",
45
+ "nuxt": "4.3.1",
46
46
  "nuxt-my-icons": "1.2.2",
47
47
  "perfect-debounce": "^2.1.0",
48
- "tailwindcss": "^4.1.18",
49
- "vue": "^3.5.27",
50
- "vue-router": "^5.0.2"
48
+ "tailwindcss": "^4.2.0",
49
+ "vue": "^3.5.28",
50
+ "vue-router": "^5.0.3",
51
+ "ts-xor": "^1.3.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/better-sqlite3": "^7.6.13"
@@ -64,7 +64,7 @@ export default defineEventHandler<Promise<MainContent>>(async (event) => {
64
64
  }
65
65
 
66
66
  if (contentTypePath.type === 'page' || contentTypePath.type === 'topic') {
67
- const proseElement = await ERUDIT.repository.prose.getContent(
67
+ const prose = await ERUDIT.repository.prose.getContent(
68
68
  contentTypePath.type === 'topic'
69
69
  ? contentTypePath.topicPart
70
70
  : contentTypePath.type,
@@ -72,7 +72,7 @@ export default defineEventHandler<Promise<MainContent>>(async (event) => {
72
72
  );
73
73
 
74
74
  const topicParts = await ERUDIT.repository.content.topicParts(fullId);
75
- const { storage } = await ERUDIT.repository.prose.finalize(proseElement);
75
+ const { storage } = await ERUDIT.repository.prose.finalize(prose);
76
76
 
77
77
  const where = (() => {
78
78
  if (contentTypePath.type === 'topic') {
@@ -101,7 +101,7 @@ export default defineEventHandler<Promise<MainContent>>(async (event) => {
101
101
  type: 'topic',
102
102
  part: contentTypePath.topicPart,
103
103
  parts: topicParts,
104
- proseElement,
104
+ prose,
105
105
  storage,
106
106
  };
107
107
 
@@ -123,7 +123,7 @@ export default defineEventHandler<Promise<MainContent>>(async (event) => {
123
123
  const mainContentPage: MainContentPage = {
124
124
  ...mainContentBase,
125
125
  type: 'page',
126
- proseElement,
126
+ prose,
127
127
  storage,
128
128
  };
129
129
 
@@ -141,6 +141,7 @@ export default defineEventHandler<Promise<MainContent>>(async (event) => {
141
141
  //
142
142
  // Rest content types
143
143
  //
144
+
144
145
  return {
145
146
  ...mainContentBase,
146
147
  type: contentTypePath.type,
@@ -68,9 +68,7 @@ export async function problemScripts() {
68
68
  for (const dbProblemScript of dbProblemScripts) {
69
69
  routes.push(
70
70
  `/api/problemScript/` +
71
- dbProblemScript
72
- .problemScript!.replace(ERUDIT.paths.project() + '/', '')
73
- .replace('.tsx', '') +
71
+ dbProblemScript.problemScript!.replace('.tsx', '') +
74
72
  '.js',
75
73
  );
76
74
  }
@@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm';
3
3
  export default defineEventHandler<Promise<PreviewContentPage>>(
4
4
  async (event) => {
5
5
  // <typeOrPart>/<fullOrShortId>.json
6
- const strContentTypePath = event.context.params!.contentTypePath.slice(
6
+ const strContentTypePath = event.context.params!.contentTypePath!.slice(
7
7
  0,
8
8
  -5,
9
9
  );
@@ -17,7 +17,6 @@ export default defineEventHandler<Promise<PreviewContentPage>>(
17
17
  columns: {
18
18
  title: true,
19
19
  description: true,
20
- // TODO: decoration svg!
21
20
  },
22
21
  where: eq(ERUDIT.db.schema.content.fullId, fullId),
23
22
  }))!;
@@ -1,73 +1,58 @@
1
- import { mixSchema, uniqueName2Id, type ProseElement } from '@jsprose/core';
2
1
  import { headingSchema } from '@erudit-js/prose/elements/heading/core';
3
2
  import { detailsSchema } from '@erudit-js/prose/elements/details/core';
3
+ import { isProseElement, makeProseElement, mixSchema } from 'tsprose';
4
4
 
5
5
  export default defineEventHandler<Promise<PreviewContentUnique>>(
6
6
  async (event) => {
7
7
  // <typeOrPart>/<fullContentId>/<uniqueName>.json
8
8
  const strContentTypePathUnique =
9
- event.context.params!.contentTypePathUnique.slice(0, -5);
9
+ event.context.params!.contentTypePathUnique!.slice(0, -5);
10
10
  const parts = strContentTypePathUnique.split('/');
11
-
12
11
  const strContentTypePath = parts.slice(0, -1).join('/');
13
- const uniqueName = parts.at(-1)!;
14
-
12
+ const uniqueName = decodeURIComponent(parts.at(-1)!);
15
13
  const contentTypePath = parseContentTypePath(strContentTypePath);
16
14
  const fullId = contentTypePath.contentId;
17
15
  const navNode = ERUDIT.contentNav.getNodeOrThrow(fullId);
18
16
  const shortId = navNode.shortId;
19
-
20
17
  const contentTitle = await ERUDIT.repository.content.title(fullId);
21
-
22
18
  const unique = await ERUDIT.repository.content.unique(
23
19
  fullId,
24
20
  contentTypePath.type === 'topic' ? contentTypePath.topicPart : 'page',
25
21
  uniqueName,
26
22
  );
27
-
28
23
  const uniqueProse = await (async () => {
29
- if (unique.prose.schemaName === headingSchema.name) {
24
+ if (isProseElement(unique.prose, headingSchema)) {
30
25
  return await ERUDIT.repository.content.uniqueHeading(
31
26
  fullId,
32
27
  contentTypePath.type === 'topic' ? contentTypePath.topicPart : 'page',
33
- uniqueName,
28
+ unique.prose.id,
34
29
  );
35
- } else if (unique.prose.schemaName === detailsSchema.name) {
36
- const mix: ProseElement<typeof mixSchema> = {
37
- __JSPROSE_element: true,
38
- schemaName: mixSchema.name,
39
- children: unique.prose.children!,
40
- } as ProseElement<typeof mixSchema>;
41
-
42
- return mix;
30
+ } else if (isProseElement(unique.prose, detailsSchema)) {
31
+ return makeProseElement({
32
+ schema: mixSchema,
33
+ elementHandler: (element) => {
34
+ element.children = unique.prose.children!;
35
+ },
36
+ });
43
37
  } else {
44
38
  return unique.prose;
45
39
  }
46
40
  })();
47
-
48
41
  const finalizedProse = await ERUDIT.repository.prose.finalize(uniqueProse);
49
-
50
42
  const link = (() => {
51
43
  if (contentTypePath.type === 'topic') {
52
- return PAGES.topic(
53
- contentTypePath.topicPart,
54
- shortId,
55
- uniqueName2Id(uniqueName),
56
- );
44
+ return PAGES.topic(contentTypePath.topicPart, shortId, unique.prose.id);
57
45
  }
58
-
59
- return PAGES[contentTypePath.type](shortId, uniqueName2Id(uniqueName));
46
+ return PAGES[contentTypePath.type](shortId, unique.prose.id);
60
47
  })();
61
-
62
48
  const previewContentUnique: PreviewContentUnique = {
63
- schemaName: unique.prose.schemaName,
49
+ schemaName: unique.prose.schema.name,
64
50
  elementTitle: unique.title || undefined,
65
- fadeOverlay: unique.prose.schemaName === headingSchema.name,
51
+ fadeOverlay: isProseElement(unique.prose, headingSchema),
66
52
  contentTitle,
67
53
  link,
68
54
  ...finalizedProse,
69
55
  };
70
-
71
56
  return previewContentUnique;
72
57
  },
73
58
  );
@@ -1,12 +1,14 @@
1
1
  import { resolve } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { build, type Plugin } from 'esbuild';
3
4
 
4
5
  import { STATIC_ASSET_EXTENSIONS } from '#layers/erudit/server/erudit/prose/transform/extensions';
5
6
  import { createGlobalContent } from '@erudit-js/core/content/global';
7
+ import { coreElements } from '#erudit/prose/global';
6
8
 
7
9
  export default defineEventHandler<Promise<string>>(async (event) => {
8
10
  // <filepathToScriptFile>.js
9
- const problemScriptPath = event.context.params!.problemScriptPath.slice(
11
+ const problemScriptPath = event.context.params!.problemScriptPath!.slice(
10
12
  0,
11
13
  -3,
12
14
  ); // remove .js
@@ -23,14 +25,14 @@ export default defineEventHandler<Promise<string>>(async (event) => {
23
25
  $CONTRIBUTOR: '{}',
24
26
  },
25
27
  jsx: 'automatic',
26
- plugins: [jsxRuntimePlugin, staticFilesPlugin],
28
+ plugins: [jsxRuntimePlugin, proseGlobalsPlugin, staticFilesPlugin],
27
29
  alias: {
28
30
  '#project': ERUDIT.paths.project() + '/',
29
31
  '#content': ERUDIT.paths.project('content') + '/',
30
32
  },
31
33
  });
32
34
 
33
- let code = buildResult.outputFiles[0].text;
35
+ let code = buildResult.outputFiles[0]!.text;
34
36
 
35
37
  // Transform $CONTENT patterns to link objects
36
38
  code = code.replace(/\$CONTENT(\.[a-zA-Z_$][\w$]*)+/g, (match) => {
@@ -71,6 +73,69 @@ const jsxRuntimePlugin: Plugin = {
71
73
  },
72
74
  };
73
75
 
76
+ // Collect all tag names that are registered in globalThis
77
+ const proseTagNames = new Set<string>(
78
+ Object.values(coreElements).flatMap((el: any) =>
79
+ (el.tags ?? []).map((t: any) => String(t.tagName)),
80
+ ),
81
+ );
82
+
83
+ // Pre-transform: rewrite any import of a known globalThis tag name → const from globalThis.
84
+ // Non-tag imports are left as real imports and bundled normally.
85
+ // Applies recursively to every .ts/.tsx file esbuild processes (including utility files).
86
+ const proseGlobalsPlugin: Plugin = {
87
+ name: 'prose-globals',
88
+ setup(build) {
89
+ build.onLoad({ filter: /\.[jt]sx?$/ }, (args) => {
90
+ const source = readFileSync(args.path, 'utf8');
91
+
92
+ const transformed = source.replace(
93
+ /^import\s+\{([^}]+)\}\s+from\s+(['"])([^'"]+)\2.*$/gm,
94
+ (_match, bindings: string, _quote: string, pkg: string) => {
95
+ const keepParts: string[] = [];
96
+ const shimLines: string[] = [];
97
+
98
+ for (const part of bindings
99
+ .split(',')
100
+ .map((s) => s.trim())
101
+ .filter(Boolean)) {
102
+ // handle "ExportName as LocalName"
103
+ const m = part.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
104
+ if (!m) {
105
+ keepParts.push(part);
106
+ continue;
107
+ }
108
+ const localName = m[2] ?? m[1]!;
109
+ if (proseTagNames.has(localName)) {
110
+ shimLines.push(
111
+ `const ${localName} = globalThis[${JSON.stringify(localName)}];`,
112
+ );
113
+ } else {
114
+ keepParts.push(part);
115
+ }
116
+ }
117
+
118
+ const lines: string[] = [];
119
+ if (keepParts.length > 0)
120
+ lines.push(`import { ${keepParts.join(', ')} } from '${pkg}';`);
121
+ lines.push(...shimLines);
122
+ return lines.join('\n');
123
+ },
124
+ );
125
+
126
+ const ext = (args.path.match(/[jt]sx?$/)?.[0] ?? 'js') as
127
+ | 'js'
128
+ | 'jsx'
129
+ | 'ts'
130
+ | 'tsx';
131
+ return {
132
+ contents: transformed,
133
+ loader: ext,
134
+ };
135
+ });
136
+ },
137
+ };
138
+
74
139
  const staticFilesPlugin: Plugin = {
75
140
  name: 'static-files',
76
141
  setup(build) {
@@ -80,7 +145,11 @@ const staticFilesPlugin: Plugin = {
80
145
  },
81
146
  async (args) => {
82
147
  const absPath = resolve(args.path).replace(/\\/g, '/');
83
- const contents = `export default ${JSON.stringify(absPath)};`;
148
+ const projectPath = ERUDIT.paths.project();
149
+ const relPath = absPath.startsWith(projectPath + '/')
150
+ ? absPath.slice(projectPath.length + 1)
151
+ : absPath;
152
+ const contents = `export default ${JSON.stringify(relPath)};`;
84
153
  return { contents, loader: 'js' };
85
154
  },
86
155
  );
@@ -9,6 +9,8 @@ let initialBuild = true;
9
9
 
10
10
  const contentRoot = () => ERUDIT.paths.project('content');
11
11
 
12
+ export let builtLinkObject: Record<string, any> | null = null;
13
+
12
14
  export async function buildGlobalContent() {
13
15
  ERUDIT.log.debug.start('Building global content...');
14
16
 
@@ -21,6 +23,7 @@ export async function buildGlobalContent() {
21
23
  }
22
24
 
23
25
  const linkObject = await buildLinkObject();
26
+ builtLinkObject = linkObject;
24
27
 
25
28
  const linkTypes = linkObjectToTypes(linkObject);
26
29
  writeFileSync(
@@ -51,6 +54,10 @@ function linkObjectToTypes(linkObject: any): string {
51
54
  return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
52
55
  }
53
56
 
57
+ function isValidIdentifier(key: string): boolean {
58
+ return /^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(key);
59
+ }
60
+
54
61
  function processObject(obj: any, level: number): string {
55
62
  const lines: string[] = [];
56
63
 
@@ -58,6 +65,9 @@ function linkObjectToTypes(linkObject: any): string {
58
65
  if (key === '__jsdoc' || key === '__typeguard') continue;
59
66
 
60
67
  const camelKey = toCamelCase(key);
68
+ const outputKey = isValidIdentifier(camelKey)
69
+ ? camelKey
70
+ : `'${camelKey}'`;
61
71
 
62
72
  // Add JSDoc comment if present
63
73
  if (value && typeof value === 'object' && value.__jsdoc) {
@@ -77,11 +87,11 @@ function linkObjectToTypes(linkObject: any): string {
77
87
  const typeguard = value?.__typeguard || 'GlobalContentItemTypeguard';
78
88
 
79
89
  if (hasNestedProps) {
80
- lines.push(indent(level) + `${camelKey}: ${typeguard} & {`);
90
+ lines.push(indent(level) + `${outputKey}: ${typeguard} & {`);
81
91
  lines.push(processObject(value, level + 1));
82
92
  lines.push(indent(level) + `}`);
83
93
  } else {
84
- lines.push(indent(level) + `${camelKey}: ${typeguard} & {}`);
94
+ lines.push(indent(level) + `${outputKey}: ${typeguard} & {}`);
85
95
  }
86
96
  }
87
97
 
@@ -118,7 +128,7 @@ async function buildLinkObject() {
118
128
 
119
129
  // Navigate through parent parts
120
130
  for (let i = 0; i < pathParts.length - 1; i++) {
121
- cursor = cursor[pathParts[i]];
131
+ cursor = cursor[pathParts[i]!];
122
132
  }
123
133
 
124
134
  //
@@ -230,10 +240,10 @@ ${jsdoc}
230
240
  }
231
241
 
232
242
  function tryGetTitle(moduleContent: string) {
233
- const titleMatch = moduleContent.match(/title:\s*['"`](.*?)['"`]/);
243
+ const titleMatch = moduleContent.match(/title:\s*(['"`])(.*?)\1/);
234
244
 
235
245
  if (titleMatch) {
236
- return titleMatch[1].trim();
246
+ return titleMatch[2]!.trim();
237
247
  }
238
248
  }
239
249
 
@@ -259,7 +269,7 @@ function tryGetUniquesObject(
259
269
  const uniquesContent = uniquesMatch[1];
260
270
 
261
271
  // Parse key-value pairs from uniques object
262
- const lines = uniquesContent.split('\n');
272
+ const lines = uniquesContent!.split('\n');
263
273
  const result: any = {};
264
274
 
265
275
  for (const line of lines) {
@@ -273,7 +283,11 @@ function tryGetUniquesObject(
273
283
  continue;
274
284
  }
275
285
 
276
- const pairMatch = line.match(/(\w+):\s*(\w+)/);
286
+ // Support bracket notation ['any string'], quoted keys "any string" / 'any string', and plain identifiers
287
+ const pairMatch =
288
+ line.match(/\[['"](.*?)['"]\]:\s*(\w+)/) ||
289
+ line.match(/['"](.*?)['"]:\s*(\w+)/) ||
290
+ line.match(/(\w+):\s*(\w+)/);
277
291
  if (!pairMatch) {
278
292
  continue;
279
293
  }
@@ -216,16 +216,16 @@ function parseContentPath(relPath: string):
216
216
 
217
217
  for (let i = 0; i < parts.length; i++) {
218
218
  const part = parts[i];
219
- const typeDelimiterPos = part.search(/[+-]/);
219
+ const typeDelimiterPos = part!.search(/[+-]/);
220
220
  if (typeDelimiterPos <= 0) return;
221
221
 
222
- const posStr = part.slice(0, typeDelimiterPos);
222
+ const posStr = part!.slice(0, typeDelimiterPos);
223
223
  if (!/^\d+$/.test(posStr)) return;
224
224
  const posNum = Number(posStr);
225
225
 
226
- const typeDelimiter = part.charAt(typeDelimiterPos);
226
+ const typeDelimiter = part!.charAt(typeDelimiterPos);
227
227
  skip = typeDelimiter === '+';
228
- idPart = part.slice(typeDelimiterPos + 1);
228
+ idPart = part!.slice(typeDelimiterPos + 1);
229
229
  if (!idPart) return;
230
230
 
231
231
  fullId += `/${idPart}`;
@@ -24,7 +24,7 @@ export async function getContentChildren(
24
24
  const elementSnippets = await ERUDIT.repository.content.elementSnippets(
25
25
  childNode.fullId,
26
26
  );
27
- const quickLinks = elementSnippets?.filter((snippet) => snippet.quick);
27
+ const keyLinks = elementSnippets?.filter((snippet) => snippet.key);
28
28
  const stats = await ERUDIT.repository.content.stats(childNode.fullId);
29
29
 
30
30
  const child: MainContentChildrenItem = {
@@ -37,8 +37,8 @@ export async function getContentChildren(
37
37
  child.description = description;
38
38
  }
39
39
 
40
- if (quickLinks && quickLinks.length > 0) {
41
- child.quickLinks = quickLinks;
40
+ if (keyLinks && keyLinks.length > 0) {
41
+ child.keyLinks = keyLinks;
42
42
  }
43
43
 
44
44
  if (stats) {