mdeck 2.3.0 → 3.1.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 +22 -1
- package/README.md +9 -9
- package/assets/base.css +4 -1
- package/assets/extensions/palettes/lagoon/extension.toml +3 -0
- package/assets/extensions/palettes/neon/extension.toml +3 -0
- package/assets/extensions/palettes/nordic/extension.toml +3 -0
- package/assets/extensions/palettes/paper/extension.toml +30 -0
- package/assets/extensions/palettes/swiss/extension.toml +3 -0
- package/assets/extensions/themes/academic/extension.toml +3 -0
- package/assets/extensions/themes/aurora/extension.toml +3 -0
- package/assets/extensions/themes/minimal/extension.toml +48 -0
- package/assets/extensions/themes/minimal/styles.css +342 -0
- package/assets/extensions/themes/neue/extension.toml +3 -0
- package/docs/reference/authoring.md +13 -18
- package/docs/reference/claude-skill.md +1 -1
- package/docs/reference/extensions.md +40 -26
- package/docs/reference/palettes.md +8 -12
- package/docs/reference/themes.md +8 -7
- package/docs/site/content/appearance.md +35 -49
- package/docs/site/content/commands.md +3 -2
- package/docs/site/content/theme-authoring.md +1 -1
- package/docs/site/content/theme-repository.md +44 -32
- package/docs/site/examples/first-talk.md +1 -1
- 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/minimal-dark.webp +0 -0
- package/docs/site/images/themes/minimal-light.webp +0 -0
- package/docs/site/images/themes/minimal.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/pages.js +1 -1
- package/examples/custom-layouts/slides.md +1 -1
- package/examples/poll/slides.md +1 -1
- package/examples/python/slides.md +2 -2
- package/examples/showcase/slides.md +3 -3
- package/package.json +2 -2
- package/skills/write-slides/SKILL.md +2 -2
- package/src/build/check.js +0 -7
- package/src/build/editorPlugin.js +26 -13
- package/src/build/themeCheck.js +17 -6
- package/src/cli/args.js +2 -1
- package/src/cli/index.js +7 -6
- package/src/cli/themes.js +117 -87
- package/src/core/validateDeck.js +4 -2
- package/src/editor/api.js +15 -6
- package/src/editor/components/App.jsx +4 -2
- package/src/editor/components/DesignApp.jsx +16 -9
- package/src/editor/components/ExtensionEditor.jsx +16 -6
- package/src/editor/components/ExtensionsPanel.jsx +28 -34
- package/src/editor/designLink.js +18 -1
- package/src/editor/sampleDeck.js +3 -1
- package/src/extensions/appearance.js +2 -1
- package/src/extensions/discover.js +44 -11
- package/src/extensions/installed.js +88 -0
- package/src/extensions/manifest.js +20 -3
- package/src/extensions/moved.js +20 -0
- package/src/extensions/packages.js +318 -0
- package/src/home/Home.jsx +12 -6
- package/src/layouts/layoutApi.jsx +10 -2
- package/src/runtime/themeLoader.js +5 -2
- package/assets/extensions/palettes/brand/extension.toml +0 -29
- package/assets/extensions/palettes/cobalt/extension.toml +0 -27
- package/assets/extensions/palettes/ember/extension.toml +0 -27
- package/assets/extensions/palettes/forest/extension.toml +0 -27
- package/assets/extensions/palettes/graphite/extension.toml +0 -27
- package/assets/extensions/palettes/pastel/extension.toml +0 -30
- package/assets/extensions/palettes/phosphor/extension.toml +0 -27
- package/assets/extensions/palettes/terra/extension.toml +0 -27
- package/assets/extensions/themes/duet/extension.toml +0 -46
- package/assets/extensions/themes/duet/styles.css +0 -573
- package/assets/extensions/themes/editorial/extension.toml +0 -46
- package/assets/extensions/themes/editorial/styles.css +0 -574
- package/assets/extensions/themes/fhnw/extension.toml +0 -61
- package/assets/extensions/themes/fhnw/styles.css +0 -604
- package/assets/extensions/themes/sketch/extension.toml +0 -48
- package/assets/extensions/themes/sketch/styles.css +0 -633
- package/assets/extensions/themes/terminal/extension.toml +0 -38
- package/assets/extensions/themes/terminal/styles.css +0 -550
- 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/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/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +0 -19
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +0 -6
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +0 -10
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +0 -13
- package/src/extensions/packs.js +0 -268
package/src/extensions/packs.js
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
// Packs: themes and palettes shared through the theme repository
|
|
2
|
-
// (https://gh.tschieber.de/mdeck-themes/). A pack is a folder with pack.toml
|
|
3
|
-
// and one folder per theme or palette; it travels as one JSON file of text
|
|
4
|
-
// files, so installing it is checking it and writing those files. Only
|
|
5
|
-
// extension.toml and styles.css are allowed: a pack never carries code.
|
|
6
|
-
import { createHash } from 'node:crypto'
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { resolve, relative } from 'node:path'
|
|
9
|
-
import { parse } from 'smol-toml'
|
|
10
|
-
import { ID_RE, ManifestError, parseManifestText, validateManifest } from './manifest.js'
|
|
11
|
-
|
|
12
|
-
export const INDEX_URL = 'https://gh.tschieber.de/mdeck-themes/index.json'
|
|
13
|
-
export const MARKER = '.mdeck-pack.json'
|
|
14
|
-
export const PACK_FILE = 'pack.toml'
|
|
15
|
-
const FILES = ['extension.toml', 'styles.css']
|
|
16
|
-
const FILE_RE = /^([a-z][a-z0-9-]*)\/(extension\.toml|styles\.css)$/
|
|
17
|
-
const VERSION_RE = /^\d+\.\d+\.\d+$/
|
|
18
|
-
const RANGE_RE = /^>=\s*(\d+\.\d+\.\d+)$/
|
|
19
|
-
// Where theme fonts may come from: stylesheet services that only serve fonts.
|
|
20
|
-
export const FONT_HOSTS = ['https://fonts.googleapis.com/', 'https://fonts.bunny.net/']
|
|
21
|
-
|
|
22
|
-
export class PackError extends Error {}
|
|
23
|
-
const fail = message => { throw new PackError(message) }
|
|
24
|
-
|
|
25
|
-
export const sha256 = text => createHash('sha256').update(text, 'utf8').digest('hex')
|
|
26
|
-
|
|
27
|
-
export function indexUrl() {
|
|
28
|
-
return process.env.MDECK_THEMES_URL || INDEX_URL
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const parts = version => version.split('.').map(Number)
|
|
32
|
-
export function compareVersions(a, b) {
|
|
33
|
-
const [x, y] = [parts(a), parts(b)]
|
|
34
|
-
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]
|
|
35
|
-
return 0
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Ranges are only of the form ">=1.2.3"; no range means any version.
|
|
39
|
-
export function satisfies(range, version) {
|
|
40
|
-
if (!range) return true
|
|
41
|
-
const match = RANGE_RE.exec(range)
|
|
42
|
-
return Boolean(match) && compareVersions(version, match[1]) >= 0
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function checkMeta(meta, where) {
|
|
46
|
-
if (meta.schema !== 1) fail(`${where}: schema must be 1`)
|
|
47
|
-
if (typeof meta.id !== 'string' || !ID_RE.test(meta.id)) fail(`${where}: id must use lowercase letters, digits and hyphens, starting with a letter`)
|
|
48
|
-
if (typeof meta.version !== 'string' || !VERSION_RE.test(meta.version)) fail(`${where}: version must look like 1.0.0`)
|
|
49
|
-
if (meta.mdeck != null && (typeof meta.mdeck !== 'string' || !RANGE_RE.test(meta.mdeck))) fail(`${where}: mdeck must look like ">=2.3.0"`)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// pack.toml and the files of each theme or palette in a pack folder.
|
|
53
|
-
export function readPackDir(dir) {
|
|
54
|
-
const file = resolve(dir, PACK_FILE)
|
|
55
|
-
if (!existsSync(file)) fail(`${dir} has no ${PACK_FILE}`)
|
|
56
|
-
let meta
|
|
57
|
-
try { meta = parse(readFileSync(file, 'utf8')) } catch (error) { fail(`${file}: ${error.message.split('\n')[0]}`) }
|
|
58
|
-
checkMeta(meta, file)
|
|
59
|
-
for (const key of ['title', 'author', 'license']) if (typeof meta[key] !== 'string' || !meta[key].trim()) fail(`${file}: ${key} is required`)
|
|
60
|
-
const folder = dir.split(/[\\/]/).filter(Boolean).at(-1)
|
|
61
|
-
if (meta.id !== folder) fail(`${file}: id "${meta.id}" must be the folder name "${folder}"`)
|
|
62
|
-
const files = {}
|
|
63
|
-
for (const entry of readdirSync(dir).sort()) {
|
|
64
|
-
if (entry === PACK_FILE || entry.startsWith('.') || entry === 'README.md') continue
|
|
65
|
-
const sub = resolve(dir, entry)
|
|
66
|
-
if (!statSync(sub).isDirectory()) fail(`${sub}: a pack holds only ${PACK_FILE}, README.md and one folder per theme or palette`)
|
|
67
|
-
for (const name of readdirSync(sub).sort()) {
|
|
68
|
-
if (!FILES.includes(name)) fail(`${resolve(sub, name)}: a theme or palette in a pack holds only ${FILES.join(' and ')}`)
|
|
69
|
-
files[`${entry}/${name}`] = readFileSync(resolve(sub, name), 'utf8')
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return { meta, files }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// The published form of a pack folder: one JSON text and its checksum.
|
|
76
|
-
export function bundlePack(dir) {
|
|
77
|
-
const { meta, files } = readPackDir(dir)
|
|
78
|
-
const bundle = { schema: 1, id: meta.id, version: meta.version, ...(meta.mdeck ? { mdeck: meta.mdeck } : {}), files }
|
|
79
|
-
const text = JSON.stringify(bundle, null, 1) + '\n'
|
|
80
|
-
return { meta, bundle, text, sha256: sha256(text) }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function checkCss(text, where) {
|
|
84
|
-
if (/@import/i.test(text)) fail(`${where}: @import is not allowed; list font stylesheets under fonts in extension.toml`)
|
|
85
|
-
for (const match of text.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi)) {
|
|
86
|
-
if (!match[2].trim().startsWith('data:')) fail(`${where}: url(${match[2]}) is not allowed; only data: URLs`)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Checks a pack before it is published or installed. `known` is the registry
|
|
91
|
-
// it will join (built-in extensions at least), for palettes a theme names.
|
|
92
|
-
// Returns the ids it holds, as { themes, palettes }.
|
|
93
|
-
export function checkPack(bundle, { known = { themes: {}, palettes: {} } } = {}) {
|
|
94
|
-
if (!bundle || typeof bundle !== 'object' || typeof bundle.files !== 'object' || !bundle.files) fail('Not a pack')
|
|
95
|
-
checkMeta(bundle, `Pack ${bundle.id ?? '?'}`)
|
|
96
|
-
const byId = {}
|
|
97
|
-
for (const [path, text] of Object.entries(bundle.files)) {
|
|
98
|
-
const match = FILE_RE.exec(path)
|
|
99
|
-
if (!match) fail(`${bundle.id}: "${path}" is not allowed; a pack holds only <id>/extension.toml and <id>/styles.css`)
|
|
100
|
-
if (typeof text !== 'string') fail(`${bundle.id}: ${path} is not text`)
|
|
101
|
-
;(byId[match[1]] ??= {})[match[2]] = text
|
|
102
|
-
}
|
|
103
|
-
if (!Object.keys(byId).length) fail(`${bundle.id}: the pack is empty`)
|
|
104
|
-
const records = []
|
|
105
|
-
for (const [id, files] of Object.entries(byId)) {
|
|
106
|
-
if (!files['extension.toml']) fail(`${bundle.id}: ${id}/extension.toml is missing`)
|
|
107
|
-
const dir = `/pack/${id}`
|
|
108
|
-
const file = `${dir}/extension.toml`
|
|
109
|
-
let record
|
|
110
|
-
try {
|
|
111
|
-
record = validateManifest(parseManifestText(files['extension.toml'], file), { file, dir, folderName: id, fileExists: path => Object.hasOwn(files, relative(dir, path)) })
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (error instanceof ManifestError) fail(`${bundle.id}: ${id}/extension.toml: ${error.message}`)
|
|
114
|
-
throw error
|
|
115
|
-
}
|
|
116
|
-
if (!['theme', 'palette'].includes(record.kind)) fail(`${bundle.id}: ${id} is a ${record.kind}; packs hold only themes and palettes`)
|
|
117
|
-
if (files['styles.css'] !== undefined && record.kind !== 'theme') fail(`${bundle.id}: ${id}/styles.css belongs only to a theme`)
|
|
118
|
-
if (record.kind === 'theme') {
|
|
119
|
-
for (const url of record.manifest.fonts) if (!FONT_HOSTS.some(host => url.startsWith(host))) fail(`${bundle.id}: ${id} loads fonts from ${url}; allowed are ${FONT_HOSTS.join(', ')}`)
|
|
120
|
-
checkCss(files['styles.css'] ?? '', `${bundle.id}: ${id}/styles.css`)
|
|
121
|
-
}
|
|
122
|
-
if (known.themes[id] || known.palettes[id]) fail(`${bundle.id}: ${id} is the id of a built-in ${known.themes[id] ? 'theme' : 'palette'}`)
|
|
123
|
-
records.push(record)
|
|
124
|
-
}
|
|
125
|
-
const themes = records.filter(r => r.kind === 'theme').map(r => r.id)
|
|
126
|
-
const palettes = records.filter(r => r.kind === 'palette').map(r => r.id)
|
|
127
|
-
const hasPalette = id => palettes.includes(id) || Boolean(known.palettes[id])
|
|
128
|
-
const hasTheme = id => themes.includes(id) || Boolean(known.themes[id])
|
|
129
|
-
for (const record of records) {
|
|
130
|
-
if (record.kind === 'theme') for (const id of [record.manifest.palette, ...(record.manifest.palettes ?? [])]) if (!hasPalette(id)) fail(`${bundle.id}: the theme ${record.id} names the palette "${id}", which is neither in the pack nor built in`)
|
|
131
|
-
if (record.kind === 'palette' && record.manifest.theme && !hasTheme(record.manifest.theme)) fail(`${bundle.id}: the palette ${record.id} belongs to the theme "${record.manifest.theme}", which is neither in the pack nor built in`)
|
|
132
|
-
}
|
|
133
|
-
return { themes, palettes }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function readMarker(dir) {
|
|
137
|
-
try { return JSON.parse(readFileSync(resolve(dir, MARKER), 'utf8')) } catch { return null }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Files changed since the pack was installed, by name.
|
|
141
|
-
function changedFiles(dir, marker) {
|
|
142
|
-
return Object.entries(marker.files ?? {}).filter(([name, hash]) => {
|
|
143
|
-
const file = resolve(dir, name)
|
|
144
|
-
return !existsSync(file) || sha256(readFileSync(file, 'utf8')) !== hash
|
|
145
|
-
}).map(([name]) => name)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// The packs installed in an extensions folder: { id: { version, url, folders } }.
|
|
149
|
-
export function installedPacks(root) {
|
|
150
|
-
const packs = {}
|
|
151
|
-
if (!existsSync(root)) return packs
|
|
152
|
-
for (const entry of readdirSync(root).sort()) {
|
|
153
|
-
const marker = readMarker(resolve(root, entry))
|
|
154
|
-
if (!marker?.pack) continue
|
|
155
|
-
const pack = packs[marker.pack] ??= { version: marker.version, url: marker.url, folders: [] }
|
|
156
|
-
pack.folders.push(entry)
|
|
157
|
-
}
|
|
158
|
-
return packs
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const refuseChanged = (root, folder, marker) => {
|
|
162
|
-
const changed = changedFiles(resolve(root, folder), marker)
|
|
163
|
-
if (changed.length) fail(`${resolve(root, folder)} was changed since it was installed (${changed.join(', ')}); add --force to replace or remove it anyway`)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Writes a checked pack into an extensions folder (a deck's, or the user's).
|
|
167
|
-
// A folder of another pack or of the deck itself is never touched; a folder
|
|
168
|
-
// of this pack is replaced unless it was changed by hand.
|
|
169
|
-
export function installPack(bundle, { root, known, url = null, digest = null, force = false } = {}) {
|
|
170
|
-
const { themes, palettes } = checkPack(bundle, { known })
|
|
171
|
-
const ids = [...themes, ...palettes]
|
|
172
|
-
const before = installedPacks(root)[bundle.id]
|
|
173
|
-
for (const id of ids) {
|
|
174
|
-
const dir = resolve(root, id)
|
|
175
|
-
if (!existsSync(dir)) continue
|
|
176
|
-
const marker = readMarker(dir)
|
|
177
|
-
if (marker?.pack !== bundle.id) fail(`${dir} already exists and is not from the pack ${bundle.id}; rename or remove it first`)
|
|
178
|
-
if (!force) refuseChanged(root, id, marker)
|
|
179
|
-
}
|
|
180
|
-
const gone = (before?.folders ?? []).filter(folder => !ids.includes(folder))
|
|
181
|
-
if (!force) for (const folder of gone) refuseChanged(root, folder, readMarker(resolve(root, folder)))
|
|
182
|
-
for (const folder of gone) rmSync(resolve(root, folder), { recursive: true, force: true })
|
|
183
|
-
for (const id of ids) {
|
|
184
|
-
// Files are overwritten in place, not deleted and written again, so a
|
|
185
|
-
// running editor sees a change instead of a file that disappears.
|
|
186
|
-
const dir = resolve(root, id)
|
|
187
|
-
mkdirSync(dir, { recursive: true })
|
|
188
|
-
const hashes = {}
|
|
189
|
-
for (const [path, content] of Object.entries(bundle.files)) {
|
|
190
|
-
if (!path.startsWith(`${id}/`)) continue
|
|
191
|
-
const name = path.slice(id.length + 1)
|
|
192
|
-
writeFileSync(resolve(dir, name), content, 'utf8')
|
|
193
|
-
hashes[name] = sha256(content)
|
|
194
|
-
}
|
|
195
|
-
for (const name of readdirSync(dir)) if (name !== MARKER && !Object.hasOwn(hashes, name)) rmSync(resolve(dir, name), { recursive: true, force: true })
|
|
196
|
-
writeFileSync(resolve(dir, MARKER), JSON.stringify({ pack: bundle.id, version: bundle.version, url, sha256: digest, files: hashes }, null, 2) + '\n')
|
|
197
|
-
}
|
|
198
|
-
return { themes, palettes, replaced: before?.version ?? null }
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function removePack(id, { root, force = false } = {}) {
|
|
202
|
-
const pack = installedPacks(root)[id]
|
|
203
|
-
if (!pack) fail(`The pack ${id} is not installed in ${root}`)
|
|
204
|
-
if (!force) for (const folder of pack.folders) refuseChanged(root, folder, readMarker(resolve(root, folder)))
|
|
205
|
-
for (const folder of pack.folders) rmSync(resolve(root, folder), { recursive: true, force: true })
|
|
206
|
-
return pack
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function get(url, as) {
|
|
210
|
-
let response
|
|
211
|
-
try { response = await fetch(url) } catch (error) { fail(`Could not reach ${url}: ${error.cause?.message ?? error.message}`) }
|
|
212
|
-
if (!response.ok) fail(`${url} answered ${response.status}`)
|
|
213
|
-
return as === 'json' ? response.json() : response.text()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export async function fetchIndex(url = indexUrl()) {
|
|
217
|
-
const index = await get(url, 'json')
|
|
218
|
-
if (index?.schema !== 1 || !Array.isArray(index.packs)) fail(`${url} is not a theme repository index`)
|
|
219
|
-
return { url, packs: index.packs }
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Downloads a pack listed in the index and checks that it is the one listed.
|
|
223
|
-
export async function fetchPack(index, entry) {
|
|
224
|
-
const url = new URL(entry.url, index.url).href
|
|
225
|
-
const text = await get(url, 'text')
|
|
226
|
-
if (sha256(text) !== entry.sha256) fail(`${url} does not match its checksum in the index; not installed`)
|
|
227
|
-
let bundle
|
|
228
|
-
try { bundle = JSON.parse(text) } catch { fail(`${url} is not a pack`) }
|
|
229
|
-
if (bundle.id !== entry.id || bundle.version !== entry.version) fail(`${url} holds ${bundle.id} ${bundle.version}, not ${entry.id} ${entry.version}`)
|
|
230
|
-
return { bundle, url, digest: entry.sha256 }
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Installs a pack listed in the index: refused when this mdeck is too old.
|
|
234
|
-
export async function installFromIndex(index, entry, { root, known, mdeckVersion, force = false }) {
|
|
235
|
-
if (!satisfies(entry.mdeck, mdeckVersion)) fail(`${entry.id} ${entry.version} needs mdeck ${entry.mdeck}; this is ${mdeckVersion}. Update mdeck first: npm install -g mdeck`)
|
|
236
|
-
const { bundle, url, digest } = await fetchPack(index, entry)
|
|
237
|
-
return installPack(bundle, { root, known, url, digest, force })
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function findPack(index, id) {
|
|
241
|
-
return index.packs.find(pack => pack.id === id) ?? fail(`There is no pack "${id}" in ${index.url}; mdeck themes search lists them`)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// The theme repository as served: index.json and packs/<id>-<version>.json,
|
|
245
|
-
// from a folder of pack folders. Each pack is checked on its own against the
|
|
246
|
-
// built-in extensions, since it is installed alone, and no two packs may
|
|
247
|
-
// share an id. Previews are added by the caller.
|
|
248
|
-
export function buildRepository(packsDir, outDir, { known }) {
|
|
249
|
-
const entries = []
|
|
250
|
-
const owner = {}
|
|
251
|
-
const dirs = readdirSync(packsDir).filter(name => !name.startsWith('.') && statSync(resolve(packsDir, name)).isDirectory()).sort()
|
|
252
|
-
mkdirSync(resolve(outDir, 'packs'), { recursive: true })
|
|
253
|
-
for (const name of dirs) {
|
|
254
|
-
const { meta, bundle, text, sha256: digest } = bundlePack(resolve(packsDir, name))
|
|
255
|
-
const { themes, palettes } = checkPack(bundle, { known })
|
|
256
|
-
for (const id of [...themes, ...palettes]) {
|
|
257
|
-
if (owner[id]) fail(`${id} is in both ${owner[id]} and ${meta.id}`)
|
|
258
|
-
owner[id] = meta.id
|
|
259
|
-
}
|
|
260
|
-
const file = `packs/${meta.id}-${meta.version}.json`
|
|
261
|
-
writeFileSync(resolve(outDir, file), text)
|
|
262
|
-
entries.push({ id: meta.id, title: meta.title, description: meta.description ?? '', version: meta.version, author: meta.author, license: meta.license,
|
|
263
|
-
...(meta.homepage ? { homepage: meta.homepage } : {}), ...(meta.mdeck ? { mdeck: meta.mdeck } : {}), themes, palettes, url: file, sha256: digest })
|
|
264
|
-
}
|
|
265
|
-
const index = { schema: 1, packs: entries }
|
|
266
|
-
writeFileSync(resolve(outDir, 'index.json'), JSON.stringify(index, null, 2) + '\n')
|
|
267
|
-
return index
|
|
268
|
-
}
|