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,184 @@
|
|
|
1
|
+
// The computer's end of `mdeck run --server` (the server's end is
|
|
2
|
+
// src/live/tunnel.js). It keeps one outbound WebSocket to the server and
|
|
3
|
+
// answers what comes down it from the local dev server, so an iPad can reach
|
|
4
|
+
// the slides through the server without any open port here.
|
|
5
|
+
//
|
|
6
|
+
// This is the trust boundary. Requests that arrive through the tunnel look,
|
|
7
|
+
// to the dev server, like requests from this computer, so two things stand
|
|
8
|
+
// between them and the launch page, the editor and the files around the deck:
|
|
9
|
+
// only addresses the presenter and audience pages need are forwarded
|
|
10
|
+
// (`shareable`), and every forwarded request carries a mark that makes
|
|
11
|
+
// `isAllowedRequest` refuse it, so it must show a paired device's token.
|
|
12
|
+
import http from 'node:http'
|
|
13
|
+
import { randomBytes } from 'node:crypto'
|
|
14
|
+
import WebSocket from 'ws'
|
|
15
|
+
import { TUNNEL_MARK } from './editorPlugin.js'
|
|
16
|
+
|
|
17
|
+
/** A name for the tunnel nobody can guess: it is the secret part of the address. */
|
|
18
|
+
export const newTunnelId = () => randomBytes(18).toString('base64url')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The dev server's address with the loopback address it listens on, when it
|
|
22
|
+
* listens on one: `localhost` may resolve to the other (IPv4 or IPv6).
|
|
23
|
+
*/
|
|
24
|
+
export function loopbackUrl(url, bound) {
|
|
25
|
+
const address = typeof bound === 'object' ? bound?.address : null
|
|
26
|
+
if (!address || !(address === '::1' || /^127\./.test(address))) return url
|
|
27
|
+
const local = new URL(url)
|
|
28
|
+
local.hostname = address.includes(':') ? `[${address}]` : address
|
|
29
|
+
return local.href
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const READ = new Set(['GET', 'HEAD'])
|
|
33
|
+
const CHUNK = 256 * 1024
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether an address may be served through the tunnel. `base` is the dev
|
|
37
|
+
* server's base path (/t/<id>/); `resources` lists exact approved module
|
|
38
|
+
* and asset URLs, including semantic queries such as ?raw.
|
|
39
|
+
*/
|
|
40
|
+
export function resourceKey(address) {
|
|
41
|
+
const url = new URL(address, 'http://localhost')
|
|
42
|
+
for (const name of ['t', 'v', 'import']) url.searchParams.delete(name)
|
|
43
|
+
url.searchParams.sort()
|
|
44
|
+
return decodeURIComponent(url.pathname) + url.search
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function shareable(address, method, { base, resources = new Set(), session = null }) {
|
|
48
|
+
let url
|
|
49
|
+
try { url = new URL(address, 'http://localhost') } catch { return false }
|
|
50
|
+
const pathname = url.pathname
|
|
51
|
+
if (!pathname.startsWith(base)) return false
|
|
52
|
+
let rest
|
|
53
|
+
try { rest = decodeURIComponent(pathname.slice(base.length)) } catch { return false }
|
|
54
|
+
const parts = rest.split('/')
|
|
55
|
+
if (rest.includes('\0') || rest.includes('\\') || parts.includes('..')) return false
|
|
56
|
+
if (rest === '' || rest === 'index.html') return READ.has(method)
|
|
57
|
+
if (parts.some(part => part.startsWith('.') && part !== '.vite')) return false
|
|
58
|
+
if (rest === '__mdeck/ink') return method === 'GET'
|
|
59
|
+
if (rest === '__mdeck/ink/ops' || rest === '__mdeck/pair/claim') return method === 'POST'
|
|
60
|
+
if (rest === '__mdeck/live/info') return READ.has(method)
|
|
61
|
+
const room = rest.match(/^__mdeck\/live\/rooms\/([^/]+)(\/events|\/state|\/reset|\/ink)?$/)
|
|
62
|
+
if (room && session && (room[1] === session || room[1].startsWith(`${session}.`))) return room[2] === '/events' ? method === 'GET' : method === 'POST'
|
|
63
|
+
if (rest.startsWith('__mdeck/')) return false
|
|
64
|
+
if (!READ.has(method)) return false
|
|
65
|
+
try { return resources.has(resourceKey(url.href)) } catch { return false }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Opens the tunnel and keeps it open, reconnecting after a drop. `server` is
|
|
70
|
+
* the server's address, `target` the dev server's (a URL), `tokens` the
|
|
71
|
+
* paired devices' tokens (sent to the server so they may steer rooms).
|
|
72
|
+
* Returns { url, state(), push(), stop() }; `onState` hears 'connecting',
|
|
73
|
+
* 'up', 'down' and 'refused' with a reason.
|
|
74
|
+
*/
|
|
75
|
+
export function startTunnel({ server, key, id, target, base, resources = new Set(), allow = (path, method) => shareable(path, method, { base, resources, session: session() }), session = () => null, tokens = () => [], onState = () => {}, log = () => {} }) {
|
|
76
|
+
const root = server.replace(/\/+$/, '')
|
|
77
|
+
const socketUrl = new URL(`${root}/tunnel/${id}`)
|
|
78
|
+
socketUrl.protocol = socketUrl.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
79
|
+
const local = new URL(target)
|
|
80
|
+
let socket = null, state = 'connecting', stopped = false, delay = 1000, timer = null
|
|
81
|
+
const requests = new Map(), peers = new Map()
|
|
82
|
+
|
|
83
|
+
const set = (next, reason) => { state = next; onState(next, reason) }
|
|
84
|
+
const send = message => { if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message)) }
|
|
85
|
+
const push = () => send({ t: 'tokens', tokens: tokens(), session: session() })
|
|
86
|
+
const refuse = (id, status, message) => send({ t: 'err', id, status, message })
|
|
87
|
+
|
|
88
|
+
function pace(res) {
|
|
89
|
+
if (!socket || socket.bufferedAmount < 8 * 1024 * 1024) return
|
|
90
|
+
res.pause()
|
|
91
|
+
const poll = setInterval(() => { if (!socket || socket.bufferedAmount < 1024 * 1024) { clearInterval(poll); res.resume() } }, 20)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function onRequest({ id, method, path, headers = {}, body }) {
|
|
95
|
+
let url
|
|
96
|
+
try { url = new URL(path, 'http://localhost') } catch { return refuse(id, 400, 'Bad address') }
|
|
97
|
+
if (typeof method !== 'string' || !await allow(url.pathname + url.search, method)) {
|
|
98
|
+
log(`not shared: ${method} ${url.pathname}`)
|
|
99
|
+
return refuse(id, 403, 'This address is not shared')
|
|
100
|
+
}
|
|
101
|
+
const outgoing = { ...headers, host: local.host, [TUNNEL_MARK]: '1' }
|
|
102
|
+
const request = http.request({ hostname: local.hostname.replace(/^\[|\]$/g, ''), port: local.port, method, path: url.pathname + url.search, headers: outgoing }, res => {
|
|
103
|
+
send({ t: 'head', id, status: res.statusCode, headers: res.headers })
|
|
104
|
+
res.on('data', chunk => {
|
|
105
|
+
for (let at = 0; at < chunk.length; at += CHUNK) send({ t: 'data', id, b: chunk.subarray(at, at + CHUNK).toString('base64') })
|
|
106
|
+
pace(res)
|
|
107
|
+
})
|
|
108
|
+
res.on('end', () => { requests.delete(id); send({ t: 'end', id }) })
|
|
109
|
+
res.on('error', () => { requests.delete(id); refuse(id, 502, 'The local server stopped answering') })
|
|
110
|
+
})
|
|
111
|
+
requests.set(id, request)
|
|
112
|
+
request.on('error', error => { requests.delete(id); refuse(id, 502, `The local server did not answer: ${error.message}`) })
|
|
113
|
+
request.end(body ? Buffer.from(body, 'base64') : undefined)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onSocketOpen({ id, path, protocols = [] }) {
|
|
117
|
+
let url
|
|
118
|
+
try { url = new URL(path, 'http://localhost') } catch { return send({ t: 'wsclose', id, code: 1008 }) }
|
|
119
|
+
// Only the dev server's reload channel, at its base address.
|
|
120
|
+
if (url.pathname !== base || !protocols.every(item => /^vite-/.test(item))) { log(`not shared: WebSocket ${url.pathname}`); return send({ t: 'wsclose', id, code: 1008 }) }
|
|
121
|
+
const queue = []
|
|
122
|
+
const peer = new WebSocket(`ws://${local.host}${url.pathname}${url.search}`, protocols, { headers: { origin: local.origin, [TUNNEL_MARK]: '1' } })
|
|
123
|
+
peers.set(id, { peer, queue })
|
|
124
|
+
peer.on('open', () => { for (const [data, bin] of queue.splice(0)) peer.send(data, { binary: bin }) })
|
|
125
|
+
peer.on('message', (data, bin) => send({ t: 'wsmsg', id, b: Buffer.from(data).toString('base64'), bin }))
|
|
126
|
+
peer.on('close', code => { if (peers.delete(id)) send({ t: 'wsclose', id, code }) })
|
|
127
|
+
peer.on('error', () => {})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function onMessage(raw) {
|
|
131
|
+
let message
|
|
132
|
+
try { message = JSON.parse(raw.toString()) } catch { return }
|
|
133
|
+
switch (message?.t) {
|
|
134
|
+
case 'req': return onRequest(message).catch(() => refuse(message.id, 502, 'Could not check the shared resource'))
|
|
135
|
+
case 'abort': requests.get(message.id)?.destroy(); requests.delete(message.id); return
|
|
136
|
+
case 'wsopen': return onSocketOpen(message)
|
|
137
|
+
case 'wsmsg': {
|
|
138
|
+
const entry = peers.get(message.id)
|
|
139
|
+
if (!entry || typeof message.b !== 'string') return
|
|
140
|
+
const data = Buffer.from(message.b, 'base64')
|
|
141
|
+
const payload = message.bin ? data : data.toString('utf8')
|
|
142
|
+
if (entry.peer.readyState === WebSocket.OPEN) entry.peer.send(payload, { binary: !!message.bin })
|
|
143
|
+
else entry.queue.push([payload, !!message.bin])
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
case 'wsclose': { const entry = peers.get(message.id); if (entry) { peers.delete(message.id); entry.peer.close() } }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function connect() {
|
|
151
|
+
if (stopped) return
|
|
152
|
+
set('connecting')
|
|
153
|
+
socket = new WebSocket(socketUrl, { headers: { Authorization: `Bearer ${key}` } })
|
|
154
|
+
let refused = false
|
|
155
|
+
socket.on('open', () => { delay = 1000; set('up'); push() })
|
|
156
|
+
socket.on('message', onMessage)
|
|
157
|
+
socket.on('unexpected-response', (_request, response) => {
|
|
158
|
+
refused = true
|
|
159
|
+
response.resume()
|
|
160
|
+
const status = response.statusCode
|
|
161
|
+
set('refused', status === 401 ? 'The server did not accept the key' : status === 404 ? 'The server does not offer sharing: update mdeck there and start it with MDECK_SERVER_KEY' : status === 503 ? 'The server has too many shared presentations' : `The server answered ${status}`)
|
|
162
|
+
})
|
|
163
|
+
socket.on('error', error => { if (!refused) log(`tunnel: ${error.message}`) })
|
|
164
|
+
socket.on('close', () => {
|
|
165
|
+
for (const request of requests.values()) request.destroy()
|
|
166
|
+
requests.clear()
|
|
167
|
+
for (const { peer } of peers.values()) peer.terminate()
|
|
168
|
+
peers.clear()
|
|
169
|
+
socket = null
|
|
170
|
+
if (stopped || refused) return
|
|
171
|
+
set('down')
|
|
172
|
+
timer = setTimeout(connect, delay)
|
|
173
|
+
delay = Math.min(delay * 2, 15000)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
connect()
|
|
178
|
+
return {
|
|
179
|
+
url: `${root}/t/${id}/`,
|
|
180
|
+
state: () => state,
|
|
181
|
+
push,
|
|
182
|
+
stop() { stopped = true; clearTimeout(timer); socket?.close(1000); socket?.terminate() },
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Parse the entire command before it can start a server or change any files.
|
|
2
|
+
const value = 'value', flag = 'flag', optional = 'optional'
|
|
3
|
+
const common = { '--port': value, '--no-open': flag }
|
|
4
|
+
const output = { '--output': value, '-o': '--output', '--no-drawings': flag }
|
|
5
|
+
const commands = {
|
|
6
|
+
new: { max: 0 },
|
|
7
|
+
run: { ...common, '--network': flag, '--server': optional, max: 1 },
|
|
8
|
+
edit: { ...common, max: 1 },
|
|
9
|
+
design: { ...common, max: 1 },
|
|
10
|
+
themes: { '--global': flag, '--force': flag, '--check': flag, '--output': value, '-o': '--output', max: 4 },
|
|
11
|
+
build: { ...output, '--single-file': flag, '--launchers': flag, '--reader': flag, '--notes': flag, max: 1 },
|
|
12
|
+
send: { ...output, '--notes': flag, '--no-pdf': flag, max: 1 },
|
|
13
|
+
pdf: { ...output, max: 1 },
|
|
14
|
+
preview: { ...common, max: 1 },
|
|
15
|
+
check: { '--strict': flag, max: 1 },
|
|
16
|
+
list: { '--json': flag, max: 2 },
|
|
17
|
+
starter: { max: 2 },
|
|
18
|
+
docs: { ...common, '--build': flag, max: 1 },
|
|
19
|
+
server: { '--port': value, '--host': optional, max: 0 },
|
|
20
|
+
skill: { '--print': flag, '--install': 'list', '--project': flag, max: 0 },
|
|
21
|
+
migrate: { '--dry-run': flag, max: 1 },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateServerOrigin(address) {
|
|
25
|
+
let url
|
|
26
|
+
try { url = new URL(address) } catch {}
|
|
27
|
+
if (!url || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw new Error('--server needs an http:// or https:// server origin, for example https://slides.example.org')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseArgs(command, argv) {
|
|
31
|
+
const schema = commands[command]
|
|
32
|
+
if (!schema) throw new Error(`Unknown command: "${command}"`)
|
|
33
|
+
const options = {}, positionals = []
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const arg = argv[i]
|
|
36
|
+
if (arg === '--') { positionals.push(...argv.slice(i + 1)); break }
|
|
37
|
+
if (!arg.startsWith('-')) { positionals.push(arg); continue }
|
|
38
|
+
const at = arg.indexOf('=')
|
|
39
|
+
let name = at < 0 ? arg : arg.slice(0, at)
|
|
40
|
+
const inline = at < 0 ? undefined : arg.slice(at + 1)
|
|
41
|
+
let type = schema[name]
|
|
42
|
+
if (type?.startsWith('--')) { name = type; type = schema[name] }
|
|
43
|
+
if (!type) throw new Error(`Unknown option for mdeck ${command}: ${name}`)
|
|
44
|
+
if (Object.hasOwn(options, name)) throw new Error(`Option ${name} was given more than once`)
|
|
45
|
+
if (type === flag) {
|
|
46
|
+
if (inline !== undefined) throw new Error(`${name} does not take a value`)
|
|
47
|
+
options[name] = true
|
|
48
|
+
} else if (type === 'list') {
|
|
49
|
+
const items = inline === undefined ? [] : [inline]
|
|
50
|
+
while (argv[i + 1] && !argv[i + 1].startsWith('-')) items.push(argv[++i])
|
|
51
|
+
if (!items.length || items.some(item => !item)) throw new Error(`${name} needs at least one assistant`)
|
|
52
|
+
options[name] = items
|
|
53
|
+
} else {
|
|
54
|
+
let given = inline
|
|
55
|
+
const next = argv[i + 1]
|
|
56
|
+
if (given === undefined && next && !next.startsWith('-') && (type === value || name === '--host' || !/\.md$/i.test(next))) given = argv[++i]
|
|
57
|
+
if (given === undefined && type === optional) given = null
|
|
58
|
+
else if (!given) throw new Error(`${name} needs a value`)
|
|
59
|
+
options[name] = given
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (positionals.length > schema.max) throw new Error(`Too many arguments for mdeck ${command}: ${positionals.slice(schema.max).join(' ')}`)
|
|
63
|
+
if (options['--port'] !== undefined) {
|
|
64
|
+
const port = Number(options['--port'])
|
|
65
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Choose a port number between 1 and 65535.')
|
|
66
|
+
options['--port'] = port
|
|
67
|
+
}
|
|
68
|
+
if (typeof options['--server'] === 'string') {
|
|
69
|
+
validateServerOrigin(options['--server'])
|
|
70
|
+
}
|
|
71
|
+
if (command === 'build' && options['--notes'] && !options['--reader']) throw new Error('--notes requires --reader for mdeck build')
|
|
72
|
+
if (command === 'build' && options['--launchers'] && options['--single-file']) throw new Error('--launchers is only available for folders, not with --single-file.')
|
|
73
|
+
if (command === 'list' && positionals.length === 2 && !['layouts', 'themes', 'palettes'].includes(positionals[0])) throw new Error('Use mdeck list [layouts|themes|palettes] [slides.md]')
|
|
74
|
+
if (command === 'skill' && options['--print'] && options['--install']) throw new Error('Choose --print or --install')
|
|
75
|
+
return { options, positionals }
|
|
76
|
+
}
|