mdeck 3.0.0 → 3.2.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 +17 -1
- package/assets/base.css +24 -0
- package/assets/{packs/lagoon → extensions/palettes}/lagoon/extension.toml +3 -0
- package/assets/{packs/aurora → extensions/palettes}/neon/extension.toml +3 -0
- package/assets/{packs/academic → extensions/palettes}/nordic/extension.toml +3 -0
- package/assets/{packs/minimal → extensions/palettes}/paper/extension.toml +3 -0
- package/assets/{packs/neue → extensions/palettes}/swiss/extension.toml +3 -0
- package/assets/{packs/academic → extensions/themes}/academic/extension.toml +3 -0
- package/assets/{packs/aurora → extensions/themes}/aurora/extension.toml +3 -0
- package/assets/{packs/minimal → extensions/themes}/minimal/extension.toml +3 -0
- package/assets/{packs/neue → extensions/themes}/neue/extension.toml +3 -0
- package/docs/reference/authoring.md +16 -3
- package/docs/reference/extensions.md +42 -35
- package/docs/reference/palettes.md +1 -1
- package/docs/reference/themes.md +3 -3
- package/docs/site/content/appearance.md +11 -9
- package/docs/site/content/commands.md +2 -1
- package/docs/site/content/theme-authoring.md +1 -1
- package/docs/site/content/theme-repository.md +36 -28
- package/package.json +2 -2
- package/skills/write-slides/SKILL.md +1 -1
- package/src/build/editorPlugin.js +22 -17
- package/src/build/themeCheck.js +14 -3
- package/src/cli/args.js +2 -1
- package/src/cli/index.js +7 -6
- package/src/cli/themes.js +99 -92
- package/src/editor/api.js +11 -10
- package/src/editor/components/DesignApp.jsx +9 -11
- package/src/editor/components/ExtensionEditor.jsx +10 -11
- package/src/editor/components/ExtensionsPanel.jsx +24 -28
- package/src/extensions/discover.js +28 -35
- package/src/extensions/installed.js +73 -48
- package/src/extensions/manifest.js +20 -3
- package/src/extensions/moved.js +14 -7
- package/src/extensions/packages.js +318 -0
- package/src/paths.js +0 -2
- package/src/runtime/markedSetup.js +7 -0
- package/assets/packs/academic/pack.toml +0 -8
- package/assets/packs/aurora/pack.toml +0 -8
- package/assets/packs/lagoon/pack.toml +0 -8
- package/assets/packs/minimal/pack.toml +0 -8
- package/assets/packs/neue/pack.toml +0 -8
- package/src/extensions/packs.js +0 -357
- /package/assets/{packs/academic → extensions/themes}/academic/styles.css +0 -0
- /package/assets/{packs/aurora → extensions/themes}/aurora/styles.css +0 -0
- /package/assets/{packs/minimal → extensions/themes}/minimal/styles.css +0 -0
- /package/assets/{packs/neue → extensions/themes}/neue/styles.css +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Packages: every theme and every palette can be installed on its own, from
|
|
2
|
+
// mdeck itself (the built-in ones) or from the theme repository
|
|
3
|
+
// (https://gh.tschieber.de/mdeck-themes/). A package is one extension folder;
|
|
4
|
+
// it travels as one JSON file of its text files, so installing it is checking
|
|
5
|
+
// it and writing those files. Only extension.toml and, for a theme,
|
|
6
|
+
// styles.css are allowed: a package never carries code.
|
|
7
|
+
// What a package needs follows from its manifest: a theme needs its default
|
|
8
|
+
// palette, and a palette that belongs to one theme needs that theme. Those
|
|
9
|
+
// are installed along with it.
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { resolve, relative, basename } from 'node:path'
|
|
13
|
+
import { ID_RE, ManifestError, RANGE_RE, parseManifestText, validateManifest } from './manifest.js'
|
|
14
|
+
import { builtinExtensionsRoot } from '../paths.js'
|
|
15
|
+
|
|
16
|
+
export const CATALOGUE_URL = 'https://gh.tschieber.de/mdeck-themes/catalogue.json'
|
|
17
|
+
export const MARKER = '.mdeck-package.json'
|
|
18
|
+
const PACKAGE_KINDS = ['theme', 'palette']
|
|
19
|
+
const FILES = { theme: ['extension.toml', 'styles.css'], palette: ['extension.toml'] }
|
|
20
|
+
// Where theme fonts may come from: stylesheet services that only serve fonts.
|
|
21
|
+
export const FONT_HOSTS = ['https://fonts.googleapis.com/', 'https://fonts.bunny.net/']
|
|
22
|
+
|
|
23
|
+
export class PackageError extends Error {}
|
|
24
|
+
const fail = message => { throw new PackageError(message) }
|
|
25
|
+
|
|
26
|
+
export const sha256 = text => createHash('sha256').update(text, 'utf8').digest('hex')
|
|
27
|
+
export const keyOf = (kind, id) => `${kind}:${id}`
|
|
28
|
+
|
|
29
|
+
export function catalogueUrl() {
|
|
30
|
+
return process.env.MDECK_THEMES_URL || CATALOGUE_URL
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parts = version => version.split('.').map(Number)
|
|
34
|
+
export function compareVersions(a, b) {
|
|
35
|
+
const [x, y] = [parts(a), parts(b)]
|
|
36
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ranges are only of the form ">=1.2.3"; no range means any version.
|
|
41
|
+
export function satisfies(range, version) {
|
|
42
|
+
if (!range) return true
|
|
43
|
+
const match = RANGE_RE.exec(range)
|
|
44
|
+
return Boolean(match) && compareVersions(version, match[1]) >= 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkCss(text, where) {
|
|
48
|
+
if (/@import/i.test(text)) fail(`${where}: @import is not allowed; list font stylesheets under fonts in extension.toml`)
|
|
49
|
+
for (const match of text.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi)) {
|
|
50
|
+
if (!match[2].trim().startsWith('data:')) fail(`${where}: url(${match[2]}) is not allowed; only data: URLs`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Checks one package's files: what it may hold, its manifest, its fonts and
|
|
55
|
+
// stylesheet. `published` also requires version, author and license, as the
|
|
56
|
+
// theme repository does. Returns the validated record.
|
|
57
|
+
export function checkPackage({ kind, id, files }, { published = false } = {}) {
|
|
58
|
+
if (!PACKAGE_KINDS.includes(kind)) fail(`${kind} is not a kind of package; only themes and palettes are`)
|
|
59
|
+
if (typeof id !== 'string' || !ID_RE.test(id)) fail(`"${id}" is not an id: lowercase letters, digits and hyphens, starting with a letter`)
|
|
60
|
+
if (!files || typeof files !== 'object') fail(`The ${kind} ${id} has no files`)
|
|
61
|
+
for (const [name, text] of Object.entries(files)) {
|
|
62
|
+
if (!FILES[kind].includes(name)) fail(`The ${kind} ${id}: "${name}" is not allowed; a ${kind} holds only ${FILES[kind].join(' and ')}`)
|
|
63
|
+
if (typeof text !== 'string') fail(`The ${kind} ${id}: ${name} is not text`)
|
|
64
|
+
}
|
|
65
|
+
if (!files['extension.toml']) fail(`The ${kind} ${id}: extension.toml is missing`)
|
|
66
|
+
const dir = `/package/${id}`
|
|
67
|
+
const file = `${dir}/extension.toml`
|
|
68
|
+
let record
|
|
69
|
+
try {
|
|
70
|
+
record = validateManifest(parseManifestText(files['extension.toml'], file), { file, dir, folderName: id, fileExists: path => Object.hasOwn(files, relative(dir, path)) })
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof ManifestError) fail(`The ${kind} ${id}: ${error.message.replace(`${file}: `, '')}`)
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
if (record.kind !== kind) fail(`${id}/extension.toml says kind = "${record.kind}", but it is in the ${kind}s`)
|
|
76
|
+
if (published) for (const key of ['version', 'author', 'license']) if (!record.package?.[key]) fail(`The ${kind} ${id}: ${key} is required in the theme repository`)
|
|
77
|
+
if (kind === 'theme') {
|
|
78
|
+
for (const url of record.manifest.fonts) if (!FONT_HOSTS.some(host => url.startsWith(host))) fail(`The theme ${id} loads fonts from ${url}; allowed are ${FONT_HOSTS.join(', ')}`)
|
|
79
|
+
checkCss(files['styles.css'] ?? '', `The theme ${id}: styles.css`)
|
|
80
|
+
}
|
|
81
|
+
return record
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// What a package needs installed with it: a theme its default palette, a
|
|
85
|
+
// palette of one theme that theme.
|
|
86
|
+
export function dependencies(kind, manifest) {
|
|
87
|
+
if (kind === 'theme') return [{ kind: 'palette', id: manifest.palette }]
|
|
88
|
+
return manifest.theme ? [{ kind: 'theme', id: manifest.theme }] : []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A package folder's files and record; `kind` comes from the manifest.
|
|
92
|
+
export function readPackageDir(dir, { published = false } = {}) {
|
|
93
|
+
const id = basename(dir)
|
|
94
|
+
const files = {}
|
|
95
|
+
for (const name of readdirSync(dir).sort()) {
|
|
96
|
+
if (name.startsWith('.') || name === 'README.md') continue
|
|
97
|
+
if (!statSync(resolve(dir, name)).isFile()) fail(`${resolve(dir, name)}: a package holds only files`)
|
|
98
|
+
files[name] = readFileSync(resolve(dir, name), 'utf8')
|
|
99
|
+
}
|
|
100
|
+
const kind = /^kind\s*=\s*"([a-z]+)"/m.exec(files['extension.toml'] ?? '')?.[1]
|
|
101
|
+
const record = checkPackage({ kind, id, files }, { published })
|
|
102
|
+
return { kind, id, files, record }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The published form of a package folder: one JSON text and its checksum.
|
|
106
|
+
export function bundlePackage(dir, options) {
|
|
107
|
+
const { kind, id, files, record } = readPackageDir(dir, options)
|
|
108
|
+
const bundle = { schema: 2, kind, id, version: record.package?.version ?? '0.0.0', ...(record.package?.mdeck ? { mdeck: record.package.mdeck } : {}), files }
|
|
109
|
+
const text = JSON.stringify(bundle, null, 1) + '\n'
|
|
110
|
+
return { bundle, text, sha256: sha256(text), record }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Installed packages ───────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
const readJson = file => { try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return null } }
|
|
116
|
+
const packageDir = (root, kind, id) => resolve(root, `${kind}s`, id)
|
|
117
|
+
|
|
118
|
+
// Files changed since the package was installed, by name.
|
|
119
|
+
function changedFiles(dir, marker) {
|
|
120
|
+
return Object.entries(marker.files ?? {}).filter(([name, hash]) => {
|
|
121
|
+
const file = resolve(dir, name)
|
|
122
|
+
return !existsSync(file) || sha256(readFileSync(file, 'utf8')) !== hash
|
|
123
|
+
}).map(([name]) => name)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const refuseChanged = (dir, marker) => {
|
|
127
|
+
const changed = changedFiles(dir, marker)
|
|
128
|
+
if (changed.length) fail(`${dir} was changed since it was installed (${changed.join(', ')}); add --force to replace or remove it anyway`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The packages installed in an extensions folder (in its themes/ and
|
|
132
|
+
// palettes/), by "kind:id".
|
|
133
|
+
export function installedPackages(root) {
|
|
134
|
+
const found = {}
|
|
135
|
+
for (const kind of PACKAGE_KINDS) {
|
|
136
|
+
const dir = resolve(root, `${kind}s`)
|
|
137
|
+
if (!existsSync(dir)) continue
|
|
138
|
+
for (const id of readdirSync(dir).sort()) {
|
|
139
|
+
const marker = readJson(resolve(dir, id, MARKER))
|
|
140
|
+
if (marker?.kind === kind) found[keyOf(kind, id)] = { kind, id, version: marker.version ?? null, url: marker.url ?? null, dir: resolve(dir, id), marker }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return found
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Writes a checked package into an extensions folder (a deck's, or the
|
|
147
|
+
// user's), as <kind>s/<id>. A folder of the deck's own is never touched; one
|
|
148
|
+
// installed before is replaced unless it was changed by hand.
|
|
149
|
+
export function installPackage(bundle, { root, url = null, digest = null, force = false } = {}) {
|
|
150
|
+
const { kind, id } = bundle
|
|
151
|
+
const record = checkPackage(bundle)
|
|
152
|
+
const dir = packageDir(root, kind, id)
|
|
153
|
+
const before = installedPackages(root)[keyOf(kind, id)]
|
|
154
|
+
if (existsSync(dir) && !readJson(resolve(dir, MARKER))) fail(`${dir} already exists and was not installed by mdeck; rename or remove it first`)
|
|
155
|
+
if (before && !force) refuseChanged(before.dir, before.marker)
|
|
156
|
+
// Files are overwritten in place, not deleted and written again, so a
|
|
157
|
+
// running editor sees a change instead of a file that disappears.
|
|
158
|
+
mkdirSync(dir, { recursive: true })
|
|
159
|
+
const hashes = {}
|
|
160
|
+
for (const [name, content] of Object.entries(bundle.files)) {
|
|
161
|
+
writeFileSync(resolve(dir, name), content, 'utf8')
|
|
162
|
+
hashes[name] = sha256(content)
|
|
163
|
+
}
|
|
164
|
+
for (const name of readdirSync(dir)) if (name !== MARKER && !Object.hasOwn(hashes, name)) rmSync(resolve(dir, name), { recursive: true, force: true })
|
|
165
|
+
writeFileSync(resolve(dir, MARKER), JSON.stringify({ kind, id, version: bundle.version, url, sha256: digest, files: hashes }, null, 2) + '\n')
|
|
166
|
+
return { record, dir, replaced: before?.version ?? null }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function removePackage(kind, id, { root, force = false } = {}) {
|
|
170
|
+
const installed = installedPackages(root)[keyOf(kind, id)]
|
|
171
|
+
if (!installed) fail(`The ${kind} ${id} is not installed in ${root}`)
|
|
172
|
+
if (!force) refuseChanged(installed.dir, installed.marker)
|
|
173
|
+
rmSync(installed.dir, { recursive: true, force: true })
|
|
174
|
+
return installed
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── The catalogue: built-in packages and the repository ──────────────────
|
|
178
|
+
|
|
179
|
+
async function get(url, as) {
|
|
180
|
+
let response
|
|
181
|
+
try { response = await fetch(url) } catch (error) { fail(`Could not reach ${url}: ${error.cause?.message ?? error.message}`) }
|
|
182
|
+
if (!response.ok) fail(`${url} answered ${response.status}`)
|
|
183
|
+
return as === 'json' ? response.json() : response.text()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function fetchCatalogue(url = catalogueUrl()) {
|
|
187
|
+
const data = await get(url, 'json')
|
|
188
|
+
if (data?.schema !== 2 || !Array.isArray(data.themes) || !Array.isArray(data.palettes)) fail(`${url} is not a theme repository catalogue`)
|
|
189
|
+
return data
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// One entry of the catalogue, from a package's record.
|
|
193
|
+
function entryOf(kind, record) {
|
|
194
|
+
return {
|
|
195
|
+
kind, id: record.id, title: record.title, description: record.description ?? '',
|
|
196
|
+
version: record.package?.version ?? '0.0.0', ...(record.package?.author ? { author: record.package.author } : {}), ...(record.package?.license ? { license: record.package.license } : {}),
|
|
197
|
+
...(record.package?.homepage ? { homepage: record.package.homepage } : {}), ...(record.package?.mdeck ? { mdeck: record.package.mdeck } : {}),
|
|
198
|
+
...(kind === 'theme' ? { palette: record.manifest.palette, ...(record.manifest.palettes?.length ? { palettes: record.manifest.palettes } : {}), appearance: record.manifest.appearance ?? 'light' } : {}),
|
|
199
|
+
...(kind === 'palette' && record.manifest.theme ? { theme: record.manifest.theme } : {}),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The themes and palettes that come with mdeck, as catalogue entries.
|
|
204
|
+
export function builtInEntries(root = builtinExtensionsRoot) {
|
|
205
|
+
const entries = []
|
|
206
|
+
for (const kind of PACKAGE_KINDS) {
|
|
207
|
+
const dir = resolve(root, `${kind}s`)
|
|
208
|
+
if (!existsSync(dir)) continue
|
|
209
|
+
for (const id of readdirSync(dir).sort()) {
|
|
210
|
+
if (id.startsWith('.')) continue
|
|
211
|
+
const { record } = readPackageDir(resolve(dir, id))
|
|
212
|
+
entries.push({ ...entryOf(kind, record), builtIn: true, dir: resolve(dir, id) })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return entries
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Every theme and palette mdeck can install: the built-in ones, and the
|
|
219
|
+
// repository's when it answers. Of two versions the newer is offered; the
|
|
220
|
+
// built-in one on a tie, since it needs no download. `offline` says why the
|
|
221
|
+
// repository is missing.
|
|
222
|
+
export async function loadCatalogue({ url = catalogueUrl(), online = true } = {}) {
|
|
223
|
+
const entries = new Map(builtInEntries().map(entry => [keyOf(entry.kind, entry.id), entry]))
|
|
224
|
+
let offline = null
|
|
225
|
+
let remote = { themes: [], palettes: [] }
|
|
226
|
+
if (online) { try { remote = await fetchCatalogue(url) } catch (error) { offline = error.message } }
|
|
227
|
+
for (const entry of [...remote.themes.map(e => ({ ...e, kind: 'theme' })), ...remote.palettes.map(e => ({ ...e, kind: 'palette' }))]) {
|
|
228
|
+
const key = keyOf(entry.kind, entry.id)
|
|
229
|
+
const have = entries.get(key)
|
|
230
|
+
if (entry.builtIn) { if (have) have.preview = entry.preview; continue }
|
|
231
|
+
if (!have || compareVersions(entry.version, have.version) > 0) entries.set(key, { ...entry, builtIn: false, builtInVersion: have?.version })
|
|
232
|
+
}
|
|
233
|
+
const sorted = [...entries.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id, 'en'))
|
|
234
|
+
return { url, offline, entries: sorted, get: (kind, id) => entries.get(keyOf(kind, id)) }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function findEntry(catalogue, kind, id) {
|
|
238
|
+
return catalogue.get(kind, id) ?? fail(`There is no ${kind} "${id}"${catalogue.offline ? ` among those that come with mdeck, and the theme repository could not be reached (${catalogue.offline})` : `; mdeck ${kind}s search lists them`}`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A package's files: read from mdeck's folder, or downloaded and checked
|
|
242
|
+
// against the repository's checksum.
|
|
243
|
+
export async function obtainPackage(catalogue, entry) {
|
|
244
|
+
if (entry.builtIn) { const { bundle, sha256: digest } = bundlePackage(entry.dir); return { bundle, url: null, digest } }
|
|
245
|
+
const url = new URL(entry.url, catalogue.url).href
|
|
246
|
+
const text = await get(url, 'text')
|
|
247
|
+
if (sha256(text) !== entry.sha256) fail(`${url} does not match its checksum in the catalogue; not installed`)
|
|
248
|
+
let bundle
|
|
249
|
+
try { bundle = JSON.parse(text) } catch { fail(`${url} is not a package`) }
|
|
250
|
+
if (bundle.kind !== entry.kind || bundle.id !== entry.id || bundle.version !== entry.version) fail(`${url} holds the ${bundle.kind} ${bundle.id} ${bundle.version}, not the ${entry.kind} ${entry.id} ${entry.version}`)
|
|
251
|
+
return { bundle, url, digest: entry.sha256 }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// A package and what it needs, in the order to install them: what is needed
|
|
255
|
+
// first, the package itself last. A theme and its own palette need each
|
|
256
|
+
// other; each is installed once.
|
|
257
|
+
export function withDependencies(catalogue, kind, id, seen = new Set()) {
|
|
258
|
+
const key = keyOf(kind, id)
|
|
259
|
+
if (seen.has(key)) return []
|
|
260
|
+
seen.add(key)
|
|
261
|
+
const entry = findEntry(catalogue, kind, id)
|
|
262
|
+
const needs = kind === 'theme' ? [{ kind: 'palette', id: entry.palette }] : entry.theme ? [{ kind: 'theme', id: entry.theme }] : []
|
|
263
|
+
const order = []
|
|
264
|
+
for (const need of needs) for (const item of withDependencies(catalogue, need.kind, need.id, seen)) if (!order.includes(item)) order.push(item)
|
|
265
|
+
return [...order, entry]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── The repository itself ────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
// The theme repository as served: catalogue.json, and <kind>s/<id>-<version>.json
|
|
271
|
+
// for each package, from a folder with themes/ and palettes/. mdeck's
|
|
272
|
+
// built-in packages are listed too (marked builtIn), so the repository's
|
|
273
|
+
// themes may use built-in palettes. Every package is checked, no id appears
|
|
274
|
+
// twice in a kind, and what each needs is in the catalogue. Previews are added
|
|
275
|
+
// by the caller.
|
|
276
|
+
export function buildRepository(repoDir, outDir) {
|
|
277
|
+
const builtIn = builtInEntries()
|
|
278
|
+
const records = {}
|
|
279
|
+
const add = (kind, id, value) => {
|
|
280
|
+
if (records[keyOf(kind, id)]) fail(`There are two ${kind}s "${id}"${value.builtIn || records[keyOf(kind, id)].builtIn ? '; one comes with mdeck, change it there' : ''}`)
|
|
281
|
+
records[keyOf(kind, id)] = value
|
|
282
|
+
}
|
|
283
|
+
for (const entry of builtIn) add(entry.kind, entry.id, { entry, builtIn: true, ...bundlePackage(entry.dir) })
|
|
284
|
+
for (const kind of PACKAGE_KINDS) {
|
|
285
|
+
const dir = resolve(repoDir, `${kind}s`)
|
|
286
|
+
if (!existsSync(dir)) continue
|
|
287
|
+
for (const id of readdirSync(dir).sort()) {
|
|
288
|
+
if (id.startsWith('.') || !statSync(resolve(dir, id)).isDirectory()) continue
|
|
289
|
+
const read = bundlePackage(resolve(dir, id), { published: true })
|
|
290
|
+
if (read.bundle.kind !== kind) fail(`${kind}s/${id} holds a ${read.bundle.kind}`)
|
|
291
|
+
add(kind, id, { entry: entryOf(kind, read.record), builtIn: false, dir: resolve(dir, id), ...read })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// What each needs must be there, and a theme may offer only palettes that
|
|
295
|
+
// are open to it.
|
|
296
|
+
for (const { record, bundle } of Object.values(records)) {
|
|
297
|
+
for (const need of dependencies(bundle.kind, record.manifest)) {
|
|
298
|
+
if (!records[keyOf(need.kind, need.id)]) fail(`The ${bundle.kind} ${bundle.id} needs the ${need.kind} "${need.id}", which is neither in the repository nor built into mdeck`)
|
|
299
|
+
}
|
|
300
|
+
if (bundle.kind === 'theme') for (const id of [record.manifest.palette, ...(record.manifest.palettes ?? [])]) {
|
|
301
|
+
const palette = records[keyOf('palette', id)]
|
|
302
|
+
if (!palette) fail(`The theme ${bundle.id} offers the palette "${id}", which is neither in the repository nor built into mdeck`)
|
|
303
|
+
const owner = palette.record.manifest.theme
|
|
304
|
+
if (owner && owner !== bundle.id) fail(`The theme ${bundle.id} offers the palette ${id}, which belongs to the theme ${owner}`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const catalogue = { schema: 2, themes: [], palettes: [] }
|
|
308
|
+
for (const key of Object.keys(records).sort()) {
|
|
309
|
+
const { entry, text, sha256: digest, bundle, builtIn: isBuiltIn } = records[key]
|
|
310
|
+
const file = `${bundle.kind}s/${bundle.id}-${bundle.version}.json`
|
|
311
|
+
mkdirSync(resolve(outDir, `${bundle.kind}s`), { recursive: true })
|
|
312
|
+
writeFileSync(resolve(outDir, file), text)
|
|
313
|
+
const { dir, ...listed } = entry
|
|
314
|
+
catalogue[`${bundle.kind}s`].push({ ...listed, ...(isBuiltIn ? { builtIn: true } : {}), url: file, sha256: digest })
|
|
315
|
+
}
|
|
316
|
+
writeFileSync(resolve(outDir, 'catalogue.json'), JSON.stringify(catalogue, null, 2) + '\n')
|
|
317
|
+
return { catalogue, dirs: Object.fromEntries(Object.entries(records).map(([key, value]) => [key, value.dir ?? value.entry.dir])) }
|
|
318
|
+
}
|
package/src/paths.js
CHANGED
|
@@ -7,5 +7,3 @@ export const runtimeRoot = resolve(frameworkRoot, 'src/runtime')
|
|
|
7
7
|
export const assetsRoot = resolve(frameworkRoot, 'assets')
|
|
8
8
|
export const docsRoot = resolve(frameworkRoot, 'docs/site')
|
|
9
9
|
export const builtinExtensionsRoot = resolve(assetsRoot, 'extensions')
|
|
10
|
-
// The packs of themes and palettes that come with mdeck (the starter set).
|
|
11
|
-
export const bundledPacksRoot = resolve(assetsRoot, 'packs')
|
|
@@ -143,5 +143,12 @@ marked.use({
|
|
|
143
143
|
.replace(/>/g, '>')
|
|
144
144
|
return `<codeblock lang="${lang}"${attrs ? ' ' + attrs : ''}>${encoded}</codeblock>`
|
|
145
145
|
},
|
|
146
|
+
// A paragraph of nothing but pictures (one, or several side by side) is
|
|
147
|
+
// marked, so themes can fit it into the slide or set it as a row; a
|
|
148
|
+
// sentence with an icon in it stays a sentence.
|
|
149
|
+
paragraph({ tokens }) {
|
|
150
|
+
const pictures = tokens.some(t => t.type === 'image') && tokens.every(t => t.type === 'image' || t.type === 'br' || (t.type === 'text' && !t.raw.trim()))
|
|
151
|
+
return `<p${pictures ? ' class="pictures"' : ''}>${this.parser.parseInline(tokens)}</p>\n`
|
|
152
|
+
},
|
|
146
153
|
},
|
|
147
154
|
})
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
schema = 1
|
|
2
|
-
id = "academic"
|
|
3
|
-
title = "Academic"
|
|
4
|
-
description = "A lecture: Source Sans and Source Serif, compact slides with the heading in a fixed place, theorem and definition blocks, booktabs tables and a footnote rule."
|
|
5
|
-
version = "1.0.0"
|
|
6
|
-
author = "Tilman Schieber"
|
|
7
|
-
license = "MIT"
|
|
8
|
-
mdeck = ">=3.0.0"
|