@erudit-js/prose 4.2.0-dev.1 → 4.3.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 (34) hide show
  1. package/dist/app/composables/context.d.ts +2 -0
  2. package/dist/app/composables/language.d.ts +2 -4
  3. package/dist/app/composables/language.js +6 -2
  4. package/dist/app/shared/block/Block.vue +5 -0
  5. package/dist/coreElement.d.ts +2 -7
  6. package/dist/elements/diagram/Diagram.vue +6 -4
  7. package/dist/elements/diagram/core.d.ts +3 -0
  8. package/dist/elements/diagram/core.js +4 -1
  9. package/dist/elements/diagram/icon.svg +2 -2
  10. package/dist/elements/math/components/InlinerMath.vue +18 -7
  11. package/dist/elements/math/core.d.ts +4 -2
  12. package/dist/elements/math/inliner.d.ts +12 -4
  13. package/dist/elements/math/inliner.js +6 -3
  14. package/dist/elements/problem/_global.d.ts +22 -0
  15. package/dist/elements/problem/components/ProblemContent.vue +10 -1
  16. package/dist/elements/problem/components/Problems.vue +9 -2
  17. package/dist/elements/problem/components/expanders/Check.vue +37 -18
  18. package/dist/elements/problem/core.d.ts +1 -0
  19. package/dist/elements/problem/problemCheck.d.ts +15 -8
  20. package/dist/elements/problem/problemCheck.js +65 -22
  21. package/dist/elements/problem/problemContent.js +4 -6
  22. package/dist/elements/problem/problems.d.ts +3 -0
  23. package/dist/elements/problem/problems.js +3 -0
  24. package/dist/elements/table/Table.vue +26 -19
  25. package/dist/elements/table/core.d.ts +21 -3
  26. package/dist/elements/table/core.js +13 -1
  27. package/dist/elements/table/icon.svg +1 -1
  28. package/dist/elements/video/icon.svg +1 -1
  29. package/dist/shared/filePath.js +29 -2
  30. package/dist/snippet.d.ts +1 -0
  31. package/dist/snippet.js +10 -5
  32. package/dist/toc.js +4 -4
  33. package/package.json +5 -4
  34. package/raw.d.ts +4 -0
@@ -3,6 +3,7 @@ import type { useFloating, UseFloatingOptions } from '@floating-ui/vue';
3
3
  import type { EruditLanguageCode } from '@erudit-js/core/eruditConfig/language';
4
4
  import type { EruditMode } from '@erudit-js/core/mode';
5
5
  import type { FormatText } from '@erudit-js/core/formatText';
6
+ import type { ProblemCheckers } from '@erudit-js/core/problemCheck';
6
7
  import type { ProseAppElement } from '../appElement.js';
7
8
  export interface ProseContext {
8
9
  mode: EruditMode;
@@ -14,6 +15,7 @@ export interface ProseContext {
14
15
  baseUrl: string;
15
16
  hashUrl: Ref<string | undefined>;
16
17
  eruditIcons: Record<string, string>;
18
+ problemCheckers: ProblemCheckers;
17
19
  EruditTransition: Component;
18
20
  EruditIcon: Component;
19
21
  EruditLink: Component;
@@ -1,8 +1,6 @@
1
1
  import type { ProseElement } from 'tsprose';
2
2
  import type { ElementPhrases } from '../language/element.js';
3
- export declare function useProseLanguage(): Promise<{
4
- copy_link: string;
5
- copied: string;
6
- }>;
3
+ import { type ProsePhrases } from '../language/prose.js';
4
+ export declare function useProseLanguage(): Promise<ProsePhrases>;
7
5
  export declare function useElementPhrase<TPhrases extends ElementPhrases>(schemaName: string): Promise<TPhrases>;
8
6
  export declare function useElementPhrase<TPhrases extends ElementPhrases>(element: ProseElement): Promise<TPhrases>;
@@ -1,9 +1,13 @@
1
1
  import { proseLanguages } from "../language/prose.js";
2
2
  import { useProseContext } from "./context.js";
3
3
  import { useAppElement } from "./appElement.js";
4
- export async function useProseLanguage() {
4
+ const proseLanguageCache = new Map();
5
+ export function useProseLanguage() {
5
6
  const { languageCode } = useProseContext();
6
- return (await proseLanguages[languageCode]()).default;
7
+ if (!proseLanguageCache.has(languageCode)) {
8
+ proseLanguageCache.set(languageCode, proseLanguages[languageCode]().then((m) => m.default));
9
+ }
10
+ return proseLanguageCache.get(languageCode);
7
11
  }
8
12
  export async function useElementPhrase(elementOrName) {
9
13
  const { languageCode } = useProseContext();
@@ -11,6 +11,7 @@ import { useFloating, autoUpdate, offset, shift } from '@floating-ui/vue';
11
11
 
12
12
  import { useProseContext } from '../../composables/context.js';
13
13
  import { useElementIcon } from '../../composables/elementIcon.js';
14
+ import { useProseLanguage } from '../../composables/language.js';
14
15
  import {
15
16
  useIsAnchor,
16
17
  useJumpToAnchor,
@@ -22,6 +23,10 @@ import type { BlockProseElement } from 'tsprose';
22
23
 
23
24
  const { element } = defineProps<{ element: BlockProseElement }>();
24
25
  const { EruditIcon, EruditTransition, setHtmlIds } = useProseContext();
26
+
27
+ // Pre-warm the prose language module so it is cached before the aside menu opens.
28
+ const _proseLang = useProseLanguage();
29
+
25
30
  const elementIcon = await useElementIcon(element);
26
31
 
27
32
  const formatTextState = useFormatTextState();
@@ -1,16 +1,11 @@
1
1
  import type { ElementStorageCreator, Schema } from 'tsprose';
2
+ import type { EruditDependencies } from '@erudit-js/core/dependencies';
2
3
  import type { RawToProseSchemaHook } from './rawToProse/hook.js';
3
4
  import type { EruditTag, ToEruditTag } from './tag.js';
4
- export interface ProseCoreElementDependencies {
5
- [dependencyName: string]: {
6
- transpile?: boolean;
7
- optimize?: boolean;
8
- };
9
- }
10
5
  export type ToProseCoreElement<TSchema extends Schema, Tags extends EruditTag[] | undefined> = {
11
6
  schema: TSchema;
12
7
  tags: Tags;
13
- dependencies?: ProseCoreElementDependencies;
8
+ dependencies?: EruditDependencies;
14
9
  } & ({
15
10
  createStorage: ElementStorageCreator<TSchema>;
16
11
  } | {
@@ -10,6 +10,7 @@ import {
10
10
  useTemplateRef,
11
11
  } from 'vue';
12
12
  import type { ToProseElement } from 'tsprose';
13
+ import type { Mermaid } from 'mermaid';
13
14
 
14
15
  import type { DiagramSchema } from './core.js';
15
16
  import { usePhotoSwipe } from '../../app/shared/photoswipe/composable.js';
@@ -183,8 +184,9 @@ async function diagramIntersectHit(entries: IntersectionObserverEntry[]) {
183
184
  }
184
185
 
185
186
  async function renderDiagram() {
186
- const mermaid = //@ts-ignore
187
- (await import('https://cdn.jsdelivr.net/npm/mermaid@11.12.2/+esm')).default;
187
+ // @ts-expect-error
188
+ const mermaid = (await import('mermaid/dist/mermaid.esm.min'))
189
+ .default as Mermaid;
188
190
 
189
191
  mermaid.initialize({
190
192
  startOnLoad: false,
@@ -192,8 +194,8 @@ async function renderDiagram() {
192
194
  markdownAutoWrap: false,
193
195
  flowchart: {
194
196
  useMaxWidth: true,
195
- padding: 10,
196
- wrappingWidth: 400,
197
+ padding: 16,
198
+ wrappingWidth: 600,
197
199
  },
198
200
  theme: 'base',
199
201
  });
@@ -14,6 +14,9 @@ declare const _default: {
14
14
  readonly schema: DiagramSchema;
15
15
  readonly tags: [import("../../tag.js").ToEruditTag<DiagramSchema, "Diagram", unknown>];
16
16
  readonly dependencies: {
17
+ readonly mermaid: {
18
+ transpile: true;
19
+ };
17
20
  readonly photoswipe: {
18
21
  optimize: boolean;
19
22
  transpile: boolean;
@@ -23,5 +23,8 @@ export const Diagram = defineEruditTag({
23
23
  export default defineProseCoreElement({
24
24
  schema: diagramSchema,
25
25
  tags: [Diagram],
26
- dependencies: { ...photoswipeDependency }
26
+ dependencies: {
27
+ ...photoswipeDependency,
28
+ ...{ mermaid: { transpile: true } }
29
+ }
27
30
  });
@@ -1,3 +1,3 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
2
- <path d="M480-60q-63 0-106.5-43.5T330-210q0-52 31-91.5t79-53.5v-85H280q-33 0-56.5-23.5T200-520v-80h-60q-17 0-28.5-11.5T100-640v-200q0-17 11.5-28.5T140-880h200q17 0 28.5 11.5T380-840v200q0 17-11.5 28.5T340-600h-60v80h400v-85q-48-14-79-53.5T570-750q0-63 43.5-106.5T720-900q63 0 106.5 43.5T870-750q0 52-31 91.5T760-605v85q0 33-23.5 56.5T680-440H520v85q48 14 79 53.5t31 91.5q0 63-43.5 106.5T480-60Z"/>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 960">
2
+ <path d="M480.31,873.58c-39.36,0-72.62-13.59-99.8-40.76-27.18-27.18-40.76-60.44-40.76-99.8,0-32.49,9.68-61.07,29.05-85.74,19.37-24.68,44.04-41.39,74.03-50.13v-79.65h-149.94c-20.62,0-38.26-7.34-52.95-22.02s-22.02-32.33-22.02-52.95v-74.97h-56.23c-10.62,0-19.52-3.59-26.71-10.78s-10.78-16.09-10.78-26.71v-187.42c0-10.62,3.59-19.52,10.78-26.71s16.09-10.78,26.71-10.78h187.42c10.62,0,19.52,3.59,26.71,10.78s10.78,16.09,10.78,26.71v187.42c0,10.62-3.59,19.52-10.78,26.71s-16.09,10.78-26.71,10.78h-56.23v74.97h374.84v-79.65c-29.99-8.75-54.66-25.46-74.03-50.13s-29.05-53.26-29.05-85.74c0-39.36,13.59-72.62,40.76-99.8,27.18-27.18,60.44-40.76,99.8-40.76s72.62,13.59,99.8,40.76,40.76,60.44,40.76,99.8c0,32.49-9.68,61.07-29.05,85.74s-44.04,41.39-74.03,50.13v79.65c0,20.62-7.34,38.26-22.02,52.95s-32.33,22.02-52.95,22.02h-149.94v79.65c29.99,8.75,54.66,25.46,74.03,50.13,19.37,24.68,29.05,53.26,29.05,85.74,0,39.36-13.59,72.62-40.76,99.8-27.18,27.18-60.44,40.76-99.8,40.76Z"/>
3
3
  </svg>
@@ -16,13 +16,19 @@ const { element } = defineProps<{
16
16
 
17
17
  const inlinerMathStorage =
18
18
  (await useElementStorage(element)) ??
19
- (await createInlinerMathStorage(element.data));
19
+ (await createInlinerMathStorage(element.data.katex));
20
20
  </script>
21
21
 
22
22
  <template>
23
23
  <Inliner :element>
24
24
  <template v-if="inlinerMathStorage.type === 'text'">
25
- <span :class="[$style.inlinerMath, $style.textMath]">
25
+ <span
26
+ :class="[
27
+ $style.inlinerMath,
28
+ $style.textMath,
29
+ element.data.currentColor && $style.currentColor,
30
+ ]"
31
+ >
26
32
  <template v-for="token of inlinerMathStorage.tokens">
27
33
  <span :class="{ [$style.word]: token.type === 'word' }">
28
34
  {{ token.value }}
@@ -32,7 +38,10 @@ const inlinerMathStorage =
32
38
  </template>
33
39
  <Katex
34
40
  v-else
35
- :class="$style.inlinerMath"
41
+ :class="[
42
+ $style.inlinerMath,
43
+ element.data.currentColor && $style.currentColor,
44
+ ]"
36
45
  :math="inlinerMathStorage.mathHtml"
37
46
  mode="inline"
38
47
  :freeze="false"
@@ -42,10 +51,12 @@ const inlinerMathStorage =
42
51
 
43
52
  <style module>
44
53
  .inlinerMath {
45
- --katex-color_default: light-dark(
46
- color-mix(in hsl, var(--color-text-muted), var(--color-brand) 35%),
47
- color-mix(in hsl, var(--color-text), var(--color-brand) 30%)
48
- );
54
+ &:not(.currentColor) {
55
+ --katex-color_default: light-dark(
56
+ color-mix(in hsl, var(--color-text-muted), var(--color-brand) 35%),
57
+ color-mix(in hsl, var(--color-text), var(--color-brand) 30%)
58
+ );
59
+ }
49
60
  }
50
61
 
51
62
  .textMath {
@@ -19,11 +19,13 @@ declare const _default: readonly [{
19
19
  };
20
20
  }, {
21
21
  readonly schema: import("./inliner.js").InlinerMathSchema;
22
- readonly tags: [import("../../tag.js").ToEruditTag<import("./inliner.js").InlinerMathSchema, "M", import("tsprose").RequiredChildren>];
22
+ readonly tags: [import("../../tag.js").ToEruditTag<import("./inliner.js").InlinerMathSchema, "M", {
23
+ currentColor?: true;
24
+ } & import("tsprose").RequiredChildren>];
23
25
  readonly createStorage: (element: {
24
26
  schema: import("./inliner.js").InlinerMathSchema;
25
27
  id: string;
26
- data: string;
28
+ data: import("./inliner.js").InlinerMathData;
27
29
  storageKey: string;
28
30
  children: undefined;
29
31
  __TSPROSE_proseElement: true;
@@ -24,19 +24,27 @@ export interface InlinerMathSchema extends Schema {
24
24
  name: 'inlinerMath';
25
25
  type: 'inliner';
26
26
  linkable: true;
27
- Data: string;
27
+ Data: InlinerMathData;
28
28
  Storage: InlinerMathStorage;
29
29
  Children: undefined;
30
30
  }
31
+ export interface InlinerMathData {
32
+ katex: string;
33
+ currentColor?: boolean;
34
+ }
31
35
  export declare const inlinerMathSchema: InlinerMathSchema;
32
- export declare const M: import("../../tag.js").ToEruditTag<InlinerMathSchema, "M", RequiredChildren>;
36
+ export declare const M: import("../../tag.js").ToEruditTag<InlinerMathSchema, "M", {
37
+ currentColor?: true;
38
+ } & RequiredChildren>;
33
39
  export declare const inlinerMathCoreElement: {
34
40
  readonly schema: InlinerMathSchema;
35
- readonly tags: [import("../../tag.js").ToEruditTag<InlinerMathSchema, "M", RequiredChildren>];
41
+ readonly tags: [import("../../tag.js").ToEruditTag<InlinerMathSchema, "M", {
42
+ currentColor?: true;
43
+ } & RequiredChildren>];
36
44
  readonly createStorage: (element: {
37
45
  schema: InlinerMathSchema;
38
46
  id: string;
39
- data: string;
47
+ data: InlinerMathData;
40
48
  storageKey: string;
41
49
  children: undefined;
42
50
  __TSPROSE_proseElement: true;
@@ -50,19 +50,22 @@ export const inlinerMathSchema = defineSchema({
50
50
  export const M = defineEruditTag({
51
51
  tagName: "M",
52
52
  schema: inlinerMathSchema
53
- })(({ element, tagName, children }) => {
53
+ })(({ props, element, tagName, children }) => {
54
54
  ensureTagChildren(tagName, children, textSchema);
55
55
  const katex = normalizeKatex(children[0].data);
56
56
  if (!katex) {
57
57
  throw new EruditProseError(`<${tagName}> tag must contain non-empty KaTeX math expression.`);
58
58
  }
59
- element.data = katex;
59
+ element.data = { katex };
60
60
  element.storageKey = `$ ${katex} $`;
61
+ if (props.currentColor) {
62
+ element.data.currentColor = true;
63
+ }
61
64
  });
62
65
  export const inlinerMathCoreElement = defineProseCoreElement({
63
66
  schema: inlinerMathSchema,
64
67
  tags: [M],
65
- createStorage: async (element) => createInlinerMathStorage(element.data),
68
+ createStorage: async (element) => createInlinerMathStorage(element.data.katex),
66
69
  dependencies: katexDependency
67
70
  });
68
71
  export async function createInlinerMathStorage(katex) {
@@ -231,6 +231,12 @@ export const Problems = '_tag_';
231
231
  * - `<ProblemCheck>` (repeatable)
232
232
  * - `<ProblemNote>`
233
233
  *
234
+ * ### Props
235
+ * - `label` — optional tab label string
236
+ * - `standalone` — when set, hides any shared description blocks defined at the
237
+ * `<Problems>` level for this sub-problem only. Useful when a general task
238
+ * applies to all sub-problems except certain ones.
239
+ *
234
240
  * @title SubProblem
235
241
  * @layout block
236
242
  * @example
@@ -243,6 +249,22 @@ export const Problems = '_tag_';
243
249
  * <ProblemCheck answer={42} />
244
250
  * </SubProblem>
245
251
  * ```
252
+ * @example
253
+ * ```tsx
254
+ * <Problems title="Problem Set">
255
+ * <P>Find all roots.</P>
256
+ * <SubProblem>
257
+ * <ProblemDescription>
258
+ * This text is visible along with the "Find all roots" description since standalone is not set.
259
+ * </ProblemDescription>
260
+ * </SubProblem>
261
+ * <SubProblem standalone>
262
+ * <ProblemDescription>
263
+ * Custom problem description. No "Find all roots" text shown since standalone is set.
264
+ * </ProblemDescription>
265
+ * </SubProblem>
266
+ * </Problems>
267
+ * ```
246
268
  */
247
269
  export const SubProblem = '_tag_';
248
270
 
@@ -189,6 +189,13 @@ onMounted(async () => {
189
189
  }
190
190
  });
191
191
 
192
+ function stripIds(el: ToProseElement<any>): void {
193
+ delete (el as any).id;
194
+ for (const child of (el.children ?? []) as ToProseElement<any>[]) {
195
+ stripIds(child);
196
+ }
197
+ }
198
+
192
199
  let currentSeed: ProblemSeed = DEFAULT_SEED;
193
200
  async function doGenerate(seed: ProblemSeed) {
194
201
  if (!scriptInstance.value) {
@@ -207,7 +214,9 @@ async function doGenerate(seed: ProblemSeed) {
207
214
  const proseElements: ToProseElement<ProblemContentChild>[] = [];
208
215
  for (const rawElement of rawElements) {
209
216
  const resolveResult = await eruditRawToProse({ rawProse: rawElement });
210
- proseElements.push(resolveResult.prose as any);
217
+ const prose = resolveResult.prose as ToProseElement<ProblemContentChild>;
218
+ stripIds(prose);
219
+ proseElements.push(prose);
211
220
  }
212
221
 
213
222
  if (currentSeed !== seed) {
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, watchEffect } from 'vue';
2
+ import { ref, computed, watchEffect } from 'vue';
3
3
  import {
4
4
  isProseElement,
5
5
  type BlockProseElement,
@@ -36,6 +36,10 @@ const subProblems = element.children.filter((child) =>
36
36
 
37
37
  const activeSubProblemI = ref(0);
38
38
 
39
+ const showShared = computed(
40
+ () => subProblems[activeSubProblemI.value]?.data.standalone !== true,
41
+ );
42
+
39
43
  function getUnlabeledOrdinal(index: number) {
40
44
  let count = 0;
41
45
  for (let i = 0; i <= index; i++) {
@@ -57,7 +61,10 @@ watchEffect(() => {
57
61
  <Block :element>
58
62
  <ProblemContainer>
59
63
  <ProblemHeader :info="element.data" />
60
- <div v-if="sharedChildren.length" class="pt-(--proseAsideWidth)">
64
+ <div
65
+ v-if="sharedChildren.length && showShared"
66
+ class="pt-(--proseAsideWidth)"
67
+ >
61
68
  <Render v-for="sharedChild of sharedChildren" :element="sharedChild" />
62
69
  </div>
63
70
  <div
@@ -6,6 +6,7 @@ import {
6
6
  checkProblemAnswer,
7
7
  fromSerializableValidator,
8
8
  type ProblemCheckSchema,
9
+ type ProblemCheckContext,
9
10
  } from '../../problemCheck.js';
10
11
  import checkIcon from '../../assets/actions/check.svg?raw';
11
12
  import plusIcon from '../../../../app/shared/assets/plus.svg?raw';
@@ -14,7 +15,7 @@ import { useFormatText } from '../../../../app/composables/formatText.js';
14
15
  import { useProseContext } from '../../../../app/index.js';
15
16
  import { useProblemPhrase } from '../../composables/phrase.js';
16
17
 
17
- type CheckStatus = 'default' | 'correct' | 'wrong';
18
+ type CheckStatus = 'default' | 'loading' | 'correct' | 'wrong';
18
19
 
19
20
  type CheckState = {
20
21
  icon: string;
@@ -40,6 +41,13 @@ const emit = defineEmits<{
40
41
  (event: 'status-change', status: CheckStatus): void;
41
42
  }>();
42
43
 
44
+ const state = ref<CheckStatus>('default');
45
+
46
+ const formatText = useFormatText();
47
+ const { EruditIcon, EruditTransition, loadingSvg, problemCheckers } =
48
+ useProseContext();
49
+ const phrase = await useProblemPhrase();
50
+
43
51
  const states: Record<CheckStatus, CheckState> = {
44
52
  default: {
45
53
  icon: checkIcon,
@@ -48,6 +56,12 @@ const states: Record<CheckStatus, CheckState> = {
48
56
  buttonClass:
49
57
  'text-text-muted border-border hocus:border-text-muted hocus:text-text',
50
58
  },
59
+ loading: {
60
+ icon: loadingSvg,
61
+ labelClass: 'text-text-muted',
62
+ inputClass: 'border-border text-text',
63
+ buttonClass: 'text-text-muted border-border',
64
+ },
51
65
  correct: {
52
66
  icon: successIcon,
53
67
  labelClass: 'text-lime-600',
@@ -63,11 +77,8 @@ const states: Record<CheckStatus, CheckState> = {
63
77
  },
64
78
  };
65
79
 
66
- const state = ref<CheckStatus>('default');
67
-
68
- const formatText = useFormatText();
69
- const { EruditIcon, EruditTransition } = useProseContext();
70
- const phrase = await useProblemPhrase();
80
+ const currentState = computed(() => states[state.value]);
81
+ const isLoading = computed(() => state.value === 'loading');
71
82
 
72
83
  const hint = computed(() => {
73
84
  if (check.data.hint) {
@@ -109,7 +120,7 @@ watch(answerInput, () => {
109
120
  emit('status-change', 'default');
110
121
  });
111
122
 
112
- function doCheck() {
123
+ async function doCheck() {
113
124
  const newInput = answerInput.value.replace(/\s+/g, ' ').trim();
114
125
 
115
126
  if (newInput === lastCheckedInput.value) {
@@ -117,6 +128,7 @@ function doCheck() {
117
128
  }
118
129
 
119
130
  lastCheckedInput.value = newInput;
131
+ state.value = 'loading';
120
132
 
121
133
  if (script) {
122
134
  const ok = script.check(newInput || undefined);
@@ -125,12 +137,17 @@ function doCheck() {
125
137
  return;
126
138
  }
127
139
 
128
- state.value = checkProblemAnswer(
140
+ const checkContext: ProblemCheckContext = {
141
+ yesRegexp: phrase.yes_regexp,
142
+ noRegexp: phrase.no_regexp,
143
+ checkers: problemCheckers,
144
+ };
145
+
146
+ state.value = (await checkProblemAnswer(
129
147
  newInput,
130
- phrase.yes_regexp,
131
- phrase.no_regexp,
132
148
  validator.value,
133
- )
149
+ checkContext,
150
+ ))
134
151
  ? 'correct'
135
152
  : 'wrong';
136
153
 
@@ -143,7 +160,7 @@ function doCheck() {
143
160
  <div
144
161
  :class="[
145
162
  'text-main-sm font-medium transition-[color]',
146
- states[state].labelClass,
163
+ currentState.labelClass,
147
164
  ]"
148
165
  >
149
166
  <span @click="answerInputElement?.focus()">
@@ -157,36 +174,38 @@ function doCheck() {
157
174
  v-model="answerInput"
158
175
  type="text"
159
176
  autocomplete="off"
177
+ :disabled="isLoading"
160
178
  :placeholder="check.data.placeholder"
161
179
  :class="[
162
180
  `bg-bg-main text-main-sm focus:ring-brand relative z-10 min-w-0 flex-1
163
181
  rounded rounded-tr-none rounded-br-none border border-r-0 px-2.5 py-1
164
182
  ring-2 ring-transparent outline-0
165
183
  transition-[border,color,background,box-shadow]`,
166
- states[state].inputClass,
184
+ currentState.inputClass,
167
185
  ]"
168
186
  />
169
187
 
170
188
  <button
171
189
  type="submit"
190
+ :disabled="isLoading"
172
191
  :class="[
173
192
  `bg-bg-main relative w-[50px] cursor-pointer rounded rounded-tl-none
174
193
  rounded-bl-none border outline-0 transition-[border,color,background]`,
175
- states[state].buttonClass,
194
+ currentState.buttonClass,
176
195
  ]"
177
196
  >
178
197
  <EruditIcon
179
198
  :key="state"
180
- :name="states[state].icon"
181
- :class="['invisible m-auto text-[1.2em]', states[state].iconClass]"
199
+ :name="currentState.icon"
200
+ :class="['invisible m-auto text-[1.2em]', currentState.iconClass]"
182
201
  />
183
202
  <EruditTransition>
184
203
  <EruditIcon
185
204
  :key="state"
186
- :name="states[state].icon"
205
+ :name="currentState.icon"
187
206
  :class="[
188
207
  'absolute top-1/2 left-1/2 -translate-1/2 text-[1.2em]',
189
- states[state].iconClass,
208
+ currentState.iconClass,
190
209
  ]"
191
210
  />
192
211
  </EruditTransition>
@@ -18,6 +18,7 @@ declare const _default: readonly [{
18
18
  readonly schema: import("./problems.js").SubProblemSchema;
19
19
  readonly tags: [import("../../tag.js").ToEruditTag<import("./problems.js").SubProblemSchema, "SubProblem", {
20
20
  label?: string;
21
+ standalone?: true;
21
22
  } & ({
22
23
  children: {};
23
24
  script?: undefined;
@@ -1,5 +1,6 @@
1
1
  import type { XOR } from 'ts-xor';
2
2
  import { type OptionalChildren, type Schema } from 'tsprose';
3
+ import { type ProblemCheckObject, type ProblemCheckers } from '@erudit-js/core/problemCheck';
3
4
  export interface ProblemCheckInfo {
4
5
  label?: string;
5
6
  hint?: string;
@@ -62,11 +63,12 @@ export declare const problemCheckCoreElement: {
62
63
  script: string;
63
64
  }> & OptionalChildren)>];
64
65
  };
66
+ export { type ProblemCheckObject, isProblemCheckObject, type ProblemCheckers, } from '@erudit-js/core/problemCheck';
65
67
  export interface ProblemCheckValidatorBoolean {
66
68
  type: 'boolean';
67
69
  answer: boolean;
68
70
  }
69
- export type ProblemCheckValue = undefined | number | string | RegExp;
71
+ export type ProblemCheckValue = undefined | number | string | RegExp | ProblemCheckObject;
70
72
  export type ProblemCheckValueDefined = Exclude<ProblemCheckValue, undefined>;
71
73
  export interface ProblemCheckValidatorValue {
72
74
  type: 'value';
@@ -83,13 +85,13 @@ export interface ProblemCheckValidatorScript {
83
85
  name: string;
84
86
  }
85
87
  export type ProblemCheckValidator = ProblemCheckValidatorBoolean | ProblemCheckValidatorValue | ProblemCheckValidatorArray | ProblemCheckValidatorScript;
86
- export declare function toSerializableValidator(validator: ProblemCheckValidator): ProblemCheckValidatorBoolean | ProblemCheckValidatorScript | {
88
+ export declare function toSerializableValidator(validator: ProblemCheckValidator): ProblemCheckValidatorBoolean | ProblemCheckValidatorValue | ProblemCheckValidatorArray | ProblemCheckValidatorScript | {
87
89
  type: string;
88
- answer: string | number | {
90
+ answer: string | number | ProblemCheckObject | {
89
91
  readonly type: "regexp";
90
92
  readonly source: string;
91
93
  readonly flags: string;
92
- } | (string | number | {
94
+ } | (string | number | ProblemCheckObject | {
93
95
  readonly type: "regexp";
94
96
  readonly source: string;
95
97
  readonly flags: string;
@@ -101,16 +103,21 @@ export declare function toSerializableValidator(validator: ProblemCheckValidator
101
103
  type: string;
102
104
  ordered: boolean;
103
105
  separator: string;
104
- answers: (string | number | {
106
+ answers: (string | number | ProblemCheckObject | {
105
107
  readonly type: "regexp";
106
108
  readonly source: string;
107
109
  readonly flags: string;
108
- } | (string | number | {
110
+ } | (string | number | ProblemCheckObject | {
109
111
  readonly type: "regexp";
110
112
  readonly source: string;
111
113
  readonly flags: string;
112
114
  } | undefined)[] | undefined)[];
113
115
  answer?: undefined;
114
116
  };
115
- export declare function fromSerializableValidator(serializedValidator: any): ProblemCheckValidator;
116
- export declare function checkProblemAnswer(answer: string, yesRegexp: RegExp, noRegexp: RegExp, validator: ProblemCheckValidator): boolean;
117
+ export declare function fromSerializableValidator(serializedValidator: any): ProblemCheckValidator | ProblemCheckObject;
118
+ export interface ProblemCheckContext {
119
+ yesRegexp: RegExp;
120
+ noRegexp: RegExp;
121
+ checkers: ProblemCheckers;
122
+ }
123
+ export declare function checkProblemAnswer(answer: string, against: ProblemCheckValidator | ProblemCheckObject, context: ProblemCheckContext): Promise<boolean>;
@@ -1,4 +1,5 @@
1
1
  import { defineSchema, ensureTagChildren } from "tsprose";
2
+ import { isProblemCheckObject } from "@erudit-js/core/problemCheck";
2
3
  import { defineEruditTag } from "../../tag.js";
3
4
  import { defineProseCoreElement } from "../../coreElement.js";
4
5
  import { EruditProseError } from "../../error.js";
@@ -46,9 +47,17 @@ export const ProblemCheck = defineEruditTag({
46
47
  answer: !!props.boolean
47
48
  };
48
49
  } else if ("answer" in props) {
50
+ const answerProp = props.answer;
51
+ if (!Array.isArray(answerProp) && isProblemCheckObject(answerProp)) {
52
+ element.data = {
53
+ ...checkInfo,
54
+ serializedValidator: answerProp
55
+ };
56
+ return;
57
+ }
49
58
  validator = {
50
59
  type: "value",
51
- answer: props.answer
60
+ answer: answerProp
52
61
  };
53
62
  } else if ("answers" in props) {
54
63
  const answersProp = props.answers;
@@ -83,7 +92,14 @@ export const problemCheckCoreElement = defineProseCoreElement({
83
92
  schema: problemCheckSchema,
84
93
  tags: [ProblemCheck]
85
94
  });
95
+ //
96
+ // ProblemCheck Data
97
+ //
98
+ export { isProblemCheckObject } from "@erudit-js/core/problemCheck";
86
99
  export function toSerializableValidator(validator) {
100
+ if (validator.__ERUDIT_CHECK === true) {
101
+ return validator;
102
+ }
87
103
  if (validator.type === "boolean") {
88
104
  return validator;
89
105
  }
@@ -122,6 +138,9 @@ export function toSerializableValidator(validator) {
122
138
  throw new Error(`Unknown ProblemCheckData type "${validator.type}"!`);
123
139
  }
124
140
  export function fromSerializableValidator(serializedValidator) {
141
+ if (serializedValidator.__ERUDIT_CHECK === true) {
142
+ return serializedValidator;
143
+ }
125
144
  if (serializedValidator.type === "boolean") {
126
145
  return serializedValidator;
127
146
  }
@@ -155,11 +174,27 @@ export function fromSerializableValidator(serializedValidator) {
155
174
  }
156
175
  throw new Error(`Unknown ProblemCheckData type "${serializedValidator.type}"!`);
157
176
  }
158
- export function checkProblemAnswer(answer, yesRegexp, noRegexp, validator) {
159
- if (validator.type === "boolean") {
160
- return validator.answer === true ? yesRegexp.test(answer) : noRegexp.test(answer);
177
+ export async function checkProblemAnswer(answer, against, context) {
178
+ if (isProblemCheckObject(against)) {
179
+ const checker = context.checkers[against.name];
180
+ if (!checker) {
181
+ console.warn(`No problem checker found for "${against.name}"`);
182
+ return false;
183
+ }
184
+ return await checker.check(against.data, answer);
161
185
  }
162
- const checkDefinedAnswer = (expected, answer) => {
186
+ if (against.type === "boolean") {
187
+ return against.answer === true ? context.yesRegexp.test(answer) : context.noRegexp.test(answer);
188
+ }
189
+ const checkDefinedAnswer = async (expected, answer) => {
190
+ if (isProblemCheckObject(expected)) {
191
+ const checker = context.checkers[expected.name];
192
+ if (!checker) {
193
+ console.warn(`No problem checker found for "${expected.name}"`);
194
+ return false;
195
+ }
196
+ return await checker.check(expected.data, answer);
197
+ }
163
198
  if (typeof expected === "number") {
164
199
  return Number(answer) === expected;
165
200
  }
@@ -168,42 +203,50 @@ export function checkProblemAnswer(answer, yesRegexp, noRegexp, validator) {
168
203
  }
169
204
  return answer === String(expected);
170
205
  };
171
- const checkAnswer = (expect, answer) => {
206
+ const checkAnswer = async (expect, answer) => {
172
207
  if (expect === undefined || expect === null) {
173
208
  return answer.trim() === "";
174
209
  }
175
210
  return checkDefinedAnswer(expect, answer);
176
211
  };
177
- if (validator.type === "value") {
178
- const anyOf = Array.isArray(validator.answer) ? validator.answer : [validator.answer];
179
- return anyOf.some((expect) => checkAnswer(expect, answer));
212
+ if (against.type === "value") {
213
+ const anyOf = Array.isArray(against.answer) ? against.answer : [against.answer];
214
+ const results = await Promise.all(anyOf.map((expect) => checkAnswer(expect, answer)));
215
+ return results.some(Boolean);
180
216
  }
181
- if (validator.type === "array") {
182
- const separatorRegexp = new RegExp(`\\s*${validator.separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "g");
217
+ if (against.type === "array") {
218
+ const separatorRegexp = new RegExp(`\\s*${against.separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "g");
183
219
  const parts = answer.split(separatorRegexp).map((p) => p.trim());
184
- const checkExpected = (expected, actual) => {
220
+ const checkExpected = async (expected, actual) => {
185
221
  if (Array.isArray(expected)) {
186
- // any-of logic
187
- return expected.some((e) => checkDefinedAnswer(e, actual));
222
+ const results = await Promise.all(expected.map((e) => checkDefinedAnswer(e, actual)));
223
+ return results.some(Boolean);
188
224
  }
189
225
  return checkDefinedAnswer(expected, actual);
190
226
  };
191
- if (parts.length !== validator.answers.length) {
227
+ if (parts.length !== against.answers.length) {
192
228
  return false;
193
229
  }
194
- if (validator.ordered) {
195
- return validator.answers.every((expected, i) => checkExpected(expected, parts[i]));
230
+ if (against.ordered) {
231
+ const results = await Promise.all(against.answers.map((expected, i) => checkExpected(expected, parts[i])));
232
+ return results.every(Boolean);
196
233
  }
197
234
  // unordered matching (multiset semantics)
198
235
  const remaining = [...parts];
199
- for (const expected of validator.answers) {
200
- const index = remaining.findIndex((actual) => checkExpected(expected, actual));
201
- if (index === -1) {
236
+ for (const expected of against.answers) {
237
+ let foundIndex = -1;
238
+ for (let i = 0; i < remaining.length; i++) {
239
+ if (await checkExpected(expected, remaining[i])) {
240
+ foundIndex = i;
241
+ break;
242
+ }
243
+ }
244
+ if (foundIndex === -1) {
202
245
  return false;
203
246
  }
204
- remaining.splice(index, 1);
247
+ remaining.splice(foundIndex, 1);
205
248
  }
206
249
  return true;
207
250
  }
208
- throw new EruditProseError(`"checkProblemAnswer" not implemented for type "${validator.type}"!`);
251
+ throw new EruditProseError(`"checkProblemAnswer" not implemented for type "${against.type}"!`);
209
252
  }
@@ -1,4 +1,4 @@
1
- import { defineSchema, ensureTagBlockChildren, ensureTagChildren, isRawBlock, isRawElement } from "tsprose";
1
+ import { defineSchema, ensureTagBlockChildren, ensureTagChildren, isRawElement } from "tsprose";
2
2
  import { defineEruditTag } from "../../tag.js";
3
3
  import { uppercaseFirst } from "../../utils/case.js";
4
4
  import { paragraphWrap } from "../../shared/paragraphWrap.js";
@@ -76,7 +76,7 @@ function defineProblemSectionContainer(name) {
76
76
  tagName,
77
77
  schema
78
78
  })(({ element, children }) => {
79
- const head = [];
79
+ const headRaw = [];
80
80
  const sections = [];
81
81
  let seenSection = false;
82
82
  for (const child of children) {
@@ -84,15 +84,13 @@ function defineProblemSectionContainer(name) {
84
84
  seenSection = true;
85
85
  sections.push(child);
86
86
  } else {
87
- if (!isRawBlock(child)) {
88
- throw new EruditProseError(`${tagName} cannot have inline children (found "${child.schema.name}").`);
89
- }
90
87
  if (seenSection) {
91
88
  throw new EruditProseError(`${tagName}: non-section children must come before <ProblemSection>.`);
92
89
  }
93
- head.push(child);
90
+ headRaw.push(child);
94
91
  }
95
92
  }
93
+ const head = headRaw.length > 0 ? toBlockChildren(tagName, headRaw) : [];
96
94
  element.children = [...head, ...sections];
97
95
  });
98
96
  const coreElement = defineProseCoreElement({
@@ -13,11 +13,13 @@ export interface SubProblemSchema extends Schema {
13
13
  }
14
14
  export interface SubProblemData {
15
15
  label?: string;
16
+ standalone?: true;
16
17
  scriptUniques?: Record<string, Unique>;
17
18
  }
18
19
  export declare const subProblemSchema: SubProblemSchema;
19
20
  export declare const SubProblem: import("../../tag.js").ToEruditTag<SubProblemSchema, "SubProblem", {
20
21
  label?: string;
22
+ standalone?: true;
21
23
  } & ({
22
24
  children: {};
23
25
  script?: undefined;
@@ -29,6 +31,7 @@ export declare const subProblemCoreElement: {
29
31
  readonly schema: SubProblemSchema;
30
32
  readonly tags: [import("../../tag.js").ToEruditTag<SubProblemSchema, "SubProblem", {
31
33
  label?: string;
34
+ standalone?: true;
32
35
  } & ({
33
36
  children: {};
34
37
  script?: undefined;
@@ -20,6 +20,9 @@ export const SubProblem = defineEruditTag({
20
20
  if (label) {
21
21
  element.data.label = label;
22
22
  }
23
+ if (props.standalone) {
24
+ element.data.standalone = true;
25
+ }
23
26
  if (props.script && children) {
24
27
  throw new EruditProseError(`<${tagName}> cannot have both script and children!`);
25
28
  }
@@ -36,12 +36,22 @@ const rows = computed(() =>
36
36
  <tr
37
37
  v-for="row in rows"
38
38
  :key="row.id"
39
- class="odd:bg-(--oddCellBg) even:bg-(--evenCellBg)"
39
+ class="group odd:bg-(--oddCellBg) even:bg-(--evenCellBg)"
40
40
  >
41
41
  <td
42
42
  v-for="cell in row.children"
43
43
  :key="cell.id"
44
- class="py-small px-normal rounded"
44
+ :class="[
45
+ `py-small px-normal group-hocus:inset-ring-(--tableBorder)
46
+ rounded inset-ring-2 inset-ring-transparent
47
+ transition-[box-shadow]`,
48
+ cell.data?.center
49
+ ? 'text-center'
50
+ : cell.data?.right
51
+ ? 'text-right'
52
+ : '',
53
+ cell.data?.freeze && 'whitespace-nowrap',
54
+ ]"
45
55
  >
46
56
  <Render
47
57
  v-for="inliner of cell.children"
@@ -59,41 +69,38 @@ const rows = computed(() =>
59
69
 
60
70
  <style module>
61
71
  .table {
62
- --tableBorder: color-mix(
63
- in srgb,
64
- var(--color-brand),
65
- var(--color-border) 85%
72
+ --tableBorder: light-dark(
73
+ color-mix(in hsl, var(--color-brand), var(--color-border) 70%),
74
+ color-mix(in hsl, var(--color-brand), var(--color-border) 85%)
66
75
  );
67
76
 
68
- --evenCellBg: color-mix(
69
- in srgb,
70
- light-dark(#f5f5f5, #282828),
71
- var(--color-brand) 3%
77
+ --evenCellBg: light-dark(
78
+ color-mix(in hsl, #f5f5f5, var(--color-brand) 12%),
79
+ color-mix(in hsl, #2b2b2b, var(--color-brand) 4%)
72
80
  );
73
81
 
74
- --oddCellBg: color-mix(
75
- in srgb,
76
- light-dark(#f5f5f5, #282828),
77
- var(--color-brand) 10%
82
+ --oddCellBg: light-dark(
83
+ color-mix(in hsl, #f5f5f5, var(--color-brand) 18%),
84
+ color-mix(in hsl, #2b2b2b, var(--color-brand) 8%)
78
85
  );
79
86
 
80
87
  [data-prose-accent] & {
81
88
  --tableBorder: color-mix(
82
89
  in srgb,
83
- var(--accentBorder),
84
- var(--color-border) 50%
90
+ var(--accentText),
91
+ var(--color-border) 60%
85
92
  );
86
93
 
87
94
  --evenCellBg: color-mix(
88
95
  in srgb,
89
- light-dark(var(--color-bg-main), #202020),
96
+ light-dark(color-mix(in srgb, #fff, var(--accentText) 13%), #252525),
90
97
  var(--accentText) 12%
91
98
  );
92
99
 
93
100
  --oddCellBg: color-mix(
94
101
  in srgb,
95
- light-dark(var(--color-bg-main), #202020),
96
- var(--accentText) 18%
102
+ light-dark(color-mix(in srgb, #fff, var(--accentText) 13%), #252525),
103
+ var(--accentText) 19%
97
104
  );
98
105
  }
99
106
  }
@@ -1,15 +1,27 @@
1
1
  import { type InlinerSchema, type Schema } from 'tsprose';
2
+ import type { XOR } from 'ts-xor';
2
3
  import { type CaptionSchema } from '../caption/core.js';
3
4
  export interface TableDataSchema extends Schema {
4
5
  name: 'tableData';
5
6
  type: 'inliner';
6
7
  linkable: false;
7
- Data: undefined;
8
+ Data: TableDataData | undefined;
8
9
  Storage: undefined;
9
10
  Children: InlinerSchema[];
10
11
  }
12
+ export interface TableDataData {
13
+ center?: boolean;
14
+ right?: boolean;
15
+ freeze?: boolean;
16
+ }
11
17
  export declare const tableDataSchema: TableDataSchema;
12
- export declare const Td: import("../../tag.js").ToEruditTag<TableDataSchema, "Td", unknown>;
18
+ export declare const Td: import("../../tag.js").ToEruditTag<TableDataSchema, "Td", {
19
+ freeze?: true;
20
+ } & XOR<{
21
+ center?: true;
22
+ }, {
23
+ right?: true;
24
+ }>>;
13
25
  export interface TableRowSchema extends Schema {
14
26
  name: 'tableRow';
15
27
  type: 'inliner';
@@ -32,7 +44,13 @@ export declare const tableSchema: TableSchema;
32
44
  export declare const Table: import("../../tag.js").ToEruditTag<TableSchema, "Table", unknown>;
33
45
  declare const _default: [{
34
46
  readonly schema: TableDataSchema;
35
- readonly tags: [import("../../tag.js").ToEruditTag<TableDataSchema, "Td", unknown>];
47
+ readonly tags: [import("../../tag.js").ToEruditTag<TableDataSchema, "Td", {
48
+ freeze?: true;
49
+ } & XOR<{
50
+ center?: true;
51
+ }, {
52
+ right?: true;
53
+ }>>];
36
54
  }, {
37
55
  readonly schema: TableRowSchema;
38
56
  readonly tags: [import("../../tag.js").ToEruditTag<TableRowSchema, "Tr", unknown>];
@@ -10,9 +10,21 @@ export const tableDataSchema = defineSchema({
10
10
  export const Td = defineEruditTag({
11
11
  tagName: "Td",
12
12
  schema: tableDataSchema
13
- })(({ element, children, tagName }) => {
13
+ })(({ props, element, children, tagName }) => {
14
14
  ensureTagInlinerChildren(tagName, children);
15
15
  element.children = children;
16
+ element.data = {};
17
+ if (props.center) {
18
+ element.data = { center: true };
19
+ } else if (props.right) {
20
+ element.data = { right: true };
21
+ }
22
+ if (props.freeze) {
23
+ element.data.freeze = true;
24
+ }
25
+ if (Object.keys(element.data).length === 0) {
26
+ delete element.data;
27
+ }
16
28
  });
17
29
  export const tableRowSchema = defineSchema({
18
30
  name: "tableRow",
@@ -1,3 +1,3 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
- <path d="M2.8,23.9c-.7,0-1.3-.3-1.9-.8-.5-.5-.8-1.1-.8-1.9V2.8C.1,2,.4,1.4.9.9,1.4.4,2,.1,2.8.1h18.5c.7,0,1.3.3,1.9.8s.8,1.1.8,1.9v18.5c0,.7-.3,1.3-.8,1.9-.5.5-1.1.8-1.9.8H2.8ZM10.7,16H2.8v5.3h7.9v-5.3ZM13.3,16v5.3h7.9v-5.3h-7.9ZM10.7,13.3v-5.3H2.8v5.3h7.9ZM13.3,13.3h7.9v-5.3h-7.9v5.3ZM2.8,5.4h18.5v-2.6H2.8v2.6Z"/>
2
+ <path d="M4.89,21.22c-.54,0-1.01-.23-1.47-.62-.39-.39-.62-.85-.62-1.47V4.89c0-.62.23-1.08.62-1.47.39-.39.85-.62,1.47-.62h14.32c.54,0,1.01.23,1.47.62.46.39.62.85.62,1.47v14.32c0,.54-.23,1.01-.62,1.47-.39.39-.85.62-1.47.62H4.89v-.08ZM11.01,15.11h-6.12v4.1h6.12s0-4.1,0-4.1ZM13.02,15.11v4.1h6.12v-4.1s-6.12,0-6.12,0ZM11.01,13.02v-4.1h-6.12v4.1h6.12ZM13.02,13.02h6.12v-4.1h-6.12s0,4.1,0,4.1ZM4.89,6.9h14.32v-2.01H4.89v2.01Z"/>
3
3
  </svg>
@@ -1,3 +1,3 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
- <path d="M10.4,16.4l5.8-3.7c.3-.2.4-.4.4-.7s-.1-.6-.4-.7l-5.8-3.7c-.3-.2-.6-.2-.9,0s-.5.4-.5.8v7.4c0,.4.2.6.5.8s.6.2.9,0ZM12,23.8c-1.6,0-3.2-.3-4.6-.9s-2.7-1.5-3.8-2.5-1.9-2.3-2.5-3.8S.2,13.6.2,12s.3-3.2.9-4.6,1.5-2.7,2.5-3.8,2.3-1.9,3.8-2.5S10.4.2,12,.2s3.2.3,4.6.9,2.7,1.5,3.8,2.5,1.9,2.3,2.5,3.8.9,3,.9,4.6-.3,3.2-.9,4.6-1.5,2.7-2.5,3.8-2.3,1.9-3.8,2.5-3,.9-4.6.9ZM12,21.5c2.6,0,4.9-.9,6.7-2.8s2.8-4.1,2.8-6.7-.9-4.9-2.8-6.7-4.1-2.8-6.7-2.8-4.9.9-6.7,2.8-2.8,4.1-2.8,6.7.9,4.9,2.8,6.7,4.1,2.8,6.7,2.8Z"/>
2
+ <path d="M10.67,15.66l4.83-3.08c.25-.17.33-.33.33-.58s-.08-.5-.33-.58l-4.83-3.08c-.25-.17-.5-.17-.75,0s-.42.33-.42.67v6.16c0,.33.17.5.42.67s.5.17.75,0v-.17ZM12,21.82c-1.33,0-2.66-.25-3.83-.75s-2.25-1.25-3.16-2.08-1.58-1.91-2.08-3.16-.75-2.5-.75-3.83.25-2.66.75-3.83,1.25-2.25,2.08-3.16,1.91-1.58,3.16-2.08,2.5-.75,3.83-.75,2.66.25,3.83.75c1.17.5,2.25,1.25,3.16,2.08s1.58,1.91,2.08,3.16.75,2.5.75,3.83-.25,2.66-.75,3.83c-.5,1.17-1.25,2.25-2.08,3.16s-1.91,1.58-3.16,2.08-2.5.75-3.83.75ZM12,19.91c2.16,0,4.08-.75,5.58-2.33s2.33-3.41,2.33-5.58-.75-4.08-2.33-5.58-3.41-2.33-5.58-2.33-4.08.75-5.58,2.33-2.33,3.41-2.33,5.58.75,4.08,2.33,5.58,3.41,2.33,5.58,2.33Z"/>
3
3
  </svg>
@@ -1,11 +1,38 @@
1
1
  import { EruditProseError } from "../error.js";
2
+ function normalizeForCompare(p) {
3
+ return p.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, d) => d.toLowerCase() + ":").replace(/\/+$/g, "");
4
+ }
5
+ function isAbsolutePath(p) {
6
+ // After normalization separators are '/', so check for:
7
+ // - Windows drive-root: C:/...
8
+ // - POSIX root: /...
9
+ // - UNC root: //server/share/...
10
+ return /^[A-Za-z]:\//.test(p) || p.startsWith("/") || p.startsWith("//");
11
+ }
12
+ function hasParentTraversal(relPath) {
13
+ return relPath.split("/").some((segment) => segment === "..");
14
+ }
2
15
  /**
3
16
  * Checks that the given absolute file path is within the given absolute project path.
4
17
  * Returns the file path relative to the project path.
5
18
  */
6
19
  export function projectRelFilePath(projectAbsPath, fileAbsPath) {
7
- if (!fileAbsPath.startsWith(projectAbsPath)) {
20
+ const project = normalizeForCompare(projectAbsPath);
21
+ const file = normalizeForCompare(fileAbsPath);
22
+ if (isAbsolutePath(file)) {
23
+ const projectPrefix = project + "/";
24
+ if (!file.startsWith(projectPrefix)) {
25
+ throw new EruditProseError(`File "${fileAbsPath}" is outside of project "${projectAbsPath}"`);
26
+ }
27
+ const rel = file.slice(projectPrefix.length);
28
+ if (!rel || hasParentTraversal(rel)) {
29
+ throw new EruditProseError(`File "${fileAbsPath}" is outside of project "${projectAbsPath}"`);
30
+ }
31
+ return rel;
32
+ }
33
+ const rel = file.replace(/^\.\/+/, "").replace(/^\/+/, "");
34
+ if (!rel || hasParentTraversal(rel)) {
8
35
  throw new EruditProseError(`File "${fileAbsPath}" is outside of project "${projectAbsPath}"`);
9
36
  }
10
- return fileAbsPath.slice(projectAbsPath.length + 1);
37
+ return rel;
11
38
  }
package/dist/snippet.d.ts CHANGED
@@ -52,6 +52,7 @@ export declare function toKeySnippet(snippet?: Snippet): KeySnippet | undefined;
52
52
  export interface SeoSnippet {
53
53
  title: string;
54
54
  description?: string;
55
+ titleInherited: boolean;
55
56
  }
56
57
  export declare function toSeoSnippet(snippet?: Snippet): SeoSnippet | undefined;
57
58
  export interface ResolvedSnippet {
package/dist/snippet.js CHANGED
@@ -118,21 +118,26 @@ export function toSeoSnippet(snippet) {
118
118
  if (snippet.seo === true) {
119
119
  return {
120
120
  title: snippet.title,
121
- description: snippet.description
121
+ description: snippet.description,
122
+ titleInherited: true
122
123
  };
123
124
  }
124
125
  if (typeof snippet.seo === "string") {
125
- const title = snippet.seo.trim() || snippet.title;
126
+ const manualTitle = snippet.seo.trim();
127
+ const title = manualTitle || snippet.title;
126
128
  return {
127
129
  title,
128
- description: snippet.description
130
+ description: snippet.description,
131
+ titleInherited: !manualTitle
129
132
  };
130
133
  }
131
- const title = snippet.seo.title?.trim() || snippet.title;
134
+ const manualTitle = snippet.seo.title?.trim();
135
+ const title = manualTitle || snippet.title;
132
136
  const description = snippet.seo.description?.trim() || snippet.description;
133
137
  return {
134
138
  title,
135
- description
139
+ description,
140
+ titleInherited: !manualTitle
136
141
  };
137
142
  }
138
143
  export const snippetHook = defineRawToProseHook(({ task: context, result }) => {
package/dist/toc.js CHANGED
@@ -19,8 +19,8 @@ export function finalizeToc(rawElement, tocTagProp) {
19
19
  return;
20
20
  }
21
21
  if (rawElement.toc === true) {
22
- // Implicitly add to TOC using general title
23
- const tocString = rawElement.title?.trim();
22
+ // Implicitly add to TOC using general title, fall back to finalized snippet title
23
+ const tocString = rawElement.title?.trim() || rawElement.snippet?.title?.trim();
24
24
  if (!tocString) {
25
25
  throw new EruditProseError("Addition to TOC was requested via internal toc: true flag, but unable to retrieve title!");
26
26
  }
@@ -32,8 +32,8 @@ export function finalizeToc(rawElement, tocTagProp) {
32
32
  return;
33
33
  }
34
34
  if (tocTagProp === true) {
35
- // Explicitly add to TOC using general internal toc string or general title
36
- const tocString = rawElement.toc?.trim() || rawElement.title?.trim();
35
+ // Explicitly add to TOC using general internal toc string, general title, or finalized snippet title
36
+ const tocString = rawElement.toc?.trim() || rawElement.title?.trim() || rawElement.snippet?.title?.trim();
37
37
  if (!tocString) {
38
38
  throw new EruditProseError("Addition to TOC was manually requested via true tag toc prop, but unable to retrieve title!");
39
39
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@erudit-js/prose",
4
- "version": "4.2.0-dev.1",
4
+ "version": "4.3.0-dev.1",
5
5
  "description": "📝 JSX prose subsystem for Erudit sites",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
- "types.d.ts"
25
+ "raw.d.ts"
26
26
  ],
27
27
  "license": "MIT",
28
28
  "scripts": {
@@ -32,12 +32,13 @@
32
32
  "prepack": "bun run build"
33
33
  },
34
34
  "dependencies": {
35
- "@erudit-js/core": "4.2.0-dev.1",
35
+ "@erudit-js/core": "4.3.0-dev.1",
36
36
  "@floating-ui/vue": "^1.1.10",
37
- "tsprose": "^1.0.0",
38
37
  "image-size": "^2.0.2",
39
38
  "katex": "^0.16.28",
39
+ "mermaid": "^11.12.3",
40
40
  "photoswipe": "^5.4.4",
41
+ "tsprose": "^1.0.1",
41
42
  "vue": "^3.5.28"
42
43
  },
43
44
  "devDependencies": {
package/raw.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*?raw' {
2
+ const content: string;
3
+ export default content;
4
+ }