mdeck 2.3.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/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/assets/base.css +196 -0
- package/assets/extensions/layouts/chapter/extension.toml +26 -0
- package/assets/extensions/layouts/chapter/layout.jsx +21 -0
- package/assets/extensions/layouts/chapter/starter.md +8 -0
- package/assets/extensions/layouts/focus/extension.toml +8 -0
- package/assets/extensions/layouts/focus/layout.jsx +10 -0
- package/assets/extensions/layouts/focus/starter.md +4 -0
- package/assets/extensions/layouts/full-bleed-image/extension.toml +31 -0
- package/assets/extensions/layouts/full-bleed-image/layout.jsx +19 -0
- package/assets/extensions/layouts/full-bleed-image/starter.md +6 -0
- package/assets/extensions/layouts/generic/extension.toml +8 -0
- package/assets/extensions/layouts/generic/layout.jsx +8 -0
- package/assets/extensions/layouts/generic/starter.md +4 -0
- package/assets/extensions/layouts/image-text/extension.toml +25 -0
- package/assets/extensions/layouts/image-text/layout.jsx +17 -0
- package/assets/extensions/layouts/image-text/starter.md +6 -0
- package/assets/extensions/layouts/split/extension.toml +25 -0
- package/assets/extensions/layouts/split/layout.jsx +19 -0
- package/assets/extensions/layouts/split/starter.md +14 -0
- package/assets/extensions/layouts/title/extension.toml +26 -0
- package/assets/extensions/layouts/title/layout.jsx +14 -0
- package/assets/extensions/layouts/title/starter.md +5 -0
- package/assets/extensions/palettes/brand/extension.toml +29 -0
- package/assets/extensions/palettes/cobalt/extension.toml +27 -0
- package/assets/extensions/palettes/ember/extension.toml +27 -0
- package/assets/extensions/palettes/forest/extension.toml +27 -0
- package/assets/extensions/palettes/graphite/extension.toml +27 -0
- package/assets/extensions/palettes/lagoon/extension.toml +27 -0
- package/assets/extensions/palettes/neon/extension.toml +27 -0
- package/assets/extensions/palettes/nordic/extension.toml +27 -0
- package/assets/extensions/palettes/pastel/extension.toml +30 -0
- package/assets/extensions/palettes/phosphor/extension.toml +27 -0
- package/assets/extensions/palettes/swiss/extension.toml +27 -0
- package/assets/extensions/palettes/terra/extension.toml +27 -0
- package/assets/extensions/themes/academic/extension.toml +53 -0
- package/assets/extensions/themes/academic/styles.css +631 -0
- package/assets/extensions/themes/aurora/extension.toml +37 -0
- package/assets/extensions/themes/aurora/styles.css +644 -0
- package/assets/extensions/themes/duet/extension.toml +46 -0
- package/assets/extensions/themes/duet/styles.css +573 -0
- package/assets/extensions/themes/editorial/extension.toml +46 -0
- package/assets/extensions/themes/editorial/styles.css +574 -0
- package/assets/extensions/themes/fhnw/extension.toml +61 -0
- package/assets/extensions/themes/fhnw/styles.css +604 -0
- package/assets/extensions/themes/neue/extension.toml +46 -0
- package/assets/extensions/themes/neue/styles.css +489 -0
- package/assets/extensions/themes/sketch/extension.toml +48 -0
- package/assets/extensions/themes/sketch/styles.css +633 -0
- package/assets/extensions/themes/terminal/extension.toml +38 -0
- package/assets/extensions/themes/terminal/styles.css +550 -0
- package/assets/logo/avatar.png +0 -0
- package/assets/logo/logo-dark.svg +10 -0
- package/assets/logo/logo-light.svg +10 -0
- package/assets/logo/mark.svg +15 -0
- package/bin/mdeck.js +2 -0
- package/docs/README.md +8 -0
- package/docs/reference/authoring.md +795 -0
- package/docs/reference/claude-skill.md +96 -0
- package/docs/reference/extensions.md +250 -0
- package/docs/reference/layouts.md +153 -0
- package/docs/reference/palettes.md +106 -0
- package/docs/reference/structured-slides.md +120 -0
- package/docs/reference/themes.md +133 -0
- package/docs/site/assets/favicon.svg +15 -0
- package/docs/site/assets/github-logo.svg +1 -0
- package/docs/site/assets/site.css +162 -0
- package/docs/site/assets/site.js +123 -0
- package/docs/site/build.js +148 -0
- package/docs/site/content/appearance.md +203 -0
- package/docs/site/content/audience.md +178 -0
- package/docs/site/content/commands.md +153 -0
- package/docs/site/content/components.md +134 -0
- package/docs/site/content/drawing.md +72 -0
- package/docs/site/content/editing.md +59 -0
- package/docs/site/content/getting-started.md +103 -0
- package/docs/site/content/layouts.md +126 -0
- package/docs/site/content/media.md +107 -0
- package/docs/site/content/more-content.md +130 -0
- package/docs/site/content/presenting.md +93 -0
- package/docs/site/content/reusable-layouts.md +74 -0
- package/docs/site/content/sharing.md +88 -0
- package/docs/site/content/theme-authoring.md +144 -0
- package/docs/site/content/theme-repository.md +76 -0
- package/docs/site/content/troubleshooting.md +92 -0
- package/docs/site/content/views.md +56 -0
- package/docs/site/content/welcome.md +71 -0
- package/docs/site/content/writing.md +121 -0
- package/docs/site/examples/first-talk.md +88 -0
- package/docs/site/examples/notebook.svg +58 -0
- package/docs/site/images/themes/academic-dark.webp +0 -0
- package/docs/site/images/themes/academic-light.webp +0 -0
- package/docs/site/images/themes/academic.webp +0 -0
- package/docs/site/images/themes/aurora-dark.webp +0 -0
- package/docs/site/images/themes/aurora-light.webp +0 -0
- package/docs/site/images/themes/aurora.webp +0 -0
- package/docs/site/images/themes/duet-dark.webp +0 -0
- package/docs/site/images/themes/duet-light.webp +0 -0
- package/docs/site/images/themes/duet.webp +0 -0
- package/docs/site/images/themes/editorial-dark.webp +0 -0
- package/docs/site/images/themes/editorial-light.webp +0 -0
- package/docs/site/images/themes/editorial.webp +0 -0
- package/docs/site/images/themes/fhnw.webp +0 -0
- package/docs/site/images/themes/neue-dark.webp +0 -0
- package/docs/site/images/themes/neue-light.webp +0 -0
- package/docs/site/images/themes/neue.webp +0 -0
- package/docs/site/images/themes/sketch-dark.webp +0 -0
- package/docs/site/images/themes/sketch-light.webp +0 -0
- package/docs/site/images/themes/sketch.webp +0 -0
- package/docs/site/images/themes/terminal-dark.webp +0 -0
- package/docs/site/images/themes/terminal-light.webp +0 -0
- package/docs/site/images/themes/terminal.webp +0 -0
- package/docs/site/package.json +9 -0
- package/docs/site/pages.js +27 -0
- package/docs/site/server.js +58 -0
- package/examples/custom-layouts/extensions/comparison/extension.toml +35 -0
- package/examples/custom-layouts/extensions/comparison/layout.jsx +15 -0
- package/examples/custom-layouts/extensions/comparison/starter.md +17 -0
- package/examples/custom-layouts/extensions/comparison/styles.css +4 -0
- package/examples/custom-layouts/slides.md +60 -0
- package/examples/poll/slides.md +127 -0
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +19 -0
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +6 -0
- package/examples/python/slides.drawings.json +6 -0
- package/examples/python/slides.md +493 -0
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +10 -0
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +13 -0
- package/examples/showcase/img/image.jpg +0 -0
- package/examples/showcase/img/mdeck.svg +66 -0
- package/examples/showcase/slides.drawings.json +11 -0
- package/examples/showcase/slides.md +427 -0
- package/package.json +75 -0
- package/skills/write-slides/SKILL.md +144 -0
- package/src/build/check.js +49 -0
- package/src/build/chrome.js +130 -0
- package/src/build/components.js +44 -0
- package/src/build/config.js +61 -0
- package/src/build/drawings.js +14 -0
- package/src/build/editorPlugin.js +272 -0
- package/src/build/editorServer.js +36 -0
- package/src/build/fonts.js +84 -0
- package/src/build/homePlugin.js +208 -0
- package/src/build/inkPlugin.js +192 -0
- package/src/build/ownWrites.js +18 -0
- package/src/build/pairing.js +81 -0
- package/src/build/pdf.js +34 -0
- package/src/build/relayAccess.js +85 -0
- package/src/build/slidesPlugin.js +371 -0
- package/src/build/themeCheck.js +151 -0
- package/src/build/tunnelClient.js +184 -0
- package/src/cli/args.js +76 -0
- package/src/cli/index.js +758 -0
- package/src/cli/migrate.js +123 -0
- package/src/cli/skill.js +40 -0
- package/src/cli/themes.js +140 -0
- package/src/components/CodeBlock.jsx +168 -0
- package/src/components/ConnectedViews.jsx +25 -0
- package/src/components/Icon.jsx +8 -0
- package/src/components/Poll.jsx +67 -0
- package/src/components/QrCode.jsx +29 -0
- package/src/components/QrCodeTag.jsx +27 -0
- package/src/components/Question.jsx +28 -0
- package/src/components/Scale.jsx +47 -0
- package/src/components/VideoPlayer.jsx +108 -0
- package/src/components/WordCloud.jsx +106 -0
- package/src/components/activity.jsx +35 -0
- package/src/components/callout.css +78 -0
- package/src/components/code-block.css +123 -0
- package/src/components/columns.css +11 -0
- package/src/components/poll.css +81 -0
- package/src/components/video-player.css +14 -0
- package/src/core/devPath.js +4 -0
- package/src/core/editDeck.js +230 -0
- package/src/core/icons.js +69 -0
- package/src/core/ink.js +221 -0
- package/src/core/labels.js +144 -0
- package/src/core/outline.js +33 -0
- package/src/core/parseSlides.js +152 -0
- package/src/core/source.js +137 -0
- package/src/core/urls.js +34 -0
- package/src/core/validateDeck.js +139 -0
- package/src/editor/api.js +67 -0
- package/src/editor/components/App.jsx +75 -0
- package/src/editor/components/ConflictBanner.jsx +10 -0
- package/src/editor/components/DeckSettings.jsx +108 -0
- package/src/editor/components/DesignApp.jsx +252 -0
- package/src/editor/components/Diagnostics.jsx +10 -0
- package/src/editor/components/ExtensionEditor.jsx +66 -0
- package/src/editor/components/ExtensionForms.jsx +134 -0
- package/src/editor/components/ExtensionsPanel.jsx +114 -0
- package/src/editor/components/Field.jsx +24 -0
- package/src/editor/components/Inspector.jsx +36 -0
- package/src/editor/components/LayoutPicker.jsx +23 -0
- package/src/editor/components/LayoutSelect.jsx +25 -0
- package/src/editor/components/LookThumbnail.jsx +8 -0
- package/src/editor/components/MetaFields.jsx +24 -0
- package/src/editor/components/Outline.jsx +28 -0
- package/src/editor/components/PalettePicker.jsx +32 -0
- package/src/editor/components/Preview.jsx +63 -0
- package/src/editor/components/PropsForm.jsx +46 -0
- package/src/editor/components/RawSlideEditor.jsx +9 -0
- package/src/editor/components/RegionEditor.jsx +15 -0
- package/src/editor/components/Resizer.jsx +18 -0
- package/src/editor/designLink.js +19 -0
- package/src/editor/editor.css +139 -0
- package/src/editor/extensions.js +86 -0
- package/src/editor/keyboard.js +21 -0
- package/src/editor/model.js +38 -0
- package/src/editor/sampleDeck.js +165 -0
- package/src/editor/saveQueue.js +47 -0
- package/src/editor/schema.js +36 -0
- package/src/editor/state.js +92 -0
- package/src/editor/useDeckDocument.js +67 -0
- package/src/extensions/appearance.js +79 -0
- package/src/extensions/contrast.js +57 -0
- package/src/extensions/discover.js +126 -0
- package/src/extensions/manifest.js +209 -0
- package/src/extensions/packs.js +268 -0
- package/src/extensions/tokens.js +25 -0
- package/src/home/Home.jsx +362 -0
- package/src/home/home.css +148 -0
- package/src/layouts/layoutApi.jsx +205 -0
- package/src/layouts/layoutProps.js +30 -0
- package/src/layouts/renderSlide.jsx +17 -0
- package/src/live/answer/answer.css +34 -0
- package/src/live/answer/answer.html +14 -0
- package/src/live/answer/answer.js +130 -0
- package/src/live/client.js +290 -0
- package/src/live/code.js +19 -0
- package/src/live/follow.js +17 -0
- package/src/live/pairing.js +34 -0
- package/src/live/presence.js +33 -0
- package/src/live/roomTag.js +38 -0
- package/src/live/rooms.js +249 -0
- package/src/live/server.js +82 -0
- package/src/live/tunnel.js +180 -0
- package/src/paths.js +9 -0
- package/src/runtime/ReaderView.jsx +193 -0
- package/src/runtime/SlideErrorBoundary.jsx +28 -0
- package/src/runtime/chrome.jsx +55 -0
- package/src/runtime/deck-stage.js +1533 -0
- package/src/runtime/design.html +12 -0
- package/src/runtime/design.jsx +5 -0
- package/src/runtime/editor.html +12 -0
- package/src/runtime/editor.jsx +5 -0
- package/src/runtime/editorBridge.js +58 -0
- package/src/runtime/fit.js +149 -0
- package/src/runtime/home.html +13 -0
- package/src/runtime/home.jsx +5 -0
- package/src/runtime/index.html +15 -0
- package/src/runtime/ink/InkLayer.jsx +17 -0
- package/src/runtime/ink/Toolbar.jsx +145 -0
- package/src/runtime/ink/attach.js +69 -0
- package/src/runtime/ink/bus.js +126 -0
- package/src/runtime/ink/controller.js +132 -0
- package/src/runtime/ink/room.js +101 -0
- package/src/runtime/ink/select.js +124 -0
- package/src/runtime/ink/server.js +75 -0
- package/src/runtime/ink/store.js +55 -0
- package/src/runtime/ink/toolbar.css +79 -0
- package/src/runtime/main.jsx +863 -0
- package/src/runtime/markedSetup.js +147 -0
- package/src/runtime/reader.css +59 -0
- package/src/runtime/registry.jsx +26 -0
- package/src/runtime/syncOrder.js +32 -0
- package/src/runtime/themeLoader.js +68 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
2
|
+
import { resolve, dirname, basename, sep } from 'path'
|
|
3
|
+
import { parseSlides } from '../core/parseSlides.js'
|
|
4
|
+
import { validateDeck, formatDiagnostics } from '../core/validateDeck.js'
|
|
5
|
+
import { loadRegistry, extensionRoots, manifestsOf } from '../extensions/discover.js'
|
|
6
|
+
import { resolvePalette } from '../extensions/appearance.js'
|
|
7
|
+
import { embedFonts } from './fonts.js'
|
|
8
|
+
import { stripNotes as stripNotesFrom } from '../core/editDeck.js'
|
|
9
|
+
import { componentFolders, componentFiles } from './components.js'
|
|
10
|
+
import { inkFileFor, normalizeInk, emptyInk } from '../core/ink.js'
|
|
11
|
+
import { isOwnWrite } from './ownWrites.js'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { assertDrawings } from './drawings.js'
|
|
14
|
+
import { marked } from 'marked'
|
|
15
|
+
|
|
16
|
+
const VIRTUAL_ID = 'virtual:slides'
|
|
17
|
+
const RESOLVED_ID = '\0virtual:slides'
|
|
18
|
+
|
|
19
|
+
const COMPONENTS_ID = 'virtual:deck-components'
|
|
20
|
+
const RESOLVED_COMPONENTS_ID = '\0virtual:deck-components'
|
|
21
|
+
const EXTENSIONS_ID = 'virtual:mdeck-extensions'
|
|
22
|
+
const RESOLVED_EXTENSIONS_ID = '\0virtual:mdeck-extensions'
|
|
23
|
+
const INK_ID = 'virtual:deck-ink'
|
|
24
|
+
const RESOLVED_INK_ID = '\0virtual:deck-ink'
|
|
25
|
+
const SVGS_ID = 'virtual:deck-svgs'
|
|
26
|
+
const RESOLVED_SVGS_ID = '\0virtual:deck-svgs'
|
|
27
|
+
|
|
28
|
+
const MIME_BY_EXT = {
|
|
29
|
+
'.png': 'image/png',
|
|
30
|
+
'.jpg': 'image/jpeg',
|
|
31
|
+
'.jpeg': 'image/jpeg',
|
|
32
|
+
'.gif': 'image/gif',
|
|
33
|
+
'.svg': 'image/svg+xml',
|
|
34
|
+
'.webp': 'image/webp',
|
|
35
|
+
'.avif': 'image/avif',
|
|
36
|
+
'.mp4': 'video/mp4',
|
|
37
|
+
'.m4v': 'video/x-m4v',
|
|
38
|
+
'.webm': 'video/webm',
|
|
39
|
+
'.ogv': 'video/ogg',
|
|
40
|
+
'.mp3': 'audio/mpeg',
|
|
41
|
+
'.m4a': 'audio/mp4',
|
|
42
|
+
'.ogg': 'audio/ogg',
|
|
43
|
+
'.wav': 'audio/wav',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isLocalAssetRef(ref = '') {
|
|
47
|
+
return ref
|
|
48
|
+
&& !/^([a-z]+:)?\/\//i.test(ref)
|
|
49
|
+
&& !ref.startsWith('data:')
|
|
50
|
+
&& !ref.startsWith('#')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cleanAssetRef(ref) {
|
|
54
|
+
const q = ref.indexOf('?')
|
|
55
|
+
const h = ref.indexOf('#')
|
|
56
|
+
const end = [q, h].filter(i => i !== -1).sort((a, b) => a - b)[0] ?? ref.length
|
|
57
|
+
return ref.slice(0, end)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// An SVG picture that uses the theme's colour variables (var(--accent) …) is
|
|
61
|
+
// drawn inline by the picture layouts, so it follows the theme and palette.
|
|
62
|
+
// Inside an <img> it could not see them.
|
|
63
|
+
const themedSvg = file => /\.svg$/i.test(file) && existsSync(file) && /var\(--/.test(readFileSync(file, 'utf8'))
|
|
64
|
+
|
|
65
|
+
export function themedSvgs(deckFile, source = readFileSync(deckFile, 'utf8')) {
|
|
66
|
+
const svgs = {}
|
|
67
|
+
for (const ref of collectLocalAssetRefs(source)) {
|
|
68
|
+
const file = resolve(dirname(deckFile), ref)
|
|
69
|
+
if (themedSvg(file)) svgs[ref] = { file, markup: readFileSync(file, 'utf8').replace(/^\s*<\?xml[^>]*>\s*/, '').replace(/<!DOCTYPE[^>]*>\s*/i, '') }
|
|
70
|
+
}
|
|
71
|
+
return svgs
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toDataUrl(ref, baseDir) {
|
|
75
|
+
if (!isLocalAssetRef(ref)) return null
|
|
76
|
+
const cleanRef = cleanAssetRef(ref)
|
|
77
|
+
const abs = resolve(baseDir, cleanRef)
|
|
78
|
+
if (!existsSync(abs) || themedSvg(abs)) return null
|
|
79
|
+
const ext = cleanRef.toLowerCase().slice(cleanRef.lastIndexOf('.'))
|
|
80
|
+
const mime = MIME_BY_EXT[ext]
|
|
81
|
+
if (!mime) return null
|
|
82
|
+
const b64 = readFileSync(abs).toString('base64')
|
|
83
|
+
return `data:${mime};base64,${b64}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function inlineMarkdownImages(markdown, baseDir) {
|
|
87
|
+
return markdown.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, src, title) => {
|
|
88
|
+
const dataUrl = toDataUrl(src, baseDir)
|
|
89
|
+
if (!dataUrl) return m
|
|
90
|
+
return ``
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function inlineHtmlImgSources(markdown, baseDir) {
|
|
95
|
+
return markdown.replace(/(<img\b[^>]*\bsrc=)(["'])([^"']+)(\2)/gi, (m, pre, quote, src, endQuote) => {
|
|
96
|
+
const dataUrl = toDataUrl(src, baseDir)
|
|
97
|
+
if (!dataUrl) return m
|
|
98
|
+
return `${pre}${quote}${dataUrl}${endQuote}`
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inlineHtmlMediaSources(markdown, baseDir) {
|
|
103
|
+
return markdown.replace(/(<(?:video|audio|source|videoplayer)\b[^>]*\bsrc=)(["'])([^"']+)(\2)/gi, (m, pre, quote, src, endQuote) => {
|
|
104
|
+
const dataUrl = toDataUrl(src, baseDir)
|
|
105
|
+
if (!dataUrl) return m
|
|
106
|
+
return `${pre}${quote}${dataUrl}${endQuote}`
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function inlineYamlImageFields(markdown, baseDir) {
|
|
111
|
+
return markdown.replace(/^(\s*)(image|logo):\s*(["']?)([^"'\n]+)\3\s*$/gm, (m, indent, key, quote, value) => {
|
|
112
|
+
const dataUrl = toDataUrl(value.trim(), baseDir)
|
|
113
|
+
if (!dataUrl) return m
|
|
114
|
+
const q = quote || '"'
|
|
115
|
+
return `${indent}${key}: ${q}${dataUrl}${q}`
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function collectLocalAssetRefs(markdown) {
|
|
120
|
+
const refs = new Set()
|
|
121
|
+
const add = ref => {
|
|
122
|
+
const clean = cleanAssetRef(String(ref || '').trim())
|
|
123
|
+
if (isLocalAssetRef(clean)) refs.add(clean)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const deck = parseSlides(markdown)
|
|
127
|
+
add(deck.deckConfig.meta?.logo)
|
|
128
|
+
for (const slide of deck.slides) {
|
|
129
|
+
add(slide.meta.image)
|
|
130
|
+
add(slide.meta.logo)
|
|
131
|
+
add(slide.meta.props?.image)
|
|
132
|
+
const bodies = [...Object.values(slide.regions).map(r => r.content), slide.meta.notes ?? '']
|
|
133
|
+
for (const body of bodies) {
|
|
134
|
+
if (typeof body !== 'string') continue
|
|
135
|
+
marked.walkTokens(marked.lexer(body), token => {
|
|
136
|
+
if (token.type === 'image') add(token.href)
|
|
137
|
+
if (token.type === 'html') {
|
|
138
|
+
for (const match of token.text.matchAll(/<(?:img|video|audio|source|videoplayer)\b[^>]*\bsrc=(["'])([^"']+)\1/gi)) add(match[2])
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return [...refs]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Applies fn to the text outside fenced code blocks, so a path shown in a code
|
|
148
|
+
// example stays text instead of becoming an embedded file.
|
|
149
|
+
function outsideCodeFences(markdown, fn) {
|
|
150
|
+
const lines = markdown.split('\n')
|
|
151
|
+
let out = '', text = '', fence = null
|
|
152
|
+
for (const [i, line] of lines.entries()) {
|
|
153
|
+
const chunk = line + (i < lines.length - 1 ? '\n' : '')
|
|
154
|
+
const marker = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1]
|
|
155
|
+
if (fence) {
|
|
156
|
+
out += chunk
|
|
157
|
+
if (marker && marker[0] === fence[0] && marker.length >= fence.length && !line.trim().slice(marker.length).trim()) fence = null
|
|
158
|
+
} else if (marker) {
|
|
159
|
+
out += fn(text) + chunk
|
|
160
|
+
text = ''
|
|
161
|
+
fence = marker
|
|
162
|
+
} else text += chunk
|
|
163
|
+
}
|
|
164
|
+
return out + fn(text)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function maybeInlineAssets(markdown, abs, { inlineImages, inlineMedia }) {
|
|
168
|
+
if (!inlineImages && !inlineMedia) return markdown
|
|
169
|
+
const baseDir = dirname(abs)
|
|
170
|
+
return outsideCodeFences(markdown, text => {
|
|
171
|
+
let out = text
|
|
172
|
+
if (inlineImages) out = inlineMarkdownImages(out, baseDir)
|
|
173
|
+
if (inlineImages) out = inlineHtmlImgSources(out, baseDir)
|
|
174
|
+
if (inlineImages) out = inlineYamlImageFields(out, baseDir)
|
|
175
|
+
if (inlineMedia) out = inlineHtmlMediaSources(out, baseDir)
|
|
176
|
+
return out
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Extension registry module ────────────────────────────────────────────
|
|
181
|
+
// Layouts, themes and palettes are discovered in Node and handed to the
|
|
182
|
+
// browser as one generated module. Layout code and styles are imported
|
|
183
|
+
// eagerly; theme stylesheets stay lazy so only the chosen theme is loaded.
|
|
184
|
+
// Layout JSX is trusted code, not sandboxed data.
|
|
185
|
+
|
|
186
|
+
export function generateExtensionsModule(registry) {
|
|
187
|
+
const imports = []
|
|
188
|
+
const layouts = Object.values(registry.layouts).map((t, i) => {
|
|
189
|
+
imports.push(`import L${i} from ${JSON.stringify(t.files.layout)}`, ...t.files.styles.map(s => `import ${JSON.stringify(s)}`))
|
|
190
|
+
return `${JSON.stringify(t.id)}: { manifest: ${JSON.stringify(t.manifest)}, render: L${i} }`
|
|
191
|
+
})
|
|
192
|
+
const themes = Object.values(registry.themes).map(t => {
|
|
193
|
+
const loads = t.files.styles.map(s => `import(${JSON.stringify(s + '?inline')})`)
|
|
194
|
+
return `${JSON.stringify(t.id)}: { manifest: ${JSON.stringify(t.manifest)}, load: () => Promise.all([${loads.join(', ')}]).then(mods => mods.map(m => m.default).join('\\n')) }`
|
|
195
|
+
})
|
|
196
|
+
const palettes = Object.values(registry.palettes).map(p => `${JSON.stringify(p.id)}: { manifest: ${JSON.stringify(p.manifest)} }`)
|
|
197
|
+
return `${imports.join('\n')}\nexport const layouts = {\n${layouts.join(',\n')}\n}\nexport const themes = {\n${themes.join(',\n')}\n}\nexport const palettes = {\n${palettes.join(',\n')}\n}\n`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A file for readers carries only the deck's own theme and the palette it
|
|
201
|
+
// shows: the reader view offers light and dark, nothing else, and only that
|
|
202
|
+
// theme's fonts travel with the file.
|
|
203
|
+
export function deckLookOnly(registry, deckConfig = {}) {
|
|
204
|
+
const theme = registry.themes[deckConfig.theme ?? 'neue']
|
|
205
|
+
if (!theme) return registry
|
|
206
|
+
const palettes = Object.fromEntries(Object.entries(registry.palettes).map(([id, record]) => [id, record.manifest]))
|
|
207
|
+
const { palette } = resolvePalette({ theme: theme.manifest, palettes, palette: deckConfig.palette })
|
|
208
|
+
return { ...registry, themes: { [theme.id]: theme }, palettes: palette ? { [palette.id]: registry.palettes[palette.id] } : {} }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// `editor` keeps the page alive while `mdeck edit` rewrites the deck: deck
|
|
212
|
+
// changes only invalidate the module, validation errors are warnings, and
|
|
213
|
+
// extension changes are announced instead of forcing a reload. `source` is
|
|
214
|
+
// the deck's text where there is no file (`mdeck design`, on the sample deck);
|
|
215
|
+
// `slidesPath` then only says where its folder is.
|
|
216
|
+
export function slidesPlugin(slidesPath, { inlineImages = false, inlineMedia = false, editor = false, stripNotes = false, ink = true, embedFonts: embedThemeFonts = false, deckLook = false, source: given = null } = {}) {
|
|
217
|
+
const abs = resolve(slidesPath)
|
|
218
|
+
const readDeck = () => given ?? readFileSync(abs, 'utf-8')
|
|
219
|
+
const watchDeck = context => { if (given === null) context.addWatchFile(abs) }
|
|
220
|
+
const sourceFor = raw => stripNotes ? stripNotesFrom(raw) : raw
|
|
221
|
+
const inkPath = inkFileFor(abs)
|
|
222
|
+
const extensionDirs = extensionRoots(abs).map(root => root.dir)
|
|
223
|
+
let componentDirs = componentFolders(abs, readDeck()).map(folder => folder.dir)
|
|
224
|
+
const watchDirs = () => [...extensionDirs, ...componentDirs]
|
|
225
|
+
const isWatched = file => watchDirs().some(dir => resolve(file).startsWith(dir + sep))
|
|
226
|
+
// A changed `components:` list takes effect without restarting the server.
|
|
227
|
+
const followComponentFolders = server => {
|
|
228
|
+
const next = componentFolders(abs, readDeck()).map(folder => folder.dir)
|
|
229
|
+
if (next.join('\n') === componentDirs.join('\n')) return false
|
|
230
|
+
componentDirs = next
|
|
231
|
+
server.watcher.add(next)
|
|
232
|
+
const allow = server.config.server.fs.allow
|
|
233
|
+
for (const dir of next) if (!allow.includes(dir)) allow.push(dir)
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
const ALL_IDS = [RESOLVED_ID, RESOLVED_COMPONENTS_ID, RESOLVED_EXTENSIONS_ID, RESOLVED_INK_ID, RESOLVED_SVGS_ID]
|
|
237
|
+
const SOURCE_IDS = [RESOLVED_ID, RESOLVED_INK_ID, RESOLVED_SVGS_ID]
|
|
238
|
+
const invalidate = (server, ids) => {
|
|
239
|
+
for (const id of ids) {
|
|
240
|
+
const mod = server.moduleGraph.getModuleById(id)
|
|
241
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const reload = (server, ids, file) => {
|
|
245
|
+
invalidate(server, ids)
|
|
246
|
+
if (!editor) server.ws.send({ type: 'full-reload' })
|
|
247
|
+
else if (file) server.ws.send('mdeck:extensions-changed', { file })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
name: 'vite-plugin-slides',
|
|
252
|
+
configureServer(server) {
|
|
253
|
+
server.watcher.add([abs, inkPath, ...watchDirs()])
|
|
254
|
+
const refresh = file => {
|
|
255
|
+
if (isWatched(file)) reload(server, ALL_IDS, resolve(file))
|
|
256
|
+
else if (resolve(file) === inkPath && !(() => { try { return isOwnWrite(inkPath, readFileSync(inkPath, 'utf-8')) } catch { return false } })()) reload(server, [RESOLVED_INK_ID], inkPath)
|
|
257
|
+
}
|
|
258
|
+
for (const event of ['add', 'unlink', 'addDir', 'unlinkDir']) server.watcher.on(event, refresh)
|
|
259
|
+
},
|
|
260
|
+
resolveId(id) {
|
|
261
|
+
if (id === 'mdeck/template-api') throw new Error('mdeck/template-api is now mdeck/layout. Run mdeck migrate to update local imports.')
|
|
262
|
+
if (id === 'mdeck/layout') return fileURLToPath(new URL('../layouts/layoutApi.jsx', import.meta.url))
|
|
263
|
+
if (id === 'mdeck/live') return fileURLToPath(new URL('../live/client.js', import.meta.url))
|
|
264
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID
|
|
265
|
+
if (id === COMPONENTS_ID) return RESOLVED_COMPONENTS_ID
|
|
266
|
+
if (id === EXTENSIONS_ID) return RESOLVED_EXTENSIONS_ID
|
|
267
|
+
if (id === INK_ID) return RESOLVED_INK_ID
|
|
268
|
+
if (id === SVGS_ID) return RESOLVED_SVGS_ID
|
|
269
|
+
},
|
|
270
|
+
load(id) {
|
|
271
|
+
if (id === RESOLVED_ID) {
|
|
272
|
+
watchDeck(this)
|
|
273
|
+
const raw = readDeck()
|
|
274
|
+
const registry = loadRegistry(abs)
|
|
275
|
+
for (const warning of registry.warnings) this.warn(warning)
|
|
276
|
+
const diagnostics = validateDeck(parseSlides(raw), {
|
|
277
|
+
layouts: manifestsOf(registry, 'layout'), themes: manifestsOf(registry, 'theme'), palettes: manifestsOf(registry, 'palette'),
|
|
278
|
+
})
|
|
279
|
+
const errors = diagnostics.filter(d => d.severity === 'error')
|
|
280
|
+
if (errors.length && !editor) throw new Error(formatDiagnostics(errors, abs))
|
|
281
|
+
if (diagnostics.length) this.warn(formatDiagnostics(diagnostics, abs))
|
|
282
|
+
const source = maybeInlineAssets(sourceFor(raw), abs, { inlineImages, inlineMedia })
|
|
283
|
+
return `export default ${JSON.stringify(source)}`
|
|
284
|
+
}
|
|
285
|
+
// The deck's saved ink (<deck>.drawings.json), scaled to its design size.
|
|
286
|
+
// Builds bundle it, so the reader view and PDFs show it too.
|
|
287
|
+
if (id === RESOLVED_INK_ID) {
|
|
288
|
+
if (ink) assertDrawings(abs)
|
|
289
|
+
// Only an existing file: Vite treats a missing watch file as a missing
|
|
290
|
+
// import. The dev server's watcher notices the file once it appears.
|
|
291
|
+
if (existsSync(inkPath)) this.addWatchFile(inkPath)
|
|
292
|
+
let size = {}
|
|
293
|
+
try { const config = parseSlides(readDeck()).deckConfig ?? {}; size = { width: config.width ?? 1920, height: config.height ?? 1080 } } catch {}
|
|
294
|
+
let data = emptyInk(size)
|
|
295
|
+
if (ink && existsSync(inkPath)) {
|
|
296
|
+
try { data = normalizeInk(JSON.parse(readFileSync(inkPath, 'utf-8')), size) } catch (error) { this.warn(`${inkPath}: ${error.message}; showing no ink`) }
|
|
297
|
+
}
|
|
298
|
+
return `export default ${JSON.stringify(data)}\nexport const inkFileName = ${JSON.stringify(basename(inkPath))}`
|
|
299
|
+
}
|
|
300
|
+
if (id === RESOLVED_SVGS_ID) {
|
|
301
|
+
watchDeck(this)
|
|
302
|
+
const svgs = themedSvgs(abs, sourceFor(readDeck()))
|
|
303
|
+
for (const { file } of Object.values(svgs)) this.addWatchFile(file)
|
|
304
|
+
return `export default ${JSON.stringify(Object.fromEntries(Object.entries(svgs).map(([ref, { markup }]) => [ref, markup])))}`
|
|
305
|
+
}
|
|
306
|
+
if (id === RESOLVED_EXTENSIONS_ID) {
|
|
307
|
+
const registry = loadRegistry(abs)
|
|
308
|
+
for (const record of registry.records) this.addWatchFile(record.file)
|
|
309
|
+
if (!deckLook) return generateExtensionsModule(registry)
|
|
310
|
+
let config = {}
|
|
311
|
+
try { config = parseSlides(readDeck()).deckConfig ?? {} } catch {}
|
|
312
|
+
return generateExtensionsModule(deckLookOnly(registry, config))
|
|
313
|
+
}
|
|
314
|
+
if (id === RESOLVED_COMPONENTS_ID) {
|
|
315
|
+
const files = componentFiles(abs, componentFolders(abs, readDeck()))
|
|
316
|
+
const imports = files
|
|
317
|
+
.map((f, i) => `import C${i} from ${JSON.stringify(f.file)}`)
|
|
318
|
+
.join('\n')
|
|
319
|
+
const entries = files
|
|
320
|
+
.map((f, i) => ` ${JSON.stringify(f.tag)}: C${i},`)
|
|
321
|
+
.join('\n')
|
|
322
|
+
return `${imports}\nexport default {\n${entries}\n}\n`
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
// The deck page carries the deck's language before any script runs, for
|
|
326
|
+
// screen readers, hyphenation and search engines.
|
|
327
|
+
// A single-file build also carries the theme's fonts (fonts.js).
|
|
328
|
+
async transformIndexHtml(html, context) {
|
|
329
|
+
if (!/(^|[\/])index\.html$/.test(context.filename ?? context.path ?? '')) return html
|
|
330
|
+
let config = {}
|
|
331
|
+
try { config = parseSlides(readDeck()).deckConfig ?? {} } catch {}
|
|
332
|
+
const lang = config.lang
|
|
333
|
+
if (typeof lang === 'string' && /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(lang)) html = html.replace(/<html lang="[^"]*"/, `<html lang="${lang}"`)
|
|
334
|
+
if (embedThemeFonts) {
|
|
335
|
+
const theme = loadRegistry(abs).themes[config.theme ?? 'neue']?.manifest
|
|
336
|
+
const { css, warnings } = await embedFonts(theme?.fonts ?? [])
|
|
337
|
+
for (const warning of warnings) console.warn(` ! ${warning}`)
|
|
338
|
+
if (css) html = html.replace('</head>', `<style data-mdeck-fonts>\n${css}\n</style>\n</head>`)
|
|
339
|
+
}
|
|
340
|
+
return html
|
|
341
|
+
},
|
|
342
|
+
handleHotUpdate({ file, server }) {
|
|
343
|
+
const changed = resolve(file)
|
|
344
|
+
if (isWatched(changed)) {
|
|
345
|
+
reload(server, ALL_IDS, changed)
|
|
346
|
+
return []
|
|
347
|
+
}
|
|
348
|
+
// mdeck's own writes (saved ink, a new slide id) must not reload the
|
|
349
|
+
// windows mid-talk: the pages refresh their ink on `mdeck:ink` instead.
|
|
350
|
+
const own = [inkPath, abs].includes(changed) && (() => { try { return isOwnWrite(changed, readFileSync(changed, 'utf-8')) } catch { return false } })()
|
|
351
|
+
if (own) {
|
|
352
|
+
invalidate(server, changed === abs ? SOURCE_IDS : [RESOLVED_INK_ID])
|
|
353
|
+
return []
|
|
354
|
+
}
|
|
355
|
+
if (changed === inkPath) {
|
|
356
|
+
reload(server, [RESOLVED_INK_ID], changed)
|
|
357
|
+
return []
|
|
358
|
+
}
|
|
359
|
+
if (changed === abs) {
|
|
360
|
+
if (followComponentFolders(server)) {
|
|
361
|
+
reload(server, ALL_IDS, changed)
|
|
362
|
+
return []
|
|
363
|
+
}
|
|
364
|
+
// The deck sets the ink's design size and which SVGs are inlined.
|
|
365
|
+
invalidate(server, SOURCE_IDS)
|
|
366
|
+
if (editor) return []
|
|
367
|
+
server.ws.send({ type: 'full-reload' })
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Themes and palettes on the sample deck (src/editor/sampleDeck.js), in a
|
|
2
|
+
// real browser: the check behind `npm run test:themes` and `mdeck themes
|
|
3
|
+
// build --check`, and the preview pictures of the theme repository. Each
|
|
4
|
+
// look is checked light and dark on every slide, and fails when content does
|
|
5
|
+
// not fit, text is cut off at the slide's edge, text is hard to read on what
|
|
6
|
+
// is behind it, or the page reports an error. Needs Chrome (MDECK_CHROME).
|
|
7
|
+
import { execFileSync } from 'node:child_process'
|
|
8
|
+
import { cpSync, mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
9
|
+
import { tmpdir } from 'node:os'
|
|
10
|
+
import { resolve } from 'node:path'
|
|
11
|
+
import { launchChrome } from './chrome.js'
|
|
12
|
+
import { frameworkRoot } from '../paths.js'
|
|
13
|
+
import { sampleDeck, SAMPLE_PICTURE } from '../editor/sampleDeck.js'
|
|
14
|
+
|
|
15
|
+
// Runs in the page before the deck: keeps what the page reports.
|
|
16
|
+
const RECORDER = `window.__report = [];
|
|
17
|
+
for (const level of ['warn', 'error']) { const original = console[level]; console[level] = (...args) => { window.__report.push(level + ': ' + args.map(String).join(' ')); original.apply(console, args) } }
|
|
18
|
+
addEventListener('error', event => window.__report.push('error: ' + event.message))
|
|
19
|
+
addEventListener('unhandledrejection', event => window.__report.push('error: ' + (event.reason?.message ?? event.reason)))`
|
|
20
|
+
|
|
21
|
+
// Runs in the page for the slide on screen: what is cut off or hard to read.
|
|
22
|
+
const MEASURE = `(() => {
|
|
23
|
+
const slide = document.querySelector('deck-stage').querySelectorAll(':scope > section')[window.__index]
|
|
24
|
+
const problems = []
|
|
25
|
+
const frame = slide.getBoundingClientRect()
|
|
26
|
+
const scale = frame.width / slide.offsetWidth
|
|
27
|
+
const body = slide.querySelector('.slide-body')
|
|
28
|
+
if (body && body.scrollHeight > body.clientHeight + 2) problems.push('the content is ' + Math.round(body.scrollHeight - body.clientHeight) + 'px taller than its place')
|
|
29
|
+
const parse = value => { const m = value.match(/rgba?\\(([^)]+)\\)/); if (!m) return null; const [r, g, b, a = 1] = m[1].split(/[ ,/]+/).filter(Boolean).map(Number); return { r, g, b, a } }
|
|
30
|
+
const lum = ({ r, g, b }) => { const c = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4 }; return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b) }
|
|
31
|
+
const ratio = (x, y) => { const [a, b] = [lum(x), lum(y)].sort((p, q) => q - p); return (a + 0.05) / (b + 0.05) }
|
|
32
|
+
// The colour behind an element, when it is a plain colour; null behind a
|
|
33
|
+
// picture or a gradient, where it cannot be told.
|
|
34
|
+
// The slide's own ::before and ::after fields (duet's chapter colour field),
|
|
35
|
+
// as rectangles in slide coordinates.
|
|
36
|
+
const fields = ['::before', '::after'].map(pseudo => getComputedStyle(slide, pseudo)).filter(drawn => drawn.content !== 'none' && drawn.position === 'absolute'
|
|
37
|
+
&& (drawn.backgroundImage !== 'none' || (parse(drawn.backgroundColor)?.a ?? 0) > 0.05))
|
|
38
|
+
.map(drawn => { const left = parseFloat(drawn.left) || 0, top = parseFloat(drawn.top) || 0; return { left, top, right: left + (parseFloat(drawn.width) || slide.offsetWidth), bottom: top + (parseFloat(drawn.height) || slide.offsetHeight) } })
|
|
39
|
+
const behind = (element, box) => {
|
|
40
|
+
if (element.closest('.slide--full-bleed-image')) return null
|
|
41
|
+
const x = (box.left + box.width / 2 - frame.left) / scale, y = (box.top + box.height / 2 - frame.top) / scale
|
|
42
|
+
if (fields.some(field => x >= field.left && x <= field.right && y >= field.top && y <= field.bottom)) return null
|
|
43
|
+
for (let node = element; node && node !== document.documentElement; node = node.parentElement) {
|
|
44
|
+
const style = getComputedStyle(node)
|
|
45
|
+
// A field drawn by ::before or ::after (duet's chapter) is behind it too.
|
|
46
|
+
for (const pseudo of ['::before', '::after']) {
|
|
47
|
+
const drawn = getComputedStyle(node, pseudo)
|
|
48
|
+
const painted = drawn.backgroundImage !== 'none' || (parse(drawn.backgroundColor)?.a ?? 0) > 0.05
|
|
49
|
+
const area = (parseFloat(drawn.width) || 0) * (parseFloat(drawn.height) || 0)
|
|
50
|
+
if (node !== slide && drawn.content !== 'none' && painted && area >= 0.25 * node.offsetWidth * node.offsetHeight) return null
|
|
51
|
+
}
|
|
52
|
+
if (style.backgroundImage !== 'none' && !(node === slide && /radial-gradient/.test(style.backgroundImage) && !/linear/.test(style.backgroundImage))) return null
|
|
53
|
+
const colour = parse(style.backgroundColor)
|
|
54
|
+
if (colour && colour.a >= 0.95) return colour
|
|
55
|
+
if (colour && colour.a > 0.05) return null
|
|
56
|
+
if (node === slide) return colour
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
const walker = document.createTreeWalker(slide, NodeFilter.SHOW_TEXT)
|
|
61
|
+
const seen = new Set()
|
|
62
|
+
for (let text = walker.nextNode(); text; text = walker.nextNode()) {
|
|
63
|
+
const element = text.parentElement
|
|
64
|
+
if (!text.textContent.trim() || seen.has(element) || element.closest('.slide-ink, .katex-mathml, [aria-hidden="true"], .slide--full-bleed-image .slide-bg')) continue
|
|
65
|
+
seen.add(element)
|
|
66
|
+
const style = getComputedStyle(element)
|
|
67
|
+
if (style.visibility === 'hidden' || Number(style.opacity) === 0 || element.closest('[data-step]:not([data-step-visible])')) continue
|
|
68
|
+
const range = document.createRange()
|
|
69
|
+
range.selectNodeContents(text)
|
|
70
|
+
const box = range.getBoundingClientRect()
|
|
71
|
+
if (!box.width || !box.height) continue
|
|
72
|
+
const words = text.textContent.trim().slice(0, 40)
|
|
73
|
+
if (box.left < frame.left - 2 * scale || box.right > frame.right + 2 * scale || box.top < frame.top - 2 * scale || box.bottom > frame.bottom + 2 * scale) problems.push('"' + words + '" is cut off at the edge')
|
|
74
|
+
const colour = parse(style.color), back = behind(element, box)
|
|
75
|
+
if (!colour || !back || colour.a < 0.5 || element.closest('.code-block, .katex')) continue
|
|
76
|
+
const large = parseFloat(style.fontSize) / scale >= 40 || (parseFloat(style.fontSize) / scale >= 30 && Number(style.fontWeight) >= 600)
|
|
77
|
+
const value = ratio(colour, back)
|
|
78
|
+
if (value < (large ? 2.6 : 3.4)) problems.push('"' + words + '" has a contrast of ' + value.toFixed(1) + ':1')
|
|
79
|
+
}
|
|
80
|
+
return problems
|
|
81
|
+
})()`
|
|
82
|
+
|
|
83
|
+
// Builds the sample deck with `extensions` ({ id: folder }) beside it and
|
|
84
|
+
// opens it in Chrome for `use({ open, ids })`. Installed user extensions are
|
|
85
|
+
// left out, so the result depends only on what is given.
|
|
86
|
+
export async function withSampleDeck({ extensions = {} } = {}, use) {
|
|
87
|
+
const temp = mkdtempSync(resolve(tmpdir(), 'mdeck-sample-'))
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(resolve(temp, 'slides.md'), sampleDeck())
|
|
90
|
+
writeFileSync(resolve(temp, 'picture.svg'), SAMPLE_PICTURE)
|
|
91
|
+
for (const [id, dir] of Object.entries(extensions)) cpSync(dir, resolve(temp, 'extensions', id), { recursive: true })
|
|
92
|
+
mkdirSync(resolve(temp, 'home'))
|
|
93
|
+
execFileSync(process.execPath, [resolve(frameworkRoot, 'bin/mdeck.js'), 'build', resolve(temp, 'slides.md'), '-o', resolve(temp, 'out/index.html')],
|
|
94
|
+
{ cwd: temp, stdio: 'pipe', env: { ...process.env, MDECK_HOME: resolve(temp, 'home') } })
|
|
95
|
+
const browser = await launchChrome({ dir: resolve(temp, 'out') })
|
|
96
|
+
try {
|
|
97
|
+
// A page with the deck in a look, recording what it reports.
|
|
98
|
+
const open = async ({ theme, palette = '', appearance = '' }) => {
|
|
99
|
+
const page = await browser.open('about:blank')
|
|
100
|
+
await page.send('Page.enable')
|
|
101
|
+
await page.send('Page.addScriptToEvaluateOnNewDocument', { source: RECORDER })
|
|
102
|
+
await page.send('Page.navigate', { url: `${browser.origin}/index.html?embedded=1&theme=${theme}&palette=${palette}&appearance=${appearance}#1` })
|
|
103
|
+
await page.waitFor("location.href.includes('index.html')")
|
|
104
|
+
await page.waitForDeck()
|
|
105
|
+
return page
|
|
106
|
+
}
|
|
107
|
+
return await use({ open })
|
|
108
|
+
} finally { await browser.close() }
|
|
109
|
+
} finally { rmSync(temp, { recursive: true, force: true }) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const settle = page => page.evaluate('new Promise(done => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(done, 60))))')
|
|
113
|
+
|
|
114
|
+
// Every slide of each look ({ theme, palette }), light and dark; returns the
|
|
115
|
+
// problems found, one line each.
|
|
116
|
+
export function checkLooks(looks, { extensions = {} } = {}) {
|
|
117
|
+
return withSampleDeck({ extensions }, async ({ open }) => {
|
|
118
|
+
const failures = []
|
|
119
|
+
for (const { theme, palette = '' } of looks) {
|
|
120
|
+
for (const appearance of ['light', 'dark']) {
|
|
121
|
+
const label = `${theme}${palette ? ` with ${palette}` : ''} (${appearance})`
|
|
122
|
+
const page = await open({ theme, palette, appearance })
|
|
123
|
+
await page.send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 900, deviceScaleFactor: 1, mobile: false })
|
|
124
|
+
const count = await page.evaluate("document.querySelector('deck-stage').length")
|
|
125
|
+
for (let index = 0; index < count; index++) {
|
|
126
|
+
await page.evaluate(`window.__index = ${index}; document.querySelector('deck-stage').goTo(${index})`)
|
|
127
|
+
// Two frames: the stage shows the slide, then fit.js measures it.
|
|
128
|
+
await settle(page)
|
|
129
|
+
for (const problem of await page.evaluate(MEASURE)) failures.push(`${label}, slide ${index + 1}: ${problem}`)
|
|
130
|
+
}
|
|
131
|
+
for (const line of await page.evaluate('window.__report')) failures.push(`${label}: ${line}`)
|
|
132
|
+
await page.close()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return failures
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Pictures of looks on the sample deck, as WebP: { file, theme, palette,
|
|
140
|
+
// slide } each, slide counting from 1.
|
|
141
|
+
export function renderPreviews(shots, { extensions = {}, width = 960, height = 540 } = {}) {
|
|
142
|
+
return withSampleDeck({ extensions }, async ({ open }) => {
|
|
143
|
+
for (const { file, theme, palette = '', slide = 1 } of shots) {
|
|
144
|
+
const page = await open({ theme, palette })
|
|
145
|
+
if (slide > 1) { await page.evaluate(`document.querySelector('deck-stage').goTo(${slide - 1})`); await settle(page) }
|
|
146
|
+
mkdirSync(resolve(file, '..'), { recursive: true })
|
|
147
|
+
writeFileSync(file, await page.screenshot({ width, height, format: 'webp' }))
|
|
148
|
+
await page.close()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|