erudit 4.1.1 → 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 (116) 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 +14 -2
  23. package/app/components/main/connections/Externals.vue +1 -1
  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 +101 -13
  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/page.ts +15 -35
  73. package/server/erudit/content/resolve/topic.ts +33 -164
  74. package/server/erudit/content/resolve/utils/insertContentResolved.ts +74 -31
  75. package/server/erudit/content/search.ts +5 -22
  76. package/server/erudit/contributors/build.ts +7 -8
  77. package/server/erudit/db/repository/pushFile.ts +10 -3
  78. package/server/erudit/db/repository/pushProblemScript.ts +14 -3
  79. package/server/erudit/db/schema/contentDeps.ts +3 -0
  80. package/server/erudit/db/schema/contentSnippets.ts +3 -3
  81. package/server/erudit/db/schema/contentUniques.ts +2 -2
  82. package/server/erudit/db/schema/contributors.ts +2 -2
  83. package/server/erudit/db/schema/news.ts +2 -2
  84. package/server/erudit/db/schema/pages.ts +2 -2
  85. package/server/erudit/db/schema/topics.ts +4 -4
  86. package/server/erudit/global.ts +4 -0
  87. package/server/erudit/importer.ts +16 -8
  88. package/server/erudit/index.ts +0 -3
  89. package/server/erudit/language/list/en.ts +1 -0
  90. package/server/erudit/language/list/ru.ts +1 -0
  91. package/server/erudit/news/build.ts +6 -6
  92. package/server/erudit/news/repository/batch.ts +2 -2
  93. package/server/erudit/prose/repository/finalize.ts +22 -25
  94. package/server/erudit/prose/repository/get.ts +3 -5
  95. package/server/erudit/prose/repository/rawToProse.ts +31 -0
  96. package/server/erudit/prose/storage/callout.ts +9 -7
  97. package/server/erudit/prose/storage/image.ts +8 -11
  98. package/server/erudit/prose/storage/link.ts +24 -32
  99. package/server/erudit/prose/storage/problemScript.ts +8 -14
  100. package/server/erudit/prose/storage/video.ts +9 -7
  101. package/server/erudit/repository.ts +4 -4
  102. package/server/routes/file/[...path].ts +1 -1
  103. package/shared/types/contentChildren.ts +5 -2
  104. package/shared/types/contentConnections.ts +9 -0
  105. package/shared/types/elementSnippet.ts +1 -1
  106. package/shared/types/indexPage.ts +3 -0
  107. package/shared/types/language.ts +1 -83
  108. package/shared/types/mainContent.ts +11 -5
  109. package/shared/types/news.ts +2 -2
  110. package/shared/types/preview.ts +3 -2
  111. package/shared/types/runtimeConfig.ts +1 -0
  112. package/shared/types/search.ts +2 -0
  113. package/shared/utils/pages.ts +4 -2
  114. package/shared/utils/stringColor.ts +16 -6
  115. package/server/erudit/prose/repository/resolve.ts +0 -17
  116. package/server/erudit/prose/transform/bundleProblemScript.ts +0 -6
@@ -1,50 +1,58 @@
1
1
  <script lang="ts" setup>
2
2
  import ItemElement from './ItemElement.vue';
3
+ import ItemLastChanged from './ItemLastChanged.vue';
3
4
  import ItemMaterials from './ItemMaterials.vue';
4
5
 
5
- defineProps<{
6
+ const props = defineProps<{
6
7
  mode: 'single' | 'children';
7
8
  stats?: ContentStats;
9
+ contentRelativePath?: string;
8
10
  }>();
9
11
 
10
12
  const phrase = await usePhrases('stats');
13
+ const lastChangedSource = useLastChangedSource(() => props.contentRelativePath);
11
14
  </script>
12
15
 
13
16
  <template>
14
- <template v-if="stats">
15
- <section v-if="mode === 'single'" class="px-main py-main-half">
16
- <MainSubTitle :title="phrase.stats + ':'" />
17
- <div
18
- class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
19
- justify-center"
20
- >
21
- <ItemMaterials
22
- v-if="stats.materials"
23
- :count="stats.materials"
24
- mode="detailed"
25
- />
26
- <ItemElement
27
- v-if="stats.elements"
28
- v-for="(count, schemaName) of stats.elements"
29
- :schemaName
30
- :count
31
- mode="detailed"
32
- />
33
- </div>
34
- </section>
35
- <div v-else class="gap-small micro:gap-normal text-main-sm flex flex-wrap">
17
+ <section
18
+ v-if="mode === 'single' && (stats || lastChangedSource)"
19
+ class="px-main py-main-half"
20
+ >
21
+ <MainSubTitle :title="phrase.stats + ':'" />
22
+ <div
23
+ class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
24
+ justify-center"
25
+ >
26
+ <ItemLastChanged v-if="lastChangedSource" :source="lastChangedSource" />
36
27
  <ItemMaterials
37
- v-if="stats.materials"
28
+ v-if="stats?.materials"
38
29
  :count="stats.materials"
39
- mode="compact"
30
+ mode="detailed"
40
31
  />
41
32
  <ItemElement
42
- v-if="stats.elements"
33
+ v-if="stats?.elements"
43
34
  v-for="(count, schemaName) of stats.elements"
44
35
  :schemaName
45
36
  :count
46
- mode="compact"
37
+ mode="detailed"
47
38
  />
48
39
  </div>
49
- </template>
40
+ </section>
41
+ <div
42
+ v-else-if="mode === 'children' && stats"
43
+ class="gap-small micro:gap-normal text-main-sm flex flex-wrap"
44
+ >
45
+ <ItemMaterials
46
+ v-if="stats.materials"
47
+ :count="stats.materials"
48
+ mode="compact"
49
+ />
50
+ <ItemElement
51
+ v-if="stats.elements"
52
+ v-for="(count, schemaName) of stats.elements"
53
+ :schemaName
54
+ :count
55
+ mode="compact"
56
+ />
57
+ </div>
50
58
  </template>
@@ -30,14 +30,14 @@ const { closePreview, hasPreviousRequest, setPreviousPreview } = usePreview();
30
30
  class="micro:text-sm overflow-hidden text-xs font-bold text-nowrap
31
31
  overflow-ellipsis"
32
32
  >
33
- {{ main }}
33
+ {{ formatText(main) }}
34
34
  </div>
35
35
  <div
36
36
  v-if="secondary"
37
37
  class="text-text-muted text-tiny micro:text-xs overflow-hidden
38
38
  text-nowrap overflow-ellipsis"
39
39
  >
40
- {{ secondary }}
40
+ {{ formatText(secondary) }}
41
41
  </div>
42
42
  </div>
43
43
  <div class="gap-small flex shrink-0 items-center">
@@ -6,13 +6,10 @@ const { request } = defineProps<{ request: PreviewRequestContentPage }>();
6
6
  const contentTypeKey =
7
7
  request.contentType === 'topic' ? request.topicPart : request.contentType;
8
8
 
9
- const previewData: PreviewContentPage = await $fetch(
9
+ const previewData: PreviewContentPage = await fetchJson(
10
10
  '/api/preview/contentPage/' +
11
11
  stringifyContentTypePath(contentTypeKey, request.fullId) +
12
12
  '.json',
13
- {
14
- responseType: 'json',
15
- },
16
13
  );
17
14
 
18
15
  const icon = ICONS[contentTypeKey];
@@ -6,12 +6,9 @@ const { request } = defineProps<{ request: PreviewRequestUnique }>();
6
6
  const contentTypeKey =
7
7
  request.contentType === 'topic' ? request.topicPart : request.contentType;
8
8
 
9
- const previewData = await $fetch<PreviewContentUnique>(
9
+ const previewData = await fetchJson<PreviewContentUnique>(
10
10
  `/api/preview/contentUnique/${stringifyContentTypePath(contentTypeKey, request.contentFullId)}/${request.uniqueName}` +
11
11
  '.json',
12
- {
13
- responseType: 'json',
14
- },
15
12
  );
16
13
 
17
14
  const elementIcon = await getElementIcon(previewData.schemaName);
@@ -38,10 +35,11 @@ const secondary = (() => {
38
35
  class="nice-scrollbars py-small relative max-h-[inherit] overflow-auto"
39
36
  >
40
37
  <Prose
41
- :element="previewData.proseElement"
38
+ :element="previewData.prose"
42
39
  :storage="previewData.storage"
43
40
  :urlPath="'/' + previewData.link.split('#')[0]"
44
41
  :useHashUrl="false"
42
+ :setHtmlIds="false"
45
43
  />
46
44
  <div
47
45
  v-if="previewData.fadeOverlay"
@@ -1,7 +1,5 @@
1
- import type { AppElement } from '@erudit-js/prose/app';
2
- import { appElements as _appElements } from '#erudit/prose/app';
3
-
4
- export const appElements: Record<string, AppElement> = { ..._appElements };
1
+ import { appElements } from '#erudit/prose/app';
2
+ export { appElements } from '#erudit/prose/app';
5
3
 
6
4
  export async function initAppElements() {
7
5
  for (const [name, element] of Object.entries(appElements)) {
@@ -78,9 +78,9 @@ export async function initGlobalContentNav() {
78
78
  const payloadValue: FrontGlobalContentNav =
79
79
  (nuxtApp.static.data[payloadKey] ||=
80
80
  nuxtApp.payload.data[payloadKey] ||=
81
- await $fetch('/api/aside/major/frontNav/global', {
82
- responseType: 'json',
83
- }));
81
+ await fetchJson<FrontGlobalContentNav>(
82
+ '/api/aside/major/frontNav/global',
83
+ ));
84
84
 
85
85
  globalContentNav = payloadValue;
86
86
  }
@@ -0,0 +1,4 @@
1
+ export const fetchJson = <T>(...args: Parameters<typeof $fetch<T>>) => {
2
+ const { $fetchJson } = useNuxtApp();
3
+ return $fetchJson(...args) as ReturnType<typeof $fetch<T>>;
4
+ };
@@ -0,0 +1,28 @@
1
+ type LastChangedSource =
2
+ | { type: 'date'; value: string }
3
+ | { type: 'github'; url: string; path: string };
4
+
5
+ export function useLastChangedSource(
6
+ contentRelativePath: MaybeRefOrGetter<string | undefined>,
7
+ ) {
8
+ return computed((): LastChangedSource | null => {
9
+ const path = toValue(contentRelativePath);
10
+ if (!path) return null;
11
+
12
+ const debug = ERUDIT.config.debug.fakeApi.lastChanged;
13
+ if (debug === true) return { type: 'date', value: '2024-01-15T12:00:00Z' };
14
+ if (typeof debug === 'string') return { type: 'date', value: debug };
15
+
16
+ const repo = ERUDIT.config.repository;
17
+ if (!repo || repo.type !== 'github') return null;
18
+ const parts = repo.name.split('/');
19
+ if (parts.length !== 2) return null;
20
+ const [owner, repoName] = parts;
21
+
22
+ return {
23
+ type: 'github',
24
+ url: `https://api.github.com/repos/${owner}/${repoName}/commits`,
25
+ path: `content/${path}`,
26
+ };
27
+ });
28
+ }
@@ -16,11 +16,8 @@ export async function useMainContent<TMainContent extends MainContent>(
16
16
  let mainContentPromise = async () => {
17
17
  if (payloadMainContent.contentPath !== contentTypePath) {
18
18
  payloadMainContent.contentPath = contentTypePath;
19
- payloadMainContent.mainContent = await $fetch<MainContent>(
19
+ payloadMainContent.mainContent = await fetchJson<MainContent>(
20
20
  '/api/main/content/' + contentTypePath,
21
- {
22
- responseType: 'json',
23
- },
24
21
  );
25
22
  }
26
23
 
@@ -1,4 +1,5 @@
1
1
  import type { ContentSeo } from '@erudit-js/core/content/seo';
2
+ import { toSeoSnippet } from '@erudit-js/prose';
2
3
 
3
4
  export function initOgSiteName() {
4
5
  const siteTitle =
@@ -105,54 +106,61 @@ export async function useContentSeo(args: {
105
106
 
106
107
  const seoSnippets = args.snippets?.filter((snippet) => snippet.seo);
107
108
 
109
+ const route = useRoute();
110
+
111
+ function findElementSnippet(elementId: string | undefined) {
112
+ if (!elementId || !seoSnippets) return undefined;
113
+ return seoSnippets.find((candidate) => {
114
+ const url = new URL(candidate.link, 'http://x');
115
+ const sameParam = url.searchParams.get('element') === elementId;
116
+ const sameType =
117
+ candidate.link.split('/')[1] ===
118
+ (args.contentTypePath.type === 'topic'
119
+ ? args.contentTypePath.topicPart
120
+ : args.contentTypePath.type);
121
+ return sameParam && sameType;
122
+ });
123
+ }
124
+
125
+ // On a static site the server always serves the canonical-path HTML,
126
+ // so SSR-time element SEO is not possible. Instead we set SEO
127
+ // synchronously (before the first await) so the title/description are
128
+ // applied during the first client render without a visible flash.
129
+ // Google's crawler executes JS and will see the final values.
108
130
  if (!import.meta.client || !seoSnippets || seoSnippets.length === 0) {
109
131
  return;
110
132
  }
111
133
 
112
- const route = useRoute();
113
- const anchor = computed(() => route.hash.replace('#', ''));
134
+ const elementQuery = computed(
135
+ () => route.query.element as string | undefined,
136
+ );
114
137
 
115
138
  const stopWatch = watch(
116
- anchor,
117
- async (hash) => {
118
- const snippet = hash
119
- ? seoSnippets.find((candidate) => {
120
- const sameHash = candidate.link.split('#')[1] === hash;
121
- const sameType =
122
- candidate.link.split('/')[1] ===
123
- (args.contentTypePath.type === 'topic'
124
- ? args.contentTypePath.topicPart
125
- : args.contentTypePath.type);
126
- return sameHash && sameType;
127
- })
128
- : undefined;
129
-
139
+ elementQuery,
140
+ async (elementId) => {
141
+ const snippet = findElementSnippet(elementId);
130
142
  if (!snippet) {
131
143
  setupSeo(baseSeo);
132
144
  return;
133
145
  }
134
146
 
135
- const elementPhrase = await getElementPhrase(snippet.schemaName);
136
-
137
- const title = (() => {
138
- if (snippet.seo?.title) {
139
- return snippet.seo.title;
140
- } else {
141
- return snippet.title;
142
- }
143
- })();
144
-
145
- const description = (() => {
146
- if (snippet.seo?.description) {
147
- return snippet.seo.description;
148
- } else {
149
- return snippet.description;
150
- }
151
- })();
147
+ // ── Synchronous: set title/description immediately so there is no
148
+ // flash of the base-page title on first render.
149
+ const seoSnippet = toSeoSnippet(snippet)!;
150
+ const quickTitle = (() => seoSnippet.title)();
151
+ const quickDescription = (() => seoSnippet.description)();
152
+ setupSeo({
153
+ title: `${quickTitle} - ${seoSiteTitle}`,
154
+ description: quickDescription || '',
155
+ urlPath: snippet.link,
156
+ });
152
157
 
158
+ // ── Async: refine title with element-type phrase once loaded.
159
+ const elementPhrase = await getElementPhrase(snippet.schemaName);
160
+ const fullTitle = (() => seoSnippet.title)();
153
161
  setupSeo({
154
- title: `${title} [${elementPhrase.element_name}] - ${seoSiteTitle}`,
155
- description: description || '',
162
+ title: `${fullTitle} [${elementPhrase.element_name}] - ${seoSiteTitle}`,
163
+ description: quickDescription || '',
156
164
  urlPath: snippet.link,
157
165
  });
158
166
  },
@@ -69,9 +69,6 @@ export function usePhrases<const T extends readonly LanguagePhraseKey[]>(
69
69
  try {
70
70
  payloadPhraseValue = await $fetch<PayloadLanguagePhraseValue>(
71
71
  `/api/language/phrase/${phraseKey}`,
72
- {
73
- responseType: 'json',
74
- },
75
72
  );
76
73
 
77
74
  payloadLanguage.phrases[phraseKey] = payloadPhraseValue;
@@ -12,7 +12,7 @@ export function initScrollUpWatcher() {
12
12
  watch(
13
13
  () => route.path,
14
14
  () => {
15
- if (route.hash) {
15
+ if (route.query.element) {
16
16
  return;
17
17
  }
18
18
 
@@ -40,7 +40,11 @@ await useContentSeo({
40
40
  <MainFlags :flags="mainContent.flags" />
41
41
  <MainDescription :description="mainContent.description" />
42
42
  <MainConnections :connections="mainContent.connections" />
43
- <MainContentStats mode="single" :stats="mainContent.stats" />
43
+ <MainContentStats
44
+ mode="single"
45
+ :stats="mainContent.stats"
46
+ :contentRelativePath="mainContent.contentRelativePath"
47
+ />
44
48
  <div class="h-main-half"></div>
45
49
  <MainAction
46
50
  v-if="mainContent.children[0]"
@@ -17,11 +17,8 @@ const pageContributor = await (async () => {
17
17
  return cachedContributor;
18
18
  }
19
19
 
20
- let fetchedContributor: PageContributor = await $fetch(
20
+ let fetchedContributor: PageContributor = await fetchJson(
21
21
  '/api/contributor/page/' + contributorId.value,
22
- {
23
- responseType: 'json',
24
- },
25
22
  );
26
23
 
27
24
  fetchedContributor =
@@ -136,9 +133,10 @@ useStandartSeo({
136
133
  </MainSectionPreamble>
137
134
  <MainSection v-if="pageContributor.description">
138
135
  <Prose
139
- :element="pageContributor.description.proseElement"
136
+ :element="pageContributor.description.prose"
140
137
  :storage="pageContributor.description.storage"
141
138
  :useHashUrl="false"
139
+ :setHtmlIds="false"
142
140
  />
143
141
  </MainSection>
144
142
  </template>
@@ -11,7 +11,7 @@ const payloadKey = 'list-contributors';
11
11
  const listContributors: ListContributor[] =
12
12
  (nuxtApp.static.data[payloadKey] ||=
13
13
  nuxtApp.payload.data[payloadKey] ||=
14
- await $fetch('/api/contributor/list', { responseType: 'json' }));
14
+ await fetchJson('/api/contributor/list'));
15
15
 
16
16
  const phrase = await usePhrases(
17
17
  'contributors',
@@ -42,7 +42,11 @@ await useContentSeo({
42
42
  <MainFlags :flags="mainContent.flags" />
43
43
  <MainDescription :description="mainContent.description" />
44
44
  <MainConnections :connections="mainContent.connections" />
45
- <MainContentStats mode="single" :stats="mainContent.stats" />
45
+ <MainContentStats
46
+ mode="single"
47
+ :stats="mainContent.stats"
48
+ :contentRelativePath="mainContent.contentRelativePath"
49
+ />
46
50
  <div class="h-main-half"></div>
47
51
  <MainAction
48
52
  v-if="mainContent.children[0]"
@@ -9,7 +9,7 @@ const payloadKey = 'index-page';
9
9
  const indexPage: IndexPage =
10
10
  (nuxtApp.static.data[payloadKey] ||=
11
11
  nuxtApp.payload.data[payloadKey] ||=
12
- await $fetch<IndexPage>('/api/indexPage', { responseType: 'json' }));
12
+ await fetchJson<IndexPage>('/api/indexPage'));
13
13
 
14
14
  useIndexSeo(indexPage);
15
15
 
@@ -38,9 +38,13 @@ await useContentSeo({
38
38
  <MainTitle icon="lines" :title="mainContent.title" />
39
39
  <MainFlags :flags="mainContent.flags" />
40
40
  <MainDescription :description="mainContent.description" />
41
- <MainQuickLinks mode="single" :elementSnippets="mainContent.snippets" />
41
+ <MainKeyLinks mode="single" :elementSnippets="mainContent.snippets" />
42
42
  <MainConnections :connections="mainContent.connections" />
43
- <MainContentStats mode="single" :stats="mainContent.stats" />
43
+ <MainContentStats
44
+ mode="single"
45
+ :stats="mainContent.stats"
46
+ :contentRelativePath="mainContent.contentRelativePath"
47
+ />
44
48
  <div class="h-main-half"></div>
45
49
  <MainQuoteLoader />
46
50
  <div class="h-main-half"></div>
@@ -48,10 +52,11 @@ await useContentSeo({
48
52
  </MainSectionPreamble>
49
53
  <MainSection>
50
54
  <Prose
51
- :element="mainContent.proseElement"
55
+ :element="mainContent.prose"
52
56
  :storage="mainContent.storage"
53
57
  :urlPath="'/' + mainContent.fullId"
54
58
  :useHashUrl="true"
59
+ :setHtmlIds="true"
55
60
  @vue:mounted="proseMounted"
56
61
  />
57
62
  </MainSection>
@@ -11,7 +11,7 @@ const payloadKey = 'page-sponsors';
11
11
  const pageSponsors: PageSponsor[] =
12
12
  (nuxtApp.static.data[payloadKey] ||=
13
13
  nuxtApp.payload.data[payloadKey] ||=
14
- await $fetch('/api/pageSponsors', { responseType: 'json' }));
14
+ await fetchJson('/api/pageSponsors'));
15
15
 
16
16
  const phrase = await usePhrases(
17
17
  'sponsors',
@@ -8,11 +8,6 @@ import { setupWelcomeMessage } from './client/welcome';
8
8
  export default defineNuxtPlugin({
9
9
  name: 'erudit-app-setup',
10
10
  async setup() {
11
- // Forgive me God for doing this but this is necessary because Nuxt App server side shares
12
- // same globalThis with Nitro server but same imports are done again causing @jsprose/core to throw "multipe singleton instances" error.
13
- // @ts-ignore
14
- //delete globalThis['__JSPROSE__'];
15
-
16
11
  await setupAppRuntimeConfig();
17
12
  },
18
13
  hooks: {
@@ -0,0 +1,11 @@
1
+ export default defineNuxtPlugin(() => {
2
+ const fetchJson = $fetch.create({
3
+ responseType: 'json',
4
+ });
5
+
6
+ return {
7
+ provide: {
8
+ fetchJson,
9
+ },
10
+ };
11
+ });
@@ -28,7 +28,7 @@ export default defineNuxtPlugin({
28
28
  ];
29
29
 
30
30
  for (const provider of routeProviders) {
31
- const fetchedRoutes = await $fetch<string[]>(provider);
31
+ const fetchedRoutes = await fetchJson<string[]>(provider);
32
32
  routes.push(...fetchedRoutes);
33
33
  }
34
34
 
@@ -2,7 +2,7 @@ import type { RouterOptions } from 'vue-router';
2
2
 
3
3
  export default {
4
4
  scrollBehavior(_to, _from, savedPosition) {
5
- if (!savedPosition && !_to.hash) {
5
+ if (!savedPosition && !_to.query.element) {
6
6
  return { top: 0 };
7
7
  }
8
8
  return savedPosition;
@@ -1,5 +1,4 @@
1
- import type { AnySchema, RawElement } from '@jsprose/core';
1
+ import type { RawElement } from 'tsprose';
2
+ export { defineDocument as defineProse, type RawElement } from 'tsprose';
2
3
 
3
- export { defineDocument as defineProse } from '@jsprose/core';
4
-
5
- export type AnyProseElement = RawElement<AnySchema>;
4
+ export type ProseElement = RawElement;
@@ -15,7 +15,7 @@ export function createAppTemplate(nuxt: Nuxt, elementsData: ElementData[]) {
15
15
  }
16
16
 
17
17
  const template = `
18
- import type { AppElement } from '@erudit-js/prose/app';
18
+ import type { ProseAppElements } from '@erudit-js/prose/app';
19
19
 
20
20
  ${Object.entries(apps)
21
21
  .map(
@@ -24,12 +24,11 @@ ${Object.entries(apps)
24
24
  )
25
25
  .join('\n')}
26
26
 
27
- export const appElementsArray: AppElement[] = [
28
- ${Object.keys(apps).join(',\n ')}
29
- ].flatMap(element => (Array.isArray(element) ? element : [element]) as any);
30
-
31
- export const appElements = Object.fromEntries(
32
- appElementsArray.map(element => [element.schema.name, element])
27
+ export const appElements: ProseAppElements = Object.fromEntries([
28
+ ${Object.keys(apps).join(',\n ')}
29
+ ]
30
+ .flatMap((element: any) => (Array.isArray(element) ? element : [element]))
31
+ .map((element: any) => [element.schema.name, element])
33
32
  );
34
33
  `.trim();
35
34
 
@@ -5,13 +5,13 @@ import { sn } from 'unslash';
5
5
  import type { ElementData } from './shared';
6
6
  import { PROJECT_PATH } from '../../env';
7
7
 
8
- export function createGlobalTypes(elementsData: ElementData[]) {
8
+ export function createElementGlobalTypes(elementsData: ElementData[]) {
9
9
  for (const elementData of elementsData) {
10
- // Collect all tag names from registry
11
- const allRegistryTagNames = new Set<string>();
12
- for (const registryItem of elementData.registryItems) {
13
- for (const tagName of registryItem.tagNames) {
14
- allRegistryTagNames.add(tagName);
10
+ // Collect tag names from core element
11
+ const allCoreTagNames = new Set<string>();
12
+ for (const coreElement of elementData.coreElements) {
13
+ for (const tagName of Object.keys(coreElement.tags)) {
14
+ allCoreTagNames.add(tagName);
15
15
  }
16
16
  }
17
17
 
@@ -32,8 +32,8 @@ export function createGlobalTypes(elementsData: ElementData[]) {
32
32
  other = parsed.other;
33
33
  }
34
34
 
35
- // Add missing tags from registry
36
- for (const tagName of allRegistryTagNames) {
35
+ // Add missing tags from core element
36
+ for (const tagName of allCoreTagNames) {
37
37
  if (!(tagName in tags)) {
38
38
  tags[tagName] = '';
39
39
  }
@@ -58,18 +58,18 @@ export function createGlobalTypes(elementsData: ElementData[]) {
58
58
  );
59
59
  });
60
60
 
61
- // Create typeof imports for tags that exist in element's registry items
61
+ // Create typeof imports for tags that exist in element's core elements
62
62
  const tagDefinitions = Object.entries(tags).map(([tagName, jsdoc]) => {
63
- // Find which registry item contains this tag
63
+ // Find which core element contains this tag
64
64
  let tagDefinition = "'_tag_'";
65
- for (let i = 0; i < elementData.registryItems.length; i++) {
66
- const registryItem = elementData.registryItems[i]!;
67
- if (registryItem.tagNames.includes(tagName)) {
65
+ for (let i = 0; i < elementData.coreElements.length; i++) {
66
+ const coreElement = elementData.coreElements[i]!;
67
+ if (coreElement.tags[tagName] !== undefined) {
68
68
  // If there are multiple schemas, include the index
69
- if (elementData.registryItems.length > 1) {
70
- tagDefinition = `typeof import('${elementData.absCorePath}')['default'][${i}]['registryItem']['tags']['${tagName}']`;
69
+ if (elementData.coreElements.length > 1) {
70
+ tagDefinition = `typeof import('${elementData.absCorePath}')['default'][${i}]['tags']['${coreElement.tags[tagName]}']`;
71
71
  } else {
72
- tagDefinition = `typeof import('${elementData.absCorePath}')['default']['registryItem']['tags']['${tagName}']`;
72
+ tagDefinition = `typeof import('${elementData.absCorePath}')['default']['tags']['${coreElement.tags[tagName]}']`;
73
73
  }
74
74
  break;
75
75
  }
@@ -78,8 +78,8 @@ export function createGlobalTypes(elementsData: ElementData[]) {
78
78
  const indentedJsdoc = indentJsDoc(jsdoc);
79
79
 
80
80
  return indentedJsdoc
81
- ? `${indentedJsdoc}\n const ${tagName}: ${tagDefinition};`
82
- : ` const ${tagName}: ${tagDefinition};`;
81
+ ? `${indentedJsdoc}\n const ${tagName}: ${tagDefinition};`
82
+ : ` const ${tagName}: ${tagDefinition};`;
83
83
  });
84
84
 
85
85
  const finalDts = `
@@ -96,8 +96,8 @@ ${Object.entries(other)
96
96
  ? `type ${typeName} = ${definition};`
97
97
  : `const ${typeName}: ${definition};`;
98
98
  return indentedJsdoc
99
- ? `${indentedJsdoc}\n ${declaration}`
100
- : ` ${declaration}`;
99
+ ? `${indentedJsdoc}\n ${declaration}`
100
+ : ` ${declaration}`;
101
101
  })
102
102
  .join('\n')}
103
103
  }
@@ -250,7 +250,7 @@ function parseGlobalDts(dts: string): {
250
250
  return { imports, tags, other };
251
251
  }
252
252
 
253
- function indentJsDoc(jsdoc: string, indent: string = ' '): string {
253
+ function indentJsDoc(jsdoc: string, indent: string = ' '): string {
254
254
  if (!jsdoc) return '';
255
255
  return jsdoc
256
256
  .split('\n')