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.
Files changed (268) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/LICENSE +21 -0
  3. package/README.md +156 -0
  4. package/assets/base.css +196 -0
  5. package/assets/extensions/layouts/chapter/extension.toml +26 -0
  6. package/assets/extensions/layouts/chapter/layout.jsx +21 -0
  7. package/assets/extensions/layouts/chapter/starter.md +8 -0
  8. package/assets/extensions/layouts/focus/extension.toml +8 -0
  9. package/assets/extensions/layouts/focus/layout.jsx +10 -0
  10. package/assets/extensions/layouts/focus/starter.md +4 -0
  11. package/assets/extensions/layouts/full-bleed-image/extension.toml +31 -0
  12. package/assets/extensions/layouts/full-bleed-image/layout.jsx +19 -0
  13. package/assets/extensions/layouts/full-bleed-image/starter.md +6 -0
  14. package/assets/extensions/layouts/generic/extension.toml +8 -0
  15. package/assets/extensions/layouts/generic/layout.jsx +8 -0
  16. package/assets/extensions/layouts/generic/starter.md +4 -0
  17. package/assets/extensions/layouts/image-text/extension.toml +25 -0
  18. package/assets/extensions/layouts/image-text/layout.jsx +17 -0
  19. package/assets/extensions/layouts/image-text/starter.md +6 -0
  20. package/assets/extensions/layouts/split/extension.toml +25 -0
  21. package/assets/extensions/layouts/split/layout.jsx +19 -0
  22. package/assets/extensions/layouts/split/starter.md +14 -0
  23. package/assets/extensions/layouts/title/extension.toml +26 -0
  24. package/assets/extensions/layouts/title/layout.jsx +14 -0
  25. package/assets/extensions/layouts/title/starter.md +5 -0
  26. package/assets/extensions/palettes/brand/extension.toml +29 -0
  27. package/assets/extensions/palettes/cobalt/extension.toml +27 -0
  28. package/assets/extensions/palettes/ember/extension.toml +27 -0
  29. package/assets/extensions/palettes/forest/extension.toml +27 -0
  30. package/assets/extensions/palettes/graphite/extension.toml +27 -0
  31. package/assets/extensions/palettes/lagoon/extension.toml +27 -0
  32. package/assets/extensions/palettes/neon/extension.toml +27 -0
  33. package/assets/extensions/palettes/nordic/extension.toml +27 -0
  34. package/assets/extensions/palettes/pastel/extension.toml +30 -0
  35. package/assets/extensions/palettes/phosphor/extension.toml +27 -0
  36. package/assets/extensions/palettes/swiss/extension.toml +27 -0
  37. package/assets/extensions/palettes/terra/extension.toml +27 -0
  38. package/assets/extensions/themes/academic/extension.toml +53 -0
  39. package/assets/extensions/themes/academic/styles.css +631 -0
  40. package/assets/extensions/themes/aurora/extension.toml +37 -0
  41. package/assets/extensions/themes/aurora/styles.css +644 -0
  42. package/assets/extensions/themes/duet/extension.toml +46 -0
  43. package/assets/extensions/themes/duet/styles.css +573 -0
  44. package/assets/extensions/themes/editorial/extension.toml +46 -0
  45. package/assets/extensions/themes/editorial/styles.css +574 -0
  46. package/assets/extensions/themes/fhnw/extension.toml +61 -0
  47. package/assets/extensions/themes/fhnw/styles.css +604 -0
  48. package/assets/extensions/themes/neue/extension.toml +46 -0
  49. package/assets/extensions/themes/neue/styles.css +489 -0
  50. package/assets/extensions/themes/sketch/extension.toml +48 -0
  51. package/assets/extensions/themes/sketch/styles.css +633 -0
  52. package/assets/extensions/themes/terminal/extension.toml +38 -0
  53. package/assets/extensions/themes/terminal/styles.css +550 -0
  54. package/assets/logo/avatar.png +0 -0
  55. package/assets/logo/logo-dark.svg +10 -0
  56. package/assets/logo/logo-light.svg +10 -0
  57. package/assets/logo/mark.svg +15 -0
  58. package/bin/mdeck.js +2 -0
  59. package/docs/README.md +8 -0
  60. package/docs/reference/authoring.md +795 -0
  61. package/docs/reference/claude-skill.md +96 -0
  62. package/docs/reference/extensions.md +250 -0
  63. package/docs/reference/layouts.md +153 -0
  64. package/docs/reference/palettes.md +106 -0
  65. package/docs/reference/structured-slides.md +120 -0
  66. package/docs/reference/themes.md +133 -0
  67. package/docs/site/assets/favicon.svg +15 -0
  68. package/docs/site/assets/github-logo.svg +1 -0
  69. package/docs/site/assets/site.css +162 -0
  70. package/docs/site/assets/site.js +123 -0
  71. package/docs/site/build.js +148 -0
  72. package/docs/site/content/appearance.md +203 -0
  73. package/docs/site/content/audience.md +178 -0
  74. package/docs/site/content/commands.md +153 -0
  75. package/docs/site/content/components.md +134 -0
  76. package/docs/site/content/drawing.md +72 -0
  77. package/docs/site/content/editing.md +59 -0
  78. package/docs/site/content/getting-started.md +103 -0
  79. package/docs/site/content/layouts.md +126 -0
  80. package/docs/site/content/media.md +107 -0
  81. package/docs/site/content/more-content.md +130 -0
  82. package/docs/site/content/presenting.md +93 -0
  83. package/docs/site/content/reusable-layouts.md +74 -0
  84. package/docs/site/content/sharing.md +88 -0
  85. package/docs/site/content/theme-authoring.md +144 -0
  86. package/docs/site/content/theme-repository.md +76 -0
  87. package/docs/site/content/troubleshooting.md +92 -0
  88. package/docs/site/content/views.md +56 -0
  89. package/docs/site/content/welcome.md +71 -0
  90. package/docs/site/content/writing.md +121 -0
  91. package/docs/site/examples/first-talk.md +88 -0
  92. package/docs/site/examples/notebook.svg +58 -0
  93. package/docs/site/images/themes/academic-dark.webp +0 -0
  94. package/docs/site/images/themes/academic-light.webp +0 -0
  95. package/docs/site/images/themes/academic.webp +0 -0
  96. package/docs/site/images/themes/aurora-dark.webp +0 -0
  97. package/docs/site/images/themes/aurora-light.webp +0 -0
  98. package/docs/site/images/themes/aurora.webp +0 -0
  99. package/docs/site/images/themes/duet-dark.webp +0 -0
  100. package/docs/site/images/themes/duet-light.webp +0 -0
  101. package/docs/site/images/themes/duet.webp +0 -0
  102. package/docs/site/images/themes/editorial-dark.webp +0 -0
  103. package/docs/site/images/themes/editorial-light.webp +0 -0
  104. package/docs/site/images/themes/editorial.webp +0 -0
  105. package/docs/site/images/themes/fhnw.webp +0 -0
  106. package/docs/site/images/themes/neue-dark.webp +0 -0
  107. package/docs/site/images/themes/neue-light.webp +0 -0
  108. package/docs/site/images/themes/neue.webp +0 -0
  109. package/docs/site/images/themes/sketch-dark.webp +0 -0
  110. package/docs/site/images/themes/sketch-light.webp +0 -0
  111. package/docs/site/images/themes/sketch.webp +0 -0
  112. package/docs/site/images/themes/terminal-dark.webp +0 -0
  113. package/docs/site/images/themes/terminal-light.webp +0 -0
  114. package/docs/site/images/themes/terminal.webp +0 -0
  115. package/docs/site/package.json +9 -0
  116. package/docs/site/pages.js +27 -0
  117. package/docs/site/server.js +58 -0
  118. package/examples/custom-layouts/extensions/comparison/extension.toml +35 -0
  119. package/examples/custom-layouts/extensions/comparison/layout.jsx +15 -0
  120. package/examples/custom-layouts/extensions/comparison/starter.md +17 -0
  121. package/examples/custom-layouts/extensions/comparison/styles.css +4 -0
  122. package/examples/custom-layouts/slides.md +60 -0
  123. package/examples/poll/slides.md +127 -0
  124. package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +19 -0
  125. package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +6 -0
  126. package/examples/python/slides.drawings.json +6 -0
  127. package/examples/python/slides.md +493 -0
  128. package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +10 -0
  129. package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +13 -0
  130. package/examples/showcase/img/image.jpg +0 -0
  131. package/examples/showcase/img/mdeck.svg +66 -0
  132. package/examples/showcase/slides.drawings.json +11 -0
  133. package/examples/showcase/slides.md +427 -0
  134. package/package.json +75 -0
  135. package/skills/write-slides/SKILL.md +144 -0
  136. package/src/build/check.js +49 -0
  137. package/src/build/chrome.js +130 -0
  138. package/src/build/components.js +44 -0
  139. package/src/build/config.js +61 -0
  140. package/src/build/drawings.js +14 -0
  141. package/src/build/editorPlugin.js +272 -0
  142. package/src/build/editorServer.js +36 -0
  143. package/src/build/fonts.js +84 -0
  144. package/src/build/homePlugin.js +208 -0
  145. package/src/build/inkPlugin.js +192 -0
  146. package/src/build/ownWrites.js +18 -0
  147. package/src/build/pairing.js +81 -0
  148. package/src/build/pdf.js +34 -0
  149. package/src/build/relayAccess.js +85 -0
  150. package/src/build/slidesPlugin.js +371 -0
  151. package/src/build/themeCheck.js +151 -0
  152. package/src/build/tunnelClient.js +184 -0
  153. package/src/cli/args.js +76 -0
  154. package/src/cli/index.js +758 -0
  155. package/src/cli/migrate.js +123 -0
  156. package/src/cli/skill.js +40 -0
  157. package/src/cli/themes.js +140 -0
  158. package/src/components/CodeBlock.jsx +168 -0
  159. package/src/components/ConnectedViews.jsx +25 -0
  160. package/src/components/Icon.jsx +8 -0
  161. package/src/components/Poll.jsx +67 -0
  162. package/src/components/QrCode.jsx +29 -0
  163. package/src/components/QrCodeTag.jsx +27 -0
  164. package/src/components/Question.jsx +28 -0
  165. package/src/components/Scale.jsx +47 -0
  166. package/src/components/VideoPlayer.jsx +108 -0
  167. package/src/components/WordCloud.jsx +106 -0
  168. package/src/components/activity.jsx +35 -0
  169. package/src/components/callout.css +78 -0
  170. package/src/components/code-block.css +123 -0
  171. package/src/components/columns.css +11 -0
  172. package/src/components/poll.css +81 -0
  173. package/src/components/video-player.css +14 -0
  174. package/src/core/devPath.js +4 -0
  175. package/src/core/editDeck.js +230 -0
  176. package/src/core/icons.js +69 -0
  177. package/src/core/ink.js +221 -0
  178. package/src/core/labels.js +144 -0
  179. package/src/core/outline.js +33 -0
  180. package/src/core/parseSlides.js +152 -0
  181. package/src/core/source.js +137 -0
  182. package/src/core/urls.js +34 -0
  183. package/src/core/validateDeck.js +139 -0
  184. package/src/editor/api.js +67 -0
  185. package/src/editor/components/App.jsx +75 -0
  186. package/src/editor/components/ConflictBanner.jsx +10 -0
  187. package/src/editor/components/DeckSettings.jsx +108 -0
  188. package/src/editor/components/DesignApp.jsx +252 -0
  189. package/src/editor/components/Diagnostics.jsx +10 -0
  190. package/src/editor/components/ExtensionEditor.jsx +66 -0
  191. package/src/editor/components/ExtensionForms.jsx +134 -0
  192. package/src/editor/components/ExtensionsPanel.jsx +114 -0
  193. package/src/editor/components/Field.jsx +24 -0
  194. package/src/editor/components/Inspector.jsx +36 -0
  195. package/src/editor/components/LayoutPicker.jsx +23 -0
  196. package/src/editor/components/LayoutSelect.jsx +25 -0
  197. package/src/editor/components/LookThumbnail.jsx +8 -0
  198. package/src/editor/components/MetaFields.jsx +24 -0
  199. package/src/editor/components/Outline.jsx +28 -0
  200. package/src/editor/components/PalettePicker.jsx +32 -0
  201. package/src/editor/components/Preview.jsx +63 -0
  202. package/src/editor/components/PropsForm.jsx +46 -0
  203. package/src/editor/components/RawSlideEditor.jsx +9 -0
  204. package/src/editor/components/RegionEditor.jsx +15 -0
  205. package/src/editor/components/Resizer.jsx +18 -0
  206. package/src/editor/designLink.js +19 -0
  207. package/src/editor/editor.css +139 -0
  208. package/src/editor/extensions.js +86 -0
  209. package/src/editor/keyboard.js +21 -0
  210. package/src/editor/model.js +38 -0
  211. package/src/editor/sampleDeck.js +165 -0
  212. package/src/editor/saveQueue.js +47 -0
  213. package/src/editor/schema.js +36 -0
  214. package/src/editor/state.js +92 -0
  215. package/src/editor/useDeckDocument.js +67 -0
  216. package/src/extensions/appearance.js +79 -0
  217. package/src/extensions/contrast.js +57 -0
  218. package/src/extensions/discover.js +126 -0
  219. package/src/extensions/manifest.js +209 -0
  220. package/src/extensions/packs.js +268 -0
  221. package/src/extensions/tokens.js +25 -0
  222. package/src/home/Home.jsx +362 -0
  223. package/src/home/home.css +148 -0
  224. package/src/layouts/layoutApi.jsx +205 -0
  225. package/src/layouts/layoutProps.js +30 -0
  226. package/src/layouts/renderSlide.jsx +17 -0
  227. package/src/live/answer/answer.css +34 -0
  228. package/src/live/answer/answer.html +14 -0
  229. package/src/live/answer/answer.js +130 -0
  230. package/src/live/client.js +290 -0
  231. package/src/live/code.js +19 -0
  232. package/src/live/follow.js +17 -0
  233. package/src/live/pairing.js +34 -0
  234. package/src/live/presence.js +33 -0
  235. package/src/live/roomTag.js +38 -0
  236. package/src/live/rooms.js +249 -0
  237. package/src/live/server.js +82 -0
  238. package/src/live/tunnel.js +180 -0
  239. package/src/paths.js +9 -0
  240. package/src/runtime/ReaderView.jsx +193 -0
  241. package/src/runtime/SlideErrorBoundary.jsx +28 -0
  242. package/src/runtime/chrome.jsx +55 -0
  243. package/src/runtime/deck-stage.js +1533 -0
  244. package/src/runtime/design.html +12 -0
  245. package/src/runtime/design.jsx +5 -0
  246. package/src/runtime/editor.html +12 -0
  247. package/src/runtime/editor.jsx +5 -0
  248. package/src/runtime/editorBridge.js +58 -0
  249. package/src/runtime/fit.js +149 -0
  250. package/src/runtime/home.html +13 -0
  251. package/src/runtime/home.jsx +5 -0
  252. package/src/runtime/index.html +15 -0
  253. package/src/runtime/ink/InkLayer.jsx +17 -0
  254. package/src/runtime/ink/Toolbar.jsx +145 -0
  255. package/src/runtime/ink/attach.js +69 -0
  256. package/src/runtime/ink/bus.js +126 -0
  257. package/src/runtime/ink/controller.js +132 -0
  258. package/src/runtime/ink/room.js +101 -0
  259. package/src/runtime/ink/select.js +124 -0
  260. package/src/runtime/ink/server.js +75 -0
  261. package/src/runtime/ink/store.js +55 -0
  262. package/src/runtime/ink/toolbar.css +79 -0
  263. package/src/runtime/main.jsx +863 -0
  264. package/src/runtime/markedSetup.js +147 -0
  265. package/src/runtime/reader.css +59 -0
  266. package/src/runtime/registry.jsx +26 -0
  267. package/src/runtime/syncOrder.js +32 -0
  268. 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
+ }
@@ -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
+ }