react-cheminfo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -2
- package/bin/check-tokens.mjs +105 -0
- package/lib/about/core/about.d.ts +95 -0
- package/lib/about/core/about.d.ts.map +1 -0
- package/lib/about/core/about.js +78 -0
- package/lib/about/core/about.js.map +1 -0
- package/lib/about/core/index.d.ts +3 -0
- package/lib/about/core/index.d.ts.map +1 -0
- package/lib/about/core/index.js +2 -0
- package/lib/about/core/index.js.map +1 -0
- package/lib/about/ui/AboutCitations.d.ts +14 -0
- package/lib/about/ui/AboutCitations.d.ts.map +1 -0
- package/lib/about/ui/AboutCitations.js +40 -0
- package/lib/about/ui/AboutCitations.js.map +1 -0
- package/lib/about/ui/AboutPage.d.ts +32 -0
- package/lib/about/ui/AboutPage.d.ts.map +1 -0
- package/lib/about/ui/AboutPage.js +70 -0
- package/lib/about/ui/AboutPage.js.map +1 -0
- package/lib/about/ui/AboutSection.d.ts +23 -0
- package/lib/about/ui/AboutSection.d.ts.map +1 -0
- package/lib/about/ui/AboutSection.js +28 -0
- package/lib/about/ui/AboutSection.js.map +1 -0
- package/lib/about/ui/index.d.ts +7 -0
- package/lib/about/ui/index.d.ts.map +1 -0
- package/lib/about/ui/index.js +4 -0
- package/lib/about/ui/index.js.map +1 -0
- package/lib/chrome/ui/index.d.ts +1 -0
- package/lib/chrome/ui/index.d.ts.map +1 -1
- package/lib/chrome/ui/index.js +3 -0
- package/lib/chrome/ui/index.js.map +1 -1
- package/lib/citation/core/index.d.ts +1 -0
- package/lib/citation/core/index.d.ts.map +1 -1
- package/lib/citation/core/index.js +1 -0
- package/lib/citation/core/index.js.map +1 -1
- package/lib/citation/core/teachingPaper.d.ts +15 -0
- package/lib/citation/core/teachingPaper.d.ts.map +1 -0
- package/lib/citation/core/teachingPaper.js +32 -0
- package/lib/citation/core/teachingPaper.js.map +1 -0
- package/lib/clipboard/ui/CodeBlock.d.ts.map +1 -1
- package/lib/clipboard/ui/CodeBlock.js +4 -1
- package/lib/clipboard/ui/CodeBlock.js.map +1 -1
- package/lib/color/ui/ColorScaleLegend.js +1 -1
- package/lib/color/ui/ColorScaleLegend.js.map +1 -1
- package/lib/core.d.ts +3 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +5 -0
- package/lib/core.js.map +1 -1
- package/lib/credits/core/credits.d.ts +36 -0
- package/lib/credits/core/credits.d.ts.map +1 -1
- package/lib/credits/core/credits.js +42 -0
- package/lib/credits/core/credits.js.map +1 -1
- package/lib/credits/ui/CreditsList.js +1 -1
- package/lib/credits/ui/CreditsList.js.map +1 -1
- package/lib/delimited/ui/DelimitedTextPanel.js +1 -1
- package/lib/delimited/ui/DelimitedTextPanel.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +10 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/core/tokens.d.ts.map +1 -1
- package/lib/ecosystem/core/tokens.js +1 -0
- package/lib/ecosystem/core/tokens.js.map +1 -1
- package/lib/ecosystem/ui/EcosystemLinks.js +2 -2
- package/lib/ecosystem/ui/EcosystemLinks.js.map +1 -1
- package/lib/ecosystem/ui/EcosystemMenu.js +1 -1
- package/lib/ecosystem/ui/EcosystemMenu.js.map +1 -1
- package/lib/ecosystem/ui/SiteTile.d.ts.map +1 -1
- package/lib/ecosystem/ui/SiteTile.js +5 -5
- package/lib/ecosystem/ui/SiteTile.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +3 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/orbital/core/index.d.ts +1 -1
- package/lib/orbital/core/index.d.ts.map +1 -1
- package/lib/orbital/core/index.js +1 -1
- package/lib/orbital/core/index.js.map +1 -1
- package/lib/orbital/core/realHarmonics.d.ts +5 -2
- package/lib/orbital/core/realHarmonics.d.ts.map +1 -1
- package/lib/orbital/core/realHarmonics.js.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +18 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalCanvas.js +84 -7
- package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +13 -0
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.js +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -1
- package/lib/orbital/ui/camera.d.ts +15 -0
- package/lib/orbital/ui/camera.d.ts.map +1 -1
- package/lib/orbital/ui/camera.js +68 -0
- package/lib/orbital/ui/camera.js.map +1 -1
- package/lib/orbital/ui/renderVolume.d.ts.map +1 -1
- package/lib/orbital/ui/renderVolume.js +8 -1
- package/lib/orbital/ui/renderVolume.js.map +1 -1
- package/lib/orbital/ui/serial.d.ts +25 -0
- package/lib/orbital/ui/serial.d.ts.map +1 -0
- package/lib/orbital/ui/serial.js +25 -0
- package/lib/orbital/ui/serial.js.map +1 -0
- package/lib/orbital/ui/viewer.d.ts +23 -0
- package/lib/orbital/ui/viewer.d.ts.map +1 -1
- package/lib/orbital/ui/viewer.js +70 -17
- package/lib/orbital/ui/viewer.js.map +1 -1
- package/lib/pedagogy/ui/ExerciseProgressHeader.js +1 -1
- package/lib/pedagogy/ui/ExerciseProgressHeader.js.map +1 -1
- package/lib/pedagogy/ui/GlossaryText.js +2 -2
- package/lib/pedagogy/ui/GlossaryText.js.map +1 -1
- package/lib/pedagogy/ui/ReferenceSectionBlock.js +5 -2
- package/lib/pedagogy/ui/ReferenceSectionBlock.js.map +1 -1
- package/lib/pedagogy/ui/SyntaxTooltip.js +2 -2
- package/lib/pedagogy/ui/SyntaxTooltip.js.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts +15 -2
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.js +19 -5
- package/lib/pedagogy/ui/TutorialStepStrip.js.map +1 -1
- package/lib/pedagogy/ui/index.d.ts +1 -1
- package/lib/pedagogy/ui/index.d.ts.map +1 -1
- package/lib/pedagogy/ui/index.js.map +1 -1
- package/lib/periodic/ui/CategoryLegend.js +1 -1
- package/lib/periodic/ui/CategoryLegend.js.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.js +2 -2
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
- package/lib/seo/vite/ogCard.d.ts.map +1 -1
- package/lib/seo/vite/ogCard.js +1 -0
- package/lib/seo/vite/ogCard.js.map +1 -1
- package/lib/share/ui/ShareDialogContent.d.ts.map +1 -1
- package/lib/share/ui/ShareDialogContent.js +5 -2
- package/lib/share/ui/ShareDialogContent.js.map +1 -1
- package/lib/share/ui/SharePartOptions.js +1 -1
- package/lib/share/ui/SharePartOptions.js.map +1 -1
- package/lib/slides/core/demoLinks.d.ts +43 -0
- package/lib/slides/core/demoLinks.d.ts.map +1 -0
- package/lib/slides/core/demoLinks.js +104 -0
- package/lib/slides/core/demoLinks.js.map +1 -0
- package/lib/slides/core/index.d.ts +13 -0
- package/lib/slides/core/index.d.ts.map +1 -0
- package/lib/slides/core/index.js +7 -0
- package/lib/slides/core/index.js.map +1 -0
- package/lib/slides/core/sectionHeading.d.ts +19 -0
- package/lib/slides/core/sectionHeading.d.ts.map +1 -0
- package/lib/slides/core/sectionHeading.js +19 -0
- package/lib/slides/core/sectionHeading.js.map +1 -0
- package/lib/slides/core/slideNavigation.d.ts +38 -0
- package/lib/slides/core/slideNavigation.d.ts.map +1 -0
- package/lib/slides/core/slideNavigation.js +65 -0
- package/lib/slides/core/slideNavigation.js.map +1 -0
- package/lib/slides/core/talk.d.ts +82 -0
- package/lib/slides/core/talk.d.ts.map +1 -0
- package/lib/slides/core/talk.js +134 -0
- package/lib/slides/core/talk.js.map +1 -0
- package/lib/slides/core/talkManifest.d.ts +90 -0
- package/lib/slides/core/talkManifest.d.ts.map +1 -0
- package/lib/slides/core/talkManifest.js +109 -0
- package/lib/slides/core/talkManifest.js.map +1 -0
- package/lib/slides/core/talkOrigin.d.ts +50 -0
- package/lib/slides/core/talkOrigin.d.ts.map +1 -0
- package/lib/slides/core/talkOrigin.js +62 -0
- package/lib/slides/core/talkOrigin.js.map +1 -0
- package/lib/slides/ui/BackToSlides.d.ts +26 -0
- package/lib/slides/ui/BackToSlides.d.ts.map +1 -0
- package/lib/slides/ui/BackToSlides.js +24 -0
- package/lib/slides/ui/BackToSlides.js.map +1 -0
- package/lib/slides/ui/DemoLink.d.ts +42 -0
- package/lib/slides/ui/DemoLink.d.ts.map +1 -0
- package/lib/slides/ui/DemoLink.js +17 -0
- package/lib/slides/ui/DemoLink.js.map +1 -0
- package/lib/slides/ui/EmbedSlide.d.ts +19 -0
- package/lib/slides/ui/EmbedSlide.d.ts.map +1 -0
- package/lib/slides/ui/EmbedSlide.js +56 -0
- package/lib/slides/ui/EmbedSlide.js.map +1 -0
- package/lib/slides/ui/SectionSlide.d.ts +18 -0
- package/lib/slides/ui/SectionSlide.d.ts.map +1 -0
- package/lib/slides/ui/SectionSlide.js +18 -0
- package/lib/slides/ui/SectionSlide.js.map +1 -0
- package/lib/slides/ui/Slide.d.ts +52 -0
- package/lib/slides/ui/Slide.d.ts.map +1 -0
- package/lib/slides/ui/Slide.js +58 -0
- package/lib/slides/ui/Slide.js.map +1 -0
- package/lib/slides/ui/SlideBody.d.ts +32 -0
- package/lib/slides/ui/SlideBody.d.ts.map +1 -0
- package/lib/slides/ui/SlideBody.js +27 -0
- package/lib/slides/ui/SlideBody.js.map +1 -0
- package/lib/slides/ui/Slideshow.d.ts +93 -0
- package/lib/slides/ui/Slideshow.d.ts.map +1 -0
- package/lib/slides/ui/Slideshow.js +122 -0
- package/lib/slides/ui/Slideshow.js.map +1 -0
- package/lib/slides/ui/SlideshowBar.d.ts +33 -0
- package/lib/slides/ui/SlideshowBar.d.ts.map +1 -0
- package/lib/slides/ui/SlideshowBar.js +20 -0
- package/lib/slides/ui/SlideshowBar.js.map +1 -0
- package/lib/slides/ui/TalkList.d.ts +37 -0
- package/lib/slides/ui/TalkList.d.ts.map +1 -0
- package/lib/slides/ui/TalkList.js +68 -0
- package/lib/slides/ui/TalkList.js.map +1 -0
- package/lib/slides/ui/index.d.ts +19 -0
- package/lib/slides/ui/index.d.ts.map +1 -0
- package/lib/slides/ui/index.js +10 -0
- package/lib/slides/ui/index.js.map +1 -0
- package/lib/slides/vite/index.d.ts +3 -0
- package/lib/slides/vite/index.d.ts.map +1 -0
- package/lib/slides/vite/index.js +2 -0
- package/lib/slides/vite/index.js.map +1 -0
- package/lib/slides/vite/talks.d.ts +50 -0
- package/lib/slides/vite/talks.d.ts.map +1 -0
- package/lib/slides/vite/talks.js +87 -0
- package/lib/slides/vite/talks.js.map +1 -0
- package/lib/slides.d.ts +3 -0
- package/lib/slides.d.ts.map +1 -0
- package/lib/slides.js +3 -0
- package/lib/slides.js.map +1 -0
- package/lib/structure/ui/StructureEditor.js +2 -2
- package/lib/structure/ui/StructureEditor.js.map +1 -1
- package/lib/structure/ui/StructurePlaceholder.js +1 -1
- package/lib/structure/ui/StructurePlaceholder.js.map +1 -1
- package/lib/tokens/core/checkTokens.d.ts +51 -0
- package/lib/tokens/core/checkTokens.d.ts.map +1 -0
- package/lib/tokens/core/checkTokens.js +188 -0
- package/lib/tokens/core/checkTokens.js.map +1 -0
- package/lib/tokens/core/colors.d.ts +42 -0
- package/lib/tokens/core/colors.d.ts.map +1 -0
- package/lib/tokens/core/colors.js +119 -0
- package/lib/tokens/core/colors.js.map +1 -0
- package/lib/tokens/core/index.d.ts +3 -0
- package/lib/tokens/core/index.d.ts.map +1 -0
- package/lib/tokens/core/index.js +2 -0
- package/lib/tokens/core/index.js.map +1 -0
- package/lib/tokens/core/rules.d.ts +23 -0
- package/lib/tokens/core/rules.d.ts.map +1 -0
- package/lib/tokens/core/rules.js +90 -0
- package/lib/tokens/core/rules.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/lib/vite.d.ts +1 -0
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +1 -0
- package/lib/vite.js.map +1 -1
- package/package.json +11 -3
- package/src/about/core/about.ts +176 -0
- package/src/about/core/index.ts +2 -0
- package/src/about/ui/AboutCitations.tsx +79 -0
- package/src/about/ui/AboutPage.tsx +177 -0
- package/src/about/ui/AboutSection.tsx +56 -0
- package/src/about/ui/index.ts +6 -0
- package/src/chrome/ui/index.ts +3 -0
- package/src/citation/core/index.ts +1 -0
- package/src/citation/core/teachingPaper.ts +36 -0
- package/src/clipboard/ui/CodeBlock.tsx +4 -1
- package/src/color/ui/ColorScaleLegend.tsx +1 -1
- package/src/core.ts +5 -0
- package/src/credits/core/credits.ts +42 -0
- package/src/credits/ui/CreditsList.tsx +1 -1
- package/src/delimited/ui/DelimitedTextPanel.tsx +1 -1
- package/src/ecosystem/core/sites.ts +12 -0
- package/src/ecosystem/core/tokens.ts +1 -0
- package/src/ecosystem/ui/EcosystemLinks.tsx +2 -2
- package/src/ecosystem/ui/EcosystemMenu.tsx +1 -1
- package/src/ecosystem/ui/SiteTile.tsx +7 -5
- package/src/ecosystem/ui/glyphs.tsx +40 -0
- package/src/orbital/core/index.ts +4 -1
- package/src/orbital/core/realHarmonics.ts +5 -2
- package/src/orbital/ui/AtomicOrbitalCanvas.tsx +126 -4
- package/src/orbital/ui/AtomicOrbitalViewer.tsx +14 -1
- package/src/orbital/ui/camera.ts +81 -0
- package/src/orbital/ui/renderVolume.ts +8 -1
- package/src/orbital/ui/serial.ts +35 -0
- package/src/orbital/ui/viewer.ts +72 -13
- package/src/pedagogy/ui/ExerciseProgressHeader.tsx +1 -1
- package/src/pedagogy/ui/GlossaryText.tsx +2 -2
- package/src/pedagogy/ui/ReferenceSectionBlock.tsx +5 -2
- package/src/pedagogy/ui/SyntaxTooltip.tsx +2 -2
- package/src/pedagogy/ui/TutorialStepStrip.tsx +39 -10
- package/src/pedagogy/ui/index.ts +4 -1
- package/src/periodic/ui/CategoryLegend.tsx +1 -1
- package/src/periodic/ui/PeriodicTableChrome.tsx +2 -2
- package/src/seo/vite/ogCard.ts +1 -0
- package/src/share/ui/ShareDialogContent.tsx +5 -2
- package/src/share/ui/SharePartOptions.tsx +1 -1
- package/src/slides/core/demoLinks.ts +113 -0
- package/src/slides/core/index.ts +22 -0
- package/src/slides/core/sectionHeading.ts +29 -0
- package/src/slides/core/slideNavigation.ts +77 -0
- package/src/slides/core/talk.ts +203 -0
- package/src/slides/core/talkManifest.ts +158 -0
- package/src/slides/core/talkOrigin.ts +83 -0
- package/src/slides/ui/BackToSlides.tsx +49 -0
- package/src/slides/ui/DemoLink.tsx +67 -0
- package/src/slides/ui/EmbedSlide.tsx +85 -0
- package/src/slides/ui/SectionSlide.tsx +44 -0
- package/src/slides/ui/Slide.tsx +143 -0
- package/src/slides/ui/SlideBody.tsx +64 -0
- package/src/slides/ui/Slideshow.tsx +282 -0
- package/src/slides/ui/SlideshowBar.tsx +83 -0
- package/src/slides/ui/TalkList.tsx +146 -0
- package/src/slides/ui/index.ts +18 -0
- package/src/slides/vite/index.ts +2 -0
- package/src/slides/vite/talks.ts +126 -0
- package/src/slides.ts +2 -0
- package/src/structure/ui/StructureEditor.tsx +2 -2
- package/src/structure/ui/StructurePlaceholder.tsx +1 -1
- package/src/tokens/core/checkTokens.ts +242 -0
- package/src/tokens/core/colors.ts +122 -0
- package/src/tokens/core/index.ts +6 -0
- package/src/tokens/core/rules.ts +103 -0
- package/src/ui.ts +1 -0
- package/src/vite.ts +1 -0
- package/styles/chrome.css +1 -0
- package/styles/slides.css +667 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Talk } from './talk.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How one site publishes its talks so another can list them.
|
|
5
|
+
*
|
|
6
|
+
* The talks themselves are Markdown files served by the site that gave them;
|
|
7
|
+
* a manifest is the small JSON index a sibling site fetches to show them
|
|
8
|
+
* without holding a copy. It is read tolerantly, because the site fetching it
|
|
9
|
+
* has no control over the version of the file it gets.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One talk, as a listing shows it. */
|
|
13
|
+
export interface TalkSummary {
|
|
14
|
+
/** How the publishing site names the talk in its addresses. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** The talk title. */
|
|
17
|
+
title: string;
|
|
18
|
+
/**
|
|
19
|
+
* A second line under the title.
|
|
20
|
+
* @default undefined
|
|
21
|
+
*/
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Conference or event it was given at.
|
|
25
|
+
* @default undefined
|
|
26
|
+
*/
|
|
27
|
+
event?: string;
|
|
28
|
+
/**
|
|
29
|
+
* When it was given, as an ISO date (`YYYY-MM-DD`).
|
|
30
|
+
* @default undefined
|
|
31
|
+
*/
|
|
32
|
+
date?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Who presented it.
|
|
35
|
+
* @default undefined
|
|
36
|
+
*/
|
|
37
|
+
author?: string;
|
|
38
|
+
/** How many slides it holds. */
|
|
39
|
+
slideCount: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Every talk one site publishes. */
|
|
43
|
+
export interface TalkManifest {
|
|
44
|
+
/** The `ECOSYSTEM_SITES` id of the site that gave the talks. */
|
|
45
|
+
site: string;
|
|
46
|
+
/** Where that site is served from, as `https://host`. */
|
|
47
|
+
origin: string;
|
|
48
|
+
/** The talks, in the order the site lists them. */
|
|
49
|
+
talks: TalkSummary[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The listing entry for one parsed talk.
|
|
54
|
+
* @param id - How the site names the talk in its addresses.
|
|
55
|
+
* @param talk - The parsed talk.
|
|
56
|
+
* @returns Its summary, carrying only the metadata a listing shows.
|
|
57
|
+
*/
|
|
58
|
+
export function talkSummary(id: string, talk: Talk): TalkSummary {
|
|
59
|
+
const summary: TalkSummary = {
|
|
60
|
+
id,
|
|
61
|
+
title: talk.meta.title,
|
|
62
|
+
slideCount: talk.slides.length,
|
|
63
|
+
};
|
|
64
|
+
if (talk.meta.subtitle !== undefined) summary.subtitle = talk.meta.subtitle;
|
|
65
|
+
if (talk.meta.event !== undefined) summary.event = talk.meta.event;
|
|
66
|
+
if (talk.meta.date !== undefined) summary.date = talk.meta.date;
|
|
67
|
+
if (talk.meta.author !== undefined) summary.author = talk.meta.author;
|
|
68
|
+
return summary;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the manifest a site publishes.
|
|
73
|
+
* @param input - The site, where it is served from, and its parsed talks.
|
|
74
|
+
* @param input.site - The `ECOSYSTEM_SITES` id of the publishing site.
|
|
75
|
+
* @param input.origin - Where that site is served from, as `https://host`.
|
|
76
|
+
* @param input.talks - The talks, each with the id the site names it by.
|
|
77
|
+
* @returns The manifest, ready to be written as JSON.
|
|
78
|
+
*/
|
|
79
|
+
export function buildTalkManifest(input: {
|
|
80
|
+
/** The `ECOSYSTEM_SITES` id of the publishing site. */
|
|
81
|
+
site: string;
|
|
82
|
+
/** Where that site is served from, as `https://host`. */
|
|
83
|
+
origin: string;
|
|
84
|
+
/** The talks, each with the id the site names it by. */
|
|
85
|
+
talks: ReadonlyArray<{ id: string } & Talk>;
|
|
86
|
+
}): TalkManifest {
|
|
87
|
+
const talks: TalkSummary[] = [];
|
|
88
|
+
for (const talk of input.talks) {
|
|
89
|
+
talks.push(talkSummary(talk.id, talk));
|
|
90
|
+
}
|
|
91
|
+
return { site: input.site, origin: input.origin, talks };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read a manifest a sibling site published.
|
|
96
|
+
*
|
|
97
|
+
* Unknown keys are ignored and an entry missing what a listing needs is
|
|
98
|
+
* dropped, so a site running an older or newer version of the format still
|
|
99
|
+
* lists whatever it can understand instead of showing nothing.
|
|
100
|
+
* @param value - The parsed JSON, of unknown shape.
|
|
101
|
+
* @returns The manifest, or `null` when the value is not one.
|
|
102
|
+
*/
|
|
103
|
+
export function parseTalkManifest(value: unknown): TalkManifest | null {
|
|
104
|
+
if (typeof value !== 'object' || value === null) return null;
|
|
105
|
+
const record = value as Record<string, unknown>;
|
|
106
|
+
const site = text(record.site);
|
|
107
|
+
const origin = text(record.origin);
|
|
108
|
+
if (site === undefined || origin === undefined) return null;
|
|
109
|
+
if (!Array.isArray(record.talks)) return null;
|
|
110
|
+
const talks: TalkSummary[] = [];
|
|
111
|
+
for (const entry of record.talks) {
|
|
112
|
+
const summary = parseSummary(entry);
|
|
113
|
+
if (summary !== null) talks.push(summary);
|
|
114
|
+
}
|
|
115
|
+
return { site, origin, talks };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Where the Markdown source of one talk of a manifest is served.
|
|
120
|
+
* @param manifest - The manifest naming the publishing site.
|
|
121
|
+
* @param id - Which talk.
|
|
122
|
+
* @returns The absolute URL of the talk source.
|
|
123
|
+
*/
|
|
124
|
+
export function talkSourceUrl(manifest: TalkManifest, id: string): string {
|
|
125
|
+
return `${manifest.origin.replace(TRAILING_SLASHES, '')}/talks/${id}.md`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseSummary(value: unknown): TalkSummary | null {
|
|
129
|
+
if (typeof value !== 'object' || value === null) return null;
|
|
130
|
+
const record = value as Record<string, unknown>;
|
|
131
|
+
const id = text(record.id);
|
|
132
|
+
const title = text(record.title);
|
|
133
|
+
const slideCount = record.slideCount;
|
|
134
|
+
if (id === undefined || title === undefined) return null;
|
|
135
|
+
if (typeof slideCount !== 'number' || !Number.isFinite(slideCount)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const summary: TalkSummary = {
|
|
139
|
+
id,
|
|
140
|
+
title,
|
|
141
|
+
slideCount: Math.max(Math.trunc(slideCount), 0),
|
|
142
|
+
};
|
|
143
|
+
const subtitle = text(record.subtitle);
|
|
144
|
+
const event = text(record.event);
|
|
145
|
+
const date = text(record.date);
|
|
146
|
+
const author = text(record.author);
|
|
147
|
+
if (subtitle !== undefined) summary.subtitle = subtitle;
|
|
148
|
+
if (event !== undefined) summary.event = event;
|
|
149
|
+
if (date !== undefined) summary.date = date;
|
|
150
|
+
if (author !== undefined) summary.author = author;
|
|
151
|
+
return summary;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function text(value: unknown): string | undefined {
|
|
155
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { firstValues, parseQuery } from '../../share/core/query.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The round trip from a slide to a live demo and back.
|
|
5
|
+
*
|
|
6
|
+
* A demo link opens the real tool, so the presenter is then on an ordinary page
|
|
7
|
+
* of an ordinary site with no way back to the talk. One query parameter carries
|
|
8
|
+
* where they came from, which is enough for that page to offer the way back —
|
|
9
|
+
* and, because the site the talk lives on may not be the site the demo runs on,
|
|
10
|
+
* the parameter names both.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The query parameter a demo link carries its origin in. */
|
|
14
|
+
export const TALK_ORIGIN_PARAM = 'from';
|
|
15
|
+
|
|
16
|
+
/** Where a demo was opened from. */
|
|
17
|
+
export interface TalkOrigin {
|
|
18
|
+
/** Which talk, as the publishing site names it. */
|
|
19
|
+
talkId: string;
|
|
20
|
+
/** Which slide of it, zero-based. */
|
|
21
|
+
slide: number;
|
|
22
|
+
/**
|
|
23
|
+
* The `ECOSYSTEM_SITES` id of the site the talk is published by, for a demo
|
|
24
|
+
* that runs on a different site than the talk.
|
|
25
|
+
* @default undefined — the talk lives on the site the demo runs on
|
|
26
|
+
*/
|
|
27
|
+
site?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Write an origin as the value of the `from` parameter.
|
|
32
|
+
* @param origin - Which talk and slide the demo was opened from.
|
|
33
|
+
* @returns `talk:<site>:<id>:<slide>`, or `talk:<id>:<slide>` when the talk
|
|
34
|
+
* lives on the site the link points at.
|
|
35
|
+
*/
|
|
36
|
+
export function formatTalkOrigin(origin: TalkOrigin): string {
|
|
37
|
+
const slide = Math.max(Math.trunc(origin.slide), 0);
|
|
38
|
+
const site = origin.site === undefined ? '' : `${origin.site}:`;
|
|
39
|
+
return `talk:${site}${origin.talkId}:${slide}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the origin out of an address's query string.
|
|
44
|
+
*
|
|
45
|
+
* Anything malformed reads as no origin at all: a link retyped from a slide, or
|
|
46
|
+
* one written before the talk was renamed, must still open the tool it points
|
|
47
|
+
* at rather than fail on its way in.
|
|
48
|
+
* @param search - The query string, with or without its leading `?`.
|
|
49
|
+
* @returns The origin, or `null` when the address carries none.
|
|
50
|
+
*/
|
|
51
|
+
export function parseTalkOrigin(search: string): TalkOrigin | null {
|
|
52
|
+
const value = firstValues(parseQuery(search)).get(TALK_ORIGIN_PARAM);
|
|
53
|
+
if (value === undefined) return null;
|
|
54
|
+
const match = ORIGIN_RE.exec(value);
|
|
55
|
+
const talkId = match?.groups?.talkId;
|
|
56
|
+
const slide = Number.parseInt(match?.groups?.slide ?? '', 10);
|
|
57
|
+
if (talkId === undefined || !Number.isFinite(slide)) return null;
|
|
58
|
+
const site = match?.groups?.site;
|
|
59
|
+
return site === undefined ? { talkId, slide } : { talkId, slide, site };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Point a demo link at its target, carrying where it was opened from.
|
|
64
|
+
* @param href - The target, relative or absolute, with or without a query.
|
|
65
|
+
* @param origin - Which talk and slide the link sits on.
|
|
66
|
+
* @returns The same address with the `from` parameter added, its own
|
|
67
|
+
* parameters and its fragment untouched.
|
|
68
|
+
*/
|
|
69
|
+
export function withTalkOrigin(href: string, origin: TalkOrigin): string {
|
|
70
|
+
const cut = href.indexOf('#');
|
|
71
|
+
const address = cut === -1 ? href : href.slice(0, cut);
|
|
72
|
+
const fragment = cut === -1 ? '' : href.slice(cut);
|
|
73
|
+
const separator = address.includes('?') ? '&' : '?';
|
|
74
|
+
// A colon reads as a colon in these links, and parses back identically.
|
|
75
|
+
const value = encodeURIComponent(formatTalkOrigin(origin)).replaceAll(
|
|
76
|
+
'%3A',
|
|
77
|
+
':',
|
|
78
|
+
);
|
|
79
|
+
return `${address}${separator}${TALK_ORIGIN_PARAM}=${value}${fragment}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ORIGIN_RE =
|
|
83
|
+
/^talk:(?:(?<site>[\da-z-]+):)?(?<talkId>[^:]+):(?<slide>\d+)$/i;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Icon } from '@blueprintjs/core';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
|
|
4
|
+
import { isModifiedClick } from '../../chrome/ui/navItem.ts';
|
|
5
|
+
import type { TalkOrigin } from '../core/index.ts';
|
|
6
|
+
|
|
7
|
+
/** Where the visitor came from, and how to send them back. */
|
|
8
|
+
export interface BackToSlidesProps {
|
|
9
|
+
/** The talk and slide a demo link was opened from, or `null` for none. */
|
|
10
|
+
origin: TalkOrigin | null;
|
|
11
|
+
/** The address of that slide. */
|
|
12
|
+
href: string;
|
|
13
|
+
/**
|
|
14
|
+
* What the site does when the pill is clicked, for a site that routes in the
|
|
15
|
+
* browser. A modified click is left to the browser either way.
|
|
16
|
+
* @default undefined — the browser follows the address
|
|
17
|
+
*/
|
|
18
|
+
onReturn?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The way back to the slide a demo was opened from.
|
|
23
|
+
*
|
|
24
|
+
* A demo link drops the presenter on an ordinary page of an ordinary site, so
|
|
25
|
+
* during a talk the site stays fully browsable and the talk stays one click
|
|
26
|
+
* away. It renders nothing at all when the page was not reached from a slide.
|
|
27
|
+
* @param props - See {@link BackToSlidesProps}.
|
|
28
|
+
* @returns The pill, or nothing.
|
|
29
|
+
*/
|
|
30
|
+
export function BackToSlides(props: BackToSlidesProps): ReactElement | null {
|
|
31
|
+
const { origin, href, onReturn } = props;
|
|
32
|
+
if (origin === null) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<a
|
|
36
|
+
className="back-to-slides no-print"
|
|
37
|
+
href={href}
|
|
38
|
+
title={`Back to slide ${origin.slide + 1}`}
|
|
39
|
+
onClick={(event) => {
|
|
40
|
+
if (onReturn === undefined || isModifiedClick(event)) return;
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
onReturn();
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<Icon icon="presentation" size={14} />
|
|
46
|
+
Back to slides
|
|
47
|
+
</a>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { TalkOrigin } from '../core/index.ts';
|
|
4
|
+
import { withTalkOrigin } from '../core/index.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How a site draws a link that leaves a slide for one of its own pages.
|
|
8
|
+
*
|
|
9
|
+
* The player never navigates: a site that routes in the browser passes its own
|
|
10
|
+
* link here, and the address it is given already carries the slide it was
|
|
11
|
+
* opened from. The children arrive already wrapped in the deck's arrow and
|
|
12
|
+
* label spans, so a site's element only has to carry `slide-demo` for the
|
|
13
|
+
* styling to apply.
|
|
14
|
+
*/
|
|
15
|
+
export type RenderSlideLink = (link: {
|
|
16
|
+
/** The address the link opens, `from` parameter included. */
|
|
17
|
+
href: string;
|
|
18
|
+
/** What the link reads. */
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}) => ReactElement;
|
|
21
|
+
|
|
22
|
+
/** What a demo link points at, and how it is drawn. */
|
|
23
|
+
export interface DemoLinkProps {
|
|
24
|
+
/** The in-app route the demo opens, as the slide wrote it. */
|
|
25
|
+
href: string;
|
|
26
|
+
/**
|
|
27
|
+
* Which slide the link sits on, added to the address so the tool it opens can
|
|
28
|
+
* offer the way back. Nothing is added when it is left out.
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
origin?: TalkOrigin;
|
|
32
|
+
/**
|
|
33
|
+
* The site's own link, for a site that routes in the browser.
|
|
34
|
+
* @default undefined — a plain anchor
|
|
35
|
+
*/
|
|
36
|
+
renderLink?: RenderSlideLink;
|
|
37
|
+
/** The link label. */
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A link from a slide into the live tool it demonstrates.
|
|
43
|
+
* @param props - See {@link DemoLinkProps}.
|
|
44
|
+
* @returns The link.
|
|
45
|
+
*/
|
|
46
|
+
export function DemoLink(props: DemoLinkProps): ReactElement {
|
|
47
|
+
const { href, origin, renderLink, children } = props;
|
|
48
|
+
const address = origin === undefined ? href : withTalkOrigin(href, origin);
|
|
49
|
+
const label = (
|
|
50
|
+
<>
|
|
51
|
+
<span className="slide-demo-arrow" aria-hidden="true">
|
|
52
|
+
▸
|
|
53
|
+
</span>
|
|
54
|
+
<span className="slide-demo-label">{children}</span>
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (renderLink !== undefined) {
|
|
59
|
+
return renderLink({ href: address, children: label });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<a className="slide-demo" href={address}>
|
|
64
|
+
{label}
|
|
65
|
+
</a>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Slide } from '../core/talk.ts';
|
|
4
|
+
|
|
5
|
+
import { SlideBody } from './SlideBody.tsx';
|
|
6
|
+
|
|
7
|
+
/** What an embedded slide draws. */
|
|
8
|
+
export interface EmbedSlideProps {
|
|
9
|
+
/** The slide, whose body holds the prose and, on its own line, the address. */
|
|
10
|
+
slide: Slide;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A slide that is the tool itself, running.
|
|
15
|
+
*
|
|
16
|
+
* Every tool of the family takes `?embed`, so a lecture can show the real thing
|
|
17
|
+
* instead of a screenshot of it — which is also the only version of the slide
|
|
18
|
+
* that stays true after the tool is improved. The heading and any prose stay
|
|
19
|
+
* above the frame; the last bare URL of the body is what is framed.
|
|
20
|
+
* @param props - See {@link EmbedSlideProps}.
|
|
21
|
+
* @returns The slide.
|
|
22
|
+
*/
|
|
23
|
+
export function EmbedSlide(props: EmbedSlideProps): ReactElement {
|
|
24
|
+
const { slide } = props;
|
|
25
|
+
const { prose, src } = splitEmbedAddress(slide.body);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="slide slide--embed">
|
|
29
|
+
<div className="slide-content">
|
|
30
|
+
{prose === '' ? null : <SlideBody body={prose} />}
|
|
31
|
+
{src === null ? (
|
|
32
|
+
<p className="slide-embed-missing">
|
|
33
|
+
This slide frames a tool, but carries no address.
|
|
34
|
+
</p>
|
|
35
|
+
) : (
|
|
36
|
+
<iframe
|
|
37
|
+
className="slide-embed-frame"
|
|
38
|
+
src={src}
|
|
39
|
+
title={frameTitle(src)}
|
|
40
|
+
allow="clipboard-write; fullscreen"
|
|
41
|
+
/>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The prose and the address of an embedded slide.
|
|
50
|
+
*
|
|
51
|
+
* The address is written as a bare URL on its own line, because a teacher
|
|
52
|
+
* pastes what the tool's Share dialog gave them and nothing else.
|
|
53
|
+
* @param body - The slide body.
|
|
54
|
+
* @returns The prose without the address, and the address.
|
|
55
|
+
*/
|
|
56
|
+
function splitEmbedAddress(body: string): {
|
|
57
|
+
prose: string;
|
|
58
|
+
src: string | null;
|
|
59
|
+
} {
|
|
60
|
+
const lines = body.split('\n');
|
|
61
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
62
|
+
const line = (lines[index] ?? '').trim();
|
|
63
|
+
if (line === '') continue;
|
|
64
|
+
if (!line.startsWith('http://') && !line.startsWith('https://')) break;
|
|
65
|
+
if (line.includes(' ')) break;
|
|
66
|
+
const prose = [...lines.slice(0, index), ...lines.slice(index + 1)]
|
|
67
|
+
.join('\n')
|
|
68
|
+
.trim();
|
|
69
|
+
return { prose, src: line };
|
|
70
|
+
}
|
|
71
|
+
return { prose: body.trim(), src: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A frame without a title is unreadable to a screen reader.
|
|
76
|
+
* @param src - The address being framed.
|
|
77
|
+
* @returns What the frame is called.
|
|
78
|
+
*/
|
|
79
|
+
function frameTitle(src: string): string {
|
|
80
|
+
try {
|
|
81
|
+
return `${new URL(src).hostname}, embedded`;
|
|
82
|
+
} catch {
|
|
83
|
+
return 'Embedded tool';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import { parseSectionHeading } from '../core/index.ts';
|
|
4
|
+
|
|
5
|
+
import { SlideBody } from './SlideBody.tsx';
|
|
6
|
+
|
|
7
|
+
/** The body of a section divider. */
|
|
8
|
+
export interface SectionSlideProps {
|
|
9
|
+
/** The Markdown body, normally starting with `# N · Title`. */
|
|
10
|
+
body: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The slide that says a new part of the talk starts here: the section number
|
|
15
|
+
* set huge and near-transparent, with the title, an accent rule and whatever
|
|
16
|
+
* else the body holds stacked beside it.
|
|
17
|
+
*
|
|
18
|
+
* The numeral is decoration, not text — the heading beside it already says
|
|
19
|
+
* where the talk is — so it is hidden from a screen reader.
|
|
20
|
+
* @param props - See {@link SectionSlideProps}.
|
|
21
|
+
* @returns The divider slide.
|
|
22
|
+
*/
|
|
23
|
+
export function SectionSlide(props: SectionSlideProps): ReactElement {
|
|
24
|
+
const { number, title, rest } = parseSectionHeading(props.body);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="slide slide--section">
|
|
28
|
+
<div className="section-slide">
|
|
29
|
+
<div className="section-number" aria-hidden="true">
|
|
30
|
+
{number}
|
|
31
|
+
</div>
|
|
32
|
+
<div className="section-body">
|
|
33
|
+
{title === '' ? null : <h1 className="section-title">{title}</h1>}
|
|
34
|
+
<hr className="section-rule" />
|
|
35
|
+
{rest === '' ? null : (
|
|
36
|
+
<div className="section-sub">
|
|
37
|
+
<SlideBody body={rest} />
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement } from 'react';
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Slide as SlideData,
|
|
6
|
+
TalkMeta,
|
|
7
|
+
TalkOrigin,
|
|
8
|
+
} from '../core/index.ts';
|
|
9
|
+
import { demoLabel, splitDemoLinks } from '../core/index.ts';
|
|
10
|
+
|
|
11
|
+
import type { RenderSlideLink } from './DemoLink.tsx';
|
|
12
|
+
import { DemoLink } from './DemoLink.tsx';
|
|
13
|
+
import { EmbedSlide } from './EmbedSlide.tsx';
|
|
14
|
+
import { SectionSlide } from './SectionSlide.tsx';
|
|
15
|
+
import { SlideBody } from './SlideBody.tsx';
|
|
16
|
+
|
|
17
|
+
/** What every layout — the generic ones and a site's own — is handed. */
|
|
18
|
+
export interface SlideLayoutProps {
|
|
19
|
+
/** The slide to draw. */
|
|
20
|
+
slide: SlideData;
|
|
21
|
+
/** How the publishing site names the talk in its addresses. */
|
|
22
|
+
talkId: string;
|
|
23
|
+
/** Which slide of the talk this is, zero-based. */
|
|
24
|
+
slideIndex: number;
|
|
25
|
+
/**
|
|
26
|
+
* Which site published the deck, written into the way back.
|
|
27
|
+
* @default undefined — the site playing it
|
|
28
|
+
*/
|
|
29
|
+
site?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Where the publishing site lives, which a relative address in the deck is
|
|
32
|
+
* resolved against.
|
|
33
|
+
* @default undefined — the addresses are this site's own
|
|
34
|
+
*/
|
|
35
|
+
talkOrigin?: string;
|
|
36
|
+
/** What the talk's front-matter said, for a layout that draws from it. */
|
|
37
|
+
meta: TalkMeta;
|
|
38
|
+
/**
|
|
39
|
+
* The site's own link, used for the in-app links of the slide.
|
|
40
|
+
* @default undefined — a plain anchor
|
|
41
|
+
*/
|
|
42
|
+
renderLink?: RenderSlideLink;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A slide, and the layouts available to draw it. */
|
|
46
|
+
export interface SlideViewProps extends SlideLayoutProps {
|
|
47
|
+
/**
|
|
48
|
+
* The layouts this site adds, by the name a slide writes in its
|
|
49
|
+
* `<!-- layout: … -->` marker. An entry here also replaces a generic layout
|
|
50
|
+
* of the same name.
|
|
51
|
+
* @default undefined
|
|
52
|
+
*/
|
|
53
|
+
layouts?: Record<string, ComponentType<SlideLayoutProps>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Draw one slide with the layout it asks for.
|
|
58
|
+
*
|
|
59
|
+
* The layout name is not a closed list — a deck is written before the site
|
|
60
|
+
* knows how to draw it — so a name neither the site nor the player recognises
|
|
61
|
+
* falls back to the plain content layout. A talk must never fail to open
|
|
62
|
+
* because one of its slides asks for something that is not there yet.
|
|
63
|
+
* @param props - See {@link SlideViewProps}.
|
|
64
|
+
* @returns The slide.
|
|
65
|
+
*/
|
|
66
|
+
export function SlideView(props: SlideViewProps): ReactElement {
|
|
67
|
+
const { layouts, ...layoutProps } = props;
|
|
68
|
+
const { slide } = layoutProps;
|
|
69
|
+
|
|
70
|
+
const Custom = layouts?.[slide.layout];
|
|
71
|
+
if (Custom !== undefined) return <Custom {...layoutProps} />;
|
|
72
|
+
if (slide.layout === 'section') return <SectionSlide body={slide.body} />;
|
|
73
|
+
if (slide.layout === 'embed') return <EmbedSlide slide={slide} />;
|
|
74
|
+
|
|
75
|
+
return <GenericSlide {...layoutProps} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The layouts the player draws itself, for any deck on any site. */
|
|
79
|
+
const GENERIC_LAYOUTS = new Set(['content', 'title', 'quote', 'thanks']);
|
|
80
|
+
|
|
81
|
+
function GenericSlide(props: SlideLayoutProps): ReactElement {
|
|
82
|
+
const { slide, talkId, slideIndex, meta, renderLink, site, talkOrigin } =
|
|
83
|
+
props;
|
|
84
|
+
const layout = GENERIC_LAYOUTS.has(slide.layout) ? slide.layout : 'content';
|
|
85
|
+
const { body, demos } = splitDemoLinks(slide.body);
|
|
86
|
+
const origin: TalkOrigin =
|
|
87
|
+
site === undefined
|
|
88
|
+
? { talkId, slide: slideIndex }
|
|
89
|
+
: { talkId, slide: slideIndex, site };
|
|
90
|
+
const logos = layout === 'title' ? (meta.logos ?? []) : [];
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={`slide slide--${layout}`}>
|
|
94
|
+
<div className="slide-content">
|
|
95
|
+
<SlideBody body={body} origin={origin} renderLink={renderLink} />
|
|
96
|
+
{logos.length === 0 ? null : (
|
|
97
|
+
<div className="slide-logos">
|
|
98
|
+
{logos.map((logo) => (
|
|
99
|
+
<img key={logo} className="slide-logo" src={logo} alt="" />
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{demos.length === 0 ? null : (
|
|
104
|
+
<div className="slide-demos">
|
|
105
|
+
<span className="slide-demos-label">Live demos:</span>
|
|
106
|
+
{demos.map((demo, index) => (
|
|
107
|
+
<Fragment key={demo.href}>
|
|
108
|
+
{index === 0 ? null : (
|
|
109
|
+
<span className="slide-demos-separator" aria-hidden="true">
|
|
110
|
+
|
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
<DemoLink
|
|
114
|
+
href={resolveDemoHref(demo.href, talkOrigin)}
|
|
115
|
+
origin={origin}
|
|
116
|
+
renderLink={renderLink}
|
|
117
|
+
>
|
|
118
|
+
{demoLabel(demo.label)}
|
|
119
|
+
</DemoLink>
|
|
120
|
+
</Fragment>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where a demo link actually points.
|
|
131
|
+
*
|
|
132
|
+
* A deck writes `/mf-finder?mass=300`, meaning its own site. Played there it is
|
|
133
|
+
* exactly that; played on another site of the family it has to be resolved
|
|
134
|
+
* against the site that wrote it, or the link lands on a page that does not
|
|
135
|
+
* exist.
|
|
136
|
+
* @param href - The address the slide wrote.
|
|
137
|
+
* @param talkOrigin - Where the publishing site lives, when it is not this one.
|
|
138
|
+
* @returns The address to open.
|
|
139
|
+
*/
|
|
140
|
+
function resolveDemoHref(href: string, talkOrigin?: string): string {
|
|
141
|
+
if (talkOrigin === undefined || !href.startsWith('/')) return href;
|
|
142
|
+
return `${talkOrigin.replace(/\/$/, '')}${href}`;
|
|
143
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import Markdown from 'react-markdown';
|
|
3
|
+
import rehypeRaw from 'rehype-raw';
|
|
4
|
+
|
|
5
|
+
import type { TalkOrigin } from '../core/index.ts';
|
|
6
|
+
|
|
7
|
+
import type { RenderSlideLink } from './DemoLink.tsx';
|
|
8
|
+
import { DemoLink } from './DemoLink.tsx';
|
|
9
|
+
|
|
10
|
+
/** The Markdown of a slide, and where its links go. */
|
|
11
|
+
export interface SlideBodyProps {
|
|
12
|
+
/** The Markdown source of the slide, or of one part of it. */
|
|
13
|
+
body: string;
|
|
14
|
+
/**
|
|
15
|
+
* Which slide the body belongs to, carried by every in-app link it holds so
|
|
16
|
+
* the tool a demo opens can offer the way back.
|
|
17
|
+
* @default undefined
|
|
18
|
+
*/
|
|
19
|
+
origin?: TalkOrigin;
|
|
20
|
+
/**
|
|
21
|
+
* The site's own link, used for the in-app links of the body.
|
|
22
|
+
* @default undefined — a plain anchor
|
|
23
|
+
*/
|
|
24
|
+
renderLink?: RenderSlideLink;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render a slide's Markdown.
|
|
29
|
+
*
|
|
30
|
+
* A talk is written as prose, so raw HTML in the source is kept: a slide
|
|
31
|
+
* routinely holds a `<sub>`, a `<br>` or a figure the Markdown syntax cannot
|
|
32
|
+
* express. A link to an in-app route becomes a demo link; every other link
|
|
33
|
+
* leaves the deck in a tab of its own, so a click during a talk never loses the
|
|
34
|
+
* slide it was made from.
|
|
35
|
+
* @param props - See {@link SlideBodyProps}.
|
|
36
|
+
* @returns The rendered body.
|
|
37
|
+
*/
|
|
38
|
+
export function SlideBody(props: SlideBodyProps): ReactElement {
|
|
39
|
+
const { body, origin, renderLink } = props;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Markdown
|
|
43
|
+
rehypePlugins={[rehypeRaw]}
|
|
44
|
+
components={{
|
|
45
|
+
a({ href, children }) {
|
|
46
|
+
if (href?.startsWith('/') === true) {
|
|
47
|
+
return (
|
|
48
|
+
<DemoLink href={href} origin={origin} renderLink={renderLink}>
|
|
49
|
+
{children}
|
|
50
|
+
</DemoLink>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<a href={href} target="_blank" rel="noreferrer">
|
|
55
|
+
{children}
|
|
56
|
+
</a>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{body}
|
|
62
|
+
</Markdown>
|
|
63
|
+
);
|
|
64
|
+
}
|